Files
webmail/components/admin/org/sections/offline-section.tsx
T
Linus Rath b001cbf109 feat: show what each account keeps on the device and let it be changed and cleared
The offline section of Settings > Backup & reset now shows, per account, the
space each part takes, the last sync, a first sync's progress with the time
left, the window and attachment choices, and clearing the account. All
conflicts, calendar and contacts too, are listed there and counted in the
banner.

- the organisation's caps beat the user's choice and the desktop default, and
  every path that takes a scope goes through `effectiveScope`
- the desktop app keeps mail without asking, says so once, and stays off for
  an account where the user turned it off
- `navigator.storage.persist()` is asked on turning it on, and a refusal
  warns with a way to ask again; Safari gets the 7-day hint when not installed
- a store the browser deleted is noticed, says what was lost and syncs again
- a full disk says so, with what to do
- those who had offline mode keep it, with the 30 days they had
- Keep offline in the Files app's menu
2026-11-03 08:57:41 +01:00

123 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { SettingItem, SettingsSection, ToggleSwitch } from '@/components/settings/settings-section';
import { InheritNote } from '../org-page';
import type { OrgPolicyDraft } from '../use-org-policy-draft';
const DOCS_URL = 'https://bulwarkmail.org/docs/guides/organisation/offline-mode';
interface CapField {
key: 'offlineMaxDays' | 'offlineMaxAttachmentMb' | 'offlineMaxTotalMb';
label: string;
description: string;
unit: string;
max: number;
}
const CAP_FIELDS: CapField[] = [
{
key: 'offlineMaxDays',
label: 'Keep mail for at most',
description: 'Mail older than this leaves the device at its next policy fetch, and calendar events that ended before it too. People can still choose less.',
unit: 'days',
max: 3650,
},
{
key: 'offlineMaxAttachmentMb',
label: 'Largest attachment or file kept',
description: 'Anything bigger leaves the device and opens from the server when there is a connection. Files someone chose to keep offline count only towards the total below.',
unit: 'MB',
max: 1024 * 1024,
},
{
key: 'offlineMaxTotalMb',
label: 'All attachments and files, per account',
description: 'The oldest go first when a device is over this. Message text isn’t counted: limit that with the days above.',
unit: 'MB',
max: 1024 * 1024,
},
];
const describeCap = (value: number, unit: string) => (value > 0 ? `${value} ${unit}` : 'no limit');
/**
* Organisation > Devices > Offline mode (#523): whether people may keep mail
* on their devices, and how much (policy.json `organisation.devices`). These
* win over what a user picks and over the desktop app's default. Nothing on
* the server can check them, since the mail is on the device: the app holds
* itself to them, which the text says.
*/
export function OfflineSection({ draft }: { draft: OrgPolicyDraft }) {
const forbid = draft.orgValue('devices', 'noOffline');
const noAttachments = draft.orgValue('devices', 'noOfflineAttachments');
return (
<SettingsSection
id="offline"
title="Offline mode"
description="People can keep mail on a device to read and write it without a connection. In browsers it is off until someone turns on “Keep mail on this device” in Settings > Backup & reset. In the Bulwark desktop app it is on unless you forbid it here."
>
<SettingItem
label="Don't let people keep mail on their devices"
description="Hides the switch. A device that already keeps mail deletes it, unsent messages in its outbox included, the next time it fetches the policy (when the tab is opened or focused, and every 15 minutes)."
>
<div className="flex flex-col items-start gap-1 sm:items-end">
<ToggleSwitch checked={forbid.value} onChange={(v) => draft.setOrgValue('devices', 'noOffline', v)} testId="org-devices-noOffline" />
<InheritNote draft={draft} own={forbid.own} everyone={forbid.everyone ? 'on' : 'off'} onReset={() => draft.clearOrgValue('devices', 'noOffline')} />
</div>
</SettingItem>
{CAP_FIELDS.map((field) => {
const { value, own, everyone } = draft.orgValue('devices', field.key);
return (
<SettingItem key={field.key} label={field.label} description={field.description}>
<div className="flex flex-col items-start gap-1 sm:items-end">
<div className="flex items-center gap-2">
<input
type="number"
inputMode="numeric"
min={0}
max={field.max}
step={1}
data-testid={`org-devices-${field.key}`}
aria-label={`${field.label} (${field.unit})`}
placeholder="No limit"
value={value > 0 ? value : ''}
onChange={(e) => {
const raw = e.target.value.trim();
const parsed = raw === '' ? 0 : Math.round(Number(raw));
if (!Number.isFinite(parsed)) return;
draft.setOrgValue('devices', field.key, Math.min(field.max, Math.max(0, parsed)));
}}
className="h-9 w-28 rounded-md border border-input bg-background px-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-hidden"
/>
<span className="text-sm text-muted-foreground">{field.unit}</span>
</div>
<InheritNote draft={draft} own={own} everyone={describeCap(everyone, field.unit)} onReset={() => draft.clearOrgValue('devices', field.key)} />
</div>
</SettingItem>
);
})}
<SettingItem
label="Don't keep attachments or files"
description="Mail stays on the device without its attachments, and files kept for offline use are removed. They open from the server when there is a connection."
>
<div className="flex flex-col items-start gap-1 sm:items-end">
<ToggleSwitch checked={noAttachments.value} onChange={(v) => draft.setOrgValue('devices', 'noOfflineAttachments', v)} testId="org-devices-noOfflineAttachments" />
<InheritNote draft={draft} own={noAttachments.own} everyone={noAttachments.everyone ? 'on' : 'off'} onReset={() => draft.clearOrgValue('devices', 'noOfflineAttachments')} />
</div>
</SettingItem>
<p className="pt-2 text-xs text-muted-foreground">
What a device keeps: mail in every folder from the window the person picks (30 days, 90 days, a year or everything),
with calendar, contacts and files, encrypted in the browser. That protects against someone copying the browser&apos;s
files, not against someone using the unlocked browser. The limits above can only lower what people pick: where several
scopes apply, the lowest value counts, so a scope can&apos;t raise what another one lowered. The app holds itself to
them when it fetches the policy; the server can&apos;t check what is on a device. Every sign-out, the automatic one
after inactivity included, deletes everything. With several accounts signed in, each account follows its own settings.
Bulwark Lite reads the same policy but has no offline mode.{' '}
<a href={DOCS_URL} target="_blank" rel="noopener noreferrer" className="font-medium underline underline-offset-2">
How offline mode works
</a>
</p>
</SettingsSection>
);
}