Bot Framework Webchat links onclick in React - reactjs

Because of other services that are integrated to the bot, we have to use HTML anchors instead of the markdown.
When there is a link, we dynamically switch it to the following code:
var newStr = sub.replace('*|', '<a class="chatbot-link" title="help" href="#" onClick={this.click}>').replace('*', '</a>');
The goal is when the link is clicked, I sends the text content back to the bot as a response. The issue is that I cannot figure out how to catch the onclick event. Only thing it does when click is that it generates a seperate index.js in the page sources.
The base of the code I am using is a github webchat sample: Link
The code added that is important for the example (in MinimizableWebChat.js):
const MinimizableWebChat = (activityID, children) => {
const store = useMemo(
() =>
createStore({}, ({ dispatch }) => next => action => {
console.log(action.type);
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'initConversation',
value: {
language: window.navigator.language
}
}
});
} else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
if (action.payload.activity.from.role === 'bot') {
setNewMessage(true);
// action.payload.activity.text = action.payload.activity.text;
// action.payload.activity.text = ();
action.payload.activity.text = ParseLinks(action.payload.activity.text);
var atts = action.payload.activity.attachments;
if (atts){
atts.forEach(att => {
att.content.body.forEach(obj => {
obj.text = EmphasizeContent(obj.text);
})
});
}
}
}
return next(action);
}),
[]
);
const items = ["Hello World!", "My name is TJ.", "I am from Denver."];
var text = "asd";
// const click = () => {
// store.dispatch({
// type: 'WEB_CHAT/SET_SEND_BOX',
// payload: {
// text
// }
// });
// }
const ParseLinks = (text) => {
if(!text) return text;
text = String(text);
var a=[], i=-1;
// Search for *|, if found, find next * and substirng. Add html to each substring
while((i=text.indexOf("*|",i+1)) >= 0) {
var iA = text.indexOf("*", i+2);
var sub = text.substring(i,iA+1);
// var newStr = sub.replace('*|', '<a class="chatbot-link" title="help" href="#" onClick={this.click}>').replace('*', '</a>');
var newStr = sub.replace('*|', '<a class="chatbot-link" title="help" href="#" onClick={this.click}>').replace('*', '</a>');
text = text.replace(sub, newStr);
}
return text;
}
I tried catching it in the middleware store and directly in bot MinimizableWebChat.js and WebChat.js. I need somekind of hook to get the event from ReactWebChat in some way to interact with it in the middleware.
Fyi, pretty new to React, thanks for the help.

Related

Lifecycle of useState hook in React.js

I have the following synchronism problem. Given that I know that the React useState hook is asynchronous, I run into the following: I'm downloading some images from Amazon S3, I manage to save it correctly in my hook: defaultSelfiePicture and depending on the weight of the image (or so I think) sometimes I get the images loaded correctly and sometimes not. I have tried to force state changes after I finish saving the object in my hook but it never renders the image, only if I change component and come back is when it is shown in the cases that it takes longer to load.
const [defaultSelfiePictures, setDefaultSelfiePictures] = useState([])
useEffect(() => {
if (savedUser.docs !== undefined) {
loadAllPictures()
}
}, [savedUser.docs.length])
const loadAllPictures = () => {
let p1 = loadUrlDefaultFrontPictures()
let p2 = loadUrlDefaultBackPictures()
let p3 = loadUrlDefaultSelfiePictures()
Promise.all([p1, p2, p3]).then(result => {
console.log('end all promises')
setTimestamp(Date.now())
})
}
const loadUrlDefaultSelfiePictures = async () => {
if (savedUser.docs.length > 0) {
let readedPictures = []
for (let i = 0; i < savedUser.docs.length; i++) {
if (
savedUser.docs[i].type === 'SELFIE'
//&& savedUser.docs[i].side === 'FRONT'
) {
if (
savedUser.docs[i].s3Href !== null &&
savedUser.docs[i].s3Href !== undefined
) {
const paramsKeyArray =
savedUser.docs[i].s3Href.split('')
let paramsKey = paramsKeyArray.pop()
let params = {
Bucket: process.env.REACT_APP_S3_BUCKET,
Key: paramsKey
}
await s3.getSignedUrl('getObject', params, function (err, url) {
readedPictures.push({
idKycDoc: savedUser.docs[i].idKycDoc,
name: 'selfie.jpeg',
type: savedUser.docs[i].type,
url: url
})
})
} else {
let urlPicture = savedUser.docs[i].localHref
let response = await axios.get(`${URL_IMG}${urlPicture}`, {
responseType: 'blob'
})
function readAsDataURL(data) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(data)
reader.onloadend = () => {
resolve(reader.result)
}
})
}
const base64Data = await readAsDataURL(response.data)
readedPictures.push({
idKycDoc: savedUser.docs[i].idKycDoc,
name: 'selfie.jpeg',
type: savedUser.docs[i].type,
url: `data:image/jpeg;base64,${base64Data.slice(21)}`
})
}
}
}
setDefaultSelfiePictures(readedPictures)
}
}
And I obtain this :
I can see that the hook has content, but that content is not updated until the next rendering of the component, also if I try to make any changes when I detect that the .length has changed it tells me that it is 0...
And right after the next render I get this:

