Let conversation view off mean off #327

Closed
opened 2026-09-10 21:16:11 +00:00 by jcoffey-dev · 0 comments
Owner

With Conversation view switched off, the list correctly showed individual messages — but clicking one highlighted every row in its thread and filled the reading pane with the whole conversation. Reported from the inbox, reproduced in prod.

Cause

conversationMode was consulted in exactly two places (MailView.tsx:89 and :96), both setting collapseThreads on the JMAP query. It changed what the server returned and nothing about what the client did afterwards.

Where Was
MessageList.tsx open={openThreadId === e.threadId} — matched the thread, so siblings lit up
MailView.tsx rendered <ThreadView> unconditionally
MailView.tsx empty state hardcoded "conversation"/"conversations" — still said "62 conversations" with the setting off

Fix

The two rules behind it now live together in lib/openMessage.ts, since they're the same question asked in two places: rowIsOpen() and visibleMessages().

The thread id stays in the path and loading is unchanged — one request, and reply/forward keep the context they need. The opened message rides in ?m=. Keeping it in the URL rather than in memory is what makes a reload or a shared link land back on the same message.

An m that names nothing in the thread falls back to the whole conversation. That covers both a link shared by someone whose conversation view is on and a stale parameter after switching the setting back — a conversation beats an empty pane for both.

currentRowIndex also learned about it, so a cold load puts j/k and scroll-into-view on the right row rather than whichever message of the thread sorts first.

With the setting on, every path is byte-identical to before.

Translation

Nine catalogues gain two strings — No message selected and Select a message to read it here · Press {key} for shortcuts — written to match each catalogue's existing register and the noun it already uses for a message (Nachricht / mensaje / message / bericht / mensagem / письмо / лист / メール / 邮件).

{n} messages was already present in all nine with correct plural forms, so the counter needed nothing.

Checks

  • npm run typecheck — clean, both packages
  • npm test — 1168 web tests / 115 files, 168 server tests, all passing
  • npm run build — clean
  • 8 new tests covering both rules, including the conversation-view-on path

Unrelated breakage found

Four of the five i18n scripts are broken on maini18n-catalog-check, i18n-literals, i18n-coverage and i18n-strings all die on ts.ScriptTarget being undefined. TypeScript 7.0.2 changed that export shape; only i18n-extract still runs. Confirmed on a clean checkout, so it predates this branch and is left alone here. Worth its own fix, since it's the gate that would normally check catalogue work like the above.

Merged 2026-09-10 as coffey-labs/ihasmail@2a323d6270

Rebuilt from: GH Archive, git history, session transcript.

With **Conversation view** switched off, the list correctly showed individual messages — but clicking one highlighted *every* row in its thread and filled the reading pane with the whole conversation. Reported from the inbox, reproduced in prod. ## Cause `conversationMode` was consulted in exactly two places (`MailView.tsx:89` and `:96`), both setting `collapseThreads` on the JMAP query. It changed what the server returned and nothing about what the client did afterwards. | Where | Was | |---|---| | `MessageList.tsx` | `open={openThreadId === e.threadId}` — matched the thread, so siblings lit up | | `MailView.tsx` | rendered `<ThreadView>` unconditionally | | `MailView.tsx` | empty state hardcoded "conversation"/"conversations" — still said "62 conversations" with the setting off | ## Fix The two rules behind it now live together in `lib/openMessage.ts`, since they're the same question asked in two places: `rowIsOpen()` and `visibleMessages()`. The thread id **stays in the path** and loading is unchanged — one request, and reply/forward keep the context they need. The opened message rides in `?m=`. Keeping it in the URL rather than in memory is what makes a reload or a shared link land back on the same message. An `m` that names nothing in the thread falls back to the whole conversation. That covers both a link shared by someone whose conversation view is *on* and a stale parameter after switching the setting back — a conversation beats an empty pane for both. `currentRowIndex` also learned about it, so a cold load puts `j`/`k` and scroll-into-view on the right row rather than whichever message of the thread sorts first. With the setting on, every path is byte-identical to before. ## Translation Nine catalogues gain **two** strings — `No message selected` and `Select a message to read it here · Press {key} for shortcuts` — written to match each catalogue's existing register and the noun it already uses for a message (Nachricht / mensaje / message / bericht / mensagem / письмо / лист / メール / 邮件). `{n} messages` was already present in all nine with correct plural forms, so the counter needed nothing. ## Checks - `npm run typecheck` — clean, both packages - `npm test` — 1168 web tests / 115 files, 168 server tests, all passing - `npm run build` — clean - 8 new tests covering both rules, including the conversation-view-on path ## Unrelated breakage found Four of the five i18n scripts are broken on `main` — `i18n-catalog-check`, `i18n-literals`, `i18n-coverage` and `i18n-strings` all die on `ts.ScriptTarget` being `undefined`. TypeScript 7.0.2 changed that export shape; only `i18n-extract` still runs. Confirmed on a clean checkout, so it predates this branch and is left alone here. Worth its own fix, since it's the gate that would normally check catalogue work like the above. **Merged** 2026-09-10 as coffey-labs/ihasmail@2a323d627042 <sub>Rebuilt from: GH Archive, git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.