import { useState } from "react";
import { useAppName } from "@/lib/brand";
import { useSettings, type ReadReceiptPolicy } from "@/store/settings";
import { useMail } from "@/store/mail";
import { domainOf } from "@/lib/address";
import { Switch } from "@/ui/misc";
import { X } from "lucide-react";
import { t } from "@/lib/i18n";
import { isEnforced } from "@/lib/settingsPolicy";
/**
* Everything about what reaches a sender, and what asks before it happens.
*
* These settings were spread through General, which had grown into five
* unrelated headings -- remote images filed under "Reading", the read-receipt
* policy under "Composing", the undo-send window beside the default message
* format. They are the same kind of decision and they belong together, and
* gathering them leaves General smaller as well.
*
* The boundary against **Security & sessions** is worth keeping sharp, since
* two similar words next to each other in a nav is how a menu becomes
* something people hunt through: that section is credentials and access --
* password, two-factor, app passwords, live sessions. This one is how the app
* behaves toward the reader and toward senders.
*/
export function PrivacySettings() {
const appName = useAppName();
const s = useSettings((st) => st.settings);
const update = useSettings((st) => st.update);
const trusted = s.trustedImageSenders;
const identities = useMail((st) => st.identities);
const ownDomains = [...new Set(identities.map((i) => domainOf(i.email)).filter(Boolean))];
return (
{t("Privacy & safety")}
{t("What reaches a sender, and what asks before it happens.")}
{t("Remote content")}
{t("An image loaded from a sender's server tells them the message was opened, when, and from roughly where. Approved images are fetched by {app}'s own server rather than the browser, so the sender learns none of those.", { app: appName })}
{trusted.length > 0 && (
{trusted.map((addr) => (
{addr}
))}
{t("Added from a message, and removable here — previously the only way to undo one was to find another message from the same sender.")}
)}
{t("Read receipts")}
update({ requestReadReceipt: v })} label={t("Always request read receipts")} />
{t("A receipt tells whoever asked that this address is live and when the message was read, and the sender chooses where it goes — so there is no automatic option. Bulk mail, mailing lists and anything marked auto-submitted are never offered one at all.")}
{t("Warnings")}
{t("All three start switched off. A client that begins by interrupting is one people learn to click through, and a warning clicked through without reading costs the same attention and buys nothing.")}
update({ externalSenderBanner: v })}
label={t("Mark messages from outside")}
hint={t("A banner on any message whose sender is not on one of your own domains.")}
/>
update({ externalRecipientConfirm: v })}
label={t("Ask before sending outside")}
hint={t("Names the outside recipients and asks, rather than refusing.")}
/>
{(s.externalSenderBanner || s.externalRecipientConfirm) && (
update({ internalDomains })}
suggestions={ownDomains}
/>
)}
{t("Counts people rather than headers, so one address in To and nine in Cc is a message to ten. Catches a reply-all onto a long thread.")}
update({ externalLinkWarning: v })}
label={t("Ask before opening a link in a message")}
hint={t("A link whose text names one domain and whose destination is another is always flagged, even where the destination is trusted — being trusted is not the same as being the place the text claimed.")}
/>
{s.externalLinkWarning && (
update({ trustedLinkDomains })}
/>
)}
{t("Before it happens")}
{t("The message is held in this browser and has not been submitted yet, so taking it back costs nothing.")}
update({ attachmentReminder: v })} label={t("Attachment reminder")} hint={t("Warn when the message mentions an attachment but none is attached.")} />
update({ confirmDelete: v })} label={t("Confirm before deleting")} />
);
}
/**
* A list of domains, added one at a time and removed by their chip.
*
* Typed entries are normalized on the way in -- a leading `@`, stray case, a
* whole address pasted instead of a domain -- because the thing being compared
* against is a hostname, and a list holding "@Example.com " silently matches
* nothing at all.
*/
function DomainList({
label,
hint,
value,
onChange,
suggestions = [],
}: {
label: string;
hint: string;
value: string[];
onChange: (next: string[]) => void;
suggestions?: string[];
}) {
const [draft, setDraft] = useState("");
const add = (raw: string) => {
const d = raw.trim().toLowerCase().replace(/^@/, "").replace(/^.*@/, "").replace(/^https?:\/\//, "").split("/")[0] ?? "";
if (!d || value.includes(d)) {
setDraft("");
return;
}
onChange([...value, d]);
setDraft("");
};
const missing = suggestions.filter((d) => !value.includes(d));
return (