From 4660cd9a24af26c314b209a36cb060078d094c8e Mon Sep 17 00:00:00 2001 From: SujithThirumalaisamy Date: Thu, 20 Mar 2025 22:22:01 +0530 Subject: [PATCH] Added participants List --- app/contexts/layout-context.tsx | 61 ++++++++++ app/custom/CustomControlBar.tsx | 9 +- app/custom/ParticipantList.tsx | 146 ++++++++++++++++++++++++ app/rooms/[roomName]/PageClientImpl.tsx | 1 - app/svg/mic.tsx | 2 +- lib/CustomVideoLayout.tsx | 121 ++++++++++---------- styles/CustomControlBar.css | 1 + 7 files changed, 276 insertions(+), 65 deletions(-) create mode 100644 app/contexts/layout-context.tsx create mode 100644 app/custom/ParticipantList.tsx diff --git a/app/contexts/layout-context.tsx b/app/contexts/layout-context.tsx new file mode 100644 index 0000000..b031d46 --- /dev/null +++ b/app/contexts/layout-context.tsx @@ -0,0 +1,61 @@ +import { createContext, useContext } from 'react'; + +type LayoutContextType = { + // isSettingsOpen: SettingsContextType, + // isChatOpen: ChatContextType, + isParticipantsListOpen: ParticipantsListContextType; +}; + +export const CustomLayoutContext = createContext(undefined); + +export function useCustomLayoutContext(): LayoutContextType { + const customLayoutContext = useContext(CustomLayoutContext); + if (!customLayoutContext) { + throw Error('Tried to access LayoutContext context outside a LayoutContextProvider provider.'); + } + return customLayoutContext; +} + +interface CustomLayoutContextProviderProps { + layoutContextValue: LayoutContextType; + children: React.ReactNode; +} + +export function CustomLayoutContextProvider({ + layoutContextValue, + children, +}: CustomLayoutContextProviderProps) { + return ( + + {' '} + {children}{' '} + + ); +} + +export type SettingsAction = { + msg: 'toggle_settings'; +}; + +export type SettingsContextType = { + dispatch?: React.Dispatch; + state?: boolean; +}; + +export type ChatAction = { + msg: 'toggle_chat'; +}; + +export type ChatContextType = { + dispatch?: React.Dispatch; + state?: boolean; +}; + +export type ParticipantsListAction = { + msg: 'toggle_participants_list'; +}; + +export type ParticipantsListContextType = { + dispatch?: React.Dispatch; + state?: boolean; +}; diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx index 7b0c9ab..87055bf 100644 --- a/app/custom/CustomControlBar.tsx +++ b/app/custom/CustomControlBar.tsx @@ -9,6 +9,7 @@ import '../../styles/CustomControlBar.css'; import { CameraOffSVG, CameraOnSVG } from '../svg/camera'; import { MicOffSVG, MicOnSVG } from '../svg/mic'; import { ScreenShareOnSVG } from '../svg/screen-share'; +import { useCustomLayoutContext } from '../contexts/layout-context'; interface CustomControlBarProps { room: Room; @@ -19,6 +20,12 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) { const [recording, setRecording] = useState(false); const [participantCount, setParticipantCount] = useState(1); const { dispatch } = useLayoutContext().widget; + const { isParticipantsListOpen } = useCustomLayoutContext(); + + function ToggleParticipantsList() { + if (isParticipantsListOpen.dispatch) + isParticipantsListOpen.dispatch({ msg: 'toggle_participants_list' }); + } useEffect(() => { if (room) { @@ -74,7 +81,7 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) { {/* Participants, Settings btn */}
-
+
people {participantCount}
diff --git a/app/custom/ParticipantList.tsx b/app/custom/ParticipantList.tsx new file mode 100644 index 0000000..d7b37be --- /dev/null +++ b/app/custom/ParticipantList.tsx @@ -0,0 +1,146 @@ +import { useLayoutContext, useRoomContext } from '@livekit/components-react'; +import { Participant, RemoteParticipant } from 'livekit-client'; +import { useEffect, useState } from 'react'; +import { MicOffSVG, MicOnSVG } from '../svg/mic'; +import { CameraOffSVG, CameraOnSVG } from '../svg/camera'; +import { ScreenShareOnSVG } from '../svg/screen-share'; +import { getAvatarColor, getInitials } from '@/lib/client-utils'; +import { useCustomLayoutContext } from '../contexts/layout-context'; + +const ParticipantList = () => { + const room = useRoomContext(); + const { localParticipant } = room; + const [participants, setParticipants] = useState>({}); + const { isParticipantsListOpen } = useCustomLayoutContext(); + + function ToggleParticipantList() { + if (isParticipantsListOpen.dispatch) + isParticipantsListOpen.dispatch({ msg: 'toggle_participants_list' }); + } + + useEffect(() => { + room.on('connectionStateChanged', () => { + setParticipants({}); + room.remoteParticipants.forEach((participant) => { + setParticipants((prev) => ({ ...prev, [participant.identity]: participant })); + }); + }); + room.on('participantConnected', (participant) => { + setParticipants((prev) => ({ ...prev, [participant.identity]: participant })); + }); + room.on('participantDisconnected', (participant) => { + setParticipants((prev) => { + const { [participant.identity]: toDelete, ...rest } = prev; + return rest; + }); + }); + room.on('participantNameChanged', (name, participant) => { + if (participant instanceof RemoteParticipant) + setParticipants((prev) => ({ ...prev, [participant.identity]: participant })); + }); + return () => { + room.off('participantConnected', (participant) => { + setParticipants((prev) => ({ ...prev, [participant.identity]: participant })); + }); + room.off('participantDisconnected', (participant) => { + setParticipants((prev) => { + const { [participant.identity]: toDelete, ...rest } = prev; + return rest; + }); + }); + room.off('participantNameChanged', (name, participant) => { + if (participant instanceof RemoteParticipant) + setParticipants((prev) => ({ ...prev, [participant.identity]: participant })); + }); + }; + }, []); + return ( +
+
+
+ {room.numParticipants} + Participants +
+
+ close +
+
+ + {Object.values(participants).map((participant: RemoteParticipant) => { + return ; + })} +
+ ); +}; + +export default ParticipantList; + +interface ParticipantItemProps { + participant: Participant; +} + +const ParticipantItem: React.FC = ({ participant }) => { + const profilePictureUrl = participant.metadata + ? JSON.parse(participant.metadata).profilePictureUrl + : null; + return ( +
+
+
+ {profilePictureUrl ? ( + {participant.name} + ) : ( +
+ {getInitials(participant.name || participant.identity)} +
+ )} +
+
{participant.name}
+
+
+ {participant.isScreenShareEnabled ? : <>} + {participant.isCameraEnabled ? : } + {participant.isMicrophoneEnabled ? : } +
+
+ ); +}; diff --git a/app/rooms/[roomName]/PageClientImpl.tsx b/app/rooms/[roomName]/PageClientImpl.tsx index 8113821..46a519f 100644 --- a/app/rooms/[roomName]/PageClientImpl.tsx +++ b/app/rooms/[roomName]/PageClientImpl.tsx @@ -8,7 +8,6 @@ import { PreJoin, LiveKitRoom, RoomAudioRenderer, - VideoConference, } from '@livekit/components-react'; import { ExternalE2EEKeyProvider, diff --git a/app/svg/mic.tsx b/app/svg/mic.tsx index d544fe2..40a2d9b 100644 --- a/app/svg/mic.tsx +++ b/app/svg/mic.tsx @@ -11,7 +11,7 @@ const MicOnSVG = () => { const MicOffSVG = () => { return ( - + = ({ room, roomName }) => { - const [showChat, setShowChat] = React.useState(false); - const [showSettings, setShowSettings] = React.useState(false); + const showChat = false; + const [showSettings, setShowSettings] = useState(false); + const [showParticipantsList, setShowParticipantsList] = useState(false); const tracks = useTracks( [ @@ -23,79 +26,73 @@ export const CustomVideoLayout: React.FC = ({ room, room ); return ( - {}, - }, - widget: { - state: { - showChat, - showSettings, - unreadMessages: 0, - }, - dispatch: (action: any) => { - if ('msg' in action && action.msg === 'toggle_chat') { - setShowChat((prev) => !prev); - } - if ('msg' in action && action.msg === 'toggle_settings') { - setShowSettings((prev) => !prev); - } - }, + setShowParticipantsList((prev) => !prev), }, }} > -
{}, + }, + widget: { + state: { + showChat, + showSettings, + unreadMessages: 0, + }, + dispatch: (action: any) => { + if ('msg' in action && action.msg === 'toggle_settings') { + setShowSettings((prev) => !prev); + } + if ('msg' in action && action.msg === 'toggle_participants_list') { + setShowParticipantsList((prev) => !prev); + } + }, + }, }} >
-
- - - -
-
- - {showChat && (
- +
+ + + + +
- )} - - -
- + {showParticipantsList && } + +
+ + ); }; diff --git a/styles/CustomControlBar.css b/styles/CustomControlBar.css index d56f8f6..b77e615 100644 --- a/styles/CustomControlBar.css +++ b/styles/CustomControlBar.css @@ -157,6 +157,7 @@ align-items: center; padding: 5px 10px; gap: 4px; + cursor: pointer; } .participant-box:hover {