import { uniqBy } from "lodash-es"; import { FileIcon, ImageIcon, LinkIcon, LoaderIcon, type LucideIcon, MapPinIcon, Maximize2Icon, MicIcon, MoreHorizontalIcon, PlusIcon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { LinkMemoDialog, LocationDialog } from "@/components/MemoMetadata"; import type { MapPoint } from "@/components/map/types"; import { useReverseGeocoding } from "@/components/map/useReverseGeocoding"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, useDropdownMenuSubHoverDelay, } from "@/components/ui/dropdown-menu"; import { useDebouncedEffect } from "@/hooks"; import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb"; import { useTranslate } from "@/utils/i18n"; import { setPreferredEditorMode } from "../editorMode"; import { useFileUpload, useLinkMemo, useLocation } from "../hooks"; import { useEditorContext, useEditorSelector } from "../state"; import type { InsertMenuProps } from "../types"; import type { LocalFile } from "../types/attachment"; const InsertMenu = (props: InsertMenuProps) => { const t = useTranslate(); const { actions, dispatch } = useEditorContext(); const relations = useEditorSelector((s) => s.metadata.relations); const editorMode = useEditorSelector((s) => s.ui.editorMode); const { location: initialLocation, onLocationChange, onToggleFocusMode, isUploading: isUploadingProp } = props; const [linkDialogOpen, setLinkDialogOpen] = useState(false); const [locationDialogOpen, setLocationDialogOpen] = useState(false); const [moreSubmenuOpen, setMoreSubmenuOpen] = useState(false); const { handleTriggerEnter, handleTriggerLeave, handleContentEnter, handleContentLeave } = useDropdownMenuSubHoverDelay( 150, setMoreSubmenuOpen, ); const { fileInputRef, selectingFlag, handleFileInputChange, handleUploadClick } = useFileUpload((newFiles: LocalFile[]) => { newFiles.forEach((file) => dispatch(actions.addLocalFile(file))); }); const linkMemo = useLinkMemo({ isOpen: linkDialogOpen, currentMemoName: props.memoName, existingRelations: relations, onAddRelation: (relation: MemoRelation) => { dispatch(actions.setMetadata({ relations: uniqBy([...relations, relation], (r) => r.relatedMemo?.name) })); setLinkDialogOpen(false); }, }); const location = useLocation(props.location); const { state: locationState, locationInitialized, handlePositionChange: handleLocationPositionChange, getLocation, reset: locationReset, updateCoordinate, setPlaceholder, } = location; const [debouncedPosition, setDebouncedPosition] = useState(undefined); useDebouncedEffect( () => { setDebouncedPosition(locationState.position); }, 1000, [locationState.position], ); const { data: displayName } = useReverseGeocoding(debouncedPosition?.lat, debouncedPosition?.lng); useEffect(() => { if (displayName) { setPlaceholder(displayName); } }, [displayName, setPlaceholder]); const isUploading = selectingFlag || isUploadingProp; const handleOpenLinkDialog = useCallback(() => { setLinkDialogOpen(true); }, []); const handleLocationClick = useCallback(() => { setLocationDialogOpen(true); if (!initialLocation && !locationInitialized) { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { handleLocationPositionChange({ lat: position.coords.latitude, lng: position.coords.longitude }); }, (error) => { console.error("Geolocation error:", error); }, ); } } }, [initialLocation, locationInitialized, handleLocationPositionChange]); const handleLocationConfirm = useCallback(() => { const newLocation = getLocation(); if (newLocation) { onLocationChange(newLocation); setLocationDialogOpen(false); } }, [getLocation, onLocationChange]); const handleLocationCancel = useCallback(() => { locationReset(); setLocationDialogOpen(false); }, [locationReset]); const handleToggleFocusMode = useCallback(() => { onToggleFocusMode?.(); setMoreSubmenuOpen(false); }, [onToggleFocusMode]); const handleEditorModeChange = useCallback( (checked: boolean) => { const next = checked ? "wysiwyg" : "raw"; dispatch(actions.setEditorMode(next)); setPreferredEditorMode(next); }, [actions, dispatch], ); const handleMediaUploadClick = useCallback(() => { handleUploadClick("image/*,video/*"); }, [handleUploadClick]); const handleFileUploadClick = useCallback(() => { handleUploadClick(); }, [handleUploadClick]); const menuItems = useMemo( () => [ { key: "upload-media", label: t("attachment-library.tabs.media"), icon: ImageIcon, onClick: handleMediaUploadClick, }, { key: "record-audio", label: t("editor.audio-recorder.trigger"), icon: MicIcon, onClick: () => props.onAudioRecorderClick?.(), }, { key: "upload-file", label: t("common.file"), icon: FileIcon, onClick: handleFileUploadClick, }, { key: "link", label: t("editor.insert-menu.link-memo"), icon: LinkIcon, onClick: handleOpenLinkDialog, }, { key: "location", label: t("editor.insert-menu.add-location"), icon: MapPinIcon, onClick: handleLocationClick, }, ] satisfies Array<{ key: string; label: string; icon: LucideIcon; onClick: () => void }>, [handleFileUploadClick, handleLocationClick, handleMediaUploadClick, handleOpenLinkDialog, props, t], ); return ( <> {menuItems.slice(0, 3).map((item) => ( {item.label} ))} {menuItems.slice(3).map((item) => ( {item.label} ))} {/* View submenu with Focus Mode + editor display mode */} {t("common.more")} {t("editor.focus-mode")} {/* Editor display mode: checked = rich text (WYSIWYG), unchecked = raw Markdown */} {t("editor.wysiwyg-editor")} {/* Hidden file input */} ); }; export default InsertMenu;