From 199e0908e9f719da5b7658ad68ba92b3ccf0f057 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 1 Aug 2025 10:48:58 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20refactor=20raise?= =?UTF-8?q?d=20hand=20to=20rely=20on=20participant's=20attributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously managed participant hand raised using raw metadata. LiveKit introduced attributes concept, saves as metadata under hood but makes update/handling easier. Still pain that attributes must be strings, cannot pass boolean… Refactor whole app to use attributes instead of metadata for the raised hand feature. This commit might introduce regressions. I've double checked participant list side pannel and the notification features. Previously I persisted a boolean, I now persist the timestamp at which the hand was raised. This will be useful in upcoming commits, especially for sorting raised hands by order of arrival. --- .../notifications/MainNotificationToast.tsx | 19 +++++++------- .../rooms/livekit/api/lowerHandParticipant.ts | 11 +++++--- .../Participants/ParticipantsList.tsx | 4 +-- .../rooms/livekit/hooks/useRaisedHand.ts | 25 +++++++++++-------- .../rooms/utils/safeParseMetadata.tsx | 22 ---------------- 5 files changed, 32 insertions(+), 49 deletions(-) delete mode 100644 src/frontend/src/features/rooms/utils/safeParseMetadata.tsx diff --git a/src/frontend/src/features/notifications/MainNotificationToast.tsx b/src/frontend/src/features/notifications/MainNotificationToast.tsx index de13b22d..4d08d052 100644 --- a/src/frontend/src/features/notifications/MainNotificationToast.tsx +++ b/src/frontend/src/features/notifications/MainNotificationToast.tsx @@ -18,7 +18,6 @@ import { ANIMATION_DURATION, ReactionPortals, } from '@/features/rooms/livekit/components/ReactionPortal' -import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata' export const MainNotificationToast = () => { const room = useRoomContext() @@ -155,17 +154,14 @@ export const MainNotificationToast = () => { useEffect(() => { const handleNotificationReceived = ( - prevMetadataStr: string | undefined, + changedAttributes: Record, participant: Participant ) => { if (!participant) return if (isMobileBrowser()) return if (participant.isLocal) return - const prevMetadata = safeParseMetadata(prevMetadataStr) - const metadata = safeParseMetadata(participant.metadata) - - if (prevMetadata?.raised == metadata?.raised) return + if (!('handRaisedAt' in changedAttributes)) return const existingToast = toastQueue.visibleToasts.find( (toast) => @@ -173,12 +169,12 @@ export const MainNotificationToast = () => { toast.content.type === NotificationType.HandRaised ) - if (existingToast && prevMetadata.raised && !metadata.raised) { + if (existingToast && !changedAttributes?.handRaisedAt) { toastQueue.close(existingToast.key) return } - if (!existingToast && !prevMetadata.raised && metadata.raised) { + if (!existingToast && !!changedAttributes?.handRaisedAt) { triggerNotificationSound(NotificationType.HandRaised) toastQueue.add( { @@ -190,10 +186,13 @@ export const MainNotificationToast = () => { } } - room.on(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived) + room.on(RoomEvent.ParticipantAttributesChanged, handleNotificationReceived) return () => { - room.off(RoomEvent.ParticipantMetadataChanged, handleNotificationReceived) + room.off( + RoomEvent.ParticipantAttributesChanged, + handleNotificationReceived + ) } }, [room, triggerNotificationSound]) diff --git a/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts index 7c9be819..13b9691d 100644 --- a/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts +++ b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts @@ -2,7 +2,6 @@ import { Participant } from 'livekit-client' import { fetchServerApi } from './fetchServerApi' import { buildServerApiUrl } from './buildServerApiUrl' import { useRoomData } from '../hooks/useRoomData' -import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata' export const useLowerHandParticipant = () => { const data = useRoomData() @@ -11,8 +10,12 @@ export const useLowerHandParticipant = () => { if (!data || !data?.livekit) { throw new Error('Room data is not available') } - const newMetadata = safeParseMetadata(participant.metadata) || {} - newMetadata.raised = !newMetadata.raised + + const newAttributes = { + ...participant.attributes, + handRaisedAt: '', + } + return fetchServerApi( buildServerApiUrl( data.livekit.url, @@ -24,7 +27,7 @@ export const useLowerHandParticipant = () => { body: JSON.stringify({ room: data.livekit.room, identity: participant.identity, - metadata: JSON.stringify(newMetadata), + attributes: newAttributes, permission: participant.permissions, }), } diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx index 7792e9b5..c34c171e 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx @@ -11,7 +11,6 @@ import { WaitingParticipantListItem } from './WaitingParticipantListItem' import { useWaitingParticipants } from '@/features/rooms/hooks/useWaitingParticipants' import { Participant } from 'livekit-client' import { WaitingParticipant } from '@/features/rooms/api/listWaitingParticipants' -import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata' // TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short. export const ParticipantsList = () => { @@ -36,8 +35,7 @@ export const ParticipantsList = () => { ] const raisedHandParticipants = participants.filter((participant) => { - const data = safeParseMetadata(participant.metadata) - return data.raised + return !!participant.attributes.handRaisedAt }) const { waitingParticipants, handleParticipantEntry } = diff --git a/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.ts b/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.ts index d54921b7..3fd89cf1 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.ts @@ -1,5 +1,5 @@ import { LocalParticipant, Participant } from 'livekit-client' -import { useParticipantInfo } from '@livekit/components-react' +import { useParticipantAttribute } from '@livekit/components-react' import { isLocal } from '@/utils/livekit' type useRaisedHandProps = { @@ -7,17 +7,22 @@ type useRaisedHandProps = { } export function useRaisedHand({ participant }: useRaisedHandProps) { - // fixme - refactor this part to rely on attributes - const { metadata } = useParticipantInfo({ participant }) - const parsedMetadata = JSON.parse(metadata || '{}') + const handRaisedAtAttribute = useParticipantAttribute('handRaisedAt', { + participant, + }) - const toggleRaisedHand = () => { - if (isLocal(participant)) { - parsedMetadata.raised = !parsedMetadata.raised - const localParticipant = participant as LocalParticipant - localParticipant.setMetadata(JSON.stringify(parsedMetadata)) + const isHandRaised = !!handRaisedAtAttribute + + const toggleRaisedHand = async () => { + if (!isLocal(participant)) return + const localParticipant = participant as LocalParticipant + + const attributes: Record = { + handRaisedAt: !isHandRaised ? new Date().toISOString() : '', } + + await localParticipant.setAttributes(attributes) } - return { isHandRaised: parsedMetadata.raised ?? false, toggleRaisedHand } + return { isHandRaised, toggleRaisedHand } } diff --git a/src/frontend/src/features/rooms/utils/safeParseMetadata.tsx b/src/frontend/src/features/rooms/utils/safeParseMetadata.tsx deleted file mode 100644 index f7d74269..00000000 --- a/src/frontend/src/features/rooms/utils/safeParseMetadata.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export const safeParseMetadata = ( - metadataStr: string | null | undefined -): Record => { - if (!metadataStr) { - return {} - } - - try { - const parsed = JSON.parse(metadataStr) - - // Ensure the result is an object - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - console.warn('Metadata parsed to non-object value:', parsed) - return {} - } - - return parsed as Record - } catch (error) { - console.error('Failed to parse metadata:', error) - return {} - } -}