mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 00:14:23 +00:00
fix: open the offline outbox from the banner without leaving the page #523
This commit is contained in:
@@ -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(<OfflineBanner />);
|
||||
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(<OfflineBanner />);
|
||||
expect(screen.queryByTestId('offline-banner')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
role="status"
|
||||
data-testid="offline-banner"
|
||||
className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 border-b border-border bg-muted px-3 py-2 text-sm text-foreground sm:px-4"
|
||||
>
|
||||
{offline && (
|
||||
<span className="flex min-w-0 items-start gap-2">
|
||||
<CloudOff className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<span className="min-w-0 [overflow-wrap:anywhere]">{t('offline')}</span>
|
||||
</span>
|
||||
)}
|
||||
{waiting > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="offline-banner-outbox"
|
||||
onClick={() => router.push(buildSettingsPath('backup', 'offline'))}
|
||||
className="min-h-11 rounded-md px-1 font-medium underline underline-offset-2 hover:bg-foreground/5 sm:min-h-0"
|
||||
>
|
||||
{t('outbox', { count: waiting })}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
role="status"
|
||||
data-testid="offline-banner"
|
||||
className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 border-b border-border bg-muted px-3 py-2 text-sm text-foreground sm:px-4"
|
||||
>
|
||||
{offline && (
|
||||
<span className="flex min-w-0 items-start gap-2">
|
||||
<CloudOff className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<span className="min-w-0 [overflow-wrap:anywhere]">{t('banner.offline')}</span>
|
||||
</span>
|
||||
)}
|
||||
{waiting > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="offline-banner-outbox"
|
||||
aria-haspopup="dialog"
|
||||
onClick={() => setOpen(true)}
|
||||
className="min-h-11 rounded-md px-1 font-medium underline underline-offset-2 hover:bg-foreground/5 sm:min-h-0"
|
||||
>
|
||||
{t('banner.outbox', { count: waiting })}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Dialog isOpen={open} onClose={() => setOpen(false)} title={t('outbox.title')} testId="offline-outbox-dialog">
|
||||
<OutboxList heading={false} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="pt-2" data-testid="offline-outbox">
|
||||
<h4 className="text-sm font-medium text-foreground">{t('title')}</h4>
|
||||
<div className={heading ? 'pt-2' : undefined} data-testid="offline-outbox">
|
||||
{heading && <h4 className="text-sm font-medium text-foreground">{t('title')}</h4>}
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('description')}</p>
|
||||
{rows.length === 0 ? (
|
||||
<p className="mt-3 text-sm text-muted-foreground" data-testid="offline-outbox-empty">{t('empty')}</p>
|
||||
|
||||
Reference in New Issue
Block a user