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.
This commit is contained in:
Shuki Vaknin
2026-07-21 20:58:27 +02:00
committed by Linus Rath
parent a3f9055541
commit f15edd336b
4 changed files with 68 additions and 21 deletions
+21
View File
@@ -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: () => {
+25 -1
View File
@@ -21,6 +21,7 @@ export interface Toast {
onClick?: () => void;
icon?: React.ReactNode;
action?: ToastAction;
secondaryAction?: ToastAction;
}
interface ToastProps {
@@ -117,6 +118,27 @@ export function ToastItem({ toast, onClose }: ToastProps) {
{toast.message && (
<p className="text-[12px] mt-1 text-muted-foreground leading-snug">{toast.message}</p>
)}
{(toast.action || toast.secondaryAction) && (
<div className="mt-2 flex items-center gap-2">
{toast.secondaryAction && (
<button
onClick={(e) => {
e.stopPropagation();
try {
toast.secondaryAction!.onClick();
dismiss();
} catch {
// Don't close toast on error so user can retry
}
}}
className={cn(
"text-[12px] font-semibold px-2.5 py-1 rounded-md transition-colors",
"bg-primary text-primary-foreground hover:bg-primary/90"
)}
>
{toast.secondaryAction.label}
</button>
)}
{toast.action && (
<button
onClick={(e) => {
@@ -129,7 +151,7 @@ export function ToastItem({ toast, onClose }: ToastProps) {
}
}}
className={cn(
"mt-2 text-[12px] font-semibold px-2.5 py-1 rounded-md transition-colors",
"text-[12px] font-semibold px-2.5 py-1 rounded-md transition-colors",
"bg-foreground/5 hover:bg-foreground/10 dark:bg-white/10 dark:hover:bg-white/15",
"text-foreground"
)}
@@ -138,6 +160,8 @@ export function ToastItem({ toast, onClose }: ToastProps) {
</button>
)}
</div>
)}
</div>
{/* Close button */}
<button
+3 -3
View File
@@ -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<EmailStore>((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<EmailStore>((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;
+2
View File
@@ -38,6 +38,7 @@ export const useToastStore = create<ToastStore>((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,
});
}