import Icon from '@/Components/Icon/Icon' import { observer } from 'mobx-react-lite' import { useState, useEffect, useMemo, useCallback } from 'react' import { NoteType, Platform, SNNote, pluralize } from '@standardnotes/snjs' import { CHANGE_EDITOR_WIDTH_COMMAND, OPEN_NOTE_HISTORY_COMMAND, PIN_NOTE_COMMAND, SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND, STAR_NOTE_COMMAND, } from '@standardnotes/ui-services' import ChangeEditorOption from './ChangeEditorOption' import ListedActionsOption from './Listed/ListedActionsOption' import AddTagOption from './AddTagOption' import { addToast, dismissToast, ToastType } from '@standardnotes/toast' import { NotesOptionsProps } from './NotesOptionsProps' import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider' import { AppPaneId } from '../Panes/AppPaneMetadata' import { createNoteExport } from '@/Utils/NoteExportUtils' import ProtectedUnauthorizedLabel from '../ProtectedItemOverlay/ProtectedUnauthorizedLabel' import { MenuItemIconSize } from '@/Constants/TailwindClassNames' import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator' import { NoteAttributes } from './NoteAttributes' import { SpellcheckOptions } from './SpellcheckOptions' import { NoteSizeWarning } from './NoteSizeWarning' import { iconClass } from './ClassNames' import SuperNoteOptions from './SuperNoteOptions' import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem' import MenuItem from '../Menu/MenuItem' import ModalOverlay from '../Modal/ModalOverlay' import SuperExportModal from './SuperExportModal' import { useApplication } from '../ApplicationProvider' import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery' import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption' import MenuSection from '../Menu/MenuSection' import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform' import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile' const iconSize = MenuItemIconSize const iconClassDanger = `text-danger mr-2 ${iconSize}` const iconClassWarning = `text-warning mr-2 ${iconSize}` const iconClassSuccess = `text-success mr-2 ${iconSize}` const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => { const application = useApplication() const [altKeyDown, setAltKeyDown] = useState(false) const { toggleAppPane } = useResponsiveAppPane() const toggleOn = (condition: (note: SNNote) => boolean) => { const notesMatchingAttribute = notes.filter(condition) const notesNotMatchingAttribute = notes.filter((note) => !condition(note)) return notesMatchingAttribute.length > notesNotMatchingAttribute.length } const hidePreviews = toggleOn((note) => note.hidePreview) const locked = toggleOn((note) => note.locked) const protect = toggleOn((note) => note.protected) const archived = notes.some((note) => note.archived) const unarchived = notes.some((note) => !note.archived) const trashed = notes.some((note) => note.trashed) const notTrashed = notes.some((note) => !note.trashed) const pinned = notes.some((note) => note.pinned) const unpinned = notes.some((note) => !note.pinned) const starred = notes.some((note) => note.starred) const editorForNote = useMemo( () => (notes[0] ? application.componentManager.editorForNote(notes[0]) : undefined), [application.componentManager, notes], ) useEffect(() => { const removeAltKeyObserver = application.keyboardService.addCommandHandler({ command: SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND, onKeyDown: () => { setAltKeyDown(true) }, onKeyUp: () => { setAltKeyDown(false) }, }) return () => { removeAltKeyObserver() } }, [application]) const [showExportSuperModal, setShowExportSuperModal] = useState(false) const closeSuperExportModal = useCallback(() => { setShowExportSuperModal(false) }, []) const downloadSelectedItems = useCallback(async () => { if (notes.length === 0) { return } const toast = addToast({ type: ToastType.Progress, message: `Exporting ${notes.length} ${pluralize(notes.length, 'note', 'notes')}...`, }) try { const result = await createNoteExport(application, notes) if (!result) { return } const { blob, fileName } = result void downloadOrShareBlobBasedOnPlatform({ archiveService: application.archiveService, platform: application.platform, mobileDevice: application.mobileDevice, blob: blob, filename: fileName, isNativeMobileWeb: application.isNativeMobileWeb(), }) dismissToast(toast) } catch (error) { console.error(error) addToast({ type: ToastType.Error, message: 'Could not export notes', }) dismissToast(toast) } }, [application, notes]) const exportSelectedItems = useCallback(() => { const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super) if (hasSuperNote) { setShowExportSuperModal(true) return } downloadSelectedItems().catch(console.error) }, [downloadSelectedItems, notes]) const shareSelectedItems = useCallback(() => { createNoteExport(application, notes) .then((result) => { if (!result) { return } const { blob, fileName } = result shareBlobOnMobile(application.mobileDevice, application.isNativeMobileWeb(), blob, fileName).catch( console.error, ) }) .catch(console.error) }, [application, notes]) const closeMenuAndToggleNotesList = useCallback(() => { const isMobileScreen = matchMedia(MutuallyExclusiveMediaQueryBreakpoints.sm).matches if (isMobileScreen) { toggleAppPane(AppPaneId.Items) } closeMenu() }, [closeMenu, toggleAppPane]) const duplicateSelectedItems = useCallback(async () => { await Promise.all( notes.map((note) => application.mutator .duplicateItem(note) .then((duplicated) => addToast({ type: ToastType.Regular, message: `Duplicated note "${duplicated.title}"`, actions: [ { label: 'Open', handler: (toastId) => { application.itemListController.selectUuids([duplicated.uuid], true).catch(console.error) dismissToast(toastId) }, }, ], autoClose: true, }), ) .catch(console.error), ), ) void application.sync.sync() closeMenuAndToggleNotesList() }, [application.mutator, application.itemListController, application.sync, closeMenuAndToggleNotesList, notes]) const openRevisionHistoryModal = useCallback(() => { application.historyModalController.openModal(application.notesController.firstSelectedNote) }, [application.historyModalController, application.notesController.firstSelectedNote]) const historyShortcut = useMemo( () => application.keyboardService.keyboardShortcutForCommand(OPEN_NOTE_HISTORY_COMMAND), [application], ) const pinShortcut = useMemo( () => application.keyboardService.keyboardShortcutForCommand(PIN_NOTE_COMMAND), [application], ) const starShortcut = useMemo( () => application.keyboardService.keyboardShortcutForCommand(STAR_NOTE_COMMAND), [application], ) const toggleLineWidthModal = useCallback(() => { application.keyboardService.triggerCommand(CHANGE_EDITOR_WIDTH_COMMAND) }, [application.keyboardService]) const editorWidthShortcut = useMemo( () => application.keyboardService.keyboardShortcutForCommand(CHANGE_EDITOR_WIDTH_COMMAND), [application], ) const unauthorized = notes.some((note) => !application.isAuthorizedToRenderItem(note)) if (unauthorized) { return } const areSomeNotesInSharedVault = notes.some((note) => application.vaults.getItemVault(note)?.isSharedVaultListing()) const areSomeNotesInReadonlySharedVault = notes.some((note) => { const vault = application.vaults.getItemVault(note) return vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault) }) const hasAdminPermissionForAllSharedNotes = notes.every((note) => { const vault = application.vaults.getItemVault(note) if (!vault?.isSharedVaultListing()) { return true } return application.vaultUsers.isCurrentUserSharedVaultAdmin(vault) }) if (notes.length === 0) { return null } return ( <> {notes.length === 1 && ( <> Note history {historyShortcut && } Editor width {editorWidthShortcut && } > )} { application.notesController.setLockSelectedNotes(locked) }} disabled={areSomeNotesInReadonlySharedVault} > Prevent editing { application.notesController.setHideSelectedNotePreviews(!hidePreviews) }} disabled={areSomeNotesInReadonlySharedVault} > Show preview { application.notesController.setProtectSelectedNotes(protect).catch(console.error) }} disabled={areSomeNotesInReadonlySharedVault} > Password protect {notes.length === 1 && ( )} 1 ? 'md:!mb-2' : ''}> {application.featuresController.isVaultsEnabled() && ( )} {application.navigationController.tagsCount > 0 && ( )} { application.notesController.setStarSelectedNotes(!starred) }} disabled={areSomeNotesInReadonlySharedVault} > {starred ? 'Unstar' : 'Star'} {starShortcut && } {unpinned && ( { application.notesController.setPinSelectedNotes(true) }} disabled={areSomeNotesInReadonlySharedVault} > Pin to top {pinShortcut && } )} {pinned && ( { application.notesController.setPinSelectedNotes(false) }} disabled={areSomeNotesInReadonlySharedVault} > Unpin {pinShortcut && } )} Export {application.platform === Platform.Android && ( Share )} Duplicate {unarchived && ( { await application.notesController.setArchiveSelectedNotes(true).catch(console.error) closeMenuAndToggleNotesList() }} disabled={areSomeNotesInReadonlySharedVault} > Archive )} {archived && ( { await application.notesController.setArchiveSelectedNotes(false).catch(console.error) closeMenuAndToggleNotesList() }} disabled={areSomeNotesInReadonlySharedVault} > Unarchive )} {notTrashed && (altKeyDown ? ( { await application.notesController.deleteNotesPermanently() closeMenuAndToggleNotesList() }} > Delete permanently ) : ( { await application.notesController.setTrashSelectedNotes(true) closeMenuAndToggleNotesList() }} disabled={areSomeNotesInReadonlySharedVault} > Move to trash ))} {trashed && ( <> { await application.notesController.setTrashSelectedNotes(false) closeMenuAndToggleNotesList() }} disabled={areSomeNotesInReadonlySharedVault} > Restore { await application.notesController.deleteNotesPermanently() closeMenuAndToggleNotesList() }} > Delete permanently { await application.notesController.emptyTrash() closeMenuAndToggleNotesList() }} disabled={areSomeNotesInReadonlySharedVault} > Empty Trash {application.notesController.trashedNotesCount} notes in Trash > )} {notes.length === 1 && ( <> {notes[0].noteType === NoteType.Super && } {!areSomeNotesInSharedVault && ( )} {editorForNote && ( )} > )} > ) } export default observer(NotesOptions)