Skip to content

Commit 882876e

Browse files
authored
feat(threads): thread drawer, thread browser, live reply counts, and unread badges (#564)
📝 **Docs:** Already live at [Threads — Sable Docs](https://docs.sable.moe/new-features/threads/) — no separate docs PR needed. --- ### Description Expands Matrix thread support with a full thread drawer, server-side thread browser, live reply counts, unread badges, and several UX fixes. #### ThreadDrawer - Slide-in drawer for reading and replying in threads without leaving the main room - Thread root message shown at top with full reply history below - Up-arrow edit-last-message: pressing ↑ in the thread input edits the user's last own message in that thread - Reactions add/remove trigger immediate re-renders without manual refresh (handles the fact that reaction events carry `m.relates_to.event_id` not `threadRootId` directly — fetches target event to check thread membership) - Thread root message box sized to content with `maxHeight: 200px` to avoid dead whitespace on short messages #### ThreadBrowser - Lists all threads in the room; calls `room.fetchRoomThreads()` on mount to load threads beyond the local timeline window - Load-more button when the server has additional thread pages - Each thread preview shows sender avatar, display name, reply count, and last message body with ellipsis truncation - Jump button navigates to the thread root in the main timeline (via `useRoomNavigate`) instead of re-opening the drawer - Thread unread badges on each preview item via `room.getThreadUnreadNotificationCount()` #### ThreadReplyChip (main timeline) - Shows unread badge for threads with unseen replies via `room.getThreadUnreadNotificationCount()` #### Thread button (RoomViewHeader) - If a thread drawer is open, button closes the drawer and opens the thread browser (allows going from a single thread to the full list) - If no thread is open, button toggles the thread browser as before - `aria-pressed` reflects both thread browser state and open thread drawer Fixes # #### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings ### AI disclosure: - [x] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does) - [ ] Fully AI generated (explain what all the generated code does in moderate detail) ThreadDrawer/ThreadBrowser/thread-chip iterations were partially AI assisted; I reviewed and adjusted logic for timeline processing, thread loading, and event rendering behavior.
2 parents f590b99 + 29d90e6 commit 882876e

11 files changed

Lines changed: 767 additions & 493 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: minor
3+
---
4+
5+
Update threads: various fixes, browse all room threads, and see live reply counts on messages.

src/app/features/room/RoomTimeline.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { MessageBase, CompactPlaceholder, DefaultPlaceholder } from '$components
3232
import { RoomIntro } from '$components/room-intro';
3333
import { useMatrixClient } from '$hooks/useMatrixClient';
3434
import { useAlive } from '$hooks/useAlive';
35+
import { useMessageEdit } from '$hooks/useMessageEdit';
3536
import { useDocumentFocusChange } from '$hooks/useDocumentFocusChange';
3637
import { markAsRead } from '$utils/notifications';
3738
import {
@@ -124,6 +125,8 @@ export function RoomTimeline({
124125
}: Readonly<RoomTimelineProps>) {
125126
const mx = useMatrixClient();
126127
const alive = useAlive();
128+
129+
const { editId, handleEdit } = useMessageEdit(editor, { onReset: onEditorReset, alive });
127130
const { navigateRoom } = useRoomNavigate();
128131

129132
const [hideReads] = useSetting(settingsAtom, 'hideReads');
@@ -167,7 +170,6 @@ export function RoomTimeline({
167170
return myPowerLevel < sendLevel;
168171
}, [powerLevels, mx]);
169172

170-
const [editId, setEditId] = useState<string>();
171173
const [unreadInfo, setUnreadInfo] = useState(() => getRoomUnreadInfo(room, true));
172174

173175
const readUptoEventIdRef = useRef<string | undefined>(undefined);
@@ -472,7 +474,6 @@ export function RoomTimeline({
472474
room,
473475
mx,
474476
editor,
475-
alive,
476477
nicknames,
477478
globalProfiles,
478479
spaceId: optionalSpace?.roomId,
@@ -481,8 +482,7 @@ export function RoomTimeline({
481482
setReplyDraft,
482483
openThreadId,
483484
setOpenThread,
484-
setEditId,
485-
onEditorReset,
485+
handleEdit,
486486
handleOpenEvent: (id) => {
487487
const evtTimeline = getEventTimeline(room, id);
488488
const absoluteIndex = evtTimeline

0 commit comments

Comments
 (0)