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 {} - } -}