mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: keep loading older mail when a saved or refreshed list has no total from the server #1364
This commit is contained in:
@@ -88,6 +88,31 @@ describe('email boot snapshot folder', { timeout: 30_000 }, () => {
|
||||
expect(state.emailListSync).toEqual(emailListSync);
|
||||
});
|
||||
|
||||
// A snapshot that says "no more mail" beside rows its total does not cover
|
||||
// kept the inbox at one page across reloads: the delta catch-up after boot
|
||||
// never re-reads the total (#1364).
|
||||
it('re-reads a page whose saved total does not cover its rows', async () => {
|
||||
const rows = [{ id: 'e1' }, { id: 'e2' }];
|
||||
const emailListSync = { state: 's1', accountId: 'acc', mailboxId: 'm-inbox' };
|
||||
const state = await boot('/', {
|
||||
mailboxes: FOLDERS, selectedMailbox: 'm-inbox', emails: rows, totalEmails: 0, hasMoreEmails: false, emailListSync,
|
||||
});
|
||||
expect(state.emails).toHaveLength(2);
|
||||
const { useEmailStore } = await import('@/stores/email-store');
|
||||
const { useSettingsStore } = await import('@/stores/settings-store');
|
||||
useSettingsStore.setState({ emailsPerPage: 2 });
|
||||
const client = {
|
||||
getAccountId: () => 'acc',
|
||||
getEmailChanges: vi.fn(async () => ({ oldState: 's1', newState: 's1', hasMoreChanges: false, created: [], updated: [], destroyed: [], updatedProperties: null })),
|
||||
getEmails: vi.fn(async () => ({ emails: rows, hasMore: true, total: 10853, state: 's1' })),
|
||||
getThreadEmailCounts: vi.fn(async () => ({})),
|
||||
};
|
||||
await useEmailStore.getState().fetchEmails(client as never, 'm-inbox', { background: true });
|
||||
expect(client.getEmails).toHaveBeenCalled();
|
||||
expect(useEmailStore.getState().totalEmails).toBe(10853);
|
||||
expect(useEmailStore.getState().hasMoreEmails).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the snapshot folder when the account has no inbox of its own', async () => {
|
||||
const state = await boot('/', { mailboxes: FOLDERS.slice(1), selectedMailbox: 'm-trash', emails: TRASH_ROWS });
|
||||
expect(state.selectedMailbox).toBe('m-trash');
|
||||
|
||||
@@ -178,6 +178,33 @@ describe('Email/changes delta sync', () => {
|
||||
expect(useEmailStore.getState().emailListSync).toEqual({ state: 's2', accountId: 'acc', mailboxId: 'inbox' });
|
||||
});
|
||||
|
||||
// A total of 0 beside the rows it was read with is a total the server did
|
||||
// not give; it says nothing about mail past the page (#1364).
|
||||
it('keeps paging when the refresh after new mail comes back without a total', async () => {
|
||||
useEmailStore.setState({ totalEmails: 10, hasMoreEmails: true });
|
||||
const client = makeClient({
|
||||
getEmailChanges: vi.fn(async () => delta({ created: ['new'] })),
|
||||
getSomeEmails: vi.fn(async () => [makeEmail('new')]),
|
||||
getEmails: vi.fn(async () => ({ emails: [makeEmail('new'), makeEmail('a'), makeEmail('b')], hasMore: true, total: 0, state: 's2' })),
|
||||
});
|
||||
|
||||
await useEmailStore.getState().handleStateChange(push({ acc: { Email: 's2' } }), client);
|
||||
|
||||
expect(useEmailStore.getState().emails.map((e) => e.id)).toEqual(['new', 'a', 'b']);
|
||||
expect(useEmailStore.getState().hasMoreEmails).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps paging when a delta is applied to a list whose total is not known', async () => {
|
||||
useEmailStore.setState({ totalEmails: 0, hasMoreEmails: true });
|
||||
const client = makeClient({ getEmailChanges: vi.fn(async () => delta({ destroyed: ['c'] })) });
|
||||
|
||||
await useEmailStore.getState().handleStateChange(push({ acc: { Email: 's2' } }), client);
|
||||
|
||||
expect(client.getEmails).not.toHaveBeenCalled();
|
||||
expect(useEmailStore.getState().emails.map((e) => e.id)).toEqual(['a', 'b']);
|
||||
expect(useEmailStore.getState().hasMoreEmails).toBe(true);
|
||||
});
|
||||
|
||||
it('falls back when the server cannot compute the delta', async () => {
|
||||
const client = makeClient({ getEmailChanges: vi.fn(async () => null) });
|
||||
await useEmailStore.getState().handleStateChange(push({ acc: { Email: 's2' } }), client);
|
||||
|
||||
+15
-3
@@ -952,10 +952,12 @@ async function applyEmailDeltaNow(client: IJMAPClient, newState: string | null,
|
||||
const threadEmailsCache = new Map(state.threadEmailsCache);
|
||||
for (const tid of changedThreadIds) threadEmailsCache.delete(tid);
|
||||
const totalEmails = Math.max(state.totalEmails - (current.length - next.length), next.length);
|
||||
// A total below the rows shown is one the server did not give (#1364).
|
||||
const totalKnown = state.totalEmails >= current.length;
|
||||
return {
|
||||
emails: next,
|
||||
totalEmails,
|
||||
hasMoreEmails: state.hasMoreEmails && next.length < totalEmails,
|
||||
hasMoreEmails: state.hasMoreEmails && (!totalKnown || next.length < totalEmails),
|
||||
threadEmailsCache,
|
||||
emailListSync: state.emailListSync?.mailboxId === sync.mailboxId ? { ...sync, state: delta.newState } : state.emailListSync,
|
||||
};
|
||||
@@ -6506,7 +6508,11 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// hasMore should reflect whether there are still more emails beyond
|
||||
// what we have loaded, using the fresh total from the server. The
|
||||
// Unread section's rows are not part of that total.
|
||||
const hasMore = merged.length - sectionRows.length < effectiveTotal;
|
||||
// A total below the page it came with is one the server did not give:
|
||||
// then only a full page tells that more may follow (#1364).
|
||||
const hasMore = (result.total || 0) >= result.emails.length
|
||||
? merged.length - sectionRows.length < effectiveTotal
|
||||
: result.hasMore;
|
||||
|
||||
// Invalidate thread email caches for threads whose composition changed
|
||||
// so expanded threads pick up new/removed emails.
|
||||
@@ -8002,7 +8008,13 @@ if (typeof window !== 'undefined') {
|
||||
// by it, so without it an "unread first" page shows by date
|
||||
// until the live fetch lands and re-sorts it.
|
||||
...(Array.isArray(snap.listOrder) ? { listOrder: sanitizeSortLevels(snap.listOrder) } : {}),
|
||||
...(isEmailListSync(snap.emailListSync, snapMailbox) ? { emailListSync: snap.emailListSync } : {}),
|
||||
// Without the baseline the live load re-reads the page and its
|
||||
// total: a saved total that doesn't cover the rows is wrong, and
|
||||
// a catch-up from the baseline would keep it, and "no more
|
||||
// mail" with it, across reloads (#1364).
|
||||
...(isEmailListSync(snap.emailListSync, snapMailbox) && !(snap.totalEmails < snap.emails.length)
|
||||
? { emailListSync: snap.emailListSync }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user