mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-07 01:13:21 +00:00
♻️(frontend) harden PiP notifications stacking and dedupe
Group banner and toasts in one stack, cap visibility, fix chat dedup.
This commit is contained in:
@@ -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<string>('')
|
||||
// Multiple Chat instances may re-emit the same RoomEvent.ChatMessage.
|
||||
// Dedupe against a small ring of recent ids.
|
||||
const seenChatMsgIdsRef = useRef<string[]>([])
|
||||
|
||||
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(
|
||||
{
|
||||
|
||||
@@ -49,14 +49,33 @@ export const PipView = () => {
|
||||
<PipReactionsToolbar />
|
||||
<PipControlBar showScreenShare={browserSupportsScreenSharing} />
|
||||
<SidePanel store={pipLayoutStore} />
|
||||
<PipConnectionStateToast />
|
||||
<PipNotificationOverlay />
|
||||
<OverlayStack>
|
||||
<PipConnectionStateToast />
|
||||
<PipNotificationOverlay />
|
||||
</OverlayStack>
|
||||
</PipContainer>
|
||||
)
|
||||
}
|
||||
|
||||
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',
|
||||
|
||||
@@ -25,19 +25,11 @@ export const PipConnectionStateToast = () => {
|
||||
|
||||
if (!label) return null
|
||||
|
||||
return (
|
||||
<Banner role="status" aria-live="polite">
|
||||
{label}
|
||||
</Banner>
|
||||
)
|
||||
return <Banner role="status">{label}</Banner>
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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<ToastData>(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 (
|
||||
<Region
|
||||
role="region"
|
||||
aria-label={t('notificationsLabel')}
|
||||
aria-live="polite"
|
||||
aria-relevant="additions"
|
||||
>
|
||||
{state.visibleToasts.map((toast) => (
|
||||
{toasts.map((toast) => (
|
||||
<ToastCard key={toast.key} aria-atomic="true">
|
||||
<PipToastBody toast={toast} />
|
||||
<Button
|
||||
@@ -48,17 +51,11 @@ export const PipNotificationOverlay = () => {
|
||||
|
||||
const Region = styled('div', {
|
||||
base: {
|
||||
position: 'absolute',
|
||||
top: '0.5rem',
|
||||
left: '0.5rem',
|
||||
right: '0.5rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.375rem',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1000,
|
||||
'& > *': { pointerEvents: 'auto' },
|
||||
width: '100%',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -75,5 +72,8 @@ const ToastCard = styled('div', {
|
||||
'rgba(0, 0, 0, 0.4) 0px 2px 6px 0px, rgba(0, 0, 0, 0.25) 0px 4px 12px 2px',
|
||||
paddingRight: '0.25rem',
|
||||
animation: 'fade 200ms',
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
animation: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user