use shared state hook to get the chat history for the newly joined users

This commit is contained in:
harshithpabbati 2021-11-11 18:23:45 +05:30
parent c5a077e95e
commit 0ede38c423
5 changed files with 169 additions and 25 deletions

View File

@ -22,6 +22,7 @@ const initialParticipantsState = {
camMutedByHost: false,
hasNameSet: false,
id: 'local',
user_id: '',
isActiveSpeaker: false,
isCamMuted: false,
isLoading: true,
@ -68,6 +69,7 @@ function getNewParticipant(participant) {
camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name,
id,
user_id: participant.user_id,
isActiveSpeaker: false,
isCamMuted:
video?.state === DEVICE_STATE_OFF ||
@ -102,6 +104,7 @@ function getUpdatedParticipant(participant, participants) {
camMutedByHost: video?.off?.byRemoteRequest,
hasNameSet: !!participant.user_name,
id,
user_id: participant.user_id,
isCamMuted:
video?.state === DEVICE_STATE_OFF ||
video?.state === DEVICE_STATE_BLOCKED,

View File

@ -0,0 +1,123 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useCallState } from '../contexts/CallProvider';
// @params initialValues - initial values of the shared state.
// @params broadcast - share the state with the other participants whenever the state changes.
export const useSharedState = ({ initialValues = {}, broadcast = true }) => {
const { callObject } = useCallState();
const stateRef = useRef(null);
const requestIntervalRef = useRef(null);
const [state, setState] = useState({
sharedState: initialValues,
setAt: undefined,
});
// handling the app-message event, to check if the state is being shared.
const handleAppMessage = useCallback(
event => {
// two types of events -
// 1. Request shared state (request-shared-state)
// 2. Set shared state (set-shared-state)
switch (event.data?.message?.type) {
// if we receive a request-shared-state message type, we check if the user has any previous state,
// if yes, we will send the shared-state to everyone in the call.
case 'request-shared-state':
if (!stateRef.current.setAt) return;
callObject.sendAppMessage(
{
message: {
type: 'set-shared-state',
value: stateRef.current,
},
},
'*',
);
break;
// if we receive a set-shared-state message type then, we check the state timestamp with the local one and
// we set the latest shared-state values into the local state.
case 'set-shared-state':
clearInterval(requestIntervalRef.current);
if (
stateRef.current.setAt &&
new Date(stateRef.current.setAt) > new Date(event.data.message.value.setAt)
)
return;
setState(event.data.message.value);
break;
}
},
[stateRef, callObject],
);
// whenever local user joins, we randomly pick a participant from the call and request him for the state.
const handleJoinedMeeting = useCallback(() => {
const randomDelay = 1000 + Math.ceil(1000 * Math.random());
requestIntervalRef.current = setInterval(() => {
const callObjectParticipants = callObject.participants();
const participants = Object.values(callObjectParticipants);
const localParticipant = callObjectParticipants.local;
if (participants.length > 1) {
const remoteParticipants = participants.filter((p) =>
!p.local && new Date(p.joined_at) < new Date(localParticipant.joined_at)
);
const randomPeer = remoteParticipants[Math.floor(Math.random() * remoteParticipants.length)];
callObject.sendAppMessage(
{
message: {
type: 'request-shared-state',
},
},
randomPeer.user_id,
);
} else clearInterval(requestIntervalRef.current);
}, randomDelay);
return () => clearInterval(requestIntervalRef.current);
}, [callObject]);
useEffect(() => {
if (!callObject) return;
callObject.on('app-message', handleAppMessage);
callObject.on('joined-meeting', handleJoinedMeeting);
return () => {
callObject.off('app-message', handleAppMessage);
callObject.off('joined-meeting', handleJoinedMeeting);
}
}, [callObject, handleAppMessage, handleJoinedMeeting]);
useEffect(() => {
stateRef.current = state;
}, [state]);
// setSharedState function takes in the state values :-
// 1. shares the state with everyone in the call.
// 2. set the state for the local user.
const setSharedState = useCallback(
values => {
setState((state) => {
const currentValues = typeof values === 'function' ? values(state.sharedState) : values;
const stateObj = { ...state, sharedState: currentValues, setAt: new Date() };
// if broadcast is true, we send the state to everyone in the call whenever the state changes.
if (broadcast) {
callObject.sendAppMessage(
{
message: {
type: 'set-shared-state',
value: stateObj,
},
},
'*',
);
}
return stateObj;
});
}, [broadcast, callObject],
);
// returns back the sharedState and the setSharedState function
return { sharedState: state.sharedState, setSharedState };
};

View File

@ -11,7 +11,7 @@
## What does this demo do?
- Use [sendAppMessage](https://docs.daily.co/reference#%EF%B8%8F-sendappmessage) to send messages
- Listen for incoming messages using the call object `app-message` event
- Using the `useSharedState` hook to get the chat history, for the new participants.
- Extend the basic call demo with a chat provider and aside
- Show a notification bubble on chat tray button when a new message is received
- Demonstrate how to play a sound whenever a message is received
@ -34,7 +34,7 @@ In this example we extend the [basic call demo](../basic-call) with the ability
We pass a custom tray object, a custom app object (wrapping the original in a new `ChatProvider`) as well as add our `ChatAside` panel. We also symlink both the `public` and `pages/api` folders from the basic call.
In a real world use case you would likely want to implement serverside logic so that participants joining a call can retrieve previously sent messages. This round trip could be done inside of the Chat context.
We use the `useSharedState` hook to retrieve the previously sent messages for the newly joined participants.
## Deploy your own on Vercel

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { Aside } from '@custom/shared/components/Aside';
import Button from '@custom/shared/components/Button';
import { TextInput } from '@custom/shared/components/Input';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
import { useChat } from '../contexts/ChatProvider';
import { useMessageSound } from '../hooks/useMessageSound';
@ -12,6 +13,7 @@ export const ChatAside = () => {
const { showAside, setShowAside } = useUIState();
const { sendMessage, chatHistory, hasNewMessages, setHasNewMessages } =
useChat();
const { localParticipant } = useParticipants();
const [newMessage, setNewMessage] = useState('');
const playMessageSound = useMessageSound();
@ -36,6 +38,8 @@ export const ChatAside = () => {
}
}, [chatHistory?.length]);
const isLocalUser = (id) => id === localParticipant.user_id;
if (!showAside || showAside !== CHAT_ASIDE) {
return null;
}
@ -45,7 +49,7 @@ export const ChatAside = () => {
<div className="messages-container" ref={chatWindowRef}>
{chatHistory.map((chatItem) => (
<div
className={chatItem.isLocal ? 'message local' : 'message'}
className={isLocalUser(chatItem.senderID) ? 'message local' : 'message'}
key={chatItem.id}
>
<span className="content">{chatItem.message}</span>
@ -91,7 +95,7 @@ export const ChatAside = () => {
}
.message.local .sender {
color: var(--primary-default);
color: var(--primary-dark);
}
.content {

View File

@ -6,6 +6,7 @@ import React, {
useState,
} from 'react';
import { useCallState } from '@custom/shared/contexts/CallProvider';
import { useSharedState } from '@custom/shared/hooks/useSharedState';
import { nanoid } from 'nanoid';
import PropTypes from 'prop-types';
@ -13,54 +14,67 @@ export const ChatContext = createContext();
export const ChatProvider = ({ children }) => {
const { callObject } = useCallState();
const [chatHistory, setChatHistory] = useState([]);
const { sharedState, setSharedState } = useSharedState({
initialValues: {
chatHistory: [],
},
broadcast: false,
});
const [hasNewMessages, setHasNewMessages] = useState(false);
const handleNewMessage = useCallback(
(e) => {
if (e?.data?.message?.type) return;
const participants = callObject.participants();
const sender = participants[e.fromId].user_name
? participants[e.fromId].user_name
: 'Guest';
setChatHistory((oldState) => [
...oldState,
{ sender, message: e.data.message, id: nanoid() },
]);
setSharedState(values => ({
...values,
chatHistory: [
...values.chatHistory,
// nanoid - we use it to generate unique ID string
{ sender, senderID: e.fromId, message: e.data.message, id: nanoid() },
]
}));
setHasNewMessages(true);
},
[callObject]
[callObject, setSharedState]
);
const sendMessage = useCallback(
(message) => {
if (!callObject) {
return false;
}
if (!callObject) return;
console.log('💬 Sending app message');
callObject.sendAppMessage({ message }, '*');
const participants = callObject.participants();
// Get the sender (local participant) name
const sender = callObject.participants().local.user_name
? callObject.participants().local.user_name
const sender = participants.local.user_name
? participants.local.user_name
: 'Guest';
const senderID = participants.local.user_id;
// Update local chat history
return setChatHistory((oldState) => [
...oldState,
{ sender, message, id: nanoid(), isLocal: true },
]);
// Update shared state chat history
return setSharedState(values => ({
...values,
chatHistory: [
...values.chatHistory,
// nanoid - we use it to generate unique ID string
{ sender, senderID, message, id: nanoid() }
]
}));
},
[callObject]
[callObject, setSharedState]
);
useEffect(() => {
if (!callObject) {
return false;
}
if (!callObject) return;
console.log(`💬 Chat provider listening for app messages`);
@ -73,7 +87,7 @@ export const ChatProvider = ({ children }) => {
<ChatContext.Provider
value={{
sendMessage,
chatHistory,
chatHistory: sharedState.chatHistory,
hasNewMessages,
setHasNewMessages,
}}