"use client" import { useState, useCallback, lazy, Suspense, } from "react" import { useEditor, type Editor } from "@tiptap/react" import { ChevronDown, Paperclip, Link as LinkIcon, Smile, HardDrive, Image as ImageIcon, Lock, PenTool, MoreVertical, Trash2, Bold, Italic, Underline as UnderlineIcon, AlignLeft, AlignCenter, AlignRight, AlignJustify, List, ListOrdered, Undo, Redo, Type, Clock, Indent, Outdent, RemoveFormatting, Palette, ALargeSmall, CaseSensitive, Send, } from "lucide-react" import { type ComposeState, SIGNATURES, useComposeActions, } from "@/lib/compose-context" import { cn, getNextLocalWallClockDate } from "@/lib/utils" import { MAIL_COMPOSE_BOTTOM_ICON_BTN, MAIL_COMPOSE_BOTTOM_ICON_BTN_ACTIVE, MAIL_COMPOSE_MENU_SELECTED_CLASS, MAIL_COMPOSE_POPOVER_CLASS, MAIL_COMPOSE_PRIMARY_SEND_BTN, MAIL_COMPOSE_TOOLBAR_BTN, MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE, MAIL_COMPOSE_TOOLBAR_SEP, MAIL_MENU_SURFACE_CLASS, } from "@/lib/mail-chrome-classes" import { useTheme } from "next-themes" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import data from "@emoji-mart/data" import { COMPOSE_PORTAL_Z, insertSignatureHtml } from "./compose-shared" const LazyPicker = lazy(() => import("@emoji-mart/react")) function ComposeEmojiPicker({ onSelect }: { onSelect: (emoji: { native: string }) => void }) { const { resolvedTheme } = useTheme() return ( Chargement…}> ) } function AlignmentDropdown({ editor, btnClass, activeClass, }: { editor: NonNullable> btnClass: string activeClass: string }) { const currentIcon = editor.isActive({ textAlign: "center" }) ? AlignCenter : editor.isActive({ textAlign: "right" }) ? AlignRight : editor.isActive({ textAlign: "justify" }) ? AlignJustify : AlignLeft const CurrentIcon = currentIcon return ( editor.chain().focus().setTextAlign("left").run()} className={cn(editor.isActive({ textAlign: "left" }) && MAIL_COMPOSE_MENU_SELECTED_CLASS)} > Aligner à gauche editor.chain().focus().setTextAlign("center").run()} className={cn(editor.isActive({ textAlign: "center" }) && MAIL_COMPOSE_MENU_SELECTED_CLASS)} > Centrer editor.chain().focus().setTextAlign("right").run()} className={cn(editor.isActive({ textAlign: "right" }) && MAIL_COMPOSE_MENU_SELECTED_CLASS)} > Aligner à droite editor.chain().focus().setTextAlign("justify").run()} className={cn(editor.isActive({ textAlign: "justify" }) && MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE)} > Justifier ) } const FONT_FAMILIES = [ { label: "Sans Serif", value: "sans-serif" }, { label: "Serif", value: "serif" }, { label: "Monospace", value: "monospace" }, { label: "Cursive", value: "cursive" }, { label: "Comic Sans MS", value: "Comic Sans MS, cursive" }, { label: "Garamond", value: "Garamond, serif" }, { label: "Georgia", value: "Georgia, serif" }, { label: "Impact", value: "Impact, sans-serif" }, { label: "Tahoma", value: "Tahoma, sans-serif" }, { label: "Trebuchet MS", value: "Trebuchet MS, sans-serif" }, { label: "Verdana", value: "Verdana, sans-serif" }, ] const FONT_SIZES = [ { label: "Très petit", value: "10px" }, { label: "Petit", value: "13px" }, { label: "Normal", value: "" }, { label: "Grand", value: "18px" }, { label: "Très grand", value: "24px" }, { label: "Énorme", value: "32px" }, ] const TEXT_COLORS = [ "#000000", "#434343", "#666666", "#999999", "#cccccc", "#efefef", "#f3f3f3", "#ffffff", "#fb4934", "#fe8019", "#fabd2f", "#b8bb26", "#8ec07c", "#83a598", "#d3869b", "#ebdbb2", "#cc241d", "#d65d0e", "#d79921", "#98971a", "#689d6a", "#458588", "#b16286", "#a89984", "#9d0006", "#af3a03", "#b57614", "#79740e", "#427b58", "#076678", "#8f3f71", "#7c6f64", ] function FontDropdown({ editor, btnClass, }: { editor: NonNullable> btnClass: string }) { return ( {FONT_FAMILIES.map((f) => ( editor.chain().focus().setMark("textStyle", { fontFamily: f.value }).run()} style={{ fontFamily: f.value }} className={cn( editor.isActive("textStyle", { fontFamily: f.value }) && MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE )} > {f.label} ))} ) } function FontSizeDropdown({ editor, btnClass, }: { editor: NonNullable> btnClass: string }) { return ( {FONT_SIZES.map((s) => ( { if (s.value) { editor.chain().focus().setMark("textStyle", { fontSize: s.value }).run() } else { editor.chain().focus().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run() } }} style={s.value ? { fontSize: s.value } : undefined} className={cn( s.value && editor.isActive("textStyle", { fontSize: s.value }) && MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE )} > {s.label} ))} ) } function ColorDropdown({ editor, btnClass, }: { editor: NonNullable> btnClass: string }) { const [tab, setTab] = useState<"text" | "bg">("text") return ( e.preventDefault()} >
{TEXT_COLORS.map((color) => (
) } export function FormattingToolbar({ editor, }: { editor: Editor | null }) { if (!editor) return null const btnClass = MAIL_COMPOSE_TOOLBAR_BTN const activeClass = MAIL_COMPOSE_TOOLBAR_BTN_ACTIVE const sep = return (
{/* Undo / Redo */} {sep} {/* Font */} {sep} {/* Font size */} {sep} {/* Bold, Italic, Underline, Colors */} {sep} {/* Alignment dropdown, lists, indent/outdent, remove formatting */}
) } function ComposeEmojiButton({ editor, }: { editor: Editor | null }) { const [open, setOpen] = useState(false) const handleSelect = useCallback( (emoji: { native: string }) => { editor?.chain().focus().insertContent(emoji.native).run() setOpen(false) }, [editor] ) if (!editor) return null return ( e.preventDefault()} > ) } function ComposeLinkButton({ editor, }: { editor: Editor | null }) { const [open, setOpen] = useState(false) const [url, setUrl] = useState("") const [text, setText] = useState("") if (!editor) return null const isLinkActive = editor.isActive("link") const handleToggle = () => { if (isLinkActive) { editor.chain().focus().extendMarkRange("link").unsetLink().run() return } setOpen(true) } const handleOpen = (isOpen: boolean) => { if (isOpen) { const { from, to, empty } = editor.state.selection if (isLinkActive) { const attrs = editor.getAttributes("link") setUrl(attrs.href || "") const selectedText = editor.state.doc.textBetween(from, to, " ") setText(selectedText) } else if (!empty) { const selectedText = editor.state.doc.textBetween(from, to, " ") setText(selectedText) setUrl("") } else { setText("") setUrl("") } } setOpen(isOpen) } const handleInsert = () => { if (!url.trim()) return const href = url.match(/^https?:\/\//) ? url : `https://${url}` const { empty } = editor.state.selection if (empty && !isLinkActive) { const displayText = text.trim() || href editor .chain() .focus() .insertContent(`${displayText}`) .run() } else { if (text.trim() && text.trim() !== editor.state.doc.textBetween( editor.state.selection.from, editor.state.selection.to, " " )) { editor .chain() .focus() .deleteSelection() .insertContent(`${text.trim()}`) .run() } else { editor .chain() .focus() .extendMarkRange("link") .setLink({ href }) .run() } } setOpen(false) setUrl("") setText("") } const handleRemoveLink = () => { editor.chain().focus().extendMarkRange("link").unsetLink().run() setOpen(false) setUrl("") setText("") } return ( e.preventDefault()} >
{isLinkActive ? "Modifier le lien" : "Insérer un lien"}
setText(e.target.value)} placeholder="Texte du lien" className="h-8 rounded border border-border bg-mail-surface px-2 text-sm text-foreground outline-none focus:border-ring focus:ring-1 focus:ring-ring" />
setUrl(e.target.value)} placeholder="https://example.com" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() handleInsert() } }} className="h-8 rounded border border-border bg-mail-surface px-2 text-sm text-foreground outline-none focus:border-ring focus:ring-1 focus:ring-ring" autoFocus />
{isLinkActive ? ( ) : ( )}
) } export function ComposeSignatureButton({ editor, compose, }: { editor: Editor | null compose: ComposeState }) { const { updateCompose } = useComposeActions() const replaceSignature = useCallback( (sigId: string | null) => { if (!editor) return const newHtml = insertSignatureHtml(editor.getHTML(), sigId) editor.commands.setContent(newHtml) updateCompose(compose.id, { bodyHtml: newHtml, signatureId: sigId }) }, [editor, compose.id, updateCompose] ) const toggleAutoInsert = useCallback(() => { const newVal = !compose.autoInsertSignature updateCompose(compose.id, { autoInsertSignature: newVal }) if (!newVal) { replaceSignature(null) } else { const sigId = compose.from.defaultSignatureId if (sigId) replaceSignature(sigId) } }, [compose.autoInsertSignature, compose.from.defaultSignatureId, compose.id, updateCompose, replaceSignature]) if (!editor) return null return ( { e.preventDefault() toggleAutoInsert() }} className="gap-2" > {compose.autoInsertSignature && } Insérer automatiquement replaceSignature(null)} className={cn("gap-2", !compose.signatureId && MAIL_COMPOSE_MENU_SELECTED_CLASS)} > {!compose.signatureId && } Aucune signature {SIGNATURES.map((sig) => ( replaceSignature(sig.id)} className={cn("gap-2", compose.signatureId === sig.id && MAIL_COMPOSE_MENU_SELECTED_CLASS)} > {compose.signatureId === sig.id && } {sig.name} ))} ) } export interface ComposeBottomToolbarProps { compose: ComposeState editor: Editor | null isEditingScheduled: boolean showFormatting: boolean sendMenuOpen: boolean setShowFormatting: (v: boolean | ((prev: boolean) => boolean)) => void setSendMenuOpen: (v: boolean) => void handleSend: () => void saveScheduledEdit: () => void | Promise sendScheduledFromEditNow: () => void | Promise applyScheduledPlanAt: (sendAt: Date) => void | Promise submitScheduledSendAt: (sendAt: Date) => void | Promise handleClose: () => void fileInputRef: React.RefObject imageInputRef: React.RefObject } export function ComposeBottomToolbar(props: ComposeBottomToolbarProps) { const { compose, editor, isEditingScheduled, showFormatting, sendMenuOpen, setShowFormatting, setSendMenuOpen, handleSend, saveScheduledEdit, sendScheduledFromEditNow, applyScheduledPlanAt, submitScheduledSendAt, handleClose, fileInputRef, imageInputRef, } = props return (
{/* Send / save + dropdown */}
{isEditingScheduled ? ( <> { void sendScheduledFromEditNow() }} > Envoyer maintenant Planifier { void applyScheduledPlanAt( new Date(Date.now() + 60 * 60 * 1000) ) }} > Envoyer dans une heure { void applyScheduledPlanAt( getNextLocalWallClockDate(9, 0) ) }} > Envoyer à 9h ) : ( <> { void submitScheduledSendAt( new Date(Date.now() + 60 * 60 * 1000) ) }} > Envoyer dans une heure { void submitScheduledSendAt( getNextLocalWallClockDate(9, 0) ) }} > Envoyer à 9h setSendMenuOpen(false)}> Programmer l'envoi )}
{/* Toolbar icons */}
) }