Let conversation view off mean off
The setting reached only as far as the query. It set `collapseThreads`, so the
list correctly showed individual messages -- and then everything downstream
carried on working in threads. Opening one message highlighted every row of its
thread and filled the reading pane with the whole conversation, which is the
grouping the setting was turned off to avoid. The empty pane went on offering
"62 conversations" either way.
Three places had to learn about it, and the two rules behind them now live
together in lib/openMessage.ts:
- the row highlight matched on threadId, so siblings lit up
- ThreadView rendered every message the thread held
- the empty state named conversations regardless
The thread id stays in the path and loading is unchanged; the opened message
rides in `m`. Keeping it in the URL rather than in memory is what makes a
reload or a shared link come back to the same message, and an id that names
nothing in the thread falls back to the conversation -- which is what a link
from somebody with the setting on looks like, and what a stale parameter looks
like after switching back. Better a conversation than an empty pane.
Nine catalogues gain "No message selected" and "Select a message to read it
here"; "{n} messages" was already there, plural forms and all.
This commit is contained in:
@@ -107,15 +107,40 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
||||
if (mailboxesLoaded && mailboxId && mailboxId === scheduledId) void reconcile();
|
||||
}, [mailboxId, scheduledId, mailboxesLoaded, reconcile]);
|
||||
|
||||
/*
|
||||
* With conversation view off, a row is a message rather than a thread, and
|
||||
* opening one must show that message and highlight that row -- not its whole
|
||||
* thread and every sibling row in the list.
|
||||
*
|
||||
* The thread id stays in the path, so loading is unchanged; the message rides
|
||||
* in `m`. Putting it in the URL rather than in memory is what makes a reload
|
||||
* or a shared link land back on the same message, and dropping the parameter
|
||||
* degrades to the conversation, which is the right thing for a link sent to
|
||||
* somebody whose setting differs.
|
||||
*/
|
||||
const openThread = useCallback(
|
||||
(tid: Id | null) => {
|
||||
(tid: Id | null, messageId?: Id | null) => {
|
||||
const base = search ? `/search` : `/mail/${mailboxId}`;
|
||||
const qs = search ? `?q=${encodeURIComponent(q)}` : "";
|
||||
const params = new URLSearchParams();
|
||||
if (search) params.set("q", q);
|
||||
if (tid && messageId) params.set("m", messageId);
|
||||
const qs = params.size ? `?${params}` : "";
|
||||
navigate(tid ? `${base}/${tid}${qs}` : `${base}${qs}`);
|
||||
},
|
||||
[navigate, search, mailboxId, q],
|
||||
);
|
||||
|
||||
/**
|
||||
* The message the URL singles out, if any. Only meaningful with conversation
|
||||
* view off; ThreadView decides what to do when the id names nothing in the
|
||||
* thread, since it is the part that knows what the thread holds.
|
||||
*/
|
||||
const openMessageId = useMemo(() => {
|
||||
if (settings.conversationMode) return null;
|
||||
const m = new URLSearchParams(searchStr).get("m");
|
||||
return m || null;
|
||||
}, [settings.conversationMode, searchStr]);
|
||||
|
||||
// Row ids in list + helpers for keyboard nav
|
||||
const ids = list?.ids ?? [];
|
||||
const emails = useMail((s) => s.emails);
|
||||
@@ -129,9 +154,17 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
||||
const i = ids.indexOf(focusId);
|
||||
if (i >= 0) return i;
|
||||
}
|
||||
// A cold load has no focus yet. With conversation view off the URL names the
|
||||
// row exactly; matching on the thread instead would land on whichever of its
|
||||
// messages sorts first, so j/k and the scroll-into-view would start from the
|
||||
// wrong row on any thread with more than one message in the folder.
|
||||
if (openMessageId) {
|
||||
const i = ids.indexOf(openMessageId);
|
||||
if (i >= 0) return i;
|
||||
}
|
||||
if (threadId) return ids.findIndex((id) => rowThreadId(id) === threadId);
|
||||
return -1;
|
||||
}, [ids, focusId, threadId, rowThreadId]);
|
||||
}, [ids, focusId, openMessageId, threadId, rowThreadId]);
|
||||
|
||||
/** Email ids affected by an action on rows (selection or focused/open row). */
|
||||
const targetIds = useCallback(
|
||||
@@ -339,9 +372,9 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
||||
void openDraft(e);
|
||||
return;
|
||||
}
|
||||
openThread(e.threadId);
|
||||
openThread(e.threadId, settings.conversationMode ? null : rowId);
|
||||
},
|
||||
[emails, mailboxId, mailboxes, openThread, openDraft],
|
||||
[emails, mailboxId, mailboxes, openThread, openDraft, settings.conversationMode],
|
||||
);
|
||||
|
||||
const title = search ? translate("Search: {query}", { query: listQuery?.label ?? q }) : (mailboxId && mailboxDisplayName(mailboxes[mailboxId])) || translate("Mail");
|
||||
@@ -373,6 +406,7 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
||||
title={title}
|
||||
list={list}
|
||||
openThreadId={threadId ?? null}
|
||||
openMessageId={openMessageId}
|
||||
focusId={focusId}
|
||||
setFocusId={setFocusId}
|
||||
onOpen={onOpenRow}
|
||||
@@ -387,12 +421,24 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
||||
{showReading && (
|
||||
<div className="mail-reading-pane">
|
||||
{threadId ? (
|
||||
<ThreadView key={threadId} threadId={threadId} mailboxId={mailboxId ?? null} onBack={() => openThread(null)} actions={actions} onNavigate={(delta) => { const idx = currentRowIndex; const next = ids[idx + delta]; const t = next ? rowThreadId(next) : undefined; if (t) { setFocusId(next!); openThread(t); } }} hasPrev={currentRowIndex > 0} hasNext={currentRowIndex >= 0 && currentRowIndex < ids.length - 1} />
|
||||
<ThreadView key={`${threadId}:${openMessageId ?? ""}`} threadId={threadId} messageId={openMessageId} mailboxId={mailboxId ?? null} onBack={() => openThread(null)} actions={actions} onNavigate={(delta) => { const idx = currentRowIndex; const next = ids[idx + delta]; const t = next ? rowThreadId(next) : undefined; if (t) { setFocusId(next!); openThread(t, settings.conversationMode ? null : next!); } }} hasPrev={currentRowIndex > 0} hasNext={currentRowIndex >= 0 && currentRowIndex < ids.length - 1} />
|
||||
) : (
|
||||
<div className="no-thread">
|
||||
<img src={withBase("/img/logo.png")} alt="" />
|
||||
<div>{list?.total ? plural(list.total, { one: "{n} conversation", other: "{n} conversations" }) : translate("No conversation selected")}</div>
|
||||
<div className="hint">{tNode("Select a conversation to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })}</div>
|
||||
<div>
|
||||
{list?.total
|
||||
? settings.conversationMode
|
||||
? plural(list.total, { one: "{n} conversation", other: "{n} conversations" })
|
||||
: plural(list.total, { one: "{n} message", other: "{n} messages" })
|
||||
: settings.conversationMode
|
||||
? translate("No conversation selected")
|
||||
: translate("No message selected")}
|
||||
</div>
|
||||
<div className="hint">
|
||||
{settings.conversationMode
|
||||
? tNode("Select a conversation to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })
|
||||
: tNode("Select a message to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user