Show sender and recipient photos

Featurekamo-internal
Shipped
August 3, 2026 at 2:32 AM UTC
Author
kamo
Commit
36ad904

The mailbox rendered a single letter on a single shared gradient for everyone, so every row looked the same and the circle carried no information. It now shows the correspondent's actual photo when one is known, and a two-letter monogram when it is not. One SenderAvatar replaces seven hand-rolled initials circles. That mattered for more than consistency: each carried its own idea of how to split an address, and the avatar cache is keyed on that split, so any disagreement between the list and the reading pane would silently halve the hit rate and show the same person two different ways on one screen. parseAddress is now the only place an RFC 5322 address is taken apart. The list and thread views also stop printing raw headers like "Jane Doe <jane@acme.com>" and show the name. Identity lives in its own store keyed by address, never on the message objects. Both list views poll every 30s and on every websocket notification, each time replacing their message array wholesale — anything merged onto a message would be wiped twice a minute, so an idle inbox would flicker photo to monogram and back forever. Keying by address also means a late-arriving answer cannot paint onto the wrong row, because rows are not what it is keyed by. Requests are coalesced into one batch per screen: fifty rows asking independently would be fifty requests, and since every call to this service mints a one-time key first, actually a hundred. A miss is re-asked once after 45s, because server-side resolution is two-phase and an unknown address is answered only on a later call. ConversationListView is now virtualized. It fetches 50 rows a page and re-rendered all of them inside an AnimatePresence with a per-index stagger on every poll; adding an image to each made it the heaviest surface in the mailbox. No-reply and mailer-daemon senders get a neutral mark rather than a monogram — a face beside a bounce notice is actively misleading. Photos are decorative (alt="") because the name is always visible text beside them, and carry referrerpolicy="no-referrer" so a thread identifier never reaches an image host. Every avatar is served from our own origin — third-party images are fetched, validated and re-hosted server-side — which is what lets the new per-request img-src policy stay pinned to the tenant's own domains. That policy is built in the proxy rather than next.config because this platform is white-labelled: a static policy would have to name one tenant and would break images for the rest. It also closes a pre-existing stored tracking-pixel vector, since Contact.photoUrl is a free-text URL rendered directly in several <img src>. next/image no longer accepts remote SVG. That was an acceptable risk when the only SVGs were our own theme logos; it is not now that the platform ingests marks published in third-party DNS. Contacts callers drop the hardcoded ORG_ID = 1 and the orgId query parameter, matching the backend change that derives tenancy from the session.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing