From 0ede38c4231a3e554ed2b6503cadb26fd4544af4 Mon Sep 17 00:00:00 2001 From: harshithpabbati Date: Thu, 11 Nov 2021 18:23:45 +0530 Subject: [PATCH] use shared state hook to get the chat history for the newly joined users --- custom/shared/contexts/participantsState.js | 3 + custom/shared/hooks/useSharedState.js | 123 ++++++++++++++++++++ custom/text-chat/README.md | 4 +- custom/text-chat/components/ChatAside.js | 8 +- custom/text-chat/contexts/ChatProvider.js | 56 +++++---- 5 files changed, 169 insertions(+), 25 deletions(-) create mode 100644 custom/shared/hooks/useSharedState.js diff --git a/custom/shared/contexts/participantsState.js b/custom/shared/contexts/participantsState.js index a447724..1b2a47c 100644 --- a/custom/shared/contexts/participantsState.js +++ b/custom/shared/contexts/participantsState.js @@ -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, diff --git a/custom/shared/hooks/useSharedState.js b/custom/shared/hooks/useSharedState.js new file mode 100644 index 0000000..6c61475 --- /dev/null +++ b/custom/shared/hooks/useSharedState.js @@ -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 }; +}; \ No newline at end of file diff --git a/custom/text-chat/README.md b/custom/text-chat/README.md index 47e5b37..5dc7e1c 100644 --- a/custom/text-chat/README.md +++ b/custom/text-chat/README.md @@ -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 diff --git a/custom/text-chat/components/ChatAside.js b/custom/text-chat/components/ChatAside.js index 16ae754..3d1a21b 100644 --- a/custom/text-chat/components/ChatAside.js +++ b/custom/text-chat/components/ChatAside.js @@ -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 = () => {
{chatHistory.map((chatItem) => (
{chatItem.message} @@ -91,7 +95,7 @@ export const ChatAside = () => { } .message.local .sender { - color: var(--primary-default); + color: var(--primary-dark); } .content { diff --git a/custom/text-chat/contexts/ChatProvider.js b/custom/text-chat/contexts/ChatProvider.js index db90609..ccb428b 100644 --- a/custom/text-chat/contexts/ChatProvider.js +++ b/custom/text-chat/contexts/ChatProvider.js @@ -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 }) => {