Repository navigation
[TW-7709] feat(@ownmail/app): natural reading for long threads - #380
Conversation
…and canvas mock data Foundation for the reading-experience redesign: a pure thread reading model (folding, new-since divider, participants, recipient summaries, system events, thread files with version folding, catch-up facts, asks-you detection, bundles, edge-state helpers), partial thread loading with failedMessageIds, untrusted-header facts and raw-header server functions, a muted-threads preference, and mock data reproducing every design board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
…anches Restore the 100% coverage gate: cover folded runs with no dates and no owner, blank header names, and the weekday helper on every day of the week; drop an unreachable fallback in the Authentication-Results parser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
… shelf Board 04 long-thread tools: CatchUpCard with the facts row and an optional summary behind ThreadSummaryProvider, ThreadMapPanel (docked glass panel from xl, sheet below) with filters and the stopped rule, and ThreadFilesShelf with folded versions. Adds person-dot, selected-row, amber and file-tile tokens and documents folding, catch-up, the thread map and the files shelf in design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
…d sender card Board 05 message anatomy: a one-line MessageHeader with recipient summary, hover actions and a details panel (verified sender, person chips, sender's local time, copy addresses, raw headers dialog); MessageNotices for Reply-To, lookalike or failed sender checks, removed participants and mailing lists; a SenderContactCard read from the signature; shared Avatar and AvatarStack on new person and notice tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
…map, stepper and reply card The standard reader now lays a thread out by the reading model: read runs fold into one stacked row, read messages next to open ones take one line, and what was unread at open opens under a "new since" divider, with the catch-up card above it on long threads. The header leads with the subject (or its stand-in), people, length, span and a files count that opens the files shelf. The toolbar gains the "12 of 14" stepper (j / k), Reply all as the labelled action and a More menu (mute, thread map, expand or collapse all, raw download); the thread map's dots sit beside the stream and g m opens its panel. The reply card is pinned under the stream, the phone gets its top bar, jump pill and reply bar, and past 50 messages rows mount only near the viewport. The route keeps its toolbar on a load error and passes header facts, failed message ids and known correspondents to the reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
Board 02 thread list (and board 01's list pane): day groups (Today,
Yesterday, This week, months) with times in the words the group leaves to
each row; rows that name their people ("Mary, Grace, Alan, you +1"), the
snippet's sender or "You:", and signal pills ("3 new" from the
conversation's messages loaded once on screen, "Asks you", the newest
file); invitation rows with lazy Accept/Decline on the calendar's own
cache entry; draft, not-sent (Edit and resend), scheduled and muted rows;
newsletter and receipt bundles that open in place and that j/k steps
past; Mute/Unmute in the row menu; "{n} unread" in the toolbar; the open
row as a green tint in a uniform 1px ring. Compact and Condensed keep the
first signal. New list tokens with contrast tests; design.md gains a
"Thread list" section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
… reply bar
Board 03: MobileThreadTopBar (pinned 56px glass toolbar with back, subject
topic over "{n} messages · {p} people", archive and more), JumpToNewPill
("{n} more new" glass pill with useNewBelowViewport) and MobileReplyBar
(Reply all labelled, Reply and Forward square icons) filling the mobile
tab bar's thread slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
Board 06 edge states and the quoted/signature pills of boards 01 and 05: quiet pills for trailing quoted history and signatures (split into quoted cards and the signature on open), plain-text reflow with per-paragraph direction and code chips, held-size striped placeholders for blocked images with a hidden-images bar, a fit-to-width bar with actual size and open in window, inert links for untrusted senders, the long-message frame (reading time, progress, outline, remembered position), and the empty body, draft-in-thread, not-sent/scheduled, trash and side-thread notices, superseded invitations (update count, struck earlier time, earlier versions), the reply-all guard in the composer, partial-load and route error cards, and the attachment chip. Adds edge-state tokens for both themes and documents the states in design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
… and files shelf Below xl, choosing a map row closes the sheet so the opened message shows, and Escape in the From… menu closes only the menu. The docked map gives focus back when it closes, the rows are one Tab stop, and repeated attachment chips fold into a count with at most three per row. The filter group and the subject-change row now match board 04. The catch-up summary survives providers that throw or return non-lines, keys bullets by position, and its mock bullets now live on the server and answer only in mock mode. Shelf thumbnails are capped at 512 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
…r card - List notice offers Unmute thread on muted threads; a List-Id that names no recipient no longer invents an address; list size only for broadcasts. - Details panel: lists show "pm-leads@ · 12" from optional listSizes; other nameless addresses show in full; panel follows the summary in tab order. - Sender card: non-modal hover card (no role=dialog), Escape closes it from anywhere, click/Enter pins rather than toggles, sits beside the heading, and shows no save or signature details for suspicious senders. - Lookalike check ignores the sender's own address in knownSenders. - Keyboard "Collapse message" control with aria-controls on open headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
The map draws your own dots on the green pair, fits its column to the
viewport on long threads (dots draw closer, one tab stop, arrow keys move
between them) and leaves 8px around the "stopped here" rule. Rows past 50
messages mount against the reader's own scroll viewport, through one shared
observer. A draft that follows a folded message stays in the stream, the
catch-up card shows on a thread that is all new, and Collapse all folds the
runs again.
Focus follows the reader: a row clicked open or a run unfolded takes focus,
a focused message drives the stepper and the map, and j / k beside a list
step the reader unless that list is on screen and focused. g m works from a
focused list row and closes the map's sheet. The map panel, files shelf,
trashed messages shown and the current message survive a new reply, and a
#message link is followed once.
`r` means Reply everywhere (it lands on "Reply only to {name}"), and a reply
to your own latest message goes to the first other person on it. The header
leaves its right-click menu to the message header, scheduled sends no longer
offer Send now and Cancel that did nothing, labels show on phones, and the
phone More menu opens the files shelf. Toolbar controls are 32px with 2px
gaps, Mark unread uses the Mail glyph and Reply all the cta-line border. The
Conversation view keeps its centred column. Map-dot and reply-shadow tokens
move to tokens.css, and design.md "Reading" describes the reader as built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 36d517522d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…to board 03
The reply bar is 76px on a phone without a safe-area inset (the inset takes
its bottom room first), and the jump pill sits 20px above it. The pill button
is the glass pill itself with a 44px hit layer, and shows focus as an outline,
which panel glass does not override. The top bar names a blank subject
"(no subject)" and keeps label-like leads ("Action required: ...") whole.
The unused useNewBelowViewport hook is removed: the reader counts and jumps
to new messages itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
Rows drop label badges (they stay in the reader), part on a lighter
--list-line rule, name drafts in --list-draft, and space the muted bell
6px from its list. The open row loses its rule under the ring and keeps
its unread weight and "{n} new" while open, as board 01 shows, also when
opened from a link.
"{n} new" and the newest file now come from one light message listing per
thread (getThreadUnreadSummary: no bodies, no focus refetch, asked again
only when a new message arrives) instead of loading whole conversations.
A bundle opened by its open conversation folds on a click and stays open
after the conversation closes; the keyboard cursor follows its row by key,
so j out of a bundle and Escape land on the right row (or the bundle).
Bundle rows get the row link's focus ring. Row links read out every state
the row shows. Rows without day groups (search) carry their own day, the
list hydrates on the server's minute, and invitation rows recheck while
syncing and offer no answers after a failed lookup. design.md follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
… header facts Muted threads are now stored per mailbox in mutedThreadsByAccount, keyed by the normalized account email like hidden calendars, with the 200-thread cap per mailbox and at most 20 mailboxes kept. Header facts and the List-Unsubscribe lookup follow the provider cursor for up to 4 pages (200 messages) and merge results; any failure still answers with no facts. Adds the minor changeset for the redesigned reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 129be68d79
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…he exact receiving host A sibling host on the receiver's site (evil.messagingengine.com behind mx3.messagingengine.com) may be a sender-written header the receiver never stripped, so it could forge dmarc=pass. Require the authserv-id to equal the topmost Received `by` host; Google and Microsoft 365 rules are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 111256dfd2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Relaxed DMARC alignment compares organizational domains, so sibling subdomains such as bounces.example.com and accounts.example.com align. The suffix-only comparison reported such senders as unverified when the receiver's header carried no DMARC result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 986207d128
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… public suffix list Comparing organizational domains with a partial suffix list let unrelated owners under multi-label suffixes (victim.ne.jp, attacker.ne.jp) align, so an attacker's SPF or DKIM pass could mark a victim's address verified. Go back to same-domain or subdomain alignment: a missed sibling only withholds the badge, it never grants it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7a972a21cc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A topmost Received hop by a bare IPv6 address proves no provider, so a forged `mx.google.com; dmarc=pass` header behind a non-Google receiver could mark a spoofed sender verified. Trust Google's authserv-id only behind a google.com hop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c8f64f7f7d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
mockSaveDraft now keeps an existing draft's thread_id across edits and derives it from the replied-to message for a new reply draft, so the thread lists it in draft_ids and latest_draft_or_message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5579768ccf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…re changing its schedule Send now cancelled the schedule and then sent, so a failed send lost the message; Cancel reopened the composer with only To/Cc, subject and plain text, losing Bcc, HTML and attachments. Both now read the whole scheduled message (To, Cc, Bcc, Reply-To, subject, HTML body, attachments, reply target) and save it as a draft before the schedule is cancelled. Cancel returns that draft id and the reader, search reader and list row open the composer on it (ComposeRequest kind 'draft'). Send now sends the same message, then removes the safety draft; when the send fails the draft stays and the receipt says "Couldn't send. Saved as a draft." with its id so the UI reports it and opens it. A failed cancel never sends: if the message is still scheduled the copy is removed, otherwise it is kept. The composer carries a draft's Cc and Bcc through every save and the send (shown read-only), since draft updates replace the whole draft. Dev mocks store Cc/Bcc/Reply-To on drafts and sent messages, serve a stored message's files, and mint unique ids after deletions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
…y loaded When some messages of a thread failed to load, the newest message may be the missing one, yet the thread reader and the search reader still offered Reply, Reply all, Forward, the inline reply card, the `r` shortcut and the phone reply bar against the last message shown. With failedMessageIds present the toolbar keeps Reply all, Reply and Forward in place but disabled (titled "Load the whole conversation to reply (Try again)"), the reply card and the phone reply bar give way to a note that says why and offers Try again, the conversation view gets no reply bar, and `r` does nothing. Reading stays available. The list-row path already refused; design.md now describes the reader's side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
… thread partly loads Opening a thread marked the whole thread read even when some messages failed to load, so messages nobody had seen lost their unread state. The thread is now marked read only after a complete load: the loader's optimistic read waits for the conversation and is rolled back (row and folder badge return to unread) when the load failed or left messages out. The reader then marks only the unread messages it shows, one by one, through a new markMessagesRead server function (per-message updateMessage; a mailbox client without it marks nothing and the thread stays unread). The reader's detail reflects only the messages the provider confirmed, the list and badge keep the thread unread, and a Try again that completes the load marks the whole thread read. The dev mock mailbox supports per-message updates and recomputes the thread's unread state from its messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
Keys: j and k step the open thread with nothing focused (a focused list row keeps them); Escape closes a docked thread map, and only the map; the route leaves an Escape a dialog or popover already spent alone; the raw headers dialog hands focus back to "Show raw headers". Mail: a reply sent from a thread keeps it in Inbox and adds it to the open reader; a discarded draft leaves its thread; drafts sit under the message they answer, open clear of the reply card and name people the thread knows; replies in a bounced thread answer the conversation, not the mail server; a thread opened offline says so with Try again (the reader's code comes with the list and mailbox lookups fail at once); the mock app knows the pm-leads list size; the map docks from xl. Boards: Compact and Condensed keep a "Not sent" pill; the split list is 360px; quiet pills and counts use the quiet and control tokens; header, action group, attachment chip, notices, details panel, sender card, catch up, map and files shelf take the boards' measures; bold is semibold; reader icons draw a 1.75 stroke; the divider is named for where you left off; phones hide labels and folded stacks end with you; the invitation card renders its loading state first so hydration matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
Reader: the new-since snapshot also takes what the route loader saw as unread, so a detail fetched again (already read) before the reader first draws still opens on the divider, Alan's message and the jump pill; j / k step messages from the open thread's own list row; a hash change on the page opens the linked message; the toolbar is a named toolbar; the divider's separator names the count and the place. Reply: the card names people the thread knows when your own reply gives addresses alone; a long recipient list scrolls in four rows and the inline composer grows rather than squeezing the message; a thread that failed to load offline loads again once back online. Lists: a discarded draft leaves its list row at once; toned rows keep their ground on hover; Condensed bundles line up with their neighbours; bundle and one-line rows take the font's own line height. Details: sticky reader chrome sits over the scroll fades; reading progress starts at 0%; the message text takes the board's ink; quoted pills keep their tone when open; map filter segments are equal and the map's event rows take the board's padding; the In Trash dot is spaced; an older invitation note is set apart from the text; the empty-body chip's paperclip takes its text colour; image placeholders keep 11px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
- Quote a selection across paragraphs as one quote (empty "> " lines).
- Name recipients sent without a name by the thread's name for them, and
give an address alone a sentence-case first name ("to Alan", "to Mary").
- Folded avatar stack follows the names' order; the roadmap mock has
Katherine answer before Ada, as board 01 reads.
- Invite and failed-send lines keep their full copy in the 360px list:
the buttons move under the words when both do not fit.
- Phone bars: bar icon ink token and a 1.75 stroke on the bottom bar.
- Header row gap 10px on a phone, 12px from md; new-since separator is
named with the words it shows.
- Spacing: quoted pills 14px under the body, outgoing status 14px apart,
partial-load card clear of the first row, stand-in caption 8px, guard
padding 8px 10px; outline level with the progress bar; image and fit
bars inset with the message furniture; notice addresses never wrap.
- Catch-up summary bullets in body ink.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
- Copy text, Edit and resend and Forward keep a message's line breaks (messageBodyText), so "Thanks,<br>Ada" stays on two lines. - Quote in reply with nothing selected keeps the message's paragraphs apart inside one quote. - The thread map column runs the reader's full height, so a click anywhere in it opens the map; dot tooltips use first names. - Split quoted/signature pills are 26px in quiet ink (board 05). - Long message progress track gets its own token (--progress-track). - Bundle Noto Naskh Arabic (Arabic subset, unicode-range) for RTL text. - Mock draft and scheduled snippets match board 02 copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
- Answer list-row Reply, Reply all and Forward from the latest sent message (replyBase), never an unsent draft or a bounce, and send a plain Reply to replyTarget, as the reader toolbar does. - Keep in-thread drafts out of the Conversation view transcript; show them as draft cards after it. The reply bar answers replyBase and names the same person the Reply action targets. - Disable links in mail from anyone else until the sender checks have answered, and keep them disabled with an amber notice when the lookup fails. The Always for offer and Open in window wait too. - getThreadHeaderFacts rejects on failure instead of answering no facts, so a failed re-ask never drops an earlier authFailed verdict. - Apply the links-disabled rule and sender-check notices in the Conversation view (bubbles, articles and cards). - Leave header lookups, row unread summaries and the side-thread search out of mail and fallback refreshes; key header facts on the newest sent message so a new reply is still checked once. - Search reader: Escape that a dialog or sheet already handled no longer closes the conversation; Reply targets replyTarget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
…hash and resume handling - One classifier, sameSenderDomain (lib/sender-domain.ts), now decides both the lookalike notice and the inert links, so sibling subdomains of a known sender (mail.example.com, news.example.com) no longer lose their links silently. Siblings under a short-generic ccTLD suffix (co.uk, ne.jp) stay apart and warn. The duplicate domain test in thread-stream is gone. - hashTarget treats a hash that does not decode (#message-%) as no target instead of throwing in the reader's mount effect. - A long message saves at most the offset of its end, so scrolling past it no longer stores a position the resume check rejects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 88c5814948
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Phone fold plan keeps only the first message as a line: the open
message's read neighbours stay in the run ("10 earlier messages" straight
above the divider, board 03).
- Mailing lists stand for the members they reached: the thread header,
list rows and the reply card read "Alan, eng-all@ +238", and a nameless
list is named by its address, never a made-up first name.
- Organisations keep their whole name where a first name would go
("reply to Bright Smile Dental", "Reply only to The Dispatch").
- Default rows run the snippet the full width when no signal ends it;
Compact and Condensed drop a snippet with no room instead of " · …".
- Edge links use the link green in both themes; quoted excerpt spacing,
the map's "g m" key, the phone pill's 92% glass and the held-image
stripes follow the boards (stripes as tokens).
- Header time test no longer fails just after midnight.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a5fde2d7b9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… branch Bring in the draggable reading-pane split (TW-7708) and the CNAME image host fix (TW-7707). The resizable split keeps this branch's 22.5rem (360px, board 01) vertical list as its default: the layout classes use min(var(--mail-list-width,22.5rem), calc(100% - 360px)), the divider's unmeasured fallback is 360px, and the Condensed rule still widens the default to 26rem unless the person dragged a width. The divider sits between the list and reader sections inside the thread-list environment on the folder and search screens, and design.md now describes the split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17224ef0f0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…den resend and file listing Sender domains match only on the same host or a subdomain, and a message the settled header lookup left out is treated as unchecked. Edit and resend copies the bounced message, files included, into a server-side draft to the bounced address. The files shelf keeps distinct attachment ids, List-Unsubscribe and the row unread summary refresh as threads change (the summary pages up to 200 messages and says "200+ new"), and each message's reply actions wait during a partial load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
…ardening fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK # Conflicts: # labs/ownmail/packages/app/src/routes/mail.search.tsx
…se the last review findings
Packed CLI size (smoke:ownmail-release, 3.5 MB budget unchanged):
3,837,317 bytes before, 3,416,085 after (main: 3,138,719).
- Minify the Vercel (Nitro) build like the Cloudflare and Node builds
already are: its server function drops from ~1.01 MB to ~0.60 MB gzipped
in the pack. The minified function still serves /healthz and SSR pages.
- Keep the mock mailbox out of deployed builds and the packed template.
nylas.ts and platform.ts read it through server/dev-mailbox.ts, which
answers null; vite.config.local.ts (dev:ui and the Node build the SSR
smoke test serves) resolves it to dev-mocks.ts. stage-template leaves
dev-mocks.ts out, and mock mode now needs the bundled mock mailbox.
- The template source no longer imports the dropped dev-mock fixture, so
an ejected or exported app typechecks and builds again; smoke-release now
fails if the template source imports a file the template does not ship.
Coverage and time zones:
- zoneOffsetMinutes is tested against every offset shape ICU writes
("GMT", "GMT+05:30", "GMT-07:00", "GMT−08:00"), so the unsigned fallback
is covered whatever the runtime's ICU data.
- The list-date test uses local times, so it passes in any zone (it failed
under TZ=America/Los_Angeles).
Review findings:
- P1 MessageNotices: "Leave list" (the sender's List-Unsubscribe link) is
left out whenever that message's links are disabled (failed checks,
lookalike, or checks not answered), using the reader's linksDisabled
decision.
- P1 saveDraft: the composer names the files a draft already holds
(keepAttachments) on every save, and the server copies them from the
draft next to the composer's own, so a cancelled scheduled message's or
an Edit and resend draft's files survive edits without doubling.
- P2 sendScheduledNow: removing the safety draft after a send is retried
once; if it still stays, the receipt warns "Sent. The safety copy in
Drafts couldn’t be removed — delete it so it isn’t sent twice.", shown on
the reader and the list row.
- P2 LongMessageFrame: the reading position key is scoped to the
normalized mailbox account; without one nothing is kept.
design.md says each of these where the reader's behaviour is described.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 15b5a947fc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| function copiedRecipients(draft: Pick<MailDraft, 'cc' | 'bcc'> | null): { cc?: string; bcc?: string } { | ||
| const emails = (people: MailDraft['cc']) => (people ?? []).map((person) => person.email).join(', ') | ||
| const cc = emails(draft?.cc) | ||
| const bcc = emails(draft?.bcc) | ||
| return { ...(cc ? { cc } : {}), ...(bcc ? { bcc } : {}) } |
There was a problem hiding this comment.
Preserve the draft's custom Reply-To address
When a canceled scheduled message has a custom reply_to, the safety draft initially contains it, but this preservation helper carries only Cc and Bcc. The composer's mandatory save before sending performs a full draft replacement without reply_to, so subsequent replies go to the sender address instead of the intended address. Fresh evidence beyond the earlier cancellation fix is that copiedRecipients still omits draft.reply_to; carry it through both saveDraft and sendDraft alongside the other preserved recipients.
Useful? React with 👍 / 👎.
| ...(message.reply_to?.length ? { reply_to: message.reply_to } : {}), | ||
| subject: message.subject ?? '', | ||
| body: message.body ?? '', | ||
| ...(restored.length > 0 ? { attachments: normalizeOutboundAttachments(restored) } : {}), |
There was a problem hiding this comment.
Retain inline attachment disposition in safety copies
When a scheduled message contains a CID-backed inline image, restoreDraftAttachments supplies is_inline and content_id, but this call passes it through normalizeOutboundAttachments, whose returned objects contain only filename, type, and content. The draft is therefore created before cancellation without the metadata that connects the image to the HTML body, permanently leaving the canceled draft—or the Send now result—with broken cid: images. Fresh evidence beyond the earlier generic attachment fix is this new normalization step on trusted provider attachments; preserve the inline fields in these server-side copies.
Useful? React with 👍 / 👎.
| ...(message.reply_to?.length ? { reply_to: message.reply_to } : {}), | ||
| subject: message.subject ?? '', | ||
| body: message.body ?? '', | ||
| ...(restored.length > 0 ? { attachments: normalizeOutboundAttachments(restored) } : {}), |
There was a problem hiding this comment.
Do not apply compose-upload limits to provider attachments
When an existing scheduled message has more than ten attachments or more than 2 MB total—a valid provider-side message—this normalization throws before the safety draft is created. Consequently both Cancel and Send now always fail for that message, even though the schedule remains intact, because the browser-upload cap is being applied to files downloaded from the provider; use a separate trusted-provider validation path or otherwise support the provider's attachment limits.
Useful? React with 👍 / 👎.
Implements the OwnMail reading-experience design canvas (TW-7709): thread list, reader, long threads, message headers, the phone thread and edge states.
What's in it
g m).Authentication-Resultswritten by the receiving server, and only for the From domain shown.@nylas-labs/cli-kitgainscancelScheduledMessage.main: the new resizable reading-pane split defaults to this branch's 360px list.design.mddocuments the new behaviour. Changesets cover@ownmail/appand@nylas-labs/cli-kit.Validation
All CI steps were run locally, as
ci.ymlruns them:smoke:ssrandsmoke:ownmail-release.The app was also checked against the design boards in the running mock app (
pnpm dev:ui) over five rounds, and every blocking finding from those rounds is fixed.🤖 Generated with Claude Code
https://claude.ai/code/session_01RmfD7PQwamKFEKBWCqi3gK