mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 16:16:59 +00:00
Settings > Backup & reset gets "Keep mail on this device", off until the user turns it on in this browser. While it is on: - the JMAP client hands the server's answers to an encrypted cache in IndexedDB (AES-GCM, non-extractable key) and answers from it when the network is gone: the folder list, the last 30 days of the Inbox and of the folders the user opened, bodies and attachments, at most 250 MB, oldest out first; a background sync keeps it current - read, flag, tags and moves made offline are queued and sent in order when the server answers again; deleting for good and emptying a folder are refused offline - the composer, quick reply and the S/MIME and PGP plugins' sends that complete the composer's go to an outbox once every step up to the transport ran (checks, label, signature, plugin send checks); only the finished send waits, with files attached offline - the service worker keeps this build's app shell, so the app also starts offline from the kept session and the last policy - a full sign-out, the idle sign-out and signing one account out delete it, a sign-out the user starts asks first when mail is unsent, and the login page names what an automatic sign-out deleted; a policy fetch that forbids it (devices.noOffline) wipes that account Not in Lite.
95 lines
4.0 KiB
TypeScript
95 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useFormatter, useTranslations } from 'next-intl';
|
|
import { AlertTriangle } from '@/components/icons';
|
|
import { SettingItem, SettingsSection, ToggleSwitch } from '@/components/settings/settings-section';
|
|
import { OrgLockedNote } from '@/components/settings/org-locked-note';
|
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
|
import { IS_LITE } from '@/lib/lite';
|
|
import { formatFileSize } from '@/lib/utils';
|
|
import { disableOffline, enableOffline, offlineAllowed, offlineSupported } from '@/lib/offline/offline-mode';
|
|
import { useOfflineStore } from '@/lib/offline/state';
|
|
import { useDevicePolicy } from '@/hooks/use-device-policy';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { OutboxList } from './outbox-list';
|
|
|
|
/**
|
|
* Settings > Backup & reset > Offline (#523): "Keep mail on this device"
|
|
* for the active account, the space it uses, and the outbox. Not in Lite.
|
|
* When the organisation forbids it, the switch is gone and a note says so.
|
|
*/
|
|
export function OfflineSettings() {
|
|
const t = useTranslations('offline.settings');
|
|
const format = useFormatter();
|
|
const account = useAuthStore((s) => s.activeAccountId);
|
|
// Re-renders when the organisation's device settings change.
|
|
const devices = useDevicePolicy();
|
|
const enabled = useOfflineStore((s) => !!account && s.enabled.includes(account));
|
|
const usage = useOfflineStore((s) => s.usageBytes);
|
|
const syncing = useOfflineStore((s) => !!account && s.syncing.includes(account));
|
|
const lastSynced = useOfflineStore((s) => (account ? s.lastSyncedAt[account] : undefined));
|
|
const unsent = useOfflineStore((s) => s.outbox.filter((row) => row.account === account).length);
|
|
const [confirmOff, setConfirmOff] = useState(false);
|
|
|
|
if (IS_LITE) return null;
|
|
const allowed = !devices.noOffline && offlineAllowed(account);
|
|
|
|
const turnOff = () => {
|
|
if (!account) return;
|
|
void disableOffline(account);
|
|
};
|
|
|
|
return (
|
|
<SettingsSection id="offline" title={t('title')} description={t('description')}>
|
|
{!offlineSupported() ? (
|
|
<p className="text-sm text-muted-foreground" data-testid="offline-unsupported">{t('unsupported')}</p>
|
|
) : !allowed ? (
|
|
<div className="py-3" data-testid="offline-forbidden">
|
|
<p className="text-sm font-medium text-foreground">{t('keep_label')}</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">{t('forbidden')}</p>
|
|
<OrgLockedNote className="mt-1" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
<SettingItem label={t('keep_label')} description={t('keep_description')}>
|
|
<ToggleSwitch
|
|
checked={enabled}
|
|
testId="offline-switch"
|
|
onChange={(on) => {
|
|
if (!account) return;
|
|
if (on) void enableOffline(account);
|
|
else if (unsent > 0) setConfirmOff(true);
|
|
else turnOff();
|
|
}}
|
|
/>
|
|
</SettingItem>
|
|
<div className="flex items-start gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-900 dark:text-amber-200">
|
|
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
|
<p>{t('warning')}</p>
|
|
</div>
|
|
{enabled && (
|
|
<p className="text-xs text-muted-foreground" role="status" aria-live="polite" data-testid="offline-status">
|
|
{syncing
|
|
? t('syncing')
|
|
: lastSynced
|
|
? t('usage_updated', { size: formatFileSize(usage), time: format.relativeTime(lastSynced, Date.now()) })
|
|
: t('usage', { size: formatFileSize(usage) })}
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
<OutboxList />
|
|
<ConfirmDialog
|
|
isOpen={confirmOff}
|
|
onClose={() => setConfirmOff(false)}
|
|
onConfirm={() => { setConfirmOff(false); turnOff(); }}
|
|
title={t('off_confirm_title')}
|
|
message={t('off_confirm_message', { count: unsent })}
|
|
confirmText={t('off_confirm')}
|
|
variant="destructive"
|
|
/>
|
|
</SettingsSection>
|
|
);
|
|
}
|