Twilio + React: How to change audio or video input device during a call?

I've been looking at the documentation but I can't figure it out. When a user selects a different device, the other users can't hear that person anymore. This must mean that something is going right with the unpublishing of tracks, right? I'm not sure.
This is my code for a user to change devices:
const setDevice = (device) => {
if(!room) return
let deviceId = device.deviceId
const localParticipant = room.localParticipant
if(device.kind === 'audioinput'){
setSelectedAudioDevice(device.label)
Video.createLocalAudioTrack({
deviceId: {exact: deviceId}
}).then((localAudioTrack) => {
const tracks = localParticipant.audioTracks
tracks.forEach((track) => {
localParticipant.unpublishTrack(track.track)
})
localParticipant.publishTrack(localAudioTrack)
})
} else if(device.kind === 'videoinput'){
setSelectedVideoDevice(device.label)
Video.createLocalVideoTrack({
deviceId: {exact: deviceId}
}).then((localVideoTrack) => {
const tracks = localParticipant.videoTracks
tracks.forEach((track) => {
localParticipant.unpublishTrack(track.track)
})
localParticipant.publishTrack(localVideoTrack)
})
}
}
Each participant has its own component in which they subscribe to tracks. However, this code was from one of the Twilio examples, so I'm not entirely sure how it works.
const trackpubsToTracks = (trackMap) =>
Array.from(trackMap.values())
.map((publication) => publication.track)
.filter((track) => track !== null);
useEffect(() => {
setVideoTracks(trackpubsToTracks(participant.videoTracks));
setAudioTracks(trackpubsToTracks(participant.audioTracks));
const trackSubscribed = (track) => {
if (track.kind === "video") {
setVideoTracks((videoTracks) => [...videoTracks, track]);
} else if (track.kind === "audio") {
setAudioTracks((audioTracks) => [...audioTracks, track]);
}
};
const trackUnsubscribed = (track) => {
if (track.kind === "video") {
setVideoTracks((videoTracks) => videoTracks.filter((v) => v !== track));
} else if (track.kind === "audio") {
setAudioTracks((audioTracks) => audioTracks.filter((a) => a !== track));
}
};
participant.on("trackSubscribed", trackSubscribed);
participant.on("trackUnsubscribed", trackUnsubscribed);
return () => {
setVideoTracks([]);
setAudioTracks([]);
participant.removeAllListeners();
};
}, [participant]);
useEffect(() => {
const videoTrack = videoTracks[0];
if (videoTrack) {
videoTrack.attach(videoRef.current);
return () => {
videoTrack.detach();
};
}
}, [videoTracks]);
useEffect(() => {
const audioTrack = audioTracks[0];
if (audioTrack) {
audioTrack.attach(audioRef.current);
return () => {
audioTrack.detach();
};
}
}, [audioTracks]);
If anyone knows how I can handle device switching mid-call, I'd greatly appreciate it.
Twilio developer evangelist here.
I have found that the best order of operations here is:
Unpublish the local participant’s existing track from the room, this will trigger the trackRemoved event on the room for any other participants
Detach the existing track from the page
Stop the track completely
Request the new track with createLocal(Video|Audio)Track
Attach the new track to the page
Publish the new track to the room, triggering the trackAdded event on the room for the other participants
This is especially true for iOS devices which do not let you access more than one camera at a time.
Here is some code I've used before, though not in a React application:
function stopTracks(tracks) {
tracks.forEach(function(track) {
if (track) { track.stop(); }
})
}
function updateVideoDevice(event) {
const select = event.target;
const localParticipant = activeRoom.localParticipant;
if (select.value !== '') {
const tracks = Array.from(localParticipant.videoTracks.values()).map(
function(trackPublication) {
return trackPublication.track;
}
);
localParticipant.unpublishTracks(tracks);
detachTracks(tracks);
stopTracks(tracks);
Video.createLocalVideoTrack({
deviceId: { exact: select.value }
}).then(function(localVideoTrack) {
localParticipant.publishTrack(localVideoTrack);
log(localParticipant.identity + ' added track: ' + localVideoTrack.kind);
const previewContainer = document.getElementById('local-media');
attachTracks([localVideoTrack], previewContainer);
});
}
}
You can see the entire application in this repo on GitHub and I wrote about it here.
I think the React example you're referring to was one of mine too. I actually had a go at adding camera changes to that repo on a branch. It was apparently a year ago, but you can see the updates here. Hopefully that can point you in the right direction too.

