diff --git a/src/services/chat/service.ts b/src/services/chat/service.ts index d73b7c0..b37a358 100644 --- a/src/services/chat/service.ts +++ b/src/services/chat/service.ts @@ -1135,8 +1135,14 @@ export class ChatService { return; } - chat.lastMessageSnippet = chat.messages[chat.messages.length - 1].text; - chat.lastMessageTime = chat.messages[chat.messages.length - 1].timestamp; + const newest = chat.messages[chat.messages.length - 1]; + chat.lastMessageSnippet = newest.text; + chat.lastMessageTime = newest.timestamp; + // Re-derive authorship from the authoritative history. Room summaries built + // from /sync default `isFromUs` to false when the room carries no timeline + // events (Reddit omits them for idle rooms), which would otherwise leave an + // unanswered opener of ours sitting in the operator's "Needs reply" queue. + chat.isFromUs = newest.isSelf; const firstSelf = chat.messages.find((m) => m.isSelf); if (firstSelf) chat.originMessage = firstSelf.text; diff --git a/tests/chat-service-progressive-backfill.test.ts b/tests/chat-service-progressive-backfill.test.ts index 2d6e9ae..1e448c5 100644 --- a/tests/chat-service-progressive-backfill.test.ts +++ b/tests/chat-service-progressive-backfill.test.ts @@ -214,6 +214,56 @@ describe('ChatService progressive full backfill', () => { } }); + it('derives isFromUs from history when /sync returns no timeline for the room', async () => { + const logger = createMockLogger(); + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-authorship-backfill-')); + try { + const matrix = new MatrixClient(credentials, logger, { minRequestIntervalMs: 0 }); + // Reddit omits timeline events for idle rooms, so the seeded summary + // carries no authorship signal at all. + matrix.sync = vi.fn(async () => ({ next_batch: 's1', rooms: { join: {} } })) as typeof matrix.sync; + matrix.getJoinedRooms = vi.fn(async () => ['!unanswered:reddit.com', '!answered:reddit.com']); + matrix.getRoomMessages = vi.fn(async (roomId: string) => ({ + start: 'start', + chunk: roomId === '!unanswered:reddit.com' + ? [{ + event_id: '$opener', + type: 'm.room.message', + sender: '@me:reddit.com', + origin_server_ts: 1_000, + content: { body: 'Hello there' }, + }] + : [{ + event_id: '$reply', + type: 'm.room.message', + sender: '@t2_bob:reddit.com', + origin_server_ts: 2_000, + content: { body: 'Hi back' }, + }, { + event_id: '$opener', + type: 'm.room.message', + sender: '@me:reddit.com', + origin_server_ts: 1_000, + content: { body: 'Hello there' }, + }], + })); + + const service = new ChatService( + matrix, logger, undefined, undefined, undefined, undefined, + new ChatCache(logger, path.join(tmpDir, 'chat-cache.json')), + undefined, undefined, authenticatedAccountAuth, + ); + + const chats = await service.fetchAllMessages({ requireComplete: true }); + const unanswered = chats.find((c) => c.roomId === '!unanswered:reddit.com'); + const answered = chats.find((c) => c.roomId === '!answered:reddit.com'); + expect(unanswered?.isFromUs).toBe(true); + expect(answered?.isFromUs).toBe(false); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); + it('paginates beyond the former 50-page history ceiling', async () => { const logger = createMockLogger(); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-long-backfill-'));