Close the composer after a message is sent (#83)

After a successful send the composer kept its window open, emptied, and the "Message sent" toast lived inside it. The popup now closes (the full-page composer goes to Sent) and the confirmation is shown by FloatingComposer through the compose context, so it stays visible once the composer is gone. It is a status region so screen readers announce it.

Sending also cancels the pending autosave and invalidates in-flight ones. Before, a save that fired during the send could recreate the message as a draft, which then sat in Drafts next to the sent copy.

Co-authored-by: Can Kirca <9063990+cankirca@users.noreply.github.com>
This commit is contained in:
Can K.andCan Kirca authored and GitHub committed 2026-10-11 02:08:08 +07:00
1 parent 21817cedcb
commit fed60c1b18
3 files changed
+37 -8

No files matched your search

+12 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { createContext, useContext, useState } from "react";
import { createContext, useContext, useEffect, useState } from "react";
import type { ReactNode } from "react";
type ComposeContextValue = {
@@ -9,6 +9,8 @@ type ComposeContextValue = {
openComposer: () => void;
openDraftComposer: (draftId: string) => void;
closeComposer: () => void;
notice: string | null;
showNotice: (message: string) => void;
};
const ComposeContext = createContext<ComposeContextValue | null>(null);
@@ -22,6 +24,13 @@ export function useCompose() {
export function ComposeProvider({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const [draftId, setDraftId] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
useEffect(() => {
if (!notice) return;
const timer = setTimeout(() => setNotice(null), 3200);
return () => clearTimeout(timer);
}, [notice]);
return (
<ComposeContext.Provider
@@ -40,6 +49,8 @@ export function ComposeProvider({ children }: { children: ReactNode }) {
setOpen(false);
setDraftId(null);
},
notice,
showNotice: setNotice,
}}
>
{children}
+11 -1
View File
@@ -13,6 +13,7 @@ import { Tooltip } from "@/components/ui/tooltip";
import { useSelectedMailbox } from "@/components/mailbox-provider";
import { authFetch } from "@/lib/auth/client";
import { formatEmailAddress, getEmailAddress } from "@/lib/email/address";
import { useCompose } from "@/components/compose/compose-context";
import { cn } from "@/lib/utils";
import { SendReview } from "@/components/agent/send-review";
import type { ReviewSnapshot } from "@/components/agent/send-review-types";
@@ -45,6 +46,7 @@ export function ComposeForm({
}) {
const { t } = useLanguage();
const router = useRouter();
const { showNotice } = useCompose();
const { selectedMailbox, setSelectedMailbox, mailboxes } = useSelectedMailbox();
const [draftId, setDraftId] = useState<string | null>(null);
const [agentRevision, setAgentRevision] = useState<number | null>(null);
@@ -253,6 +255,7 @@ export function ComposeForm({
setToast({ type: "error", message: t("compose.error.emptyBody") });
return;
}
if (saveTimer.current) clearTimeout(saveTimer.current);
setLoading(true);
const fullHtml = joinQuotedHtml(html, quotedHtml);
if (draftId && agentRevision !== null) {
@@ -310,6 +313,7 @@ export function ComposeForm({
window.dispatchEvent(new Event("mailflare:messages-changed"));
});
}
draftGeneration.current += 1;
setDraftId(null);
setTo([]);
setCc([]);
@@ -323,8 +327,14 @@ export function ComposeForm({
setQuotedHtml(null);
setAttachments([]);
setScheduledAt(null);
setToast({ type: "success", message: data.scheduled ? t("compose.scheduled") : t("compose.sent") });
showNotice(data.scheduled ? t("compose.scheduled") : t("compose.sent"));
window.dispatchEvent(new Event("mailflare:messages-changed"));
if (onClose) {
onClose();
return;
}
router.push("/sent");
}
async function deleteDraftAndClose() {
+14 -6
View File
@@ -5,12 +5,20 @@ import { ComposeForm } from "@/components/compose/compose-form";
import { useCompose } from "@/components/compose/compose-context";
export function FloatingComposer() {
const { open, draftId, closeComposer } = useCompose();
const { open, draftId, closeComposer, notice } = useCompose();
const pathname = usePathname();
const router = useRouter();
if (!open) return null;
return <ComposeForm key={draftId ?? "new"} mode="popup" draftIdToLoad={draftId} onClose={() => {
closeComposer();
if (/^\/drafts\/[^/]+\/?$/.test(pathname)) router.replace("/drafts");
}} />;
return (
<>
{notice && (
<div role="status" className="fixed right-6 top-6 z-[60] rounded-lg bg-green-600 px-4 py-3 text-sm font-medium text-white shadow-lg">
{notice}
</div>
)}
{open && <ComposeForm key={draftId ?? "new"} mode="popup" draftIdToLoad={draftId} onClose={() => {
closeComposer();
if (/^\/drafts\/[^/]+\/?$/.test(pathname)) router.replace("/drafts");
}} />}
</>
);
}