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,
|
camMutedByHost: false,
|
||||||
hasNameSet: false,
|
hasNameSet: false,
|
||||||
id: 'local',
|
id: 'local',
|
||||||
|
user_id: '',
|
||||||
isActiveSpeaker: false,
|
isActiveSpeaker: false,
|
||||||
isCamMuted: false,
|
isCamMuted: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
|
@ -68,6 +69,7 @@ function getNewParticipant(participant) {
|
||||||
camMutedByHost: video?.off?.byRemoteRequest,
|
camMutedByHost: video?.off?.byRemoteRequest,
|
||||||
hasNameSet: !!participant.user_name,
|
hasNameSet: !!participant.user_name,
|
||||||
id,
|
id,
|
||||||
|
user_id: participant.user_id,
|
||||||
isActiveSpeaker: false,
|
isActiveSpeaker: false,
|
||||||
isCamMuted:
|
isCamMuted:
|
||||||
video?.state === DEVICE_STATE_OFF ||
|
video?.state === DEVICE_STATE_OFF ||
|
||||||
|
|
@ -102,6 +104,7 @@ function getUpdatedParticipant(participant, participants) {
|
||||||
camMutedByHost: video?.off?.byRemoteRequest,
|
camMutedByHost: video?.off?.byRemoteRequest,
|
||||||
hasNameSet: !!participant.user_name,
|
hasNameSet: !!participant.user_name,
|
||||||
id,
|
id,
|
||||||
|
user_id: participant.user_id,
|
||||||
isCamMuted:
|
isCamMuted:
|
||||||
video?.state === DEVICE_STATE_OFF ||
|
video?.state === DEVICE_STATE_OFF ||
|
||||||
video?.state === DEVICE_STATE_BLOCKED,
|
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?
|
## What does this demo do?
|
||||||
|
|
||||||
- Use [sendAppMessage](https://docs.daily.co/reference#%EF%B8%8F-sendappmessage) to send messages
|
- 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
|
- 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
|
- 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
|
- 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.
|
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
|
## Deploy your own on Vercel
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Aside } from '@custom/shared/components/Aside';
|
import { Aside } from '@custom/shared/components/Aside';
|
||||||
import Button from '@custom/shared/components/Button';
|
import Button from '@custom/shared/components/Button';
|
||||||
import { TextInput } from '@custom/shared/components/Input';
|
import { TextInput } from '@custom/shared/components/Input';
|
||||||
|
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||||
import { useChat } from '../contexts/ChatProvider';
|
import { useChat } from '../contexts/ChatProvider';
|
||||||
import { useMessageSound } from '../hooks/useMessageSound';
|
import { useMessageSound } from '../hooks/useMessageSound';
|
||||||
|
|
@ -12,6 +13,7 @@ export const ChatAside = () => {
|
||||||
const { showAside, setShowAside } = useUIState();
|
const { showAside, setShowAside } = useUIState();
|
||||||
const { sendMessage, chatHistory, hasNewMessages, setHasNewMessages } =
|
const { sendMessage, chatHistory, hasNewMessages, setHasNewMessages } =
|
||||||
useChat();
|
useChat();
|
||||||
|
const { localParticipant } = useParticipants();
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
const playMessageSound = useMessageSound();
|
const playMessageSound = useMessageSound();
|
||||||
|
|
||||||
|
|
@ -36,6 +38,8 @@ export const ChatAside = () => {
|
||||||
}
|
}
|
||||||
}, [chatHistory?.length]);
|
}, [chatHistory?.length]);
|
||||||
|
|
||||||
|
const isLocalUser = (id) => id === localParticipant.user_id;
|
||||||
|
|
||||||
if (!showAside || showAside !== CHAT_ASIDE) {
|
if (!showAside || showAside !== CHAT_ASIDE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +49,7 @@ export const ChatAside = () => {
|
||||||
<div className="messages-container" ref={chatWindowRef}>
|
<div className="messages-container" ref={chatWindowRef}>
|
||||||
{chatHistory.map((chatItem) => (
|
{chatHistory.map((chatItem) => (
|
||||||
<div
|
<div
|
||||||
className={chatItem.isLocal ? 'message local' : 'message'}
|
className={isLocalUser(chatItem.senderID) ? 'message local' : 'message'}
|
||||||
key={chatItem.id}
|
key={chatItem.id}
|
||||||
>
|
>
|
||||||
<span className="content">{chatItem.message}</span>
|
<span className="content">{chatItem.message}</span>
|
||||||
|
|
@ -91,7 +95,7 @@ export const ChatAside = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.local .sender {
|
.message.local .sender {
|
||||||
color: var(--primary-default);
|
color: var(--primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import React, {
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||||
|
import { useSharedState } from '@custom/shared/hooks/useSharedState';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
|
@ -13,54 +14,67 @@ export const ChatContext = createContext();
|
||||||
|
|
||||||
export const ChatProvider = ({ children }) => {
|
export const ChatProvider = ({ children }) => {
|
||||||
const { callObject } = useCallState();
|
const { callObject } = useCallState();
|
||||||
const [chatHistory, setChatHistory] = useState([]);
|
const { sharedState, setSharedState } = useSharedState({
|
||||||
|
initialValues: {
|
||||||
|
chatHistory: [],
|
||||||
|
},
|
||||||
|
broadcast: false,
|
||||||
|
});
|
||||||
const [hasNewMessages, setHasNewMessages] = useState(false);
|
const [hasNewMessages, setHasNewMessages] = useState(false);
|
||||||
|
|
||||||
const handleNewMessage = useCallback(
|
const handleNewMessage = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (e?.data?.message?.type) return;
|
||||||
const participants = callObject.participants();
|
const participants = callObject.participants();
|
||||||
const sender = participants[e.fromId].user_name
|
const sender = participants[e.fromId].user_name
|
||||||
? participants[e.fromId].user_name
|
? participants[e.fromId].user_name
|
||||||
: 'Guest';
|
: 'Guest';
|
||||||
|
|
||||||
setChatHistory((oldState) => [
|
setSharedState(values => ({
|
||||||
...oldState,
|
...values,
|
||||||
{ sender, message: e.data.message, id: nanoid() },
|
chatHistory: [
|
||||||
]);
|
...values.chatHistory,
|
||||||
|
// nanoid - we use it to generate unique ID string
|
||||||
|
{ sender, senderID: e.fromId, message: e.data.message, id: nanoid() },
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
setHasNewMessages(true);
|
setHasNewMessages(true);
|
||||||
},
|
},
|
||||||
[callObject]
|
[callObject, setSharedState]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
(message) => {
|
(message) => {
|
||||||
if (!callObject) {
|
if (!callObject) return;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('💬 Sending app message');
|
console.log('💬 Sending app message');
|
||||||
|
|
||||||
callObject.sendAppMessage({ message }, '*');
|
callObject.sendAppMessage({ message }, '*');
|
||||||
|
|
||||||
|
const participants = callObject.participants();
|
||||||
// Get the sender (local participant) name
|
// Get the sender (local participant) name
|
||||||
const sender = callObject.participants().local.user_name
|
const sender = participants.local.user_name
|
||||||
? callObject.participants().local.user_name
|
? participants.local.user_name
|
||||||
: 'Guest';
|
: 'Guest';
|
||||||
|
const senderID = participants.local.user_id;
|
||||||
|
|
||||||
// Update local chat history
|
// Update shared state chat history
|
||||||
return setChatHistory((oldState) => [
|
return setSharedState(values => ({
|
||||||
...oldState,
|
...values,
|
||||||
{ sender, message, id: nanoid(), isLocal: true },
|
chatHistory: [
|
||||||
]);
|
...values.chatHistory,
|
||||||
|
// nanoid - we use it to generate unique ID string
|
||||||
|
{ sender, senderID, message, id: nanoid() }
|
||||||
|
]
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
[callObject]
|
[callObject, setSharedState]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!callObject) {
|
if (!callObject) return;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`💬 Chat provider listening for app messages`);
|
console.log(`💬 Chat provider listening for app messages`);
|
||||||
|
|
||||||
|
|
@ -73,7 +87,7 @@ export const ChatProvider = ({ children }) => {
|
||||||
<ChatContext.Provider
|
<ChatContext.Provider
|
||||||
value={{
|
value={{
|
||||||
sendMessage,
|
sendMessage,
|
||||||
chatHistory,
|
chatHistory: sharedState.chatHistory,
|
||||||
hasNewMessages,
|
hasNewMessages,
|
||||||
setHasNewMessages,
|
setHasNewMessages,
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue