From e535040ac6069ffff73a97b38b910e696ca54935 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 23 Feb 2025 00:13:18 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20implement=20persistent=20?= =?UTF-8?q?notifications=20for=20waiting=20participants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add special non-closing notifications for waiting participants that remain visible until all have been reviewed. Implement complementary system alongside existing toaster notifications. Designed with accessibility in mind but would benefit from expert review. Current UX provides good foundation with quick actions planned for v2. --- src/frontend/src/components/Avatar.tsx | 17 ++- .../notifications/MainNotificationToast.tsx | 10 +- .../notifications/components/Toast.tsx | 2 +- .../WaitingParticipantNotification.tsx | 102 ++++++++++++++++++ .../src/locales/de/notifications.json | 5 + .../src/locales/en/notifications.json | 5 + .../src/locales/fr/notifications.json | 5 + 7 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 src/frontend/src/features/notifications/components/WaitingParticipantNotification.tsx diff --git a/src/frontend/src/components/Avatar.tsx b/src/frontend/src/components/Avatar.tsx index 90fe0618..d007d293 100644 --- a/src/frontend/src/components/Avatar.tsx +++ b/src/frontend/src/components/Avatar.tsx @@ -26,6 +26,11 @@ const avatar = cva({ height: '100%', }, }, + notification: { + true: { + border: '2px solid white', + }, + }, }, defaultVariants: { context: 'list', @@ -37,14 +42,22 @@ export type AvatarProps = React.HTMLAttributes & { bgColor?: string } & RecipeVariantProps -export const Avatar = ({ name, bgColor, context, ...props }: AvatarProps) => { +export const Avatar = ({ + name, + bgColor, + context, + notification, + style, + ...props +}: AvatarProps) => { const initial = name?.trim()?.charAt(0) || '' return (
{initial} diff --git a/src/frontend/src/features/notifications/MainNotificationToast.tsx b/src/frontend/src/features/notifications/MainNotificationToast.tsx index 1a9274ff..d8dfd213 100644 --- a/src/frontend/src/features/notifications/MainNotificationToast.tsx +++ b/src/frontend/src/features/notifications/MainNotificationToast.tsx @@ -1,13 +1,15 @@ import { useEffect, useRef, useState } from 'react' import { useRoomContext } from '@livekit/components-react' import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client' -import { ToastProvider, toastQueue } from './components/ToastProvider' +import { ChatMessage, isMobileBrowser } from '@livekit/components-core' +import { useTranslation } from 'react-i18next' +import { Div } from '@/primitives' import { NotificationType } from './NotificationType' import { NotificationDuration } from './NotificationDuration' import { decodeNotificationDataReceived } from './utils' -import { Div } from '@/primitives' -import { ChatMessage, isMobileBrowser } from '@livekit/components-core' import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification' +import { ToastProvider, toastQueue } from './components/ToastProvider' +import { WaitingParticipantNotification } from './components/WaitingParticipantNotification' import { EMOJIS, Reaction, @@ -16,7 +18,6 @@ import { ANIMATION_DURATION, ReactionPortals, } from '@/features/rooms/livekit/components/ReactionPortal' -import { useTranslation } from 'react-i18next' export const MainNotificationToast = () => { const room = useRoomContext() @@ -195,6 +196,7 @@ export const MainNotificationToast = () => { return (
+
) diff --git a/src/frontend/src/features/notifications/components/Toast.tsx b/src/frontend/src/features/notifications/components/Toast.tsx index 59b5c1f3..4f146102 100644 --- a/src/frontend/src/features/notifications/components/Toast.tsx +++ b/src/frontend/src/features/notifications/components/Toast.tsx @@ -22,7 +22,7 @@ export const StyledToastContainer = styled('div', { }, }) -const StyledToast = styled('div', { +export const StyledToast = styled('div', { base: { display: 'flex', justifyContent: 'space-between', diff --git a/src/frontend/src/features/notifications/components/WaitingParticipantNotification.tsx b/src/frontend/src/features/notifications/components/WaitingParticipantNotification.tsx new file mode 100644 index 00000000..fc9fd3f0 --- /dev/null +++ b/src/frontend/src/features/notifications/components/WaitingParticipantNotification.tsx @@ -0,0 +1,102 @@ +import { useListWaitingParticipants } from '@/features/rooms/api/listWaitingParticipants' +import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' +import { StyledToastContainer } from './Toast' +import { HStack } from '@/styled-system/jsx' +import { Avatar } from '@/components/Avatar' +import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' +import { Button, Text } from '@/primitives' +import { css } from '@/styled-system/css' +import { RiInfinityLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' + +export const WaitingParticipantNotification = () => { + const data = useRoomData() + const { t } = useTranslation('notifications', { + keyPrefix: 'waitingParticipants', + }) + const { isParticipantsOpen, toggleParticipants } = useSidePanel() + const { data: readOnlyData } = useListWaitingParticipants(data!.id, { + retry: false, + enabled: false, + }) + const participants = readOnlyData?.participants || [] + if (!participants.length) return + return ( + + 1 ? t('several') : t('one')} + aria-modal={false} + > + + + {participants.length > 1 && ( + + )} + {participants.length > 2 && ( + + {participants.length < 102 ? ( +

+{participants.length - 2}

+ ) : ( + + )} +
+ )} +
+ + {participants.length > 1 ? t('several') : t('one')} + + {!isParticipantsOpen && ( + + )} +
+
+ ) +} diff --git a/src/frontend/src/locales/de/notifications.json b/src/frontend/src/locales/de/notifications.json index 587e7364..aa8c1508 100644 --- a/src/frontend/src/locales/de/notifications.json +++ b/src/frontend/src/locales/de/notifications.json @@ -15,5 +15,10 @@ }, "reaction": { "description": "" + }, + "waitingParticipants": { + "one": "", + "several": "", + "open": "" } } diff --git a/src/frontend/src/locales/en/notifications.json b/src/frontend/src/locales/en/notifications.json index d81d6a2e..44f45cdf 100644 --- a/src/frontend/src/locales/en/notifications.json +++ b/src/frontend/src/locales/en/notifications.json @@ -15,5 +15,10 @@ }, "reaction": { "description": "{{name}} reacted with {{emoji}}" + }, + "waitingParticipants": { + "one": "One person wants to join this call.", + "several": "Several people want to join this call.", + "open": "Open" } } diff --git a/src/frontend/src/locales/fr/notifications.json b/src/frontend/src/locales/fr/notifications.json index 47ae5a83..af88f072 100644 --- a/src/frontend/src/locales/fr/notifications.json +++ b/src/frontend/src/locales/fr/notifications.json @@ -15,5 +15,10 @@ }, "reaction": { "description": "{{name}} a reagi avec {{emoji}}" + }, + "waitingParticipants": { + "one": "Une personne souhaite participer à cet appel.", + "several": "Plusieurs personnes souhaitent participer à cet appel.", + "open": "Afficher" } }