import Icon from '@/Components/Icon/Icon' import Switch from '@/Components/Switch/Switch' import { observer } from 'mobx-react-lite' import { useState, useEffect, useMemo, useCallback, FunctionComponent } from 'react' import { Platform, SNApplication, SNComponent, SNNote } from '@standardnotes/snjs' import { KeyboardModifier } from '@standardnotes/ui-services' import ChangeEditorOption from './ChangeEditorOption' import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants' import ListedActionsOption from './ListedActionsOption' import AddTagOption from './AddTagOption' import { addToast, dismissToast, ToastType } from '@standardnotes/toast' import { NotesOptionsProps } from './NotesOptionsProps' import { NotesController } from '@/Controllers/NotesController' import HorizontalSeparator from '../Shared/HorizontalSeparator' import { formatDateForContextMenu } from '@/Utils/DateUtils' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils' import { shareSelectedNotes } from '@/NativeMobileWeb/ShareSelectedNotes' import { downloadSelectedNotesOnAndroid } from '@/NativeMobileWeb/DownloadSelectedNotesOnAndroid' import ProtectedUnauthorizedLabel from '../ProtectedItemOverlay/ProtectedUnauthorizedLabel' import { classNames } from '@/Utils/ConcatenateClassNames' import { MenuItemIconSize } from '@/Constants/TailwindClassNames' type DeletePermanentlyButtonProps = { onClick: () => void } const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => ( ) const iconSize = MenuItemIconSize const iconClass = `text-neutral mr-2 ${iconSize}` const iconClassDanger = `text-danger mr-2 ${iconSize}` const iconClassWarning = `text-warning mr-2 ${iconSize}` const iconClassSuccess = `text-success mr-2 ${iconSize}` const getWordCount = (text: string) => { if (text.trim().length === 0) { return 0 } return text.split(/\s+/).length } const getParagraphCount = (text: string) => { if (text.trim().length === 0) { return 0 } return text.replace(/\n$/gm, '').split(/\n/).length } const countNoteAttributes = (text: string) => { try { JSON.parse(text) return { characters: 'N/A', words: 'N/A', paragraphs: 'N/A', } } catch { const characters = text.length const words = getWordCount(text) const paragraphs = getParagraphCount(text) return { characters, words, paragraphs, } } } const calculateReadTime = (words: number) => { const timeToRead = Math.round(words / 200) if (timeToRead === 0) { return '< 1 minute' } else { return `${timeToRead} ${timeToRead > 1 ? 'minutes' : 'minute'}` } } const NoteAttributes: FunctionComponent<{ application: SNApplication note: SNNote }> = ({ application, note }) => { const { words, characters, paragraphs } = useMemo(() => countNoteAttributes(note.text), [note.text]) const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words]) const dateLastModified = useMemo(() => formatDateForContextMenu(note.userModifiedDate), [note.userModifiedDate]) const dateCreated = useMemo(() => formatDateForContextMenu(note.created_at), [note.created_at]) const editor = application.componentManager.editorForNote(note) const format = editor?.package_info?.file_type || 'txt' return (
{typeof words === 'number' && (format === 'txt' || format === 'md') ? ( <>
{words} words · {characters} characters · {paragraphs} paragraphs
Read time: {readTime}
) : null}
Last modified: {dateLastModified}
Created: {dateCreated}
Note ID: {note.uuid}
) } const SpellcheckOptions: FunctionComponent<{ editorForNote: SNComponent | undefined notesController: NotesController note: SNNote className: string }> = ({ editorForNote, notesController, note, className }) => { const spellcheckControllable = Boolean(!editorForNote || editorForNote.package_info.spellcheckControl) const noteSpellcheck = !spellcheckControllable ? true : note ? notesController.getSpellcheckStateForNote(note) : undefined return (
{!spellcheckControllable && (

Spellcheck cannot be controlled for this editor.

)}
) } const NOTE_SIZE_WARNING_THRESHOLD = 0.5 * BYTES_IN_ONE_MEGABYTE const NoteSizeWarning: FunctionComponent<{ note: SNNote }> = ({ note }) => { return new Blob([note.text]).size > NOTE_SIZE_WARNING_THRESHOLD ? ( <>
This note may have trouble syncing to the mobile application due to its size.
) : null } const NotesOptions = ({ application, navigationController, notesController, linkingController, historyModalController, closeMenu, }: NotesOptionsProps) => { 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 notes = notesController.selectedNotes 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.io.addKeyObserver({ modifiers: [KeyboardModifier.Alt], onKeyDown: () => { setAltKeyDown(true) }, onKeyUp: () => { setAltKeyDown(false) }, }) return () => { removeAltKeyObserver() } }, [application]) const downloadSelectedItems = useCallback(async () => { if (notes.length === 1) { const note = notes[0] const blob = getNoteBlob(application, note) application.getArchiveService().downloadData(blob, getNoteFileName(application, note)) return } if (notes.length > 1) { const loadingToastId = addToast({ type: ToastType.Loading, message: `Exporting ${notes.length} notes...`, }) await application.getArchiveService().downloadDataAsZip( notes.map((note) => { return { name: getNoteFileName(application, note), content: getNoteBlob(application, note), } }), ) dismissToast(loadingToastId) addToast({ type: ToastType.Success, message: `Exported ${notes.length} notes`, }) } }, [application, notes]) const closeMenuAndToggleNotesList = useCallback(() => { toggleAppPane(AppPaneId.Items) closeMenu() }, [closeMenu, toggleAppPane]) const duplicateSelectedItems = useCallback(async () => { await Promise.all(notes.map((note) => application.mutator.duplicateItem(note).catch(console.error))) closeMenuAndToggleNotesList() }, [application.mutator, closeMenuAndToggleNotesList, notes]) const openRevisionHistoryModal = useCallback(() => { historyModalController.openModal(notesController.firstSelectedNote) }, [historyModalController, notesController.firstSelectedNote]) const unauthorized = notes.some((note) => !application.isAuthorizedToRenderItem(note)) if (unauthorized) { return } const textClassNames = 'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item' const defaultClassNames = classNames( textClassNames, 'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none', ) const switchClassNames = classNames(textClassNames, defaultClassNames, 'justify-between') const firstItemClass = 'pt-4' return ( <> {notes.length === 1 && ( <> )} {notes.length === 1 && ( <> )} {navigationController.tagsCount > 0 && ( )} {unpinned && ( )} {pinned && ( )} {application.platform === Platform.Android && ( )} {unarchived && ( )} {archived && ( )} {notTrashed && (altKeyDown ? ( { await notesController.deleteNotesPermanently() closeMenuAndToggleNotesList() }} /> ) : ( ))} {trashed && ( <> { await notesController.deleteNotesPermanently() closeMenuAndToggleNotesList() }} /> )} {notes.length === 1 ? ( <> ) : null} ) } export default observer(NotesOptions)