Offline access: read recent mail without a connection
An opt-in per device (Settings → Security → Offline access) installs a service worker that keeps the newest 50 Inbox conversations, and the ones opened recently, for reading when the server cannot be reached. While online everything still comes from the server; the saved copy is only the fallback, marked X-WM-Offline so the app knows it is offline. Offline, an amber cloud next to the theme toggle says so and when mail was last saved; actions that need the server are greyed out and say "Not available offline"; conversations and folders not saved here are faded or say so. A probe notices the server coming back, shows "Back online" and refreshes everything. Saved mail is wiped on sign-out, on any 401, and when another user signs in. nginx never caches /sw.js, so releases reach it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
|
||||
|
||||
import { useT } from "../../i18n";
|
||||
import { api } from "../../lib/api";
|
||||
import { useSavedThreads } from "../../lib/offline";
|
||||
import { formatClock, getDateLocale } from "../../lib/timeFormat";
|
||||
import type { Address, ThreadSummary } from "../../lib/types";
|
||||
import { Icon } from "../Icon";
|
||||
@@ -137,6 +138,8 @@ export function MessageList({
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
const labelMap = labelsQuery.data ?? {};
|
||||
// Offline: conversations not saved on this device are shown faded.
|
||||
const saved = useSavedThreads();
|
||||
|
||||
// The user's own addresses, shown as "me" in the sender column.
|
||||
const accounts = useQuery({ queryKey: ["accounts"], queryFn: api.accounts });
|
||||
@@ -170,6 +173,7 @@ export function MessageList({
|
||||
const selected = selectedId === thread.id;
|
||||
const checked = checkedIds.has(thread.id);
|
||||
const dimmed = dimmedIds?.has(thread.id) ?? false;
|
||||
const unsaved = saved !== null && !saved.has(thread.id);
|
||||
// The sender column lists everyone who wrote in the thread, oldest first,
|
||||
// with the user's own accounts shown as "me" — so a thread you started
|
||||
// and someone answered reads "me, Alice". Recipients are not listed.
|
||||
@@ -206,7 +210,7 @@ export function MessageList({
|
||||
// A row leaving the list lets the ones below glide up into its place.
|
||||
className={`group absolute inset-x-0 px-2 ${compact ? "pt-5" : "pt-[7px]"} motion-safe:transition-transform motion-safe:duration-200 motion-safe:ease-out ${
|
||||
dimmed ? "pointer-events-none opacity-40" : "cursor-pointer"
|
||||
}`}
|
||||
} ${unsaved ? "opacity-45" : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-full items-center gap-3 rounded-lg px-3 ${
|
||||
@@ -355,8 +359,8 @@ export function MessageList({
|
||||
onClick={stop(() => onTogglePin(thread.id, thread.pinned))}
|
||||
className={
|
||||
thread.pinned
|
||||
? "text-[rgb(220_129_143)]"
|
||||
: "invisible text-app-muted hover:text-app-fg group-hover:visible"
|
||||
? "wm-online-only text-[rgb(220_129_143)]"
|
||||
: "wm-online-only invisible text-app-muted hover:text-app-fg group-hover:visible"
|
||||
}
|
||||
>
|
||||
<Icon name="pin" size={17} />
|
||||
@@ -368,7 +372,7 @@ export function MessageList({
|
||||
type="button"
|
||||
title={t("mail.restore")}
|
||||
onClick={stop(() => onRestore(thread.id))}
|
||||
className="invisible text-app-muted hover:text-app-fg group-hover:visible"
|
||||
className="wm-online-only invisible text-app-muted hover:text-app-fg group-hover:visible"
|
||||
>
|
||||
<Icon name="restore" size={17} />
|
||||
</button>
|
||||
@@ -377,7 +381,7 @@ export function MessageList({
|
||||
type="button"
|
||||
title={t("mail.archive")}
|
||||
onClick={stop(() => onArchive(thread.id))}
|
||||
className="invisible text-app-muted hover:text-app-fg group-hover:visible"
|
||||
className="wm-online-only invisible text-app-muted hover:text-app-fg group-hover:visible"
|
||||
>
|
||||
<Icon name="archive" size={15} />
|
||||
</button>
|
||||
@@ -388,7 +392,7 @@ export function MessageList({
|
||||
onClick={stop(() =>
|
||||
folder === "trash" ? onDeletePermanent(thread.id) : onTrash(thread.id),
|
||||
)}
|
||||
className="invisible mr-[7px] text-app-muted hover:text-app-fg group-hover:visible"
|
||||
className="wm-online-only invisible mr-[7px] text-app-muted hover:text-app-fg group-hover:visible"
|
||||
>
|
||||
<Icon name="trash" size={17} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user