UNPKG

@100mslive/react-native-room-kit

Version:

100ms Room Kit provides simple & easy to use UI components to build Live Streaming & Video Conferencing experiences in your apps.

1,576 lines (1,414 loc) 101 kB
import type { HMSHLSPlayer, HMSPIPConfig, HMSRole, HMSSessionStore, HMSSpeaker, JsonValue, } from '@100mslive/react-native-hms'; import { getSoftInputMode, HMSChangeTrackStateRequest, HMSConfig, HMSHLSPlayerPlaybackState, HMSLocalPeer, HMSMessage, HMSMessageRecipient, HMSMessageRecipientType, HMSPeer, HMSPeerUpdate, HMSPIPListenerActions, HMSPollUpdateType, HMSRoleChangeRequest, HMSRoom, HMSRoomUpdate, HMSSDK, HMSTrack, HMSTrackSource, HMSTrackType, HMSTrackUpdate, HMSUpdateListenerActions, HMSVideoViewMode, setSoftInputMode, SoftInputModes, TranscriptionsMode, TranscriptionState, useHMSHLSPlayerCue, useHMSHLSPlayerPlaybackState, useHMSHLSPlayerResolution, useHmsViewsResolutionsState, WindowController, } from '@100mslive/react-native-hms'; import type { Chat as ChatConfig } from '@100mslive/types-prebuilt/elements/chat'; import type { ColorPalette, DefaultConferencingScreen, HLSLiveStreamingScreen, Layout, Theme, Typography, } from '@100mslive/types-prebuilt'; import Toast from 'react-native-simple-toast'; import type { DependencyList } from 'react'; import { useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import type { OnLeaveHandler, PeerTrackNode } from './utils/types'; import { ChatBroadcastFilter, MaxTilesInOnePage, ModalTypes, OnLeaveReason, PeerListRefreshInterval, PipModes, } from './utils/types'; import { createPeerTrackNode, parseMetadata } from './utils/functions'; import { batch, shallowEqual, useDispatch, useSelector, useStore, } from 'react-redux'; import type { AppDispatch, RootState } from './redux'; import { addCuedPollId, addMessage, addNotification, addParticipant, addParticipants, addPinnedMessages, addScreenshareTile, addUpdateParticipant, changeMeetingState, changePipModeStatus, changeStartingHLSStream, clearStore, filterOutMsgsFromBlockedPeers, removeNotification, removeParticipant, removeParticipants, removeScreenshareTile, replaceParticipantsList, saveUserData, setActiveChatBottomSheetTab, setActiveSpeakers, setAndroidHLSStreamPaused, setAutoEnterPipMode, setChatPeerBlacklist, setChatState, setEditUsernameDisabled, setFullScreenPeerTrackNode, setHandleBackButton, setHMSLocalPeerState, setHMSRoleState, setHMSRoomState, setIsLocalAudioMutedState, setIsLocalVideoMutedState, setLayoutConfig, setLocalPeerTrackNode, setMiniViewPeerTrackNode, setModalType, setOnLeaveHandler, setPrebuiltData, setReconnecting, setRoleChangeRequest, setStartingOrStoppingRecording, updateFullScreenPeerTrackNode, updateLocalPeerTrackNode, updateMiniViewPeerTrackNode, updateScreenshareTile, } from './redux/actions'; import { createPeerTrackNodeUniqueId, degradeOrRestorePeerTrackNodes, peerTrackNodeExistForPeer, peerTrackNodeExistForPeerAndTrack, removePeerTrackNodes, removePeerTrackNodesWithTrack, replacePeerTrackNodes, replacePeerTrackNodesWithTrack, } from './peerTrackNodeUtils'; import type { ChatState, HMSPrebuiltProps, PinnedMessage } from './types'; import { MeetingState, NotificationTypes } from './types'; import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native'; import { BackHandler, InteractionManager, Keyboard, Platform, } from 'react-native'; import { NavigationContext } from '@react-navigation/native'; import { useIsLandscapeOrientation, useIsPortraitOrientation, } from './utils/dimension'; import { selectChatLayoutConfig, selectConferencingScreenConfig, selectIsHLSViewer, selectLayoutConfigForRole, selectShouldGoLive, selectVideoTileLayoutConfig, } from './hooks-util-selectors'; import type { GridViewRefAttrs } from './components/GridView'; import { getRoomLayout } from './modules/HMSManager'; import { DEFAULT_THEME, DEFAULT_TYPOGRAPHY } from './utils/theme'; import { KeyboardState, useSharedValue } from 'react-native-reanimated'; import { useCanPublishAudio, useCanPublishScreen, useCanPublishVideo, useHMSActions, } from './hooks-sdk'; import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; export const useHMSListeners = ( setPeerTrackNodes: React.Dispatch<React.SetStateAction<PeerTrackNode[]>> ) => { const hmsInstance = useHMSInstance(); const updateLocalPeer = useUpdateHMSLocalPeer(hmsInstance); useHMSRoomUpdate(hmsInstance); useHMSPeersUpdate(hmsInstance, updateLocalPeer, setPeerTrackNodes); useHMSPeerListUpdated(hmsInstance); useHMSTrackUpdate(hmsInstance, updateLocalPeer, setPeerTrackNodes); }; const useHMSRoomUpdate = (hmsInstance: HMSSDK) => { const dispatch = useDispatch(); const reduxStore = useStore<RootState>(); useEffect(() => { const roomUpdateHandler = (data: { room: HMSRoom; type: HMSRoomUpdate; }) => { const { room, type } = data; dispatch(setHMSRoomState(room)); if (type === HMSRoomUpdate.BROWSER_RECORDING_STATE_UPDATED) { const startingOrStoppingRecording = reduxStore.getState().app.startingOrStoppingRecording; if (startingOrStoppingRecording) { dispatch(setStartingOrStoppingRecording(false)); } } else if (type === HMSRoomUpdate.HLS_STREAMING_STATE_UPDATED) { dispatch(changeStartingHLSStream(false)); } else if (type === HMSRoomUpdate.TRANSCRIPTIONS_UPDATED) { const captionTranscription = room.transcriptions?.find( (transcription) => transcription.mode === TranscriptionsMode.CAPTION ); if (captionTranscription?.state === TranscriptionState.STARTED) { batch(() => { dispatch(removeNotification('enable-cc')); dispatch(removeNotification('TranscriptionState.STARTED')); dispatch( addNotification({ id: 'TranscriptionState.STARTED', type: NotificationTypes.INFO, icon: 'cc', title: 'Closed Captioning enabled for everyone', }) ); }); } else if (captionTranscription?.state === TranscriptionState.STOPPED) { batch(() => { dispatch(removeNotification('disable-cc')); dispatch( addNotification({ id: Math.random().toString(16).slice(2), type: NotificationTypes.INFO, icon: 'cc', title: 'Closed Captioning disabled for everyone', }) ); }); } else if (captionTranscription?.state === TranscriptionState.FAILED) { const transcriptionError = captionTranscription.error; batch(() => { dispatch(removeNotification('enable-cc')); dispatch(removeNotification('disable-cc')); if (transcriptionError !== undefined) { dispatch( addNotification({ id: Math.random().toString(16).slice(2), title: transcriptionError.message || 'Failed to enable/disable Closed Captions', type: NotificationTypes.ERROR, }) ); } }); } } }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_ROOM_UPDATE, roomUpdateHandler ); return () => { hmsInstance.removeEventListener(HMSUpdateListenerActions.ON_ROOM_UPDATE); }; }, [hmsInstance]); }; type PeerUpdate = { peer: HMSPeer; type: HMSPeerUpdate; }; const useHMSPeersUpdate = ( hmsInstance: HMSSDK, updateLocalPeer: () => void, setPeerTrackNodes: React.Dispatch<React.SetStateAction<PeerTrackNode[]>> ) => { const dispatch = useDispatch(); const store = useStore<RootState>(); // const inMeeting = useSelector( // (state: RootState) => state.app.meetingState === MeetingState.IN_MEETING // ); const hmsActions = useHMSActions(); const isFirstRunForRoleChangeModal = useRef(true); useEffect(() => { const peerUpdateHandler = ({ peer, type }: PeerUpdate) => { // Handle State from Preview screen // TODO: When `inMeeting` becomes true Peer Update is resubscribed, we might lose some events during that time // if (!inMeeting) { // if (type === HMSPeerUpdate.PEER_JOINED) { // dispatch(addToPreviewPeersList(peer)); // } else if (type === HMSPeerUpdate.PEER_LEFT) { // dispatch(removeFromPreviewPeersList(peer)); // } // } // Handle State for Meeting screen if (type === HMSPeerUpdate.PEER_JOINED) { dispatch(addParticipant(peer)); return; } if (type === HMSPeerUpdate.PEER_LEFT) { dispatch(removeParticipant(peer)); // Handling regular tiles list setPeerTrackNodes((prevPeerTrackNodes) => removePeerTrackNodes(prevPeerTrackNodes, peer) ); const reduxState = store.getState(); // Handling Screenshare tiles list const screensharePeerTrackNodes = reduxState.app.screensharePeerTrackNodes; const nodeToRemove = screensharePeerTrackNodes.find( (node) => node.peer.peerID === peer.peerID ); if (nodeToRemove) { dispatch(removeScreenshareTile(nodeToRemove.id)); } // Handling Full screen view const fullScreenPeerTrackNode = reduxState.app.fullScreenPeerTrackNode; if ( fullScreenPeerTrackNode !== null && fullScreenPeerTrackNode.peer.peerID === peer.peerID ) { dispatch(setFullScreenPeerTrackNode(null)); } return; } if (peer.isLocal) { const reduxState = store.getState(); const fullScreenPeerTrackNode = reduxState.app.fullScreenPeerTrackNode; const miniviewPeerTrackNode = reduxState.app.miniviewPeerTrackNode; const localPeerTrackNode = reduxState.app.localPeerTrackNode; const initialRole = reduxState.app.initialRole; // Currently Applied Layout config const currentLayoutConfig = selectLayoutConfigForRole( reduxState.hmsStates.layoutConfig, peer.role || null ); // Local Tile Inset layout is enabled const enableLocalTileInset = selectVideoTileLayoutConfig(currentLayoutConfig)?.grid ?.enable_local_tile_inset; // Local Tile Inset layout is disabled const localTileInsetDisbaled = !enableLocalTileInset; // Local Tile Inset layout is disabled // then update local peer track node available in list of peer track nodes if (localTileInsetDisbaled) { setPeerTrackNodes((prevPeerTrackNodes) => { if (peerTrackNodeExistForPeer(prevPeerTrackNodes, peer)) { return replacePeerTrackNodes(prevPeerTrackNodes, peer); } return prevPeerTrackNodes; }); } batch(() => { if (localPeerTrackNode) { dispatch(updateLocalPeerTrackNode({ peer })); } else if (isPublishingAllowed(peer)) { dispatch( setLocalPeerTrackNode(createPeerTrackNode(peer, peer.videoTrack)) ); } if ( fullScreenPeerTrackNode && fullScreenPeerTrackNode.peer.peerID === peer.peerID ) { dispatch(updateFullScreenPeerTrackNode({ peer })); } // If Local Tile Inset layout is enabled, then update or set it if (enableLocalTileInset) { if ( miniviewPeerTrackNode !== null && miniviewPeerTrackNode.peer.peerID === peer.peerID ) { dispatch(updateMiniViewPeerTrackNode({ peer })); } else if ( miniviewPeerTrackNode === null && peer.role?.publishSettings?.allowed && peer.role.publishSettings.allowed.length > 0 ) { dispatch( setMiniViewPeerTrackNode( createPeerTrackNode(peer, peer.videoTrack) ) ); } } // If Local Tile Inset layout is disabled, then remove it if it exists else if (miniviewPeerTrackNode) { dispatch(setMiniViewPeerTrackNode(null)); } }); // - TODO: update local localPeer state // - Pass this updated data to Meeting component -> DisplayView component updateLocalPeer(); if (type === HMSPeerUpdate.ROLE_CHANGED) { const parsedLocalPeerMetadata = parseMetadata(peer.metadata); if (parsedLocalPeerMetadata.prevRole !== initialRole) { const newMetadata = { ...parsedLocalPeerMetadata, prevRole: initialRole?.name, }; hmsActions .changeMetadata(newMetadata) .then((r) => { console.log('Metadata changed successfully', r); }) .catch((e) => { console.log('Metadata change failed', e); }); if (isFirstRunForRoleChangeModal.current) { isFirstRunForRoleChangeModal.current = false; } else { dispatch( addNotification({ id: Math.random().toString(16).slice(2), type: NotificationTypes.INFO, title: `You are now a ${peer.role?.name}`, }) ); } } } return; } if (type === HMSPeerUpdate.ROLE_CHANGED) { dispatch(addUpdateParticipant(peer)); // saving current role in peer metadata, // so that when peer is removed from stage, we can assign previous role to it. // if (localPeerRoleName) { // const newMetadata = { // ...localPeerMetadata, // prevRole: localPeerRoleName, // }; // await hmsActions.changeMetadata(newMetadata); // } // Handling regular tiles list if ( peer.role?.publishSettings?.allowed === undefined || (peer.role?.publishSettings?.allowed && peer.role?.publishSettings?.allowed.length < 1) ) { setPeerTrackNodes((prevPeerTrackNodes) => { if (peerTrackNodeExistForPeer(prevPeerTrackNodes, peer)) { return removePeerTrackNodes(prevPeerTrackNodes, peer); } return prevPeerTrackNodes; }); } const reduxState = store.getState(); // Handling screenshare tiles list if ( peer.role?.publishSettings?.allowed === undefined || (peer.role?.publishSettings?.allowed && !peer.role?.publishSettings?.allowed.includes('screen')) ) { const screensharePeerTrackNodes = reduxState.app.screensharePeerTrackNodes; const nodeToRemove = screensharePeerTrackNodes.find( (node) => node.peer.peerID === peer.peerID ); if (nodeToRemove) { dispatch(removeScreenshareTile(nodeToRemove.id)); } } // Handling full screen view if ( peer.role?.publishSettings?.allowed === undefined || (peer.role?.publishSettings?.allowed && !peer.role?.publishSettings?.allowed.includes('video')) ) { const fullScreenPeerTrackNode = reduxState.app.fullScreenPeerTrackNode; if ( fullScreenPeerTrackNode !== null && fullScreenPeerTrackNode.peer.peerID === peer.peerID ) { dispatch(setFullScreenPeerTrackNode(null)); } } return; } if ( type === HMSPeerUpdate.METADATA_CHANGED || type === HMSPeerUpdate.HAND_RAISED_CHANGED || type === HMSPeerUpdate.NAME_CHANGED || type === HMSPeerUpdate.NETWORK_QUALITY_UPDATED ) { dispatch(addUpdateParticipant(peer)); const reduxState = store.getState(); if (type === HMSPeerUpdate.HAND_RAISED_CHANGED) { const handRaised = peer.isHandRaised; if (handRaised) { const { layoutConfig, localPeer } = reduxState.hmsStates; const selectedLayoutConfig = selectLayoutConfigForRole( layoutConfig, localPeer?.role || null ); // list of roles which should be brought on stage when they raise hand const offStageRoles = selectedLayoutConfig?.screens?.conferencing?.default?.elements ?.on_stage_exp?.off_stage_roles; // checking if the current peer role is included in the above list const shouldBringOnStage = offStageRoles && offStageRoles.includes(peer.role?.name!); const canChangeRole = reduxState.hmsStates.localPeer?.role?.permissions?.changeRole; if (shouldBringOnStage && canChangeRole) { dispatch( addNotification({ id: `${peer.peerID}-${NotificationTypes.HAND_RAISE}`, type: NotificationTypes.HAND_RAISE, peer, }) ); } } else { const notifications = reduxState.app.notifications; const notificationToRemove = notifications.find( (notification) => notification.id === `${peer.peerID}-${NotificationTypes.HAND_RAISE}` ); if (notificationToRemove) { dispatch(removeNotification(notificationToRemove.id)); } } } setPeerTrackNodes((prevPeerTrackNodes) => { if (peerTrackNodeExistForPeer(prevPeerTrackNodes, peer)) { return replacePeerTrackNodes(prevPeerTrackNodes, peer); } return prevPeerTrackNodes; }); // Handling screenshare tile views const screensharePeerTrackNodes = reduxState.app.screensharePeerTrackNodes; const nodeToUpdate = screensharePeerTrackNodes.find( (node) => node.peer.peerID === peer.peerID ); if (nodeToUpdate) { dispatch(updateScreenshareTile({ id: nodeToUpdate.id, peer })); } // Handling fullscreen view const fullScreenPeerTrackNode = reduxState.app.fullScreenPeerTrackNode; if ( fullScreenPeerTrackNode !== null && fullScreenPeerTrackNode.peer.peerID === peer.peerID ) { dispatch(updateFullScreenPeerTrackNode({ peer })); } return; } }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_PEER_UPDATE, peerUpdateHandler ); return () => { hmsInstance.removeEventListener(HMSUpdateListenerActions.ON_PEER_UPDATE); }; }, [hmsInstance]); }; type PeerListUpdate = { addedPeers: HMSPeer[]; removedPeers: HMSPeer[]; }; const useHMSPeerListUpdated = (hmsInstance: HMSSDK) => { const dispatch = useDispatch(); useEffect(() => { const peerListUpdateHandler = ({ addedPeers, removedPeers, }: PeerListUpdate) => { batch(() => { dispatch(addParticipants(addedPeers)); dispatch(removeParticipants(removedPeers)); }); }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_PEER_LIST_UPDATED, peerListUpdateHandler ); return () => { hmsInstance.removeEventListener( HMSUpdateListenerActions.ON_PEER_LIST_UPDATED ); }; }, [hmsInstance]); }; type TrackUpdate = { peer: HMSPeer; track: HMSTrack; type: HMSTrackUpdate; }; export const isPublishingAllowed = (peer: HMSPeer): boolean => { return ( (peer.role?.publishSettings?.allowed && peer.role?.publishSettings?.allowed?.length > 0) ?? false ); }; const useHMSTrackUpdate = ( hmsInstance: HMSSDK, updateLocalPeer: () => void, setPeerTrackNodes: React.Dispatch<React.SetStateAction<PeerTrackNode[]>> ) => { const dispatch = useDispatch(); const store = useStore<RootState>(); useEffect(() => { const trackUpdateHandler = ({ peer, track, type }: TrackUpdate) => { const reduxState = store.getState(); const fullScreenPeerTrackNode = reduxState.app.fullScreenPeerTrackNode; const miniviewPeerTrackNode = reduxState.app.miniviewPeerTrackNode; const localPeerTrackNode = reduxState.app.localPeerTrackNode; const localPeerRole = reduxState.hmsStates.localPeer?.role ?? null; const currentLayoutConfig = selectLayoutConfigForRole( reduxState.hmsStates.layoutConfig, localPeerRole ); const localTileInsetEnabled = selectVideoTileLayoutConfig(currentLayoutConfig)?.grid ?.enable_local_tile_inset; if (type === HMSTrackUpdate.TRACK_ADDED) { const newPeerTrackNode = createPeerTrackNode(peer, track); if (track.source === HMSTrackSource.SCREEN) { if (!peer.isLocal && track.type === HMSTrackType.VIDEO) { dispatch(addScreenshareTile(newPeerTrackNode)); } if (track.type === HMSTrackType.VIDEO) { const whiteboard = reduxState.hmsStates.whiteboard; // If white board is open and local peer is owner, close whiteboard if ( whiteboard && // Is local peer has whiteboard admin permission !!localPeerRole?.permissions?.whiteboard?.admin && // Is local peer owner of whiteboard whiteboard.isOwner ) { hmsInstance.interactivityCenter .stopWhiteboard() .then((success) => { console.log('StopWhiteboard on Screenshare: ', success); }) .catch((error) => { console.log('StopWhiteboard error: ', error); }); } } } else { setPeerTrackNodes((prevPeerTrackNodes) => { if ( peerTrackNodeExistForPeerAndTrack(prevPeerTrackNodes, peer, track) ) { if (track.type === HMSTrackType.VIDEO) { return replacePeerTrackNodesWithTrack( prevPeerTrackNodes, peer, track ); } return replacePeerTrackNodes(prevPeerTrackNodes, peer); } if (peer.isLocal && !localTileInsetEnabled) { return [newPeerTrackNode, ...prevPeerTrackNodes]; } if ( !peer.isLocal && (miniviewPeerTrackNode ? newPeerTrackNode.id !== miniviewPeerTrackNode.id : true) ) { return [...prevPeerTrackNodes, newPeerTrackNode]; } return prevPeerTrackNodes; }); } // - TODO: update local localPeer state // - Pass this updated data to Meeting component -> DisplayView component if (peer.isLocal) { if (track.source === HMSTrackSource.REGULAR) { if (!localPeerTrackNode) { if (isPublishingAllowed(newPeerTrackNode.peer)) { dispatch(setLocalPeerTrackNode(newPeerTrackNode)); } } else { dispatch( updateLocalPeerTrackNode( track.type === HMSTrackType.VIDEO ? { peer, track } : { peer } ) ); } if (localTileInsetEnabled) { // only setting `miniviewPeerTrackNode`, when: // - there is no `miniviewPeerTrackNode` // - if there is, then it is of regular track if (!miniviewPeerTrackNode) { dispatch(setMiniViewPeerTrackNode(newPeerTrackNode)); } else if (miniviewPeerTrackNode.id === newPeerTrackNode.id) { dispatch( updateMiniViewPeerTrackNode( track.type === HMSTrackType.VIDEO ? { peer, track } : { peer } ) ); } } // if (track.type === HMSTrackType.AUDIO) { // dispatch(setIsLocalAudioMutedState(track.isMute())); // } else if (track.type === HMSTrackType.VIDEO) { // dispatch(setIsLocalVideoMutedState(track.isMute())); // } } // else -> { // should `localPeerTrackNode` be created/updated for non-regular track addition? // should `miniviewPeerTrackNode` be created/updated for non-regular track addition? // } updateLocalPeer(); } else { // only setting `miniviewPeerTrackNode`, when: // - there is already `miniviewPeerTrackNode` // - and it is of same peer's regular track if ( miniviewPeerTrackNode && miniviewPeerTrackNode.id === newPeerTrackNode.id ) { dispatch( updateMiniViewPeerTrackNode( track.type === HMSTrackType.VIDEO ? { peer, track } : { peer } ) ); } } return; } if (type === HMSTrackUpdate.TRACK_REMOVED) { if (track.source === HMSTrackSource.SCREEN) { if (!peer.isLocal && track.type === HMSTrackType.VIDEO) { hmsInstance.setActiveSpeakerInIOSPIP(true); const screensharePeerTrackNodes = reduxState.app.screensharePeerTrackNodes; const nodeToRemove = screensharePeerTrackNodes.find( (node) => node.track?.trackId === track.trackId ); if (nodeToRemove) { dispatch(removeScreenshareTile(nodeToRemove.id)); } } } else if ( track.source === HMSTrackSource.PLUGIN || (peer.audioTrack?.trackId === undefined && peer.videoTrack?.trackId === undefined) ) { setPeerTrackNodes((prevPeerTrackNodes) => removePeerTrackNodesWithTrack(prevPeerTrackNodes, peer, track) ); } if ( fullScreenPeerTrackNode && fullScreenPeerTrackNode.track && fullScreenPeerTrackNode.track.trackId === track.trackId ) { dispatch(setFullScreenPeerTrackNode(null)); } // - TODO: update local localPeer state // - Pass this updated data to Meeting component -> DisplayView component if (peer.isLocal) { if (track.source === HMSTrackSource.REGULAR) { if (!peer.audioTrack?.trackId && !peer.videoTrack?.trackId) { dispatch(setLocalPeerTrackNode(null)); // removing `miniviewPeerTrackNode`, when: // - `localPeerTrack` was used as `miniviewPeerTrackNode` // - and now local peer doesn't have any tracks if ( miniviewPeerTrackNode && miniviewPeerTrackNode.peer.peerID === peer.peerID ) { dispatch(setMiniViewPeerTrackNode(null)); } } else { if (track.type === HMSTrackType.VIDEO) { dispatch(updateLocalPeerTrackNode({ peer, track: undefined })); } else { dispatch(updateLocalPeerTrackNode({ peer })); } // updating `miniviewPeerTrackNode` if ( miniviewPeerTrackNode && miniviewPeerTrackNode.peer.peerID === peer.peerID ) { if (track.type === HMSTrackType.VIDEO) { dispatch( updateMiniViewPeerTrackNode({ peer, track: undefined }) ); } else { dispatch(updateMiniViewPeerTrackNode({ peer })); } } } } updateLocalPeer(); } else { // only removing `miniviewPeerTrackNode`, when: // - there is already `miniviewPeerTrackNode` // - and it is of same peer's regular track const uniqueId = createPeerTrackNodeUniqueId(peer, track); if (miniviewPeerTrackNode && miniviewPeerTrackNode.id === uniqueId) { dispatch(setMiniViewPeerTrackNode(null)); } } return; } if ( type === HMSTrackUpdate.TRACK_MUTED || type === HMSTrackUpdate.TRACK_UNMUTED ) { // - TODO: update local mute states // - Pass this updated data to Meeting component -> DisplayView component if (peer.isLocal) { if (track.type === HMSTrackType.AUDIO) { dispatch(setIsLocalAudioMutedState(track.isMute())); } else if (track.type === HMSTrackType.VIDEO) { dispatch(setIsLocalVideoMutedState(track.isMute())); } } setPeerTrackNodes((prevPeerTrackNodes) => { if ( peerTrackNodeExistForPeerAndTrack(prevPeerTrackNodes, peer, track) ) { if (track.type === HMSTrackType.VIDEO) { return replacePeerTrackNodesWithTrack( prevPeerTrackNodes, peer, track ); } return replacePeerTrackNodes(prevPeerTrackNodes, peer); } return prevPeerTrackNodes; }); const uniqueId = createPeerTrackNodeUniqueId(peer, track); // - TODO: update local localPeer state // - Pass this updated data to Meeting component -> DisplayView component const updatePayload = track.type === HMSTrackType.VIDEO ? { peer, track } : { peer }; if (peer.isLocal) { dispatch(updateLocalPeerTrackNode(updatePayload)); updateLocalPeer(); } if (miniviewPeerTrackNode && miniviewPeerTrackNode.id === uniqueId) { dispatch(updateMiniViewPeerTrackNode(updatePayload)); } if ( fullScreenPeerTrackNode && fullScreenPeerTrackNode.id === uniqueId ) { dispatch(updateFullScreenPeerTrackNode(updatePayload)); } return; } if ( type === HMSTrackUpdate.TRACK_RESTORED || type === HMSTrackUpdate.TRACK_DEGRADED ) { // Checking if track source is screenshare if (track.source === HMSTrackSource.SCREEN) { // Handling screenshare tiles list const screensharePeerTrackNodes = reduxState.app.screensharePeerTrackNodes; const nodeToUpdate = screensharePeerTrackNodes.find( (node) => node.track?.trackId === track.trackId ); if (nodeToUpdate) { dispatch( updateScreenshareTile({ id: nodeToUpdate.id, isDegraded: type === HMSTrackUpdate.TRACK_DEGRADED, }) ); } } else { // Handling regular tiles list setPeerTrackNodes((prevPeerTrackNodes) => { if ( peerTrackNodeExistForPeerAndTrack(prevPeerTrackNodes, peer, track) ) { return degradeOrRestorePeerTrackNodes( prevPeerTrackNodes, peer, track, type === HMSTrackUpdate.TRACK_DEGRADED ); } return prevPeerTrackNodes; }); } const uniqueId = createPeerTrackNodeUniqueId(peer, track); // Handling miniview if (miniviewPeerTrackNode && miniviewPeerTrackNode.id === uniqueId) { dispatch( updateMiniViewPeerTrackNode({ isDegraded: type === HMSTrackUpdate.TRACK_DEGRADED, }) ); } // Handling full screen view if ( fullScreenPeerTrackNode && fullScreenPeerTrackNode.id === uniqueId ) { dispatch( updateFullScreenPeerTrackNode({ isDegraded: type === HMSTrackUpdate.TRACK_DEGRADED, }) ); } return; } }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_TRACK_UPDATE, trackUpdateHandler ); return () => { hmsInstance.removeEventListener(HMSUpdateListenerActions.ON_TRACK_UPDATE); }; }, [hmsInstance]); }; const useUpdateHMSLocalPeer = (hmsInstance: HMSSDK) => { const mountRef = useRef(false); const dispatch = useDispatch(); const updateLocalPeer = useCallback(() => { hmsInstance.getLocalPeer().then((latestLocalPeer) => { if (mountRef.current) { dispatch(setHMSLocalPeerState(latestLocalPeer)); } }); }, [hmsInstance]); useEffect(() => { mountRef.current = true; updateLocalPeer(); return () => { mountRef.current = false; }; }, [updateLocalPeer]); return updateLocalPeer; }; export const useHMSInstance = () => { const hmsInstance = useSelector((state: RootState) => state.user.hmsInstance); if (!hmsInstance) { throw new Error('HMS Instance not available'); } return hmsInstance; }; export const useIsHLSViewer = () => { return useSelector((state: RootState) => { const { layoutConfig, localPeer } = state.hmsStates; const selectedLayoutConfig = selectLayoutConfigForRole( layoutConfig, localPeer?.role || null ); return selectIsHLSViewer(localPeer?.role, selectedLayoutConfig); }); }; type TrackStateChangeRequest = { requestedBy?: string; suggestedRole?: string; }; export const useHMSChangeTrackStateRequest = ( callback?: (unmuteRequest: Omit<HMSChangeTrackStateRequest, 'mute'>) => void, deps?: React.DependencyList ) => { const hmsInstance = useHMSInstance(); const [trackStateChangeRequest, setTrackStateChangeRequest] = useState<TrackStateChangeRequest | null>(null); useEffect(() => { const changeTrackStateRequestHandler = ( request: HMSChangeTrackStateRequest ) => { if (!request?.mute) { setTrackStateChangeRequest({ requestedBy: request?.requestedBy?.name, suggestedRole: request?.trackType, }); callback?.(request); } else { Toast.showWithGravity( `Track Muted: ${request?.requestedBy?.name} Muted Your ${request?.trackType}`, Toast.LONG, Toast.TOP ); } }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_CHANGE_TRACK_STATE_REQUEST, changeTrackStateRequestHandler ); return () => { hmsInstance.removeEventListener( HMSUpdateListenerActions.ON_CHANGE_TRACK_STATE_REQUEST ); }; }, [...(deps || []), hmsInstance]); return trackStateChangeRequest; }; export const useHMSRoleChangeRequest = ( callback?: (request: HMSRoleChangeRequest) => void, deps?: React.DependencyList ) => { const taskRef = useRef<any>(null); const dispatch = useDispatch(); const hmsInstance = useHMSInstance(); useEffect(() => { const changeRoleRequestHandler = async (request: HMSRoleChangeRequest) => { taskRef.current = InteractionManager.runAfterInteractions(() => { dispatch(setRoleChangeRequest(request)); callback?.(request); }); }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_ROLE_CHANGE_REQUEST, changeRoleRequestHandler ); return () => { taskRef.current?.cancel(); hmsInstance.removeEventListener( HMSUpdateListenerActions.ON_ROLE_CHANGE_REQUEST ); }; }, [...(deps || []), hmsInstance]); }; type SessionStoreListeners = Array<{ remove: () => void }>; export const useHMSSessionStoreListeners = ( gridViewRef?: React.MutableRefObject<GridViewRefAttrs | null> ) => { const store = useStore<RootState>(); const dispatch = useDispatch(); const hmsSessionStore = useSelector( (state: RootState) => state.user.hmsSessionStore ); const sessionStoreListenersRef = useRef<SessionStoreListeners>([]); useEffect(() => { // Check if instance of HMSSessionStore is available if (hmsSessionStore) { // let toastTimeoutId: NodeJS.Timeout | null = null; const addSessionStoreListeners = () => { // Handle 'spotlight' key values const handleSpotlightIdChange = (id: JsonValue) => { if (id === null || id === undefined || typeof id === 'string') { // set value to the state to rerender the component to reflect changes dispatch(saveUserData({ spotlightTrackId: id })); // Scroll to start of the list gridViewRef?.current ?.getRegularTilesFlatlistRef() .current?.scrollToOffset({ animated: true, offset: 0 }); } }; // Handle 'pinnedMessages' key values const handlePinnedMessagesChange = (data: JsonValue) => { if (Array.isArray(data)) { dispatch(addPinnedMessages(data as PinnedMessage[])); } }; // Handle 'chatState' key values const handleChatStateChange = (data: JsonValue) => { try { if ( typeof data !== 'object' || Array.isArray(data) || data === null ) { throw new Error('`data` is a falsy value'); } if (!('enabled' in data)) { throw new Error("`data` doesn't have `enabled` property"); } const parsedData = data as ChatState; const reduxState = store.getState(); const currentChatState = reduxState.app.chatState; if (parsedData.enabled === currentChatState?.enabled) { return; } const currentLayoutConfig = selectLayoutConfigForRole( reduxState.hmsStates.layoutConfig, reduxState.hmsStates.localPeer?.role ?? null ); const chatLayoutConfig = selectChatLayoutConfig(currentLayoutConfig); const isAllowedToSendMessage = (chatLayoutConfig?.private_chat_enabled || chatLayoutConfig?.public_chat_enabled || (chatLayoutConfig?.roles_whitelist && chatLayoutConfig?.roles_whitelist.length > 0)) ?? false; batch(() => { if ( isAllowedToSendMessage && // Only show notification when allowed to send message, AND (!parsedData.enabled || // Chat is Paused, OR (currentChatState && parsedData.enabled !== currentChatState.enabled)) // current Chat state is different from previous state ) { dispatch( addNotification({ id: `chat-state-enabled-${Math.random() .toString(16) .slice(2)}`, icon: parsedData.enabled ? 'chat-on' : 'chat-off', type: NotificationTypes.INFO, title: `Chat ${parsedData.enabled ? 'Resumed' : 'Paused'}`, message: `Chat ${ parsedData.enabled ? 'resumed' : 'paused' } ${ parsedData.updatedBy ? `by ${parsedData.updatedBy.userName}` : '' }`, }) ); } dispatch(setChatState(parsedData)); }); } catch (error) { dispatch(setChatState(null)); } }; // Handle 'chatPeerBlacklist' key values const handleChatPeerBlacklistChange = (data: JsonValue) => { // Whenever list changes : // - check if local peer is blocked or unblocked // - filter out messages of blocked peers if (Array.isArray(data)) { batch(() => { dispatch(setChatPeerBlacklist(data as string[])); dispatch(filterOutMsgsFromBlockedPeers(data as string[])); }); } }; // Getting value for 'spotlight' key by using `get` method on HMSSessionStore instance hmsSessionStore .get('spotlight') .then((data) => { console.log( 'Session Store get `spotlight` key value success: ', data ); handleSpotlightIdChange(data); }) .catch((error) => console.log( 'Session Store get `spotlight` key value error: ', error ) ); // Getting value for 'pinnedMessages' key by using `get` method on HMSSessionStore instance hmsSessionStore .get('pinnedMessages') .then((data) => { console.log( 'Session Store get `pinnedMessages` key value success: ', data ); handlePinnedMessagesChange(data); }) .catch((error) => console.log( 'Session Store get `pinnedMessages` key value error: ', error ) ); // Getting value for 'chatState' key by using `get` method on HMSSessionStore instance hmsSessionStore .get('chatState') .then((data) => { console.log( 'Session Store get `chatState` key value success: ', data ); handleChatStateChange(data); }) .catch((error) => console.log( 'Session Store get `chatState` key value error: ', error ) ); // Getting value for 'chatPeerBlacklist' key by using `get` method on HMSSessionStore instance hmsSessionStore .get('chatPeerBlacklist') .then((data) => { console.log( 'Session Store get `chatPeerBlacklist` key value success: ', data ); handleChatPeerBlacklistChange(data); }) .catch((error) => console.log( 'Session Store get `chatPeerBlacklist` key value error: ', error ) ); // let lastSpotlightValue: HMSSessionStoreValue = null; // let lastPinnedMessageValue: HMSSessionStoreValue = null; // Add subscription for `spotlight`, `pinnedMessages`, `chatState` & `chatPeerBlacklist` keys updates on Session Store const subscription = hmsSessionStore.addKeyChangeListener< ['spotlight', 'pinnedMessages', 'chatState', 'chatPeerBlacklist'] >( ['spotlight', 'pinnedMessages', 'chatState', 'chatPeerBlacklist'], (error, data) => { // If error occurs, handle error and return early if (error !== null) { console.log( '`spotlight`, `pinnedMessages`, `chatState` & `chatPeerBlacklist` key listener Error -> ', error ); return; } // If no error, handle data if (data !== null) { switch (data.key) { case 'spotlight': { handleSpotlightIdChange(data.value); break; } case 'pinnedMessages': { handlePinnedMessagesChange(data.value); break; } case 'chatState': { handleChatStateChange(data.value); break; } case 'chatPeerBlacklist': { handleChatPeerBlacklistChange(data.value); break; } } } } ); // Save reference of `subscription` in a ref sessionStoreListenersRef.current.push(subscription); }; addSessionStoreListeners(); return () => { // remove Session Store key update listener on cleanup sessionStoreListenersRef.current.forEach((listener) => listener.remove() ); // if (toastTimeoutId !== null) clearTimeout(toastTimeoutId); }; } }, [store, hmsSessionStore]); }; export const useHMSSessionStore = () => { const hmsInstance = useHMSInstance(); const dispatch = useDispatch(); useEffect(() => { const onSessionStoreAvailableListener = ({ sessionStore, }: { sessionStore: HMSSessionStore; }) => { // Saving `sessionStore` reference in `redux` dispatch(saveUserData({ hmsSessionStore: sessionStore })); }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_SESSION_STORE_AVAILABLE, onSessionStoreAvailableListener ); return () => { hmsInstance.removeEventListener( HMSUpdateListenerActions.ON_SESSION_STORE_AVAILABLE ); }; }, [hmsInstance]); }; export const useHMSMessages = () => { const hmsInstance = useHMSInstance(); const dispatch = useDispatch(); const canChangeRole = useSelector( (state: RootState) => state.hmsStates.localPeer?.role?.permissions?.changeRole ); const canShowChat = useHMSConferencingScreenConfig( (conferencingScreenConfig) => !!conferencingScreenConfig?.elements?.chat ); useEffect(() => { const onMessageListener = (message: HMSMessage) => { if (message.type === NotificationTypes.ROLE_CHANGE_DECLINED) { if (canChangeRole) { dispatch( addNotification({ id: `${message.sender?.peerID}-${NotificationTypes.ROLE_CHANGE_DECLINED}`, type: NotificationTypes.ROLE_CHANGE_DECLINED, peer: message.sender!, }) ); } } else if (message.type === 'EMOJI_REACTION') { console.log('Ignoring Emoji Reaction Message: ', message); } else if (canShowChat) { dispatch(addMessage(message)); } }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_MESSAGE, onMessageListener ); return () => { // TODO: Remove this listener when user leaves, removed or room is ended hmsInstance.removeEventListener(HMSUpdateListenerActions.ON_MESSAGE); }; }, [canChangeRole, canShowChat, hmsInstance]); }; export const useHMSReconnection = () => { const dispatch = useDispatch(); const hmsInstance = useHMSInstance(); useEffect(() => { let mounted = true; hmsInstance.addEventListener(HMSUpdateListenerActions.RECONNECTING, () => { if (mounted) { batch(() => { dispatch(setReconnecting(true)); dispatch( addNotification({ id: NotificationTypes.RECONNECTING, type: NotificationTypes.RECONNECTING, }) ); }); } }); hmsInstance.addEventListener(HMSUpdateListenerActions.RECONNECTED, () => { if (mounted) { batch(() => { dispatch(setReconnecting(false)); dispatch(removeNotification(NotificationTypes.RECONNECTING)); }); } }); return () => { mounted = false; hmsInstance.removeEventListener(HMSUpdateListenerActions.RECONNECTING); hmsInstance.removeEventListener(HMSUpdateListenerActions.RECONNECTED); }; }, [hmsInstance]); }; export const useHMSPIPRoomLeave = () => { const hmsInstance = useHMSInstance(); const { destroy } = useLeaveMethods(); useEffect(() => { const pipRoomLeaveHandler = () => { destroy(OnLeaveReason.PIP); }; hmsInstance.addEventListener( HMSPIPListenerActions.ON_PIP_ROOM_LEAVE, pipRoomLeaveHandler ); return () => { hmsInstance.removeEventListener(HMSPIPListenerActions.ON_PIP_ROOM_LEAVE); }; }, [destroy, hmsInstance]); }; export const useHMSRemovedFromRoomUpdate = () => { const hmsInstance = useHMSInstance(); const { destroy } = useLeaveMethods(); useEffect(() => { const removedFromRoomHandler = (data: { requestedBy?: HMSPeer | null; reason?: string; roomEnded?: boolean; }) => { destroy( data.roomEnded ? OnLeaveReason.ROOM_END : OnLeaveReason.PEER_KICKED ); }; hmsInstance.addEventListener( HMSUpdateListenerActions.ON_REMOVED_FROM_ROOM, removedFromRoomHandler ); return () => { hmsInstance.removeEventListener( HMSUpdateListenerActions.ON_REMOVED_FROM_ROOM ); }; }, [destroy, hmsInstance]); }; export const usePIPListener = () => { const hmsInstance = useHMSInstance(); const dispatch = useDispatch(); const isPipModeActive = useSelector( (state: RootState) => state.app.pipModeStatus === PipModes.ACTIVE ); useEffect(() => { const pipModeChangedHandler = (data: { isInPictureInPictureMode: boolean; }) => { dispatch( changePipModeStatus( data.isInPictureInPictureMode ? PipModes.ACTIVE : PipModes.INACTIVE ) ); }; hmsInstance.addEventListener( HMSPIPListenerActions.ON_PIP_MODE_CHANGED, pipModeChangedHandler ); return () => { hmsInstance.removeEventListener( HMSPIPListenerActions.ON_PIP_MODE_CHANGED ); }; }, []); // Check if PIP is supported or not useEffect(() => { // Only check for PIP support if PIP is not active if (hmsInstance && !isPipModeActive) { const check = async () => { try { const isSupported = await hmsInstance.isPipModeSupported(); if (!isSupported) { dispatch(changePipModeStatus(PipModes.NOT_AVAILABLE)); }