feat(webmail): trusted senders on the server, Not spam, and a trust pill in the reader

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-22 15:48:35 -03:00
1 parent d7f9659b5e
commit d2611d39fa
9 files changed
+203 -37

No files matched your search

@@ -11,6 +11,8 @@ import {
FolderInput,
Forward,
Image as ImageIcon,
ShieldAlert,
ShieldCheck,
MailOpen,
Pencil,
Reply,
@@ -72,14 +74,18 @@ interface Props {
/** Whether anything was held back, so the privacy bar is not always shown. */
hasBlockedContent: boolean;
imagesLoaded: boolean;
senderRemembered: boolean;
/** Whether the account trusts this sender: not spam, and images load. */
senderTrusted: boolean;
/** The message is being read in the spam folder. */
isJunk: boolean;
onClose: () => void;
onToggleSeen: () => void;
onToggleFlagged: () => void;
onDelete: () => void;
onMove: (target: string) => void;
onToggleImages: () => void;
onToggleRemember: () => void;
onToggleTrusted: () => void;
onNotSpam: () => void;
}
/** The sender's name and address, from a header like `Jenny <jenny@x.test>`. */
@@ -324,6 +330,22 @@ export function MessageReader(props: Props) {
<span className="text-[13px] text-slate-400 [overflow-wrap:anywhere]">&lt;{address}&gt;</span>
)}
</div>
<div className="mt-0.5">
<button
type="button"
onClick={props.onToggleTrusted}
aria-pressed={props.senderTrusted}
title={props.senderTrusted ? t("mail.trustedSenderHint") : t("mail.trustSenderHint")}
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11.5px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 ${
props.senderTrusted
? "bg-indigo-500/20 text-indigo-200 hover:bg-indigo-500/30"
: "text-slate-400 shadow-[inset_0_0_0_1px_rgba(233,233,237,0.18)] hover:bg-white/[0.07] hover:text-slate-200"
}`}
>
<ShieldCheck className="h-3 w-3" aria-hidden />
{props.senderTrusted ? t("mail.trustedSender") : t("mail.trustSender")}
</button>
</div>
<div className="text-[12.5px] leading-relaxed text-slate-400 [overflow-wrap:anywhere]">
{visibleTo.length > 0 && (
<>
@@ -361,18 +383,35 @@ export function MessageReader(props: Props) {
error. Two separate decisions - look now, or stop asking about
this sender - so a reader can see one message without granting
anything permanently. */}
{(props.hasBlockedContent || props.senderRemembered) && (
{props.isJunk && (
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-lg bg-amber-500/10 px-3 py-2.5 shadow-[inset_0_0_0_1px_rgba(245,158,11,0.35)]">
<ShieldAlert className="h-4 w-4 flex-none text-amber-400" aria-hidden />
<span className="min-w-[180px] flex-1 text-[12.5px] leading-relaxed text-amber-100">
{t("mail.junkNotice")}
</span>
<button
type="button"
onClick={props.onNotSpam}
className="rounded-md bg-amber-400 px-2.5 py-1 text-[12.5px] font-semibold text-slate-900 transition-colors hover:bg-amber-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-200"
>
{t("mail.notSpam")}
</button>
</div>
)}
{props.hasBlockedContent && (
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 rounded-lg bg-dark-card px-3 py-2 shadow-edge">
<ImageIcon className="h-4 w-4 flex-none text-indigo-400" aria-hidden />
<span className="min-w-[180px] flex-1 text-[12.5px] leading-relaxed text-slate-300">
{props.imagesLoaded ? t("mail.imagesEnabled") : t("mail.remoteContentBlocked")}
</span>
<button type="button" onClick={props.onToggleImages} className={infoLinkClass}>
{props.imagesLoaded ? t("mail.hideImages") : t("mail.loadImages")}
</button>
<button type="button" onClick={props.onToggleRemember} className={infoLinkClass}>
{props.senderRemembered ? t("mail.forgetSender") : t("mail.alwaysFromSender")}
</button>
{/* A trusted sender's images are shown for as long as they are
trusted, so hiding them here would do nothing. */}
{!props.senderTrusted && (
<button type="button" onClick={props.onToggleImages} className={infoLinkClass}>
{props.imagesLoaded ? t("mail.hideImages") : t("mail.loadImages")}
</button>
)}
</div>
)}