Files
webmail/components/mail-transfer/transfer-progress.tsx
T
Linus Rath cbd23db2fe fix: give mail import and export their own settings section and name the upload limit as the server does
- Settings > Backup & reset gets a Mail files section for Import mail
  and Export all mail, instead of sitting under settings backup.
- The pause message names Stalwart's 50000000-byte quota as 50 MB, not
  48 MB, and the dialog and settings texts mention .ost files.
- The old import's leftovers go: expandImportableEmails and
  EML_IMPORT_ACCEPT in lib/eml-import.ts, and the strings
  email_viewer.import_email, mailbox_context_menu.import_email and
  notifications.import_email_success/_error in all 30 locales.

lib/__tests__/eml-import.test.ts loses the tests of the removed
functions; its #891 checks that an .eml and an .eml in a ZIP keep their
Date header now run against the import's own sources.
2026-11-01 07:05:02 +01:00

56 lines
2.1 KiB
TypeScript

"use client";
import { useEffect, type RefObject } from "react";
import { cn } from "@/lib/utils";
/**
* Keeps the focus in the dialog when its buttons change under it: Import or
* Export turns into the progress view, then into the summary, and the button
* that had the focus is gone. `stage` names the view; `target` is the button
* to focus in it.
*/
export function useFocusOnStage(target: RefObject<HTMLElement | null>, stage: string): void {
useEffect(() => {
const el = target.current;
if (!el) return;
const dialog = el.closest('[role="dialog"], [role="alertdialog"]');
if (!dialog || !dialog.contains(document.activeElement)) el.focus();
}, [target, stage]);
}
/** The bar of an import or export, 0 to 1; indeterminate while nothing is known. */
export function TransferBar({ fraction, label, paused }: { fraction: number | null; label: string; paused?: boolean }) {
const percent = fraction === null ? null : Math.round(Math.max(0, Math.min(1, fraction)) * 100);
return (
<div
role="progressbar"
aria-label={label}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={percent ?? undefined}
className="h-2 w-full rounded-full bg-primary/15 overflow-hidden"
>
<div
className={cn(
"h-full rounded-full transition-[width] duration-300",
paused ? "bg-amber-500" : "bg-primary",
percent === null && "w-1/3 animate-pulse",
)}
style={percent === null ? undefined : { width: `${percent}%` }}
/>
</div>
);
}
/** A server limit in the server's own unit: Stalwart's 50000000 bytes are "50 MB" (not the 48 MB of a 1024-based size). */
export function serverMegabytes(bytes: number): string {
return `${Math.round(bytes / 1_000_000)} MB`;
}
/** "14:00", or a date and time when the moment isn't today. */
export function formatClock(ms: number, locale: string, now = Date.now()): string {
const date = new Date(ms);
const sameDay = new Date(now).toDateString() === date.toDateString();
return new Intl.DateTimeFormat(locale, sameDay ? { hour: "2-digit", minute: "2-digit" } : { dateStyle: "medium", timeStyle: "short" }).format(date);
}