diff --git a/src/frontend/src/features/notifications/MainNotificationToast.tsx b/src/frontend/src/features/notifications/MainNotificationToast.tsx index bc17d81e..9758e0d4 100644 --- a/src/frontend/src/features/notifications/MainNotificationToast.tsx +++ b/src/frontend/src/features/notifications/MainNotificationToast.tsx @@ -16,6 +16,10 @@ import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce' import { Emoji } from '@/features/reactions/types' import { useReactions } from '@/features/reactions/hooks/useReactions' +// Sliding window of recent chat ids kept for deduplication. Sized to comfortably +// cover bursts and re-emits while staying negligible in memory. +const MAX_TRACKED_CHAT_IDS = 16 + export const MainNotificationToast = () => { const room = useRoomContext() const { triggerNotificationSound } = useNotificationSound() @@ -24,9 +28,9 @@ export const MainNotificationToast = () => { const { appendReaction } = useReactions() - // Chat uses keepAlive in multiple SidePanels (main + PiP), so the same - // message event can fire more than once. Track the last id to deduplicate. - const lastChatMsgIdRef = useRef('') + // Multiple Chat instances may re-emit the same RoomEvent.ChatMessage. + // Dedupe against a small ring of recent ids. + const seenChatMsgIdsRef = useRef([]) useEffect(() => { const handleChatMessage = ( @@ -34,8 +38,13 @@ export const MainNotificationToast = () => { participant?: Participant | undefined ) => { if (!participant || participant.isLocal) return - if (chatMessage.id && chatMessage.id === lastChatMsgIdRef.current) return - lastChatMsgIdRef.current = chatMessage.id ?? '' + const id = chatMessage.id + if (id) { + const seen = seenChatMsgIdsRef.current + if (seen.includes(id)) return + seen.push(id) + if (seen.length > MAX_TRACKED_CHAT_IDS) seen.shift() + } triggerNotificationSound(NotificationType.MessageReceived) toastQueue.add( { diff --git a/src/frontend/src/features/pip/components/PipView.tsx b/src/frontend/src/features/pip/components/PipView.tsx index 92e39d6d..555d7bbc 100644 --- a/src/frontend/src/features/pip/components/PipView.tsx +++ b/src/frontend/src/features/pip/components/PipView.tsx @@ -49,14 +49,33 @@ export const PipView = () => { - - + + + + ) } +const OverlayStack = styled('div', { + base: { + position: 'absolute', + top: '0.5rem', + left: '0.5rem', + right: '0.5rem', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '0.375rem', + pointerEvents: 'none', + zIndex: 1000, + '& > *': { pointerEvents: 'auto' }, + }, +}) + const PipContainer = styled('div', { base: { + position: 'relative', width: '100%', height: '100%', display: 'grid', diff --git a/src/frontend/src/features/pip/components/notifications/PipConnectionStateToast.tsx b/src/frontend/src/features/pip/components/notifications/PipConnectionStateToast.tsx index b8bf7b0e..9067c906 100644 --- a/src/frontend/src/features/pip/components/notifications/PipConnectionStateToast.tsx +++ b/src/frontend/src/features/pip/components/notifications/PipConnectionStateToast.tsx @@ -25,19 +25,11 @@ export const PipConnectionStateToast = () => { if (!label) return null - return ( - - {label} - - ) + return {label} } const Banner = styled('div', { base: { - position: 'absolute', - top: '0.5rem', - left: '50%', - transform: 'translateX(-50%)', backgroundColor: 'greyscale.800', color: 'white', fontSize: '0.8125rem', @@ -46,7 +38,9 @@ const Banner = styled('div', { borderRadius: '6px', boxShadow: 'rgba(0, 0, 0, 0.4) 0px 2px 6px 0px, rgba(0, 0, 0, 0.25) 0px 4px 12px 2px', - zIndex: 1001, animation: 'fade 200ms', + '@media (prefers-reduced-motion: reduce)': { + animation: 'none', + }, }, }) diff --git a/src/frontend/src/features/pip/components/notifications/PipNotificationOverlay.tsx b/src/frontend/src/features/pip/components/notifications/PipNotificationOverlay.tsx index f0ae5f01..90671b89 100644 --- a/src/frontend/src/features/pip/components/notifications/PipNotificationOverlay.tsx +++ b/src/frontend/src/features/pip/components/notifications/PipNotificationOverlay.tsx @@ -13,6 +13,8 @@ import { PipToastBody } from './PipToastBody' * Shows shared toasts in the PiP window. * We use a local aria-live region so screen readers can read them in PiP. */ +const MAX_VISIBLE = 3 + export const PipNotificationOverlay = () => { const state = useToastQueue(toastQueue) const { t } = useTranslation('rooms', { @@ -21,14 +23,15 @@ export const PipNotificationOverlay = () => { if (state.visibleToasts.length === 0) return null + const toasts = state.visibleToasts.slice(0, MAX_VISIBLE) + return ( - {state.visibleToasts.map((toast) => ( + {toasts.map((toast) => (