180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
import { Ban, Loader2, Send } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { MAIL_TOAST_SURFACE_CLASS } from "@/lib/mail-chrome-classes"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const DEFAULT_DURATION_MS = 3000
|
|
|
|
type RefBox<T> = { current: T }
|
|
|
|
export function showPendingSendToast(options: {
|
|
durationMs?: number
|
|
onCommit: () => void | Promise<void>
|
|
onCancel: () => void
|
|
}) {
|
|
const durationMs = options.durationMs ?? DEFAULT_DURATION_MS
|
|
const committedRef: RefBox<boolean> = { current: false }
|
|
const timerRef: RefBox<number | null> = {
|
|
current: null,
|
|
}
|
|
|
|
toast.custom(
|
|
(toastId) => (
|
|
<PendingSendToastBody
|
|
toastId={toastId}
|
|
durationMs={durationMs}
|
|
committedRef={committedRef}
|
|
timerRef={timerRef}
|
|
onCommit={options.onCommit}
|
|
onCancel={options.onCancel}
|
|
/>
|
|
),
|
|
{
|
|
duration: Infinity,
|
|
onDismiss: () => {
|
|
if (timerRef.current != null) {
|
|
clearTimeout(timerRef.current)
|
|
timerRef.current = null
|
|
}
|
|
if (!committedRef.current) {
|
|
committedRef.current = true
|
|
options.onCancel()
|
|
}
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
function PendingSendToastBody({
|
|
toastId,
|
|
durationMs,
|
|
committedRef,
|
|
timerRef,
|
|
onCommit,
|
|
onCancel,
|
|
}: {
|
|
toastId: string | number
|
|
durationMs: number
|
|
committedRef: RefBox<boolean>
|
|
timerRef: RefBox<number | null>
|
|
onCommit: () => void | Promise<void>
|
|
onCancel: () => void
|
|
}) {
|
|
const onCommitRef = useRef(onCommit)
|
|
const onCancelRef = useRef(onCancel)
|
|
onCommitRef.current = onCommit
|
|
onCancelRef.current = onCancel
|
|
|
|
const [barImmediate, setBarImmediate] = useState(false)
|
|
|
|
const commitNowRef = useRef<(opts?: { manual?: boolean }) => void>(() => {})
|
|
|
|
commitNowRef.current = (opts?: { manual?: boolean }) => {
|
|
if (committedRef.current) return
|
|
if (opts?.manual) setBarImmediate(true)
|
|
committedRef.current = true
|
|
if (timerRef.current != null) {
|
|
clearTimeout(timerRef.current)
|
|
timerRef.current = null
|
|
}
|
|
void (async () => {
|
|
try {
|
|
await onCommitRef.current()
|
|
toast.success("Message envoyé")
|
|
} catch {
|
|
toast.error("L'envoi a échoué")
|
|
onCancelRef.current()
|
|
} finally {
|
|
toast.dismiss(toastId)
|
|
}
|
|
})()
|
|
}
|
|
|
|
useEffect(() => {
|
|
timerRef.current = window.setTimeout(() => {
|
|
commitNowRef.current()
|
|
}, durationMs)
|
|
return () => {
|
|
if (timerRef.current != null) {
|
|
clearTimeout(timerRef.current)
|
|
timerRef.current = null
|
|
}
|
|
}
|
|
}, [durationMs, timerRef])
|
|
|
|
const handleSendNow = () => {
|
|
commitNowRef.current({ manual: true })
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
if (committedRef.current) return
|
|
committedRef.current = true
|
|
if (timerRef.current != null) {
|
|
clearTimeout(timerRef.current)
|
|
timerRef.current = null
|
|
}
|
|
onCancelRef.current()
|
|
toast.dismiss(toastId)
|
|
}
|
|
|
|
return (
|
|
<div className={cn(MAIL_TOAST_SURFACE_CLASS, "backdrop-blur-sm")}>
|
|
<div className="px-3.5 pb-2.5 pt-3">
|
|
<div className="flex items-center gap-3">
|
|
<span
|
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/15 text-primary"
|
|
aria-hidden
|
|
>
|
|
<Loader2 className="h-4 w-4 animate-spin" strokeWidth={2} />
|
|
</span>
|
|
<div className="min-w-0">
|
|
<p className="text-[13px] font-semibold leading-snug tracking-tight text-foreground">
|
|
Envoi en cours…
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="mt-2.5 grid grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)] gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={handleSendNow}
|
|
className="inline-flex min-h-9 min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg bg-[#1a73e8] px-2.5 py-2 text-xs font-semibold leading-tight text-white shadow-sm transition-colors hover:bg-[#1765cc] active:bg-[#1557b0]"
|
|
>
|
|
<Send className="h-3.5 w-3.5 shrink-0 opacity-95" strokeWidth={2} aria-hidden />
|
|
<span>Envoyer maintenant</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleCancel}
|
|
className="inline-flex min-h-9 min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg border border-border bg-mail-surface px-2.5 py-2 text-xs font-semibold leading-tight text-muted-foreground shadow-sm transition-colors hover:border-border hover:bg-accent hover:text-foreground"
|
|
>
|
|
<Ban className="h-3.5 w-3.5 shrink-0" strokeWidth={2} aria-hidden />
|
|
<span>Annuler l'envoi</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="relative h-1 w-full shrink-0 bg-muted">
|
|
<div
|
|
className="absolute inset-y-0 left-0 bg-linear-to-r from-[#1a73e8] to-[#4285f4]"
|
|
style={
|
|
barImmediate
|
|
? { width: "100%" }
|
|
: {
|
|
width: "0%",
|
|
animation: `ultimail-pending-send-progress ${durationMs}ms linear forwards`,
|
|
}
|
|
}
|
|
/>
|
|
</div>
|
|
<style>{`
|
|
@keyframes ultimail-pending-send-progress {
|
|
from { width: 0%; }
|
|
to { width: 100%; }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
)
|
|
}
|