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:
@@ -1428,6 +1428,27 @@ export default function Home() {
|
|||||||
|
|
||||||
toast.success(t('email_viewer.scheduled_send_created'), {
|
toast.success(t('email_viewer.scheduled_send_created'), {
|
||||||
duration: undoDurationMs,
|
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: {
|
action: {
|
||||||
label: t('email_viewer.undo_send'),
|
label: t('email_viewer.undo_send'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
|||||||
+42
-18
@@ -21,6 +21,7 @@ export interface Toast {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
action?: ToastAction;
|
action?: ToastAction;
|
||||||
|
secondaryAction?: ToastAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToastProps {
|
interface ToastProps {
|
||||||
@@ -117,25 +118,48 @@ export function ToastItem({ toast, onClose }: ToastProps) {
|
|||||||
{toast.message && (
|
{toast.message && (
|
||||||
<p className="text-[12px] mt-1 text-muted-foreground leading-snug">{toast.message}</p>
|
<p className="text-[12px] mt-1 text-muted-foreground leading-snug">{toast.message}</p>
|
||||||
)}
|
)}
|
||||||
{toast.action && (
|
{(toast.action || toast.secondaryAction) && (
|
||||||
<button
|
<div className="mt-2 flex items-center gap-2">
|
||||||
onClick={(e) => {
|
{toast.secondaryAction && (
|
||||||
e.stopPropagation();
|
<button
|
||||||
try {
|
onClick={(e) => {
|
||||||
toast.action!.onClick();
|
e.stopPropagation();
|
||||||
dismiss();
|
try {
|
||||||
} catch {
|
toast.secondaryAction!.onClick();
|
||||||
// Don't close toast on error so user can retry
|
dismiss();
|
||||||
}
|
} catch {
|
||||||
}}
|
// Don't close toast on error so user can retry
|
||||||
className={cn(
|
}
|
||||||
"mt-2 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",
|
className={cn(
|
||||||
"text-foreground"
|
"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 && (
|
||||||
{toast.action.label}
|
<button
|
||||||
</button>
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
try {
|
||||||
|
toast.action!.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-foreground/5 hover:bg-foreground/10 dark:bg-white/10 dark:hover:bg-white/15",
|
||||||
|
"text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{toast.action.label}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type ScheduledSubmissionMetadata = {
|
|||||||
|
|
||||||
const VIRTUAL_SCHEDULED_MAILBOX_ID = '__scheduled__';
|
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 {
|
interface EmailStore {
|
||||||
emails: Email[];
|
emails: Email[];
|
||||||
@@ -1287,7 +1287,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
set({
|
set({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
pendingUndoSend: result.scheduled && result.emailSubmissionId && result.sendAt
|
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,
|
: get().pendingUndoSend,
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
@@ -1311,7 +1311,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
set({
|
set({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
pendingUndoSend: result.scheduled && result.emailSubmissionId && result.sendAt
|
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,
|
: get().pendingUndoSend,
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const useToastStore = create<ToastStore>((set) => ({
|
|||||||
interface ToastOptions {
|
interface ToastOptions {
|
||||||
message?: string;
|
message?: string;
|
||||||
action?: ToastAction;
|
action?: ToastAction;
|
||||||
|
secondaryAction?: ToastAction;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ function showToast(type: Toast["type"], title: string, options?: string | ToastO
|
|||||||
title,
|
title,
|
||||||
message: opts?.message,
|
message: opts?.message,
|
||||||
action: opts?.action,
|
action: opts?.action,
|
||||||
|
secondaryAction: opts?.secondaryAction,
|
||||||
duration: opts?.duration ?? defaultDuration,
|
duration: opts?.duration ?? defaultDuration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user