diff --git a/app/assets/icons/ic-archive.svg b/app/assets/icons/ic-archive.svg
new file mode 100644
index 000000000..baebb7932
--- /dev/null
+++ b/app/assets/icons/ic-archive.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/assets/icons/ic_close.svg b/app/assets/icons/ic-close.svg
similarity index 100%
rename from app/assets/icons/ic_close.svg
rename to app/assets/icons/ic-close.svg
diff --git a/app/assets/icons/ic-more.svg b/app/assets/icons/ic-more.svg
new file mode 100644
index 000000000..7512c412f
--- /dev/null
+++ b/app/assets/icons/ic-more.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ic-pencil-off.svg b/app/assets/icons/ic-pencil-off.svg
new file mode 100644
index 000000000..2ebdc93de
--- /dev/null
+++ b/app/assets/icons/ic-pencil-off.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ic-pin-off.svg b/app/assets/icons/ic-pin-off.svg
new file mode 100644
index 000000000..aff807dae
--- /dev/null
+++ b/app/assets/icons/ic-pin-off.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ic-pin.svg b/app/assets/icons/ic-pin.svg
new file mode 100644
index 000000000..c19d600d8
--- /dev/null
+++ b/app/assets/icons/ic-pin.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ic-text-rich.svg b/app/assets/icons/ic-text-rich.svg
new file mode 100644
index 000000000..4fcb62677
--- /dev/null
+++ b/app/assets/icons/ic-text-rich.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ic-trash.svg b/app/assets/icons/ic-trash.svg
new file mode 100644
index 000000000..4bbb76144
--- /dev/null
+++ b/app/assets/icons/ic-trash.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ic_tune.svg b/app/assets/icons/ic-tune.svg
similarity index 100%
rename from app/assets/icons/ic_tune.svg
rename to app/assets/icons/ic-tune.svg
diff --git a/app/assets/icons/ic-unarchive.svg b/app/assets/icons/ic-unarchive.svg
new file mode 100644
index 000000000..0506ec471
--- /dev/null
+++ b/app/assets/icons/ic-unarchive.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/il-notes.svg b/app/assets/icons/il-notes.svg
new file mode 100644
index 000000000..2d9cfc0bc
--- /dev/null
+++ b/app/assets/icons/il-notes.svg
@@ -0,0 +1,35 @@
+
diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts
index a2bcbd446..1db921aef 100644
--- a/app/assets/javascripts/app.ts
+++ b/app/assets/javascripts/app.ts
@@ -59,6 +59,7 @@ import { SessionsModalDirective } from './components/SessionsModal';
import { NoAccountWarningDirective } from './components/NoAccountWarning';
import { NoProtectionsdNoteWarningDirective } from './components/NoProtectionsNoteWarning';
import { SearchOptionsDirective } from './components/SearchOptions';
+import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNotes';
function reloadHiddenFirefoxTab(): boolean {
/**
@@ -147,7 +148,8 @@ const startApplication: StartApplication = async function startApplication(
.directive('sessionsModal', SessionsModalDirective)
.directive('noAccountWarning', NoAccountWarningDirective)
.directive('protectedNotePanel', NoProtectionsdNoteWarningDirective)
- .directive('searchOptions', SearchOptionsDirective);
+ .directive('searchOptions', SearchOptionsDirective)
+ .directive('multipleSelectedNotesPanel', MultipleSelectedNotesDirective);
// Filters
angular.module('app').filter('trusted', ['$sce', trusted]);
diff --git a/app/assets/javascripts/components/MultipleSelectedNotes.tsx b/app/assets/javascripts/components/MultipleSelectedNotes.tsx
new file mode 100644
index 000000000..d28da4a1d
--- /dev/null
+++ b/app/assets/javascripts/components/MultipleSelectedNotes.tsx
@@ -0,0 +1,192 @@
+import { AppState } from '@/ui_models/app_state';
+import VisuallyHidden from '@reach/visually-hidden';
+import { toDirective, useCloseOnBlur } from './utils';
+import {
+ Disclosure,
+ DisclosureButton,
+ DisclosurePanel,
+} from '@reach/disclosure';
+import MoreIcon from '../../icons/ic-more.svg';
+import PencilOffIcon from '../../icons/ic-pencil-off.svg';
+import RichTextIcon from '../../icons/ic-text-rich.svg';
+import TrashIcon from '../../icons/ic-trash.svg';
+import PinIcon from '../../icons/ic-pin.svg';
+import UnpinIcon from '../../icons/ic-pin-off.svg';
+import ArchiveIcon from '../../icons/ic-archive.svg';
+import UnarchiveIcon from '../../icons/ic-unarchive.svg';
+import NotesIcon from '../../icons/il-notes.svg';
+import { useRef, useState } from 'preact/hooks';
+import { Switch } from './Switch';
+import { observer } from 'mobx-react-lite';
+import { SNApplication } from '@standardnotes/snjs';
+
+type Props = {
+ application: SNApplication;
+ appState: AppState;
+};
+
+const MultipleSelectedNotes = observer(({ appState }: Props) => {
+ const count = appState.notes.selectedNotesCount;
+ const [open, setOpen] = useState(false);
+ const [optionsPanelPosition, setOptionsPanelPosition] = useState({
+ top: 0,
+ right: 0,
+ });
+ const buttonRef = useRef();
+ const panelRef = useRef();
+ const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(panelRef, setOpen);
+
+ const notes = Object.values(appState.notes.selectedNotes);
+ const hidePreviews = !notes.some((note) => !note.hidePreview);
+ const locked = !notes.some((note) => !note.locked);
+ const archived = !notes.some((note) => !note.archived);
+ const trashed = !notes.some((note) => !note.trashed);
+ const pinned = !notes.some((note) => !note.pinned);
+
+ const iconClass = 'fill-current color-neutral mr-2.5';
+ const buttonClass =
+ 'flex items-center border-0 capitalize focus:inner-ring-info ' +
+ 'cursor-pointer hover:bg-contrast color-text bg-transparent h-10 px-3 ' +
+ 'text-left';
+
+ return (
+
+
+
{count} selected notes
+
{
+ const rect = buttonRef.current.getBoundingClientRect();
+ setOptionsPanelPosition({
+ top: rect.bottom,
+ right: document.body.clientWidth - rect.right,
+ });
+ setOpen((prevOpen) => !prevOpen);
+ }}
+ >
+ {
+ if (event.key === 'Escape') {
+ setOpen(false);
+ }
+ }}
+ onBlur={closeOnBlur}
+ ref={buttonRef}
+ className={
+ 'bg-transparent border-solid border-1 border-gray-300 ' +
+ 'cursor-pointer w-32px h-32px rounded-full p-0 ' +
+ 'flex justify-center items-center'
+ }
+ >
+ Actions
+
+
+ {
+ if (event.key === 'Escape') {
+ setOpen(false);
+ buttonRef.current.focus();
+ }
+ }}
+ ref={panelRef}
+ style={{
+ ...optionsPanelPosition,
+ }}
+ className="sn-dropdown sn-dropdown-anchor-right flex flex-col py-2 select-none"
+ >
+ {
+ appState.notes.setLockSelectedNotes(!locked);
+ }}
+ >
+
+
+ Prevent editing
+
+
+ {
+ appState.notes.setHideSelectedNotePreviews(!hidePreviews);
+ }}
+ >
+
+
+ Show Preview
+
+
+
+
+
+
+
+
+
+
+
+
+ {count} selected notes
+
+
+ Actions will be performed on all selected notes.
+
+
+
+ );
+});
+
+export const MultipleSelectedNotesDirective = toDirective(
+ MultipleSelectedNotes
+);
diff --git a/app/assets/javascripts/components/NoAccountWarning.tsx b/app/assets/javascripts/components/NoAccountWarning.tsx
index 680393604..0b24927e7 100644
--- a/app/assets/javascripts/components/NoAccountWarning.tsx
+++ b/app/assets/javascripts/components/NoAccountWarning.tsx
@@ -1,13 +1,12 @@
-import { toDirective, useAutorunValue } from './utils';
-import Close from '../../icons/ic_close.svg';
+import { toDirective } from './utils';
+import Close from '../../icons/ic-close.svg';
import { AppState } from '@/ui_models/app_state';
+import { observer } from 'mobx-react-lite';
type Props = { appState: AppState };
-function NoAccountWarning({ appState }: Props) {
- const canShow = useAutorunValue(() => appState.noAccountWarning.show, [
- appState,
- ]);
+const NoAccountWarning = observer(({ appState }: Props) => {
+ const canShow = appState.noAccountWarning.show;
if (!canShow) {
return null;
}
@@ -39,6 +38,6 @@ function NoAccountWarning({ appState }: Props) {
);
-}
+});
export const NoAccountWarningDirective = toDirective(NoAccountWarning);
diff --git a/app/assets/javascripts/components/SearchOptions.tsx b/app/assets/javascripts/components/SearchOptions.tsx
index 2c24e6c44..91d7f6409 100644
--- a/app/assets/javascripts/components/SearchOptions.tsx
+++ b/app/assets/javascripts/components/SearchOptions.tsx
@@ -1,5 +1,5 @@
import { AppState } from '@/ui_models/app_state';
-import { toDirective, useAutorunValue } from './utils';
+import { toDirective, useCloseOnBlur } from './utils';
import { useRef, useState } from 'preact/hooks';
import { WebApplication } from '@/ui_models/application';
import VisuallyHidden from '@reach/visually-hidden';
@@ -10,54 +10,35 @@ import {
} from '@reach/disclosure';
import { FocusEvent } from 'react';
import { Switch } from './Switch';
-import TuneIcon from '../../icons/ic_tune.svg';
+import TuneIcon from '../../icons/ic-tune.svg';
+import { observer } from 'mobx-react-lite';
type Props = {
appState: AppState;
application: WebApplication;
};
-function SearchOptions({ appState }: Props) {
+const SearchOptions = observer(({ appState }: Props) => {
const { searchOptions } = appState;
const {
includeProtectedContents,
includeArchived,
includeTrashed,
- } = useAutorunValue(
- () => ({
- includeProtectedContents: searchOptions.includeProtectedContents,
- includeArchived: searchOptions.includeArchived,
- includeTrashed: searchOptions.includeTrashed,
- }),
- [searchOptions]
- );
-
- const [
- togglingIncludeProtectedContents,
- setTogglingIncludeProtectedContents,
- ] = useState(false);
-
- async function toggleIncludeProtectedContents() {
- setTogglingIncludeProtectedContents(true);
- try {
- await searchOptions.toggleIncludeProtectedContents();
- } finally {
- setTogglingIncludeProtectedContents(false);
- }
- }
+ } = searchOptions;
const [open, setOpen] = useState(false);
const [optionsPanelTop, setOptionsPanelTop] = useState(0);
const buttonRef = useRef();
const panelRef = useRef();
+ const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(panelRef, setOpen);
- function closeOnBlur(event: FocusEvent) {
- if (
- !togglingIncludeProtectedContents &&
- !panelRef.current.contains(event.relatedTarget as Node)
- ) {
- setOpen(false);
+ async function toggleIncludeProtectedContents() {
+ setLockCloseOnBlur(true);
+ try {
+ await searchOptions.toggleIncludeProtectedContents();
+ } finally {
+ setLockCloseOnBlur(false);
}
}
@@ -86,6 +67,7 @@ function SearchOptions({ appState }: Props) {
className="sn-dropdown sn-dropdown-anchor-right grid gap-2 py-2"
>
Include protected contents
Include archived notes
);
-}
+});
export const SearchOptionsDirective = toDirective(SearchOptions);
diff --git a/app/assets/javascripts/components/SessionsModal.tsx b/app/assets/javascripts/components/SessionsModal.tsx
index b8c82daf4..facaa3dc4 100644
--- a/app/assets/javascripts/components/SessionsModal.tsx
+++ b/app/assets/javascripts/components/SessionsModal.tsx
@@ -15,8 +15,9 @@ import {
AlertDialogDescription,
AlertDialogLabel,
} from '@reach/alert-dialog';
-import { toDirective, useAutorunValue } from './utils';
+import { toDirective } from './utils';
import { WebApplication } from '@/ui_models/application';
+import { observer } from 'mobx-react-lite';
type Session = RemoteSession & {
revoking?: true;
@@ -242,16 +243,12 @@ const SessionsModal: FunctionComponent<{
const Sessions: FunctionComponent<{
appState: AppState;
application: WebApplication;
-}> = ({ appState, application }) => {
- const showModal = useAutorunValue(() => appState.isSessionsModalVisible, [
- appState,
- ]);
-
- if (showModal) {
+}> = observer(({ appState, application }) => {
+ if (appState.isSessionsModalVisible) {
return ;
} else {
return null;
}
-};
+});
export const SessionsModalDirective = toDirective(Sessions);
diff --git a/app/assets/javascripts/components/Switch.tsx b/app/assets/javascripts/components/Switch.tsx
index 44dbf7dc5..1f4185021 100644
--- a/app/assets/javascripts/components/Switch.tsx
+++ b/app/assets/javascripts/components/Switch.tsx
@@ -11,6 +11,7 @@ import '@reach/checkbox/styles.css';
export type SwitchProps = HTMLProps & {
checked?: boolean;
onChange: (checked: boolean) => void;
+ className?: string;
children: ComponentChildren;
};
@@ -19,8 +20,9 @@ export const Switch: FunctionalComponent = (
) => {
const [checkedState, setChecked] = useState(props.checked || false);
const checked = props.checked ?? checkedState;
+ const className = props.className ?? '';
return (
-