Files
webmail/components/offline/offline-settings.tsx
T
Linus Rath 4b426e1469 fix: find the desktop app's own settings in search, and drop browser words from its pages
Settings search knew none of the desktop app's rows, short words matched
a dozen descriptions, and the app's Notifications, Privacy and Backup
pages talked about websites, browsers and Safari.

- Search indexes the app's rows when its bridge is there (tray, start at
  sign-in, menu bar, quiet hours, taskbar, default mail app, updates,
  developer tools, usage info, crash reports) with translated synonyms,
  and the browser-only push rows only in a browser. New synonyms for
  threads, block images, dark mode, timezone, reading pane, week start,
  empty trash and quiet hours. A single word of up to five letters looks
  only in names and synonyms, so "about" finds About.
- In the app, Notifications has no push relay section; one row says the
  app tells about new mail while it runs, with a link to General. Its
  taskbar and quiet hours section is "On this computer". Privacy has no
  relay link there, Backup says "other devices".
- In a browser that can't push, two sentences, and the home screen hint
  only on iPhone and iPad.
- Several accounts get one notification switch each instead of a
  checklist with "Enable for selected" and "Disable for selected".
- Developer tools show only with advanced settings on.
- "Two-step sign-in" heads its section, "Pictures from company websites"
  replaces "Sender favicons", the Privacy relay link no longer points to
  a hidden choice, Offline warns about disk encryption once, and the
  reset confirm says what stays and whether other devices change. A
  reset keeps the choice to sync settings.

Changed assertions: account-security-signin looks for the heading by its
new name, settings-search expects the renamed rows, multi-account-push
tests the switches that replace the checklist.
2026-11-04 12:38:35 +01:00

100 lines
4.9 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { AlertTriangle } from '@/components/icons';
import { SettingsSection } from '@/components/settings/settings-section';
import { Button } from '@/components/ui/button';
import { IS_LITE } from '@/lib/lite';
import { isDesktopApp } from '@/lib/desktop/bridge';
import { signedInAccountIds } from '@/lib/device-policy';
import { offlineAllowed, offlineSupported, readPersistence, refreshUsage, requestPersistence, safariMayDelete } from '@/lib/offline/offline-mode';
import { useOfflineStore } from '@/lib/offline/state';
import { useDevicePolicy } from '@/hooks/use-device-policy';
import { useAccountStore } from '@/stores/account-store';
import { useAuthStore } from '@/stores/auth-store';
import { AccountStorage } from './account-storage';
import { OutboxList } from './outbox-list';
import { KeyProtectionNotes } from './key-protection-notes';
import { WipeDeviceSection } from './wipe-device-section';
/**
* Settings > Offline (#523), a page of its own: for each account signed in
* here, whether it keeps mail on this device, how much, what that takes, and
* the way to clear it. Above that what the browser may do to it; in a section
* below it the outbox and whatever didn't go through. Not in Lite.
*/
export function OfflineSettings() {
const t = useTranslations('offline.settings');
const tOutbox = useTranslations('offline.outbox');
const tStorage = useTranslations('offlineStorage');
const active = useAuthStore((s) => s.activeAccountId);
useAuthStore((s) => s.connectedAccountsRevision);
// Re-renders when the organisation's device settings change.
useDevicePolicy();
const labels = useAccountStore((s) => s.accounts);
const enabledCount = useOfflineStore((s) => s.enabled.length);
const persist = useOfflineStore((s) => s.persist);
const blocked = useOfflineStore((s) => s.storageBlocked);
const tNotes = useTranslations('offlineConflicts');
const [asking, setAsking] = useState(false);
useEffect(() => {
if (IS_LITE || enabledCount === 0) return;
void readPersistence();
void refreshUsage();
}, [enabledCount]);
if (IS_LITE) return null;
const accounts = [...new Set([...(active ? [active] : []), ...signedInAccountIds()])];
const several = accounts.length > 1;
const askAgain = async () => {
setAsking(true);
await requestPersistence();
setAsking(false);
};
return (
<div className="space-y-8">
<SettingsSection id="offline" title={t('title')} description={t('description')}>
{blocked ? (
<p className="text-sm text-muted-foreground" data-testid="offline-private">{tNotes('store_private')}</p>
) : !offlineSupported() ? (
<p className="text-sm text-muted-foreground" data-testid="offline-unsupported">{t('unsupported')}</p>
) : (
<>
<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 data-testid="offline-warning">{isDesktopApp() ? tStorage('warning_desktop') : t('warning')}</p>
</div>
{/* The warning above already says "use disk encryption"; the browser note said it again (settings-p2-18). */}
<KeyProtectionNotes />
{enabledCount > 0 && persist === 'refused' && (
<div className="flex flex-col 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 sm:flex-row sm:items-center sm:justify-between" role="status" data-testid="offline-persist-refused">
<p className="min-w-0">{tStorage('persist_refused')}</p>
<Button variant="outline" size="sm" className="shrink-0" disabled={asking} onClick={() => void askAgain()} data-testid="offline-persist-retry">{tStorage('persist_retry')}</Button>
</div>
)}
{enabledCount > 0 && safariMayDelete() && (
<p className="rounded-md border border-border bg-muted px-3 py-2 text-xs text-foreground" data-testid="offline-safari-hint">{tStorage('safari_hint')}</p>
)}
{accounts.map((account, index) => (
<AccountStorage
key={account}
account={account}
primary={index === 0}
heading={several ? labels.find((entry) => entry.id === account)?.email || labels.find((entry) => entry.id === account)?.label || account : null}
/>
))}
{[active, ...accounts].some((account) => offlineAllowed(account)) && <WipeDeviceSection />}
</>
)}
</SettingsSection>
<SettingsSection id="outbox" title={tOutbox('title')}>
<OutboxList heading={false} />
</SettingsSection>
</div>
);
}