Files
standardnotes-app/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx
T
Aman HarwaraandGitHub efba7c682d feat: Added command palette for quick actions and switching between items (#2933) [skip e2e]
* wip: command palette

* use code instead of key

* show recent items above commands

* refactor

* fix command

* add placeholder

* Tab/Shift-Tab to switch tabs

* Fix test

* Add menu item to general account menu

* if shortcut_id is available, use that as the id

* make toggle fn more stable

* small naming changes

* fix name

* Close open modals and popovers when opening command palette

* use stable ids + make sure selectedNotesCount only changes when the count actually changes

* display all commands, even ones in recents list
2025-09-25 10:06:09 -03:00

240 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { observer } from 'mobx-react-lite'
import Icon from '@/Components/Icon/Icon'
import { SyncQueueStrategy } from '@standardnotes/snjs'
import { STRING_GENERIC_SYNC_ERROR } from '@/Constants/Strings'
import { useCallback, useMemo, useState, FunctionComponent } from 'react'
import { AccountMenuPane } from './AccountMenuPane'
import Menu from '@/Components/Menu/Menu'
import MenuItem from '@/Components/Menu/MenuItem'
import WorkspaceSwitcherOption from './WorkspaceSwitcher/WorkspaceSwitcherOption'
import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { formatLastSyncDate } from '@/Utils/DateUtils'
import Spinner from '@/Components/Spinner/Spinner'
import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
import { useApplication } from '../ApplicationProvider'
import MenuSection from '../Menu/MenuSection'
import { TOGGLE_COMMAND_PALETTE, TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
type Props = {
mainApplicationGroup: WebApplicationGroup
setMenuPane: (pane: AccountMenuPane) => void
closeMenu: () => void
}
const iconClassName = `text-neutral mr-2 ${MenuItemIconSize}`
const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu, mainApplicationGroup }) => {
const application = useApplication()
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
const doSynchronization = useCallback(async () => {
setIsSyncingInProgress(true)
application.sync
.sync({
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
checkIntegrity: true,
})
.then((res) => {
if (res && (res as any).error) {
throw new Error()
} else {
setLastSyncDate(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
}
})
.catch(() => {
application.alerts.alert(STRING_GENERIC_SYNC_ERROR).catch(console.error)
})
.finally(() => {
setIsSyncingInProgress(false)
})
}, [application])
const user = useMemo(() => application.sessions.getUser(), [application])
const openPreferences = useCallback(() => {
application.accountMenuController.closeAccountMenu()
application.preferencesController.setCurrentPane('account')
application.preferencesController.openPreferences()
}, [application])
const openHelp = useCallback(() => {
application.accountMenuController.closeAccountMenu()
application.preferencesController.setCurrentPane('help-feedback')
application.preferencesController.openPreferences()
}, [application])
const openEmail = useCallback(() => {
const subject = 'Standard Notes Feedback'
const body = `App Version: ${application.version}`
application.device.openUrl(
`mailto:[email protected]?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`,
)
}, [application.device, application.version])
const signOut = useCallback(() => {
application.accountMenuController.setSigningOut(true)
}, [application])
const activateRegisterPane = useCallback(() => {
setMenuPane(AccountMenuPane.Register)
}, [setMenuPane])
const activateSignInPane = useCallback(() => {
setMenuPane(AccountMenuPane.SignIn)
}, [setMenuPane])
const CREATE_ACCOUNT_INDEX = 1
const SWITCHER_INDEX = 0
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 (
<>
<div className="mb-1 mt-1 hidden items-center justify-between px-4 md:flex md:px-3">
<div className="text-lg font-bold lg:text-base">Account</div>
<div className="flex cursor-pointer" onClick={closeMenu}>
<Icon type="close" className="text-neutral" />
</div>
</div>
{user ? (
<>
<div className="mb-3 px-4 text-lg text-foreground md:px-3 lg:text-sm">
<div>You're signed in as:</div>
<div className="wrap my-0.5 font-bold">{user.email}</div>
<span className="text-neutral">{application.getHost.execute().getValue()}</span>
</div>
<div className="mb-2 flex items-start justify-between px-4 text-mobile-menu-item md:px-3 md:text-tablet-menu-item lg:text-menu-item">
{isSyncingInProgress ? (
<div className="flex items-center font-semibold text-info">
<Spinner className="mr-2 h-5 w-5" />
Syncing...
</div>
) : (
<div className="flex items-start">
<Icon type="check-circle" className={`mr-2 text-success ${MenuItemIconSize}`} />
<div>
<div className="font-semibold text-success">Last synced:</div>
<div className="text-text">{lastSyncDate}</div>
</div>
</div>
)}
<div className="flex cursor-pointer text-passive-1" onClick={doSynchronization}>
<Icon type="sync" className={`${MenuItemIconSize}`} />
</div>
</div>
</>
) : (
<>
<div className="mb-1 px-4 md:px-3">
<div className="mb-3 text-base text-foreground lg:text-sm">
Youre offline. Sign in to sync your notes and preferences across all your devices and enable end-to-end
encryption.
</div>
<div className="flex items-center text-passive-1">
<Icon type="cloud-off" className={`mr-2 ${MenuItemIconSize}`} />
<span className="text-lg font-semibold lg:text-sm">Offline</span>
</div>
</div>
</>
)}
<Menu
a11yLabel="General account menu"
closeMenu={closeMenu}
initialFocus={!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX}
>
<MenuSection className="md:border-t md:pt-2">
<WorkspaceSwitcherOption mainApplicationGroup={mainApplicationGroup} />
</MenuSection>
<MenuSection>
{user ? (
<MenuItem onClick={openPreferences}>
<Icon type="user" className={iconClassName} />
Account settings
</MenuItem>
) : (
<>
<MenuItem onClick={activateRegisterPane}>
<Icon type="user" className={iconClassName} />
Create free account
</MenuItem>
<MenuItem onClick={activateSignInPane}>
<Icon type="signIn" className={iconClassName} />
Sign in
</MenuItem>
</>
)}
<MenuItem
onClick={() => {
application.importModalController.setIsVisible(true)
application.accountMenuController.closeAccountMenu()
}}
>
<Icon type="archive" className={iconClassName} />
Import
</MenuItem>
{application.isNativeMobileWeb() && (
<MenuItem onClick={openEmail}>
<Icon type="email-filled" className={iconClassName} />
Email us
</MenuItem>
)}
<MenuItem className="justify-between" onClick={openHelp}>
<div className="flex items-center">
<Icon type="help" className={iconClassName} />
Help &amp; feedback
</div>
<span className="text-neutral">v{application.version}</span>
</MenuItem>
{!isMobilePlatform(application.platform) && (
<>
<MenuItem
onClick={() => {
application.keyboardService.triggerCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
}}
>
<Icon type="keyboard" className={iconClassName} />
Keyboard shortcuts
{keyboardShortcutsHelpShortcut && (
<KeyboardShortcutIndicator shortcut={keyboardShortcutsHelpShortcut} className="ml-auto" />
)}
</MenuItem>
<MenuItem
onClick={() => {
application.keyboardService.triggerCommand(TOGGLE_COMMAND_PALETTE)
}}
>
<Icon type="info" className={iconClassName} />
Command palette
{commandPaletteShortcut && (
<KeyboardShortcutIndicator shortcut={commandPaletteShortcut} className="ml-auto" />
)}
</MenuItem>
</>
)}
</MenuSection>
{user ? (
<MenuSection>
<MenuItem onClick={signOut}>
<Icon type="signOut" className={iconClassName} />
Sign out workspace
</MenuItem>
</MenuSection>
) : null}
</Menu>
</>
)
}
export default observer(GeneralAccountMenu)