@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
text/typescript
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));
}