Files
webmail/lib/offline/sync-progress.ts
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

61 lines
2.2 KiB
TypeScript

/**
* How far a first sync got, and how long it may still take. The store reports
* batches of a phase: messages listed, then threads, then bodies, then files.
*/
export interface SyncProgress {
area: string;
phase: string;
done: number;
total: number | null;
/** When this phase started here, and how far it was then. */
startedAt: number;
startedDone: number;
at: number;
}
/**
* Listing a message is about ten times as fast as fetching its body and
* indexing it (bench rig, 50,000 messages: 35 s to list, 400 s in all). So
* while listing, the time left includes the bodies that come after it.
*/
const BODY_COST_PER_LIST_ITEM = 10;
/** An estimate from less than this jumps about too much to show. */
const MIN_SAMPLE_MS = 5_000;
/** Too small to be worth a progress line: new mail arriving in a normal pass. */
const MIN_ITEMS_SHOWN = 50;
export function nextProgress(
previous: SyncProgress | undefined,
event: { area: string; phase: string; done: number; total: number | null },
now: number,
): SyncProgress {
const samePhase = previous?.area === event.area && previous.phase === event.phase && event.done >= previous.done;
return {
area: event.area,
phase: event.phase,
done: event.done,
total: event.total,
startedAt: samePhase ? previous.startedAt : now,
startedDone: samePhase ? previous.startedDone : event.done,
at: now,
};
}
export function progressWorthShowing(progress: SyncProgress | undefined): progress is SyncProgress {
return !!progress && (progress.total === null || progress.total >= MIN_ITEMS_SHOWN) && progress.done >= 0;
}
/** Minutes left, rounded up; null while there's nothing sensible to say. */
export function minutesLeft(progress: SyncProgress): number | null {
const { total, done, startedAt, startedDone, at } = progress;
if (total === null || total <= done) return null;
const elapsed = at - startedAt;
const moved = done - startedDone;
if (elapsed < MIN_SAMPLE_MS || moved <= 0) return null;
const perItem = elapsed / moved;
let left = (total - done) * perItem;
if (progress.phase === 'list') left += total * perItem * BODY_COST_PER_LIST_ITEM;
return Math.max(1, Math.ceil(left / 60_000));
}