From 62cf34e8947831017b2d4d586e7d902aad328bf9 Mon Sep 17 00:00:00 2001 From: Mo Date: Mon, 16 May 2022 21:14:18 -0500 Subject: [PATCH] chore: app group optimizations (#1027) --- app/assets/javascripts/@types/Svg.d.ts | 4 + app/assets/javascripts/@types/modules.ts | 3 - app/assets/javascripts/App.tsx | 55 ++- .../Components/Abstract/PureComponent.tsx | 10 + .../AccountMenu/AdvancedOptions.tsx | 46 ++- .../AccountMenu/ConfirmPassword.tsx | 100 ++--- .../Components/AccountMenu/CreateAccount.tsx | 83 ++-- .../AccountMenu/GeneralAccountMenu.tsx | 72 ++-- .../Components/AccountMenu/SignIn.tsx | 81 ++-- .../WorkspaceSwitcher/WorkspaceMenuItem.tsx | 21 +- .../WorkspaceSwitcherMenu.tsx | 32 +- .../WorkspaceSwitcherOption.tsx | 6 +- .../Components/AccountMenu/index.tsx | 44 +- .../Components/ApplicationGroupView/index.tsx | 124 +++++- .../Components/ApplicationView/index.tsx | 377 +++++++++--------- .../AttachedFilesButton.tsx | 32 +- .../AttachedFilesPopover.tsx | 10 +- .../AttachedFilesPopover/PopoverFileItem.tsx | 6 +- .../PopoverFileItemAction.tsx | 8 +- .../PopoverFileSubmenu.tsx | 8 +- .../ChallengeModal/ChallengeModal.tsx | 34 +- .../ChallengeModal/ChallengePrompt.tsx | 2 +- .../LockscreenWorkspaceSwitcher.tsx | 6 +- .../ChangeEditor/ChangeEditorButton.tsx | 7 +- .../Components/Files/FilePreviewInfoPanel.tsx | 4 +- .../Components/Files/FilePreviewModal.tsx | 3 +- .../Components/Files/PreviewComponent.tsx | 4 +- .../javascripts/Components/Footer/index.tsx | 13 +- .../javascripts/Components/Icon/index.tsx | 2 +- .../Components/Input/DecoratedInput.tsx | 2 + .../javascripts/Components/Menu/Menu.tsx | 78 ++-- .../javascripts/Components/Menu/MenuItem.tsx | 1 - .../MultipleSelectedNotes/index.tsx | 2 +- .../Components/NoAccountWarning/index.tsx | 26 +- .../Components/NoteGroupView/index.tsx | 26 +- .../Components/NoteTags/NoteTag.tsx | 108 ++--- .../Components/NoteTags/NoteTagsContainer.tsx | 7 +- .../Components/NoteView/NoteView.tsx | 27 +- .../Components/NotesContextMenu/index.tsx | 2 +- .../Components/NotesList/NotesListItem.tsx | 2 +- .../NotesList/NotesListOptionsMenu.tsx | 69 ++-- .../Components/NotesList/index.tsx | 104 +++-- .../Components/NotesOptions/AddTagOption.tsx | 4 +- .../NotesOptions/ChangeEditorOption.tsx | 6 +- .../NotesOptions/ListedActionsOption.tsx | 51 +-- .../Components/NotesOptions/NotesOptions.tsx | 27 +- .../Components/NotesView/index.tsx | 80 ++-- .../Components/OtherSessionsSignOut/index.tsx | 7 +- .../Components/PinNoteButton/index.tsx | 12 +- .../Panes/Account/Authentication.tsx | 2 +- .../Preferences/Panes/Account/Files.tsx | 2 +- .../Panes/Backups/Files/BackupsDropZone.tsx | 30 +- .../Panes/Backups/Files/FileBackups.tsx | 2 +- .../Preferences/Panes/General/Defaults.tsx | 2 +- .../Preferences/Panes/General/Labs.tsx | 2 +- .../Panes/TwoFactorAuth/AuthAppInfoPopup.tsx | 7 +- .../Components/PremiumFeaturesModal/index.tsx | 8 +- .../PurchaseFlow/Panes/CreateAccount.tsx | 2 +- .../Components/PurchaseFlow/Panes/SignIn.tsx | 2 +- .../PurchaseFlow/PurchaseFlowView.tsx | 2 +- .../QuickSettingsMenu/ThemesMenuButton.tsx | 25 +- .../Components/QuickSettingsMenu/index.tsx | 69 ++-- .../HistoryListContainer.tsx | 5 +- .../RevisionContentLocked.tsx | 2 +- .../RevisionHistoryModalWrapper.tsx | 4 +- .../SelectedRevisionContent.tsx | 4 +- .../Components/SearchOptions/index.tsx | 5 +- .../Components/Shared/AccordionItem.tsx | 2 +- .../Components/Shared/ModalDialog.tsx | 4 +- .../javascripts/Components/Switch/index.tsx | 5 +- .../TagAutocomplete/AutocompleteTagHint.tsx | 44 +- .../TagAutocomplete/AutocompleteTagInput.tsx | 2 +- .../TagAutocomplete/AutocompleteTagResult.tsx | 2 +- .../Components/Tags/SmartViewsList.tsx | 7 +- .../Components/Tags/TagContextMenu.tsx | 15 +- .../javascripts/Components/Tags/TagsList.tsx | 38 +- .../Components/Tags/TagsListItem.tsx | 4 +- .../Components/Tags/TagsSection.tsx | 12 +- app/assets/javascripts/Device/WebDevice.ts | 15 + .../javascripts/Device/WebOrDesktopDevice.ts | 8 + .../javascripts/Hooks/usePremiumModal.tsx | 73 ++-- app/assets/javascripts/Services/IOService.ts | 5 +- .../javascripts/Services/ThemeManager.ts | 3 + .../UIModels/AppState/AbstractState.ts | 23 ++ .../UIModels/AppState/AccountMenuState.ts | 17 +- .../javascripts/UIModels/AppState/AppState.ts | 104 +++-- .../UIModels/AppState/FeaturesState.ts | 22 +- .../AppState/FilePreviewModalState.ts | 10 +- .../UIModels/AppState/FilesState.ts | 12 +- .../AppState/NoAccountWarningState.ts | 11 +- .../UIModels/AppState/NoteTagsState.ts | 18 +- .../UIModels/AppState/NotesState.ts | 24 +- .../UIModels/AppState/NotesViewState.ts | 45 ++- .../UIModels/AppState/PurchaseFlowState.ts | 7 +- .../UIModels/AppState/SearchOptionsState.ts | 7 +- .../UIModels/AppState/SubscriptionState.ts | 23 +- .../UIModels/AppState/TagsState.ts | 22 +- .../javascripts/UIModels/Application.ts | 13 +- .../javascripts/UIModels/ApplicationGroup.ts | 85 ++-- .../{DragTypeCheck.tsx => DragTypeCheck.ts} | 16 +- app/assets/javascripts/Utils/PreactUtils.ts | 10 + app/assets/javascripts/Utils/Utils.ts | 10 + app/assets/javascripts/tsconfig.json | 2 +- app/assets/svg/il-history-locked.svg | 26 -- package.json | 6 +- webpack.config.js | 25 +- webpack.prod.js | 12 +- yarn.lock | 181 +++++---- 108 files changed, 1796 insertions(+), 1187 deletions(-) create mode 100644 app/assets/javascripts/@types/Svg.d.ts delete mode 100644 app/assets/javascripts/@types/modules.ts create mode 100644 app/assets/javascripts/UIModels/AppState/AbstractState.ts rename app/assets/javascripts/Utils/{DragTypeCheck.tsx => DragTypeCheck.ts} (52%) create mode 100644 app/assets/javascripts/Utils/PreactUtils.ts delete mode 100644 app/assets/svg/il-history-locked.svg diff --git a/app/assets/javascripts/@types/Svg.d.ts b/app/assets/javascripts/@types/Svg.d.ts new file mode 100644 index 000000000..9b9471da0 --- /dev/null +++ b/app/assets/javascripts/@types/Svg.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: any + export default content +} diff --git a/app/assets/javascripts/@types/modules.ts b/app/assets/javascripts/@types/modules.ts deleted file mode 100644 index 8b139eba2..000000000 --- a/app/assets/javascripts/@types/modules.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module '*.svg' { - export default function SvgComponent(props: React.SVGProps): JSX.Element -} diff --git a/app/assets/javascripts/App.tsx b/app/assets/javascripts/App.tsx index a4df70848..1222efb87 100644 --- a/app/assets/javascripts/App.tsx +++ b/app/assets/javascripts/App.tsx @@ -13,18 +13,30 @@ declare global { startApplication?: StartApplication websocketUrl: string electronAppVersion?: string + webClient?: DesktopManagerInterface + + application?: WebApplication + mainApplicationGroup?: ApplicationGroup } } import { IsWebPlatform, WebAppVersion } from '@/Version' -import { Runtime, SNLog } from '@standardnotes/snjs' +import { DesktopManagerInterface, SNLog } from '@standardnotes/snjs' import { render } from 'preact' import { ApplicationGroupView } from './Components/ApplicationGroupView' import { WebDevice } from './Device/WebDevice' import { StartApplication } from './Device/StartApplication' import { ApplicationGroup } from './UIModels/ApplicationGroup' -import { isDev } from './Utils' import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice' +import { WebApplication } from './UIModels/Application' +import { unmountComponentAtRoot } from './Utils/PreactUtils' + +let keyCount = 0 +const getKey = () => { + return keyCount++ +} + +const RootId = 'app-group-root' const startApplication: StartApplication = async function startApplication( defaultSyncServerHost: string, @@ -35,34 +47,41 @@ const startApplication: StartApplication = async function startApplication( SNLog.onLog = console.log SNLog.onError = console.error - const mainApplicationGroup = new ApplicationGroup( - defaultSyncServerHost, - device, - enableUnfinishedFeatures ? Runtime.Dev : Runtime.Prod, - webSocketUrl, - ) - - if (isDev) { - Object.defineProperties(window, { - application: { - get: () => mainApplicationGroup.primaryApplication, - }, - }) + const onDestroy = () => { + const root = document.getElementById(RootId) as HTMLElement + unmountComponentAtRoot(root) + root.remove() + renderApp() } const renderApp = () => { + const root = document.createElement('div') + root.id = RootId + + const parentNode = document.body.appendChild(root) + render( - , - document.body.appendChild(document.createElement('div')), + , + parentNode, ) } const domReady = document.readyState === 'complete' || document.readyState === 'interactive' + if (domReady) { renderApp() } else { - window.addEventListener('DOMContentLoaded', () => { + window.addEventListener('DOMContentLoaded', function callback() { renderApp() + + window.removeEventListener('DOMContentLoaded', callback) }) } } diff --git a/app/assets/javascripts/Components/Abstract/PureComponent.tsx b/app/assets/javascripts/Components/Abstract/PureComponent.tsx index 74041cef2..1d562d7ec 100644 --- a/app/assets/javascripts/Components/Abstract/PureComponent.tsx +++ b/app/assets/javascripts/Components/Abstract/PureComponent.tsx @@ -31,6 +31,9 @@ export abstract class PureComponent

{ + if (!this.application) { + return + } + this.onAppEvent(eventName, data) + if (eventName === ApplicationEvent.Started) { await this.onAppStart() } else if (eventName === ApplicationEvent.Launched) { diff --git a/app/assets/javascripts/Components/AccountMenu/AdvancedOptions.tsx b/app/assets/javascripts/Components/AccountMenu/AdvancedOptions.tsx index 469f7a2e7..7aa70e5dc 100644 --- a/app/assets/javascripts/Components/AccountMenu/AdvancedOptions.tsx +++ b/app/assets/javascripts/Components/AccountMenu/AdvancedOptions.tsx @@ -2,7 +2,7 @@ import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'preact' -import { useEffect, useState } from 'preact/hooks' +import { useCallback, useEffect, useState } from 'preact/hooks' import { Checkbox } from '@/Components/Checkbox' import { DecoratedInput } from '@/Components/Input/DecoratedInput' import { Icon } from '@/Components/Icon' @@ -51,38 +51,44 @@ export const AdvancedOptions: FunctionComponent = observer( onPrivateWorkspaceChange?.(isPrivateWorkspace) }, [isPrivateWorkspace, onPrivateWorkspaceChange]) - const handleIsPrivateWorkspaceChange = () => { + const handleIsPrivateWorkspaceChange = useCallback(() => { setIsPrivateWorkspace(!isPrivateWorkspace) - } + }, [isPrivateWorkspace]) - const handlePrivateWorkspaceNameChange = (name: string) => { + const handlePrivateWorkspaceNameChange = useCallback((name: string) => { setPrivateWorkspaceName(name) - } + }, []) - const handlePrivateWorkspaceUserphraseChange = (userphrase: string) => { + const handlePrivateWorkspaceUserphraseChange = useCallback((userphrase: string) => { setPrivateWorkspaceUserphrase(userphrase) - } + }, []) - const handleServerOptionChange = (e: Event) => { - if (e.target instanceof HTMLInputElement) { - setEnableServerOption(e.target.checked) - } - } + const handleServerOptionChange = useCallback( + (e: Event) => { + if (e.target instanceof HTMLInputElement) { + setEnableServerOption(e.target.checked) + } + }, + [setEnableServerOption], + ) - const handleSyncServerChange = (server: string) => { - setServer(server) - application.setCustomHost(server).catch(console.error) - } + const handleSyncServerChange = useCallback( + (server: string) => { + setServer(server) + application.setCustomHost(server).catch(console.error) + }, + [application, setServer], + ) - const handleStrictSigninChange = () => { + const handleStrictSigninChange = useCallback(() => { const newValue = !isStrictSignin setIsStrictSignin(newValue) onStrictSignInChange?.(newValue) - } + }, [isStrictSignin, onStrictSignInChange]) - const toggleShowAdvanced = () => { + const toggleShowAdvanced = useCallback(() => { setShowAdvanced(!showAdvanced) - } + }, [showAdvanced]) return ( <> diff --git a/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx b/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx index 66afcc84e..67774a613 100644 --- a/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx +++ b/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx @@ -3,7 +3,7 @@ import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'preact' -import { useEffect, useRef, useState } from 'preact/hooks' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { AccountMenuPane } from '.' import { Button } from '@/Components/Button/Button' import { Checkbox } from '@/Components/Checkbox' @@ -34,63 +34,69 @@ export const ConfirmPassword: FunctionComponent = observer( passwordInputRef.current?.focus() }, []) - const handlePasswordChange = (text: string) => { + const handlePasswordChange = useCallback((text: string) => { setConfirmPassword(text) - } + }, []) - const handleEphemeralChange = () => { + const handleEphemeralChange = useCallback(() => { setIsEphemeral(!isEphemeral) - } + }, [isEphemeral]) - const handleShouldMergeChange = () => { + const handleShouldMergeChange = useCallback(() => { setShouldMergeLocal(!shouldMergeLocal) - } + }, [shouldMergeLocal]) - const handleKeyDown = (e: KeyboardEvent) => { - if (error.length) { - setError('') - } - if (e.key === 'Enter') { - handleConfirmFormSubmit(e) - } - } + const handleConfirmFormSubmit = useCallback( + (e: Event) => { + e.preventDefault() - const handleConfirmFormSubmit = (e: Event) => { - e.preventDefault() + if (!password) { + passwordInputRef.current?.focus() + return + } - if (!password) { - passwordInputRef.current?.focus() - return - } + if (password === confirmPassword) { + setIsRegistering(true) + application + .register(email, password, isEphemeral, shouldMergeLocal) + .then((res) => { + if (res.error) { + throw new Error(res.error.message) + } + appState.accountMenu.closeAccountMenu() + appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu) + }) + .catch((err) => { + console.error(err) + setError(err.message) + }) + .finally(() => { + setIsRegistering(false) + }) + } else { + setError(STRING_NON_MATCHING_PASSWORDS) + setConfirmPassword('') + passwordInputRef.current?.focus() + } + }, + [appState, application, confirmPassword, email, isEphemeral, password, shouldMergeLocal], + ) - if (password === confirmPassword) { - setIsRegistering(true) - application - .register(email, password, isEphemeral, shouldMergeLocal) - .then((res) => { - if (res.error) { - throw new Error(res.error.message) - } - appState.accountMenu.closeAccountMenu() - appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu) - }) - .catch((err) => { - console.error(err) - setError(err.message) - }) - .finally(() => { - setIsRegistering(false) - }) - } else { - setError(STRING_NON_MATCHING_PASSWORDS) - setConfirmPassword('') - passwordInputRef.current?.focus() - } - } + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (error.length) { + setError('') + } + if (e.key === 'Enter') { + handleConfirmFormSubmit(e) + } + }, + [handleConfirmFormSubmit, error], + ) - const handleGoBack = () => { + const handleGoBack = useCallback(() => { setMenuPane(AccountMenuPane.Register) - } + }, [setMenuPane]) return ( <> diff --git a/app/assets/javascripts/Components/AccountMenu/CreateAccount.tsx b/app/assets/javascripts/Components/AccountMenu/CreateAccount.tsx index 91127a0d3..23ee0be3b 100644 --- a/app/assets/javascripts/Components/AccountMenu/CreateAccount.tsx +++ b/app/assets/javascripts/Components/AccountMenu/CreateAccount.tsx @@ -2,7 +2,7 @@ import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'preact' -import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks' +import { StateUpdater, useCallback, useEffect, useRef, useState } from 'preact/hooks' import { AccountMenuPane } from '.' import { Button } from '@/Components/Button/Button' import { DecoratedInput } from '@/Components/Input/DecoratedInput' @@ -33,50 +33,65 @@ export const CreateAccount: FunctionComponent = observer( } }, []) - const handleEmailChange = (text: string) => { - setEmail(text) - } + const handleEmailChange = useCallback( + (text: string) => { + setEmail(text) + }, + [setEmail], + ) - const handlePasswordChange = (text: string) => { - setPassword(text) - } + const handlePasswordChange = useCallback( + (text: string) => { + setPassword(text) + }, + [setPassword], + ) - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - handleRegisterFormSubmit(e) - } - } + const handleRegisterFormSubmit = useCallback( + (e: Event) => { + e.preventDefault() - const handleRegisterFormSubmit = (e: Event) => { - e.preventDefault() + if (!email || email.length === 0) { + emailInputRef.current?.focus() + return + } - if (!email || email.length === 0) { - emailInputRef.current?.focus() - return - } + if (!password || password.length === 0) { + passwordInputRef.current?.focus() + return + } - if (!password || password.length === 0) { - passwordInputRef.current?.focus() - return - } + setEmail(email) + setPassword(password) + setMenuPane(AccountMenuPane.ConfirmPassword) + }, + [email, password, setPassword, setMenuPane, setEmail], + ) - setEmail(email) - setPassword(password) - setMenuPane(AccountMenuPane.ConfirmPassword) - } + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleRegisterFormSubmit(e) + } + }, + [handleRegisterFormSubmit], + ) - const handleClose = () => { + const handleClose = useCallback(() => { setMenuPane(AccountMenuPane.GeneralMenu) setEmail('') setPassword('') - } + }, [setEmail, setMenuPane, setPassword]) - const onPrivateWorkspaceChange = (isPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => { - setIsPrivateWorkspace(isPrivateWorkspace) - if (isPrivateWorkspace && privateWorkspaceIdentifier) { - setEmail(privateWorkspaceIdentifier) - } - } + const onPrivateWorkspaceChange = useCallback( + (isPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => { + setIsPrivateWorkspace(isPrivateWorkspace) + if (isPrivateWorkspace && privateWorkspaceIdentifier) { + setEmail(privateWorkspaceIdentifier) + } + }, + [setEmail], + ) return ( <> diff --git a/app/assets/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx b/app/assets/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx index 64e537a32..48f6c2509 100644 --- a/app/assets/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx +++ b/app/assets/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx @@ -5,7 +5,7 @@ import { Icon } from '@/Components/Icon' import { formatLastSyncDate } from '@/Components/Preferences/Panes/Account/Sync' import { SyncQueueStrategy } from '@standardnotes/snjs' import { STRING_GENERIC_SYNC_ERROR } from '@/Strings' -import { useState } from 'preact/hooks' +import { useCallback, useMemo, useState } from 'preact/hooks' import { AccountMenuPane } from '.' import { FunctionComponent } from 'preact' import { Menu } from '@/Components/Menu/Menu' @@ -28,7 +28,7 @@ export const GeneralAccountMenu: FunctionComponent = observer( const [isSyncingInProgress, setIsSyncingInProgress] = useState(false) const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date)) - const doSynchronization = async () => { + const doSynchronization = useCallback(async () => { setIsSyncingInProgress(true) application.sync @@ -49,9 +49,33 @@ export const GeneralAccountMenu: FunctionComponent = observer( .finally(() => { setIsSyncingInProgress(false) }) - } + }, [application]) - const user = application.getUser() + const user = useMemo(() => application.getUser(), [application]) + + const openPreferences = useCallback(() => { + appState.accountMenu.closeAccountMenu() + appState.preferences.setCurrentPane('account') + appState.preferences.openPreferences() + }, [appState]) + + const openHelp = useCallback(() => { + appState.accountMenu.closeAccountMenu() + appState.preferences.setCurrentPane('help-feedback') + appState.preferences.openPreferences() + }, [appState]) + + const signOut = useCallback(() => { + appState.accountMenu.setSigningOut(true) + }, [appState]) + + const activateRegisterPane = useCallback(() => { + setMenuPane(AccountMenuPane.Register) + }, [setMenuPane]) + + const activateSignInPane = useCallback(() => { + setMenuPane(AccountMenuPane.SignIn) + }, [setMenuPane]) const CREATE_ACCOUNT_INDEX = 1 const SWITCHER_INDEX = 0 @@ -115,48 +139,23 @@ export const GeneralAccountMenu: FunctionComponent = observer( {user ? ( - { - appState.accountMenu.closeAccountMenu() - appState.preferences.setCurrentPane('account') - appState.preferences.openPreferences() - }} - > + Account settings ) : ( <> - { - setMenuPane(AccountMenuPane.Register) - }} - > + Create free account - { - setMenuPane(AccountMenuPane.SignIn) - }} - > + Sign in )} - { - appState.accountMenu.closeAccountMenu() - appState.preferences.setCurrentPane('help-feedback') - appState.preferences.openPreferences() - }} - > +

Help & feedback @@ -166,12 +165,7 @@ export const GeneralAccountMenu: FunctionComponent = observer( {user ? ( <> - { - appState.accountMenu.setSigningOut(true) - }} - > + Sign out workspace diff --git a/app/assets/javascripts/Components/AccountMenu/SignIn.tsx b/app/assets/javascripts/Components/AccountMenu/SignIn.tsx index 20ce1e013..f03e0949e 100644 --- a/app/assets/javascripts/Components/AccountMenu/SignIn.tsx +++ b/app/assets/javascripts/Components/AccountMenu/SignIn.tsx @@ -44,36 +44,39 @@ export const SignInPane: FunctionComponent = observer(({ application, app } }, []) - const resetInvalid = () => { + const resetInvalid = useCallback(() => { if (error.length) { setError('') } - } + }, [setError, error]) - const handleEmailChange = (text: string) => { + const handleEmailChange = useCallback((text: string) => { setEmail(text) - } + }, []) - const handlePasswordChange = (text: string) => { - if (error.length) { - setError('') - } - setPassword(text) - } + const handlePasswordChange = useCallback( + (text: string) => { + if (error.length) { + setError('') + } + setPassword(text) + }, + [setPassword, error], + ) - const handleEphemeralChange = () => { + const handleEphemeralChange = useCallback(() => { setIsEphemeral(!isEphemeral) - } + }, [isEphemeral]) - const handleStrictSigninChange = () => { + const handleStrictSigninChange = useCallback(() => { setIsStrictSignin(!isStrictSignin) - } + }, [isStrictSignin]) - const handleShouldMergeChange = () => { + const handleShouldMergeChange = useCallback(() => { setShouldMergeLocal(!shouldMergeLocal) - } + }, [shouldMergeLocal]) - const signIn = () => { + const signIn = useCallback(() => { setIsSigningIn(true) emailInputRef?.current?.blur() passwordInputRef?.current?.blur() @@ -95,13 +98,7 @@ export const SignInPane: FunctionComponent = observer(({ application, app .finally(() => { setIsSigningIn(false) }) - } - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - handleSignInFormSubmit(e) - } - } + }, [appState, application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal]) const onPrivateWorkspaceChange = useCallback( (newIsPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => { @@ -113,21 +110,33 @@ export const SignInPane: FunctionComponent = observer(({ application, app [setEmail], ) - const handleSignInFormSubmit = (e: Event) => { - e.preventDefault() + const handleSignInFormSubmit = useCallback( + (e: Event) => { + e.preventDefault() - if (!email || email.length === 0) { - emailInputRef?.current?.focus() - return - } + if (!email || email.length === 0) { + emailInputRef?.current?.focus() + return + } - if (!password || password.length === 0) { - passwordInputRef?.current?.focus() - return - } + if (!password || password.length === 0) { + passwordInputRef?.current?.focus() + return + } - signIn() - } + signIn() + }, + [email, password, signIn], + ) + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleSignInFormSubmit(e) + } + }, + [handleSignInFormSubmit], + ) return ( <> diff --git a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx index 2e0bdb9a7..663dd919a 100644 --- a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx +++ b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx @@ -1,9 +1,9 @@ import { Icon } from '@/Components/Icon' import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem' import { KeyboardKey } from '@/Services/IOService' -import { ApplicationDescriptor } from '@standardnotes/snjs/dist/@types' +import { ApplicationDescriptor } from '@standardnotes/snjs' import { FunctionComponent } from 'preact' -import { useEffect, useRef, useState } from 'preact/hooks' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' type Props = { descriptor: ApplicationDescriptor @@ -29,17 +29,20 @@ export const WorkspaceMenuItem: FunctionComponent = ({ } }, [isRenaming]) - const handleInputKeyDown = (event: KeyboardEvent) => { + const handleInputKeyDown = useCallback((event: KeyboardEvent) => { if (event.key === KeyboardKey.Enter) { inputRef.current?.blur() } - } + }, []) - const handleInputBlur = (event: FocusEvent) => { - const name = (event.target as HTMLInputElement).value - renameDescriptor(name) - setIsRenaming(false) - } + const handleInputBlur = useCallback( + (event: FocusEvent) => { + const name = (event.target as HTMLInputElement).value + renameDescriptor(name) + setIsRenaming(false) + }, + [renameDescriptor], + ) return ( = observer( - ({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }) => { + ({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }: Props) => { const [applicationDescriptors, setApplicationDescriptors] = useState([]) useEffect(() => { - const removeAppGroupObserver = mainApplicationGroup.addApplicationChangeObserver(() => { - const applicationDescriptors = mainApplicationGroup.getDescriptors() - setApplicationDescriptors(applicationDescriptors) + const applicationDescriptors = mainApplicationGroup.getDescriptors() + setApplicationDescriptors(applicationDescriptors) + + const removeAppGroupObserver = mainApplicationGroup.addEventObserver((event) => { + if (event === ApplicationGroupEvent.DescriptorsDataChanged) { + const applicationDescriptors = mainApplicationGroup.getDescriptors() + setApplicationDescriptors(applicationDescriptors) + } }) return () => { @@ -42,20 +47,21 @@ export const WorkspaceSwitcherMenu: FunctionComponent = observer( return } mainApplicationGroup.signOutAllWorkspaces().catch(console.error) - }, [mainApplicationGroup, appState.application.alertService]) + }, [mainApplicationGroup, appState]) + + const destroyWorkspace = useCallback(() => { + appState.accountMenu.setSigningOut(true) + }, [appState]) return ( {applicationDescriptors.map((descriptor) => ( { - appState.accountMenu.setSigningOut(true) - }} - onClick={() => { - mainApplicationGroup.loadApplicationForDescriptor(descriptor) - }} + onDelete={destroyWorkspace} + onClick={() => void mainApplicationGroup.unloadCurrentAndActivateDescriptor(descriptor)} renameDescriptor={(label: string) => mainApplicationGroup.renameDescriptor(descriptor, label)} /> ))} @@ -64,7 +70,7 @@ export const WorkspaceSwitcherMenu: FunctionComponent = observer( { - mainApplicationGroup.addNewApplication() + void mainApplicationGroup.unloadCurrentAndCreateNewDescriptor() }} > diff --git a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx index 79e58b63f..a8881180c 100644 --- a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx +++ b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx @@ -4,7 +4,7 @@ import { AppState } from '@/UIModels/AppState' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'preact' -import { useEffect, useRef, useState } from 'preact/hooks' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { Icon } from '@/Components/Icon' import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu' @@ -19,7 +19,7 @@ export const WorkspaceSwitcherOption: FunctionComponent = observer(({ mai const [isOpen, setIsOpen] = useState(false) const [menuStyle, setMenuStyle] = useState() - const toggleMenu = () => { + const toggleMenu = useCallback(() => { if (!isOpen) { const menuPosition = calculateSubmenuStyle(buttonRef.current) if (menuPosition) { @@ -28,7 +28,7 @@ export const WorkspaceSwitcherOption: FunctionComponent = observer(({ mai } setIsOpen(!isOpen) - } + }, [isOpen, setIsOpen]) useEffect(() => { if (isOpen) { diff --git a/app/assets/javascripts/Components/AccountMenu/index.tsx b/app/assets/javascripts/Components/AccountMenu/index.tsx index d27726865..982231644 100644 --- a/app/assets/javascripts/Components/AccountMenu/index.tsx +++ b/app/assets/javascripts/Components/AccountMenu/index.tsx @@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite' import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' import { AppState } from '@/UIModels/AppState' import { WebApplication } from '@/UIModels/Application' -import { useRef, useState } from 'preact/hooks' +import { useCallback, useRef, useState } from 'preact/hooks' import { GeneralAccountMenu } from './GeneralAccountMenu' import { FunctionComponent } from 'preact' import { SignInPane } from './SignIn' @@ -80,26 +80,40 @@ const MenuPaneSelector: FunctionComponent = observer( export const AccountMenu: FunctionComponent = observer( ({ application, appState, onClickOutside, mainApplicationGroup }) => { - const { currentPane, setCurrentPane, shouldAnimateCloseMenu, closeAccountMenu } = appState.accountMenu + const { currentPane, shouldAnimateCloseMenu } = appState.accountMenu + + const closeAccountMenu = useCallback(() => { + appState.accountMenu.closeAccountMenu() + }, [appState]) + + const setCurrentPane = useCallback( + (pane: AccountMenuPane) => { + appState.accountMenu.setCurrentPane(pane) + }, + [appState], + ) const ref = useRef(null) useCloseOnClickOutside(ref, () => { onClickOutside() }) - const handleKeyDown: JSXInternal.KeyboardEventHandler = (event) => { - switch (event.key) { - case 'Escape': - if (currentPane === AccountMenuPane.GeneralMenu) { - closeAccountMenu() - } else if (currentPane === AccountMenuPane.ConfirmPassword) { - setCurrentPane(AccountMenuPane.Register) - } else { - setCurrentPane(AccountMenuPane.GeneralMenu) - } - break - } - } + const handleKeyDown: JSXInternal.KeyboardEventHandler = useCallback( + (event) => { + switch (event.key) { + case 'Escape': + if (currentPane === AccountMenuPane.GeneralMenu) { + closeAccountMenu() + } else if (currentPane === AccountMenuPane.ConfirmPassword) { + setCurrentPane(AccountMenuPane.Register) + } else { + setCurrentPane(AccountMenuPane.GeneralMenu) + } + break + } + }, + [closeAccountMenu, currentPane, setCurrentPane], + ) return (
diff --git a/app/assets/javascripts/Components/ApplicationGroupView/index.tsx b/app/assets/javascripts/Components/ApplicationGroupView/index.tsx index 570673766..102d2a953 100644 --- a/app/assets/javascripts/Components/ApplicationGroupView/index.tsx +++ b/app/assets/javascripts/Components/ApplicationGroupView/index.tsx @@ -2,40 +2,126 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { WebApplication } from '@/UIModels/Application' import { Component } from 'preact' import { ApplicationView } from '@/Components/ApplicationView' +import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice' +import { ApplicationGroupEvent, Runtime } from '@standardnotes/snjs' +import { unmountComponentAtNode, findDOMNode } from 'preact/compat' +import { DialogContent, DialogOverlay } from '@reach/dialog' +import { isDesktopApplication } from '@/Utils' + +type Props = { + server: string + device: WebOrDesktopDevice + enableUnfinished: boolean + websocketUrl: string + onDestroy: () => void +} type State = { activeApplication?: WebApplication -} - -type Props = { - mainApplicationGroup: ApplicationGroup + dealloced?: boolean + deviceDestroyed?: boolean } export class ApplicationGroupView extends Component { + applicationObserverRemover?: () => void + private group?: ApplicationGroup + private application?: WebApplication + constructor(props: Props) { super(props) - props.mainApplicationGroup.addApplicationChangeObserver(() => { - const activeApplication = props.mainApplicationGroup.primaryApplication as WebApplication - this.setState({ activeApplication }) + if (props.device.isDeviceDestroyed()) { + this.state = { + deviceDestroyed: true, + } + + return + } + + this.group = new ApplicationGroup( + props.server, + props.device, + props.enableUnfinished ? Runtime.Dev : Runtime.Prod, + props.websocketUrl, + ) + + window.mainApplicationGroup = this.group + + this.applicationObserverRemover = this.group.addEventObserver((event, data) => { + if (event === ApplicationGroupEvent.PrimaryApplicationSet) { + this.application = data?.primaryApplication as WebApplication + + this.setState({ activeApplication: this.application }) + } else if (event === ApplicationGroupEvent.DeviceWillRestart) { + this.setState({ dealloced: true }) + } }) - props.mainApplicationGroup.initialize().catch(console.error) + this.state = {} + + this.group.initialize().catch(console.error) + } + + deinit() { + this.application = undefined + + this.applicationObserverRemover?.() + ;(this.applicationObserverRemover as unknown) = undefined + + this.group?.deinit() + ;(this.group as unknown) = undefined + + this.setState({ dealloced: true, activeApplication: undefined }) + + const onDestroy = this.props.onDestroy + + const node = findDOMNode(this) as Element + unmountComponentAtNode(node) + + onDestroy() } render() { + const renderDialog = (message: string) => { + return ( + + + {message} + + + ) + } + + if (this.state.deviceDestroyed) { + const message = `Secure memory has destroyed this application instance. ${ + isDesktopApplication() + ? 'Restart the app to continue.' + : 'Close this browser tab and open a new one to continue.' + }` + + return renderDialog(message) + } + + if (this.state.dealloced) { + return renderDialog('Switching workspace...') + } + + if (!this.group || !this.state.activeApplication || this.state.activeApplication.dealloced) { + return null + } + return ( - <> - {this.state.activeApplication && ( -
- -
- )} - +
+ +
) } } diff --git a/app/assets/javascripts/Components/ApplicationView/index.tsx b/app/assets/javascripts/Components/ApplicationView/index.tsx index 0e5e9e115..80f6d1def 100644 --- a/app/assets/javascripts/Components/ApplicationView/index.tsx +++ b/app/assets/javascripts/Components/ApplicationView/index.tsx @@ -4,8 +4,7 @@ import { AppStateEvent, PanelResizedData } from '@/UIModels/AppState' import { ApplicationEvent, Challenge, PermissionDialog, removeFromArray } from '@standardnotes/snjs' import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants' import { alertDialog } from '@/Services/AlertService' -import { WebAppEvent, WebApplication } from '@/UIModels/Application' -import { PureComponent } from '@/Components/Abstract/PureComponent' +import { WebApplication } from '@/UIModels/Application' import { Navigation } from '@/Components/Navigation' import { NotesView } from '@/Components/NotesView' import { NoteGroupView } from '@/Components/NoteGroupView' @@ -15,7 +14,7 @@ import { PreferencesViewWrapper } from '@/Components/Preferences/PreferencesView import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal' import { NotesContextMenu } from '@/Components/NotesContextMenu' import { PurchaseFlowWrapper } from '@/Components/PurchaseFlow/PurchaseFlowWrapper' -import { render } from 'preact' +import { render, FunctionComponent } from 'preact' import { PermissionsModal } from '@/Components/PermissionsModal' import { RevisionHistoryModalWrapper } from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper' import { PremiumModalProvider } from '@/Hooks/usePremiumModal' @@ -23,199 +22,221 @@ import { ConfirmSignoutContainer } from '@/Components/ConfirmSignoutModal' import { TagsContextMenu } from '@/Components/Tags/TagContextMenu' import { ToastContainer } from '@standardnotes/stylekit' import { FilePreviewModal } from '../Files/FilePreviewModal' +import { useCallback, useEffect, useMemo, useState } from 'preact/hooks' +import { isStateDealloced } from '@/UIModels/AppState/AbstractState' type Props = { application: WebApplication mainApplicationGroup: ApplicationGroup } -type State = { - started?: boolean - launched?: boolean - needsUnlock?: boolean - appClass: string - challenges: Challenge[] -} +export const ApplicationView: FunctionComponent = ({ application, mainApplicationGroup }) => { + const platformString = getPlatformString() + const [appClass, setAppClass] = useState('') + const [launched, setLaunched] = useState(false) + const [needsUnlock, setNeedsUnlock] = useState(true) + const [challenges, setChallenges] = useState([]) + const [dealloced, setDealloced] = useState(false) -export class ApplicationView extends PureComponent { - public readonly platformString = getPlatformString() + const componentManager = application.componentManager + const appState = application.getAppState() - constructor(props: Props) { - super(props, props.application) - this.state = { - appClass: '', - challenges: [], - } - } + useEffect(() => { + setDealloced(application.dealloced) + }, [application.dealloced]) - override deinit() { - ;(this.application as unknown) = undefined - super.deinit() - } - - override componentDidMount(): void { - super.componentDidMount() - - void this.loadApplication() - } - - async loadApplication() { - const desktopService = this.application.getDesktopService() - if (desktopService) { - this.application.componentManager.setDesktopManager(desktopService) - } - - await this.application.prepareForLaunch({ - receiveChallenge: async (challenge) => { - const challenges = this.state.challenges.slice() - challenges.push(challenge) - this.setState({ challenges: challenges }) - }, - }) - - await this.application.launch() - } - - public removeChallenge = async (challenge: Challenge) => { - const challenges = this.state.challenges.slice() - removeFromArray(challenges, challenge) - this.setState({ challenges: challenges }) - } - - override async onAppStart() { - super.onAppStart().catch(console.error) - this.setState({ - started: true, - needsUnlock: this.application.hasPasscode(), - }) - - this.application.componentManager.presentPermissionsDialog = this.presentPermissionsDialog - } - - override async onAppLaunch() { - super.onAppLaunch().catch(console.error) - this.setState({ - launched: true, - needsUnlock: false, - }) - this.handleDemoSignInFromParams().catch(console.error) - } - - onUpdateAvailable() { - this.application.notifyWebEvent(WebAppEvent.NewUpdateAvailable) - } - - override async onAppEvent(eventName: ApplicationEvent) { - super.onAppEvent(eventName) - switch (eventName) { - case ApplicationEvent.LocalDatabaseReadError: - alertDialog({ - text: 'Unable to load local database. Please restart the app and try again.', - }).catch(console.error) - break - case ApplicationEvent.LocalDatabaseWriteError: - alertDialog({ - text: 'Unable to write to local database. Please restart the app and try again.', - }).catch(console.error) - break - } - } - - override async onAppStateEvent(eventName: AppStateEvent, data?: unknown) { - if (eventName === AppStateEvent.PanelResized) { - const { panel, collapsed } = data as PanelResizedData - let appClass = '' - if (panel === PANEL_NAME_NOTES && collapsed) { - appClass += 'collapsed-notes' - } - if (panel === PANEL_NAME_NAVIGATION && collapsed) { - appClass += ' collapsed-navigation' - } - this.setState({ appClass }) - } else if (eventName === AppStateEvent.WindowDidFocus) { - if (!(await this.application.isLocked())) { - this.application.sync.sync().catch(console.error) - } - } - } - - async handleDemoSignInFromParams() { - const token = getWindowUrlParams().get('demo-token') - if (!token || this.application.hasAccount()) { + useEffect(() => { + if (dealloced) { return } - await this.application.sessions.populateSessionFromDemoShareToken(token) - } + const desktopService = application.getDesktopService() - presentPermissionsDialog = (dialog: PermissionDialog) => { - render( - , - document.body.appendChild(document.createElement('div')), - ) - } - - override render() { - if (this.application['dealloced'] === true) { - console.error('Attempting to render dealloced application') - return
+ if (desktopService) { + application.componentManager.setDesktopManager(desktopService) } - const renderAppContents = !this.state.needsUnlock && this.state.launched + application + .prepareForLaunch({ + receiveChallenge: async (challenge) => { + const challengesCopy = challenges.slice() + challengesCopy.push(challenge) + setChallenges(challengesCopy) + }, + }) + .then(() => { + void application.launch() + }) + .catch(console.error) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [application, dealloced]) + const removeChallenge = useCallback( + (challenge: Challenge) => { + const challengesCopy = challenges.slice() + removeFromArray(challengesCopy, challenge) + setChallenges(challengesCopy) + }, + [challenges], + ) + + const presentPermissionsDialog = useCallback( + (dialog: PermissionDialog) => { + render( + , + document.body.appendChild(document.createElement('div')), + ) + }, + [application], + ) + + const onAppStart = useCallback(() => { + setNeedsUnlock(application.hasPasscode()) + componentManager.presentPermissionsDialog = presentPermissionsDialog + + return () => { + ;(componentManager.presentPermissionsDialog as unknown) = undefined + } + }, [application, componentManager, presentPermissionsDialog]) + + const handleDemoSignInFromParams = useCallback(() => { + const token = getWindowUrlParams().get('demo-token') + if (!token || application.hasAccount()) { + return + } + + void application.sessions.populateSessionFromDemoShareToken(token) + }, [application]) + + const onAppLaunch = useCallback(() => { + setLaunched(true) + setNeedsUnlock(false) + handleDemoSignInFromParams() + }, [handleDemoSignInFromParams]) + + useEffect(() => { + if (application.isStarted()) { + onAppStart() + } + + if (application.isLaunched()) { + onAppLaunch() + } + + const removeAppObserver = application.addEventObserver(async (eventName) => { + if (eventName === ApplicationEvent.Started) { + onAppStart() + } else if (eventName === ApplicationEvent.Launched) { + onAppLaunch() + } else if (eventName === ApplicationEvent.LocalDatabaseReadError) { + alertDialog({ + text: 'Unable to load local database. Please restart the app and try again.', + }).catch(console.error) + } else if (eventName === ApplicationEvent.LocalDatabaseWriteError) { + alertDialog({ + text: 'Unable to write to local database. Please restart the app and try again.', + }).catch(console.error) + } + }) + + return () => { + removeAppObserver() + } + }, [application, onAppLaunch, onAppStart]) + + useEffect(() => { + const removeObserver = application.getAppState().addObserver(async (eventName, data) => { + if (eventName === AppStateEvent.PanelResized) { + const { panel, collapsed } = data as PanelResizedData + let appClass = '' + if (panel === PANEL_NAME_NOTES && collapsed) { + appClass += 'collapsed-notes' + } + if (panel === PANEL_NAME_NAVIGATION && collapsed) { + appClass += ' collapsed-navigation' + } + setAppClass(appClass) + } else if (eventName === AppStateEvent.WindowDidFocus) { + if (!(await application.isLocked())) { + application.sync.sync().catch(console.error) + } + } + }) + + return () => { + removeObserver() + } + }, [application]) + + const renderAppContents = useMemo(() => { + return !needsUnlock && launched + }, [needsUnlock, launched]) + + const renderChallenges = useCallback(() => { return ( - -
- {renderAppContents && ( -
- - - -
- )} - {renderAppContents && ( - <> -
- - - - - )} - {this.state.challenges.map((challenge) => { - return ( -
- -
- ) - })} - {renderAppContents && ( - <> - - - - + {challenges.map((challenge) => { + return ( +
+ - - - - )} -
- +
+ ) + })} + ) + }, [appState, challenges, mainApplicationGroup, removeChallenge, application]) + + if (dealloced || isStateDealloced(appState)) { + return null } + + if (!renderAppContents) { + return renderChallenges() + } + + return ( + +
+
+ + + +
+ + <> +
+ + + + + + {renderChallenges()} + + <> + + + + + + + +
+
+ ) } diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx index 975c2b724..164fdf0a1 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx @@ -8,7 +8,7 @@ import { FunctionComponent } from 'preact' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { Icon } from '@/Components/Icon' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' -import { ChallengeReason, CollectionSort, ContentType, SNFile, SNNote } from '@standardnotes/snjs' +import { ChallengeReason, CollectionSort, ContentType, FileItem, SNNote } from '@standardnotes/snjs' import { confirmDialog } from '@/Services/AlertService' import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit' import { StreamingFileReader } from '@standardnotes/filepicker' @@ -17,6 +17,7 @@ import { AttachedFilesPopover } from './AttachedFilesPopover' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { PopoverTabs } from './PopoverTabs' import { isHandlingFileDrag } from '@/Utils/DragTypeCheck' +import { isStateDealloced } from '@/UIModels/AppState/AbstractState' type Props = { application: WebApplication @@ -25,9 +26,12 @@ type Props = { } export const AttachedFilesButton: FunctionComponent = observer( - ({ application, appState, onClickPreprocessing }) => { - const premiumModal = usePremiumModal() + ({ application, appState, onClickPreprocessing }: Props) => { + if (isStateDealloced(appState)) { + return null + } + const premiumModal = usePremiumModal() const note: SNNote | undefined = Object.values(appState.notes.selectedNotes)[0] const [open, setOpen] = useState(false) @@ -50,15 +54,15 @@ export const AttachedFilesButton: FunctionComponent = observer( }, [appState.filePreviewModal.isOpen, keepMenuOpen]) const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles) - const [allFiles, setAllFiles] = useState([]) - const [attachedFiles, setAttachedFiles] = useState([]) + const [allFiles, setAllFiles] = useState([]) + const [attachedFiles, setAttachedFiles] = useState([]) const attachedFilesCount = attachedFiles.length useEffect(() => { application.items.setDisplayOptions(ContentType.File, CollectionSort.Title, 'dsc') const unregisterFileStream = application.streamItems(ContentType.File, () => { - setAllFiles(application.items.getDisplayableItems(ContentType.File)) + setAllFiles(application.items.getDisplayableItems(ContentType.File)) if (note) { setAttachedFiles(application.items.getFilesForNote(note)) } @@ -106,7 +110,7 @@ export const AttachedFilesButton: FunctionComponent = observer( await toggleAttachedFilesMenu() }, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal]) - const deleteFile = async (file: SNFile) => { + const deleteFile = async (file: FileItem) => { const shouldDelete = await confirmDialog({ text: `Are you sure you want to permanently delete "${file.name}"?`, confirmButtonStyle: 'danger', @@ -125,12 +129,12 @@ export const AttachedFilesButton: FunctionComponent = observer( } } - const downloadFile = async (file: SNFile) => { + const downloadFile = async (file: FileItem) => { appState.files.downloadFile(file).catch(console.error) } const attachFileToNote = useCallback( - async (file: SNFile) => { + async (file: FileItem) => { if (!note) { addToast({ type: ToastType.Error, @@ -144,7 +148,7 @@ export const AttachedFilesButton: FunctionComponent = observer( [application.items, note], ) - const detachFileFromNote = async (file: SNFile) => { + const detachFileFromNote = async (file: FileItem) => { if (!note) { addToast({ type: ToastType.Error, @@ -155,8 +159,8 @@ export const AttachedFilesButton: FunctionComponent = observer( await application.items.disassociateFileWithNote(file, note) } - const toggleFileProtection = async (file: SNFile) => { - let result: SNFile | undefined + const toggleFileProtection = async (file: FileItem) => { + let result: FileItem | undefined if (file.protected) { keepMenuOpen(true) result = await application.mutator.unprotectFile(file) @@ -169,13 +173,13 @@ export const AttachedFilesButton: FunctionComponent = observer( return isProtected } - const authorizeProtectedActionForFile = async (file: SNFile, challengeReason: ChallengeReason) => { + const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => { const authorizedFiles = await application.protections.authorizeProtectedActionForFiles([file], challengeReason) const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) return isAuthorized } - const renameFile = async (file: SNFile, fileName: string) => { + const renameFile = async (file: FileItem, fileName: string) => { await application.items.renameFile(file, fileName) } diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx index aeeeb675a..6c978617e 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx @@ -1,8 +1,8 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' -import { SNFile } from '@standardnotes/snjs' -import { FilesIllustration } from '@standardnotes/stylekit' +import { FileItem } from '@standardnotes/snjs' +import { FilesIllustration } from '@standardnotes/icons' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'preact' import { StateUpdater, useRef, useState } from 'preact/hooks' @@ -15,8 +15,8 @@ import { PopoverTabs } from './PopoverTabs' type Props = { application: WebApplication appState: AppState - allFiles: SNFile[] - attachedFiles: SNFile[] + allFiles: FileItem[] + attachedFiles: FileItem[] closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void currentTab: PopoverTabs handleFileAction: (action: PopoverFileItemAction) => Promise @@ -126,7 +126,7 @@ export const AttachedFilesPopover: FunctionComponent = observer(
) : null} {filteredList.length > 0 ? ( - filteredList.map((file: SNFile) => { + filteredList.map((file: FileItem) => { return ( { } export type PopoverFileItemProps = { - file: SNFile + file: FileItem isAttachedToNote: boolean handleFileAction: (action: PopoverFileItemAction) => Promise getIconType(type: string): IconType @@ -40,7 +40,7 @@ export const PopoverFileItem: FunctionComponent = ({ } }, [isRenamingFile]) - const renameFile = async (file: SNFile, name: string) => { + const renameFile = async (file: FileItem, name: string) => { await handleFileAction({ type: PopoverFileItemActionType.RenameFile, payload: { diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx index 6f6cdf994..b1e9368e6 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx @@ -1,4 +1,4 @@ -import { SNFile } from '@standardnotes/snjs' +import { FileItem } from '@standardnotes/snjs' export enum PopoverFileItemActionType { AttachFileToNote, @@ -16,17 +16,17 @@ export type PopoverFileItemAction = PopoverFileItemActionType, PopoverFileItemActionType.RenameFile | PopoverFileItemActionType.ToggleFileProtection > - payload: SNFile + payload: FileItem } | { type: PopoverFileItemActionType.ToggleFileProtection - payload: SNFile + payload: FileItem callback: (isProtected: boolean) => void } | { type: PopoverFileItemActionType.RenameFile payload: { - file: SNFile + file: FileItem name: string } } diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx index d1d77a054..649ab903c 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx @@ -34,11 +34,11 @@ export const PopoverFileSubmenu: FunctionComponent = ({ }) const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen) - const closeMenu = () => { + const closeMenu = useCallback(() => { setIsMenuOpen(false) - } + }, []) - const toggleMenu = () => { + const toggleMenu = useCallback(() => { if (!isMenuOpen) { const menuPosition = calculateSubmenuStyle(menuButtonRef.current) if (menuPosition) { @@ -47,7 +47,7 @@ export const PopoverFileSubmenu: FunctionComponent = ({ } setIsMenuOpen(!isMenuOpen) - } + }, [isMenuOpen]) const recalculateMenuStyle = useCallback(() => { const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current) diff --git a/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx b/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx index c3210f5af..93c4586f4 100644 --- a/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx +++ b/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx @@ -8,7 +8,7 @@ import { ChallengeValue, removeFromArray, } from '@standardnotes/snjs' -import { ProtectedIllustration } from '@standardnotes/stylekit' +import { ProtectedIllustration } from '@standardnotes/icons' import { FunctionComponent } from 'preact' import { useCallback, useEffect, useState } from 'preact/hooks' import { Button } from '@/Components/Button/Button' @@ -31,7 +31,7 @@ type Props = { appState: AppState mainApplicationGroup: ApplicationGroup challenge: Challenge - onDismiss: (challenge: Challenge) => Promise + onDismiss?: (challenge: Challenge) => void } const validateValues = (values: ChallengeModalValues, prompts: ChallengePrompt[]): ChallengeModalValues | undefined => { @@ -77,7 +77,7 @@ export const ChallengeModal: FunctionComponent = ({ ) const shouldShowWorkspaceSwitcher = challenge.reason === ChallengeReason.ApplicationUnlock - const submit = async () => { + const submit = useCallback(() => { const validatedValues = validateValues(values, challenge.prompts) if (!validatedValues) { return @@ -87,12 +87,14 @@ export const ChallengeModal: FunctionComponent = ({ } setIsSubmitting(true) setIsProcessing(true) + const valuesToProcess: ChallengeValue[] = [] for (const inputValue of Object.values(validatedValues)) { const rawValue = inputValue.value const value = { prompt: inputValue.prompt, value: rawValue } valuesToProcess.push(value) } + const processingPrompts = valuesToProcess.map((v) => v.prompt) setIsProcessing(processingPrompts.length > 0) setProcessingPrompts(processingPrompts) @@ -109,7 +111,7 @@ export const ChallengeModal: FunctionComponent = ({ } setIsSubmitting(false) }, 50) - } + }, [application, challenge, isProcessing, isSubmitting, values]) const onValueChange = useCallback( (value: string | number, prompt: ChallengePrompt) => { @@ -121,12 +123,12 @@ export const ChallengeModal: FunctionComponent = ({ [values], ) - const cancelChallenge = () => { + const cancelChallenge = useCallback(() => { if (challenge.cancelable) { application.cancelChallenge(challenge) - onDismiss(challenge).catch(console.error) + onDismiss?.(challenge) } - } + }, [application, challenge, onDismiss]) useEffect(() => { const removeChallengeObserver = application.addChallengeObserver(challenge, { @@ -163,10 +165,10 @@ export const ChallengeModal: FunctionComponent = ({ } }, onComplete: () => { - onDismiss(challenge).catch(console.error) + onDismiss?.(challenge) }, onCancel: () => { - onDismiss(challenge).catch(console.error) + onDismiss?.(challenge) }, }) @@ -186,6 +188,7 @@ export const ChallengeModal: FunctionComponent = ({ }`} onDismiss={cancelChallenge} dangerouslyBypassFocusLock={bypassModalFocusLock} + key={challenge.id} > = ({ )}
{challenge.heading}
+ {challenge.subheading && (
{challenge.subheading}
)} +
{ e.preventDefault() - submit().catch(console.error) + submit() }} > {challenge.prompts.map((prompt, index) => ( @@ -226,14 +231,7 @@ export const ChallengeModal: FunctionComponent = ({ /> ))} - {shouldShowForgotPasscode && ( diff --git a/app/assets/javascripts/Components/ChallengeModal/ChallengePrompt.tsx b/app/assets/javascripts/Components/ChallengeModal/ChallengePrompt.tsx index 116c7a5f9..4f2159d1a 100644 --- a/app/assets/javascripts/Components/ChallengeModal/ChallengePrompt.tsx +++ b/app/assets/javascripts/Components/ChallengeModal/ChallengePrompt.tsx @@ -29,7 +29,7 @@ export const ChallengeModalPrompt: FunctionComponent = ({ prompt, values, }, [isInvalid]) return ( -
+
{prompt.validation === ChallengeValidation.ProtectionSessionDuration ? (
Allow protected access for
diff --git a/app/assets/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx b/app/assets/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx index 19f626a02..ece6735ab 100644 --- a/app/assets/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx +++ b/app/assets/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx @@ -2,7 +2,7 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { AppState } from '@/UIModels/AppState' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { FunctionComponent } from 'preact' -import { useEffect, useRef, useState } from 'preact/hooks' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { WorkspaceSwitcherMenu } from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu' import { Button } from '@/Components/Button/Button' import { Icon } from '@/Components/Icon' @@ -22,7 +22,7 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent = ({ mainAppl useCloseOnClickOutside(containerRef, () => setIsOpen(false)) - const toggleMenu = () => { + const toggleMenu = useCallback(() => { if (!isOpen) { const menuPosition = calculateSubmenuStyle(buttonRef.current) if (menuPosition) { @@ -31,7 +31,7 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent = ({ mainAppl } setIsOpen(!isOpen) - } + }, [isOpen]) useEffect(() => { if (isOpen) { diff --git a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx index 35db46c92..bc77f8d65 100644 --- a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx +++ b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx @@ -9,6 +9,7 @@ import { useRef, useState } from 'preact/hooks' import { Icon } from '@/Components/Icon' import { ChangeEditorMenu } from './ChangeEditorMenu' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' +import { isStateDealloced } from '@/UIModels/AppState/AbstractState' type Props = { application: WebApplication @@ -17,7 +18,11 @@ type Props = { } export const ChangeEditorButton: FunctionComponent = observer( - ({ application, appState, onClickPreprocessing }) => { + ({ application, appState, onClickPreprocessing }: Props) => { + if (isStateDealloced(appState)) { + return null + } + const note = Object.values(appState.notes.selectedNotes)[0] const [isOpen, setIsOpen] = useState(false) const [isVisible, setIsVisible] = useState(false) diff --git a/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx b/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx index b106e9acc..6db5878cf 100644 --- a/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx @@ -1,10 +1,10 @@ import { formatSizeToReadableString } from '@standardnotes/filepicker' -import { SNFile } from '@standardnotes/snjs' +import { FileItem } from '@standardnotes/snjs' import { FunctionComponent } from 'preact' import { Icon } from '@/Components/Icon' type Props = { - file: SNFile + file: FileItem } export const FilePreviewInfoPanel: FunctionComponent = ({ file }) => { diff --git a/app/assets/javascripts/Components/Files/FilePreviewModal.tsx b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx index 42635ecc8..e1facb053 100644 --- a/app/assets/javascripts/Components/Files/FilePreviewModal.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx @@ -1,7 +1,8 @@ import { WebApplication } from '@/UIModels/Application' import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays' import { DialogContent, DialogOverlay } from '@reach/dialog' -import { addToast, NoPreviewIllustration, ToastType } from '@standardnotes/stylekit' +import { addToast, ToastType } from '@standardnotes/stylekit' +import { NoPreviewIllustration } from '@standardnotes/icons' import { FunctionComponent } from 'preact' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { getFileIconComponent } from '@/Components/AttachedFilesPopover/PopoverFileItem' diff --git a/app/assets/javascripts/Components/Files/PreviewComponent.tsx b/app/assets/javascripts/Components/Files/PreviewComponent.tsx index 0da65f88d..f673dc62d 100644 --- a/app/assets/javascripts/Components/Files/PreviewComponent.tsx +++ b/app/assets/javascripts/Components/Files/PreviewComponent.tsx @@ -1,9 +1,9 @@ -import { SNFile } from '@standardnotes/snjs' +import { FileItem } from '@standardnotes/snjs' import { FunctionComponent } from 'preact' import { ImagePreview } from './ImagePreview' type Props = { - file: SNFile + file: FileItem objectUrl: string } diff --git a/app/assets/javascripts/Components/Footer/index.tsx b/app/assets/javascripts/Components/Footer/index.tsx index 7304bd612..0ca9fe746 100644 --- a/app/assets/javascripts/Components/Footer/index.tsx +++ b/app/assets/javascripts/Components/Footer/index.tsx @@ -1,7 +1,7 @@ import { WebAppEvent, WebApplication } from '@/UIModels/Application' import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { PureComponent } from '@/Components/Abstract/PureComponent' -import { preventRefreshing } from '@/Utils' +import { destroyAllObjectProperties, preventRefreshing } from '@/Utils' import { ApplicationEvent, ContentType, CollectionSort, ApplicationDescriptor } from '@standardnotes/snjs' import { STRING_NEW_UPDATE_READY, @@ -44,6 +44,7 @@ export class Footer extends PureComponent { private completedInitialSync = false private showingDownloadStatus = false private webEventListenerDestroyer: () => void + private removeStatusObserver!: () => void constructor(props: Props) { super(props, props.application) @@ -69,18 +70,26 @@ export class Footer extends PureComponent { } override deinit() { + this.removeStatusObserver() + ;(this.removeStatusObserver as unknown) = undefined + this.webEventListenerDestroyer() ;(this.webEventListenerDestroyer as unknown) = undefined + super.deinit() + + destroyAllObjectProperties(this) } override componentDidMount(): void { super.componentDidMount() - this.application.status.addEventObserver((_event, message) => { + + this.removeStatusObserver = this.application.status.addEventObserver((_event, message) => { this.setState({ arbitraryStatusMessage: message, }) }) + this.autorun(() => { const showBetaWarning = this.appState.showBetaWarning this.setState({ diff --git a/app/assets/javascripts/Components/Icon/index.tsx b/app/assets/javascripts/Components/Icon/index.tsx index 35d2cf2cc..cb3593814 100644 --- a/app/assets/javascripts/Components/Icon/index.tsx +++ b/app/assets/javascripts/Components/Icon/index.tsx @@ -89,7 +89,7 @@ import { WarningIcon, WindowIcon, SubtractIcon, -} from '@standardnotes/stylekit' +} from '@standardnotes/icons' export const ICONS = { 'account-circle': AccountCircleIcon, diff --git a/app/assets/javascripts/Components/Input/DecoratedInput.tsx b/app/assets/javascripts/Components/Input/DecoratedInput.tsx index a4936088b..6548c09cf 100644 --- a/app/assets/javascripts/Components/Input/DecoratedInput.tsx +++ b/app/assets/javascripts/Components/Input/DecoratedInput.tsx @@ -47,6 +47,7 @@ export const DecoratedInput: FunctionalComponent = forwardR ))}
)} + = forwardR autocomplete={autocomplete ? 'on' : 'off'} ref={ref} /> + {right && (
{right.map((rightChild, index) => ( diff --git a/app/assets/javascripts/Components/Menu/Menu.tsx b/app/assets/javascripts/Components/Menu/Menu.tsx index 050468f55..74410c2f9 100644 --- a/app/assets/javascripts/Components/Menu/Menu.tsx +++ b/app/assets/javascripts/Components/Menu/Menu.tsx @@ -1,5 +1,5 @@ import { JSX, FunctionComponent, ComponentChildren, VNode, RefCallback, ComponentChild, toChildArray } from 'preact' -import { useEffect, useRef } from 'preact/hooks' +import { useCallback, useEffect, useRef } from 'preact/hooks' import { JSXInternal } from 'preact/src/jsx' import { MenuItem, MenuItemListElement } from './MenuItem' import { KeyboardKey } from '@/Services/IOService' @@ -28,16 +28,19 @@ export const Menu: FunctionComponent = ({ const menuElementRef = useRef(null) - const handleKeyDown: JSXInternal.KeyboardEventHandler = (event) => { - if (!menuItemRefs.current) { - return - } + const handleKeyDown: JSXInternal.KeyboardEventHandler = useCallback( + (event) => { + if (!menuItemRefs.current) { + return + } - if (event.key === KeyboardKey.Escape) { - closeMenu?.() - return - } - } + if (event.key === KeyboardKey.Escape) { + closeMenu?.() + return + } + }, + [closeMenu], + ) useListKeyboardNavigation(menuElementRef, initialFocus) @@ -49,7 +52,7 @@ export const Menu: FunctionComponent = ({ } }, [isOpen]) - const pushRefToArray: RefCallback = (instance) => { + const pushRefToArray: RefCallback = useCallback((instance) => { if (instance && instance.children) { Array.from(instance.children).forEach((child) => { if ( @@ -60,36 +63,39 @@ export const Menu: FunctionComponent = ({ } }) } - } + }, []) - const mapMenuItems = (child: ComponentChild, index: number, array: ComponentChild[]): ComponentChild => { - if (!child || (Array.isArray(child) && child.length < 1)) { - return - } + const mapMenuItems = useCallback( + (child: ComponentChild, index: number, array: ComponentChild[]): ComponentChild => { + if (!child || (Array.isArray(child) && child.length < 1)) { + return + } - if (Array.isArray(child)) { - return child.map(mapMenuItems) - } + if (Array.isArray(child)) { + return child.map(mapMenuItems) + } - const _child = child as VNode - const isFirstMenuItem = index === array.findIndex((child) => (child as VNode).type === MenuItem) + const _child = child as VNode + const isFirstMenuItem = index === array.findIndex((child) => (child as VNode).type === MenuItem) - const hasMultipleItems = Array.isArray(_child.props.children) - ? Array.from(_child.props.children as ComponentChild[]).some( - (child) => (child as VNode).type === MenuItem, + const hasMultipleItems = Array.isArray(_child.props.children) + ? Array.from(_child.props.children as ComponentChild[]).some( + (child) => (child as VNode).type === MenuItem, + ) + : false + + const items = hasMultipleItems ? [...(_child.props.children as ComponentChild[])] : [_child] + + return items.map((child) => { + return ( + + {child} + ) - : false - - const items = hasMultipleItems ? [...(_child.props.children as ComponentChild[])] : [_child] - - return items.map((child) => { - return ( - - {child} - - ) - }) - } + }) + }, + [pushRefToArray], + ) return ( = forwardRef( ({ children, isFirstMenuItem }: ListElementProps, ref: Ref) => { const child = children as VNode - return (
  • {{ diff --git a/app/assets/javascripts/Components/MultipleSelectedNotes/index.tsx b/app/assets/javascripts/Components/MultipleSelectedNotes/index.tsx index df78e2817..432672c6c 100644 --- a/app/assets/javascripts/Components/MultipleSelectedNotes/index.tsx +++ b/app/assets/javascripts/Components/MultipleSelectedNotes/index.tsx @@ -1,5 +1,5 @@ import { AppState } from '@/UIModels/AppState' -import { IlNotesIcon } from '@standardnotes/stylekit' +import { IlNotesIcon } from '@standardnotes/icons' import { observer } from 'mobx-react-lite' import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel' import { WebApplication } from '@/UIModels/Application' diff --git a/app/assets/javascripts/Components/NoAccountWarning/index.tsx b/app/assets/javascripts/Components/NoAccountWarning/index.tsx index c0b4db0de..9a222cfcc 100644 --- a/app/assets/javascripts/Components/NoAccountWarning/index.tsx +++ b/app/assets/javascripts/Components/NoAccountWarning/index.tsx @@ -1,6 +1,7 @@ import { Icon } from '@/Components/Icon' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' +import { useCallback } from 'preact/hooks' type Props = { appState: AppState } @@ -9,23 +10,28 @@ export const NoAccountWarning = observer(({ appState }: Props) => { if (!canShow) { return null } + + const showAccountMenu = useCallback( + (event: Event) => { + event.stopPropagation() + appState.accountMenu.setShow(true) + }, + [appState], + ) + + const hideWarning = useCallback(() => { + appState.noAccountWarning.hide() + }, [appState]) + return (

    Data not backed up

    Sign in or register to back up your notes.

    -