feat(threads): Thread Panel — full side drawer (P3-8)
Right-side thread drawer (MembersDrawer pattern; mobile fullscreen): - ThreadPanel: header + close/Escape, ThreadTimeline, its own RoomInput (threadRootId prop; drafts/replies/uploads isolated per roomId::threadId; schedule + slash-commands off in threads v1) and threaded mark-as-read. - ThreadTimeline: lean reimplementation over thread.liveTimeline — copied useTimelinePagination pattern (/relations back-pagination + decryption), virtualized, root event emphasized + "N replies" divider, reactions/edits/ redactions, and a pending strip (chronological local echo never enters the thread timelineSet — rendered from LocalEchoUpdated instead). - ThreadSummary chips on root messages (server-aggregated bundle or live Thread; unread badge via getThreadUnreadNotificationCount) keep threads discoverable now that replies leave the main timeline. - Reply-in-Thread menu + thread indicators open the panel; deep links to thread events redirect into it. - State: roomIdToActiveThreadIdAtomFamily + getThreadDraftKey (+18 tests). Gates: tsc clean, eslint 0 errors, build OK, 616/617 tests (1 IDB skip). Awaiting live QA; release note: threaded replies no longer render inline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
MatrixEvent,
|
||||
NotificationCountType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
RoomEventHandlerMap,
|
||||
ThreadEvent,
|
||||
} from 'matrix-js-sdk';
|
||||
import { getThreadSummary, ThreadSummaryData } from '../features/room/thread/threadSummary';
|
||||
|
||||
/**
|
||||
* Reactive thread summary + unread count for a root event's "N replies" chip.
|
||||
*
|
||||
* Re-computes the summary on `ThreadEvent.Update` (the SDK re-emits this on the
|
||||
* root MatrixEvent) and the unread count on `RoomEvent.UnreadNotifications`.
|
||||
*/
|
||||
export const useThreadSummary = (
|
||||
rootEvent: MatrixEvent,
|
||||
room: Room,
|
||||
): { summary: ThreadSummaryData | undefined; unread: number } => {
|
||||
const threadId = rootEvent.getId();
|
||||
|
||||
const [summary, setSummary] = useState<ThreadSummaryData | undefined>(() =>
|
||||
getThreadSummary(rootEvent),
|
||||
);
|
||||
const [unread, setUnread] = useState<number>(() =>
|
||||
threadId
|
||||
? (room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Total) ?? 0)
|
||||
: 0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshSummary = () => setSummary(getThreadSummary(rootEvent));
|
||||
const refreshUnread = () => {
|
||||
if (!threadId) return;
|
||||
setUnread(room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Total) ?? 0);
|
||||
};
|
||||
|
||||
refreshSummary();
|
||||
refreshUnread();
|
||||
|
||||
const handleUnread: RoomEventHandlerMap[RoomEvent.UnreadNotifications] = (_counts, tId) => {
|
||||
if (tId && tId !== threadId) return;
|
||||
refreshUnread();
|
||||
};
|
||||
|
||||
rootEvent.on(ThreadEvent.Update, refreshSummary);
|
||||
room.on(RoomEvent.UnreadNotifications, handleUnread);
|
||||
return () => {
|
||||
rootEvent.removeListener(ThreadEvent.Update, refreshSummary);
|
||||
room.removeListener(RoomEvent.UnreadNotifications, handleUnread);
|
||||
};
|
||||
}, [rootEvent, room, threadId]);
|
||||
|
||||
return { summary, unread };
|
||||
};
|
||||
Reference in New Issue
Block a user