diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx
index 27d2389..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,
@@ -29,7 +28,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 +64,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);
@@ -93,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) {
@@ -149,6 +152,7 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
{roomName}
@@ -157,7 +161,6 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
-
people
{participantCount}
-
+
+ chat
+
{
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..bc0bcd1
--- /dev/null
+++ b/app/custom/layout/CustomVideoLayout.tsx
@@ -0,0 +1,97 @@
+import React, { useEffect } from 'react';
+import {
+ GridLayout,
+ useTracks,
+ Chat,
+ CarouselLayout,
+ usePinnedTracks,
+ useLayoutContext,
+} from '@livekit/components-react';
+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 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..2876f9f
--- /dev/null
+++ b/app/custom/layout/LayoutContextProvider.tsx
@@ -0,0 +1,84 @@
+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';
+
+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 fba7ae0..0000000
--- a/lib/CustomVideoLayout.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-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;
- roomName: string;
-}
-
-export const CustomVideoLayout: React.FC = ({ room, roomName }) => {
- const showChat = 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 (
- 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);
- }
- },
- },
- }}
- >
-
-
- {showParticipantsList &&
}
-
-
-
-
- );
-};
-
-export default CustomVideoLayout;
diff --git a/lib/ParticipantTile.tsx b/lib/ParticipantTile.tsx
index d9e8137..0e3cb8f 100644
--- a/lib/ParticipantTile.tsx
+++ b/lib/ParticipantTile.tsx
@@ -1,132 +1,188 @@
-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,
+ useMaybeLayoutContext,
+ useMaybeParticipantContext,
+ useMaybeTrackRefContext,
+ useParticipantTile,
+ ConnectionQualityIndicator,
+ FocusToggle,
} 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 ? (
-

- ) : (
-
{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 ? (
+

+ ) : (
+
{initials}
+ )}
+
+
+
+
+
+ {isMicrophoneEnabled ? (
+ <>
+ {isSpeaking ? (
+ graphic_eq
+ ) : (
+ mic
+ )}
+ >
+ ) : (
+ mic_off
+ )}
+
+ {name || identity}
+ {trackReference.source === Track.Source.ScreenShare
+ ? ' (Screen Share)'
+ : ''}
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ );
+ },
+);
diff --git a/lib/SettingsMenu.tsx b/lib/SettingsMenu.tsx
index 9e035a4..c8e1131 100644
--- a/lib/SettingsMenu.tsx
+++ b/lib/SettingsMenu.tsx
@@ -6,7 +6,6 @@ import {
useLocalParticipant,
MediaDeviceMenu,
TrackToggle,
- useRoomContext,
} from '@livekit/components-react';
import type { KrispNoiseFilterProcessor } from '@livekit/krisp-noise-filter';
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;
+}
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..1169d2e 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 {
@@ -44,14 +50,19 @@
}
.speaking-icon {
+ font-family: 'Material Symbols Outlined';
+ color: white;
background-color: #618aff;
border-radius: 50%;
}
-.speaking-icon {
- font-family: 'Material Symbols Outlined';
- color: white;
- font-size: 16px;
+.lk-carousel .avatar-container {
+ width: 60px;
+ height: 60px;
+}
+
+.lk-carousel .avatar-initials {
+ font-size: 35px;
}
.avatar-container {
@@ -93,3 +104,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;
+}