How to wait for the end of an action with useDispatch to move on?

I currently have a real problem. I want to redirect my user to the right conversation or publication when they press a notification.
All the code works, but I have the same problem all the time: the redirection happens before the action is completed, which results in a nice error telling me that the item is "null".
If I redirect to a publication with a new comment, it shows the publication, but the comments load one or two seconds after being redirected.
How is it possible to wait for the end of an action before redirecting?
Thanks a lot
My action (with Redux Thunk)
export const fetchPublications = token => {
return async dispatch => {
await axios
.get(`/articles?token=${token}`)
.then(response => {
const articles = response.data.articles;
const groups = response.data.groups;
const groupPosts = response.data.groupPosts;
const comments = response.data.comments;
const loadedArticles = [];
const loadedGroups = [];
const loadedGroupPosts = [];
const loadedComments = [];
for (const key in articles) {
loadedArticles.push(
new Article(
articles[key].id,
articles[key].title,
articles[key].content,
articles[key].description,
articles[key].cover,
articles[key].dateCreation,
articles[key].creatorPhoto,
articles[key].creatorFirstName,
articles[key].creatorLastName,
articles[key].creatorId,
articles[key].slug,
articles[key].isOnline,
articles[key].isForPro,
'article',
),
);
}
for (const key in groups) {
loadedGroups.push(
new Group(
groups[key].id,
groups[key].name,
groups[key].icon,
groups[key].cover,
groups[key].description,
groups[key].isPublic,
groups[key].isOnInvitation,
groups[key].dateCreation,
groups[key].slug,
groups[key].safeMode,
groups[key].isOnTeam,
groups[key].role,
groups[key].isWaitingValidation,
'group',
),
);
}
for (const key in groupPosts) {
loadedGroupPosts.push(
new GroupPost(
groupPosts[key].id,
groupPosts[key].content,
groupPosts[key].dateCreation,
groupPosts[key].lastModification,
groupPosts[key].creatorPhoto,
groupPosts[key].creatorFirstName,
groupPosts[key].creatorLastName,
groupPosts[key].creatorId,
groupPosts[key].onGroupId,
groupPosts[key].groupName,
groupPosts[key].groupIcon,
'groupPost',
groupPosts[key].liked,
groupPosts[key].likesCounter,
groupPosts[key].commentsCounter,
),
);
}
for (const key in comments) {
loadedComments.push(
new Comment(
comments[key].id,
comments[key].content,
comments[key].dateCreation,
comments[key].lastModification,
comments[key].creatorPhoto,
comments[key].creatorFirstName,
comments[key].creatorLastName,
comments[key].creatorId,
comments[key].onPostId,
),
);
}
dispatch({
type: FETCH_PUBLICATIONS,
articles: loadedArticles,
groups: loadedGroups,
groupPosts: loadedGroupPosts,
comments: loadedComments,
});
})
.catch(error => {
console.log(error);
throw new Error('Une erreur est survenue.');
});
};
};
My notification handler
const handleNotificationResponse = async response => {
if (response.actionIdentifier === 'expo.modules.notifications.actions.DEFAULT') {
try {
if (response.notification.request.content.data.discussionId) {
if (isAuth) {
const discussionId =
response.notification.request.content.data.discussionId;
dispatch(messengerActions.fetchMessenger(userToken));
const item = messages.filter(
message => message.id == discussionId,
);
navigationRef.current?.navigate('MessengerApp', {
screen: 'Discussion',
params: { item: item[0] },
});
}
} else if (response.notification.request.content.data.groupPostId) {
if (isAuth) {
const groupPostId =
response.notification.request.content.data.groupPostId;
dispatch(newsfeedActions.fetchPublications(userToken));
const item = groupPosts.filter(
groupPost => groupPost.id == groupPostId,
);
navigationRef.current?.navigate('App', {
screen: 'Comments',
params: {
item: item[0],
},
});
}
}
} catch (err) {}
} else {
}
};

