import { atom, useSetAtom } from 'jotai'; import { MatrixClient, MatrixEvent, Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk'; import { useEffect } from 'react'; import { AccountDataEvent } from '../../../types/matrix/accountData'; // MSC2867 — "mark a room as unread". A per-room account-data flag `{ unread }`. // Stable type `m.marked_unread`; servers/clients predating the stabilization use // the unstable `com.famedly.marked_unread`. We read either and write both so the // flag round-trips across the ecosystem. const UNSTABLE_MARKED_UNREAD = 'com.famedly.marked_unread'; const readMarkedUnread = (room: Room): boolean => { const stable = room.getAccountData(AccountDataEvent.MarkedUnread)?.getContent()?.unread; if (typeof stable === 'boolean') return stable; return room.getAccountData(UNSTABLE_MARKED_UNREAD)?.getContent()?.unread === true; }; /** Set of room ids the user has explicitly marked as unread. */ export const markedUnreadAtom = atom>(new Set()); /** Write (or clear) the marked-unread flag on both the stable + unstable keys. */ export const setMarkedUnread = ( mx: MatrixClient, roomId: string, unread: boolean, ): Promise => Promise.all([ // eslint-disable-next-line @typescript-eslint/no-explicit-any mx.setRoomAccountData(roomId, AccountDataEvent.MarkedUnread as any, { unread }), // Best-effort mirror for older servers; never fail the primary write on it. // eslint-disable-next-line @typescript-eslint/no-explicit-any mx.setRoomAccountData(roomId, UNSTABLE_MARKED_UNREAD as any, { unread }).catch(() => undefined), ]); export const receiptIsMine = (event: MatrixEvent, userId: string): boolean => { const content = event.getContent(); return Object.keys(content).some((eventId) => Object.keys(content[eventId] ?? {}).some( (receiptType) => content[eventId][receiptType]?.[userId], ), ); }; export const useBindMarkedUnreadAtom = (mx: MatrixClient, anAtom: typeof markedUnreadAtom) => { const setAtom = useSetAtom(anAtom); useEffect(() => { const seed = new Set(); mx.getRooms().forEach((room) => { if (readMarkedUnread(room)) seed.add(room.roomId); }); setAtom(seed); const syncRoom = (room: Room) => { const marked = readMarkedUnread(room); setAtom((prev) => { if (marked === prev.has(room.roomId)) return prev; const next = new Set(prev); if (marked) next.add(room.roomId); else next.delete(room.roomId); return next; }); }; const onAccountData: RoomEventHandlerMap[RoomEvent.AccountData] = (_event, room) => { syncRoom(room); }; // Reading a room clears its marked-unread flag (MSC2867): when our own read // receipt lands for a room that's currently marked, clear it. const onReceipt: RoomEventHandlerMap[RoomEvent.Receipt] = (event, room) => { const myId = mx.getUserId(); if (!myId || !readMarkedUnread(room)) return; if (receiptIsMine(event, myId)) { setMarkedUnread(mx, room.roomId, false).catch(() => undefined); } }; const onMembership: RoomEventHandlerMap[RoomEvent.MyMembership] = (room) => { if (room.getMyMembership() !== 'join') { setAtom((prev) => { if (!prev.has(room.roomId)) return prev; const next = new Set(prev); next.delete(room.roomId); return next; }); } }; mx.on(RoomEvent.AccountData, onAccountData); mx.on(RoomEvent.Receipt, onReceipt); mx.on(RoomEvent.MyMembership, onMembership); return () => { mx.removeListener(RoomEvent.AccountData, onAccountData); mx.removeListener(RoomEvent.Receipt, onReceipt); mx.removeListener(RoomEvent.MyMembership, onMembership); }; }, [mx, setAtom]); };