Open conversations in one request, and start them early (#392)

On a 250 ms link, opening a conversation took two round trips: Thread/get,
then the bodies. It now takes one. A known thread sends Thread/get and the
missing bodies in the same tick; an unknown one chains Email/get off
Thread/get with a back-reference, and falls back to fetching in parts when
the thread is longer than one Email/get may carry.

Conversations also start loading before the click: when the pointer rests
on a row, as soon as a press begins, and for the row below the open one.
The open waits for that load and does not repeat it.

Going back to one of the last twelve folders shows its previous list at
once, less messages that have left it, while the query runs.
This commit is contained in:
jcoffey
2026-09-16 13:15:21 -07:00
committed by GitHub
parent 5fe89d6e15
commit 786976312f
5 changed files with 261 additions and 8 deletions
+18
View File
@@ -183,6 +183,24 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
return -1;
}, [ids, focusId, openMessageId, threadId, rowThreadId]);
/*
* Reading down a folder usually means the next row is next. Once the open
* conversation has had its turn, the one below starts loading while the
* reader reads, so moving on waits on nothing.
*/
const nextRowId = threadId && currentRowIndex >= 0 ? ids[currentRowIndex + 1] : undefined;
const nextThreadId = nextRowId ? rowThreadId(nextRowId) : undefined;
useEffect(() => {
if (!nextThreadId || nextThreadId === threadId) return;
const start = () => useMail.getState().prefetchThread(nextThreadId);
if (typeof window.requestIdleCallback === "function") {
const handle = window.requestIdleCallback(start, { timeout: 2000 });
return () => window.cancelIdleCallback(handle);
}
const handle = window.setTimeout(start, 500);
return () => window.clearTimeout(handle);
}, [nextThreadId, threadId]);
/** Email ids affected by an action on rows (selection or focused/open row). */
const targetIds = useCallback(
async (rowIds?: Id[]): Promise<Id[]> => {