mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
The review of offline mode found ways to send a message twice, to lose unsent mail without a word, and to send mail without its signing, encryption or check plugin. This fixes them and the smaller findings: - each outbox message is taken for sending in one database transaction under a Web Lock per account; only a failure before the submission request (SendNotStartedError from the client) puts it back, any other one marks it "may have gone out"; read-backs after the submission no longer fail a send; a send of a closed tab turns uncertain after a minute - every sign-out the user didn't confirm names the deleted messages on the sign-in page, or in a toast when another account stays signed in - nothing is queued while an enabled plugin isn't running (after a start without the network none can); the composer says why - the outbox waits for a fresh policy and holds messages the rules have since made stricter (a required label, a required send check) - another tab switching offline mode off sticks: only switching it on may create the database - only attachments of kept messages are kept, filed with their message, deleted with it and under the cap between syncs; file names and types are sealed; folders that went away leave the scope, and a folder that couldn't be asked keeps its mail without holding the others back - a group scope that forbids offline mode waits until the account's groups are known; one server down no longer holds another's mail - an offline start only when the server is really unreachable, and the real sign-in follows by itself once it answers - the service worker files pages and chunks under the build that served them, and the shell list of another build isn't kept under this one - a raw send counts as the composer's only from the plugin running its takeover, for that identity - errors and refusals are in all languages; the banner tells "offline" from "server can't be reached"; outbox buttons name their message; changes and messages go in the order they were made
60 lines
2.5 KiB
TypeScript
60 lines
2.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { CloudOff } from '@/components/icons';
|
|
import { Dialog } from '@/components/ui/dialog';
|
|
import { IS_LITE } from '@/lib/lite';
|
|
import { serverKey, 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 browser is offline (or the mail server can't be
|
|
* reached), and how many messages wait in the 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');
|
|
const account = useAuthStore((s) => s.activeAccountId);
|
|
const server = useAuthStore((s) => s.serverUrl);
|
|
const active = useOfflineStore((s) => !!account && s.enabled.includes(account));
|
|
const browserOffline = useOfflineStore((s) => s.browserOffline || s.startedOffline);
|
|
const serverDown = useOfflineStore((s) => !!server && s.downServers.includes(serverKey(server)));
|
|
const waiting = useOfflineStore((s) => s.outbox.length);
|
|
const [open, setOpen] = useState(false);
|
|
const offline = browserOffline || serverDown;
|
|
if (IS_LITE || !active || (!offline && waiting === 0 && !open)) return null;
|
|
return (
|
|
<>
|
|
<div
|
|
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" role="status">
|
|
<CloudOff className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
|
<span className="min-w-0 [overflow-wrap:anywhere]">{browserOffline ? t('banner.offline') : t('banner.server_down')}</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>
|
|
</>
|
|
);
|
|
}
|