diff --git a/src/app/features/room/thread/threadSummary.test.ts b/src/app/features/room/thread/threadSummary.test.ts deleted file mode 100644 index 795a69df9..000000000 --- a/src/app/features/room/thread/threadSummary.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { test } from 'node:test'; -import assert from 'node:assert/strict'; -import { EventStatus, MatrixEvent, RelationType } from 'matrix-js-sdk'; -import { getThreadSummary, isPendingThreadReply } from './threadSummary'; - -// getThreadSummary reads either the live Thread (preferred) or the -// server-aggregated `m.thread` bundle. We stub only the members it touches and -// cast through `unknown` to MatrixEvent, mirroring the light mocking used in -// the state tests. - -type ThreadStub = { length: number; lastReplyTs?: number }; -type BundleStub = { count: number; latestTs?: number }; - -const makeRootEvent = (opts: { thread?: ThreadStub; bundle?: BundleStub }): MatrixEvent => { - const thread = opts.thread - ? { - length: opts.thread.length, - lastReply: () => - opts.thread?.lastReplyTs === undefined - ? null - : ({ getTs: () => opts.thread?.lastReplyTs } as unknown as MatrixEvent), - } - : undefined; - - return { - getThread: () => thread, - getServerAggregatedRelation: (relType: string) => { - if (relType !== RelationType.Thread || !opts.bundle) return undefined; - return { - count: opts.bundle.count, - latest_event: - opts.bundle.latestTs === undefined - ? undefined - : { origin_server_ts: opts.bundle.latestTs }, - }; - }, - } as unknown as MatrixEvent; -}; - -// --------------------------------------------------------------------------- -// getThreadSummary -// --------------------------------------------------------------------------- - -test('prefers the live thread: count from length, latestTs from lastReply', () => { - const rootEvent = makeRootEvent({ - thread: { length: 3, lastReplyTs: 1700 }, - bundle: { count: 99, latestTs: 1 }, - }); - assert.deepEqual(getThreadSummary(rootEvent), { count: 3, latestTs: 1700 }); -}); - -test('live thread with no replies yields undefined latestTs', () => { - const rootEvent = makeRootEvent({ thread: { length: 0 } }); - assert.deepEqual(getThreadSummary(rootEvent), { count: 0, latestTs: undefined }); -}); - -test('falls back to the server bundle when no live thread', () => { - const rootEvent = makeRootEvent({ bundle: { count: 5, latestTs: 1234 } }); - assert.deepEqual(getThreadSummary(rootEvent), { count: 5, latestTs: 1234 }); -}); - -test('bundle without latest_event yields undefined latestTs', () => { - const rootEvent = makeRootEvent({ bundle: { count: 2 } }); - assert.deepEqual(getThreadSummary(rootEvent), { count: 2, latestTs: undefined }); -}); - -test('returns undefined when there is neither a thread nor a bundle', () => { - const rootEvent = makeRootEvent({}); - assert.equal(getThreadSummary(rootEvent), undefined); -}); - -// --------------------------------------------------------------------------- -// isPendingThreadReply -// --------------------------------------------------------------------------- - -const ROOT = '$root:server'; - -const makeReply = (opts: { - status: EventStatus | null; - threadRootId?: string; - relation?: { rel_type?: string; event_id?: string } | null; -}): MatrixEvent => - ({ - status: opts.status, - threadRootId: opts.threadRootId, - getRelation: () => opts.relation ?? null, - }) as unknown as MatrixEvent; - -test('SENDING with matching threadRootId is pending', () => { - const event = makeReply({ status: EventStatus.SENDING, threadRootId: ROOT }); - assert.equal(isPendingThreadReply(event, ROOT), true); -}); - -test('NOT_SENT with matching threadRootId is pending', () => { - const event = makeReply({ status: EventStatus.NOT_SENT, threadRootId: ROOT }); - assert.equal(isPendingThreadReply(event, ROOT), true); -}); - -test('SENDING resolved via the m.thread relation content is pending', () => { - const event = makeReply({ - status: EventStatus.SENDING, - relation: { rel_type: RelationType.Thread, event_id: ROOT }, - }); - assert.equal(isPendingThreadReply(event, ROOT), true); -}); - -test('SENT (confirmed) event is not pending', () => { - const event = makeReply({ status: EventStatus.SENT, threadRootId: ROOT }); - assert.equal(isPendingThreadReply(event, ROOT), false); -}); - -test('null status is not pending', () => { - const event = makeReply({ status: null, threadRootId: ROOT }); - assert.equal(isPendingThreadReply(event, ROOT), false); -}); - -test('SENDING but for a different thread is not pending', () => { - const event = makeReply({ status: EventStatus.SENDING, threadRootId: '$other:server' }); - assert.equal(isPendingThreadReply(event, ROOT), false); -}); - -test('SENDING with a non-thread relation is not pending', () => { - const event = makeReply({ - status: EventStatus.SENDING, - relation: { rel_type: RelationType.Reference, event_id: ROOT }, - }); - assert.equal(isPendingThreadReply(event, ROOT), false); -}); - -test('SENDING with no relation and no threadRootId is not pending', () => { - const event = makeReply({ status: EventStatus.SENDING }); - assert.equal(isPendingThreadReply(event, ROOT), false); -}); diff --git a/src/app/features/room/thread/threadSummary.ts b/src/app/features/room/thread/threadSummary.ts deleted file mode 100644 index 6896332db..000000000 --- a/src/app/features/room/thread/threadSummary.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { EventStatus, IThreadBundledRelationship, MatrixEvent, RelationType } from 'matrix-js-sdk'; - -export type ThreadSummaryData = { - count: number; - latestTs: number | undefined; -}; - -/** - * Summary data for a thread root's "N replies" chip. - * - * Prefers the live {@link Thread} object when it exists (it reflects local - * echo + pagination), otherwise falls back to the server-aggregated bundle - * (`unsigned['m.relations']['m.thread']`) so the chip renders before any - * Thread object has been created. Returns `undefined` when the root has no - * thread at all. - */ -export const getThreadSummary = (rootEvent: MatrixEvent): ThreadSummaryData | undefined => { - const thread = rootEvent.getThread(); - if (thread) { - const lastReply = thread.lastReply(); - return { - count: thread.length, - latestTs: lastReply?.getTs(), - }; - } - - const bundle = rootEvent.getServerAggregatedRelation( - RelationType.Thread, - ); - if (bundle) { - return { - count: bundle.count, - latestTs: bundle.latest_event?.origin_server_ts, - }; - } - - return undefined; -}; - -/** - * True when `event` is a still-in-flight (local echo) reply belonging to the - * given thread root. Used to render the pending strip, since pending thread - * sends never enter the thread's timelineSet. - */ -export const isPendingThreadReply = (event: MatrixEvent, threadRootId: string): boolean => { - const { status } = event; - if (status !== EventStatus.SENDING && status !== EventStatus.NOT_SENT) return false; - - // Prefer the SDK's resolved thread root id; fall back to the raw relation - // content for events the SDK hasn't associated with a thread yet. - if (event.threadRootId === threadRootId) return true; - - const relation = event.getRelation(); - return relation?.rel_type === RelationType.Thread && relation.event_id === threadRootId; -};