UNPKG

stream-chat-react-native-core

Version:

The official React Native and Expo components for Stream Chat, a service for building chat applications

158 lines (138 loc) 5.22 kB
import { useCallback, useEffect, useRef } from 'react'; import type { Channel, Event, MessageResponse } from 'stream-chat'; import { useAccessibilityContext } from '../../../contexts/accessibilityContext/AccessibilityContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; import { useAccessibilityAnnouncer } from '../useAccessibilityAnnouncer'; const MESSAGE_ANNOUNCEMENT_THROTTLE_MS = 1000; const isAnnounceableIncomingMessage = (message: MessageResponse, ownUserId?: string): boolean => { const messageUserId = message.user?.id; if (!message.id || !messageUserId || messageUserId === ownUserId) return false; return ( message.type !== 'deleted' && message.type !== 'ephemeral' && message.type !== 'error' && message.type !== 'system' && message.status !== 'failed' && message.status !== 'sending' ); }; const getSenderName = ( message: MessageResponse, t: ReturnType<typeof useTranslationContext>['t'], ) => message.user?.name?.trim() || message.user?.id || t('Anonymous'); export type UseIncomingMessageAnnouncementsParams = { activeThreadId?: string; channel?: Channel; ownUserId?: string; threadList?: boolean; }; /** * Mirrors stream-chat-react's `useIncomingMessageAnnouncements`: * - 1 message → "New message from {{user}}" * - >1 messages within throttle window → "{{count}} new messages" * - Throttled to one announcement per second * - Bounded `announcedMessageIds` set so a long-running session does not leak. * * Subscribes to `channel.on('message.new')`. When AccessibilityContext.enabled * is false OR `announceNewMessages` is false, the hook is a no-op (no * subscription is opened, no listener cost is paid). */ export const useIncomingMessageAnnouncements = ({ activeThreadId, channel, ownUserId, threadList = false, }: UseIncomingMessageAnnouncementsParams) => { const { announceNewMessages, enabled } = useAccessibilityContext(); const announce = useAccessibilityAnnouncer(); const { t } = useTranslationContext(); const lastAnnouncementTimestampRef = useRef(0); const flushTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const announcedMessageIdsRef = useRef(new Set<string>()); const pendingAnnouncementBatchRef = useRef<{ count: number; firstSender: string | null }>({ count: 0, firstSender: null, }); const flushPendingAnnouncements = useCallback(() => { const pending = pendingAnnouncementBatchRef.current; if (pending.count <= 0) return; if (pending.count === 1) { announce( t('a11y/New message from {{user}}', { user: pending.firstSender || t('Anonymous'), }), ); } else { announce(t('a11y/{{count}} new messages', { count: pending.count })); } pending.count = 0; pending.firstSender = null; lastAnnouncementTimestampRef.current = Date.now(); }, [announce, t]); const scheduleFlush = useCallback(() => { if (flushTimeoutRef.current) return; const now = Date.now(); const elapsed = now - lastAnnouncementTimestampRef.current; if (elapsed >= MESSAGE_ANNOUNCEMENT_THROTTLE_MS) { flushPendingAnnouncements(); return; } flushTimeoutRef.current = setTimeout(() => { flushTimeoutRef.current = undefined; flushPendingAnnouncements(); }, MESSAGE_ANNOUNCEMENT_THROTTLE_MS - elapsed); }, [flushPendingAnnouncements]); useEffect( () => () => { if (flushTimeoutRef.current) clearTimeout(flushTimeoutRef.current); }, [], ); useEffect(() => { if (!enabled || !announceNewMessages || !channel) return; const handleMessageNew = (event: Event) => { const message = event.message; if (!message) return; if ( (event.cid && event.cid !== channel.cid) || !isAnnounceableIncomingMessage(message, ownUserId) ) { return; } const isReply = !!message.parent_id; const belongsToActiveThread = !!activeThreadId && message.parent_id === activeThreadId; const shouldAnnounceInThreadList = threadList && belongsToActiveThread; const shouldAnnounceInMainList = !threadList && !isReply; if (!shouldAnnounceInThreadList && !shouldAnnounceInMainList) return; if (announcedMessageIdsRef.current.has(message.id || '')) return; if (message.id) { if (announcedMessageIdsRef.current.size > 500) { announcedMessageIdsRef.current.clear(); } announcedMessageIdsRef.current.add(message.id); } pendingAnnouncementBatchRef.current.count += 1; if (!pendingAnnouncementBatchRef.current.firstSender) { pendingAnnouncementBatchRef.current.firstSender = getSenderName(message, t); } scheduleFlush(); }; const subscription = channel.on('message.new', handleMessageNew); return () => { subscription.unsubscribe(); if (flushTimeoutRef.current) { clearTimeout(flushTimeoutRef.current); flushTimeoutRef.current = undefined; } }; }, [ activeThreadId, announceNewMessages, channel, enabled, ownUserId, scheduleFlush, t, threadList, ]); };