feat: Add note attributes to notes options menu (#681)
This commit is contained in:
@@ -2,7 +2,7 @@ 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 } from 'preact/hooks';
|
||||
import { useRef, useState, useEffect, useMemo } from 'preact/hooks';
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
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;
|
||||
@@ -20,9 +21,9 @@ type Props = {
|
||||
};
|
||||
|
||||
type DeletePermanentlyButtonProps = {
|
||||
closeOnBlur: Props["closeOnBlur"];
|
||||
closeOnBlur: Props['closeOnBlur'];
|
||||
onClick: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
const DeletePermanentlyButton = ({
|
||||
closeOnBlur,
|
||||
@@ -34,6 +35,87 @@ const DeletePermanentlyButton = ({
|
||||
</button>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="px-3 pt-1.5 pb-1 text-xs color-neutral font-medium">
|
||||
{typeof words === 'number' ? (
|
||||
<>
|
||||
<div className="mb-1">
|
||||
{words} words · {characters} characters · {paragraphs} paragraphs
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Read time:</span> {readTime}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Last modified:</span> {dateLastModified}
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Created:</span> {dateCreated}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Note ID:</span> {note.uuid}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NotesOptions = observer(
|
||||
({ application, appState, closeOnBlur, onSubmenuChange }: Props) => {
|
||||
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
||||
@@ -45,8 +127,9 @@ export const NotesOptions = observer(
|
||||
top: 0,
|
||||
right: 0,
|
||||
});
|
||||
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] =
|
||||
useState<number | 'auto'>('auto');
|
||||
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState<number | 'auto'>(
|
||||
'auto'
|
||||
);
|
||||
const [altKeyDown, setAltKeyDown] = useState(false);
|
||||
|
||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
||||
@@ -86,7 +169,7 @@ export const NotesOptions = observer(
|
||||
},
|
||||
onKeyUp: () => {
|
||||
setAltKeyDown(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -163,7 +246,7 @@ export const NotesOptions = observer(
|
||||
Protect
|
||||
</span>
|
||||
</Switch>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<div className="min-h-1px my-2 bg-border"></div>
|
||||
{appState.tags.tagsCount > 0 && (
|
||||
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
|
||||
<DisclosureButton
|
||||
@@ -327,6 +410,12 @@ export const NotesOptions = observer(
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{notes.length === 1 ? (
|
||||
<>
|
||||
<div className="min-h-1px my-2 bg-border"></div>
|
||||
<NoteAttributes note={notes[0]} />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user