98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
|
|
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<Set<string>>(new Set<string>());
|
||
|
|
|
||
|
|
/** Write (or clear) the marked-unread flag on both the stable + unstable keys. */
|
||
|
|
export const setMarkedUnread = (
|
||
|
|
mx: MatrixClient,
|
||
|
|
roomId: string,
|
||
|
|
unread: boolean,
|
||
|
|
): Promise<unknown> =>
|
||
|
|
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<string>();
|
||
|
|
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]);
|
||
|
|
};
|