Mail list: show who wrote, not everyone involved
The first column listed a thread's `participants`, which the threader builds from from + to + cc — so every ordinary email read "Sender, You", and the +N badge beside the avatar counted the user themselves. `participants` cannot simply be narrowed: the subject-fallback join matches an incoming message's recipients against it (threader._by_subject), so dropping recipients would change how mail is threaded. Hence a separate `threads.senders`, holding only from addresses, newest writer first — which is also the right thing for a conversation where several people replied. Migration 0019 backfills it, so existing threads do not wait to be touched again. The list falls back to `participants` if a row somehow has no senders, rather than rendering an empty column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
This commit is contained in:
@@ -17,6 +17,7 @@ class ThreadSummary(BaseModel):
|
||||
id: uuid.UUID
|
||||
subject: str
|
||||
snippet: str
|
||||
senders: list[Address]
|
||||
participants: list[Address]
|
||||
account_ids: list[uuid.UUID]
|
||||
message_count: int
|
||||
|
||||
@@ -38,6 +38,7 @@ def _summary(thread: Thread, message_count: int | None = None) -> ThreadSummary:
|
||||
id=thread.id,
|
||||
subject=thread.subject_norm or "(no subject)",
|
||||
snippet=thread.snippet,
|
||||
senders=_addresses(thread.senders),
|
||||
participants=_addresses(thread.participants),
|
||||
account_ids=list(thread.account_ids),
|
||||
# Count only the messages this view actually shows (deleted/spam excluded
|
||||
|
||||
@@ -152,6 +152,13 @@ class Thread(Base):
|
||||
)
|
||||
subject_norm: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
snippet: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
|
||||
# Who WROTE in this thread, newest first. Distinct from `participants`, which
|
||||
# also holds recipients because the subject-fallback threading matches on them
|
||||
# (threader._by_subject). The message list shows senders; conflating the two
|
||||
# put "you" next to every sender in the list.
|
||||
senders: Mapped[list[dict[str, str]]] = mapped_column(
|
||||
JSONB, nullable=False, server_default=text("'[]'")
|
||||
)
|
||||
participants: Mapped[list[dict[str, str]]] = mapped_column(
|
||||
JSONB, nullable=False, server_default=text("'[]'")
|
||||
)
|
||||
|
||||
@@ -197,6 +197,16 @@ async def denormalize_thread(session: AsyncSession, thread_id: uuid.UUID) -> Non
|
||||
seen_emails.add(email)
|
||||
participants.append({"name": addr.get("name", ""), "email": addr.get("email", "")})
|
||||
|
||||
senders: list[dict[str, str]] = []
|
||||
seen_senders: set[str] = set()
|
||||
for msg in messages: # newest first => most recent writer leads
|
||||
for addr in msg.from_addr:
|
||||
email = (addr.get("email") or "").lower()
|
||||
if email and email not in seen_senders:
|
||||
seen_senders.add(email)
|
||||
senders.append({"name": addr.get("name", ""), "email": addr.get("email", "")})
|
||||
|
||||
thread.senders = senders
|
||||
thread.message_count = len(messages)
|
||||
thread.unread_count = sum(1 for m in messages if "\\Seen" not in m.flags)
|
||||
thread.has_attachments = any(m.has_attachments for m in messages)
|
||||
|
||||
Reference in New Issue
Block a user