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,43 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createStore } from 'jotai';
|
||||
import { getThreadDraftKey, roomIdToActiveThreadIdAtomFamily } from './thread';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getThreadDraftKey
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('getThreadDraftKey joins roomId and threadRootId with "::"', () => {
|
||||
assert.equal(getThreadDraftKey('!room:server', '$root'), '!room:server::$root');
|
||||
});
|
||||
|
||||
test('getThreadDraftKey keeps the two ids distinguishable', () => {
|
||||
assert.notEqual(getThreadDraftKey('!a:server', '$b'), getThreadDraftKey('!a:server', '$c'));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// roomIdToActiveThreadIdAtomFamily
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('returns the same atom instance for the same roomId', () => {
|
||||
const a = roomIdToActiveThreadIdAtomFamily('!room:server');
|
||||
const b = roomIdToActiveThreadIdAtomFamily('!room:server');
|
||||
assert.equal(a, b);
|
||||
});
|
||||
|
||||
test('returns different atoms for different roomIds', () => {
|
||||
const a = roomIdToActiveThreadIdAtomFamily('!a:server');
|
||||
const b = roomIdToActiveThreadIdAtomFamily('!b:server');
|
||||
assert.notEqual(a, b);
|
||||
});
|
||||
|
||||
test('the active-thread atom defaults to null and is writable', () => {
|
||||
const store = createStore();
|
||||
const activeThreadIdAtom = roomIdToActiveThreadIdAtomFamily('!store:server');
|
||||
|
||||
assert.equal(store.get(activeThreadIdAtom), null);
|
||||
store.set(activeThreadIdAtom, '$root');
|
||||
assert.equal(store.get(activeThreadIdAtom), '$root');
|
||||
store.set(activeThreadIdAtom, null);
|
||||
assert.equal(store.get(activeThreadIdAtom), null);
|
||||
});
|
||||
Reference in New Issue
Block a user