(frontend) add toast notifications for unread chat messages

Users frequently miss chat messages due to discrete visual indicators.
Implemented toast notifications showing sender name and message preview to
improve visibility. Added message tracking and auto-dismiss when chat panel
opens.

Remove the warning in handleDataReceived function, it was triggered by
chat message events.
This commit is contained in:
lebaudantoine
2025-02-11 23:14:30 +01:00
committed by aleb_the_flash
parent 591a3a5d8b
commit da95e804a0
7 changed files with 110 additions and 6 deletions

View File

@@ -4,13 +4,34 @@ import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
import { ToastProvider, toastQueue } from './components/ToastProvider'
import { NotificationType } from './NotificationType'
import { Div } from '@/primitives'
import { isMobileBrowser } from '@livekit/components-core'
import { ChatMessage, isMobileBrowser } from '@livekit/components-core'
import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification'
export const MainNotificationToast = () => {
const room = useRoomContext()
const { triggerNotificationSound } = useNotificationSound()
useEffect(() => {
const handleChatMessage = (
chatMessage: ChatMessage,
participant?: Participant | undefined
) => {
if (!participant || participant.isLocal) return
toastQueue.add(
{
participant: participant,
message: chatMessage.message,
type: NotificationType.MessageReceived,
},
{ timeout: 5000 }
)
}
room.on(RoomEvent.ChatMessage, handleChatMessage)
return () => {
room.off(RoomEvent.ChatMessage, handleChatMessage)
}
}, [room])
useEffect(() => {
const handleDataReceived = (
payload: Uint8Array,
@@ -32,7 +53,7 @@ export const MainNotificationToast = () => {
)
break
default:
console.warn(`Unhandled notification type: ${notificationType}`)
return
}
}
room.on(RoomEvent.DataReceived, handleDataReceived)

View File

@@ -2,5 +2,5 @@ export enum NotificationType {
ParticipantJoined = 'participantJoined',
HandRaised = 'handRaised',
ParticipantMuted = 'participantMuted',
// todo - implement message received notification
MessageReceived = 'messageReceived',
}

View File

@@ -0,0 +1,74 @@
import { useToast } from '@react-aria/toast'
import { useEffect, useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { Text } from '@/primitives'
import { RiMessage2Line } from '@remixicon/react'
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { Button as RACButton } from 'react-aria-components'
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
export function ToastMessageReceived({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps } = useToast(props, state, ref)
const toast = props.toast
const participant = toast.content.participant
const message = toast.content.message
const { isChatOpen, toggleChat } = useSidePanel()
useEffect(() => {
if (isChatOpen) {
state.close(toast.key)
}
}, [isChatOpen, toast, state])
if (isChatOpen) return null
return (
<StyledToastContainer {...toastProps} ref={ref}>
<RACButton onPress={() => toggleChat()} aria-label={t('openChat')}>
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
padding: '14px',
gap: '0.75rem',
textAlign: 'start',
width: '150px',
md: {
width: '260px',
},
})}
>
<div
className={css({
display: 'flex',
flexDirection: 'row',
justifyContent: 'start',
gap: '0.5rem',
})}
>
<RiMessage2Line
size={20}
className={css({
color: 'primary.300',
marginTop: '3px',
})}
aria-hidden="true"
/>
<span>{participant.name}</span>
</div>
<Text margin={false} wrap={'pretty'} centered={false}>
{message}
</Text>
</div>
</RACButton>
</StyledToastContainer>
)
}

View File

@@ -7,6 +7,7 @@ import { ToastJoined } from './ToastJoined'
import { ToastData } from './ToastProvider'
import { ToastRaised } from './ToastRaised'
import { ToastMuted } from './ToastMuted'
import { ToastMessageReceived } from './ToastMessageReceived'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -27,6 +28,11 @@ export function ToastRegion({ state, ...props }: ToastRegionProps) {
if (toast.content?.type === NotificationType.ParticipantMuted) {
return <ToastMuted key={toast.key} toast={toast} state={state} />
}
if (toast.content?.type === NotificationType.MessageReceived) {
return (
<ToastMessageReceived key={toast.key} toast={toast} state={state} />
)
}
return <Toast key={toast.key} toast={toast} state={state} />
})}
</div>

View File

@@ -7,5 +7,6 @@
"description": "",
"cta": ""
},
"muted": ""
"muted": "",
"openChat": ""
}

View File

@@ -7,5 +7,6 @@
"description": "{{name}} has raised their hand.",
"cta": "Open waiting list"
},
"muted": "{{name}} has muted your microphone. No participant can hear you."
"muted": "{{name}} has muted your microphone. No participant can hear you.",
"openChat": "Open chat"
}

View File

@@ -7,5 +7,6 @@
"description": "{{name}} a levé la main.",
"cta": "Ouvrir la file d'attente"
},
"muted": "{{name}} a coupé votre micro. Aucun participant ne peut l'entendre."
"muted": "{{name}} a coupé votre micro. Aucun participant ne peut l'entendre.",
"openChat": "Ouvrir le chat"
}