import { AppState } from '@/ui_models/app_state';
import { Icon } from './Icon';
import { Switch } from './Switch';
import { observer } from 'mobx-react-lite';
import { useRef, useState, useEffect, useMemo } from 'preact/hooks';
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
} from '@reach/disclosure';
import { SNNote } from '@standardnotes/snjs/dist/@types';
import { WebApplication } from '@/ui_models/application';
import { KeyboardModifier } from '@/services/ioService';
import { FunctionComponent } from 'preact';
type Props = {
application: WebApplication;
appState: AppState;
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
onSubmenuChange?: (submenuOpen: boolean) => void;
};
type DeletePermanentlyButtonProps = {
closeOnBlur: Props['closeOnBlur'];
onClick: () => void;
};
const DeletePermanentlyButton = ({
closeOnBlur,
onClick,
}: DeletePermanentlyButtonProps) => (
);
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 = text.match(/[\w’'-]+\b/g)?.length;
const paragraphs = text.replace(/\n$/gm, '').split(/\n/).length;
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<{ note: SNNote }> = ({ 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.serverUpdatedAt),
[note.serverUpdatedAt]
);
const dateCreated = useMemo(
() => formatDate(note.created_at),
[note.created_at]
);
return (
{typeof words === 'number' ? (
<>
{words} words · {characters} characters · {paragraphs} paragraphs
Read time: {readTime}
>
) : null}
Last modified: {dateLastModified}
Created: {dateCreated}
Note ID: {note.uuid}
);
};
export const NotesOptions = observer(
({ application, appState, closeOnBlur, onSubmenuChange }: Props) => {
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
const [tagsMenuPosition, setTagsMenuPosition] = useState<{
top: number;
right?: number;
left?: number;
}>({
top: 0,
right: 0,
});
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState(
'auto'
);
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 tagsButtonRef = useRef();
const iconClass = 'color-neutral mr-2';
useEffect(() => {
if (onSubmenuChange) {
onSubmenuChange(tagsMenuOpen);
}
}, [tagsMenuOpen, onSubmenuChange]);
useEffect(() => {
const removeAltKeyObserver = application.io.addKeyObserver({
modifiers: [KeyboardModifier.Alt],
onKeyDown: () => {
setAltKeyDown(true);
},
onKeyUp: () => {
setAltKeyDown(false);
},
});
return () => {
removeAltKeyObserver();
};
}, [application]);
const openTagsMenu = () => {
const defaultFontSize = window.getComputedStyle(
document.documentElement
).fontSize;
const maxTagsMenuSize = parseFloat(defaultFontSize) * 30;
const { clientWidth, clientHeight } = document.documentElement;
const buttonRect = tagsButtonRef.current.getBoundingClientRect();
const footerHeight = 32;
if (buttonRect.top + maxTagsMenuSize > clientHeight - footerHeight) {
setTagsMenuMaxHeight(clientHeight - buttonRect.top - footerHeight - 2);
}
if (buttonRect.right + maxTagsMenuSize > clientWidth) {
setTagsMenuPosition({
top: buttonRect.top,
right: clientWidth - buttonRect.left,
});
} else {
setTagsMenuPosition({
top: buttonRect.top,
left: buttonRect.right,
});
}
setTagsMenuOpen(!tagsMenuOpen);
};
return (
<>
{
appState.notes.setLockSelectedNotes(!locked);
}}
>
Prevent editing
{
appState.notes.setHideSelectedNotePreviews(!hidePreviews);
}}
>
Show preview
{
appState.notes.setProtectSelectedNotes(!protect);
}}
>
Protect
{appState.tags.tagsCount > 0 && (
{
if (event.key === 'Escape') {
setTagsMenuOpen(false);
}
}}
onBlur={closeOnBlur}
ref={tagsButtonRef}
className="sn-dropdown-item justify-between"
>
{'Add tag'}
{
if (event.key === 'Escape') {
setTagsMenuOpen(false);
tagsButtonRef.current.focus();
}
}}
style={{
...tagsMenuPosition,
maxHeight: tagsMenuMaxHeight,
position: 'fixed',
}}
className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto"
>
{appState.tags.tags.map((tag) => (
))}
)}
{unpinned && (
)}
{pinned && (
)}
{unarchived && (
)}
{archived && (
)}
{notTrashed &&
(altKeyDown ? (
{
await appState.notes.deleteNotesPermanently();
}}
/>
) : (
))}
{trashed && (
<>
{
await appState.notes.deleteNotesPermanently();
}}
/>
>
)}
{notes.length === 1 ? (
<>
>
) : null}
>
);
}
);