diff --git a/backend/app/api/schemas.py b/backend/app/api/schemas.py index 72c47a9..f365dd4 100644 --- a/backend/app/api/schemas.py +++ b/backend/app/api/schemas.py @@ -17,7 +17,6 @@ class ThreadSummary(BaseModel): id: uuid.UUID subject: str snippet: str - senders: list[Address] participants: list[Address] account_ids: list[uuid.UUID] message_count: int diff --git a/backend/app/api/threads.py b/backend/app/api/threads.py index 9bc503b..7413647 100644 --- a/backend/app/api/threads.py +++ b/backend/app/api/threads.py @@ -38,7 +38,6 @@ 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 diff --git a/backend/app/models.py b/backend/app/models.py index b007e34..96fe85d 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -152,13 +152,6 @@ 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("'[]'") ) diff --git a/backend/app/threading_/threader.py b/backend/app/threading_/threader.py index c2244c2..0509090 100644 --- a/backend/app/threading_/threader.py +++ b/backend/app/threading_/threader.py @@ -197,16 +197,6 @@ 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) diff --git a/backend/migrations/versions/0019_thread_senders.py b/backend/migrations/versions/0019_thread_senders.py deleted file mode 100644 index e58039f..0000000 --- a/backend/migrations/versions/0019_thread_senders.py +++ /dev/null @@ -1,58 +0,0 @@ -"""threads.senders — who wrote, as distinct from who was involved - -The message list showed a thread's `participants`, which the threader builds from -from + to + cc because the subject-fallback join matches on recipients. In a list -that meant every ordinary email read "Sender, You" — and the "+N" badge counted -the user themselves. - -`senders` holds only the from addresses, newest writer first. Backfilled here so -existing threads do not have to wait to be touched again. - -Revision ID: 0019 -Revises: 0018 -Create Date: 2026-09-18 -""" - -from __future__ import annotations - -from collections.abc import Sequence - -from alembic import op - -revision: str = "0019" -down_revision: str | None = "0018" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - op.execute("ALTER TABLE threads ADD COLUMN senders jsonb NOT NULL DEFAULT '[]'") - # Newest writer first, de-duplicated by lower-cased address, matching what - # denormalize_thread computes from now on. - op.execute( - """ - WITH ranked AS ( - SELECT m.thread_id, - lower(a->>'email') AS email_key, - jsonb_build_object('name', coalesce(a->>'name', ''), - 'email', coalesce(a->>'email', '')) AS addr, - row_number() OVER (PARTITION BY m.thread_id, lower(a->>'email') - ORDER BY m.internal_date DESC) AS dup_rank, - max(m.internal_date) OVER (PARTITION BY m.thread_id, lower(a->>'email')) - AS recency - FROM messages m - CROSS JOIN LATERAL jsonb_array_elements(m.from_addr) AS a - WHERE m.thread_id IS NOT NULL AND coalesce(a->>'email', '') <> '' - ), - collected AS ( - SELECT thread_id, jsonb_agg(addr ORDER BY recency DESC) AS senders - FROM ranked WHERE dup_rank = 1 GROUP BY thread_id - ) - UPDATE threads t SET senders = c.senders - FROM collected c WHERE c.thread_id = t.id - """ - ) - - -def downgrade() -> None: - op.execute("ALTER TABLE threads DROP COLUMN IF EXISTS senders") diff --git a/frontend/src/components/list/MessageList.tsx b/frontend/src/components/list/MessageList.tsx index 8437e16..0bd6233 100644 --- a/frontend/src/components/list/MessageList.tsx +++ b/frontend/src/components/list/MessageList.tsx @@ -119,12 +119,10 @@ export function MessageList({ const selected = selectedId === thread.id; const checked = checkedIds.has(thread.id); const dimmed = dimmedIds?.has(thread.id) ?? false; - // Senders, not participants: the latter includes recipients, so every - // ordinary email read "Sender, You" and the +N badge counted the user. - const people = thread.senders.length ? thread.senders : thread.participants; - const primary = people[0]; - const names = people.map((p) => p.name || p.email).join(", ") || "(no one)"; - const extra = Math.max(0, people.length - 1); + const primary = thread.participants[0]; + const names = + thread.participants.map((p) => p.name || p.email).join(", ") || "(no one)"; + const extra = Math.max(0, thread.participants.length - 1); const avatarBg = `hsl(${hue(primary?.email || primary?.name || "?")} 45% 45%)`; const stop = (fn: () => void) => (e: React.MouseEvent) => { e.stopPropagation(); diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 392150a..258ef34 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -31,9 +31,6 @@ export interface ThreadSummary { id: string; subject: string; snippet: string; - /** Who wrote in the thread, newest first. The list shows these. */ - senders: Address[]; - /** Everyone involved, recipients included — used for threading, not display. */ participants: Address[]; account_ids: string[]; message_count: number;