mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 00:14:18 +00:00
The flush keeps one `outbox.dropOrphans` caller, the mail branch's daily one with its 7 days, and drafts' own query says what's needed, so `addHeldFileSource` and its test are gone. `unsentCount` counts local drafts, and a draft a waiting send names counts once. The scope check in `recordBlob` and the permanent-status rule moved to `lib/store/mail/attachments.ts`, with tests.
123 lines
4.8 KiB
TypeScript
123 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
|
import { useConfirmDialog } from '@/hooks/use-confirm-dialog';
|
|
import { addSignOutGate } from '@/lib/sign-out-cleanup';
|
|
import { reloadToNewBuild } from '@/lib/app-update';
|
|
import { confirmLoss, setOfflineNotifier, startOfflineMode, unsentCount, type OfflineNotice } from '@/lib/offline/offline-mode';
|
|
import { toast } from '@/stores/toast-store';
|
|
|
|
/**
|
|
* Runs offline mode (#523) in this tab: the JMAP client's offline hooks,
|
|
* the outbox, the wipes. Its notices become toasts, and a sign-out the
|
|
* user asked for while messages wait in the outbox asks first. Not mounted
|
|
* in Lite (no offline mode there).
|
|
*/
|
|
export function OfflineHost() {
|
|
const t = useTranslations('offline');
|
|
const tRef = useRef(t);
|
|
tRef.current = t;
|
|
const tConflicts = useTranslations('offlineConflicts');
|
|
const tConflictsRef = useRef(tConflicts);
|
|
tConflictsRef.current = tConflicts;
|
|
const tDrafts = useTranslations('offlineDrafts');
|
|
const tDraftsRef = useRef(tDrafts);
|
|
tDraftsRef.current = tDrafts;
|
|
const { dialogProps, confirm } = useConfirmDialog();
|
|
const confirmRef = useRef(confirm);
|
|
confirmRef.current = confirm;
|
|
|
|
useEffect(() => {
|
|
const show = (notice: OfflineNotice) => {
|
|
const tr = tRef.current;
|
|
switch (notice.kind) {
|
|
case 'queued':
|
|
toast.info(tr('toast.queued'));
|
|
break;
|
|
case 'sent':
|
|
toast.success(tr('toast.sent', { subject: notice.subject || tr('outbox.no_subject') }));
|
|
break;
|
|
case 'send-failed':
|
|
toast.error(tr('toast.send_failed', { subject: notice.subject || tr('outbox.no_subject') }), {
|
|
message: notice.error.code ? tr(`errors.${notice.error.code}`) : notice.error.text,
|
|
});
|
|
break;
|
|
case 'held':
|
|
toast.warning(tr('toast.held', { subject: notice.subject || tr('outbox.no_subject') }));
|
|
break;
|
|
case 'lost':
|
|
toast.warning(tr('toast.lost', { subjects: notice.subjects.map((s) => s || tr('outbox.no_subject')).join(', ') }), { duration: 0 });
|
|
break;
|
|
case 'conflict':
|
|
toast.warning(tConflictsRef.current('toast', { count: notice.count }));
|
|
break;
|
|
case 'draft-refused':
|
|
toast.error(tDraftsRef.current('toast_refused', { subject: notice.subject || tr('outbox.no_subject') }), {
|
|
message: notice.error.code ? tDraftsRef.current(notice.error.code) : notice.error.text,
|
|
});
|
|
break;
|
|
case 'draft-conflict':
|
|
toast.warning(tDraftsRef.current('toast_conflict', { subject: notice.subject || tr('outbox.no_subject') }));
|
|
break;
|
|
case 'storage-failed':
|
|
toast.warning(tConflictsRef.current('storage_failed'), { duration: 0 });
|
|
break;
|
|
case 'legacy-stranded':
|
|
toast.warning(tConflictsRef.current('legacy_stranded'), { duration: 0 });
|
|
break;
|
|
case 'store-reset':
|
|
toast.warning(tConflictsRef.current('store_reset'), { duration: 0 });
|
|
break;
|
|
case 'store-unavailable':
|
|
toast.warning(tConflictsRef.current('store_unavailable'), { duration: 0 });
|
|
break;
|
|
case 'refused-delete':
|
|
toast.warning(tr('toast.refused_delete'));
|
|
break;
|
|
case 'refused-other':
|
|
toast.warning(tr('toast.refused_other'));
|
|
break;
|
|
case 'forbidden':
|
|
toast.warning(tr('toast.forbidden'));
|
|
break;
|
|
case 'back-online-reload':
|
|
toast.info(tr('toast.back_online'), {
|
|
duration: 0,
|
|
action: { label: tr('toast.reload'), onClick: () => { void reloadToNewBuild(); } },
|
|
});
|
|
break;
|
|
}
|
|
};
|
|
setOfflineNotifier(show);
|
|
const stop = startOfflineMode();
|
|
// Messages that would be lost: ask before a sign-out the user started.
|
|
const removeGate = addSignOutGate(async (accountId) => {
|
|
const { messages, changes } = await unsentCount(accountId);
|
|
if (messages === 0 && changes === 0) return true;
|
|
const tr = tRef.current;
|
|
const trConflicts = tConflictsRef.current;
|
|
const ok = await confirmRef.current({
|
|
title: messages > 0 ? tr('signout.title') : trConflicts('signout_changes_title'),
|
|
message: [
|
|
...(messages > 0 ? [tr('signout.message', { count: messages })] : []),
|
|
...(changes > 0 ? [trConflicts('signout_changes', { count: changes })] : []),
|
|
].join(' '),
|
|
confirmText: tr('signout.confirm'),
|
|
variant: 'destructive',
|
|
});
|
|
// The user agreed: no word about these messages afterwards.
|
|
if (ok) confirmLoss(accountId);
|
|
return ok;
|
|
});
|
|
return () => {
|
|
removeGate();
|
|
stop();
|
|
setOfflineNotifier(() => {});
|
|
};
|
|
}, []);
|
|
|
|
return <ConfirmDialog {...dialogProps} />;
|
|
}
|