import { useEffect, useRef } from 'react'; import { MatrixClient, MatrixEvent, MatrixEventEvent, Room } from 'matrix-js-sdk'; import { roomHaveNotification, unreadIsOnlyVerification } from '../../utils/room'; import { markAsRead } from '../../utils/notifications'; /** * A COMPLETED in-room device-verification request is a plain `m.room.message` * that permanently keeps a DM's server/SDK notification count > 0 (it matches the * default DM push rule and there's no recency gate), so the DM re-lights as unread * on every fresh sync. `getUnreadInfo`'s suppression hides the dot, but the raw * SDK count stays "dirty" (desktop badge, other consumers) and the SDK re-inflates * it on every decrypt. The only durable, SDK-supported fix is a read receipt that * covers the request event. * * This hook sends that receipt — but ONLY when a room's ENTIRE unread span is * verification-flow events (`unreadIsOnlyVerification`), so it can never mark a * real unread message read. It fires at most once per room per session, after the * tail decrypts (the count is only attributable to the request post-decryption). * `markAsRead` honours the user's private-read-receipt setting. */ export const useAutoMarkVerificationRead = (mx: MatrixClient): void => { const doneRef = useRef>(new Set()); useEffect(() => { const done = doneRef.current; const maybeMark = (room: Room) => { const { roomId } = room; if (done.has(roomId)) return; if (room.getMyMembership() !== 'join') return; // Only touch rooms the SDK actually counts as notifying... if (!roomHaveNotification(room)) return; // ...and only when the whole unread span is a completed verification. if (!unreadIsOnlyVerification(room, mx.getUserId())) return; done.add(roomId); markAsRead(mx, roomId, false).catch(() => { // Let a later decrypt/sweep retry on transient failure. done.delete(roomId); }); }; // Sweep once on mount (after initial sync some verification tails are already // decrypted), then re-check whenever an event decrypts — the count only // becomes attributable to the verification request once it's decrypted. mx.getRooms().forEach(maybeMark); const onDecrypted = (event: MatrixEvent) => { const roomId = event.getRoomId(); const room = roomId ? mx.getRoom(roomId) : null; if (room) maybeMark(room); }; mx.on(MatrixEventEvent.Decrypted, onDecrypted); return () => { mx.removeListener(MatrixEventEvent.Decrypted, onDecrypted); }; }, [mx]); };