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
59 lines
2.2 KiB
Python
59 lines
2.2 KiB
Python
"""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")
|