From a28617cbbba7bf793258d6892ae3117cd373a8fb Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 06:56:24 +0100 Subject: [PATCH] fix: say a connection is needed only when offline or the server is down, not at a start while online --- lib/offline/__tests__/client-fallback.test.ts | 26 ++++++++++++++++++- lib/offline/offline-mode.ts | 9 +++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/offline/__tests__/client-fallback.test.ts b/lib/offline/__tests__/client-fallback.test.ts index fa8106f2b..d0649421f 100644 --- a/lib/offline/__tests__/client-fallback.test.ts +++ b/lib/offline/__tests__/client-fallback.test.ts @@ -8,7 +8,7 @@ import { DEFAULT_ORGANISATION_POLICY } from '@/lib/admin/organisation-policy'; import { setStoreBackend } from '@/lib/store/client/api'; import { FakeJmapMail } from '@/lib/store/__tests__/helpers/fake-jmap-mail'; import { InProcessStore } from '@/lib/store/__tests__/helpers/in-process-store'; -import { startOfflineMode, wipeEverything } from '../offline-mode'; +import { setOfflineNotifier, startOfflineMode, wipeEverything } from '../offline-mode'; import { forgetLegacyMove } from '../migrate-legacy'; import { useOfflineStore, writeEnabled } from '../state'; @@ -291,3 +291,27 @@ describe('what the store answers and is told', () => { stop(); }); }); + +describe('saying a connection is needed', () => { + it('not while online with the server answering, as at a start with a draft waiting', async () => { + const { client, stop } = await ready(); + const { getOfflineHooks } = await import('@/lib/jmap/offline-hooks'); + const notices: { kind: string }[] = []; + setOfflineNotifier((notice) => notices.push(notice)); + await getOfflineHooks()?.answer(client, [['Identity/set', { create: { x: { email: 'a@example.org' } } }, '0']]); + expect(notices.filter((n) => n.kind.startsWith('refused'))).toEqual([]); + stop(); + }); + + it('when offline, once', async () => { + const { client, stop } = await ready(); + const { getOfflineHooks } = await import('@/lib/jmap/offline-hooks'); + const notices: { kind: string }[] = []; + setOfflineNotifier((notice) => notices.push(notice)); + await offline(async () => { + await getOfflineHooks()?.answer(client, [['Identity/set', { create: { x: { email: 'a@example.org' } } }, '0']]); + }); + expect(notices.filter((n) => n.kind === 'refused-other')).toHaveLength(1); + stop(); + }); +}); diff --git a/lib/offline/offline-mode.ts b/lib/offline/offline-mode.ts index 7e1f8f9b7..733c8229f 100644 --- a/lib/offline/offline-mode.ts +++ b/lib/offline/offline-mode.ts @@ -319,7 +319,12 @@ function hasWholeText(email: { bodyValues?: Record NEEDS_SERVER.test(name) && (!answered || answered.methodResponses.some(([responseName, , responseId]) => responseId === id && responseName === 'error'))); const refusedChange = answered?.methodResponses.some(([name, result]) => name === 'Email/set' && result?.notUpdated) ?? false; @@ -382,7 +387,7 @@ const hooks: OfflineHooks = { } catch { // The store couldn't start or lost its owner: as if nothing were kept. } - noticeRefused(answered, methodCalls); + noticeRefused(client, answered, methodCalls); if (answered && methodCalls.some(([name]) => QUEUEING_METHOD.test(name))) { requestOutboxSync(); void refreshQueueState();