Replies: cursor on the first line, own messages answered to their recipients
A new reply opens with the caret on the empty first line above the quoted original (on a phone the keyboard comes up with it). Replying to one of your own messages goes to the people it was sent to (reply all keeps its Cc), not back to you; your own addresses, under every account, are never among reply-all recipients; a note sent only to yourself still answers yourself. The per-message Reply / Reply all button counts recipients the same way (lib/replyTargets.ts, with tests). The ThreadView test suite runs again: jsdom gets a stand-in Worker (pdf.js sets one up at import). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { useT } from "../../i18n";
|
||||
import { personHue } from "../../lib/avatarColor";
|
||||
import { api } from "../../lib/api";
|
||||
import { printMessages } from "../../lib/print";
|
||||
import { replyTargets } from "../../lib/replyTargets";
|
||||
import { useIsMobile } from "../../lib/useIsMobile";
|
||||
import { getDateLocale, getHour12 } from "../../lib/timeFormat";
|
||||
import type { AccountOut, MailboxOut, MessageEnvelope } from "../../lib/types";
|
||||
@@ -60,9 +61,12 @@ function attributionFor(msg: MessageEnvelope): string {
|
||||
return `On ${date}, at ${time}, ${who} wrote:`;
|
||||
}
|
||||
|
||||
function buildReplyInit(subject: string, msg: MessageEnvelope, kind: string): ComposerInit {
|
||||
const from = msg.from_addr[0]?.email ?? "";
|
||||
const toEmails = msg.to_addr.map((a) => a.email);
|
||||
function buildReplyInit(
|
||||
subject: string,
|
||||
msg: MessageEnvelope,
|
||||
kind: string,
|
||||
own: Set<string>,
|
||||
): ComposerInit {
|
||||
const norm = subject.replace(/^\s*(re|fwd?):\s*/i, "");
|
||||
const quote = { attribution: attributionFor(msg), messageId: msg.id };
|
||||
if (kind === "forward") {
|
||||
@@ -76,8 +80,9 @@ function buildReplyInit(subject: string, msg: MessageEnvelope, kind: string): Co
|
||||
forwardFrom: msg.id,
|
||||
};
|
||||
}
|
||||
const to = kind === "reply_all" ? [from, ...toEmails].filter(Boolean).join(", ") : from;
|
||||
const cc = kind === "reply_all" ? msg.cc_addr.map((a) => a.email).join(", ") : "";
|
||||
const targets = replyTargets(msg, own, kind);
|
||||
const to = targets.to.join(", ");
|
||||
const cc = targets.cc.join(", ");
|
||||
return {
|
||||
accountId: msg.account_id,
|
||||
threadId: msg.thread_id,
|
||||
@@ -315,6 +320,7 @@ function MessageItem({
|
||||
messageLabels,
|
||||
onToggleLabel,
|
||||
onCreateLabel,
|
||||
ownAddresses,
|
||||
}: {
|
||||
message: MessageEnvelope;
|
||||
expanded: boolean;
|
||||
@@ -337,6 +343,7 @@ function MessageItem({
|
||||
messageLabels: string[];
|
||||
onToggleLabel: (label: string) => void;
|
||||
onCreateLabel: (name: string) => void;
|
||||
ownAddresses: Set<string>;
|
||||
}) {
|
||||
const t = useT();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
@@ -367,7 +374,9 @@ function MessageItem({
|
||||
{message.from_addr[0]?.name || message.from_addr[0]?.email || "(unknown)"}
|
||||
</span>
|
||||
);
|
||||
const multiRecipient = message.to_addr.length + message.cc_addr.length > 1;
|
||||
// Reply all only when it would reach more than one person (not counting you).
|
||||
const allTargets = replyTargets(message, ownAddresses, "reply_all");
|
||||
const multiRecipient = allTargets.to.length + allTargets.cc.length > 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -631,6 +640,8 @@ export function ThreadView({
|
||||
for (const a of accounts ?? []) m.set(a.email.toLowerCase(), a.color);
|
||||
return m;
|
||||
}, [accounts]);
|
||||
// Your own addresses (every account's): replies never go back to them.
|
||||
const ownAddresses = useMemo(() => new Set(accountColor.keys()), [accountColor]);
|
||||
const accentFor = (msg: MessageEnvelope) =>
|
||||
accountColor.get((msg.from_addr[0]?.email ?? "").toLowerCase()) ?? null;
|
||||
|
||||
@@ -646,7 +657,7 @@ export function ThreadView({
|
||||
const openReply = (msg: MessageEnvelope, kind: string) => {
|
||||
if (specialUse.get(msg.mailbox_id) === "trash") return; // no reply/forward from Trash
|
||||
setExpanded((prev) => new Set(prev).add(msg.id));
|
||||
setComposing({ anchorId: msg.id, init: buildReplyInit(subject, msg, kind) });
|
||||
setComposing({ anchorId: msg.id, init: buildReplyInit(subject, msg, kind, ownAddresses) });
|
||||
};
|
||||
|
||||
// A mailto: link in the message body opens a fresh composer to that address,
|
||||
@@ -785,6 +796,7 @@ export function ThreadView({
|
||||
messageLabels={msgLabels[message.id] ?? []}
|
||||
onToggleLabel={(l) => toggleLabel(message.id, l)}
|
||||
onCreateLabel={(name) => void createLabel(name, message.id)}
|
||||
ownAddresses={ownAddresses}
|
||||
/>
|
||||
{isNewest && (
|
||||
<div data-testid="composer-anchor">
|
||||
|
||||
Reference in New Issue
Block a user