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:
1 parent
9e50b47b5d
commit
fece870d49
17 files changed
+336
-77
No files matched your search
@@ -30,6 +30,7 @@ import {
|
||||
type ThreadSummary,
|
||||
} from "../lib/conversations";
|
||||
import { hasBlockedRemoteContent, messageDocument } from "../lib/renderMessage";
|
||||
import { useTheme } from "../lib/theme";
|
||||
import { offersReplyAll, replyAllRecipients, replyRecipients } from "../lib/recipients";
|
||||
import { plainSnippet } from "../lib/snippet";
|
||||
import { notifyMailStateChanged } from "../lib/useFolders";
|
||||
@@ -97,6 +98,9 @@ function initialsOf(name: string, address: string): string {
|
||||
export function ThreadReader(props: Props) {
|
||||
const { thread, folder, ownAddress, trusted } = props;
|
||||
const { t, locale } = useI18n();
|
||||
const { theme } = useTheme();
|
||||
// Plain messages that are shown as sent instead of in the app's colours.
|
||||
const [originals, setOriginals] = useState<Set<string>>(new Set());
|
||||
|
||||
const [infos, setInfos] = useState<ThreadMessageInfo[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -365,13 +369,15 @@ export function ThreadReader(props: Props) {
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="mx-auto flex w-full max-w-[1000px] flex-col gap-3 px-4 pb-10 pt-5 sm:px-7 2xl:max-w-[1140px]">
|
||||
<div className="mx-auto flex w-full max-w-[820px] flex-col gap-3 px-4 pb-14 pt-6 sm:px-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-[19px] font-semibold leading-snug tracking-[-0.01em] text-slate-100 [overflow-wrap:anywhere] [text-wrap:pretty] sm:text-[22px]">
|
||||
<h1 className="text-[22px] font-semibold leading-[1.22] tracking-[-0.022em] text-slate-50 [overflow-wrap:anywhere] [text-wrap:balance] sm:text-[27px]">
|
||||
{thread.subject || t("mail.noSubject")}
|
||||
</h1>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[12.5px] text-slate-400">
|
||||
<span>{t("conversations.messagesInConversation", { count: infos?.length ?? thread.message_count })}</span>
|
||||
{(infos?.length ?? thread.message_count) > 1 && (
|
||||
<span>{t("conversations.messagesInConversation", { count: infos?.length ?? thread.message_count })}</span>
|
||||
)}
|
||||
{infos && waitingOnMe && lastIncoming && (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11.5px] font-medium ${
|
||||
@@ -404,7 +410,7 @@ export function ThreadReader(props: Props) {
|
||||
const displayName = info.own ? t("conversations.me") : sender.name || info.from_display_name || sender.address;
|
||||
const trustedHere = loaded ? trusted.isTrusted(loaded.from) : false;
|
||||
const showImages = trustedHere || imagesOnce.has(key);
|
||||
const doc = loaded ? messageDocument(loaded, showImages) : null;
|
||||
const rendered = loaded ? messageDocument(loaded, showImages, { theme, original: originals.has(key) }) : null;
|
||||
const blocked = loaded ? hasBlockedRemoteContent(loaded) : false;
|
||||
|
||||
return (
|
||||
@@ -532,10 +538,29 @@ export function ThreadReader(props: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{doc ? (
|
||||
<MessageFrame document={doc} />
|
||||
{rendered?.canAdapt && (
|
||||
<div className="-mb-1 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOriginals((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
className={infoLinkClass}
|
||||
>
|
||||
{originals.has(key) ? t("viewer.adapt") : t("viewer.asSent")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rendered ? (
|
||||
<MessageFrame document={rendered.document} paper={rendered.paper} />
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap break-words rounded-xl bg-dark-bg px-4 py-4 font-sans text-[14.5px] leading-relaxed text-slate-200">
|
||||
<pre className="whitespace-pre-wrap break-words font-sans text-[15.5px] leading-[1.68] text-slate-100">
|
||||
{loaded.text || t("mail.emptyBody")}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user