use shared state hook to get the chat history for the newly joined users
This commit is contained in:
parent
c5a077e95e
commit
0ede38c423
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue