fix(webmail): Undo send really undoes, and a send is not made twice
The countdown sent from inside a state updater and Undo's button submitted the form, so Undo stopped the timer and then started the send over. The timer is now held in a ref, Undo is type=button, and leaving the screen flushes a pending send once. Snippets drop a stylesheet the preview was cut inside of. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2080489319
commit
897f0122f4
5 files changed
+97
-12
No files matched your search
@@ -87,6 +87,11 @@ export function Composer({ initial, variant = "page", onSent, onDiscarded }: Com
|
||||
// way when that happens would otherwise land afterwards and bring the draft
|
||||
// back, so every save checks this before and after it talks to the server.
|
||||
const discardedRef = useRef(false);
|
||||
// The send waiting out its ten seconds, so Undo can stop it and leaving the
|
||||
// screen cannot lose it.
|
||||
const pendingSend = useRef<{ timer: ReturnType<typeof setInterval>; html: string } | null>(null);
|
||||
const sendRef = useRef<(html: string) => Promise<void>>(async () => undefined);
|
||||
const unmounted = useRef(false);
|
||||
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
// The body, mirrored into React state.
|
||||
@@ -98,6 +103,21 @@ export function Composer({ initial, variant = "page", onSent, onDiscarded }: Com
|
||||
// that is why a draft left behind by a sent message was empty.
|
||||
const [body, setBody] = useState("");
|
||||
|
||||
// Leaving with a send still counting down sends it now. Dropping it would lose
|
||||
// a message the writer was told was on its way; Undo is the way to cancel.
|
||||
useEffect(() => {
|
||||
unmounted.current = false;
|
||||
return () => {
|
||||
unmounted.current = true;
|
||||
const pending = pendingSend.current;
|
||||
if (pending) {
|
||||
clearInterval(pending.timer);
|
||||
pendingSend.current = null;
|
||||
void sendRef.current(pending.html);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// An inline reply is opened to be written in, so the cursor starts in it.
|
||||
useEffect(() => {
|
||||
if (inline) editorRef.current?.focus();
|
||||
@@ -290,16 +310,24 @@ export function Composer({ initial, variant = "page", onSent, onDiscarded }: Com
|
||||
|
||||
const bodyHtml = editorRef.current?.innerHTML || body;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setUndoSeconds((prev) => {
|
||||
if (prev === null || prev <= 1) {
|
||||
clearInterval(interval);
|
||||
void sendActualMessage(bodyHtml);
|
||||
return null;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
// The countdown is counted here and not inside a state updater. The send
|
||||
// used to be a side effect of that updater, which React may run twice (the
|
||||
// message went out twice) and which Undo could not stop: Undo only cleared
|
||||
// the number, and the next tick - finding it empty - sent the message
|
||||
// anyway.
|
||||
let remaining = 10;
|
||||
const timer = setInterval(() => {
|
||||
remaining -= 1;
|
||||
if (remaining > 0) {
|
||||
setUndoSeconds(remaining);
|
||||
return;
|
||||
}
|
||||
clearInterval(timer);
|
||||
pendingSend.current = null;
|
||||
setUndoSeconds(null);
|
||||
void sendRef.current(bodyHtml);
|
||||
}, 1000);
|
||||
pendingSend.current = { timer, html: bodyHtml };
|
||||
};
|
||||
|
||||
const sendActualMessage = async (htmlBody: string) => {
|
||||
@@ -329,14 +357,22 @@ export function Composer({ initial, variant = "page", onSent, onDiscarded }: Com
|
||||
setSending(false);
|
||||
return;
|
||||
}
|
||||
onSent();
|
||||
// A flush after leaving must not drag the writer back to Sent.
|
||||
if (!unmounted.current) onSent();
|
||||
} catch {
|
||||
setSendError(t("errors.serverError"));
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
sendRef.current = sendActualMessage;
|
||||
|
||||
const handleUndo = () => {
|
||||
// Stops the timer itself: clearing the number on screen is not enough.
|
||||
if (pendingSend.current) {
|
||||
clearInterval(pendingSend.current.timer);
|
||||
pendingSend.current = null;
|
||||
}
|
||||
setUndoSeconds(null);
|
||||
setSending(false);
|
||||
};
|
||||
@@ -412,7 +448,9 @@ export function Composer({ initial, variant = "page", onSent, onDiscarded }: Com
|
||||
remaining) (9s remaining)". */}
|
||||
<span>{t("mail.undoSend", { seconds: undoSeconds })}</span>
|
||||
</div>
|
||||
<Button variant="primary" size="sm" onClick={handleUndo}>
|
||||
{/* type="button": inside this form a button submits it, so Undo used to
|
||||
stop the countdown and then send the message again. */}
|
||||
<Button type="button" variant="primary" size="sm" onClick={handleUndo}>
|
||||
<Undo2 className="h-3.5 w-3.5" />
|
||||
{t("mail.undoSendAction")}
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user