Render only the message rows that changed
The rows were memoized, but nothing they were given kept its identity: the list built each row's thread messages afresh, passed inline handlers and the whole selection, and the click and context-menu handlers changed with the selection and the menu. Every visible row rendered on every store write. Rows now select their own message and conversation from the store, take a plain selected flag, and get handlers whose identity never changes. The conversation summary is memoized. Refreshes also keep the object for a message whose fetched properties did not change, so a refresh that changed one message renders one row.
This commit is contained in:
@@ -152,6 +152,30 @@ describe("refreshList", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("merging a refresh", () => {
|
||||
it("keeps the object for a message that did not change, so its row need not render", async () => {
|
||||
const { listed } = server(3);
|
||||
await useMail.getState().query({ key: "", filter: { inMailbox: INBOX }, sort: [], collapseThreads: false, mailboxId: INBOX });
|
||||
const before = { ...useMail.getState().emails };
|
||||
await useMail.getState().refreshList();
|
||||
const after = useMail.getState().emails;
|
||||
for (const id of listed) expect(after[id]).toBe(before[id]);
|
||||
});
|
||||
|
||||
it("replaces the object for a message that did change", async () => {
|
||||
const { listed } = server(2);
|
||||
await useMail.getState().query({ key: "", filter: { inMailbox: INBOX }, sort: [], collapseThreads: false, mailboxId: INBOX });
|
||||
// Held as starred; the server says it is not.
|
||||
useMail.setState((s) => ({ emails: { ...s.emails, [listed[0]!]: { ...s.emails[listed[0]!]!, keywords: { $flagged: true } } } }));
|
||||
const held = useMail.getState().emails;
|
||||
await useMail.getState().refreshList();
|
||||
const after = useMail.getState().emails;
|
||||
expect(after[listed[0]!]).not.toBe(held[listed[0]!]);
|
||||
expect(after[listed[0]!]!.keywords).toEqual({});
|
||||
expect(after[listed[1]!]).toBe(held[listed[1]!]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadThread", () => {
|
||||
it("fetches no bodies for messages already held in full", async () => {
|
||||
const { calls } = server(1, 3);
|
||||
|
||||
@@ -255,7 +255,7 @@ export const useMail = create<MailState>((set, get) => ({
|
||||
for (const r of results) {
|
||||
state = r.state;
|
||||
for (const e of r.list) {
|
||||
next[e.id] = { ...next[e.id], ...e };
|
||||
next[e.id] = mergeEmail(next[e.id], e);
|
||||
if (full) nextFull[e.id] = true;
|
||||
}
|
||||
}
|
||||
@@ -990,7 +990,7 @@ export const useMail = create<MailState>((set, get) => ({
|
||||
);
|
||||
set((s) => {
|
||||
const next = { ...s.emails };
|
||||
for (const r of results) for (const e of r.list) next[e.id] = { ...next[e.id], ...e };
|
||||
for (const r of results) for (const e of r.list) next[e.id] = mergeEmail(next[e.id], e);
|
||||
return { emails: next };
|
||||
});
|
||||
}
|
||||
@@ -1038,6 +1038,26 @@ function sortIdentities(list: Identity[], accountId: Id): Identity[] {
|
||||
*
|
||||
* Keyed by nothing: a refusal is about the server, and there is only one.
|
||||
*/
|
||||
/**
|
||||
* Fold freshly fetched properties into the copy already held.
|
||||
*
|
||||
* Returns the held object itself when nothing in `next` differs from it. A
|
||||
* refresh fetches every listed message again, and a new object for each one
|
||||
* -- the same data, a new identity -- made every row of the list render
|
||||
* again after any change at all.
|
||||
*/
|
||||
function mergeEmail(prev: Email | undefined, next: Email): Email {
|
||||
if (!prev) return next;
|
||||
for (const key of Object.keys(next) as (keyof Email)[]) {
|
||||
const a = prev[key];
|
||||
const b = next[key];
|
||||
if (a === b) continue;
|
||||
if (a && b && typeof a === "object" && JSON.stringify(a) === JSON.stringify(b)) continue;
|
||||
return { ...prev, ...next };
|
||||
}
|
||||
return prev;
|
||||
}
|
||||
|
||||
let sortRefused = false;
|
||||
|
||||
async function runQuery(accountId: Id, q: ListQuery, position: number, limit: number) {
|
||||
@@ -1096,7 +1116,7 @@ async function runQueryOnce(accountId: Id, q: ListQuery, position: number, reque
|
||||
const following = useMail.getState().emailState !== null;
|
||||
useMail.setState((s) => {
|
||||
const emails = { ...s.emails };
|
||||
for (const e of emailsRes.list) emails[e.id] = { ...emails[e.id], ...e };
|
||||
for (const e of emailsRes.list) emails[e.id] = mergeEmail(emails[e.id], e);
|
||||
const threads = { ...s.threads };
|
||||
for (const t of threadsRes?.list ?? []) threads[t.id] = t;
|
||||
return { emails, threads, emailState: s.emailState ?? emailsRes.state };
|
||||
@@ -1115,7 +1135,7 @@ async function refreshEmails(accountId: Id, ids: Id[]): Promise<void> {
|
||||
);
|
||||
useMail.setState((s) => {
|
||||
const emails = { ...s.emails };
|
||||
for (const r of results) for (const e of r.list) emails[e.id] = { ...emails[e.id], ...e };
|
||||
for (const r of results) for (const e of r.list) emails[e.id] = mergeEmail(emails[e.id], e);
|
||||
return { emails };
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user