fix(threads): viewing a room no longer marks threads you follow read (#217)
markAsRead ran on every room visit (timeline at the bottom and focused) and sent a threaded receipt for every unread thread, so a reply in a thread you started or replied in lost its unread badge the moment you glanced at the room, without opening the thread. Reads from just viewing the timeline are now "passive": - threads you follow (started, replied in, or were mentioned in) stay unread until their panel is opened; other threads are still cleared so they don't keep the room dot lit forever; - while a followed thread has an unread reply, the main receipt is scoped to the main timeline instead of unthreaded, because an unthreaded receipt also reads every older thread reply (the next main message would clear the thread anyway). The check also asks whether the latest reply is read, since the thread's count lags when the reply and a main message share a sync; - the thread open in the panel is skipped, as the panel sends its own receipt (was two identical receipts per reply). Explicit "mark as read" (room menu, Escape, bulk actions) still clears everything. Unit tests for each rule plus a local-homeserver e2e that checks the server's per-thread count survives a reply + newer main message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f5d3c43b6c
commit
a8db61f79f
@@ -108,7 +108,7 @@ import {
|
||||
getIntersectionObserverEntry,
|
||||
useIntersectionObserver,
|
||||
} from '../../hooks/useIntersectionObserver';
|
||||
import { markAsRead } from '../../utils/notifications';
|
||||
import { markAsRead, MarkAsReadOptions } from '../../utils/notifications';
|
||||
import { useDebounce } from '../../hooks/useDebounce';
|
||||
import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver';
|
||||
import * as css from './RoomTimeline.css';
|
||||
@@ -505,6 +505,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
|
||||
const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId));
|
||||
const setActiveThreadId = useSetAtom(roomIdToActiveThreadIdAtomFamily(room.roomId));
|
||||
// [Gitea #217] Reads from just viewing the timeline are passive: threads the
|
||||
// user follows stay unread, and the open thread panel sends its own receipts.
|
||||
const activeThreadId = useAtomValue(roomIdToActiveThreadIdAtomFamily(room.roomId));
|
||||
const passiveReadRef = useRef<MarkAsReadOptions>({ passive: true });
|
||||
passiveReadRef.current = { passive: true, openThreadId: activeThreadId ?? undefined };
|
||||
// Thread summary chips only mount for events that already carry thread data
|
||||
// (perf: a chip subscribes room-level listeners, so mounting one per rendered
|
||||
// message would exceed the SDK's emitter cap). This single room-level
|
||||
@@ -707,7 +712,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
// and either there are no unread messages or the latest message is from the current user.
|
||||
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
||||
const _roomId = mEvt.getRoomId();
|
||||
if (_roomId) requestAnimationFrame(() => markAsRead(mx, _roomId, hideActivity));
|
||||
if (_roomId)
|
||||
requestAnimationFrame(() =>
|
||||
markAsRead(mx, _roomId, hideActivity, passiveReadRef.current),
|
||||
);
|
||||
}
|
||||
|
||||
if (!document.hasFocus() && !unreadInfo) {
|
||||
@@ -829,13 +837,17 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const tryAutoMarkAsRead = useCallback(() => {
|
||||
const readUptoEventId = readUptoEventIdRef.current;
|
||||
if (!readUptoEventId) {
|
||||
requestAnimationFrame(() => markAsRead(mx, room.roomId, hideActivity));
|
||||
requestAnimationFrame(() =>
|
||||
markAsRead(mx, room.roomId, hideActivity, passiveReadRef.current),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const evtTimeline = getEventTimeline(room, readUptoEventId);
|
||||
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
||||
if (latestTimeline === room.getLiveTimeline()) {
|
||||
requestAnimationFrame(() => markAsRead(mx, room.roomId, hideActivity));
|
||||
requestAnimationFrame(() =>
|
||||
markAsRead(mx, room.roomId, hideActivity, passiveReadRef.current),
|
||||
);
|
||||
}
|
||||
}, [mx, room, hideActivity]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user