✨(frontend) implement persistent notifications for waiting participants
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.
This commit is contained in:
committed by
aleb_the_flash
parent
92851b10cc
commit
e535040ac6
@@ -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<HTMLDivElement> & {
|
||||
bgColor?: string
|
||||
} & RecipeVariantProps<typeof avatar>
|
||||
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
...style,
|
||||
}}
|
||||
className={avatar({ context })}
|
||||
className={avatar({ context, notification })}
|
||||
{...props}
|
||||
>
|
||||
{initial}
|
||||
|
||||
@@ -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 (
|
||||
<Div position="absolute" bottom={0} right={5} zIndex={1000}>
|
||||
<ToastProvider />
|
||||
<WaitingParticipantNotification />
|
||||
<ReactionPortals reactions={reactions} />
|
||||
</Div>
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ export const StyledToastContainer = styled('div', {
|
||||
},
|
||||
})
|
||||
|
||||
const StyledToast = styled('div', {
|
||||
export const StyledToast = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
@@ -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 (
|
||||
<StyledToastContainer role="alert">
|
||||
<HStack
|
||||
padding={'1rem'}
|
||||
gap={'1rem'}
|
||||
role={'alertdialog'}
|
||||
aria-label={participants.length > 1 ? t('several') : t('one')}
|
||||
aria-modal={false}
|
||||
>
|
||||
<HStack gap={0}>
|
||||
<Avatar
|
||||
name={participants[0].username}
|
||||
bgColor={participants[0].color}
|
||||
context="list"
|
||||
notification
|
||||
/>
|
||||
{participants.length > 1 && (
|
||||
<Avatar
|
||||
name={participants[1].username}
|
||||
bgColor={participants[1].color}
|
||||
context="list"
|
||||
notification
|
||||
style={{
|
||||
marginLeft: '-10px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{participants.length > 2 && (
|
||||
<span
|
||||
className={css({
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
fontSize: '1rem',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
borderRadius: '50%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'primaryDark.100',
|
||||
border: '2px solid white',
|
||||
marginLeft: '-10px',
|
||||
})}
|
||||
>
|
||||
{participants.length < 102 ? (
|
||||
<p>+{participants.length - 2}</p>
|
||||
) : (
|
||||
<RiInfinityLine size={20} />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</HStack>
|
||||
<Text
|
||||
variant="paragraph"
|
||||
margin={false}
|
||||
wrap={'balance'}
|
||||
style={{
|
||||
maxWidth: participants.length == 1 ? '10rem' : '15rem',
|
||||
}}
|
||||
>
|
||||
{participants.length > 1 ? t('several') : t('one')}
|
||||
</Text>
|
||||
{!isParticipantsOpen && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="text"
|
||||
style={{
|
||||
color: '#60a5fa',
|
||||
}}
|
||||
onPress={() => {
|
||||
toggleParticipants()
|
||||
}}
|
||||
>
|
||||
{t('open')}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
</StyledToastContainer>
|
||||
)
|
||||
}
|
||||
@@ -15,5 +15,10 @@
|
||||
},
|
||||
"reaction": {
|
||||
"description": ""
|
||||
},
|
||||
"waitingParticipants": {
|
||||
"one": "",
|
||||
"several": "",
|
||||
"open": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user