From da4c880c5f31d8f3cdf18fcfdf9c952579d8aa80 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:14:50 +0100 Subject: [PATCH] fix: open the offline outbox from the banner without leaving the page #523 --- .../__tests__/offline-settings.test.tsx | 22 +++++++ components/offline/offline-banner.tsx | 65 +++++++++++-------- components/offline/outbox-list.tsx | 6 +- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/components/offline/__tests__/offline-settings.test.tsx b/components/offline/__tests__/offline-settings.test.tsx index 7a3a35f58..7096b3efc 100644 --- a/components/offline/__tests__/offline-settings.test.tsx +++ b/components/offline/__tests__/offline-settings.test.tsx @@ -74,3 +74,25 @@ describe('Settings > Offline', () => { expect(screen.getByRole('button', { name: 'retry' })).toBeInTheDocument(); }); }); + +describe('the offline banner', () => { + it('says the app is offline and opens the outbox in place', async () => { + const { OfflineBanner } = await import('../offline-banner'); + useOfflineStore.setState({ + enabled: ['acc'], + browserOffline: true, + outbox: [{ id: 1, account: 'acc', createdAt: Date.now(), subject: 'Quarterly', to: ['b@example.org'], status: 'waiting' }], + }); + render(); + expect(screen.getByTestId('offline-banner').textContent).toContain('banner.offline'); + fireEvent.click(screen.getByTestId('offline-banner-outbox')); + expect(screen.getByTestId('offline-outbox-row').textContent).toContain('Quarterly'); + }); + + it('stays away for an account that doesn\'t keep mail here', async () => { + const { OfflineBanner } = await import('../offline-banner'); + useOfflineStore.setState({ enabled: [], browserOffline: true, outbox: [] }); + render(); + expect(screen.queryByTestId('offline-banner')).toBeNull(); + }); +}); diff --git a/components/offline/offline-banner.tsx b/components/offline/offline-banner.tsx index ecd498e2d..b48eccab8 100644 --- a/components/offline/offline-banner.tsx +++ b/components/offline/offline-banner.tsx @@ -1,48 +1,57 @@ 'use client'; +import { useState } from 'react'; import { useTranslations } from 'next-intl'; import { CloudOff } from '@/components/icons'; -import { useRouter } from '@/i18n/navigation'; -import { buildSettingsPath } from '@/lib/deep-links'; +import { Dialog } from '@/components/ui/dialog'; import { IS_LITE } from '@/lib/lite'; import { useOfflineStore } from '@/lib/offline/state'; import { useAuthStore } from '@/stores/auth-store'; +import { OutboxList } from './outbox-list'; /** * A strip across the top of the apps while offline mode (#523) answers from * this device: the app is offline, and how many messages wait in the - * outbox (a link to it). Shown only for an account that keeps mail here. + * outbox. The count opens the outbox in a dialog, which works without a + * network (a page change might not). Shown only for an account that keeps + * mail here. */ export function OfflineBanner() { - const t = useTranslations('offline.banner'); - const router = useRouter(); + const t = useTranslations('offline'); const account = useAuthStore((s) => s.activeAccountId); const active = useOfflineStore((s) => !!account && s.enabled.includes(account)); const offline = useOfflineStore((s) => s.browserOffline || s.networkDown || s.startedOffline); const waiting = useOfflineStore((s) => s.outbox.length); - if (IS_LITE || !active || (!offline && waiting === 0)) return null; + const [open, setOpen] = useState(false); + if (IS_LITE || !active || (!offline && waiting === 0 && !open)) return null; return ( -
- {offline && ( - - - )} - {waiting > 0 && ( - - )} -
+ <> +
+ {offline && ( + + + )} + {waiting > 0 && ( + + )} +
+ setOpen(false)} title={t('outbox.title')} testId="offline-outbox-dialog"> + + + ); } diff --git a/components/offline/outbox-list.tsx b/components/offline/outbox-list.tsx index 2fdc22f42..c89382699 100644 --- a/components/offline/outbox-list.tsx +++ b/components/offline/outbox-list.tsx @@ -15,7 +15,7 @@ import { useAccountStore } from '@/stores/account-store'; * be sent again or deleted; one whose connection broke on the way is never * sent again on its own (it may have gone out). */ -export function OutboxList() { +export function OutboxList({ heading = true }: { heading?: boolean }) { const t = useTranslations('offline.outbox'); const format = useFormatter(); const rows = useOfflineStore((s) => s.outbox); @@ -40,8 +40,8 @@ export function OutboxList() { }; return ( -
-

{t('title')}

+
+ {heading &&

{t('title')}

}

{t('description')}

{rows.length === 0 ? (

{t('empty')}