59 lines
2.5 KiB
TypeScript
59 lines
2.5 KiB
TypeScript
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<Set<string>>(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]);
|
||
|
|
};
|