✨(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:
committed by
aleb_the_flash
parent
591a3a5d8b
commit
da95e804a0
@@ -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)
|
||||
|
||||
@@ -2,5 +2,5 @@ export enum NotificationType {
|
||||
ParticipantJoined = 'participantJoined',
|
||||
HandRaised = 'handRaised',
|
||||
ParticipantMuted = 'participantMuted',
|
||||
// todo - implement message received notification
|
||||
MessageReceived = 'messageReceived',
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"description": "",
|
||||
"cta": ""
|
||||
},
|
||||
"muted": ""
|
||||
"muted": "",
|
||||
"openChat": ""
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user