update react state using previous data

This is a follow up question to this question:
Why calling react setState method doesn't mutate the state immediately?
I got a React component with a form which can be used to add items or edit a current item. The form is being saved as a state of the component along with all its values.
When submitting the form I'm doing this:
const onSubmitForm = () =>
{
if(editedItem) //the item to edit
{
EditSelectedItem();
setEditedItem(undefined);
}
else
{
//handle new item addition
}
clearFormValues();
setEditedItem(undefined);
}
And the edit method:
const EditSelectedItem = () =>
{
setItemsList(prevItemsList =>
{
return prevItemsList.map(item=>
{
if(item.id !== editedItem.id)
{
return item;
}
item.name = formSettings["name"].value ?? "";
item.description = formSettings["description"].value ?? "";
item.modified = getNowDate();
return item;
});
})
}
The problem is that because the setItemsList is not being called synchronously, the clearFormValues(); in the submit form method is being called before, and I lose the form's old values (in formSettings)..
How can I keep the old values of formSettings when the setItemsList is called?
The solution is easy here, you can store the formValues in an object before using it an setItemsList
const EditSelectedItem = () =>
{
const values = {
name: formSettings["name"].value ?? "";
description: formSettings["description"].value ?? "";
modified: getNowDate();
}
setItemsList(prevItemsList =>
{
return prevItemsList.map(item=>
{
if(item.id !== editedItem.id)
{
return item;
}
return {...item, ...values};
});
})
}

Firebase upload multiple files and get status

I have a React form where the user can upload multiple files. These are stored in fileList
async function uploadFiles(id) {
try {
const meta = await storageUploadFile(fileList, id);
console.log(meta);
} catch (e) {
console.log(e);
}
}
This calls my helper function that uploads the files to Firebase
export const storageUploadFile = function(files, id) {
const user = firebase.auth().currentUser.uid;
return Promise.all(
files.map((file) => {
return storage.child(`designs/${user}/${id}/${file.name}`).put(file)
})
)
};
What I'd like is on calling uploadFiles, get the total filesize of all items, and then show the overall progress.
At the moment, my code is only returning the file status in an array on completion
[
{bytesTransferred: 485561, totalBytes: 485561, state: "success"},
{bytesTransferred: 656289, totalBytes: 656289, state: "success"}
]
This is the way i do it:
import Deferred from 'es6-deferred';
export const storageUploadFile = function(files, id) {
const user = firebase.auth().currentUser.uid;
// To track the remaining files
let itemsCount = files.length;
// To store our files refs
const thumbRef = [];
// Our main tasks
const tumbUploadTask = [];
// This will store our primses
const thumbCompleter = [];
for (let i = 0; i < files.length; i += 1) {
thumbRef[i] = storage.ref(`designs/${user}/${id}/${file.name}`);
tumbUploadTask[i] = thumbRef[i].put(files[i]);
thumbCompleter[i] = new Deferred();
tumbUploadTask[i].on('state_changed',
(snap) => {
// Here you can check the progress
console.log(i, (snap.bytesTransferred / snap.totalBytes) * 100);
},
(error) => {
thumbCompleter[i].reject(error);
}, () => {
const url = tumbUploadTask[i].snapshot.metadata.downloadURLs[0];
itemsCount -= 1;
console.log(`Items left: ${itemsCount}`)
thumbCompleter[i].resolve(url);
});
}
return Promise.all(thumbCompleter).then((urls) => {
// Here we can see our files urls
console.log(urls);
});
};
Hope it helps.

Resources