From 37d134c81c3605d683d694c7cb7456232013c14c Mon Sep 17 00:00:00 2001 From: SujithThirumalaisamy Date: Mon, 7 Apr 2025 20:39:07 +0530 Subject: [PATCH 1/5] Added Chat --- app/custom/CustomControlBar.tsx | 11 ++++- lib/CustomVideoLayout.tsx | 25 +++++++++-- styles/Chat.css | 76 +++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 styles/Chat.css diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx index 27d2389..4f2f61a 100644 --- a/app/custom/CustomControlBar.tsx +++ b/app/custom/CustomControlBar.tsx @@ -29,7 +29,7 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) { const [isRecordingRequestPending, setIsRecordingRequestPending] = useState(false); const [participantCount, setParticipantCount] = useState(1); const { dispatch } = useLayoutContext().widget; - const { isParticipantsListOpen } = useCustomLayoutContext(); + const { isParticipantsListOpen, isChatOpen } = useCustomLayoutContext(); const { toast } = useToast(); const [recordingState, setRecordingState] = useState({ recording: { isRecording: false, recorder: '' }, @@ -65,6 +65,10 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) { isParticipantsListOpen.dispatch({ msg: 'toggle_participants_list' }); } + const toggleChat = () => { + if (isChatOpen.dispatch) isChatOpen.dispatch({ msg: 'toggle_chat' }); + }; + const toggleRoomRecording = async () => { if (isRecordingRequestPending || (isRecording && !isSelfRecord)) return; setIsRecordingRequestPending(true); @@ -149,6 +153,7 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
{roomName}
@@ -157,7 +162,9 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
- +
+ chat +
= ({ room, roomName }) => { - const showChat = false; + const [showChat, setShowChat] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showParticipantsList, setShowParticipantsList] = useState(false); @@ -30,7 +31,17 @@ export const CustomVideoLayout: React.FC = ({ room, room layoutContextValue={{ isParticipantsListOpen: { state: showParticipantsList, - dispatch: () => setShowParticipantsList((prev) => !prev), + dispatch: () => { + if (showChat) setShowChat(false); + setShowParticipantsList((prev) => !prev); + }, + }, + isChatOpen: { + state: showChat, + dispatch: () => { + if (showParticipantsList) setShowParticipantsList(false); + setShowChat((prev) => !prev); + }, }, }} > @@ -50,7 +61,12 @@ export const CustomVideoLayout: React.FC = ({ room, room if ('msg' in action && action.msg === 'toggle_settings') { setShowSettings((prev) => !prev); } + if ('msg' in action && action.msg === 'toggle_chat') { + if (showParticipantsList) setShowParticipantsList(false); + setShowChat((prev) => !prev); + } if ('msg' in action && action.msg === 'toggle_participants_list') { + if (showChat) setShowChat(false); setShowParticipantsList((prev) => !prev); } }, @@ -85,10 +101,11 @@ export const CustomVideoLayout: React.FC = ({ room, room > - + {' '}
{showParticipantsList && } + {showChat && } diff --git a/styles/Chat.css b/styles/Chat.css new file mode 100644 index 0000000..b3f34b9 --- /dev/null +++ b/styles/Chat.css @@ -0,0 +1,76 @@ +.lk-chat { + display: flex; + width: 25vw; + flex-direction: column; + background-color: #151e27; + margin: 1rem 1rem 4.1rem 0; + background-color: #151e27; + border-radius: 0.5rem; + border: 0px; +} + +.lk-chat-header { + display: flex; + width: 100%; + padding: 1.25rem 1.75rem; + height: auto; + justify-content: start; + align-items: center; + font-weight: bold; + font-size: 1.05rem; +} + +.lk-close-button.lk-button.lk-chat-toggle { + color: #556171; + cursor: pointer; + background-color: transparent; + padding: 0.5rem; +} + +.lk-close-button.lk-button.lk-chat-toggle > svg { + width: 1.3rem; + height: 1.3rem; +} + +.lk-close-button.lk-button.lk-chat-toggle > svg > path { + fill: #556171; +} + +.lk-close-button.lk-button.lk-chat-toggle:hover { + background-color: transparent; +} + +.lk-list.lk-chat-messages { + margin-bottom: auto; + padding-inline: 1rem; +} + +.lk-form-control.lk-chat-form-input { + background-color: #151e27; +} + +.lk-form-control.lk-chat-form-input:focus-visible { + outline: none; +} + +.lk-button.lk-chat-form-button { + background-color: #151e27; + border: 1px solid var(--lk-border-color); +} + +.lk-button.lk-chat-form-button:hover { + background-color: #212e3a; +} + +.lk-chat-form { + display: flex; + width: 100%; +} + +.lk-chat-entry[data-lk-message-origin='local'] .lk-message-body { + background-color: #212e3a; +} + +.lk-chat-entry[data-lk-message-origin='remote'] .lk-message-body { + background-color: #3e6189; +} From e34b8a39af0c9a336672b16696a5784e65acfba0 Mon Sep 17 00:00:00 2001 From: SujithThirumalaisamy Date: Mon, 7 Apr 2025 22:54:34 +0530 Subject: [PATCH 2/5] Added custom Layout for Focus --- app/custom/ParticipantList.tsx | 48 +-- app/custom/layout/CustomVideoLayout.tsx | 99 +++++++ app/custom/layout/FocusLayout.tsx | 24 ++ app/custom/layout/LayoutContextProvider.tsx | 85 ++++++ app/rooms/[roomName]/PageClientImpl.tsx | 9 +- lib/CustomVideoLayout.tsx | 116 -------- lib/ParticipantTile.tsx | 308 ++++++++++++-------- styles/ParticipantTile.css | 103 ------- styles/participant-tile.css | 37 ++- 9 files changed, 434 insertions(+), 395 deletions(-) create mode 100644 app/custom/layout/CustomVideoLayout.tsx create mode 100644 app/custom/layout/FocusLayout.tsx create mode 100644 app/custom/layout/LayoutContextProvider.tsx delete mode 100644 lib/CustomVideoLayout.tsx delete mode 100644 styles/ParticipantTile.css diff --git a/app/custom/ParticipantList.tsx b/app/custom/ParticipantList.tsx index d7b37be..85577ff 100644 --- a/app/custom/ParticipantList.tsx +++ b/app/custom/ParticipantList.tsx @@ -1,6 +1,5 @@ -import { useLayoutContext, useRoomContext } from '@livekit/components-react'; +import { 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'; @@ -9,8 +8,7 @@ import { useCustomLayoutContext } from '../contexts/layout-context'; const ParticipantList = () => { const room = useRoomContext(); - const { localParticipant } = room; - const [participants, setParticipants] = useState>({}); + const { localParticipant, remoteParticipants } = room; const { isParticipantsListOpen } = useCustomLayoutContext(); function ToggleParticipantList() { @@ -18,42 +16,6 @@ const ParticipantList = () => { 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 (
{
- {Object.values(participants).map((participant: RemoteParticipant) => { - return ; + {[...remoteParticipants.entries()].map((participant) => { + return ; })} ); @@ -134,7 +96,7 @@ const ParticipantItem: React.FC = ({ participant }) => { )} -
{participant.name}
+
{participant.name}
{participant.isScreenShareEnabled ? : <>} diff --git a/app/custom/layout/CustomVideoLayout.tsx b/app/custom/layout/CustomVideoLayout.tsx new file mode 100644 index 0000000..5ea6f16 --- /dev/null +++ b/app/custom/layout/CustomVideoLayout.tsx @@ -0,0 +1,99 @@ +import React, { useEffect } from 'react'; +import { + GridLayout, + useTracks, + Chat, + CarouselLayout, + usePinnedTracks, + useLayoutContext, +} from '@livekit/components-react'; +import { isEqualTrackRef } from '@livekit/components-core'; +import { Track, Room } from 'livekit-client'; +import { CustomControlBar } from '@/app/custom/CustomControlBar'; +import ParticipantList from '@/app/custom/ParticipantList'; +import { ParticipantTile } from '@/lib/ParticipantTile'; +import { SettingsMenu } from '@/lib/SettingsMenu'; +import { useCustomLayoutContext } from '@/app/contexts/layout-context'; +import '@/styles/Chat.css'; +import { FocusLayout, FocusLayoutContainer } from './FocusLayout'; + +interface CustomVideoLayoutProps { + room: Room; + roomName: string; +} + +export const CustomVideoLayout: React.FC = ({ room, roomName }) => { + const { isChatOpen, isParticipantsListOpen } = useCustomLayoutContext(); + const layoutContext = useLayoutContext(); + + const tracks = useTracks( + [ + { source: Track.Source.Camera, withPlaceholder: true }, + { source: Track.Source.ScreenShare, withPlaceholder: false }, + ], + { onlySubscribed: false }, + ); + + const focusTrack = usePinnedTracks()[0]; + const carouselTracks = tracks.filter((track) => !isEqualTrackRef(track, focusTrack)); + const test = usePinnedTracks(); + + useEffect(() => { + console.log({ test }); + }, [test]); + + return ( +
+
+
+ {!focusTrack ? ( + + + + ) : ( + + + + + {focusTrack && }{' '} + + )} + {' '} +
+
+ {isParticipantsListOpen.state && } + {isChatOpen.state && } + +
+ ); +}; + +export default CustomVideoLayout; diff --git a/app/custom/layout/FocusLayout.tsx b/app/custom/layout/FocusLayout.tsx new file mode 100644 index 0000000..372849c --- /dev/null +++ b/app/custom/layout/FocusLayout.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'; +import type { ParticipantClickEvent } from '@livekit/components-core'; +import { ParticipantTile } from '@/lib/ParticipantTile'; + +export interface FocusLayoutContainerProps extends React.HTMLAttributes {} + +export function FocusLayoutContainer(props: FocusLayoutContainerProps) { + return ( +
+ {props.children} +
+ ); +} + +export interface FocusLayoutProps extends React.HTMLAttributes { + trackRef?: TrackReferenceOrPlaceholder; + + onParticipantClick?: (evt: ParticipantClickEvent) => void; +} + +export function FocusLayout({ trackRef, ...htmlProps }: FocusLayoutProps) { + return ; +} diff --git a/app/custom/layout/LayoutContextProvider.tsx b/app/custom/layout/LayoutContextProvider.tsx new file mode 100644 index 0000000..f66e215 --- /dev/null +++ b/app/custom/layout/LayoutContextProvider.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { LayoutContextProvider, TrackReferenceOrPlaceholder } from '@livekit/components-react'; +import { CustomLayoutContextProvider } from '@/app/contexts/layout-context'; +import { PinAction } from '@livekit/components-react/dist/context/pin-context'; +import { PanelTopInactive, Pi } from 'lucide-react'; + +interface CustomVideoLayoutContextProviderProps { + children: React.ReactNode; +} + +export const CustomVideoLayoutContextProvider: React.FC = ({ + children, +}) => { + const [showChat, setShowChat] = useState(false); + const [showSettings, setShowSettings] = useState(false); + const [showParticipantsList, setShowParticipantsList] = useState(false); + const [pinnedTracks, setPinnedTracks] = useState(); + + const toggleParticipantsList = () => { + if (showChat) setShowChat(false); + setShowParticipantsList((prev) => !prev); + }; + + const toggleChat = () => { + if (showParticipantsList) setShowParticipantsList(false); + setShowChat((prev) => !prev); + }; + + const toggleSettings = () => { + setShowSettings((prev) => !prev); + }; + + return ( + + { + if (action.msg === 'set_pin') { + setPinnedTracks([action.trackReference]); + } + if (action.msg === 'clear_pin') { + setPinnedTracks([]); + } + }, + }, + widget: { + state: { + showChat, + showSettings, + unreadMessages: 0, + }, + dispatch: (action: any) => { + switch (action && action.msg) { + case 'toggle_settings': + toggleSettings(); + break; + case 'toggle_chat': + toggleChat(); + break; + case 'toggle_participants_list': + toggleParticipantsList(); + break; + } + }, + }, + }} + > + {children} + + + ); +}; diff --git a/app/rooms/[roomName]/PageClientImpl.tsx b/app/rooms/[roomName]/PageClientImpl.tsx index 0f74fcb..a74e2b4 100644 --- a/app/rooms/[roomName]/PageClientImpl.tsx +++ b/app/rooms/[roomName]/PageClientImpl.tsx @@ -20,7 +20,8 @@ import { } from 'livekit-client'; import { useRouter } from 'next/navigation'; import '../../../styles/PageClientImpl.css'; -import { CustomVideoLayout } from '@/lib/CustomVideoLayout'; +import { CustomVideoLayoutContextProvider } from '@/app/custom/layout/LayoutContextProvider'; +import CustomVideoLayout from '@/app/custom/layout/CustomVideoLayout'; const CONN_DETAILS_ENDPOINT = process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT ?? '/api/connection-details'; @@ -177,8 +178,10 @@ function VideoConferenceComponent(props: { audio={props.userChoices.audioEnabled} onDisconnected={handleOnLeave} > - - + + + + ); } diff --git a/lib/CustomVideoLayout.tsx b/lib/CustomVideoLayout.tsx deleted file mode 100644 index b03a9ba..0000000 --- a/lib/CustomVideoLayout.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { useState } from 'react'; -import { GridLayout, useTracks, LayoutContextProvider, Chat } 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'; -import '../styles/Chat.css'; - -interface CustomVideoLayoutProps { - room: Room; - roomName: string; -} - -export const CustomVideoLayout: React.FC = ({ room, roomName }) => { - const [showChat, setShowChat] = useState(false); - const [showSettings, setShowSettings] = useState(false); - const [showParticipantsList, setShowParticipantsList] = useState(false); - - const tracks = useTracks( - [ - { source: Track.Source.Camera, withPlaceholder: true }, - { source: Track.Source.ScreenShare, withPlaceholder: false }, - ], - { onlySubscribed: false }, - ); - - return ( - { - if (showChat) setShowChat(false); - setShowParticipantsList((prev) => !prev); - }, - }, - isChatOpen: { - state: showChat, - dispatch: () => { - if (showParticipantsList) setShowParticipantsList(false); - setShowChat((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_chat') { - if (showParticipantsList) setShowParticipantsList(false); - setShowChat((prev) => !prev); - } - if ('msg' in action && action.msg === 'toggle_participants_list') { - if (showChat) setShowChat(false); - setShowParticipantsList((prev) => !prev); - } - }, - }, - }} - > -
-
-
- - - - {' '} -
-
- {showParticipantsList && } - {showChat && } - -
-
-
- ); -}; - -export default CustomVideoLayout; diff --git a/lib/ParticipantTile.tsx b/lib/ParticipantTile.tsx index d9e8137..745cef4 100644 --- a/lib/ParticipantTile.tsx +++ b/lib/ParticipantTile.tsx @@ -1,132 +1,192 @@ -import React, { useEffect, useState } from 'react'; +import * as React from 'react'; +import type { Participant } from 'livekit-client'; +import { Track } from 'livekit-client'; +import type { ParticipantClickEvent, TrackReferenceOrPlaceholder } from '@livekit/components-core'; +import { isTrackReference, isTrackReferencePinned } from '@livekit/components-core'; import { AudioTrack, - useTracks, VideoTrack, - useTrackRefContext, + ParticipantContext, + TrackRefContext, useEnsureTrackRef, - TrackRefContextIfNeeded, + useFeatureContext, + useIsEncrypted, + useMaybeLayoutContext, + useMaybeParticipantContext, + useMaybeTrackRefContext, + useParticipantTile, + ParticipantPlaceholder, + LockLockedIcon, + TrackMutedIndicator, + ParticipantName, + ConnectionQualityIndicator, + FocusToggle, + useIsSpeaking, + useIsMuted, + useTrackByName, + useTracks, } from '@livekit/components-react'; -import { Track, Participant } from 'livekit-client'; -import { isTrackReference } from '@livekit/components-core'; +import { getAvatarColor, getInitials } from './client-utils'; -function getAvatarColor(identity: string): string { - const colors = [ - '#4CAF50', - '#8BC34A', - '#CDDC39', - '#FFC107', - '#FF9800', - '#FF5722', - '#F44336', - '#E91E63', - '#9C27B0', - '#673AB7', - '#3F51B5', - '#2196F3', - '#03A9F4', - '#00BCD4', - '#009688', - ]; - - let hash = 0; - for (let i = 0; i < identity.length; i++) { - hash = identity.charCodeAt(i) + ((hash << 5) - hash); - } - - const index = Math.abs(hash) % colors.length; - return colors[index]; -} - -function getInitials(name: string): string { - if (!name) return '?'; - - const parts = name.split(' '); - if (parts.length === 1) { - return parts[0].charAt(0).toUpperCase(); - } - - return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase(); -} - -export interface ParticipantTileProps { - participant?: Participant; -} - -export const ParticipantTile: React.FC = ({ - participant: propParticipant, -}) => { - const trackRef = useTrackRefContext(); - const trackReference = useEnsureTrackRef(trackRef); - const participant = propParticipant || trackRef?.participant; - - if (!participant) return null; - - const [profilePictureUrl, setProfilePictureUrl] = useState(null); - - const microphoneTrack = useTracks([Track.Source.Microphone], { onlySubscribed: false }).filter( - (track) => track.participant.identity === participant.identity, - )[0]; - - const isSpeaking = participant.isSpeaking; - - useEffect(() => { - if (participant.metadata) { - try { - const metadata = JSON.parse(participant.metadata); - if (metadata.profilePictureUrl) { - setProfilePictureUrl(metadata.profilePictureUrl); - } - } catch (e) { - console.error('Failed to parse participant metadata', e); - } - } - }, [participant.metadata]); - - const isCameraEnabled = - (trackReference.source === Track.Source.Camera && !trackReference.publication?.isMuted) || - trackReference.source === Track.Source.ScreenShare; - - const hasMicrophone = !!microphoneTrack; - const isMicrophoneEnabled = hasMicrophone && !microphoneTrack.publication?.isMuted; - - const avatarColor = getAvatarColor(participant.identity); - const initials = getInitials(participant.name || participant.identity); - - return ( -
- {isTrackReference(trackReference) && isCameraEnabled ? ( -
- -
- ) : ( -
- {profilePictureUrl ? ( - {participant.name} - ) : ( - {initials} - )} -
- )} - -
- {isMicrophoneEnabled ? ( - <> - {isSpeaking ? ( - graphic_eq - ) : ( - mic - )} - - ) : ( - mic_off - )} - {participant.name || participant.identity} -
- - {hasMicrophone && microphoneTrack && } -
+export function ParticipantContextIfNeeded( + props: React.PropsWithChildren<{ + participant?: Participant; + }>, +) { + const hasContext = !!useMaybeParticipantContext(); + return props.participant && !hasContext ? ( + + {props.children} + + ) : ( + <>{props.children} ); -}; +} -export default ParticipantTile; +export function TrackRefContextIfNeeded( + props: React.PropsWithChildren<{ + trackRef?: TrackReferenceOrPlaceholder; + }>, +) { + const hasContext = !!useMaybeTrackRefContext(); + return props.trackRef && !hasContext ? ( + {props.children} + ) : ( + <>{props.children} + ); +} + +export interface ParticipantTileProps extends React.HTMLAttributes { + trackRef?: TrackReferenceOrPlaceholder; + disableSpeakingIndicator?: boolean; + + onParticipantClick?: (event: ParticipantClickEvent) => void; +} + +export const ParticipantTile: ( + props: ParticipantTileProps & React.RefAttributes, +) => React.ReactNode = React.forwardRef( + function ParticipantTile( + { + trackRef, + children, + onParticipantClick, + disableSpeakingIndicator, + ...htmlProps + }: ParticipantTileProps, + ref, + ) { + const trackReference = useEnsureTrackRef(trackRef); + const { + name, + identity, + metadata, + isEncrypted, + isSpeaking, + isMicrophoneEnabled, + isScreenShareEnabled, + } = trackReference.participant; + + const { elementProps } = useParticipantTile({ + htmlProps, + disableSpeakingIndicator, + onParticipantClick, + trackRef: trackReference, + }); + const layoutContext = useMaybeLayoutContext(); + + const autoManageSubscription = useFeatureContext()?.autoSubscription; + + const handleSubscribe = React.useCallback( + (subscribed: boolean) => { + if ( + trackReference.source && + !subscribed && + layoutContext && + layoutContext.pin.dispatch && + isTrackReferencePinned(trackReference, layoutContext.pin.state) + ) { + layoutContext.pin.dispatch({ msg: 'clear_pin' }); + } + }, + [trackReference, layoutContext], + ); + + const [profilePictureUrl, setProfilePictureUrl] = React.useState(null); + + React.useEffect(() => { + if (metadata) { + try { + const parsedMetadata = JSON.parse(metadata); + if (parsedMetadata.profilePictureUrl) { + setProfilePictureUrl(parsedMetadata.profilePictureUrl); + } + } catch (e) { + console.error('Failed to parse participant metadata', e); + } + } + }, [metadata]); + + const avatarColor = getAvatarColor(identity); + const initials = getInitials(name || identity); + + return ( +
+ + + {children ?? ( + <> + {isTrackReference(trackReference) && + (trackReference.publication?.kind === 'video' || + trackReference.source === Track.Source.Camera || + trackReference.source === Track.Source.ScreenShare) ? ( + + ) : ( + isTrackReference(trackReference) && ( + + ) + )} +
+
+ {profilePictureUrl ? ( + {name} + ) : ( + {initials} + )} +
+
+
+
+
+ {isMicrophoneEnabled ? ( + <> + {isSpeaking ? ( + graphic_eq + ) : ( + mic + )} + + ) : ( + mic_off + )} + {name || identity} +
+
+ +
+ + )} + +
+
+
+ ); + }, +); diff --git a/styles/ParticipantTile.css b/styles/ParticipantTile.css deleted file mode 100644 index 0a17ac7..0000000 --- a/styles/ParticipantTile.css +++ /dev/null @@ -1,103 +0,0 @@ -.participant-tile { - position: relative; - background-color: #1a242e; - border-radius: 5px; - overflow: hidden; - width: 100%; - height: 100%; -} - -.participant-tile.speaking { - border: 2px solid #618aff; -} - -.participant-info { - position: absolute; - bottom: 0; - left: 0; - display: flex; - align-items: center; - padding: 8px; - z-index: 10; -} - -.participant-name { - font-family: 'Roboto', sans-serif; - font-weight: 500; - font-size: 14px; - color: white; - margin-left: 5px; -} - -.mic-icon { - font-family: 'Material Symbols Outlined'; - font-size: 18px; -} - -.mic-on { - color: #ffffff; -} - -.mic-off { - color: #ff5252; -} - -.speaking-indicator { - position: absolute; - top: 10px; - right: 10px; - background-color: #618aff; - border-radius: 50%; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - z-index: 10; -} - -.speaking-icon { - font-family: 'Material Symbols Outlined'; - color: white; - font-size: 16px; -} - -.avatar-container { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 100px; - height: 100px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - z-index: 5; -} - -.avatar-initials { - font-family: 'Roboto', sans-serif; - font-weight: 500; - font-size: 50px; - color: white; -} - -.avatar-image { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; -} - -.video-container { - width: 100%; - height: 100%; -} - -.custom-control-bar.lk-control-bar { - padding: 6px !important; - border-top: 1px solid rgba(255, 255, 255, 0.1); - margin-right: -10px; - width: calc(100% + 10px); -} diff --git a/styles/participant-tile.css b/styles/participant-tile.css index af7d169..15187c1 100644 --- a/styles/participant-tile.css +++ b/styles/participant-tile.css @@ -33,6 +33,12 @@ font-family: 'Material Symbols Outlined'; font-size: 18px; padding: 0.1rem 0.3rem; + width: 1.5rem; + height: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + margin-right: 0.25rem; } .mic-on { @@ -43,15 +49,11 @@ color: #ff5252; } -.speaking-icon { - background-color: #618aff; - border-radius: 50%; -} - .speaking-icon { font-family: 'Material Symbols Outlined'; color: white; - font-size: 16px; + background-color: #618aff; + border-radius: 50%; } .avatar-container { @@ -93,3 +95,26 @@ margin-right: -10px; width: calc(100% + 10px); } + +.focus-toggle { + width: 2rem; + background-color: #151e27; + position: absolute; + right: 0.5rem; + top: 0.5rem; + z-index: 10; +} + +.lk-participant-tile .lk-participant-placeholder { + background-color: #1a242e; +} + +.lk-participant-metadata-item, +.lk-participant-tile .lk-focus-toggle-button, +.lk-participant-tile .lk-focus-toggle-button:hover { + background-color: #151e27; +} + +.data-lk-quality { + top: 0; +} From 89567b4e719fff29cc5a660358cd5139a2dc6708 Mon Sep 17 00:00:00 2001 From: SujithThirumalaisamy Date: Mon, 7 Apr 2025 23:08:01 +0530 Subject: [PATCH 3/5] Moved chat icon and fixed track render --- app/custom/CustomControlBar.tsx | 7 +++---- app/custom/layout/CustomVideoLayout.tsx | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx index 4f2f61a..1519ef2 100644 --- a/app/custom/CustomControlBar.tsx +++ b/app/custom/CustomControlBar.tsx @@ -162,9 +162,6 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
-
- chat -
people {participantCount}
- +
+ chat +
{ diff --git a/app/custom/layout/CustomVideoLayout.tsx b/app/custom/layout/CustomVideoLayout.tsx index 5ea6f16..405b58b 100644 --- a/app/custom/layout/CustomVideoLayout.tsx +++ b/app/custom/layout/CustomVideoLayout.tsx @@ -35,7 +35,6 @@ export const CustomVideoLayout: React.FC = ({ room, room ); const focusTrack = usePinnedTracks()[0]; - const carouselTracks = tracks.filter((track) => !isEqualTrackRef(track, focusTrack)); const test = usePinnedTracks(); useEffect(() => { @@ -80,7 +79,7 @@ export const CustomVideoLayout: React.FC = ({ room, room padding: '1rem 1rem 0.5rem 1rem', }} > - + {focusTrack && }{' '} From a585842062c64d2c9bc8bb1d8cab5a9fcc1eece2 Mon Sep 17 00:00:00 2001 From: SujithThirumalaisamy Date: Mon, 7 Apr 2025 23:32:53 +0530 Subject: [PATCH 4/5] Fixed Font Size --- lib/ParticipantTile.tsx | 7 ++++++- styles/participant-tile.css | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/ParticipantTile.tsx b/lib/ParticipantTile.tsx index 745cef4..0b62e8b 100644 --- a/lib/ParticipantTile.tsx +++ b/lib/ParticipantTile.tsx @@ -176,7 +176,12 @@ export const ParticipantTile: ( ) : ( mic_off )} - {name || identity} + + {name || identity} + {trackReference.source === Track.Source.ScreenShare + ? ' (Screen Share)' + : ''} +
diff --git a/styles/participant-tile.css b/styles/participant-tile.css index 15187c1..1169d2e 100644 --- a/styles/participant-tile.css +++ b/styles/participant-tile.css @@ -56,6 +56,15 @@ border-radius: 50%; } +.lk-carousel .avatar-container { + width: 60px; + height: 60px; +} + +.lk-carousel .avatar-initials { + font-size: 35px; +} + .avatar-container { position: absolute; top: 50%; From d4a91a32cc67928d4fd24083427843cc56d6e8cb Mon Sep 17 00:00:00 2001 From: SujithThirumalaisamy Date: Mon, 7 Apr 2025 23:34:11 +0530 Subject: [PATCH 5/5] Removed unused imports --- app/custom/CustomControlBar.tsx | 3 +-- app/custom/layout/CustomVideoLayout.tsx | 1 - app/custom/layout/LayoutContextProvider.tsx | 1 - lib/ParticipantTile.tsx | 15 +++------------ lib/SettingsMenu.tsx | 1 - 5 files changed, 4 insertions(+), 17 deletions(-) diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx index 1519ef2..ddca0ca 100644 --- a/app/custom/CustomControlBar.tsx +++ b/app/custom/CustomControlBar.tsx @@ -3,7 +3,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import { DisconnectButton, - useIsRecording, useLayoutContext, useLocalParticipant, useRoomContext, @@ -97,7 +96,7 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) { } else { response = await fetch( recordingEndpoint + - `/start?roomName=${room.name}&now=${now}&identity=${localParticipant.identity}`, + `/start?roomName=${room.name}&now=${now}&identity=${localParticipant.identity}`, ); } if (response.ok) { diff --git a/app/custom/layout/CustomVideoLayout.tsx b/app/custom/layout/CustomVideoLayout.tsx index 405b58b..bc0bcd1 100644 --- a/app/custom/layout/CustomVideoLayout.tsx +++ b/app/custom/layout/CustomVideoLayout.tsx @@ -7,7 +7,6 @@ import { usePinnedTracks, useLayoutContext, } from '@livekit/components-react'; -import { isEqualTrackRef } from '@livekit/components-core'; import { Track, Room } from 'livekit-client'; import { CustomControlBar } from '@/app/custom/CustomControlBar'; import ParticipantList from '@/app/custom/ParticipantList'; diff --git a/app/custom/layout/LayoutContextProvider.tsx b/app/custom/layout/LayoutContextProvider.tsx index f66e215..2876f9f 100644 --- a/app/custom/layout/LayoutContextProvider.tsx +++ b/app/custom/layout/LayoutContextProvider.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { LayoutContextProvider, TrackReferenceOrPlaceholder } from '@livekit/components-react'; import { CustomLayoutContextProvider } from '@/app/contexts/layout-context'; import { PinAction } from '@livekit/components-react/dist/context/pin-context'; -import { PanelTopInactive, Pi } from 'lucide-react'; interface CustomVideoLayoutContextProviderProps { children: React.ReactNode; diff --git a/lib/ParticipantTile.tsx b/lib/ParticipantTile.tsx index 0b62e8b..0e3cb8f 100644 --- a/lib/ParticipantTile.tsx +++ b/lib/ParticipantTile.tsx @@ -10,21 +10,12 @@ import { TrackRefContext, useEnsureTrackRef, useFeatureContext, - useIsEncrypted, useMaybeLayoutContext, useMaybeParticipantContext, useMaybeTrackRefContext, useParticipantTile, - ParticipantPlaceholder, - LockLockedIcon, - TrackMutedIndicator, - ParticipantName, ConnectionQualityIndicator, FocusToggle, - useIsSpeaking, - useIsMuted, - useTrackByName, - useTracks, } from '@livekit/components-react'; import { getAvatarColor, getInitials } from './client-utils'; @@ -137,9 +128,9 @@ export const ParticipantTile: ( {children ?? ( <> {isTrackReference(trackReference) && - (trackReference.publication?.kind === 'video' || - trackReference.source === Track.Source.Camera || - trackReference.source === Track.Source.ScreenShare) ? ( + (trackReference.publication?.kind === 'video' || + trackReference.source === Track.Source.Camera || + trackReference.source === Track.Source.ScreenShare) ? (