diff --git a/src/frontend/src/features/notifications/MainNotificationToast.tsx b/src/frontend/src/features/notifications/MainNotificationToast.tsx index ac1c00d7..cd664160 100644 --- a/src/frontend/src/features/notifications/MainNotificationToast.tsx +++ b/src/frontend/src/features/notifications/MainNotificationToast.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useRoomContext } from '@livekit/components-react' -import { Participant, RoomEvent } from 'livekit-client' +import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client' import { ToastProvider, toastQueue } from './components/ToastProvider' import { NotificationType } from './NotificationType' import { Div } from '@/primitives' @@ -27,6 +27,45 @@ export const MainNotificationToast = () => { } }, [room]) + // fixme - close all related toasters when hands are lowered remotely + useEffect(() => { + const decoder = new TextDecoder() + + const handleNotificationReceived = ( + payload: Uint8Array, + participant?: RemoteParticipant + ) => { + if (!participant) { + return + } + const notification = decoder.decode(payload) + const existingToast = toastQueue.visibleToasts.find( + (toast) => + toast.content.participant === participant && + toast.content.type === NotificationType.Raised + ) + if (existingToast && notification === NotificationType.Lowered) { + toastQueue.close(existingToast.key) + return + } + if (!existingToast && notification === NotificationType.Raised) { + toastQueue.add( + { + participant, + type: NotificationType.Raised, + }, + { timeout: 5000 } + ) + } + } + + room.on(RoomEvent.DataReceived, handleNotificationReceived) + + return () => { + room.off(RoomEvent.DataReceived, handleNotificationReceived) + } + }, [room]) + return (
diff --git a/src/frontend/src/features/notifications/NotificationType.ts b/src/frontend/src/features/notifications/NotificationType.ts index 2f965016..6b1570f9 100644 --- a/src/frontend/src/features/notifications/NotificationType.ts +++ b/src/frontend/src/features/notifications/NotificationType.ts @@ -1,4 +1,6 @@ export enum NotificationType { Joined = 'joined', Default = 'default', + Raised = 'raised', + Lowered = 'lowered', } diff --git a/src/frontend/src/features/notifications/components/ToastRaised.tsx b/src/frontend/src/features/notifications/components/ToastRaised.tsx new file mode 100644 index 00000000..1479d959 --- /dev/null +++ b/src/frontend/src/features/notifications/components/ToastRaised.tsx @@ -0,0 +1,66 @@ +import { useToast } from '@react-aria/toast' +import { useRef } from 'react' + +import { StyledToastContainer, ToastProps } from './Toast' +import { HStack } from '@/styled-system/jsx' +import { Button, Div } from '@/primitives' +import { useTranslation } from 'react-i18next' +import { RiCloseLine, RiHand } from '@remixicon/react' +import { useWidgetInteraction } from '@/features/rooms/livekit/hooks/useWidgetInteraction' + +export function ToastRaised({ state, ...props }: ToastProps) { + const { t } = useTranslation('notifications') + const ref = useRef(null) + const { toastProps, contentProps, titleProps, closeButtonProps } = useToast( + props, + state, + ref + ) + const participant = props.toast.content.participant + const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction() + + return ( + + + +
+ {t('raised.description', { + name: participant.name || t('defaultName'), + })} +
+ {!isParticipantsOpen && ( + + )} + +
+
+ ) +} diff --git a/src/frontend/src/features/notifications/components/ToastRegion.tsx b/src/frontend/src/features/notifications/components/ToastRegion.tsx index b27ffc1c..0c0b4ab2 100644 --- a/src/frontend/src/features/notifications/components/ToastRegion.tsx +++ b/src/frontend/src/features/notifications/components/ToastRegion.tsx @@ -5,6 +5,7 @@ import { useRef } from 'react' import { NotificationType } from '../NotificationType' import { ToastJoined } from './ToastJoined' import { ToastData } from './ToastProvider' +import { ToastRaised } from '@/features/notifications/components/ToastRaised.tsx' interface ToastRegionProps extends AriaToastRegionProps { state: ToastState @@ -19,6 +20,9 @@ export function ToastRegion({ state, ...props }: ToastRegionProps) { if (toast.content?.type === NotificationType.Joined) { return } + if (toast.content?.type === NotificationType.Raised) { + return + } return })}
diff --git a/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx index db917c4c..ce55e079 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx @@ -2,21 +2,33 @@ import { useTranslation } from 'react-i18next' import { RiHand } from '@remixicon/react' import { ToggleButton } from '@/primitives' import { css } from '@/styled-system/css' -import { useLocalParticipant } from '@livekit/components-react' +import { useRoomContext } from '@livekit/components-react' import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand' +import { NotificationType } from '@/features/notifications/NotificationType' export const HandToggle = () => { const { t } = useTranslation('rooms') - const localParticipant = useLocalParticipant().localParticipant + const room = useRoomContext() const { isHandRaised, toggleRaisedHand } = useRaisedHand({ - participant: localParticipant, + participant: room.localParticipant, }) const label = isHandRaised ? t('controls.hand.lower') : t('controls.hand.raise') + const notifyOtherParticipants = (isHandRaised: boolean) => { + room.localParticipant.publishData( + new TextEncoder().encode( + !isHandRaised ? NotificationType.Raised : NotificationType.Lowered + ), + { + reliable: true, + } + ) + } + return (
{ aria-label={label} tooltip={label} isSelected={isHandRaised} - onPress={() => toggleRaisedHand()} + onPress={() => { + notifyOtherParticipants(isHandRaised) + toggleRaisedHand() + }} > diff --git a/src/frontend/src/locales/de/notifications.json b/src/frontend/src/locales/de/notifications.json index a173f1ed..788b95c8 100644 --- a/src/frontend/src/locales/de/notifications.json +++ b/src/frontend/src/locales/de/notifications.json @@ -2,5 +2,9 @@ "defaultName": "", "joined": { "description": "" + }, + "raised": { + "description": "", + "cta": "" } } diff --git a/src/frontend/src/locales/en/notifications.json b/src/frontend/src/locales/en/notifications.json index 82351f11..d63a070a 100644 --- a/src/frontend/src/locales/en/notifications.json +++ b/src/frontend/src/locales/en/notifications.json @@ -2,5 +2,9 @@ "defaultName": "A contributor", "joined": { "description": "{{name}} join the room" + }, + "raised": { + "description": "{{name}} raised its hand.", + "cta": "Open waiting list" } } diff --git a/src/frontend/src/locales/fr/notifications.json b/src/frontend/src/locales/fr/notifications.json index 1449a497..9405a191 100644 --- a/src/frontend/src/locales/fr/notifications.json +++ b/src/frontend/src/locales/fr/notifications.json @@ -2,5 +2,9 @@ "defaultName": "Un contributeur", "joined": { "description": "{{name}} participe à l'appel" + }, + "raised": { + "description": "{{name}} a levé la main.", + "cta": "Ouvrir la file d'attente" } } diff --git a/src/frontend/src/primitives/buttonRecipe.ts b/src/frontend/src/primitives/buttonRecipe.ts index 628d8206..bfc645d6 100644 --- a/src/frontend/src/primitives/buttonRecipe.ts +++ b/src/frontend/src/primitives/buttonRecipe.ts @@ -77,6 +77,7 @@ export const buttonRecipe = cva({ color: 'primary', '&[data-hovered]': { background: 'gray.100 !important', + color: 'primary !important', }, }, danger: {