Skip to content

[TW-7709] feat(@ownmail/app): natural reading for long threads - #380

Merged
AaronDDM merged 45 commits into
mainfrom
adm/jolly-knuth-tuf5o5
Oct 18, 2026
Merged

AaronDDM merged 45 commits into
mainfrom
adm/jolly-knuth-tuf5o5

Conversation

@AaronDDM

@AaronDDM AaronDDM commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Reader:
    • Thread header showing participants, message count, date range and file count.
    • Read runs fold into one stacked row, and a "N new since " divider marks where you left off.
    • Each message has its own Reply, Forward and Quote actions.
    • Message stepper (j/k, "12 of 14").
    • Thread-map dots, plus a filterable map panel (g m).
    • Inline reply card with "Reply only to …", and a reply-all guard above 25 people.
    • Threads over 50 messages are virtualised.
  • Long-thread tools:
    • A catch-up card summarising what happened since you last read.
    • An assistant summary slot, hidden unless a provider is configured.
    • A files shelf that folds older versions, with Download all.
  • Message anatomy:
    • One-line header and a details panel (verified sender, sender-local time, Copy addresses, raw headers).
    • Notices for:
      • a Reply-To that differs from the sender;
      • a lookalike sender or failed sender checks, which disables links, including Leave list;
      • a removed participant;
      • a mailing list, with Mute or Leave.
    • The sender's signature becomes a contact card.
  • Thread list:
    • Day groups, participant names, and "N new" counts (paged, so they can read "200+ new").
    • Signals: Asks you, inline RSVP, attachment chips.
    • Draft, failed-send and scheduled states.
    • Muted threads, stored per mailbox.
    • Newsletter and receipt bundles.
  • Edge states:
    • Empty body, and a stand-in when there is no subject.
    • Drafts shown inside the thread.
    • Scheduled messages with Send now and Cancel. Both keep a full safety draft, attachments included, and a safety copy that can't be cleaned up is reported.
    • Edit and resend goes to the bounced address only and keeps the original's files.
    • Notices for messages in Trash or a side thread, and for superseded invites.
    • Blocked images keep their size, and designed mail scales to fit.
    • Long-message progress and outline, with a reading position kept per mailbox.
    • Plain-text reflow with per-paragraph text direction.
    • Partial-load notices: every reply action is held, and only the messages shown are marked read.
  • Phone: compact top bar, "N more new" pill and a bottom reply bar.
  • Data and safety:
    • Thread loads tolerate partial failures.
    • Header lookups page through the thread, newest first.
    • Sender checks trust only Authentication-Results written by the receiving server, and only for the From domain shown.
    • Domains match only when they are equal or one is a subdomain of the other.
    • A message without facts counts as unchecked.
    • @nylas-labs/cli-kit gains cancelScheduledMessage.
  • Packaging:
    • The Vercel build is now minified.
    • Dev mocks stay out of deployed builds and the template.
    • The packed CLI is 3.42 MB, under the 3.5 MB budget.
    • The release smoke test now fails if the template imports a file it doesn't ship.
  • Merged with main: the new resizable reading-pane split defaults to this branch's 360px list.
  • design.md documents the new behaviour. Changesets cover @ownmail/app and @nylas-labs/cli-kit.

Validation

All CI steps were run locally, as ci.yml runs them:

  • lint;
  • cli-kit, ownmail and @ownmail/app tests, at 100% coverage in both UTC and America/Los_Angeles;
  • the turbo build and typecheck;
  • smoke:ssr and smoke: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

…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
@AaronDDM
AaronDDM requested a review from qasim-nylas October 17, 2026 06:27
claude added 10 commits October 17, 2026 06:43
…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
@AaronDDM
AaronDDM marked this pull request as ready for review October 17, 2026 10:08
@AaronDDM
AaronDDM requested a review from radenkovic as a code owner October 17, 2026 10:08
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T02:35:38.256094Z 15b5a94 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

qasim-nylas
qasim-nylas previously approved these changes Oct 17, 2026

@qasim-nylas qasim-nylas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/app/preferences/preference-values.ts Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/server/mail-functions.ts Outdated
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/server/header-facts.ts Outdated
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/server/header-facts.ts Outdated
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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/server/header-facts.ts Outdated
… 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/server/header-facts.ts Outdated
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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/server/header-facts.ts Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/server/header-facts.ts Outdated
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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/lib/thread-stream.ts Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/components/LongMessageFrame.tsx Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/lib/thread-stream.ts Outdated
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/components/MessageNotices.tsx Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/lib/sender-domain.ts Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/lib/thread-stream.ts Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/lib/thread-stream.ts Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/lib/thread-reading-model.ts Outdated
Comment thread labs/ownmail/packages/app/src/app/query/query-provider.tsx
- 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
@AaronDDM
AaronDDM enabled auto-merge (squash) October 18, 2026 00:56
@AaronDDM
AaronDDM requested a review from qasim-nylas October 18, 2026 00:56
qasim-nylas
qasim-nylas previously approved these changes Oct 18, 2026

@qasim-nylas qasim-nylas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/state/mail-queries.ts
Comment thread labs/ownmail/packages/app/src/features/mail/server/thread-list-functions.ts Outdated
… 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
qasim-nylas
qasim-nylas previously approved these changes Oct 18, 2026

@qasim-nylas qasim-nylas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread labs/ownmail/packages/app/src/features/mail/server/mail-functions.ts Outdated
Comment thread labs/ownmail/packages/app/src/features/mail/components/LongMessageFrame.tsx Outdated
…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

@qasim-nylas qasim-nylas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@AaronDDM
AaronDDM merged commit bc2255b into main Oct 18, 2026
11 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +135 to +139
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 } : {}) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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) } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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) } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants