import { AppState } from '@/ui_models/app_state';
import { Icon } from '../Icon';
import { Switch } from '../Switch';
import { observer } from 'mobx-react-lite';
import { useState, useEffect, useMemo } from 'preact/hooks';
import { SNApplication, SNNote } from '@standardnotes/snjs';
import { WebApplication } from '@/ui_models/application';
import { KeyboardModifier } from '@/services/ioService';
import { FunctionComponent } from 'preact';
import { ChangeEditorOption } from './ChangeEditorOption';
import { BYTES_IN_ONE_MEGABYTE } from '@/constants';
import { ListedActionsOption } from './ListedActionsOption';
import { AddTagOption } from './AddTagOption';
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit';
export type NotesOptionsProps = {
application: WebApplication;
appState: AppState;
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
};
type DeletePermanentlyButtonProps = {
closeOnBlur: NotesOptionsProps['closeOnBlur'];
onClick: () => void;
};
const DeletePermanentlyButton = ({
closeOnBlur,
onClick,
}: DeletePermanentlyButtonProps) => (
);
const iconClass = 'color-neutral mr-2';
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 formatDate = (date: Date | undefined) => {
if (!date) return;
return `${date.toDateString()} ${date.toLocaleTimeString()}`;
};
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(
() => formatDate(note.userModifiedDate),
[note.userModifiedDate]
);
const dateCreated = useMemo(
() => formatDate(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<{
appState: AppState;
note: SNNote;
}> = ({ appState, note }) => {
const editor = appState.application.componentManager.editorForNote(note);
const spellcheckControllable = Boolean(
!editor || editor.package_info.spellcheckControl
);
const noteSpellcheck = !spellcheckControllable
? true
: note
? appState.notes.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 }) =>
new Blob([note.text]).size > NOTE_SIZE_WARNING_THRESHOLD ? (
This note may have trouble syncing to the mobile application due to its
size.
) : null;
export const NotesOptions = observer(
({ application, appState, closeOnBlur }: NotesOptionsProps) => {
const [altKeyDown, setAltKeyDown] = useState(false);
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 = Object.values(appState.notes.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 errored = notes.some((note) => note.errorDecrypting);
useEffect(() => {
const removeAltKeyObserver = application.io.addKeyObserver({
modifiers: [KeyboardModifier.Alt],
onKeyDown: () => {
setAltKeyDown(true);
},
onKeyUp: () => {
setAltKeyDown(false);
},
});
return () => {
removeAltKeyObserver();
};
}, [application]);
const getNoteFileName = (note: SNNote): string => {
const editor = application.componentManager.editorForNote(note);
const format = editor?.package_info?.file_type || 'txt';
return `${note.title}.${format}`;
};
const downloadSelectedItems = async () => {
if (notes.length === 1) {
application
.getArchiveService()
.downloadData(new Blob([notes[0].text]), getNoteFileName(notes[0]));
return;
}
if (notes.length > 1) {
const loadingToastId = addToast({
type: ToastType.Loading,
message: `Exporting ${notes.length} notes...`,
});
await application.getArchiveService().downloadDataAsZip(
notes.map((note) => {
return {
filename: getNoteFileName(note),
content: new Blob([note.text]),
};
})
);
dismissToast(loadingToastId);
addToast({
type: ToastType.Success,
message: `Exported ${notes.length} notes`,
});
}
};
const duplicateSelectedItems = () => {
notes.forEach((note) => {
application.duplicateItem(note);
});
};
if (errored) {
return (
<>
{notes.length === 1 ? (
) : null}
{
await appState.notes.deleteNotesPermanently();
}}
/>
>
);
}
const openRevisionHistoryModal = () => {
appState.notes.setShowRevisionHistoryModal(true);
};
return (
<>
{notes.length === 1 && (
<>
>
)}
{notes.length === 1 && (
<>
>
)}
{appState.tags.tagsCount > 0 && }
{unpinned && (
)}
{pinned && (
)}
{unarchived && (
)}
{archived && (
)}
{notTrashed &&
(altKeyDown ? (
{
await appState.notes.deleteNotesPermanently();
}}
/>
) : (
))}
{trashed && (
<>
{
await appState.notes.deleteNotesPermanently();
}}
/>
>
)}
{notes.length === 1 ? (
<>
>
) : null}
>
);
}
);