(pageThreads);
+ threadsRef.current = pageThreads;
const detailQuery = useQuery({
queryKey: ["thread", threadId, threadOrder, view],
@@ -157,6 +172,27 @@ export function MailLayout() {
queryClient.invalidateQueries({ queryKey: ["folder-counts"] });
},
});
+ // Saved searches (virtual mailboxes, §12 M2).
+ const savedSearches = savedSearchesQuery.data ?? [];
+ const saveSearch = useMutation({
+ mutationFn: (name: string) => api.createSavedSearch(name, term.trim()),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["saved-searches"] }),
+ });
+ const deleteSaved = useMutation({
+ mutationFn: (id: string) => api.deleteSavedSearch(id),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["saved-searches"] }),
+ });
+ const onSaveSearch = () => {
+ const q = term.trim();
+ if (!q) return;
+ const name = window.prompt(t("search.saveNamePrompt"), q);
+ if (name && name.trim()) saveSearch.mutate(name.trim());
+ };
+ const runSavedSearch = (s: SavedSearch) => {
+ setTerm(s.query);
+ navigate("/mail");
+ };
+
// Auto-mark an opened thread read after a short dwell (4s), not instantly — and
// cancel it if you leave before then, so a quick glance doesn't mark it read.
useEffect(() => {
@@ -359,6 +395,7 @@ export function MailLayout() {
return (
+
{/* Sidebar */}
@@ -612,14 +704,14 @@ export function MailLayout() {
setPage(curPage - 1)}
+ disabled={page === 0}
+ onClick={() => setPage(page - 1)}
label={t("pager.prev")}
dir="left"
/>
= total}
- onClick={() => setPage(curPage + 1)}
+ onClick={() => setPage(page + 1)}
label={t("pager.next")}
dir="right"
/>
@@ -765,6 +857,42 @@ export function MailLayout() {
);
}
+// Account bullet in the sidebar. While the account is backfilling it becomes a
+// spinning ring that fills with the account colour to show import progress
+// (SPEC §12 M2); once live it's the plain colour-filled dot.
+function AccountDot({
+ color,
+ syncing,
+ total,
+ done,
+}: {
+ color: string;
+ syncing: boolean;
+ total: number;
+ done: number;
+}) {
+ if (!syncing) {
+ return (
+
+ );
+ }
+ const pct = total > 0 ? Math.min(100, Math.round((done / Math.max(done, total)) * 100)) : 0;
+ const ring = "rgba(128,128,128,0.35)";
+ return (
+
+ );
+}
+
function FilterTab({
active,
onClick,
diff --git a/frontend/src/routes/settings/Settings.tsx b/frontend/src/routes/settings/Settings.tsx
index 24f5af0..cb6c051 100644
--- a/frontend/src/routes/settings/Settings.tsx
+++ b/frontend/src/routes/settings/Settings.tsx
@@ -225,6 +225,10 @@ function AccountsSection() {
mutationFn: (id: string) => api.deleteAccount(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["accounts"] }),
});
+ const downloadAll = useMutation({
+ mutationFn: (id: string) => api.updateAccount(id, { sync_window: "all" }),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["accounts"] }),
+ });
const close = () => {
setEditing(null);
@@ -257,8 +261,25 @@ function AccountsSection() {
{t("settings.syncState", { state: a.state })}
+ {a.sync_window !== "all" && (
+ <>
+ ·
+
+ {t("settings.syncedPartially")}
+
+ >
+ )}
+ {a.sync_window !== "all" && (
+
+ )}
+
{tested && {t("wizard.testOk")}
}
{!tested ? (
diff --git a/frontend/src/test/ThreadView.test.tsx b/frontend/src/test/ThreadView.test.tsx
index 31e6953..8b72c00 100644
--- a/frontend/src/test/ThreadView.test.tsx
+++ b/frontend/src/test/ThreadView.test.tsx
@@ -24,6 +24,7 @@ function msg(id: string, minutes: number, unread = false): MessageEnvelope {
is_starred: false,
has_attachments: false,
has_remote_images: false,
+ images_allowed: false,
size_bytes: 1024,
snippet: `snippet ${id}`,
also_in_account_ids: [],
diff --git a/screenshots/PDF_preview.png b/screenshots/PDF_preview.png
new file mode 100644
index 0000000..e6bb017
Binary files /dev/null and b/screenshots/PDF_preview.png differ
diff --git a/screenshots/image_preview.png b/screenshots/image_preview.png
new file mode 100644
index 0000000..989dc97
Binary files /dev/null and b/screenshots/image_preview.png differ
diff --git a/screenshots/progress-circles.jpg b/screenshots/progress-circles.jpg
new file mode 100644
index 0000000..0454fd3
Binary files /dev/null and b/screenshots/progress-circles.jpg differ