diff --git a/.yarn/cache/@ariakit-core-npm-0.3.8-0e10e529aa-e416596efe.zip b/.yarn/cache/@ariakit-core-npm-0.3.8-0e10e529aa-e416596efe.zip deleted file mode 100644 index ae9432c4e..000000000 Binary files a/.yarn/cache/@ariakit-core-npm-0.3.8-0e10e529aa-e416596efe.zip and /dev/null differ diff --git a/.yarn/cache/@ariakit-core-npm-0.4.15-411f831ec8-add800c855.zip b/.yarn/cache/@ariakit-core-npm-0.4.15-411f831ec8-add800c855.zip new file mode 100644 index 000000000..ba2600316 Binary files /dev/null and b/.yarn/cache/@ariakit-core-npm-0.4.15-411f831ec8-add800c855.zip differ diff --git a/.yarn/cache/@ariakit-react-core-npm-0.3.9-cb7b09428e-ef44543aec.zip b/.yarn/cache/@ariakit-react-core-npm-0.3.9-cb7b09428e-ef44543aec.zip deleted file mode 100644 index b7c503ecf..000000000 Binary files a/.yarn/cache/@ariakit-react-core-npm-0.3.9-cb7b09428e-ef44543aec.zip and /dev/null differ diff --git a/.yarn/cache/@ariakit-react-core-npm-0.4.18-d72e68fa75-53a012f087.zip b/.yarn/cache/@ariakit-react-core-npm-0.4.18-d72e68fa75-53a012f087.zip new file mode 100644 index 000000000..ec75a32fe Binary files /dev/null and b/.yarn/cache/@ariakit-react-core-npm-0.4.18-d72e68fa75-53a012f087.zip differ diff --git a/.yarn/cache/@ariakit-react-npm-0.3.9-b5ae09b712-901424a4e4.zip b/.yarn/cache/@ariakit-react-npm-0.3.9-b5ae09b712-901424a4e4.zip deleted file mode 100644 index 3f9bd71cf..000000000 Binary files a/.yarn/cache/@ariakit-react-npm-0.3.9-b5ae09b712-901424a4e4.zip and /dev/null differ diff --git a/.yarn/cache/@ariakit-react-npm-0.4.18-26fc12aa5e-e8bc2df82f.zip b/.yarn/cache/@ariakit-react-npm-0.4.18-26fc12aa5e-e8bc2df82f.zip new file mode 100644 index 000000000..92d91bc7f Binary files /dev/null and b/.yarn/cache/@ariakit-react-npm-0.4.18-26fc12aa5e-e8bc2df82f.zip differ diff --git a/packages/services/src/Domain/Item/ItemManagerInterface.ts b/packages/services/src/Domain/Item/ItemManagerInterface.ts index c84337862..f5b5bc883 100644 --- a/packages/services/src/Domain/Item/ItemManagerInterface.ts +++ b/packages/services/src/Domain/Item/ItemManagerInterface.ts @@ -128,6 +128,8 @@ export interface ItemManagerInterface extends AbstractService { getDisplayableFiles(): FileItem[] setVaultDisplayOptions(options: VaultDisplayOptions): void numberOfNotesWithConflicts(): number + /** Returns all notes, files, tags and views */ + getInteractableItems(): DecryptedItemInterface[] getDisplayableNotes(): SNNote[] getDisplayableNotesAndFiles(): (SNNote | FileItem)[] setPrimaryItemDisplayOptions(options: NotesAndFilesDisplayControllerOptions): void diff --git a/packages/snjs/lib/Services/Items/ItemManager.ts b/packages/snjs/lib/Services/Items/ItemManager.ts index ce3cbfa2a..9ad4204f2 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.ts @@ -220,6 +220,17 @@ export class ItemManager extends Services.AbstractService implements Services.It this.itemCounter.setVaultDisplayOptions(options) } + public getInteractableItems(): Models.DecryptedItemInterface[] { + return (this.systemSmartViews as Models.DecryptedItemInterface[]).concat( + this.collection.all([ + ContentType.TYPES.Note, + ContentType.TYPES.File, + ContentType.TYPES.Tag, + ContentType.TYPES.SmartView, + ]) as Models.DecryptedItemInterface[], + ) + } + public getDisplayableNotes(): Models.SNNote[] { assert(this.navigationDisplayController.contentTypes.length === 2) diff --git a/packages/ui-services/src/Keyboard/KeyboardCommands.ts b/packages/ui-services/src/Keyboard/KeyboardCommands.ts index 4ecd457b3..b23a447c3 100644 --- a/packages/ui-services/src/Keyboard/KeyboardCommands.ts +++ b/packages/ui-services/src/Keyboard/KeyboardCommands.ts @@ -41,3 +41,4 @@ export const OPEN_PREFERENCES_COMMAND = createKeyboardCommand('OPEN_PREFERENCES_ export const CHANGE_EDITOR_WIDTH_COMMAND = createKeyboardCommand('CHANGE_EDITOR_WIDTH_COMMAND') export const TOGGLE_KEYBOARD_SHORTCUTS_MODAL = createKeyboardCommand('TOGGLE_KEYBOARD_SHORTCUTS_MODAL') +export const TOGGLE_COMMAND_PALETTE = createKeyboardCommand('TOGGLE_COMMAND_PALETTE') diff --git a/packages/ui-services/src/Keyboard/KeyboardService.ts b/packages/ui-services/src/Keyboard/KeyboardService.ts index e3bed5f09..527a5d2e2 100644 --- a/packages/ui-services/src/Keyboard/KeyboardService.ts +++ b/packages/ui-services/src/Keyboard/KeyboardService.ts @@ -1,4 +1,4 @@ -import { Environment, Platform } from '@standardnotes/snjs' +import { Environment, Platform, UuidGenerator } from '@standardnotes/snjs' import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers' import { KeyboardCommand } from './KeyboardCommands' import { KeyboardKeyEvent } from './KeyboardKeyEvent' @@ -28,6 +28,20 @@ export class KeyboardService { } } + private isDisabled = false + /** + * When called, the service will stop triggering command handlers + * on keydown/keyup events. Useful when you need to handle events + * yourself while keeping the rest of behaviours inert. + * Make sure to call {@link enableEventHandling} once done. + */ + public disableEventHandling() { + this.isDisabled = true + } + public enableEventHandling() { + this.isDisabled = false + } + get isMac() { return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb } @@ -116,6 +130,9 @@ export class KeyboardService { } private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void { + if (this.isDisabled) { + return + } for (const command of this.commandMap.keys()) { const shortcut = this.commandMap.get(command) if (!shortcut) { @@ -243,6 +260,7 @@ export class KeyboardService { ...shortcut, category: handler.category, description: handler.description, + id: UuidGenerator.GenerateUuid(), } } @@ -250,11 +268,12 @@ export class KeyboardService { * Register help item for a keyboard shortcut that is handled outside of the KeyboardService, * for example by a library like Lexical. */ - registerExternalKeyboardShortcutHelpItem(item: KeyboardShortcutHelpItem): () => void { - this.keyboardShortcutHelpItems.add(item) + registerExternalKeyboardShortcutHelpItem(item: Omit): () => void { + const itemWithId = { ...item, id: UuidGenerator.GenerateUuid() } + this.keyboardShortcutHelpItems.add(itemWithId) return () => { - this.keyboardShortcutHelpItems.delete(item) + this.keyboardShortcutHelpItems.delete(itemWithId) } } @@ -262,7 +281,7 @@ export class KeyboardService { * Register help item for a keyboard shortcut that is handled outside of the KeyboardService, * for example by a library like Lexical. */ - registerExternalKeyboardShortcutHelpItems(items: KeyboardShortcutHelpItem[]): () => void { + registerExternalKeyboardShortcutHelpItems(items: Omit[]): () => void { const disposers = items.map((item) => this.registerExternalKeyboardShortcutHelpItem(item)) return () => { diff --git a/packages/ui-services/src/Keyboard/KeyboardShortcut.ts b/packages/ui-services/src/Keyboard/KeyboardShortcut.ts index baf28819d..085d59003 100644 --- a/packages/ui-services/src/Keyboard/KeyboardShortcut.ts +++ b/packages/ui-services/src/Keyboard/KeyboardShortcut.ts @@ -21,8 +21,9 @@ export type PlatformedKeyboardShortcut = KeyboardShortcut & { export type KeyboardShortcutCategory = 'General' | 'Notes list' | 'Current note' | 'Super notes' | 'Formatting' -export type KeyboardShortcutHelpItem = Omit & { +export interface KeyboardShortcutHelpItem extends Omit { command?: KeyboardCommand category: KeyboardShortcutCategory description: string + id: string } diff --git a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts index 6cf67c193..1f4f4eaca 100644 --- a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts +++ b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts @@ -31,6 +31,7 @@ import { CHANGE_EDITOR_WIDTH_COMMAND, SUPER_TOGGLE_TOOLBAR, TOGGLE_KEYBOARD_SHORTCUTS_MODAL, + TOGGLE_COMMAND_PALETTE, } from './KeyboardCommands' import { KeyboardKey } from './KeyboardKey' import { KeyboardModifier, getPrimaryModifier } from './KeyboardModifier' @@ -108,7 +109,7 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme }, { command: CHANGE_EDITOR_COMMAND, - key: '/', + key: '?', modifiers: [primaryModifier, KeyboardModifier.Shift], preventDefault: true, }, @@ -200,5 +201,10 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme key: '/', modifiers: [primaryModifier], }, + { + command: TOGGLE_COMMAND_PALETTE, + code: 'Semicolon', + modifiers: [primaryModifier, KeyboardModifier.Shift], + }, ] } diff --git a/packages/ui-services/src/Keyboard/keyboardCharacterForKey.ts b/packages/ui-services/src/Keyboard/keyboardCharacterForKey.ts index 7ce2f23fb..010131346 100644 --- a/packages/ui-services/src/Keyboard/keyboardCharacterForKey.ts +++ b/packages/ui-services/src/Keyboard/keyboardCharacterForKey.ts @@ -1,4 +1,4 @@ -export function keyboardCharacterForKeyOrCode(keyOrCode: string) { +export function keyboardCharacterForKeyOrCode(keyOrCode: string, shiftKey = false) { if (keyOrCode.startsWith('Digit')) { return keyOrCode.replace('Digit', '') } @@ -14,6 +14,8 @@ export function keyboardCharacterForKeyOrCode(keyOrCode: string) { return '←' case 'ArrowRight': return '→' + case 'Semicolon': + return shiftKey ? ':' : ';' default: return keyOrCode } diff --git a/packages/web/package.json b/packages/web/package.json index 4584688b5..3f5eee88c 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -107,7 +107,7 @@ "app/**/*.{js,ts,jsx,tsx,css,md}": "prettier --write" }, "dependencies": { - "@ariakit/react": "^0.3.9", + "@ariakit/react": "^0.4.18", "@lexical/clipboard": "0.32.1", "@lexical/headless": "0.32.1", "@lexical/link": "0.32.1", diff --git a/packages/web/src/javascripts/Application/Dependencies/Types.ts b/packages/web/src/javascripts/Application/Dependencies/Types.ts index 12cb179f8..b2ad12352 100644 --- a/packages/web/src/javascripts/Application/Dependencies/Types.ts +++ b/packages/web/src/javascripts/Application/Dependencies/Types.ts @@ -9,6 +9,7 @@ export const Web_TYPES = { Importer: Symbol.for('Importer'), ItemGroupController: Symbol.for('ItemGroupController'), KeyboardService: Symbol.for('KeyboardService'), + CommandService: Symbol.for('CommandService'), MobileWebReceiver: Symbol.for('MobileWebReceiver'), MomentsService: Symbol.for('MomentsService'), PersistenceService: Symbol.for('PersistenceService'), diff --git a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts index 8d2ba4b80..eea0204b4 100644 --- a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts +++ b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts @@ -14,7 +14,6 @@ import { ThemeManager, ToastService, VaultDisplayService, - WebApplicationInterface, } from '@standardnotes/ui-services' import { DependencyContainer } from '@standardnotes/utils' import { Web_TYPES } from './Types' @@ -50,9 +49,11 @@ import { LoadPurchaseFlowUrl } from '../UseCase/LoadPurchaseFlowUrl' import { GetPurchaseFlowUrl } from '../UseCase/GetPurchaseFlowUrl' import { OpenSubscriptionDashboard } from '../UseCase/OpenSubscriptionDashboard' import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter' +import { WebApplication } from '../WebApplication' +import { CommandService } from '../../Components/CommandPalette/CommandService' export class WebDependencies extends DependencyContainer { - constructor(private application: WebApplicationInterface) { + constructor(private application: WebApplication) { super() this.bind(Web_TYPES.SuperConverter, () => { @@ -124,6 +125,9 @@ export class WebDependencies extends DependencyContainer { this.bind(Web_TYPES.KeyboardService, () => { return new KeyboardService(application.platform, application.environment) }) + this.bind(Web_TYPES.CommandService, () => { + return new CommandService(this.get(Web_TYPES.KeyboardService), application.generateUuid) + }) this.bind(Web_TYPES.ArchiveManager, () => { return new ArchiveManager(application) @@ -199,6 +203,7 @@ export class WebDependencies extends DependencyContainer { return new PaneController( application.preferences, this.get(Web_TYPES.KeyboardService), + application.commands, this.get(Web_TYPES.IsTabletOrMobileScreen), this.get(Web_TYPES.PanesForLayout), application.events, @@ -233,7 +238,7 @@ export class WebDependencies extends DependencyContainer { return new NavigationController( this.get(Web_TYPES.FeaturesController), this.get(Web_TYPES.VaultDisplayService), - this.get(Web_TYPES.KeyboardService), + this.get(Web_TYPES.CommandService), this.get(Web_TYPES.PaneController), application.sync, application.mutator, @@ -241,25 +246,16 @@ export class WebDependencies extends DependencyContainer { application.preferences, application.alerts, application.changeAndSaveItem, + application.recents, application.events, ) }) this.bind(Web_TYPES.NotesController, () => { return new NotesController( - this.get(Web_TYPES.ItemListController), - this.get(Web_TYPES.NavigationController), - this.get(Web_TYPES.ItemGroupController), - this.get(Web_TYPES.KeyboardService), - application.preferences, - application.items, - application.mutator, - application.sync, - application.protections, - application.alerts, + application, this.get(Web_TYPES.IsGlobalSpellcheckEnabled), this.get(Web_TYPES.GetItemTags), - application.events, ) }) @@ -304,6 +300,7 @@ export class WebDependencies extends DependencyContainer { application.options, this.get(Web_TYPES.IsNativeMobileWeb), application.changeAndSaveItem, + application.recents, application.events, ) }) @@ -374,6 +371,7 @@ export class WebDependencies extends DependencyContainer { application.platform, application.mobileDevice, this.get(Web_TYPES.IsNativeMobileWeb), + application.recents, application.events, ) }) @@ -381,7 +379,7 @@ export class WebDependencies extends DependencyContainer { this.bind(Web_TYPES.HistoryModalController, () => { return new HistoryModalController( this.get(Web_TYPES.NotesController), - this.get(Web_TYPES.KeyboardService), + this.get(Web_TYPES.CommandService), application.events, ) }) diff --git a/packages/web/src/javascripts/Application/Recents.ts b/packages/web/src/javascripts/Application/Recents.ts new file mode 100644 index 000000000..425f8b382 --- /dev/null +++ b/packages/web/src/javascripts/Application/Recents.ts @@ -0,0 +1,33 @@ +const MaxCommands = 5 +const MaxItems = 10 + +export class RecentActionsState { + #commandUuids: string[] = [] + #itemUuids: string[] = [] + + /** + * Recently used commands, most recent at the start + */ + get commandUuids() { + return this.#commandUuids + } + /** + * Recently opened items, most recent at the start + */ + get itemUuids() { + return this.#itemUuids + } + + add(id: string, action_type: 'item' | 'command' = 'item') { + const action_array = action_type === 'item' ? this.#itemUuids : this.#commandUuids + const existing = action_array.findIndex((uuid) => uuid === id) + if (existing !== -1) { + action_array.splice(existing, 1) + } + const max = action_type === 'item' ? MaxItems : MaxCommands + if (action_array.length == max) { + action_array.pop() + } + action_array.unshift(id) + } +} diff --git a/packages/web/src/javascripts/Application/WebApplication.spec.ts b/packages/web/src/javascripts/Application/WebApplication.spec.ts index 513e44c46..ef883fbca 100644 --- a/packages/web/src/javascripts/Application/WebApplication.spec.ts +++ b/packages/web/src/javascripts/Application/WebApplication.spec.ts @@ -2,6 +2,19 @@ import { Environment, namespacedKey, Platform, RawStorageKey, SNLog } from '@sta import { WebApplication } from '@/Application/WebApplication' import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice' +jest.mock('@standardnotes/sncrypto-web', () => { + return { + SNWebCrypto: class { + initialize() { + return Promise.resolve() + } + generateUUID() { + return 'mock-uuid' + } + }, + } +}) + describe('web application', () => { let application: WebApplication diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts index 46c91062b..aa27bd5b8 100644 --- a/packages/web/src/javascripts/Application/WebApplication.ts +++ b/packages/web/src/javascripts/Application/WebApplication.ts @@ -80,6 +80,8 @@ import { SearchOptionsController } from '@/Controllers/SearchOptionsController' import { PersistenceService } from '@/Controllers/Abstract/PersistenceService' import { removeFromArray } from '@standardnotes/utils' import { FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction' +import { RecentActionsState } from './Recents' +import { CommandService } from '../Components/CommandPalette/CommandService' export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void @@ -95,6 +97,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter public isSessionsModalVisible = false public devMode?: DevMode + public recents = new RecentActionsState() constructor( deviceInterface: WebOrDesktopDevice, @@ -597,6 +600,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter return this.deps.get(Web_TYPES.KeyboardService) } + get commands(): CommandService { + return this.deps.get(Web_TYPES.CommandService) + } + get featuresController(): FeaturesController { return this.deps.get(Web_TYPES.FeaturesController) } diff --git a/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx b/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx index 0a7f55c94..04b1fa03d 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx @@ -13,7 +13,7 @@ import Spinner from '@/Components/Spinner/Spinner' import { MenuItemIconSize } from '@/Constants/TailwindClassNames' import { useApplication } from '../ApplicationProvider' import MenuSection from '../Menu/MenuSection' -import { TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services' +import { TOGGLE_COMMAND_PALETTE, TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services' import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator' type Props = { @@ -95,6 +95,9 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, const keyboardShortcutsHelpShortcut = useMemo(() => { return application.keyboardService.keyboardShortcutForCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL) }, [application.keyboardService]) + const commandPaletteShortcut = useMemo(() => { + return application.keyboardService.keyboardShortcutForCommand(TOGGLE_COMMAND_PALETTE) + }, [application.keyboardService]) return ( <> @@ -194,17 +197,30 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, v{application.version} {!isMobilePlatform(application.platform) && ( - { - application.keyboardService.triggerCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL) - }} - > - - Keyboard shortcuts - {keyboardShortcutsHelpShortcut && ( - - )} - + <> + { + application.keyboardService.triggerCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL) + }} + > + + Keyboard shortcuts + {keyboardShortcutsHelpShortcut && ( + + )} + + { + application.keyboardService.triggerCommand(TOGGLE_COMMAND_PALETTE) + }} + > + + Command palette + {commandPaletteShortcut && ( + + )} + + )} {user ? ( diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index 9bfabe577..6570bbe6d 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -23,7 +23,7 @@ import ResponsivePaneProvider from '../Panes/ResponsivePaneProvider' import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler' import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal' import ApplicationProvider from '../ApplicationProvider' -import CommandProvider from '../CommandProvider' +import KeyboardServiceProvider from '../KeyboardServiceProvider' import PanesSystemComponent from '../Panes/PanesSystemComponent' import DotOrgNotice from './DotOrgNotice' import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider' @@ -32,6 +32,8 @@ import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose' import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal' import { ProtectionEvent } from '@standardnotes/services' import KeyboardShortcutsModal from '../KeyboardShortcutsHelpModal/KeyboardShortcutsHelpModal' +import CommandPalette from '../CommandPalette/CommandPalette' +import SuperExportModal from '../NotesOptions/SuperExportModal' type Props = { application: WebApplication @@ -212,7 +214,7 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio if (route.type === RouteType.AppViewRoute && route.appViewRouteParam === 'extension') { return ( - + @@ -227,14 +229,14 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio - + ) } return ( - + @@ -269,6 +271,8 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio + + {application.routeService.isDotOrg && } {isIOS() && } @@ -277,7 +281,7 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio - + ) } diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx index c57c13e02..004a03560 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx @@ -55,14 +55,15 @@ const ChangeEditorButton: FunctionComponent = ({ noteViewController, onCl }, [isOpen, onClickPreprocessing, onClick]) useEffect(() => { - return application.keyboardService.addCommandHandler({ - command: CHANGE_EDITOR_COMMAND, - category: 'Current note', - description: 'Change note type', - onKeyDown: () => { + return application.commands.addWithShortcut( + CHANGE_EDITOR_COMMAND, + 'Current note', + 'Change note type', + () => { void toggleMenu() }, - }) + 'notes', + ) }, [application, toggleMenu]) const shortcut = useMemo( diff --git a/packages/web/src/javascripts/Components/CommandPalette/CommandPalette.tsx b/packages/web/src/javascripts/Components/CommandPalette/CommandPalette.tsx new file mode 100644 index 000000000..ad3e5d36a --- /dev/null +++ b/packages/web/src/javascripts/Components/CommandPalette/CommandPalette.tsx @@ -0,0 +1,425 @@ +import { observer } from 'mobx-react-lite' +import { startTransition, useCallback, useEffect, useState } from 'react' +import { useKeyboardService } from '../KeyboardServiceProvider' +import { PlatformedKeyboardShortcut, TOGGLE_COMMAND_PALETTE } from '@standardnotes/ui-services' +import { + Combobox, + ComboboxGroup, + ComboboxGroupLabel, + ComboboxItem, + ComboboxList, + ComboboxProvider, + Dialog, + Tab, + TabList, + TabPanel, + TabProvider, + useDialogStore, + useTabContext, +} from '@ariakit/react' +import { + classNames, + DecryptedItemInterface, + FileItem, + SmartView, + SNNote, + SNTag, + UuidGenerator, +} from '@standardnotes/snjs' +import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator' +import { useApplication } from '../ApplicationProvider' +import Icon from '../Icon/Icon' +import { getIconForItem } from '../../Utils/Items/Icons/getIconForItem' +import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction' +import type { CommandService } from './CommandService' +import { requestCloseAllOpenModalsAndPopovers } from '../../Utils/CloseOpenModalsAndPopovers' + +type CommandPaletteItem = { + id: string + description: string + icon: JSX.Element + shortcut?: PlatformedKeyboardShortcut + resultRange?: [number, number] +} & ({ section: 'notes' | 'files' | 'tags'; itemUuid: string } | { section: 'commands' }) + +function ListItemDescription({ item }: { item: CommandPaletteItem }) { + const range = item.resultRange + if (!range) { + return item.description + } + return ( + <> + {item.description.slice(0, range[0])} + + {item.description.slice(range[0], range[1])} + + {item.description.slice(range[1])} + + ) +} + +const Tabs = ['all', 'commands', 'notes', 'files', 'tags'] as const +type TabId = (typeof Tabs)[number] + +function CommandPaletteListItem({ + id, + item, + index, + handleClick, + selectedTab, +}: { + id: string + item: CommandPaletteItem + index: number + handleClick: (item: CommandPaletteItem) => void + selectedTab: TabId +}) { + if (selectedTab !== 'all' && selectedTab !== item.section) { + return null + } + return ( + svg]:flex-shrink-0', + index === 0 && 'scroll-m-8', + )} + onClick={() => handleClick(item)} + > + {item.icon} +
+ +
+ {item.shortcut && } +
+ ) +} + +function ComboboxInput() { + const tab = useTabContext() + return ( + { + if (event.key !== 'Tab') { + return + } + const activeId = tab?.getState().selectedId + const options = { activeId } + const nextId = event.shiftKey ? tab?.previous(options) : tab?.next(options) + if (nextId) { + event.preventDefault() + tab?.select(nextId) + } + }} + /> + ) +} + +// Future TODO, nice to have: A way to expose items like the current note's options +// directly in the command palette rather than only having a way to open the menu +function CommandPalette() { + const application = useApplication() + const keyboardService = useKeyboardService() + const [isOpen, setIsOpen] = useState(false) + const [query, setQuery] = useState('') + const [recents, setRecents] = useState([]) + const [items, setItems] = useState([]) + // Storing counts as separate state to avoid iterating items multiple times + const [itemCountsPerTab, setItemCounts] = useState({ + commands: 0, + notes: 0, + files: 0, + tags: 0, + }) + const [selectedTab, setSelectedTab] = useState('all') + const dialog = useDialogStore({ + open: isOpen, + setOpen: setIsOpen, + }) + + useEffect(() => { + if (isOpen) { + keyboardService.disableEventHandling() + requestCloseAllOpenModalsAndPopovers() + } else { + keyboardService.enableEventHandling() + } + }, [keyboardService, isOpen]) + + useEffect(() => { + return keyboardService.addCommandHandler({ + command: TOGGLE_COMMAND_PALETTE, + category: 'General', + description: 'Toggle command palette', + onKeyDown: (e) => { + e.preventDefault() + setIsOpen((open) => !open) + setQuery('') + }, + }) + }, [keyboardService]) + + const handleItemClick = useCallback( + (item: CommandPaletteItem) => { + if (item.section === 'commands') { + application.commands.triggerCommand(item.id) + application.recents.add(item.id, 'command') + } else { + const decryptedItem = application.items.findItem(item.itemUuid) + if (!decryptedItem) { + return + } + if (decryptedItem instanceof SNNote) { + void application.itemListController.selectItemUsingInstance(decryptedItem, true) + } else if (decryptedItem instanceof FileItem) { + void application.filesController.handleFileAction({ + type: FileItemActionType.PreviewFile, + payload: { file: decryptedItem }, + }) + } else if (decryptedItem instanceof SNTag || decryptedItem instanceof SmartView) { + void application.navigationController.setSelectedTag(decryptedItem, 'all', { + userTriggered: true, + }) + } + } + }, + [ + application.commands, + application.filesController, + application.itemListController, + application.items, + application.navigationController, + application.recents, + ], + ) + + const createItemForInteractableItem = useCallback( + (item: DecryptedItemInterface): CommandPaletteItem => { + const icon = getIconForItem(item, application) + let section: 'notes' | 'files' | 'tags' + if (item instanceof SNNote) { + section = 'notes' + } else if (item instanceof FileItem) { + section = 'files' + } else if (item instanceof SNTag || item instanceof SmartView) { + section = 'tags' + } else { + throw new Error('Item is not a note, file or tag') + } + return { + section, + id: UuidGenerator.GenerateUuid(), + itemUuid: item.uuid, + description: item.title || '', + icon: , + } + }, + [application], + ) + + const createItemForCommand = useCallback( + (command: ReturnType[0]): CommandPaletteItem => { + const shortcut = command.shortcut_id + ? application.keyboardService.keyboardShortcutForCommand(command.shortcut_id) + : undefined + return { + id: command.id, + description: command.description, + section: 'commands', + icon: , + shortcut, + } + }, + [application.keyboardService], + ) + + useEffect( + function updateCommandPaletteItems() { + if (!isOpen) { + setSelectedTab('all') + setItems([]) + return + } + + const recents: CommandPaletteItem[] = [] + const items: CommandPaletteItem[] = [] + const itemCounts: typeof itemCountsPerTab = { + commands: 0, + notes: 0, + files: 0, + tags: 0, + } + + const searchQuery = query.toLowerCase() + const hasQuery = searchQuery.length > 0 + + if (hasQuery) { + const commands = application.commands.getCommandDescriptions() + for (let i = 0; i < commands.length; i++) { + const command = commands[i] + if (!command) { + continue + } + if (items.length >= 50) { + break + } + const index = command.description.toLowerCase().indexOf(searchQuery) + if (index === -1) { + continue + } + const item = createItemForCommand(command) + item.resultRange = [index, index + searchQuery.length] + items.push(item) + itemCounts[item.section]++ + } + + const interactableItems = application.items.getInteractableItems() + for (let i = 0; i < interactableItems.length; i++) { + if (items.length >= 50) { + break + } + const decryptedItem = interactableItems[i] + if (!decryptedItem || !decryptedItem.title) { + continue + } + const index = decryptedItem.title.toLowerCase().indexOf(searchQuery) + if (index === -1) { + continue + } + const item = createItemForInteractableItem(decryptedItem) + item.resultRange = [index, index + searchQuery.length] + items.push(item) + itemCounts[item.section]++ + } + } else { + const recentCommands = application.recents.commandUuids + for (let i = 0; i < recentCommands.length; i++) { + const command = application.commands.getCommandDescription(recentCommands[i]) + if (!command) { + continue + } + const item = createItemForCommand(command) + recents.push(item) + itemCounts[item.section]++ + } + const recentItems = application.recents.itemUuids + for (let i = 0; i < recentItems.length; i++) { + const decryptedItem = application.items.findItem(recentItems[i]) + if (!decryptedItem) { + continue + } + const item = createItemForInteractableItem(decryptedItem) + recents.push(item) + itemCounts[item.section]++ + } + + const commands = application.commands.getCommandDescriptions() + for (let i = 0; i < commands.length; i++) { + const command = commands[i] + if (!command) { + continue + } + const item = createItemForCommand(command) + items.push(item) + itemCounts[item.section]++ + } + items.sort((a, b) => (a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1)) + } + + setItems(items) + setRecents(recents) + setItemCounts(itemCounts) + }, + [application, createItemForCommand, createItemForInteractableItem, isOpen, query], + ) + + const hasNoItemsAtAll = items.length === 0 + const hasNoItemsInSelectedTab = selectedTab !== 'all' && itemCountsPerTab[selectedTab] === 0 + + return ( + } + > + { + startTransition(() => setQuery(value)) + }} + > + setSelectedTab((id as TabId) || 'all')}> +
+ +
+ + {Tabs.map((id) => ( + + {id} + + ))} + + + {query.length > 0 && (hasNoItemsAtAll || hasNoItemsInSelectedTab) && ( +
No items found
+ )} + + {recents.length > 0 && ( + + Recent + {recents.map((item, index) => ( + + ))} + + )} + {!hasNoItemsAtAll && ( + + {recents.length > 0 && ( + All commands + )} + {items.map((item, index) => ( + + ))} + + )} + +
+
+
+
+ ) +} + +export default observer(CommandPalette) diff --git a/packages/web/src/javascripts/Components/CommandPalette/CommandService.ts b/packages/web/src/javascripts/Components/CommandPalette/CommandService.ts new file mode 100644 index 000000000..37b454dae --- /dev/null +++ b/packages/web/src/javascripts/Components/CommandPalette/CommandService.ts @@ -0,0 +1,66 @@ +import { GenerateUuid, IconType } from '@standardnotes/snjs' +import { KeyboardCommand, KeyboardService, KeyboardShortcutCategory } from '@standardnotes/ui-services' +import mergeRegister from '../../Hooks/mergeRegister' + +type CommandInfo = { + description: string + icon: IconType + shortcut_id?: KeyboardCommand +} + +type CommandDescription = { id: string } & CommandInfo + +export class CommandService { + #commandInfo = new Map() + #commandHandlers = new Map void>() + + constructor( + private keyboardService: KeyboardService, + private generateUuid: GenerateUuid, + ) {} + + public add(id: string, description: string, handler: () => void, icon?: IconType, shortcut_id?: KeyboardCommand) { + this.#commandInfo.set(id, { description, icon: icon ?? 'info', shortcut_id }) + this.#commandHandlers.set(id, handler) + return () => { + this.#commandInfo.delete(id) + this.#commandHandlers.delete(id) + } + } + + public addWithShortcut( + id: KeyboardCommand, + category: KeyboardShortcutCategory, + description: string, + handler: (event?: KeyboardEvent, data?: unknown) => void, + icon?: IconType, + ) { + return mergeRegister( + this.add(id.description ?? this.generateUuid.execute().getValue(), description, handler, icon, id), + this.keyboardService.addCommandHandler({ command: id, category, description, onKeyDown: handler }), + ) + } + + public triggerCommand(id: string) { + const handler = this.#commandHandlers.get(id) + if (handler) { + handler() + } + } + + public getCommandDescriptions() { + const descriptions: CommandDescription[] = [] + for (const [id, { description, icon, shortcut_id }] of this.#commandInfo) { + descriptions.push({ id, description, icon, shortcut_id }) + } + return descriptions + } + + public getCommandDescription(id: string): CommandDescription | undefined { + const command = this.#commandInfo.get(id) + if (!command) { + return + } + return { id, ...command } + } +} diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index 3d9328a10..c66d0e242 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -175,15 +175,6 @@ const ContentListView = forwardRef( * probably better to be consistent. */ return application.keyboardService.addCommandHandlers([ - { - command: CREATE_NEW_NOTE_KEYBOARD_COMMAND, - category: 'General', - description: 'Create new note', - onKeyDown: (event) => { - event.preventDefault() - void addNewItem() - }, - }, { command: NEXT_LIST_ITEM_KEYBOARD_COMMAND, category: 'Notes list', @@ -262,11 +253,28 @@ const ContentListView = forwardRef( ) const addButtonLabel = useMemo(() => { - return isFilesSmartView - ? 'Upload file' - : `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})` + let shortcut = keyboardStringForShortcut(shortcutForCreate) + if (shortcut) { + shortcut = '(' + shortcut + ')' + } + return isFilesSmartView ? `Upload file ${shortcut}` : `Create a new note in the selected tag ${shortcut}` }, [isFilesSmartView, shortcutForCreate]) + useEffect( + () => + application.commands.addWithShortcut( + CREATE_NEW_NOTE_KEYBOARD_COMMAND, + 'General', + isFilesSmartView ? 'Upload file' : 'Create new note', + (event) => { + event?.preventDefault() + void addNewItem() + }, + isFilesSmartView ? 'upload' : 'add', + ), + [addNewItem, application.commands, isFilesSmartView], + ) + const dailyMode = selectedAsTag?.isDailyEntry const handleDailyListSelection = useCallback( diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx index 8217e335d..586d57bcb 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/ContentListHeader.tsx @@ -103,6 +103,17 @@ const ContentListHeader = ({ setShowDisplayOptionsMenu((show) => !show) }, []) + useEffect( + () => + application.commands.add( + 'open-display-opts-menu', + 'Open display options menu', + toggleDisplayOptionsMenu, + 'sort-descending', + ), + [application.commands, toggleDisplayOptionsMenu], + ) + const OptionsMenu = useMemo(() => { return (
diff --git a/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx b/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx index d8722bf97..7869eb1f1 100644 --- a/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx +++ b/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx @@ -178,11 +178,11 @@ const EditorWidthSelectionModalWrapper = () => { }, []) useEffect(() => { - return application.keyboardService.addCommandHandler({ - command: CHANGE_EDITOR_WIDTH_COMMAND, - category: 'Current note', - description: 'Change editor width', - onKeyDown: (_, data) => { + return application.commands.addWithShortcut( + CHANGE_EDITOR_WIDTH_COMMAND, + 'Current note', + 'Change editor width', + (_, data) => { if (typeof data === 'boolean' && data) { setIsGlobal(data) } else { @@ -190,7 +190,8 @@ const EditorWidthSelectionModalWrapper = () => { } toggle() }, - }) + 'line-width', + ) }, [application, toggle]) return ( diff --git a/packages/web/src/javascripts/Components/Footer/AccountMenuButton.tsx b/packages/web/src/javascripts/Components/Footer/AccountMenuButton.tsx index a273b6f33..3d9d86c45 100644 --- a/packages/web/src/javascripts/Components/Footer/AccountMenuButton.tsx +++ b/packages/web/src/javascripts/Components/Footer/AccountMenuButton.tsx @@ -1,11 +1,12 @@ import { classNames } from '@standardnotes/utils' -import { useRef } from 'react' +import { useEffect, useRef } from 'react' import AccountMenu, { AccountMenuProps } from '../AccountMenu/AccountMenu' import Icon from '../Icon/Icon' import Popover from '../Popover/Popover' import StyledTooltip from '../StyledTooltip/StyledTooltip' import { observer } from 'mobx-react-lite' import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' +import { useApplication } from '../ApplicationProvider' type Props = AccountMenuProps & { controller: AccountMenuController @@ -15,9 +16,15 @@ type Props = AccountMenuProps & { } const AccountMenuButton = ({ hasError, controller, mainApplicationGroup, onClickOutside, toggleMenu, user }: Props) => { + const application = useApplication() const buttonRef = useRef(null) const { show: isOpen } = controller + useEffect( + () => application.commands.add('open-acc-menu', 'Open account menu', toggleMenu, 'account-circle'), + [application.commands, toggleMenu], + ) + return ( <> diff --git a/packages/web/src/javascripts/Components/Footer/PreferencesButton.tsx b/packages/web/src/javascripts/Components/Footer/PreferencesButton.tsx index e4146eabc..a6398bd86 100644 --- a/packages/web/src/javascripts/Components/Footer/PreferencesButton.tsx +++ b/packages/web/src/javascripts/Components/Footer/PreferencesButton.tsx @@ -2,7 +2,7 @@ import { compareSemVersions, StatusServiceEvent } from '@standardnotes/snjs' import { keyboardStringForShortcut, OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services' import { useCallback, useEffect, useMemo, useState } from 'react' import { useApplication } from '../ApplicationProvider' -import { useCommandService } from '../CommandProvider' +import { useKeyboardService } from '../KeyboardServiceProvider' import Icon from '../Icon/Icon' import StyledTooltip from '../StyledTooltip/StyledTooltip' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' @@ -16,10 +16,10 @@ type Props = { const PreferencesButton = ({ openPreferences }: Props) => { const application = useApplication() - const commandService = useCommandService() + const keyboardService = useKeyboardService() const shortcut = useMemo( - () => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(OPEN_PREFERENCES_COMMAND)), - [commandService], + () => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(OPEN_PREFERENCES_COMMAND)), + [keyboardService], ) const [changelogLastReadVersion, setChangelogLastReadVersion] = useState(() => diff --git a/packages/web/src/javascripts/Components/Footer/QuickSettingsButton.tsx b/packages/web/src/javascripts/Components/Footer/QuickSettingsButton.tsx index 13cc02b61..354b7d090 100644 --- a/packages/web/src/javascripts/Components/Footer/QuickSettingsButton.tsx +++ b/packages/web/src/javascripts/Components/Footer/QuickSettingsButton.tsx @@ -2,13 +2,14 @@ import { WebApplication } from '@/Application/WebApplication' import { UIFeature, GetDarkThemeFeature } from '@standardnotes/snjs' import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services' import { classNames } from '@standardnotes/utils' -import { useEffect, useRef, useState } from 'react' -import { useCommandService } from '../CommandProvider' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useKeyboardService } from '../KeyboardServiceProvider' import Icon from '../Icon/Icon' import Popover from '../Popover/Popover' import QuickSettingsMenu from '../QuickSettingsMenu/QuickSettingsMenu' import StyledTooltip from '../StyledTooltip/StyledTooltip' import RoundIconButton from '../Button/RoundIconButton' +import mergeRegister from '../../Hooks/mergeRegister' type Props = { application: WebApplication @@ -17,30 +18,37 @@ type Props = { const QuickSettingsButton = ({ application, isMobileNavigation = false }: Props) => { const buttonRef = useRef(null) - const commandService = useCommandService() + const keyboardService = useKeyboardService() const [isOpen, setIsOpen] = useState(false) - const toggleMenu = () => setIsOpen(!isOpen) + const toggleMenu = useCallback(() => setIsOpen((isOpen) => !isOpen), []) useEffect(() => { + if (isMobileNavigation) { + return + } + const darkThemeFeature = new UIFeature(GetDarkThemeFeature()) - return commandService.addCommandHandler({ - command: TOGGLE_DARK_MODE_COMMAND, - category: 'General', - description: 'Toggle dark mode', - onKeyDown: () => { + return mergeRegister( + application.commands.addWithShortcut(TOGGLE_DARK_MODE_COMMAND, 'General', 'Toggle dark mode', () => { void application.componentManager.toggleTheme(darkThemeFeature) return true - }, - }) - }, [application, commandService]) + }), + application.commands.add('open-quick-settings-menu', 'Open quick settings menu', toggleMenu, 'themes'), + ) + }, [application, isMobileNavigation, keyboardService, toggleMenu]) return ( <> {isMobileNavigation ? ( - + ) : (
@@ -468,7 +379,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => { @@ -480,10 +391,6 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => { )} - - - - ) } diff --git a/packages/web/src/javascripts/Components/NotesOptions/SuperExportModal.tsx b/packages/web/src/javascripts/Components/NotesOptions/SuperExportModal.tsx index c271bd979..eb3055d11 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/SuperExportModal.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/SuperExportModal.tsx @@ -1,4 +1,4 @@ -import { PrefKey, PrefValue, SNNote } from '@standardnotes/snjs' +import { PrefKey, PrefValue } from '@standardnotes/snjs' import { useApplication } from '../ApplicationProvider' import Modal from '../Modal/Modal' import usePreference from '@/Hooks/usePreference' @@ -6,15 +6,13 @@ import { useEffect } from 'react' import Switch from '../Switch/Switch' import { noteHasEmbeddedFiles } from '@/Utils/NoteExportUtils' import Dropdown from '../Dropdown/Dropdown' +import ModalOverlay from '../Modal/ModalOverlay' +import { observer } from 'mobx-react-lite' -type Props = { - notes: SNNote[] - exportNotes: () => void - close: () => void -} - -const SuperExportModal = ({ notes, exportNotes, close }: Props) => { +const ModalContent = observer(() => { const application = useApplication() + const notesController = application.notesController + const notes = notesController.selectedNotes const superNoteExportFormat = usePreference(PrefKey.SuperNoteExportFormat) const superNoteExportEmbedBehavior = usePreference(PrefKey.SuperNoteExportEmbedBehavior) @@ -53,8 +51,8 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => { label: 'Export', type: 'primary', onClick: () => { - close() - exportNotes() + void notesController.downloadSelectedNotes() + notesController.closeSuperExportModal() }, mobileSlot: 'right', }, @@ -157,6 +155,21 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => { )} ) +}) + +const SuperExportModal = () => { + const application = useApplication() + const notesController = application.notesController + + return ( + + + + ) } -export default SuperExportModal +export default observer(SuperExportModal) diff --git a/packages/web/src/javascripts/Components/NotesOptions/SuperNoteOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/SuperNoteOptions.tsx index 7cc2f2969..08b94401a 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/SuperNoteOptions.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/SuperNoteOptions.tsx @@ -5,30 +5,30 @@ import { iconClass } from './ClassNames' import MenuSection from '../Menu/MenuSection' import { SUPER_SHOW_MARKDOWN_PREVIEW, SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services' import { useMemo, useCallback } from 'react' -import { useCommandService } from '../CommandProvider' +import { useKeyboardService } from '../KeyboardServiceProvider' type Props = { closeMenu: () => void } const SuperNoteOptions = ({ closeMenu }: Props) => { - const commandService = useCommandService() + const keyboardService = useKeyboardService() const markdownShortcut = useMemo( - () => commandService.keyboardShortcutForCommand(SUPER_SHOW_MARKDOWN_PREVIEW), - [commandService], + () => keyboardService.keyboardShortcutForCommand(SUPER_SHOW_MARKDOWN_PREVIEW), + [keyboardService], ) - const findShortcut = useMemo(() => commandService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH), [commandService]) + const findShortcut = useMemo(() => keyboardService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH), [keyboardService]) const enableSuperMarkdownPreview = useCallback(() => { - commandService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW) - }, [commandService]) + keyboardService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW) + }, [keyboardService]) const findInNote = useCallback(() => { - commandService.triggerCommand(SUPER_TOGGLE_SEARCH) + keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH) closeMenu() - }, [closeMenu, commandService]) + }, [closeMenu, keyboardService]) return ( diff --git a/packages/web/src/javascripts/Components/PinNoteButton/PinNoteButton.tsx b/packages/web/src/javascripts/Components/PinNoteButton/PinNoteButton.tsx index dbe4843db..0fafc37cc 100644 --- a/packages/web/src/javascripts/Components/PinNoteButton/PinNoteButton.tsx +++ b/packages/web/src/javascripts/Components/PinNoteButton/PinNoteButton.tsx @@ -4,7 +4,7 @@ import Icon from '@/Components/Icon/Icon' import { NotesController } from '@/Controllers/NotesController/NotesController' import { classNames } from '@standardnotes/utils' import { keyboardStringForShortcut, PIN_NOTE_COMMAND } from '@standardnotes/ui-services' -import { useCommandService } from '../CommandProvider' +import { useKeyboardService } from '../KeyboardServiceProvider' import { VisuallyHidden } from '@ariakit/react' type Props = { @@ -24,11 +24,11 @@ const PinNoteButton: FunctionComponent = ({ className = '', notesControll notesController.togglePinSelectedNotes() }, [onClickPreprocessing, notesController]) - const commandService = useCommandService() + const keyboardService = useKeyboardService() const shortcut = useMemo( - () => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(PIN_NOTE_COMMAND)), - [commandService], + () => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(PIN_NOTE_COMMAND)), + [keyboardService], ) const label = pinned ? `Unpin note (${shortcut})` : `Pin note (${shortcut})` diff --git a/packages/web/src/javascripts/Components/Preferences/PreferencesViewWrapper.tsx b/packages/web/src/javascripts/Components/Preferences/PreferencesViewWrapper.tsx index cc00e741a..1e74a3d14 100644 --- a/packages/web/src/javascripts/Components/Preferences/PreferencesViewWrapper.tsx +++ b/packages/web/src/javascripts/Components/Preferences/PreferencesViewWrapper.tsx @@ -2,7 +2,6 @@ import { FunctionComponent, useEffect } from 'react' import { observer } from 'mobx-react-lite' import PreferencesView from './PreferencesView' import { PreferencesViewWrapperProps } from './PreferencesViewWrapperProps' -import { useCommandService } from '../CommandProvider' import { OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services' import ModalOverlay from '../Modal/ModalOverlay' import { usePaneSwipeGesture } from '../Panes/usePaneGesture' @@ -10,16 +9,15 @@ import { performSafariAnimationFix } from '../Panes/PaneAnimator' import { IosModalAnimationEasing } from '../Modal/useModalAnimation' const PreferencesViewWrapper: FunctionComponent = ({ application }) => { - const commandService = useCommandService() - useEffect(() => { - return commandService.addCommandHandler({ - command: OPEN_PREFERENCES_COMMAND, - category: 'General', - description: 'Open preferences', - onKeyDown: () => application.preferencesController.openPreferences(), - }) - }, [commandService, application]) + return application.commands.addWithShortcut( + OPEN_PREFERENCES_COMMAND, + 'General', + 'Open preferences', + () => application.preferencesController.openPreferences(), + 'tune', + ) + }, [application.commands, application.preferencesController]) const [setElement] = usePaneSwipeGesture('right', async (element) => { const animation = element.animate( diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/PanelSettingsSection.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/PanelSettingsSection.tsx index 3793ac25d..edafed530 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/PanelSettingsSection.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/PanelSettingsSection.tsx @@ -2,23 +2,23 @@ import { TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMM import { useMemo } from 'react' import { observer } from 'mobx-react-lite' import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider' -import { useCommandService } from '../CommandProvider' +import { useKeyboardService } from '../KeyboardServiceProvider' import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem' const PanelSettingsSection = () => { const { isListPaneCollapsed, isNavigationPaneCollapsed, toggleListPane, toggleNavigationPane } = useResponsiveAppPane() - const commandService = useCommandService() + const keyboardService = useKeyboardService() const navigationShortcut = useMemo( - () => commandService.keyboardShortcutForCommand(TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND), - [commandService], + () => keyboardService.keyboardShortcutForCommand(TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND), + [keyboardService], ) const listShortcut = useMemo( - () => commandService.keyboardShortcutForCommand(TOGGLE_LIST_PANE_KEYBOARD_COMMAND), - [commandService], + () => keyboardService.keyboardShortcutForCommand(TOGGLE_LIST_PANE_KEYBOARD_COMMAND), + [keyboardService], ) return ( diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx index bce245c41..318ec8eaf 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx @@ -7,7 +7,7 @@ import { isMobileScreen } from '@/Utils' import { classNames } from '@standardnotes/utils' import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem' import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem' -import { useCommandService } from '../CommandProvider' +import { useKeyboardService } from '../KeyboardServiceProvider' import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services' import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator' import { useApplication } from '../ApplicationProvider' @@ -18,7 +18,7 @@ type Props = { const ThemesMenuButton: FunctionComponent = ({ uiFeature }) => { const application = useApplication() - const commandService = useCommandService() + const keyboardService = useKeyboardService() const premiumModal = usePremiumModal() const isThirdPartyTheme = useMemo( @@ -59,9 +59,9 @@ const ThemesMenuButton: FunctionComponent = ({ uiFeature }) => { const darkThemeShortcut = useMemo(() => { if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme) { - return commandService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND) + return keyboardService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND) } - }, [commandService, uiFeature.featureIdentifier]) + }, [keyboardService, uiFeature.featureIdentifier]) if (shouldHideButton) { return null diff --git a/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx b/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx index a5be89c1e..3545a861f 100644 --- a/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx +++ b/packages/web/src/javascripts/Components/StyledTooltip/StyledTooltip.tsx @@ -20,6 +20,7 @@ const StyledTooltip = ({ side, documentElement, closeOnClick = true, + portal = true, ...props }: { children: ReactNode @@ -41,7 +42,6 @@ const StyledTooltip = ({ hideTimeout: 0, skipTimeout: 0, open: forceOpen, - animated: true, type, }) @@ -156,9 +156,11 @@ const StyledTooltip = ({ return } - Object.assign(popoverElement.style, styles) + for (const [key, value] of Object.entries(styles)) { + popoverElement.style.setProperty(key, value) + } - if (!props.portal) { + if (!portal) { const adjustedStyles = getAdjustedStylesForNonPortalPopover( popoverElement, styles, diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/SearchPlugin/SearchPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/SearchPlugin/SearchPlugin.tsx index 55705d6fd..df54c7b51 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/SearchPlugin/SearchPlugin.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/SearchPlugin/SearchPlugin.tsx @@ -15,7 +15,7 @@ import { useLifecycleAnimation } from '../../../../Hooks/useLifecycleAnimation' import { classNames, debounce } from '@standardnotes/utils' import DecoratedInput from '../../../Input/DecoratedInput' import { searchInElement } from './searchInElement' -import { useCommandService } from '../../../CommandProvider' +import { useKeyboardService } from '../../../KeyboardServiceProvider' import { ArrowDownIcon, ArrowRightIcon, ArrowUpIcon, CloseIcon } from '@standardnotes/icons' import Button from '../../../Button/Button' import { canUseCSSHiglights, SearchHighlightRenderer, SearchHighlightRendererMethods } from './SearchHighlightRenderer' @@ -272,18 +272,18 @@ export function SearchPlugin() { } }, []) - const commandService = useCommandService() + const keyboardService = useKeyboardService() const searchToggleShortcut = useMemo( - () => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH)), - [commandService], + () => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH)), + [keyboardService], ) const toggleReplaceShortcut = useMemo( - () => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(SUPER_SEARCH_TOGGLE_REPLACE_MODE)), - [commandService], + () => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(SUPER_SEARCH_TOGGLE_REPLACE_MODE)), + [keyboardService], ) const caseSensitivityShortcut = useMemo( - () => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(SUPER_SEARCH_TOGGLE_CASE_SENSITIVE)), - [commandService], + () => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(SUPER_SEARCH_TOGGLE_CASE_SENSITIVE)), + [keyboardService], ) if (!isMounted) { @@ -447,7 +447,6 @@ export function SearchPlugin() { label="May lead to performance degradation, especially on large documents." className="!z-modal" showOnMobile - portal={false} >