Merge pull request #13 from SujithThirumalaisamy/participant/list
Added participants List
This commit is contained in:
commit
71eecfd5ff
61
app/contexts/layout-context.tsx
Normal file
61
app/contexts/layout-context.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
type LayoutContextType = {
|
||||
// isSettingsOpen: SettingsContextType,
|
||||
// isChatOpen: ChatContextType,
|
||||
isParticipantsListOpen: ParticipantsListContextType;
|
||||
};
|
||||
|
||||
export const CustomLayoutContext = createContext<LayoutContextType | undefined>(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 (
|
||||
<CustomLayoutContext.Provider value={layoutContextValue}>
|
||||
{' '}
|
||||
{children}{' '}
|
||||
</CustomLayoutContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export type SettingsAction = {
|
||||
msg: 'toggle_settings';
|
||||
};
|
||||
|
||||
export type SettingsContextType = {
|
||||
dispatch?: React.Dispatch<SettingsAction>;
|
||||
state?: boolean;
|
||||
};
|
||||
|
||||
export type ChatAction = {
|
||||
msg: 'toggle_chat';
|
||||
};
|
||||
|
||||
export type ChatContextType = {
|
||||
dispatch?: React.Dispatch<ChatAction>;
|
||||
state?: boolean;
|
||||
};
|
||||
|
||||
export type ParticipantsListAction = {
|
||||
msg: 'toggle_participants_list';
|
||||
};
|
||||
|
||||
export type ParticipantsListContextType = {
|
||||
dispatch?: React.Dispatch<ParticipantsListAction>;
|
||||
state?: boolean;
|
||||
};
|
||||
@ -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 */}
|
||||
<div className="top-right-controls">
|
||||
<div className="participant-box">
|
||||
<div className="participant-box" onClick={ToggleParticipantsList}>
|
||||
<span className="material-symbols-outlined">people</span>
|
||||
<span className="participant-count">{participantCount}</span>
|
||||
</div>
|
||||
|
||||
146
app/custom/ParticipantList.tsx
Normal file
146
app/custom/ParticipantList.tsx
Normal file
@ -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<Record<string, RemoteParticipant>>({});
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '25vw',
|
||||
margin: '1rem 1rem 4.1rem 0',
|
||||
padding: '1.25rem 1.75rem',
|
||||
backgroundColor: '#151E27',
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '1.75rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
fontSize: '1.05rem',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
<span>{room.numParticipants}</span>
|
||||
<span>Participants</span>
|
||||
</div>
|
||||
<div
|
||||
className="material-symbols-outlined"
|
||||
style={{ color: '#556171', cursor: 'pointer' }}
|
||||
onClick={ToggleParticipantList}
|
||||
>
|
||||
close
|
||||
</div>
|
||||
</div>
|
||||
<ParticipantItem participant={localParticipant} />
|
||||
{Object.values(participants).map((participant: RemoteParticipant) => {
|
||||
return <ParticipantItem participant={participant} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticipantList;
|
||||
|
||||
interface ParticipantItemProps {
|
||||
participant: Participant;
|
||||
}
|
||||
|
||||
const ParticipantItem: React.FC<ParticipantItemProps> = ({ participant }) => {
|
||||
const profilePictureUrl = participant.metadata
|
||||
? JSON.parse(participant.metadata).profilePictureUrl
|
||||
: null;
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%', marginBottom: '1.25rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginRight: 'auto' }}>
|
||||
<div>
|
||||
{profilePictureUrl ? (
|
||||
<img src={profilePictureUrl} alt={participant.name} className="avatar-image" />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '2rem',
|
||||
height: '2rem',
|
||||
backgroundColor: getAvatarColor(participant.identity),
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
>
|
||||
{getInitials(participant.name || participant.identity)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>{participant.name}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
{participant.isScreenShareEnabled ? <ScreenShareOnSVG /> : <></>}
|
||||
{participant.isCameraEnabled ? <CameraOnSVG /> : <CameraOffSVG />}
|
||||
{participant.isMicrophoneEnabled ? <MicOnSVG /> : <MicOffSVG />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -8,7 +8,6 @@ import {
|
||||
PreJoin,
|
||||
LiveKitRoom,
|
||||
RoomAudioRenderer,
|
||||
VideoConference,
|
||||
} from '@livekit/components-react';
|
||||
import {
|
||||
ExternalE2EEKeyProvider,
|
||||
|
||||
@ -11,7 +11,7 @@ const MicOnSVG = () => {
|
||||
|
||||
const MicOffSVG = () => {
|
||||
return (
|
||||
<svg width="45" height="45" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="25" height="25" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask
|
||||
id="mask0_5659_3520"
|
||||
style={{ maskType: 'alpha' }}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { GridLayout, useTracks, LayoutContextProvider, Chat } from '@livekit/components-react';
|
||||
import React, { useState } from 'react';
|
||||
import { GridLayout, useTracks, LayoutContextProvider } from '@livekit/components-react';
|
||||
import { Track, Room } from 'livekit-client';
|
||||
import { ParticipantTile } from './ParticipantTile';
|
||||
import { CustomControlBar } from '@/app/custom/CustomControlBar';
|
||||
import { SettingsMenu } from './SettingsMenu';
|
||||
import ParticipantList from '@/app/custom/ParticipantList';
|
||||
import { CustomLayoutContextProvider } from '@/app/contexts/layout-context';
|
||||
|
||||
interface CustomVideoLayoutProps {
|
||||
room: Room;
|
||||
@ -11,8 +13,9 @@ interface CustomVideoLayoutProps {
|
||||
}
|
||||
|
||||
export const CustomVideoLayout: React.FC<CustomVideoLayoutProps> = ({ 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<CustomVideoLayoutProps> = ({ room, room
|
||||
);
|
||||
|
||||
return (
|
||||
<LayoutContextProvider
|
||||
value={{
|
||||
pin: {
|
||||
state: [],
|
||||
dispatch: () => {},
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
<CustomLayoutContextProvider
|
||||
layoutContextValue={{
|
||||
isParticipantsListOpen: {
|
||||
state: showParticipantsList,
|
||||
dispatch: () => setShowParticipantsList((prev) => !prev),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
position: 'relative',
|
||||
backgroundColor: '#070707',
|
||||
<LayoutContextProvider
|
||||
value={{
|
||||
pin: {
|
||||
state: [],
|
||||
dispatch: () => {},
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexDirection: 'row',
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
position: 'relative',
|
||||
backgroundColor: '#070707',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<GridLayout
|
||||
tracks={tracks}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '1rem 1rem 0.5rem 1rem',
|
||||
}}
|
||||
>
|
||||
<ParticipantTile />
|
||||
</GridLayout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showChat && (
|
||||
<div
|
||||
className="lk-chat-container"
|
||||
style={{
|
||||
width: '470px',
|
||||
borderLeft: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Chat
|
||||
style={{
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<GridLayout
|
||||
tracks={tracks}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '1rem 1rem 0.5rem 1rem',
|
||||
}}
|
||||
>
|
||||
<ParticipantTile />
|
||||
</GridLayout>
|
||||
<CustomControlBar room={room} roomName={roomName} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CustomControlBar room={room} roomName={roomName} />
|
||||
<SettingsMenu showSettings={showSettings} />
|
||||
</div>
|
||||
</LayoutContextProvider>
|
||||
{showParticipantsList && <ParticipantList />}
|
||||
<SettingsMenu showSettings={showSettings} />
|
||||
</div>
|
||||
</LayoutContextProvider>
|
||||
</CustomLayoutContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -157,6 +157,7 @@
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.participant-box:hover {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user