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,6 +20,8 @@ interface ChatFixtureOptions {
|
||||
lastMessageSnippet?: string;
|
||||
hasReceivedImages?: boolean;
|
||||
sellerDetected?: boolean;
|
||||
automationStatus?: 'active' | 'image-received' | 'human-required' | 'boundary' | 'user-overridden';
|
||||
automationHandoffReason?: string;
|
||||
}
|
||||
|
||||
function chatFixture(options: ChatFixtureOptions) {
|
||||
@@ -34,6 +36,10 @@ function chatFixture(options: ChatFixtureOptions) {
|
||||
unread: options.unread ?? false,
|
||||
...(options.hasReceivedImages === undefined ? {} : { hasReceivedImages: options.hasReceivedImages }),
|
||||
...(options.sellerDetected === undefined ? {} : { sellerDetected: options.sellerDetected }),
|
||||
...(options.automationStatus === undefined ? {} : { automationStatus: options.automationStatus }),
|
||||
...(options.automationHandoffReason === undefined
|
||||
? {}
|
||||
: { automationHandoffReason: options.automationHandoffReason }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,6 +275,30 @@ describe('ConversationsPage', () => {
|
||||
expect(screen.getByRole('img', { name: 'Image received from u/alice' })).not.toHaveClass('is-blurred');
|
||||
});
|
||||
|
||||
it('marks and filters conversations that are being replied to by hand', async () => {
|
||||
const manual = chatFixture({
|
||||
username: 'alice',
|
||||
roomId: 'room-alice',
|
||||
isFromUs: true,
|
||||
automationStatus: 'user-overridden',
|
||||
automationHandoffReason: 'A manual reply was sent in this conversation.',
|
||||
});
|
||||
const automated = chatFixture({ username: 'bob', roomId: 'room-bob', automationStatus: 'active' });
|
||||
server.use(http.get('/api/conversations', () => HttpResponse.json([manual, automated])));
|
||||
const user = userEvent.setup();
|
||||
renderConversations();
|
||||
|
||||
await screen.findByRole('button', { name: /u\/alice/ });
|
||||
expect(screen.getByText('Manual', { selector: '.conversation-chip' })).toHaveAttribute(
|
||||
'title',
|
||||
'Manual — you replied — A manual reply was sent in this conversation.',
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Manual, 1' }));
|
||||
expect(screen.getByRole('button', { name: /u\/alice/ })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /u\/bob/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('turns a suggestion into an editable draft and confirms sending without losing failed input', async () => {
|
||||
installConversationList();
|
||||
let sendRequests = 0;
|
||||
|
||||
Reference in new issue
Block a user