From f15edd336b3af7b3d2b483a124b0d8f2b276ba9e Mon Sep 17 00:00:00 2001 From: Shuki Vaknin Date: Thu, 25 Jun 2026 18:52:24 +0300 Subject: [PATCH] feat(send): 'Send now' on the send-delay toast The post-send undo toast ('scheduled to send' + Cancel send) now also offers a 'Send now' action that reschedules the delayed submission for immediate release, so you can skip the undo window without waiting it out. Adds an optional secondaryAction to the toast component and carries identityId on the pending undo-send state so the reschedule can target the right identity. --- app/(main)/[locale]/page.tsx | 21 +++++++++++++ components/ui/toast.tsx | 60 +++++++++++++++++++++++++----------- stores/email-store.ts | 6 ++-- stores/toast-store.ts | 2 ++ 4 files changed, 68 insertions(+), 21 deletions(-) diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index 464b795c..0db35bc3 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -1428,6 +1428,27 @@ export default function Home() { toast.success(t('email_viewer.scheduled_send_created'), { duration: undoDurationMs, + secondaryAction: (pending.emailId && pending.identityId) + ? { + label: t('email_viewer.send_now'), + onClick: () => { + void (async () => { + try { + await client.rescheduleEmailSubmission( + pending.submissionId, + pending.emailId!, + pending.identityId!, + new Date(Date.now() + 1000).toISOString(), + ); + clearPendingUndoSend(); + if (isScheduledView) await fetchScheduledEmails(client); + } catch (error) { + console.error('Failed to send now:', error); + } + })(); + }, + } + : undefined, action: { label: t('email_viewer.undo_send'), onClick: () => { diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx index 9e55dc92..25ffe56e 100644 --- a/components/ui/toast.tsx +++ b/components/ui/toast.tsx @@ -21,6 +21,7 @@ export interface Toast { onClick?: () => void; icon?: React.ReactNode; action?: ToastAction; + secondaryAction?: ToastAction; } interface ToastProps { @@ -117,25 +118,48 @@ export function ToastItem({ toast, onClose }: ToastProps) { {toast.message && (

{toast.message}

)} - {toast.action && ( - )} - > - {toast.action.label} - + {toast.action && ( + + )} + )} diff --git a/stores/email-store.ts b/stores/email-store.ts index 33d13867..6ee083cf 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -20,7 +20,7 @@ type ScheduledSubmissionMetadata = { const VIRTUAL_SCHEDULED_MAILBOX_ID = '__scheduled__'; -type PendingUndoSend = { submissionId: string; emailId?: string; sendAt: string; isSmime: boolean }; +type PendingUndoSend = { submissionId: string; emailId?: string; identityId?: string; sendAt: string; isSmime: boolean }; interface EmailStore { emails: Email[]; @@ -1287,7 +1287,7 @@ export const useEmailStore = create((set, get) => ({ set({ isLoading: false, pendingUndoSend: result.scheduled && result.emailSubmissionId && result.sendAt - ? { submissionId: result.emailSubmissionId, emailId: result.emailId, sendAt: result.sendAt, isSmime: false } + ? { submissionId: result.emailSubmissionId, emailId: result.emailId, identityId, sendAt: result.sendAt, isSmime: false } : get().pendingUndoSend, }); return result; @@ -1311,7 +1311,7 @@ export const useEmailStore = create((set, get) => ({ set({ isLoading: false, pendingUndoSend: result.scheduled && result.emailSubmissionId && result.sendAt - ? { submissionId: result.emailSubmissionId, emailId: result.emailId, sendAt: result.sendAt, isSmime: true } + ? { submissionId: result.emailSubmissionId, emailId: result.emailId, identityId, sendAt: result.sendAt, isSmime: true } : get().pendingUndoSend, }); return result; diff --git a/stores/toast-store.ts b/stores/toast-store.ts index 35e3e4aa..820e178a 100644 --- a/stores/toast-store.ts +++ b/stores/toast-store.ts @@ -38,6 +38,7 @@ export const useToastStore = create((set) => ({ interface ToastOptions { message?: string; action?: ToastAction; + secondaryAction?: ToastAction; duration?: number; } @@ -48,6 +49,7 @@ function showToast(type: Toast["type"], title: string, options?: string | ToastO title, message: opts?.message, action: opts?.action, + secondaryAction: opts?.secondaryAction, duration: opts?.duration ?? defaultDuration, }); }