From fed60c1b185fdffed8dbead380f3c30e30d0fe6f Mon Sep 17 00:00:00 2001 From: "Can K." Date: Sat, 10 Oct 2026 22:08:08 +0300 Subject: [PATCH] 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> --- src/components/compose/compose-context.tsx | 13 ++++++++++++- src/components/compose/compose-form.tsx | 12 +++++++++++- src/components/compose/floating-composer.tsx | 20 ++++++++++++++------ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/components/compose/compose-context.tsx b/src/components/compose/compose-context.tsx index c6411ec..3eb34ef 100644 --- a/src/components/compose/compose-context.tsx +++ b/src/components/compose/compose-context.tsx @@ -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(null); @@ -22,6 +24,13 @@ export function useCompose() { export function ComposeProvider({ children }: { children: ReactNode }) { const [open, setOpen] = useState(false); const [draftId, setDraftId] = useState(null); + const [notice, setNotice] = useState(null); + + useEffect(() => { + if (!notice) return; + const timer = setTimeout(() => setNotice(null), 3200); + return () => clearTimeout(timer); + }, [notice]); return ( {children} diff --git a/src/components/compose/compose-form.tsx b/src/components/compose/compose-form.tsx index c9d7c0c..8b352b2 100644 --- a/src/components/compose/compose-form.tsx +++ b/src/components/compose/compose-form.tsx @@ -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(null); const [agentRevision, setAgentRevision] = useState(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() { diff --git a/src/components/compose/floating-composer.tsx b/src/components/compose/floating-composer.tsx index 8370a09..ee0007b 100644 --- a/src/components/compose/floating-composer.tsx +++ b/src/components/compose/floating-composer.tsx @@ -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 { - closeComposer(); - if (/^\/drafts\/[^/]+\/?$/.test(pathname)) router.replace("/drafts"); - }} />; + return ( + <> + {notice && ( +
+ {notice} +
+ )} + {open && { + closeComposer(); + if (/^\/drafts\/[^/]+\/?$/.test(pathname)) router.replace("/drafts"); + }} />} + + ); }