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
@@ -22,6 +22,7 @@ import {
|
||||
accountOptions,
|
||||
filterCounts,
|
||||
isUnread,
|
||||
manualFlowLabel,
|
||||
needsReply,
|
||||
partitionConversations,
|
||||
triageStatus,
|
||||
@@ -53,17 +54,41 @@ interface ConversationRowProps {
|
||||
registerRef(element: HTMLButtonElement | null): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A chip's visible label, plus the longer form used for screen readers and the
|
||||
* hover tooltip when the label alone does not say why the chip is there.
|
||||
*/
|
||||
interface ConversationChip {
|
||||
label: string;
|
||||
hint?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
/** Independent state chips. A conversation can be several of these at once. */
|
||||
function conversationChips(conversation: ActiveChat, now: number): string[] {
|
||||
const chips: string[] = [];
|
||||
if (isUnread(conversation)) chips.push('Unread');
|
||||
if (needsReply(conversation)) chips.push('Needs reply');
|
||||
else if (conversation.isFromUs && triageStatus(conversation) === 'open') chips.push('Waiting on them');
|
||||
function conversationChips(conversation: ActiveChat, now: number): ConversationChip[] {
|
||||
const chips: ConversationChip[] = [];
|
||||
if (conversation.recipientAccountStatus === 'deleted') {
|
||||
chips.push({ label: 'Account deleted' });
|
||||
return chips;
|
||||
}
|
||||
if (isUnread(conversation)) chips.push({ label: 'Unread' });
|
||||
if (needsReply(conversation)) chips.push({ label: 'Needs reply' });
|
||||
else if (conversation.isFromUs && triageStatus(conversation) === 'open') chips.push({ label: 'Waiting on them' });
|
||||
const manualLabel = manualFlowLabel(conversation);
|
||||
if (manualLabel) {
|
||||
chips.push({
|
||||
label: 'Manual',
|
||||
hint: manualLabel,
|
||||
tooltip: conversation.automationHandoffReason
|
||||
? `${manualLabel} — ${conversation.automationHandoffReason}`
|
||||
: manualLabel,
|
||||
});
|
||||
}
|
||||
const status = triageStatus(conversation);
|
||||
if (status === 'snoozed') chips.push('Snoozed');
|
||||
if (status === 'done') chips.push('Done');
|
||||
if (status === 'snoozed') chips.push({ label: 'Snoozed' });
|
||||
if (status === 'done') chips.push({ label: 'Done' });
|
||||
if (needsReply(conversation) && conversationAge(conversation.lastMessageTime, now) !== 'fresh') {
|
||||
chips.push('Overdue');
|
||||
chips.push({ label: 'Overdue' });
|
||||
}
|
||||
return chips;
|
||||
}
|
||||
@@ -82,7 +107,7 @@ function ConversationRow({
|
||||
|
||||
const descriptors = [
|
||||
redditUsernameLabel(conversation.username),
|
||||
...chips,
|
||||
...chips.map((chip) => chip.hint ?? chip.label),
|
||||
relativeTime,
|
||||
...(conversation.sellerDetected ? ['Seller'] : []),
|
||||
...(conversation.hasReceivedImages ? ['Image received'] : []),
|
||||
@@ -122,8 +147,12 @@ function ConversationRow({
|
||||
</button>
|
||||
<span className="conversation-list-chips">
|
||||
{chips.map((chip) => (
|
||||
<span className={`conversation-chip is-${chip.toLowerCase().replaceAll(' ', '-')}`} key={chip}>
|
||||
{chip}
|
||||
<span
|
||||
className={`conversation-chip is-${chip.label.toLowerCase().replaceAll(' ', '-')}`}
|
||||
key={chip.label}
|
||||
title={chip.tooltip}
|
||||
>
|
||||
{chip.label}
|
||||
</span>
|
||||
))}
|
||||
{showOwner ? <AccountOwnerBadge owner={conversation.owner} /> : null}
|
||||
|
||||
Reference in new issue
Block a user