fix(build): complete the threadSummary rename — remove the old casing
The deletions from the git-mv in 992d2b83 were unstaged by a concurrent
worktree operation before commit, so the pushed tree contained BOTH
threadSummary.ts and threadSummaryData.ts (and the Windows case-collision
persisted). This commit removes the stale originals; caseCollision.test.ts
would have failed CI on the incomplete state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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<IThreadBundledRelationship>(
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user