feat(webmail): a better reader and a drawn logo

The mark is rebuilt as a lambda of two square-cut strokes of one weight, joined
without a seam, on a flat ultramarine tile with continuous corners: the rounded
strokes and the gradient are gone. A message written plainly is now drawn in the
app's own colours instead of a white slab, with text too dark for the ground
lifted and white backdrops cleared; a designed one (newsletter, receipt) keeps
its white sheet. A reply's quoted history folds behind a button. A narrower
reading column, a larger title, and a way to see a plain message as sent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-23 05:57:52 -03:00
1 parent 9e50b47b5d
commit fece870d49
17 files changed
+336 -77

No files matched your search

@@ -30,6 +30,7 @@ import {
} from "../../../../../lib/mailCounts";
import { plainSnippet } from "../../../../../lib/snippet";
import { useTrustedSenders } from "../../../../../lib/trustedSenders";
import { useTheme } from "../../../../../lib/theme";
import type { InlineImages } from "../../../../../lib/emailBody";
import { messageDocument, hasBlockedRemoteContent } from "../../../../../lib/renderMessage";
import {
@@ -158,6 +159,9 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
const [messages, setMessages] = useState<MessageSummary[]>([]);
const [selected, setSelected] = useState<RenderedMessage | null>(null);
// A plainly written message takes the app's colours; this shows it as sent.
const [showOriginal, setShowOriginal] = useState(false);
const { theme } = useTheme();
// The open conversation, when the list is showing conversations.
const [selectedThread, setSelectedThread] = useState<MessageSummary | null>(null);
const conversations = useConversationView();
@@ -444,6 +448,7 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
}
}
setLoadRemoteImages(false);
setShowOriginal(false);
setReaderHeight(420);
try {
const res = await fetch(`/api/v1/mail/message/${uid}?folder=${encodeURIComponent(folder)}`, {
@@ -478,13 +483,13 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
* markup, then resolve the message's own inline parts, then block or restore
* remote sources - which is the only part the reader chooses.
*/
const readerDocument = useMemo(() => {
const readerBody = useMemo(() => {
if (!selected) return null;
// A trusted sender's images load without asking; anyone else's wait for
// the reader's choice, one message at a time.
const showImages = loadRemoteImages || trustedSenders.isTrusted(selected.from ?? "");
return messageDocument(selected, showImages);
}, [selected, loadRemoteImages, trustedSenders]);
return messageDocument(selected, showImages, { theme, original: showOriginal });
}, [selected, loadRemoteImages, trustedSenders, theme, showOriginal]);
// Whether anything was actually held back, so the privacy banner is not shown
// over a message that has no remote content at all.
@@ -794,7 +799,11 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
name: target.name,
label: FOLDER_LABELS[(target.special_use || target.name).toLowerCase()] ?? target.name,
}))}
document={readerDocument}
document={readerBody?.document ?? null}
paper={readerBody?.paper ?? true}
canAdapt={readerBody?.canAdapt ?? false}
showingOriginal={showOriginal}
onToggleOriginal={() => setShowOriginal((v) => !v)}
frameHeight={readerHeight}
hasBlockedContent={hasBlockedContent}
imagesLoaded={loadRemoteImages || senderTrusted}