feat: recover missing conversations, fix media attachment, add manual inbox tab
Missing conversations: recipient Reddit accounts that are deleted or suspended left conversations unaccounted for. A new RecipientAccountStatusService polls Reddit for the users behind cached conversations and records their account status durably, with progressive backfill and delta-media fixes in ChatService so history and images are no longer dropped mid-sync. Media: attachMedia now surfaces download failures as MediaAttachmentError so the sync cursor can be retried instead of silently losing an image, recognizes image bytes when Matrix returns a generic content type, and keeps separate gallery occurrences when the same asset appears in more than one event. Manual inbox tab: the conversations list now carries the reply-loop status, so the inbox can separate conversations the operator is driving by hand from the ones automation is still handling. A Manual filter pill and a per-row chip explain why each conversation was handed over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
725b5fc4e6
commit
ac12a3146a
35 files changed
+1164
-129
No files matched your search
@@ -20,4 +20,55 @@ describe('MediaSyncService account authentication gate', () => {
|
||||
expect(chat.refreshTopChats).not.toHaveBeenCalled();
|
||||
expect(chat.fetchAllMessages).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports the actual number of rooms changed by an incremental sync', async () => {
|
||||
const chat = {
|
||||
refreshTopChats: vi.fn().mockResolvedValue(3),
|
||||
fetchAllMessages: vi.fn(),
|
||||
};
|
||||
const service = new MediaSyncService(
|
||||
chat as never,
|
||||
createMockLogger(),
|
||||
30 * 60 * 1000,
|
||||
{ isFullyAuthenticated: () => true },
|
||||
);
|
||||
|
||||
await expect(service.triggerSync()).resolves.toBe(true);
|
||||
expect(service.getStatus().lastSyncChats).toBe(3);
|
||||
});
|
||||
|
||||
it('requires a complete historical backfill for a full media sync', async () => {
|
||||
const chat = {
|
||||
refreshTopChats: vi.fn(),
|
||||
fetchAllMessages: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
const service = new MediaSyncService(
|
||||
chat as never,
|
||||
createMockLogger(),
|
||||
30 * 60 * 1000,
|
||||
{ isFullyAuthenticated: () => true },
|
||||
);
|
||||
|
||||
await service.triggerSync(true);
|
||||
expect(chat.fetchAllMessages).toHaveBeenCalledWith({ requireComplete: true });
|
||||
});
|
||||
|
||||
it('returns failure and preserves the operator-visible error when sync work is incomplete', async () => {
|
||||
const chat = {
|
||||
refreshTopChats: vi.fn().mockRejectedValue(new Error('media history incomplete')),
|
||||
fetchAllMessages: vi.fn(),
|
||||
};
|
||||
const service = new MediaSyncService(
|
||||
chat as never,
|
||||
createMockLogger(),
|
||||
30 * 60 * 1000,
|
||||
{ isFullyAuthenticated: () => true },
|
||||
);
|
||||
|
||||
await expect(service.triggerSync()).resolves.toBe(false);
|
||||
expect(service.getStatus()).toMatchObject({
|
||||
lastSyncAt: null,
|
||||
lastError: 'media history incomplete',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user