chore: app group optimizations (#1027)

This commit is contained in:
Mo
2022-05-16 21:14:18 -05:00
committed by GitHub
parent 754a189532
commit 62cf34e894
108 changed files with 1796 additions and 1187 deletions

View File

@@ -0,0 +1,4 @@
declare module '*.svg' {
const content: any
export default content
}

View File

@@ -1,3 +0,0 @@
declare module '*.svg' {
export default function SvgComponent(props: React.SVGProps<SVGSVGElement>): JSX.Element
}

View File

@@ -13,18 +13,30 @@ declare global {
startApplication?: StartApplication startApplication?: StartApplication
websocketUrl: string websocketUrl: string
electronAppVersion?: string electronAppVersion?: string
webClient?: DesktopManagerInterface
application?: WebApplication
mainApplicationGroup?: ApplicationGroup
} }
} }
import { IsWebPlatform, WebAppVersion } from '@/Version' import { IsWebPlatform, WebAppVersion } from '@/Version'
import { Runtime, SNLog } from '@standardnotes/snjs' import { DesktopManagerInterface, SNLog } from '@standardnotes/snjs'
import { render } from 'preact' import { render } from 'preact'
import { ApplicationGroupView } from './Components/ApplicationGroupView' import { ApplicationGroupView } from './Components/ApplicationGroupView'
import { WebDevice } from './Device/WebDevice' import { WebDevice } from './Device/WebDevice'
import { StartApplication } from './Device/StartApplication' import { StartApplication } from './Device/StartApplication'
import { ApplicationGroup } from './UIModels/ApplicationGroup' import { ApplicationGroup } from './UIModels/ApplicationGroup'
import { isDev } from './Utils'
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice' 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( const startApplication: StartApplication = async function startApplication(
defaultSyncServerHost: string, defaultSyncServerHost: string,
@@ -35,34 +47,41 @@ const startApplication: StartApplication = async function startApplication(
SNLog.onLog = console.log SNLog.onLog = console.log
SNLog.onError = console.error SNLog.onError = console.error
const mainApplicationGroup = new ApplicationGroup( const onDestroy = () => {
defaultSyncServerHost, const root = document.getElementById(RootId) as HTMLElement
device, unmountComponentAtRoot(root)
enableUnfinishedFeatures ? Runtime.Dev : Runtime.Prod, root.remove()
webSocketUrl, renderApp()
)
if (isDev) {
Object.defineProperties(window, {
application: {
get: () => mainApplicationGroup.primaryApplication,
},
})
} }
const renderApp = () => { const renderApp = () => {
const root = document.createElement('div')
root.id = RootId
const parentNode = document.body.appendChild(root)
render( render(
<ApplicationGroupView mainApplicationGroup={mainApplicationGroup} />, <ApplicationGroupView
document.body.appendChild(document.createElement('div')), key={getKey()}
server={defaultSyncServerHost}
device={device}
enableUnfinished={enableUnfinishedFeatures}
websocketUrl={webSocketUrl}
onDestroy={onDestroy}
/>,
parentNode,
) )
} }
const domReady = document.readyState === 'complete' || document.readyState === 'interactive' const domReady = document.readyState === 'complete' || document.readyState === 'interactive'
if (domReady) { if (domReady) {
renderApp() renderApp()
} else { } else {
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', function callback() {
renderApp() renderApp()
window.removeEventListener('DOMContentLoaded', callback)
}) })
} }
} }

View File

@@ -31,6 +31,9 @@ export abstract class PureComponent<P = PureComponentProps, S = PureComponentSta
this.reactionDisposers.length = 0 this.reactionDisposers.length = 0
;(this.unsubApp as unknown) = undefined ;(this.unsubApp as unknown) = undefined
;(this.unsubState as unknown) = undefined ;(this.unsubState as unknown) = undefined
;(this.application as unknown) = undefined
;(this.props as unknown) = undefined
;(this.state as unknown) = undefined
} }
protected dismissModal(): void { protected dismissModal(): void {
@@ -81,11 +84,18 @@ export abstract class PureComponent<P = PureComponentProps, S = PureComponentSta
if (this.application.isStarted()) { if (this.application.isStarted()) {
this.onAppStart().catch(console.error) this.onAppStart().catch(console.error)
} }
if (this.application.isLaunched()) { if (this.application.isLaunched()) {
this.onAppLaunch().catch(console.error) this.onAppLaunch().catch(console.error)
} }
this.unsubApp = this.application.addEventObserver(async (eventName, data: unknown) => { this.unsubApp = this.application.addEventObserver(async (eventName, data: unknown) => {
if (!this.application) {
return
}
this.onAppEvent(eventName, data) this.onAppEvent(eventName, data)
if (eventName === ApplicationEvent.Started) { if (eventName === ApplicationEvent.Started) {
await this.onAppStart() await this.onAppStart()
} else if (eventName === ApplicationEvent.Launched) { } else if (eventName === ApplicationEvent.Launched) {

View File

@@ -2,7 +2,7 @@ import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useEffect, useState } from 'preact/hooks' import { useCallback, useEffect, useState } from 'preact/hooks'
import { Checkbox } from '@/Components/Checkbox' import { Checkbox } from '@/Components/Checkbox'
import { DecoratedInput } from '@/Components/Input/DecoratedInput' import { DecoratedInput } from '@/Components/Input/DecoratedInput'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
@@ -51,38 +51,44 @@ export const AdvancedOptions: FunctionComponent<Props> = observer(
onPrivateWorkspaceChange?.(isPrivateWorkspace) onPrivateWorkspaceChange?.(isPrivateWorkspace)
}, [isPrivateWorkspace, onPrivateWorkspaceChange]) }, [isPrivateWorkspace, onPrivateWorkspaceChange])
const handleIsPrivateWorkspaceChange = () => { const handleIsPrivateWorkspaceChange = useCallback(() => {
setIsPrivateWorkspace(!isPrivateWorkspace) setIsPrivateWorkspace(!isPrivateWorkspace)
} }, [isPrivateWorkspace])
const handlePrivateWorkspaceNameChange = (name: string) => { const handlePrivateWorkspaceNameChange = useCallback((name: string) => {
setPrivateWorkspaceName(name) setPrivateWorkspaceName(name)
} }, [])
const handlePrivateWorkspaceUserphraseChange = (userphrase: string) => { const handlePrivateWorkspaceUserphraseChange = useCallback((userphrase: string) => {
setPrivateWorkspaceUserphrase(userphrase) setPrivateWorkspaceUserphrase(userphrase)
} }, [])
const handleServerOptionChange = (e: Event) => { const handleServerOptionChange = useCallback(
if (e.target instanceof HTMLInputElement) { (e: Event) => {
setEnableServerOption(e.target.checked) if (e.target instanceof HTMLInputElement) {
} setEnableServerOption(e.target.checked)
} }
},
[setEnableServerOption],
)
const handleSyncServerChange = (server: string) => { const handleSyncServerChange = useCallback(
setServer(server) (server: string) => {
application.setCustomHost(server).catch(console.error) setServer(server)
} application.setCustomHost(server).catch(console.error)
},
[application, setServer],
)
const handleStrictSigninChange = () => { const handleStrictSigninChange = useCallback(() => {
const newValue = !isStrictSignin const newValue = !isStrictSignin
setIsStrictSignin(newValue) setIsStrictSignin(newValue)
onStrictSignInChange?.(newValue) onStrictSignInChange?.(newValue)
} }, [isStrictSignin, onStrictSignInChange])
const toggleShowAdvanced = () => { const toggleShowAdvanced = useCallback(() => {
setShowAdvanced(!showAdvanced) setShowAdvanced(!showAdvanced)
} }, [showAdvanced])
return ( return (
<> <>

View File

@@ -3,7 +3,7 @@ import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks' import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { AccountMenuPane } from '.' import { AccountMenuPane } from '.'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
import { Checkbox } from '@/Components/Checkbox' import { Checkbox } from '@/Components/Checkbox'
@@ -34,63 +34,69 @@ export const ConfirmPassword: FunctionComponent<Props> = observer(
passwordInputRef.current?.focus() passwordInputRef.current?.focus()
}, []) }, [])
const handlePasswordChange = (text: string) => { const handlePasswordChange = useCallback((text: string) => {
setConfirmPassword(text) setConfirmPassword(text)
} }, [])
const handleEphemeralChange = () => { const handleEphemeralChange = useCallback(() => {
setIsEphemeral(!isEphemeral) setIsEphemeral(!isEphemeral)
} }, [isEphemeral])
const handleShouldMergeChange = () => { const handleShouldMergeChange = useCallback(() => {
setShouldMergeLocal(!shouldMergeLocal) setShouldMergeLocal(!shouldMergeLocal)
} }, [shouldMergeLocal])
const handleKeyDown = (e: KeyboardEvent) => { const handleConfirmFormSubmit = useCallback(
if (error.length) { (e: Event) => {
setError('') e.preventDefault()
}
if (e.key === 'Enter') {
handleConfirmFormSubmit(e)
}
}
const handleConfirmFormSubmit = (e: Event) => { if (!password) {
e.preventDefault() passwordInputRef.current?.focus()
return
}
if (!password) { if (password === confirmPassword) {
passwordInputRef.current?.focus() setIsRegistering(true)
return 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) { const handleKeyDown = useCallback(
setIsRegistering(true) (e: KeyboardEvent) => {
application if (error.length) {
.register(email, password, isEphemeral, shouldMergeLocal) setError('')
.then((res) => { }
if (res.error) { if (e.key === 'Enter') {
throw new Error(res.error.message) handleConfirmFormSubmit(e)
} }
appState.accountMenu.closeAccountMenu() },
appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu) [handleConfirmFormSubmit, error],
}) )
.catch((err) => {
console.error(err)
setError(err.message)
})
.finally(() => {
setIsRegistering(false)
})
} else {
setError(STRING_NON_MATCHING_PASSWORDS)
setConfirmPassword('')
passwordInputRef.current?.focus()
}
}
const handleGoBack = () => { const handleGoBack = useCallback(() => {
setMenuPane(AccountMenuPane.Register) setMenuPane(AccountMenuPane.Register)
} }, [setMenuPane])
return ( return (
<> <>

View File

@@ -2,7 +2,7 @@ import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' 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 { AccountMenuPane } from '.'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
import { DecoratedInput } from '@/Components/Input/DecoratedInput' import { DecoratedInput } from '@/Components/Input/DecoratedInput'
@@ -33,50 +33,65 @@ export const CreateAccount: FunctionComponent<Props> = observer(
} }
}, []) }, [])
const handleEmailChange = (text: string) => { const handleEmailChange = useCallback(
setEmail(text) (text: string) => {
} setEmail(text)
},
[setEmail],
)
const handlePasswordChange = (text: string) => { const handlePasswordChange = useCallback(
setPassword(text) (text: string) => {
} setPassword(text)
},
[setPassword],
)
const handleKeyDown = (e: KeyboardEvent) => { const handleRegisterFormSubmit = useCallback(
if (e.key === 'Enter') { (e: Event) => {
handleRegisterFormSubmit(e) e.preventDefault()
}
}
const handleRegisterFormSubmit = (e: Event) => { if (!email || email.length === 0) {
e.preventDefault() emailInputRef.current?.focus()
return
}
if (!email || email.length === 0) { if (!password || password.length === 0) {
emailInputRef.current?.focus() passwordInputRef.current?.focus()
return return
} }
if (!password || password.length === 0) { setEmail(email)
passwordInputRef.current?.focus() setPassword(password)
return setMenuPane(AccountMenuPane.ConfirmPassword)
} },
[email, password, setPassword, setMenuPane, setEmail],
)
setEmail(email) const handleKeyDown = useCallback(
setPassword(password) (e: KeyboardEvent) => {
setMenuPane(AccountMenuPane.ConfirmPassword) if (e.key === 'Enter') {
} handleRegisterFormSubmit(e)
}
},
[handleRegisterFormSubmit],
)
const handleClose = () => { const handleClose = useCallback(() => {
setMenuPane(AccountMenuPane.GeneralMenu) setMenuPane(AccountMenuPane.GeneralMenu)
setEmail('') setEmail('')
setPassword('') setPassword('')
} }, [setEmail, setMenuPane, setPassword])
const onPrivateWorkspaceChange = (isPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => { const onPrivateWorkspaceChange = useCallback(
setIsPrivateWorkspace(isPrivateWorkspace) (isPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => {
if (isPrivateWorkspace && privateWorkspaceIdentifier) { setIsPrivateWorkspace(isPrivateWorkspace)
setEmail(privateWorkspaceIdentifier) if (isPrivateWorkspace && privateWorkspaceIdentifier) {
} setEmail(privateWorkspaceIdentifier)
} }
},
[setEmail],
)
return ( return (
<> <>

View File

@@ -5,7 +5,7 @@ import { Icon } from '@/Components/Icon'
import { formatLastSyncDate } from '@/Components/Preferences/Panes/Account/Sync' import { formatLastSyncDate } from '@/Components/Preferences/Panes/Account/Sync'
import { SyncQueueStrategy } from '@standardnotes/snjs' import { SyncQueueStrategy } from '@standardnotes/snjs'
import { STRING_GENERIC_SYNC_ERROR } from '@/Strings' import { STRING_GENERIC_SYNC_ERROR } from '@/Strings'
import { useState } from 'preact/hooks' import { useCallback, useMemo, useState } from 'preact/hooks'
import { AccountMenuPane } from '.' import { AccountMenuPane } from '.'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { Menu } from '@/Components/Menu/Menu' import { Menu } from '@/Components/Menu/Menu'
@@ -28,7 +28,7 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false) const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date)) const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
const doSynchronization = async () => { const doSynchronization = useCallback(async () => {
setIsSyncingInProgress(true) setIsSyncingInProgress(true)
application.sync application.sync
@@ -49,9 +49,33 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
.finally(() => { .finally(() => {
setIsSyncingInProgress(false) 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 CREATE_ACCOUNT_INDEX = 1
const SWITCHER_INDEX = 0 const SWITCHER_INDEX = 0
@@ -115,48 +139,23 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
<WorkspaceSwitcherOption mainApplicationGroup={mainApplicationGroup} appState={appState} /> <WorkspaceSwitcherOption mainApplicationGroup={mainApplicationGroup} appState={appState} />
<MenuItemSeparator /> <MenuItemSeparator />
{user ? ( {user ? (
<MenuItem <MenuItem type={MenuItemType.IconButton} onClick={openPreferences}>
type={MenuItemType.IconButton}
onClick={() => {
appState.accountMenu.closeAccountMenu()
appState.preferences.setCurrentPane('account')
appState.preferences.openPreferences()
}}
>
<Icon type="user" className={iconClassName} /> <Icon type="user" className={iconClassName} />
Account settings Account settings
</MenuItem> </MenuItem>
) : ( ) : (
<> <>
<MenuItem <MenuItem type={MenuItemType.IconButton} onClick={activateRegisterPane}>
type={MenuItemType.IconButton}
onClick={() => {
setMenuPane(AccountMenuPane.Register)
}}
>
<Icon type="user" className={iconClassName} /> <Icon type="user" className={iconClassName} />
Create free account Create free account
</MenuItem> </MenuItem>
<MenuItem <MenuItem type={MenuItemType.IconButton} onClick={activateSignInPane}>
type={MenuItemType.IconButton}
onClick={() => {
setMenuPane(AccountMenuPane.SignIn)
}}
>
<Icon type="signIn" className={iconClassName} /> <Icon type="signIn" className={iconClassName} />
Sign in Sign in
</MenuItem> </MenuItem>
</> </>
)} )}
<MenuItem <MenuItem className="justify-between" type={MenuItemType.IconButton} onClick={openHelp}>
className="justify-between"
type={MenuItemType.IconButton}
onClick={() => {
appState.accountMenu.closeAccountMenu()
appState.preferences.setCurrentPane('help-feedback')
appState.preferences.openPreferences()
}}
>
<div className="flex items-center"> <div className="flex items-center">
<Icon type="help" className={iconClassName} /> <Icon type="help" className={iconClassName} />
Help &amp; feedback Help &amp; feedback
@@ -166,12 +165,7 @@ export const GeneralAccountMenu: FunctionComponent<Props> = observer(
{user ? ( {user ? (
<> <>
<MenuItemSeparator /> <MenuItemSeparator />
<MenuItem <MenuItem type={MenuItemType.IconButton} onClick={signOut}>
type={MenuItemType.IconButton}
onClick={() => {
appState.accountMenu.setSigningOut(true)
}}
>
<Icon type="signOut" className={iconClassName} /> <Icon type="signOut" className={iconClassName} />
Sign out workspace Sign out workspace
</MenuItem> </MenuItem>

View File

@@ -44,36 +44,39 @@ export const SignInPane: FunctionComponent<Props> = observer(({ application, app
} }
}, []) }, [])
const resetInvalid = () => { const resetInvalid = useCallback(() => {
if (error.length) { if (error.length) {
setError('') setError('')
} }
} }, [setError, error])
const handleEmailChange = (text: string) => { const handleEmailChange = useCallback((text: string) => {
setEmail(text) setEmail(text)
} }, [])
const handlePasswordChange = (text: string) => { const handlePasswordChange = useCallback(
if (error.length) { (text: string) => {
setError('') if (error.length) {
} setError('')
setPassword(text) }
} setPassword(text)
},
[setPassword, error],
)
const handleEphemeralChange = () => { const handleEphemeralChange = useCallback(() => {
setIsEphemeral(!isEphemeral) setIsEphemeral(!isEphemeral)
} }, [isEphemeral])
const handleStrictSigninChange = () => { const handleStrictSigninChange = useCallback(() => {
setIsStrictSignin(!isStrictSignin) setIsStrictSignin(!isStrictSignin)
} }, [isStrictSignin])
const handleShouldMergeChange = () => { const handleShouldMergeChange = useCallback(() => {
setShouldMergeLocal(!shouldMergeLocal) setShouldMergeLocal(!shouldMergeLocal)
} }, [shouldMergeLocal])
const signIn = () => { const signIn = useCallback(() => {
setIsSigningIn(true) setIsSigningIn(true)
emailInputRef?.current?.blur() emailInputRef?.current?.blur()
passwordInputRef?.current?.blur() passwordInputRef?.current?.blur()
@@ -95,13 +98,7 @@ export const SignInPane: FunctionComponent<Props> = observer(({ application, app
.finally(() => { .finally(() => {
setIsSigningIn(false) setIsSigningIn(false)
}) })
} }, [appState, application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
handleSignInFormSubmit(e)
}
}
const onPrivateWorkspaceChange = useCallback( const onPrivateWorkspaceChange = useCallback(
(newIsPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => { (newIsPrivateWorkspace: boolean, privateWorkspaceIdentifier?: string) => {
@@ -113,21 +110,33 @@ export const SignInPane: FunctionComponent<Props> = observer(({ application, app
[setEmail], [setEmail],
) )
const handleSignInFormSubmit = (e: Event) => { const handleSignInFormSubmit = useCallback(
e.preventDefault() (e: Event) => {
e.preventDefault()
if (!email || email.length === 0) { if (!email || email.length === 0) {
emailInputRef?.current?.focus() emailInputRef?.current?.focus()
return return
} }
if (!password || password.length === 0) { if (!password || password.length === 0) {
passwordInputRef?.current?.focus() passwordInputRef?.current?.focus()
return return
} }
signIn() signIn()
} },
[email, password, signIn],
)
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Enter') {
handleSignInFormSubmit(e)
}
},
[handleSignInFormSubmit],
)
return ( return (
<> <>

View File

@@ -1,9 +1,9 @@
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem' import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem'
import { KeyboardKey } from '@/Services/IOService' import { KeyboardKey } from '@/Services/IOService'
import { ApplicationDescriptor } from '@standardnotes/snjs/dist/@types' import { ApplicationDescriptor } from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks' import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
type Props = { type Props = {
descriptor: ApplicationDescriptor descriptor: ApplicationDescriptor
@@ -29,17 +29,20 @@ export const WorkspaceMenuItem: FunctionComponent<Props> = ({
} }
}, [isRenaming]) }, [isRenaming])
const handleInputKeyDown = (event: KeyboardEvent) => { const handleInputKeyDown = useCallback((event: KeyboardEvent) => {
if (event.key === KeyboardKey.Enter) { if (event.key === KeyboardKey.Enter) {
inputRef.current?.blur() inputRef.current?.blur()
} }
} }, [])
const handleInputBlur = (event: FocusEvent) => { const handleInputBlur = useCallback(
const name = (event.target as HTMLInputElement).value (event: FocusEvent) => {
renameDescriptor(name) const name = (event.target as HTMLInputElement).value
setIsRenaming(false) renameDescriptor(name)
} setIsRenaming(false)
},
[renameDescriptor],
)
return ( return (
<MenuItem <MenuItem

View File

@@ -1,6 +1,6 @@
import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { ApplicationDescriptor, ButtonType } from '@standardnotes/snjs' import { ApplicationDescriptor, ApplicationGroupEvent, ButtonType } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useCallback, useEffect, useState } from 'preact/hooks' import { useCallback, useEffect, useState } from 'preact/hooks'
@@ -17,13 +17,18 @@ type Props = {
} }
export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer( export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }) => { ({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }: Props) => {
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([]) const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([])
useEffect(() => { useEffect(() => {
const removeAppGroupObserver = mainApplicationGroup.addApplicationChangeObserver(() => { const applicationDescriptors = mainApplicationGroup.getDescriptors()
const applicationDescriptors = mainApplicationGroup.getDescriptors() setApplicationDescriptors(applicationDescriptors)
setApplicationDescriptors(applicationDescriptors)
const removeAppGroupObserver = mainApplicationGroup.addEventObserver((event) => {
if (event === ApplicationGroupEvent.DescriptorsDataChanged) {
const applicationDescriptors = mainApplicationGroup.getDescriptors()
setApplicationDescriptors(applicationDescriptors)
}
}) })
return () => { return () => {
@@ -42,20 +47,21 @@ export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
return return
} }
mainApplicationGroup.signOutAllWorkspaces().catch(console.error) mainApplicationGroup.signOutAllWorkspaces().catch(console.error)
}, [mainApplicationGroup, appState.application.alertService]) }, [mainApplicationGroup, appState])
const destroyWorkspace = useCallback(() => {
appState.accountMenu.setSigningOut(true)
}, [appState])
return ( return (
<Menu a11yLabel="Workspace switcher menu" className="px-0 focus:shadow-none" isOpen={isOpen}> <Menu a11yLabel="Workspace switcher menu" className="px-0 focus:shadow-none" isOpen={isOpen}>
{applicationDescriptors.map((descriptor) => ( {applicationDescriptors.map((descriptor) => (
<WorkspaceMenuItem <WorkspaceMenuItem
key={descriptor.identifier}
descriptor={descriptor} descriptor={descriptor}
hideOptions={hideWorkspaceOptions} hideOptions={hideWorkspaceOptions}
onDelete={() => { onDelete={destroyWorkspace}
appState.accountMenu.setSigningOut(true) onClick={() => void mainApplicationGroup.unloadCurrentAndActivateDescriptor(descriptor)}
}}
onClick={() => {
mainApplicationGroup.loadApplicationForDescriptor(descriptor)
}}
renameDescriptor={(label: string) => mainApplicationGroup.renameDescriptor(descriptor, label)} renameDescriptor={(label: string) => mainApplicationGroup.renameDescriptor(descriptor, label)}
/> />
))} ))}
@@ -64,7 +70,7 @@ export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
<MenuItem <MenuItem
type={MenuItemType.IconButton} type={MenuItemType.IconButton}
onClick={() => { onClick={() => {
mainApplicationGroup.addNewApplication() void mainApplicationGroup.unloadCurrentAndCreateNewDescriptor()
}} }}
> >
<Icon type="user-add" className="color-neutral mr-2" /> <Icon type="user-add" className="color-neutral mr-2" />

View File

@@ -4,7 +4,7 @@ import { AppState } from '@/UIModels/AppState'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' 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 { Icon } from '@/Components/Icon'
import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu' import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu'
@@ -19,7 +19,7 @@ export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(({ mai
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>() const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
const toggleMenu = () => { const toggleMenu = useCallback(() => {
if (!isOpen) { if (!isOpen) {
const menuPosition = calculateSubmenuStyle(buttonRef.current) const menuPosition = calculateSubmenuStyle(buttonRef.current)
if (menuPosition) { if (menuPosition) {
@@ -28,7 +28,7 @@ export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(({ mai
} }
setIsOpen(!isOpen) setIsOpen(!isOpen)
} }, [isOpen, setIsOpen])
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {

View File

@@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { useRef, useState } from 'preact/hooks' import { useCallback, useRef, useState } from 'preact/hooks'
import { GeneralAccountMenu } from './GeneralAccountMenu' import { GeneralAccountMenu } from './GeneralAccountMenu'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { SignInPane } from './SignIn' import { SignInPane } from './SignIn'
@@ -80,26 +80,40 @@ const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
export const AccountMenu: FunctionComponent<Props> = observer( export const AccountMenu: FunctionComponent<Props> = observer(
({ application, appState, onClickOutside, mainApplicationGroup }) => { ({ 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<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(ref, () => { useCloseOnClickOutside(ref, () => {
onClickOutside() onClickOutside()
}) })
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (event) => { const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = useCallback(
switch (event.key) { (event) => {
case 'Escape': switch (event.key) {
if (currentPane === AccountMenuPane.GeneralMenu) { case 'Escape':
closeAccountMenu() if (currentPane === AccountMenuPane.GeneralMenu) {
} else if (currentPane === AccountMenuPane.ConfirmPassword) { closeAccountMenu()
setCurrentPane(AccountMenuPane.Register) } else if (currentPane === AccountMenuPane.ConfirmPassword) {
} else { setCurrentPane(AccountMenuPane.Register)
setCurrentPane(AccountMenuPane.GeneralMenu) } else {
} setCurrentPane(AccountMenuPane.GeneralMenu)
break }
} break
} }
},
[closeAccountMenu, currentPane, setCurrentPane],
)
return ( return (
<div ref={ref} id="account-menu" className="sn-component"> <div ref={ref} id="account-menu" className="sn-component">

View File

@@ -2,40 +2,126 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { Component } from 'preact' import { Component } from 'preact'
import { ApplicationView } from '@/Components/ApplicationView' 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 = { type State = {
activeApplication?: WebApplication activeApplication?: WebApplication
} dealloced?: boolean
deviceDestroyed?: boolean
type Props = {
mainApplicationGroup: ApplicationGroup
} }
export class ApplicationGroupView extends Component<Props, State> { export class ApplicationGroupView extends Component<Props, State> {
applicationObserverRemover?: () => void
private group?: ApplicationGroup
private application?: WebApplication
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
props.mainApplicationGroup.addApplicationChangeObserver(() => { if (props.device.isDeviceDestroyed()) {
const activeApplication = props.mainApplicationGroup.primaryApplication as WebApplication this.state = {
this.setState({ activeApplication }) 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() { render() {
const renderDialog = (message: string) => {
return (
<DialogOverlay className={'sn-component challenge-modal-overlay'}>
<DialogContent
className={
'challenge-modal flex flex-col items-center bg-default p-8 rounded relative shadow-overlay-light border-1 border-solid border-main'
}
>
{message}
</DialogContent>
</DialogOverlay>
)
}
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 ( return (
<> <div id={this.state.activeApplication.identifier} key={this.state.activeApplication.ephemeralIdentifier}>
{this.state.activeApplication && ( <ApplicationView
<div id={this.state.activeApplication.identifier}> key={this.state.activeApplication.ephemeralIdentifier}
<ApplicationView mainApplicationGroup={this.group}
key={this.state.activeApplication.ephemeralIdentifier} application={this.state.activeApplication}
mainApplicationGroup={this.props.mainApplicationGroup} />
application={this.state.activeApplication} </div>
/>
</div>
)}
</>
) )
} }
} }

View File

@@ -4,8 +4,7 @@ import { AppStateEvent, PanelResizedData } from '@/UIModels/AppState'
import { ApplicationEvent, Challenge, PermissionDialog, removeFromArray } from '@standardnotes/snjs' import { ApplicationEvent, Challenge, PermissionDialog, removeFromArray } from '@standardnotes/snjs'
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants' import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants'
import { alertDialog } from '@/Services/AlertService' import { alertDialog } from '@/Services/AlertService'
import { WebAppEvent, WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { PureComponent } from '@/Components/Abstract/PureComponent'
import { Navigation } from '@/Components/Navigation' import { Navigation } from '@/Components/Navigation'
import { NotesView } from '@/Components/NotesView' import { NotesView } from '@/Components/NotesView'
import { NoteGroupView } from '@/Components/NoteGroupView' import { NoteGroupView } from '@/Components/NoteGroupView'
@@ -15,7 +14,7 @@ import { PreferencesViewWrapper } from '@/Components/Preferences/PreferencesView
import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal' import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal'
import { NotesContextMenu } from '@/Components/NotesContextMenu' import { NotesContextMenu } from '@/Components/NotesContextMenu'
import { PurchaseFlowWrapper } from '@/Components/PurchaseFlow/PurchaseFlowWrapper' import { PurchaseFlowWrapper } from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { render } from 'preact' import { render, FunctionComponent } from 'preact'
import { PermissionsModal } from '@/Components/PermissionsModal' import { PermissionsModal } from '@/Components/PermissionsModal'
import { RevisionHistoryModalWrapper } from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper' import { RevisionHistoryModalWrapper } from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper'
import { PremiumModalProvider } from '@/Hooks/usePremiumModal' import { PremiumModalProvider } from '@/Hooks/usePremiumModal'
@@ -23,199 +22,221 @@ import { ConfirmSignoutContainer } from '@/Components/ConfirmSignoutModal'
import { TagsContextMenu } from '@/Components/Tags/TagContextMenu' import { TagsContextMenu } from '@/Components/Tags/TagContextMenu'
import { ToastContainer } from '@standardnotes/stylekit' import { ToastContainer } from '@standardnotes/stylekit'
import { FilePreviewModal } from '../Files/FilePreviewModal' import { FilePreviewModal } from '../Files/FilePreviewModal'
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = { type Props = {
application: WebApplication application: WebApplication
mainApplicationGroup: ApplicationGroup mainApplicationGroup: ApplicationGroup
} }
type State = { export const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicationGroup }) => {
started?: boolean const platformString = getPlatformString()
launched?: boolean const [appClass, setAppClass] = useState('')
needsUnlock?: boolean const [launched, setLaunched] = useState(false)
appClass: string const [needsUnlock, setNeedsUnlock] = useState(true)
challenges: Challenge[] const [challenges, setChallenges] = useState<Challenge[]>([])
} const [dealloced, setDealloced] = useState(false)
export class ApplicationView extends PureComponent<Props, State> { const componentManager = application.componentManager
public readonly platformString = getPlatformString() const appState = application.getAppState()
constructor(props: Props) { useEffect(() => {
super(props, props.application) setDealloced(application.dealloced)
this.state = { }, [application.dealloced])
appClass: '',
challenges: [],
}
}
override deinit() { useEffect(() => {
;(this.application as unknown) = undefined if (dealloced) {
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()) {
return return
} }
await this.application.sessions.populateSessionFromDemoShareToken(token) const desktopService = application.getDesktopService()
}
presentPermissionsDialog = (dialog: PermissionDialog) => { if (desktopService) {
render( application.componentManager.setDesktopManager(desktopService)
<PermissionsModal
application={this.application}
callback={dialog.callback}
component={dialog.component}
permissionsString={dialog.permissionsString}
/>,
document.body.appendChild(document.createElement('div')),
)
}
override render() {
if (this.application['dealloced'] === true) {
console.error('Attempting to render dealloced application')
return <div></div>
} }
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(
<PermissionsModal
application={application}
callback={dialog.callback}
component={dialog.component}
permissionsString={dialog.permissionsString}
/>,
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 ( return (
<PremiumModalProvider application={this.application} appState={this.appState}> <>
<div className={this.platformString + ' main-ui-view sn-component'}> {challenges.map((challenge) => {
{renderAppContents && ( return (
<div id="app" className={this.state.appClass + ' app app-column-container'}> <div className="sk-modal">
<Navigation application={this.application} /> <ChallengeModal
<NotesView application={this.application} appState={this.appState} /> key={`${challenge.id}${application.ephemeralIdentifier}`}
<NoteGroupView application={this.application} /> application={application}
</div> appState={appState}
)} mainApplicationGroup={mainApplicationGroup}
{renderAppContents && ( challenge={challenge}
<> onDismiss={removeChallenge}
<Footer application={this.application} applicationGroup={this.props.mainApplicationGroup} />
<SessionsModal application={this.application} appState={this.appState} />
<PreferencesViewWrapper appState={this.appState} application={this.application} />
<RevisionHistoryModalWrapper application={this.application} appState={this.appState} />
</>
)}
{this.state.challenges.map((challenge) => {
return (
<div className="sk-modal">
<ChallengeModal
key={challenge.id}
application={this.application}
appState={this.appState}
mainApplicationGroup={this.props.mainApplicationGroup}
challenge={challenge}
onDismiss={this.removeChallenge}
/>
</div>
)
})}
{renderAppContents && (
<>
<NotesContextMenu application={this.application} appState={this.appState} />
<TagsContextMenu appState={this.appState} />
<PurchaseFlowWrapper application={this.application} appState={this.appState} />
<ConfirmSignoutContainer
applicationGroup={this.props.mainApplicationGroup}
appState={this.appState}
application={this.application}
/> />
<ToastContainer /> </div>
<FilePreviewModal application={this.application} appState={this.appState} /> )
</> })}
)} </>
</div>
</PremiumModalProvider>
) )
}, [appState, challenges, mainApplicationGroup, removeChallenge, application])
if (dealloced || isStateDealloced(appState)) {
return null
} }
if (!renderAppContents) {
return renderChallenges()
}
return (
<PremiumModalProvider application={application} appState={appState}>
<div className={platformString + ' main-ui-view sn-component'}>
<div id="app" className={appClass + ' app app-column-container'}>
<Navigation application={application} />
<NotesView application={application} appState={appState} />
<NoteGroupView application={application} />
</div>
<>
<Footer application={application} applicationGroup={mainApplicationGroup} />
<SessionsModal application={application} appState={appState} />
<PreferencesViewWrapper appState={appState} application={application} />
<RevisionHistoryModalWrapper application={application} appState={appState} />
</>
{renderChallenges()}
<>
<NotesContextMenu application={application} appState={appState} />
<TagsContextMenu appState={appState} />
<PurchaseFlowWrapper application={application} appState={appState} />
<ConfirmSignoutContainer
applicationGroup={mainApplicationGroup}
appState={appState}
application={application}
/>
<ToastContainer />
<FilePreviewModal application={application} appState={appState} />
</>
</div>
</PremiumModalProvider>
)
} }

View File

@@ -8,7 +8,7 @@ import { FunctionComponent } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' 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 { confirmDialog } from '@/Services/AlertService'
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit' import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit'
import { StreamingFileReader } from '@standardnotes/filepicker' import { StreamingFileReader } from '@standardnotes/filepicker'
@@ -17,6 +17,7 @@ import { AttachedFilesPopover } from './AttachedFilesPopover'
import { usePremiumModal } from '@/Hooks/usePremiumModal' import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { PopoverTabs } from './PopoverTabs' import { PopoverTabs } from './PopoverTabs'
import { isHandlingFileDrag } from '@/Utils/DragTypeCheck' import { isHandlingFileDrag } from '@/Utils/DragTypeCheck'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -25,9 +26,12 @@ type Props = {
} }
export const AttachedFilesButton: FunctionComponent<Props> = observer( export const AttachedFilesButton: FunctionComponent<Props> = observer(
({ application, appState, onClickPreprocessing }) => { ({ application, appState, onClickPreprocessing }: Props) => {
const premiumModal = usePremiumModal() if (isStateDealloced(appState)) {
return null
}
const premiumModal = usePremiumModal()
const note: SNNote | undefined = Object.values(appState.notes.selectedNotes)[0] const note: SNNote | undefined = Object.values(appState.notes.selectedNotes)[0]
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@@ -50,15 +54,15 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
}, [appState.filePreviewModal.isOpen, keepMenuOpen]) }, [appState.filePreviewModal.isOpen, keepMenuOpen])
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles) const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles)
const [allFiles, setAllFiles] = useState<SNFile[]>([]) const [allFiles, setAllFiles] = useState<FileItem[]>([])
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([]) const [attachedFiles, setAttachedFiles] = useState<FileItem[]>([])
const attachedFilesCount = attachedFiles.length const attachedFilesCount = attachedFiles.length
useEffect(() => { useEffect(() => {
application.items.setDisplayOptions(ContentType.File, CollectionSort.Title, 'dsc') application.items.setDisplayOptions(ContentType.File, CollectionSort.Title, 'dsc')
const unregisterFileStream = application.streamItems(ContentType.File, () => { const unregisterFileStream = application.streamItems(ContentType.File, () => {
setAllFiles(application.items.getDisplayableItems<SNFile>(ContentType.File)) setAllFiles(application.items.getDisplayableItems<FileItem>(ContentType.File))
if (note) { if (note) {
setAttachedFiles(application.items.getFilesForNote(note)) setAttachedFiles(application.items.getFilesForNote(note))
} }
@@ -106,7 +110,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
await toggleAttachedFilesMenu() await toggleAttachedFilesMenu()
}, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal]) }, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal])
const deleteFile = async (file: SNFile) => { const deleteFile = async (file: FileItem) => {
const shouldDelete = await confirmDialog({ const shouldDelete = await confirmDialog({
text: `Are you sure you want to permanently delete "${file.name}"?`, text: `Are you sure you want to permanently delete "${file.name}"?`,
confirmButtonStyle: 'danger', confirmButtonStyle: 'danger',
@@ -125,12 +129,12 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
} }
} }
const downloadFile = async (file: SNFile) => { const downloadFile = async (file: FileItem) => {
appState.files.downloadFile(file).catch(console.error) appState.files.downloadFile(file).catch(console.error)
} }
const attachFileToNote = useCallback( const attachFileToNote = useCallback(
async (file: SNFile) => { async (file: FileItem) => {
if (!note) { if (!note) {
addToast({ addToast({
type: ToastType.Error, type: ToastType.Error,
@@ -144,7 +148,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
[application.items, note], [application.items, note],
) )
const detachFileFromNote = async (file: SNFile) => { const detachFileFromNote = async (file: FileItem) => {
if (!note) { if (!note) {
addToast({ addToast({
type: ToastType.Error, type: ToastType.Error,
@@ -155,8 +159,8 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
await application.items.disassociateFileWithNote(file, note) await application.items.disassociateFileWithNote(file, note)
} }
const toggleFileProtection = async (file: SNFile) => { const toggleFileProtection = async (file: FileItem) => {
let result: SNFile | undefined let result: FileItem | undefined
if (file.protected) { if (file.protected) {
keepMenuOpen(true) keepMenuOpen(true)
result = await application.mutator.unprotectFile(file) result = await application.mutator.unprotectFile(file)
@@ -169,13 +173,13 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
return isProtected 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 authorizedFiles = await application.protections.authorizeProtectedActionForFiles([file], challengeReason)
const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file)
return isAuthorized return isAuthorized
} }
const renameFile = async (file: SNFile, fileName: string) => { const renameFile = async (file: FileItem, fileName: string) => {
await application.items.renameFile(file, fileName) await application.items.renameFile(file, fileName)
} }

View File

@@ -1,8 +1,8 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { SNFile } from '@standardnotes/snjs' import { FileItem } from '@standardnotes/snjs'
import { FilesIllustration } from '@standardnotes/stylekit' import { FilesIllustration } from '@standardnotes/icons'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { StateUpdater, useRef, useState } from 'preact/hooks' import { StateUpdater, useRef, useState } from 'preact/hooks'
@@ -15,8 +15,8 @@ import { PopoverTabs } from './PopoverTabs'
type Props = { type Props = {
application: WebApplication application: WebApplication
appState: AppState appState: AppState
allFiles: SNFile[] allFiles: FileItem[]
attachedFiles: SNFile[] attachedFiles: FileItem[]
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
currentTab: PopoverTabs currentTab: PopoverTabs
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean> handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
@@ -126,7 +126,7 @@ export const AttachedFilesPopover: FunctionComponent<Props> = observer(
</div> </div>
) : null} ) : null}
{filteredList.length > 0 ? ( {filteredList.length > 0 ? (
filteredList.map((file: SNFile) => { filteredList.map((file: FileItem) => {
return ( return (
<PopoverFileItem <PopoverFileItem
key={file.uuid} key={file.uuid}

View File

@@ -1,7 +1,7 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
import { KeyboardKey } from '@/Services/IOService' import { KeyboardKey } from '@/Services/IOService'
import { formatSizeToReadableString } from '@standardnotes/filepicker' import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { IconType, SNFile } from '@standardnotes/snjs' import { IconType, FileItem } from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import { Icon, ICONS } from '@/Components/Icon' import { Icon, ICONS } from '@/Components/Icon'
@@ -15,7 +15,7 @@ export const getFileIconComponent = (iconType: string, className: string) => {
} }
export type PopoverFileItemProps = { export type PopoverFileItemProps = {
file: SNFile file: FileItem
isAttachedToNote: boolean isAttachedToNote: boolean
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean> handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
getIconType(type: string): IconType getIconType(type: string): IconType
@@ -40,7 +40,7 @@ export const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
} }
}, [isRenamingFile]) }, [isRenamingFile])
const renameFile = async (file: SNFile, name: string) => { const renameFile = async (file: FileItem, name: string) => {
await handleFileAction({ await handleFileAction({
type: PopoverFileItemActionType.RenameFile, type: PopoverFileItemActionType.RenameFile,
payload: { payload: {

View File

@@ -1,4 +1,4 @@
import { SNFile } from '@standardnotes/snjs' import { FileItem } from '@standardnotes/snjs'
export enum PopoverFileItemActionType { export enum PopoverFileItemActionType {
AttachFileToNote, AttachFileToNote,
@@ -16,17 +16,17 @@ export type PopoverFileItemAction =
PopoverFileItemActionType, PopoverFileItemActionType,
PopoverFileItemActionType.RenameFile | PopoverFileItemActionType.ToggleFileProtection PopoverFileItemActionType.RenameFile | PopoverFileItemActionType.ToggleFileProtection
> >
payload: SNFile payload: FileItem
} }
| { | {
type: PopoverFileItemActionType.ToggleFileProtection type: PopoverFileItemActionType.ToggleFileProtection
payload: SNFile payload: FileItem
callback: (isProtected: boolean) => void callback: (isProtected: boolean) => void
} }
| { | {
type: PopoverFileItemActionType.RenameFile type: PopoverFileItemActionType.RenameFile
payload: { payload: {
file: SNFile file: FileItem
name: string name: string
} }
} }

View File

@@ -34,11 +34,11 @@ export const PopoverFileSubmenu: FunctionComponent<Props> = ({
}) })
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen) const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
const closeMenu = () => { const closeMenu = useCallback(() => {
setIsMenuOpen(false) setIsMenuOpen(false)
} }, [])
const toggleMenu = () => { const toggleMenu = useCallback(() => {
if (!isMenuOpen) { if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current) const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) { if (menuPosition) {
@@ -47,7 +47,7 @@ export const PopoverFileSubmenu: FunctionComponent<Props> = ({
} }
setIsMenuOpen(!isMenuOpen) setIsMenuOpen(!isMenuOpen)
} }, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => { const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current) const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)

View File

@@ -8,7 +8,7 @@ import {
ChallengeValue, ChallengeValue,
removeFromArray, removeFromArray,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { ProtectedIllustration } from '@standardnotes/stylekit' import { ProtectedIllustration } from '@standardnotes/icons'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useCallback, useEffect, useState } from 'preact/hooks' import { useCallback, useEffect, useState } from 'preact/hooks'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
@@ -31,7 +31,7 @@ type Props = {
appState: AppState appState: AppState
mainApplicationGroup: ApplicationGroup mainApplicationGroup: ApplicationGroup
challenge: Challenge challenge: Challenge
onDismiss: (challenge: Challenge) => Promise<void> onDismiss?: (challenge: Challenge) => void
} }
const validateValues = (values: ChallengeModalValues, prompts: ChallengePrompt[]): ChallengeModalValues | undefined => { const validateValues = (values: ChallengeModalValues, prompts: ChallengePrompt[]): ChallengeModalValues | undefined => {
@@ -77,7 +77,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
) )
const shouldShowWorkspaceSwitcher = challenge.reason === ChallengeReason.ApplicationUnlock const shouldShowWorkspaceSwitcher = challenge.reason === ChallengeReason.ApplicationUnlock
const submit = async () => { const submit = useCallback(() => {
const validatedValues = validateValues(values, challenge.prompts) const validatedValues = validateValues(values, challenge.prompts)
if (!validatedValues) { if (!validatedValues) {
return return
@@ -87,12 +87,14 @@ export const ChallengeModal: FunctionComponent<Props> = ({
} }
setIsSubmitting(true) setIsSubmitting(true)
setIsProcessing(true) setIsProcessing(true)
const valuesToProcess: ChallengeValue[] = [] const valuesToProcess: ChallengeValue[] = []
for (const inputValue of Object.values(validatedValues)) { for (const inputValue of Object.values(validatedValues)) {
const rawValue = inputValue.value const rawValue = inputValue.value
const value = { prompt: inputValue.prompt, value: rawValue } const value = { prompt: inputValue.prompt, value: rawValue }
valuesToProcess.push(value) valuesToProcess.push(value)
} }
const processingPrompts = valuesToProcess.map((v) => v.prompt) const processingPrompts = valuesToProcess.map((v) => v.prompt)
setIsProcessing(processingPrompts.length > 0) setIsProcessing(processingPrompts.length > 0)
setProcessingPrompts(processingPrompts) setProcessingPrompts(processingPrompts)
@@ -109,7 +111,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
} }
setIsSubmitting(false) setIsSubmitting(false)
}, 50) }, 50)
} }, [application, challenge, isProcessing, isSubmitting, values])
const onValueChange = useCallback( const onValueChange = useCallback(
(value: string | number, prompt: ChallengePrompt) => { (value: string | number, prompt: ChallengePrompt) => {
@@ -121,12 +123,12 @@ export const ChallengeModal: FunctionComponent<Props> = ({
[values], [values],
) )
const cancelChallenge = () => { const cancelChallenge = useCallback(() => {
if (challenge.cancelable) { if (challenge.cancelable) {
application.cancelChallenge(challenge) application.cancelChallenge(challenge)
onDismiss(challenge).catch(console.error) onDismiss?.(challenge)
} }
} }, [application, challenge, onDismiss])
useEffect(() => { useEffect(() => {
const removeChallengeObserver = application.addChallengeObserver(challenge, { const removeChallengeObserver = application.addChallengeObserver(challenge, {
@@ -163,10 +165,10 @@ export const ChallengeModal: FunctionComponent<Props> = ({
} }
}, },
onComplete: () => { onComplete: () => {
onDismiss(challenge).catch(console.error) onDismiss?.(challenge)
}, },
onCancel: () => { onCancel: () => {
onDismiss(challenge).catch(console.error) onDismiss?.(challenge)
}, },
}) })
@@ -186,6 +188,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
}`} }`}
onDismiss={cancelChallenge} onDismiss={cancelChallenge}
dangerouslyBypassFocusLock={bypassModalFocusLock} dangerouslyBypassFocusLock={bypassModalFocusLock}
key={challenge.id}
> >
<DialogContent <DialogContent
className={`challenge-modal flex flex-col items-center bg-default p-8 rounded relative ${ className={`challenge-modal flex flex-col items-center bg-default p-8 rounded relative ${
@@ -205,14 +208,16 @@ export const ChallengeModal: FunctionComponent<Props> = ({
)} )}
<ProtectedIllustration className="w-30 h-30 mb-4" /> <ProtectedIllustration className="w-30 h-30 mb-4" />
<div className="font-bold text-lg text-center max-w-76 mb-3">{challenge.heading}</div> <div className="font-bold text-lg text-center max-w-76 mb-3">{challenge.heading}</div>
{challenge.subheading && ( {challenge.subheading && (
<div className="text-center text-sm max-w-76 mb-4 break-word">{challenge.subheading}</div> <div className="text-center text-sm max-w-76 mb-4 break-word">{challenge.subheading}</div>
)} )}
<form <form
className="flex flex-col items-center min-w-76" className="flex flex-col items-center min-w-76"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
submit().catch(console.error) submit()
}} }}
> >
{challenge.prompts.map((prompt, index) => ( {challenge.prompts.map((prompt, index) => (
@@ -226,14 +231,7 @@ export const ChallengeModal: FunctionComponent<Props> = ({
/> />
))} ))}
</form> </form>
<Button <Button variant="primary" disabled={isProcessing} className="min-w-76 mt-1 mb-3.5" onClick={submit}>
variant="primary"
disabled={isProcessing}
className="min-w-76 mt-1 mb-3.5"
onClick={() => {
submit().catch(console.error)
}}
>
{isProcessing ? 'Generating Keys...' : 'Submit'} {isProcessing ? 'Generating Keys...' : 'Submit'}
</Button> </Button>
{shouldShowForgotPasscode && ( {shouldShowForgotPasscode && (

View File

@@ -29,7 +29,7 @@ export const ChallengeModalPrompt: FunctionComponent<Props> = ({ prompt, values,
}, [isInvalid]) }, [isInvalid])
return ( return (
<div className="w-full mb-3"> <div key={prompt.id} className="w-full mb-3">
{prompt.validation === ChallengeValidation.ProtectionSessionDuration ? ( {prompt.validation === ChallengeValidation.ProtectionSessionDuration ? (
<div className="min-w-76"> <div className="min-w-76">
<div className="text-sm font-medium mb-2">Allow protected access for</div> <div className="text-sm font-medium mb-2">Allow protected access for</div>

View File

@@ -2,7 +2,7 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { FunctionComponent } from 'preact' 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 { WorkspaceSwitcherMenu } from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
@@ -22,7 +22,7 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainAppl
useCloseOnClickOutside(containerRef, () => setIsOpen(false)) useCloseOnClickOutside(containerRef, () => setIsOpen(false))
const toggleMenu = () => { const toggleMenu = useCallback(() => {
if (!isOpen) { if (!isOpen) {
const menuPosition = calculateSubmenuStyle(buttonRef.current) const menuPosition = calculateSubmenuStyle(buttonRef.current)
if (menuPosition) { if (menuPosition) {
@@ -31,7 +31,7 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainAppl
} }
setIsOpen(!isOpen) setIsOpen(!isOpen)
} }, [isOpen])
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {

View File

@@ -9,6 +9,7 @@ import { useRef, useState } from 'preact/hooks'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { ChangeEditorMenu } from './ChangeEditorMenu' import { ChangeEditorMenu } from './ChangeEditorMenu'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -17,7 +18,11 @@ type Props = {
} }
export const ChangeEditorButton: FunctionComponent<Props> = observer( export const ChangeEditorButton: FunctionComponent<Props> = observer(
({ application, appState, onClickPreprocessing }) => { ({ application, appState, onClickPreprocessing }: Props) => {
if (isStateDealloced(appState)) {
return null
}
const note = Object.values(appState.notes.selectedNotes)[0] const note = Object.values(appState.notes.selectedNotes)[0]
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isVisible, setIsVisible] = useState(false) const [isVisible, setIsVisible] = useState(false)

View File

@@ -1,10 +1,10 @@
import { formatSizeToReadableString } from '@standardnotes/filepicker' import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { SNFile } from '@standardnotes/snjs' import { FileItem } from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
type Props = { type Props = {
file: SNFile file: FileItem
} }
export const FilePreviewInfoPanel: FunctionComponent<Props> = ({ file }) => { export const FilePreviewInfoPanel: FunctionComponent<Props> = ({ file }) => {

View File

@@ -1,7 +1,8 @@
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays' import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
import { DialogContent, DialogOverlay } from '@reach/dialog' 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 { FunctionComponent } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { getFileIconComponent } from '@/Components/AttachedFilesPopover/PopoverFileItem' import { getFileIconComponent } from '@/Components/AttachedFilesPopover/PopoverFileItem'

View File

@@ -1,9 +1,9 @@
import { SNFile } from '@standardnotes/snjs' import { FileItem } from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { ImagePreview } from './ImagePreview' import { ImagePreview } from './ImagePreview'
type Props = { type Props = {
file: SNFile file: FileItem
objectUrl: string objectUrl: string
} }

View File

@@ -1,7 +1,7 @@
import { WebAppEvent, WebApplication } from '@/UIModels/Application' import { WebAppEvent, WebApplication } from '@/UIModels/Application'
import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
import { PureComponent } from '@/Components/Abstract/PureComponent' import { PureComponent } from '@/Components/Abstract/PureComponent'
import { preventRefreshing } from '@/Utils' import { destroyAllObjectProperties, preventRefreshing } from '@/Utils'
import { ApplicationEvent, ContentType, CollectionSort, ApplicationDescriptor } from '@standardnotes/snjs' import { ApplicationEvent, ContentType, CollectionSort, ApplicationDescriptor } from '@standardnotes/snjs'
import { import {
STRING_NEW_UPDATE_READY, STRING_NEW_UPDATE_READY,
@@ -44,6 +44,7 @@ export class Footer extends PureComponent<Props, State> {
private completedInitialSync = false private completedInitialSync = false
private showingDownloadStatus = false private showingDownloadStatus = false
private webEventListenerDestroyer: () => void private webEventListenerDestroyer: () => void
private removeStatusObserver!: () => void
constructor(props: Props) { constructor(props: Props) {
super(props, props.application) super(props, props.application)
@@ -69,18 +70,26 @@ export class Footer extends PureComponent<Props, State> {
} }
override deinit() { override deinit() {
this.removeStatusObserver()
;(this.removeStatusObserver as unknown) = undefined
this.webEventListenerDestroyer() this.webEventListenerDestroyer()
;(this.webEventListenerDestroyer as unknown) = undefined ;(this.webEventListenerDestroyer as unknown) = undefined
super.deinit() super.deinit()
destroyAllObjectProperties(this)
} }
override componentDidMount(): void { override componentDidMount(): void {
super.componentDidMount() super.componentDidMount()
this.application.status.addEventObserver((_event, message) => {
this.removeStatusObserver = this.application.status.addEventObserver((_event, message) => {
this.setState({ this.setState({
arbitraryStatusMessage: message, arbitraryStatusMessage: message,
}) })
}) })
this.autorun(() => { this.autorun(() => {
const showBetaWarning = this.appState.showBetaWarning const showBetaWarning = this.appState.showBetaWarning
this.setState({ this.setState({

View File

@@ -89,7 +89,7 @@ import {
WarningIcon, WarningIcon,
WindowIcon, WindowIcon,
SubtractIcon, SubtractIcon,
} from '@standardnotes/stylekit' } from '@standardnotes/icons'
export const ICONS = { export const ICONS = {
'account-circle': AccountCircleIcon, 'account-circle': AccountCircleIcon,

View File

@@ -47,6 +47,7 @@ export const DecoratedInput: FunctionalComponent<DecoratedInputProps> = forwardR
))} ))}
</div> </div>
)} )}
<input <input
type={type} type={type}
className={`${classNames.input} ${disabled ? classNames.disabled : ''}`} className={`${classNames.input} ${disabled ? classNames.disabled : ''}`}
@@ -60,6 +61,7 @@ export const DecoratedInput: FunctionalComponent<DecoratedInputProps> = forwardR
autocomplete={autocomplete ? 'on' : 'off'} autocomplete={autocomplete ? 'on' : 'off'}
ref={ref} ref={ref}
/> />
{right && ( {right && (
<div className="flex items-center px-2 py-1.5"> <div className="flex items-center px-2 py-1.5">
{right.map((rightChild, index) => ( {right.map((rightChild, index) => (

View File

@@ -1,5 +1,5 @@
import { JSX, FunctionComponent, ComponentChildren, VNode, RefCallback, ComponentChild, toChildArray } from 'preact' 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 { JSXInternal } from 'preact/src/jsx'
import { MenuItem, MenuItemListElement } from './MenuItem' import { MenuItem, MenuItemListElement } from './MenuItem'
import { KeyboardKey } from '@/Services/IOService' import { KeyboardKey } from '@/Services/IOService'
@@ -28,16 +28,19 @@ export const Menu: FunctionComponent<MenuProps> = ({
const menuElementRef = useRef<HTMLMenuElement>(null) const menuElementRef = useRef<HTMLMenuElement>(null)
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLMenuElement> = (event) => { const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLMenuElement> = useCallback(
if (!menuItemRefs.current) { (event) => {
return if (!menuItemRefs.current) {
} return
}
if (event.key === KeyboardKey.Escape) { if (event.key === KeyboardKey.Escape) {
closeMenu?.() closeMenu?.()
return return
} }
} },
[closeMenu],
)
useListKeyboardNavigation(menuElementRef, initialFocus) useListKeyboardNavigation(menuElementRef, initialFocus)
@@ -49,7 +52,7 @@ export const Menu: FunctionComponent<MenuProps> = ({
} }
}, [isOpen]) }, [isOpen])
const pushRefToArray: RefCallback<HTMLLIElement> = (instance) => { const pushRefToArray: RefCallback<HTMLLIElement> = useCallback((instance) => {
if (instance && instance.children) { if (instance && instance.children) {
Array.from(instance.children).forEach((child) => { Array.from(instance.children).forEach((child) => {
if ( if (
@@ -60,36 +63,39 @@ export const Menu: FunctionComponent<MenuProps> = ({
} }
}) })
} }
} }, [])
const mapMenuItems = (child: ComponentChild, index: number, array: ComponentChild[]): ComponentChild => { const mapMenuItems = useCallback(
if (!child || (Array.isArray(child) && child.length < 1)) { (child: ComponentChild, index: number, array: ComponentChild[]): ComponentChild => {
return if (!child || (Array.isArray(child) && child.length < 1)) {
} return
}
if (Array.isArray(child)) { if (Array.isArray(child)) {
return child.map(mapMenuItems) return child.map(mapMenuItems)
} }
const _child = child as VNode<unknown> const _child = child as VNode<unknown>
const isFirstMenuItem = index === array.findIndex((child) => (child as VNode<unknown>).type === MenuItem) const isFirstMenuItem = index === array.findIndex((child) => (child as VNode<unknown>).type === MenuItem)
const hasMultipleItems = Array.isArray(_child.props.children) const hasMultipleItems = Array.isArray(_child.props.children)
? Array.from(_child.props.children as ComponentChild[]).some( ? Array.from(_child.props.children as ComponentChild[]).some(
(child) => (child as VNode<unknown>).type === MenuItem, (child) => (child as VNode<unknown>).type === MenuItem,
)
: false
const items = hasMultipleItems ? [...(_child.props.children as ComponentChild[])] : [_child]
return items.map((child) => {
return (
<MenuItemListElement isFirstMenuItem={isFirstMenuItem} ref={pushRefToArray}>
{child}
</MenuItemListElement>
) )
: false })
},
const items = hasMultipleItems ? [...(_child.props.children as ComponentChild[])] : [_child] [pushRefToArray],
)
return items.map((child) => {
return (
<MenuItemListElement isFirstMenuItem={isFirstMenuItem} ref={pushRefToArray}>
{child}
</MenuItemListElement>
)
})
}
return ( return (
<menu <menu

View File

@@ -85,7 +85,6 @@ type ListElementProps = {
export const MenuItemListElement: FunctionComponent<ListElementProps> = forwardRef( export const MenuItemListElement: FunctionComponent<ListElementProps> = forwardRef(
({ children, isFirstMenuItem }: ListElementProps, ref: Ref<HTMLLIElement>) => { ({ children, isFirstMenuItem }: ListElementProps, ref: Ref<HTMLLIElement>) => {
const child = children as VNode<unknown> const child = children as VNode<unknown>
return ( return (
<li className="list-style-none" role="none" ref={ref}> <li className="list-style-none" role="none" ref={ref}>
{{ {{

View File

@@ -1,5 +1,5 @@
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { IlNotesIcon } from '@standardnotes/stylekit' import { IlNotesIcon } from '@standardnotes/icons'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel' import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'

View File

@@ -1,6 +1,7 @@
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useCallback } from 'preact/hooks'
type Props = { appState: AppState } type Props = { appState: AppState }
@@ -9,23 +10,28 @@ export const NoAccountWarning = observer(({ appState }: Props) => {
if (!canShow) { if (!canShow) {
return null return null
} }
const showAccountMenu = useCallback(
(event: Event) => {
event.stopPropagation()
appState.accountMenu.setShow(true)
},
[appState],
)
const hideWarning = useCallback(() => {
appState.noAccountWarning.hide()
}, [appState])
return ( return (
<div className="mt-5 p-5 rounded-md shadow-sm grid grid-template-cols-1fr"> <div className="mt-5 p-5 rounded-md shadow-sm grid grid-template-cols-1fr">
<h1 className="sk-h3 m-0 font-semibold">Data not backed up</h1> <h1 className="sk-h3 m-0 font-semibold">Data not backed up</h1>
<p className="m-0 mt-1 col-start-1 col-end-3">Sign in or register to back up your notes.</p> <p className="m-0 mt-1 col-start-1 col-end-3">Sign in or register to back up your notes.</p>
<button <button className="sn-button small info mt-3 col-start-1 col-end-3 justify-self-start" onClick={showAccountMenu}>
className="sn-button small info mt-3 col-start-1 col-end-3 justify-self-start"
onClick={(event) => {
event.stopPropagation()
appState.accountMenu.setShow(true)
}}
>
Open Account menu Open Account menu
</button> </button>
<button <button
onClick={() => { onClick={hideWarning}
appState.noAccountWarning.hide()
}}
title="Ignore" title="Ignore"
label="Ignore" label="Ignore"
style="height: 20px" style="height: 20px"

View File

@@ -15,6 +15,8 @@ type Props = {
} }
export class NoteGroupView extends PureComponent<Props, State> { export class NoteGroupView extends PureComponent<Props, State> {
private removeChangeObserver!: () => void
constructor(props: Props) { constructor(props: Props) {
super(props, props.application) super(props, props.application)
this.state = { this.state = {
@@ -25,18 +27,32 @@ export class NoteGroupView extends PureComponent<Props, State> {
override componentDidMount(): void { override componentDidMount(): void {
super.componentDidMount() super.componentDidMount()
this.application.noteControllerGroup.addActiveControllerChangeObserver(() => {
const controllerGroup = this.application.noteControllerGroup
this.removeChangeObserver = this.application.noteControllerGroup.addActiveControllerChangeObserver(() => {
const controllers = controllerGroup.noteControllers
this.setState({ this.setState({
controllers: this.application.noteControllerGroup.noteControllers, controllers: controllers,
}) })
}) })
this.autorun(() => { this.autorun(() => {
this.setState({ if (this.appState && this.appState.notes) {
showMultipleSelectedNotes: this.appState.notes.selectedNotesCount > 1, this.setState({
}) showMultipleSelectedNotes: this.appState.notes.selectedNotesCount > 1,
})
}
}) })
} }
override deinit() {
this.removeChangeObserver?.()
;(this.removeChangeObserver as unknown) = undefined
super.deinit()
}
override render() { override render() {
return ( return (
<div id={ElementIds.EditorColumn} className="h-full app-column app-column-third"> <div id={ElementIds.EditorColumn} className="h-full app-column app-column-third">

View File

@@ -1,5 +1,5 @@
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { useEffect, useRef, useState } from 'preact/hooks' import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { SNTag } from '@standardnotes/snjs' import { SNTag } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
@@ -24,40 +24,49 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
const prefixTitle = noteTags.getPrefixTitle(tag) const prefixTitle = noteTags.getPrefixTitle(tag)
const longTitle = noteTags.getLongTitle(tag) const longTitle = noteTags.getLongTitle(tag)
const deleteTag = () => { const deleteTag = useCallback(() => {
appState.noteTags.focusPreviousTag(tag) appState.noteTags.focusPreviousTag(tag)
appState.noteTags.removeTagFromActiveNote(tag).catch(console.error) appState.noteTags.removeTagFromActiveNote(tag).catch(console.error)
} }, [appState, tag])
const onDeleteTagClick = (event: MouseEvent) => { const onDeleteTagClick = useCallback(
event.stopImmediatePropagation() (event: MouseEvent) => {
event.stopPropagation() event.stopImmediatePropagation()
deleteTag() event.stopPropagation()
} deleteTag()
},
[deleteTag],
)
const onTagClick = (event: MouseEvent) => { const onTagClick = useCallback(
if (tagClicked && event.target !== deleteTagRef.current) { (event: MouseEvent) => {
setTagClicked(false) if (tagClicked && event.target !== deleteTagRef.current) {
appState.selectedTag = tag setTagClicked(false)
} else { appState.selectedTag = tag
setTagClicked(true) } else {
} setTagClicked(true)
} }
},
[appState, tagClicked, tag],
)
const onFocus = () => { const onFocus = useCallback(() => {
appState.noteTags.setFocusedTagUuid(tag.uuid) appState.noteTags.setFocusedTagUuid(tag.uuid)
setShowDeleteButton(true) setShowDeleteButton(true)
} }, [appState, tag])
const onBlur = (event: FocusEvent) => { const onBlur = useCallback(
const relatedTarget = event.relatedTarget as Node (event: FocusEvent) => {
if (relatedTarget !== deleteTagRef.current) { const relatedTarget = event.relatedTarget as Node
appState.noteTags.setFocusedTagUuid(undefined) if (relatedTarget !== deleteTagRef.current) {
setShowDeleteButton(false) appState.noteTags.setFocusedTagUuid(undefined)
} setShowDeleteButton(false)
} }
},
[appState],
)
const getTabIndex = () => { const getTabIndex = useCallback(() => {
if (focusedTagUuid) { if (focusedTagUuid) {
return focusedTagUuid === tag.uuid ? 0 : -1 return focusedTagUuid === tag.uuid ? 0 : -1
} }
@@ -65,34 +74,37 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
return -1 return -1
} }
return tags[0].uuid === tag.uuid ? 0 : -1 return tags[0].uuid === tag.uuid ? 0 : -1
} }, [autocompleteInputFocused, tags, tag, focusedTagUuid])
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = useCallback(
const tagIndex = appState.noteTags.getTagIndex(tag, tags) (event: KeyboardEvent) => {
switch (event.key) { const tagIndex = appState.noteTags.getTagIndex(tag, tags)
case 'Backspace': switch (event.key) {
deleteTag() case 'Backspace':
break deleteTag()
case 'ArrowLeft': break
appState.noteTags.focusPreviousTag(tag) case 'ArrowLeft':
break appState.noteTags.focusPreviousTag(tag)
case 'ArrowRight': break
if (tagIndex === tags.length - 1) { case 'ArrowRight':
appState.noteTags.setAutocompleteInputFocused(true) if (tagIndex === tags.length - 1) {
} else { appState.noteTags.setAutocompleteInputFocused(true)
appState.noteTags.focusNextTag(tag) } else {
} appState.noteTags.focusNextTag(tag)
break }
default: break
return default:
} return
} }
},
[appState, deleteTag, tag, tags],
)
useEffect(() => { useEffect(() => {
if (focusedTagUuid === tag.uuid) { if (focusedTagUuid === tag.uuid) {
tagRef.current?.focus() tagRef.current?.focus()
} }
}, [appState.noteTags, focusedTagUuid, tag]) }, [appState, focusedTagUuid, tag])
return ( return (
<button <button

View File

@@ -3,17 +3,22 @@ import { observer } from 'mobx-react-lite'
import { AutocompleteTagInput } from '@/Components/TagAutocomplete/AutocompleteTagInput' import { AutocompleteTagInput } from '@/Components/TagAutocomplete/AutocompleteTagInput'
import { NoteTag } from './NoteTag' import { NoteTag } from './NoteTag'
import { useEffect } from 'preact/hooks' import { useEffect } from 'preact/hooks'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = { type Props = {
appState: AppState appState: AppState
} }
export const NoteTagsContainer = observer(({ appState }: Props) => { export const NoteTagsContainer = observer(({ appState }: Props) => {
if (isStateDealloced(appState)) {
return null
}
const { tags, tagsContainerMaxWidth } = appState.noteTags const { tags, tagsContainerMaxWidth } = appState.noteTags
useEffect(() => { useEffect(() => {
appState.noteTags.reloadTagsContainerMaxWidth() appState.noteTags.reloadTagsContainerMaxWidth()
}, [appState.noteTags]) }, [appState])
return ( return (
<div <div

View File

@@ -134,6 +134,7 @@ export class NoteView extends PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props, props.application) super(props, props.application)
this.controller = props.controller this.controller = props.controller
this.onEditorComponentLoad = () => { this.onEditorComponentLoad = () => {
@@ -171,19 +172,42 @@ export class NoteView extends PureComponent<Props, State> {
override deinit() { override deinit() {
this.removeComponentStreamObserver?.() this.removeComponentStreamObserver?.()
;(this.removeComponentStreamObserver as unknown) = undefined ;(this.removeComponentStreamObserver as unknown) = undefined
this.removeInnerNoteObserver?.() this.removeInnerNoteObserver?.()
;(this.removeInnerNoteObserver as unknown) = undefined ;(this.removeInnerNoteObserver as unknown) = undefined
this.removeComponentManagerObserver?.() this.removeComponentManagerObserver?.()
;(this.removeComponentManagerObserver as unknown) = undefined ;(this.removeComponentManagerObserver as unknown) = undefined
this.removeTrashKeyObserver?.() this.removeTrashKeyObserver?.()
this.removeTrashKeyObserver = undefined this.removeTrashKeyObserver = undefined
this.clearNoteProtectionInactivityTimer() this.clearNoteProtectionInactivityTimer()
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
;(this.controller as unknown) = undefined
this.removeTabObserver?.() this.removeTabObserver?.()
this.removeTabObserver = undefined this.removeTabObserver = undefined
this.onEditorComponentLoad = undefined this.onEditorComponentLoad = undefined
this.statusTimeout = undefined this.statusTimeout = undefined
;(this.onPanelResizeFinish as unknown) = undefined ;(this.onPanelResizeFinish as unknown) = undefined
super.deinit() super.deinit()
;(this.dismissProtectedWarning as unknown) = undefined
;(this.editorComponentViewerRequestsReload as unknown) = undefined
;(this.onTextAreaChange as unknown) = undefined
;(this.onTitleEnter as unknown) = undefined
;(this.onTitleChange as unknown) = undefined
;(this.onContentFocus as unknown) = undefined
;(this.onPanelResizeFinish as unknown) = undefined
;(this.stackComponentExpanded as unknown) = undefined
;(this.toggleStackComponent as unknown) = undefined
;(this.setScrollPosition as unknown) = undefined
;(this.resetScrollPosition as unknown) = undefined
;(this.onSystemEditorLoad as unknown) = undefined
;(this.debounceReloadEditorComponent as unknown) = undefined
;(this.textAreaChangeDebounceSave as unknown) = undefined
;(this.editorContentRef as unknown) = undefined
} }
getState() { getState() {
@@ -295,6 +319,7 @@ export class NoteView extends PureComponent<Props, State> {
} }
super.componentWillUnmount() super.componentWillUnmount()
} }
override async onAppLaunch() { override async onAppLaunch() {
await super.onAppLaunch() await super.onAppLaunch()
this.streamItems() this.streamItems()
@@ -1016,7 +1041,7 @@ export class NoteView extends PureComponent<Props, State> {
readonly={this.state.noteLocked} readonly={this.state.noteLocked}
onFocus={this.onContentFocus} onFocus={this.onContentFocus}
spellcheck={this.state.spellcheck} spellcheck={this.state.spellcheck}
ref={(ref) => this.onSystemEditorLoad(ref)} ref={(ref) => ref && this.onSystemEditorLoad(ref)}
></textarea> ></textarea>
)} )}

View File

@@ -21,7 +21,7 @@ export const NotesContextMenu = observer(({ application, appState }: Props) => {
const reloadContextMenuLayout = useCallback(() => { const reloadContextMenuLayout = useCallback(() => {
appState.notes.reloadContextMenuLayout() appState.notes.reloadContextMenuLayout()
}, [appState.notes]) }, [appState])
useEffect(() => { useEffect(() => {
window.addEventListener('resize', reloadContextMenuLayout) window.addEventListener('resize', reloadContextMenuLayout)

View File

@@ -53,7 +53,7 @@ export const NotesListItem: FunctionComponent<Props> = ({
const showModifiedDate = sortedBy === CollectionSort.UpdatedAt const showModifiedDate = sortedBy === CollectionSort.UpdatedAt
const editorForNote = application.componentManager.editorForNote(note) const editorForNote = application.componentManager.editorForNote(note)
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
const [icon, tint] = application.iconsController.getIconAndTintForEditor(editorForNote?.identifier) const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
return ( return (
<div <div

View File

@@ -2,7 +2,7 @@ import { WebApplication } from '@/UIModels/Application'
import { CollectionSort, CollectionSortProperty, PrefKey } from '@standardnotes/snjs' import { CollectionSort, CollectionSortProperty, PrefKey } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useState } from 'preact/hooks' import { useCallback, useState } from 'preact/hooks'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { Menu } from '@/Components/Menu/Menu' import { Menu } from '@/Components/Menu/Menu'
import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem' import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem'
@@ -31,71 +31,74 @@ export const NotesListOptionsMenu: FunctionComponent<Props> = observer(
application.getPreference(PrefKey.NotesHideEditorIcon, false), application.getPreference(PrefKey.NotesHideEditorIcon, false),
) )
const toggleSortReverse = () => { const toggleSortReverse = useCallback(() => {
application.setPreference(PrefKey.SortNotesReverse, !sortReverse).catch(console.error) application.setPreference(PrefKey.SortNotesReverse, !sortReverse).catch(console.error)
setSortReverse(!sortReverse) setSortReverse(!sortReverse)
} }, [application, sortReverse])
const toggleSortBy = (sort: CollectionSortProperty) => { const toggleSortBy = useCallback(
if (sortBy === sort) { (sort: CollectionSortProperty) => {
toggleSortReverse() if (sortBy === sort) {
} else { toggleSortReverse()
setSortBy(sort) } else {
application.setPreference(PrefKey.SortNotesBy, sort).catch(console.error) setSortBy(sort)
} application.setPreference(PrefKey.SortNotesBy, sort).catch(console.error)
} }
},
[application, sortBy, toggleSortReverse],
)
const toggleSortByDateModified = () => { const toggleSortByDateModified = useCallback(() => {
toggleSortBy(CollectionSort.UpdatedAt) toggleSortBy(CollectionSort.UpdatedAt)
} }, [toggleSortBy])
const toggleSortByCreationDate = () => { const toggleSortByCreationDate = useCallback(() => {
toggleSortBy(CollectionSort.CreatedAt) toggleSortBy(CollectionSort.CreatedAt)
} }, [toggleSortBy])
const toggleSortByTitle = () => { const toggleSortByTitle = useCallback(() => {
toggleSortBy(CollectionSort.Title) toggleSortBy(CollectionSort.Title)
} }, [toggleSortBy])
const toggleHidePreview = () => { const toggleHidePreview = useCallback(() => {
setHidePreview(!hidePreview) setHidePreview(!hidePreview)
application.setPreference(PrefKey.NotesHideNotePreview, !hidePreview).catch(console.error) application.setPreference(PrefKey.NotesHideNotePreview, !hidePreview).catch(console.error)
} }, [application, hidePreview])
const toggleHideDate = () => { const toggleHideDate = useCallback(() => {
setHideDate(!hideDate) setHideDate(!hideDate)
application.setPreference(PrefKey.NotesHideDate, !hideDate).catch(console.error) application.setPreference(PrefKey.NotesHideDate, !hideDate).catch(console.error)
} }, [application, hideDate])
const toggleHideTags = () => { const toggleHideTags = useCallback(() => {
setHideTags(!hideTags) setHideTags(!hideTags)
application.setPreference(PrefKey.NotesHideTags, !hideTags).catch(console.error) application.setPreference(PrefKey.NotesHideTags, !hideTags).catch(console.error)
} }, [application, hideTags])
const toggleHidePinned = () => { const toggleHidePinned = useCallback(() => {
setHidePinned(!hidePinned) setHidePinned(!hidePinned)
application.setPreference(PrefKey.NotesHidePinned, !hidePinned).catch(console.error) application.setPreference(PrefKey.NotesHidePinned, !hidePinned).catch(console.error)
} }, [application, hidePinned])
const toggleShowArchived = () => { const toggleShowArchived = useCallback(() => {
setShowArchived(!showArchived) setShowArchived(!showArchived)
application.setPreference(PrefKey.NotesShowArchived, !showArchived).catch(console.error) application.setPreference(PrefKey.NotesShowArchived, !showArchived).catch(console.error)
} }, [application, showArchived])
const toggleShowTrashed = () => { const toggleShowTrashed = useCallback(() => {
setShowTrashed(!showTrashed) setShowTrashed(!showTrashed)
application.setPreference(PrefKey.NotesShowTrashed, !showTrashed).catch(console.error) application.setPreference(PrefKey.NotesShowTrashed, !showTrashed).catch(console.error)
} }, [application, showTrashed])
const toggleHideProtected = () => { const toggleHideProtected = useCallback(() => {
setHideProtected(!hideProtected) setHideProtected(!hideProtected)
application.setPreference(PrefKey.NotesHideProtected, !hideProtected).catch(console.error) application.setPreference(PrefKey.NotesHideProtected, !hideProtected).catch(console.error)
} }, [application, hideProtected])
const toggleEditorIcon = () => { const toggleEditorIcon = useCallback(() => {
setHideEditorIcon(!hideEditorIcon) setHideEditorIcon(!hideEditorIcon)
application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon).catch(console.error) application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon).catch(console.error)
} }, [application, hideEditorIcon])
return ( return (
<Menu <Menu

View File

@@ -7,6 +7,7 @@ import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { NotesListItem } from './NotesListItem' import { NotesListItem } from './NotesListItem'
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants' import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants'
import { useCallback } from 'preact/hooks'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -19,55 +20,72 @@ type Props = {
export const NotesList: FunctionComponent<Props> = observer( export const NotesList: FunctionComponent<Props> = observer(
({ application, appState, notes, selectedNotes, displayOptions, paginate }) => { ({ application, appState, notes, selectedNotes, displayOptions, paginate }) => {
const { selectPreviousNote, selectNextNote } = appState.notesView const selectNextNote = useCallback(() => appState.notesView.selectNextNote, [appState])
const selectPreviousNote = useCallback(() => appState.notesView.selectPreviousNote, [appState])
const { hideTags, hideDate, hideNotePreview, hideEditorIcon, sortBy } = displayOptions const { hideTags, hideDate, hideNotePreview, hideEditorIcon, sortBy } = displayOptions
const tagsForNote = (note: SNNote): string[] => { const tagsForNote = useCallback(
if (hideTags) { (note: SNNote): string[] => {
return [] if (hideTags) {
} return []
const selectedTag = appState.selectedTag }
if (!selectedTag) { const selectedTag = appState.selectedTag
return [] if (!selectedTag) {
} return []
const tags = appState.getNoteTags(note) }
if (selectedTag instanceof SNTag && tags.length === 1) { const tags = appState.getNoteTags(note)
return [] if (selectedTag instanceof SNTag && tags.length === 1) {
} return []
return tags.map((tag) => tag.title).sort() }
} return tags.map((tag) => tag.title).sort()
},
[appState, hideTags],
)
const openNoteContextMenu = (posX: number, posY: number) => { const openNoteContextMenu = useCallback(
appState.notes.setContextMenuClickLocation({ (posX: number, posY: number) => {
x: posX, appState.notes.setContextMenuClickLocation({
y: posY, x: posX,
}) y: posY,
appState.notes.reloadContextMenuLayout() })
appState.notes.setContextMenuOpen(true) appState.notes.reloadContextMenuLayout()
} appState.notes.setContextMenuOpen(true)
},
[appState],
)
const onContextMenu = (note: SNNote, posX: number, posY: number) => { const onContextMenu = useCallback(
appState.notes.selectNote(note.uuid, true).catch(console.error) (note: SNNote, posX: number, posY: number) => {
openNoteContextMenu(posX, posY) appState.notes.selectNote(note.uuid, true).catch(console.error)
} openNoteContextMenu(posX, posY)
},
[appState, openNoteContextMenu],
)
const onScroll = (e: Event) => { const onScroll = useCallback(
const offset = NOTES_LIST_SCROLL_THRESHOLD (e: Event) => {
const element = e.target as HTMLElement const offset = NOTES_LIST_SCROLL_THRESHOLD
if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) { const element = e.target as HTMLElement
paginate() if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) {
} paginate()
} }
},
[paginate],
)
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = useCallback(
if (e.key === KeyboardKey.Up) { (e: KeyboardEvent) => {
e.preventDefault() if (e.key === KeyboardKey.Up) {
selectPreviousNote() e.preventDefault()
} else if (e.key === KeyboardKey.Down) { selectPreviousNote()
e.preventDefault() } else if (e.key === KeyboardKey.Down) {
selectNextNote() e.preventDefault()
} selectNextNote()
} }
},
[selectNextNote, selectPreviousNote],
)
return ( return (
<div <div

View File

@@ -25,7 +25,7 @@ export const AddTagOption: FunctionComponent<Props> = observer(({ appState }) =>
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen) const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
const toggleTagsMenu = () => { const toggleTagsMenu = useCallback(() => {
if (!isMenuOpen) { if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current) const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) { if (menuPosition) {
@@ -34,7 +34,7 @@ export const AddTagOption: FunctionComponent<Props> = observer(({ appState }) =>
} }
setIsMenuOpen(!isMenuOpen) setIsMenuOpen(!isMenuOpen)
} }, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => { const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current) const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)

View File

@@ -4,7 +4,7 @@ import { AppState } from '@/UIModels/AppState'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { IconType, SNComponent, SNNote } from '@standardnotes/snjs' import { IconType, SNComponent, SNNote } from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' 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 { Icon } from '@/Components/Icon'
import { ChangeEditorMenu } from '@/Components/ChangeEditor/ChangeEditorMenu' import { ChangeEditorMenu } from '@/Components/ChangeEditor/ChangeEditorMenu'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
@@ -48,7 +48,7 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
setIsVisible(open) setIsVisible(open)
}) })
const toggleChangeEditorMenu = () => { const toggleChangeEditorMenu = useCallback(() => {
if (!isOpen) { if (!isOpen) {
const menuStyle = calculateSubmenuStyle(buttonRef.current) const menuStyle = calculateSubmenuStyle(buttonRef.current)
if (menuStyle) { if (menuStyle) {
@@ -57,7 +57,7 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
} }
setIsOpen(!isOpen) setIsOpen(!isOpen)
} }, [isOpen])
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {

View File

@@ -35,7 +35,7 @@ const ListedMenuItem: FunctionComponent<ListedMenuItemProps> = ({
}) => { }) => {
const [isRunning, setIsRunning] = useState(false) const [isRunning, setIsRunning] = useState(false)
const handleClick = async () => { const handleClick = useCallback(async () => {
if (isRunning) { if (isRunning) {
return return
} }
@@ -47,7 +47,7 @@ const ListedMenuItem: FunctionComponent<ListedMenuItemProps> = ({
setIsRunning(false) setIsRunning(false)
reloadMenuGroup(group).catch(console.error) reloadMenuGroup(group).catch(console.error)
} }, [application, action, group, isRunning, note, reloadMenuGroup])
return ( return (
<button <button
@@ -80,29 +80,32 @@ const ListedActionsMenu: FunctionComponent<ListedActionsMenuProps> = ({ applicat
const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([]) const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([])
const [isFetchingAccounts, setIsFetchingAccounts] = useState(true) const [isFetchingAccounts, setIsFetchingAccounts] = useState(true)
const reloadMenuGroup = async (group: ListedMenuGroup) => { const reloadMenuGroup = useCallback(
const updatedAccountInfo = await application.getListedAccountInfo(group.account, note.uuid) async (group: ListedMenuGroup) => {
const updatedAccountInfo = await application.getListedAccountInfo(group.account, note.uuid)
if (!updatedAccountInfo) { if (!updatedAccountInfo) {
return return
}
const updatedGroup: ListedMenuGroup = {
name: updatedAccountInfo.display_name,
account: group.account,
actions: updatedAccountInfo.actions as Action[],
}
const updatedGroups = menuGroups.map((group) => {
if (updatedGroup.account.authorId === group.account.authorId) {
return updatedGroup
} else {
return group
} }
})
setMenuGroups(updatedGroups) const updatedGroup: ListedMenuGroup = {
} name: updatedAccountInfo.display_name,
account: group.account,
actions: updatedAccountInfo.actions as Action[],
}
const updatedGroups = menuGroups.map((group) => {
if (updatedGroup.account.authorId === group.account.authorId) {
return updatedGroup
} else {
return group
}
})
setMenuGroups(updatedGroups)
},
[application, menuGroups, note],
)
useEffect(() => { useEffect(() => {
const fetchListedAccounts = async () => { const fetchListedAccounts = async () => {
@@ -217,7 +220,7 @@ export const ListedActionsOption: FunctionComponent<Props> = ({ application, not
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen) const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
const toggleListedMenu = () => { const toggleListedMenu = useCallback(() => {
if (!isMenuOpen) { if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current) const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) { if (menuPosition) {
@@ -226,7 +229,7 @@ export const ListedActionsOption: FunctionComponent<Props> = ({ application, not
} }
setIsMenuOpen(!isMenuOpen) setIsMenuOpen(!isMenuOpen)
} }, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => { const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current) const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)

View File

@@ -2,7 +2,7 @@ import { AppState } from '@/UIModels/AppState'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { Switch } from '@/Components/Switch' import { Switch } from '@/Components/Switch'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useState, useEffect, useMemo } from 'preact/hooks' import { useState, useEffect, useMemo, useCallback } from 'preact/hooks'
import { SNApplication, SNNote } from '@standardnotes/snjs' import { SNApplication, SNNote } from '@standardnotes/snjs'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { KeyboardModifier } from '@/Services/IOService' import { KeyboardModifier } from '@/Services/IOService'
@@ -211,13 +211,16 @@ export const NotesOptions = observer(({ application, appState, closeOnBlur }: No
} }
}, [application]) }, [application])
const getNoteFileName = (note: SNNote): string => { const getNoteFileName = useCallback(
const editor = application.componentManager.editorForNote(note) (note: SNNote): string => {
const format = editor?.package_info?.file_type || 'txt' const editor = application.componentManager.editorForNote(note)
return `${note.title}.${format}` const format = editor?.package_info?.file_type || 'txt'
} return `${note.title}.${format}`
},
[application],
)
const downloadSelectedItems = async () => { const downloadSelectedItems = useCallback(async () => {
if (notes.length === 1) { if (notes.length === 1) {
application.getArchiveService().downloadData(new Blob([notes[0].text]), getNoteFileName(notes[0])) application.getArchiveService().downloadData(new Blob([notes[0].text]), getNoteFileName(notes[0]))
return return
@@ -242,17 +245,17 @@ export const NotesOptions = observer(({ application, appState, closeOnBlur }: No
message: `Exported ${notes.length} notes`, message: `Exported ${notes.length} notes`,
}) })
} }
} }, [application, getNoteFileName, notes])
const duplicateSelectedItems = () => { const duplicateSelectedItems = useCallback(() => {
notes.forEach((note) => { notes.forEach((note) => {
application.mutator.duplicateItem(note).catch(console.error) application.mutator.duplicateItem(note).catch(console.error)
}) })
} }, [application, notes])
const openRevisionHistoryModal = () => { const openRevisionHistoryModal = useCallback(() => {
appState.notes.setShowRevisionHistoryModal(true) appState.notes.setShowRevisionHistoryModal(true)
} }, [appState])
return ( return (
<> <>

View File

@@ -5,7 +5,7 @@ import { PANEL_NAME_NOTES } from '@/Constants'
import { PrefKey } from '@standardnotes/snjs' import { PrefKey } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks' import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { NoAccountWarning } from '@/Components/NoAccountWarning' import { NoAccountWarning } from '@/Components/NoAccountWarning'
import { NotesList } from '@/Components/NotesList' import { NotesList } from '@/Components/NotesList'
import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu' import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu'
@@ -13,45 +13,46 @@ import { SearchOptions } from '@/Components/SearchOptions'
import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer' import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = { type Props = {
application: WebApplication application: WebApplication
appState: AppState appState: AppState
} }
export const NotesView: FunctionComponent<Props> = observer(({ application, appState }) => { export const NotesView: FunctionComponent<Props> = observer(({ application, appState }: Props) => {
if (isStateDealloced(appState)) {
return null
}
const notesViewPanelRef = useRef<HTMLDivElement>(null) const notesViewPanelRef = useRef<HTMLDivElement>(null)
const displayOptionsMenuRef = useRef<HTMLDivElement>(null) const displayOptionsMenuRef = useRef<HTMLDivElement>(null)
const { const {
completedFullSync, completedFullSync,
createNewNote,
displayOptions, displayOptions,
noteFilterText, noteFilterText,
optionsSubtitle, optionsSubtitle,
panelTitle, panelTitle,
renderedNotes, renderedNotes,
selectedNotes, selectedNotes,
setNoteFilterText,
searchBarElement, searchBarElement,
selectNextNote,
selectPreviousNote,
onFilterEnter,
handleFilterTextChanged,
clearFilterText,
paginate, paginate,
panelWidth, panelWidth,
} = appState.notesView } = appState.notesView
const createNewNote = useCallback(() => appState.notesView.createNewNote, [appState])
const onFilterEnter = useCallback(() => appState.notesView.onFilterEnter, [appState])
const clearFilterText = useCallback(() => appState.notesView.clearFilterText, [appState])
const setNoteFilterText = useCallback((text: string) => appState.notesView.setNoteFilterText(text), [appState])
const selectNextNote = useCallback(() => appState.notesView.selectNextNote, [appState])
const selectPreviousNote = useCallback(() => appState.notesView.selectPreviousNote, [appState])
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false) const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
const [focusedSearch, setFocusedSearch] = useState(false) const [focusedSearch, setFocusedSearch] = useState(false)
const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu) const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu)
useEffect(() => {
handleFilterTextChanged()
}, [noteFilterText, handleFilterTextChanged])
useEffect(() => { useEffect(() => {
/** /**
* In the browser we're not allowed to override cmd/ctrl + n, so we have to * In the browser we're not allowed to override cmd/ctrl + n, so we have to
@@ -63,7 +64,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl], modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl],
onKeyDown: (event) => { onKeyDown: (event) => {
event.preventDefault() event.preventDefault()
createNewNote().catch(console.error) createNewNote()
}, },
}) })
@@ -102,34 +103,43 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
previousNoteKeyObserver() previousNoteKeyObserver()
searchKeyObserver() searchKeyObserver()
} }
}, [application.io, createNewNote, searchBarElement, selectNextNote, selectPreviousNote]) }, [application, createNewNote, selectPreviousNote, searchBarElement, selectNextNote])
const onNoteFilterTextChange = (e: Event) => { const onNoteFilterTextChange = useCallback(
setNoteFilterText((e.target as HTMLInputElement).value) (e: Event) => {
} setNoteFilterText((e.target as HTMLInputElement).value)
},
[setNoteFilterText],
)
const onSearchFocused = () => setFocusedSearch(true) const onSearchFocused = useCallback(() => setFocusedSearch(true), [])
const onSearchBlurred = () => setFocusedSearch(false) const onSearchBlurred = useCallback(() => setFocusedSearch(false), [])
const onNoteFilterKeyUp = (e: KeyboardEvent) => { const onNoteFilterKeyUp = useCallback(
if (e.key === KeyboardKey.Enter) { (e: KeyboardEvent) => {
onFilterEnter() if (e.key === KeyboardKey.Enter) {
} onFilterEnter()
} }
},
[onFilterEnter],
)
const panelResizeFinishCallback: ResizeFinishCallback = (width, _lastLeft, _isMaxWidth, isCollapsed) => { const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error) (width, _lastLeft, _isMaxWidth, isCollapsed) => {
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
appState.noteTags.reloadTagsContainerMaxWidth()
appState.panelDidResize(PANEL_NAME_NOTES, isCollapsed)
},
[appState, application],
)
const panelWidthEventCallback = useCallback(() => {
appState.noteTags.reloadTagsContainerMaxWidth() appState.noteTags.reloadTagsContainerMaxWidth()
appState.panelDidResize(PANEL_NAME_NOTES, isCollapsed) }, [appState])
}
const panelWidthEventCallback = () => { const toggleDisplayOptionsMenu = useCallback(() => {
appState.noteTags.reloadTagsContainerMaxWidth()
}
const toggleDisplayOptionsMenu = () => {
setShowDisplayOptionsMenu(!showDisplayOptionsMenu) setShowDisplayOptionsMenu(!showDisplayOptionsMenu)
} }, [showDisplayOptionsMenu])
return ( return (
<div <div

View File

@@ -1,4 +1,4 @@
import { useRef } from 'preact/hooks' import { useCallback, useRef } from 'preact/hooks'
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog' import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
@@ -18,9 +18,10 @@ export const OtherSessionsSignOutContainer = observer((props: Props) => {
const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props) => { const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props) => {
const cancelRef = useRef<HTMLButtonElement>(null) const cancelRef = useRef<HTMLButtonElement>(null)
function closeDialog() {
const closeDialog = useCallback(() => {
appState.accountMenu.setOtherSessionsSignOut(false) appState.accountMenu.setOtherSessionsSignOut(false)
} }, [appState])
return ( return (
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}> <AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}>

View File

@@ -3,6 +3,8 @@ import VisuallyHidden from '@reach/visually-hidden'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { useCallback } from 'preact/hooks'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = { type Props = {
appState: AppState appState: AppState
@@ -11,11 +13,15 @@ type Props = {
} }
export const PinNoteButton: FunctionComponent<Props> = observer( export const PinNoteButton: FunctionComponent<Props> = observer(
({ appState, className = '', onClickPreprocessing }) => { ({ appState, className = '', onClickPreprocessing }: Props) => {
if (isStateDealloced(appState)) {
return null
}
const notes = Object.values(appState.notes.selectedNotes) const notes = Object.values(appState.notes.selectedNotes)
const pinned = notes.some((note) => note.pinned) const pinned = notes.some((note) => note.pinned)
const togglePinned = async () => { const togglePinned = useCallback(async () => {
if (onClickPreprocessing) { if (onClickPreprocessing) {
await onClickPreprocessing() await onClickPreprocessing()
} }
@@ -24,7 +30,7 @@ export const PinNoteButton: FunctionComponent<Props> = observer(
} else { } else {
appState.notes.setPinSelectedNotes(false) appState.notes.setPinSelectedNotes(false)
} }
} }, [appState, onClickPreprocessing, pinned])
return ( return (
<button <button

View File

@@ -5,7 +5,7 @@ import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { AccountIllustration } from '@standardnotes/stylekit' import { AccountIllustration } from '@standardnotes/icons'
export const Authentication: FunctionComponent<{ export const Authentication: FunctionComponent<{
application: WebApplication application: WebApplication

View File

@@ -34,7 +34,7 @@ export const FilesSection: FunctionComponent<Props> = ({ application }) => {
} }
getFilesQuota().catch(console.error) getFilesQuota().catch(console.error)
}, [application.settings]) }, [application])
return ( return (
<PreferencesGroup> <PreferencesGroup>

View File

@@ -1,7 +1,7 @@
import { PreferencesSegment, Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents' import { PreferencesSegment, Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents'
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks' import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
import { FileBackupMetadataFile, FileBackupsConstantsV1, FileContent, FileHandleRead } from '@standardnotes/snjs' import { FileBackupMetadataFile, FileBackupsConstantsV1, FileItem, FileHandleRead } from '@standardnotes/snjs'
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
import { EncryptionStatusItem } from '../../Security/Encryption' import { EncryptionStatusItem } from '../../Security/Encryption'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
@@ -16,7 +16,7 @@ type Props = {
export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => { export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
const [droppedFile, setDroppedFile] = useState<FileBackupMetadataFile | undefined>(undefined) const [droppedFile, setDroppedFile] = useState<FileBackupMetadataFile | undefined>(undefined)
const [decryptedFileContent, setDecryptedFileContent] = useState<FileContent | undefined>(undefined) const [decryptedFileItem, setDecryptedFileItem] = useState<FileItem | undefined>(undefined)
const [binaryFile, setBinaryFile] = useState<FileHandleRead | undefined>(undefined) const [binaryFile, setBinaryFile] = useState<FileHandleRead | undefined>(undefined)
const [isSavingAsDecrypted, setIsSavingAsDecrypted] = useState(false) const [isSavingAsDecrypted, setIsSavingAsDecrypted] = useState(false)
@@ -24,9 +24,9 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
useEffect(() => { useEffect(() => {
if (droppedFile) { if (droppedFile) {
void application.files.decryptBackupMetadataFile(droppedFile).then(setDecryptedFileContent) void application.files.decryptBackupMetadataFile(droppedFile).then(setDecryptedFileItem)
} else { } else {
setDecryptedFileContent(undefined) setDecryptedFileItem(undefined)
} }
}, [droppedFile, application]) }, [droppedFile, application])
@@ -41,20 +41,20 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
}, [application, fileSystem]) }, [application, fileSystem])
const downloadBinaryFileAsDecrypted = useCallback(async () => { const downloadBinaryFileAsDecrypted = useCallback(async () => {
if (!decryptedFileContent || !binaryFile) { if (!decryptedFileItem || !binaryFile) {
return return
} }
setIsSavingAsDecrypted(true) setIsSavingAsDecrypted(true)
const result = await application.files.readBackupFileAndSaveDecrypted(binaryFile, decryptedFileContent, fileSystem) const result = await application.files.readBackupFileAndSaveDecrypted(binaryFile, decryptedFileItem, fileSystem)
if (result === 'success') { if (result === 'success') {
void application.alertService.alert( void application.alertService.alert(
`<strong>${decryptedFileContent.name}</strong> has been successfully decrypted and saved to your chosen directory.`, `<strong>${decryptedFileItem.name}</strong> has been successfully decrypted and saved to your chosen directory.`,
) )
setBinaryFile(undefined) setBinaryFile(undefined)
setDecryptedFileContent(undefined) setDecryptedFileItem(undefined)
setDroppedFile(undefined) setDroppedFile(undefined)
} else if (result === 'failed') { } else if (result === 'failed') {
void application.alertService.alert( void application.alertService.alert(
@@ -63,7 +63,7 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
} }
setIsSavingAsDecrypted(false) setIsSavingAsDecrypted(false)
}, [decryptedFileContent, application, binaryFile, fileSystem]) }, [decryptedFileItem, application, binaryFile, fileSystem])
const handleDrag = useCallback( const handleDrag = useCallback(
(event: DragEvent) => { (event: DragEvent) => {
@@ -123,6 +123,12 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
const text = await file.text() const text = await file.text()
const type = application.files.isFileNameFileBackupRelated(file.name)
if (type === 'binary') {
void application.alertService.alert('Please drag the metadata file instead of the encrypted data file.')
return
}
try { try {
const metadata = JSON.parse(text) as FileBackupMetadataFile const metadata = JSON.parse(text) as FileBackupMetadataFile
setDroppedFile(metadata) setDroppedFile(metadata)
@@ -160,14 +166,14 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
return ( return (
<> <>
<PreferencesSegment> <PreferencesSegment>
{!decryptedFileContent && <Text>Attempting to decrypt metadata file...</Text>} {!decryptedFileItem && <Text>Attempting to decrypt metadata file...</Text>}
{decryptedFileContent && ( {decryptedFileItem && (
<> <>
<Title>Backup Decryption</Title> <Title>Backup Decryption</Title>
<EncryptionStatusItem <EncryptionStatusItem
status={decryptedFileContent.name} status={decryptedFileItem.name}
icon={[<Icon type="attachment-file" className="min-w-5 min-h-5" />]} icon={[<Icon type="attachment-file" className="min-w-5 min-h-5" />]}
checkmark={true} checkmark={true}
/> />

View File

@@ -22,7 +22,7 @@ type Props = {
export const FileBackups = observer(({ application }: Props) => { export const FileBackups = observer(({ application }: Props) => {
const [backupsEnabled, setBackupsEnabled] = useState(false) const [backupsEnabled, setBackupsEnabled] = useState(false)
const [backupsLocation, setBackupsLocation] = useState('') const [backupsLocation, setBackupsLocation] = useState('')
const backupsService = useMemo(() => application.fileBackups, [application.fileBackups]) const backupsService = useMemo(() => application.fileBackups, [application])
if (!backupsService) { if (!backupsService) {
return ( return (

View File

@@ -69,7 +69,7 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
.componentsForArea(ComponentArea.Editor) .componentsForArea(ComponentArea.Editor)
.map((editor): EditorOption => { .map((editor): EditorOption => {
const identifier = editor.package_info.identifier const identifier = editor.package_info.identifier
const [iconType, tint] = application.iconsController.getIconAndTintForEditor(identifier) const [iconType, tint] = application.iconsController.getIconAndTintForNoteType(editor.package_info.note_type)
return { return {
label: editor.name, label: editor.name,

View File

@@ -40,7 +40,7 @@ export const LabsPane: FunctionComponent<Props> = ({ application }) => {
} }
}) })
setExperimentalFeatures(experimentalFeatures) setExperimentalFeatures(experimentalFeatures)
}, [application.features]) }, [application])
useEffect(() => { useEffect(() => {
reloadExperimentalFeatures() reloadExperimentalFeatures()

View File

@@ -1,15 +1,14 @@
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { MouseEventHandler } from 'react'
import { useState, useRef, useEffect } from 'preact/hooks' import { useState, useRef, useEffect } from 'preact/hooks'
import { IconType } from '@standardnotes/snjs' import { IconType } from '@standardnotes/snjs'
const DisclosureIconButton: FunctionComponent<{ const DisclosureIconButton: FunctionComponent<{
className?: string className?: string
icon: IconType icon: IconType
onMouseEnter?: MouseEventHandler onMouseEnter?: any
onMouseLeave?: MouseEventHandler onMouseLeave?: any
}> = ({ className = '', icon, onMouseEnter, onMouseLeave }) => ( }> = ({ className = '', icon, onMouseEnter, onMouseLeave }) => (
<DisclosureButton <DisclosureButton
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
@@ -51,7 +50,7 @@ export const AuthAppInfoTooltip: FunctionComponent = () => {
/> />
<DisclosurePanel> <DisclosurePanel>
<div <div
className={`bg-inverted-default color-inverted-default text-center rounded shadow-overlay className={`bg-inverted-default color-inverted-default text-center rounded shadow-overlay
py-1.5 px-2 absolute w-103 -top-10 -left-51`} py-1.5 px-2 absolute w-103 -top-10 -left-51`}
> >
Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or

View File

@@ -1,8 +1,8 @@
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog' import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog'
import { FunctionalComponent } from 'preact' import { FunctionalComponent } from 'preact'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { PremiumIllustration } from '@standardnotes/stylekit' import { PremiumIllustration } from '@standardnotes/icons'
import { useRef } from 'preact/hooks' import { useCallback, useRef } from 'preact/hooks'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription' import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
@@ -23,13 +23,13 @@ export const PremiumFeaturesModal: FunctionalComponent<Props> = ({
}) => { }) => {
const plansButtonRef = useRef<HTMLButtonElement>(null) const plansButtonRef = useRef<HTMLButtonElement>(null)
const handleClick = () => { const handleClick = useCallback(() => {
if (hasSubscription) { if (hasSubscription) {
openSubscriptionDashboard(application) openSubscriptionDashboard(application)
} else if (window.plansUrl) { } else if (window.plansUrl) {
window.location.assign(window.plansUrl) window.location.assign(window.plansUrl)
} }
} }, [application, hasSubscription])
return showModal ? ( return showModal ? (
<AlertDialog leastDestructiveRef={plansButtonRef}> <AlertDialog leastDestructiveRef={plansButtonRef}>

View File

@@ -8,7 +8,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'
import { FloatingLabelInput } from '@/Components/Input/FloatingLabelInput' import { FloatingLabelInput } from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils' import { isEmailValid } from '@/Utils'
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper' import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/stylekit' import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/icons'
type Props = { type Props = {
appState: AppState appState: AppState

View File

@@ -8,7 +8,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'
import { FloatingLabelInput } from '@/Components/Input/FloatingLabelInput' import { FloatingLabelInput } from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils' import { isEmailValid } from '@/Utils'
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper' import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/stylekit' import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
type Props = { type Props = {
appState: AppState appState: AppState

View File

@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { CreateAccount } from './Panes/CreateAccount' import { CreateAccount } from './Panes/CreateAccount'
import { SignIn } from './Panes/SignIn' import { SignIn } from './Panes/SignIn'
import { SNLogoFull } from '@standardnotes/stylekit' import { SNLogoFull } from '@standardnotes/icons'
type PaneSelectorProps = { type PaneSelectorProps = {
currentPane: PurchaseFlowPane currentPane: PurchaseFlowPane

View File

@@ -1,7 +1,7 @@
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { FeatureStatus } from '@standardnotes/snjs' import { FeatureStatus } from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useMemo } from 'preact/hooks' import { useCallback, useMemo } from 'preact/hooks'
import { JSXInternal } from 'preact/src/jsx' import { JSXInternal } from 'preact/src/jsx'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { usePremiumModal } from '@/Hooks/usePremiumModal' import { usePremiumModal } from '@/Hooks/usePremiumModal'
@@ -27,19 +27,22 @@ export const ThemesMenuButton: FunctionComponent<Props> = ({ application, item,
) )
const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme]) const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme])
const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = (e) => { const toggleTheme: JSXInternal.MouseEventHandler<HTMLButtonElement> = useCallback(
e.preventDefault() (e) => {
e.preventDefault()
if (item.component && canActivateTheme) { if (item.component && canActivateTheme) {
const themeIsLayerableOrNotActive = item.component.isLayerable() || !item.component.active const themeIsLayerableOrNotActive = item.component.isLayerable() || !item.component.active
if (themeIsLayerableOrNotActive) { if (themeIsLayerableOrNotActive) {
application.mutator.toggleTheme(item.component).catch(console.error) application.mutator.toggleTheme(item.component).catch(console.error)
}
} else {
premiumModal.activate(`${item.name} theme`)
} }
} else { },
premiumModal.activate(`${item.name} theme`) [application, canActivateTheme, item, premiumModal],
} )
}
return ( return (
<button <button

View File

@@ -139,7 +139,7 @@ export const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(({ appli
const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen) const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen)
const toggleThemesMenu = () => { const toggleThemesMenu = useCallback(() => {
if (!themesMenuOpen && themesButtonRef.current) { if (!themesMenuOpen && themesButtonRef.current) {
const themesButtonRect = themesButtonRef.current.getBoundingClientRect() const themesButtonRect = themesButtonRef.current.getBoundingClientRect()
setThemesMenuPosition({ setThemesMenuPosition({
@@ -150,48 +150,57 @@ export const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(({ appli
} else { } else {
setThemesMenuOpen(false) setThemesMenuOpen(false)
} }
} }, [themesMenuOpen])
const openPreferences = () => { const openPreferences = useCallback(() => {
closeQuickSettingsMenu() closeQuickSettingsMenu()
appState.preferences.openPreferences() appState.preferences.openPreferences()
} }, [appState, closeQuickSettingsMenu])
const toggleComponent = (component: SNComponent) => { const toggleComponent = useCallback(
if (component.isTheme()) { (component: SNComponent) => {
application.mutator.toggleTheme(component).catch(console.error) if (component.isTheme()) {
} else { application.mutator.toggleTheme(component).catch(console.error)
application.mutator.toggleComponent(component).catch(console.error) } else {
} application.mutator.toggleComponent(component).catch(console.error)
} }
},
[application],
)
const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (event) => { const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = useCallback(
switch (event.key) { (event) => {
case 'Escape': switch (event.key) {
setThemesMenuOpen(false) case 'Escape':
themesButtonRef.current?.focus() setThemesMenuOpen(false)
break themesButtonRef.current?.focus()
case 'ArrowRight': break
if (!themesMenuOpen) { case 'ArrowRight':
toggleThemesMenu() if (!themesMenuOpen) {
} toggleThemesMenu()
} }
} }
},
[themesMenuOpen, toggleThemesMenu],
)
const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (event) => { const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = useCallback(
quickSettingsKeyDownHandler(closeQuickSettingsMenu, event, quickSettingsMenuRef, themesMenuOpen) (event) => {
} quickSettingsKeyDownHandler(closeQuickSettingsMenu, event, quickSettingsMenuRef, themesMenuOpen)
},
[closeQuickSettingsMenu, themesMenuOpen],
)
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => { const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback((event) => {
themesMenuKeyDownHandler(event, themesMenuRef, setThemesMenuOpen, themesButtonRef) themesMenuKeyDownHandler(event, themesMenuRef, setThemesMenuOpen, themesButtonRef)
} }, [])
const toggleDefaultTheme = () => { const toggleDefaultTheme = useCallback(() => {
const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable()) const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable())
if (activeTheme) { if (activeTheme) {
application.mutator.toggleTheme(activeTheme).catch(console.error) application.mutator.toggleTheme(activeTheme).catch(console.error)
} }
} }, [application, themes])
return ( return (
<div ref={mainRef} className="sn-component"> <div ref={mainRef} className="sn-component">

View File

@@ -2,8 +2,7 @@ import { WebApplication } from '@/UIModels/Application'
import { Action, ActionVerb, HistoryEntry, NoteHistoryEntry, RevisionListEntry, SNNote } from '@standardnotes/snjs' import { Action, ActionVerb, HistoryEntry, NoteHistoryEntry, RevisionListEntry, SNNote } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { StateUpdater, useCallback, useState } from 'preact/hooks' import { StateUpdater, useCallback, useState, useEffect } from 'preact/hooks'
import { useEffect } from 'react'
import { LegacyHistoryList } from './LegacyHistoryList' import { LegacyHistoryList } from './LegacyHistoryList'
import { RemoteHistoryList } from './RemoteHistoryList' import { RemoteHistoryList } from './RemoteHistoryList'
import { SessionHistoryList } from './SessionHistoryList' import { SessionHistoryList } from './SessionHistoryList'
@@ -67,7 +66,7 @@ export const HistoryListContainer: FunctionComponent<Props> = observer(
} }
fetchLegacyHistory().catch(console.error) fetchLegacyHistory().catch(console.error)
}, [application.actionsManager, note]) }, [application, note])
const TabButton: FunctionComponent<{ const TabButton: FunctionComponent<{
type: RevisionListTabType type: RevisionListTabType

View File

@@ -1,7 +1,7 @@
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import HistoryLockedIllustration from '../../../svg/il-history-locked.svg' import { HistoryLockedIllustration } from '@standardnotes/icons'
import { Button } from '@/Components/Button/Button' import { Button } from '@/Components/Button/Button'
const getPlanHistoryDuration = (planName: string | undefined) => { const getPlanHistoryDuration = (planName: string | undefined) => {

View File

@@ -64,7 +64,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
const note = Object.values(appState.notes.selectedNotes)[0] const note = Object.values(appState.notes.selectedNotes)[0]
const editorForCurrentNote = useMemo(() => { const editorForCurrentNote = useMemo(() => {
return application.componentManager.editorForNote(note) return application.componentManager.editorForNote(note)
}, [application.componentManager, note]) }, [application, note])
const [isFetchingSelectedRevision, setIsFetchingSelectedRevision] = useState(false) const [isFetchingSelectedRevision, setIsFetchingSelectedRevision] = useState(false)
const [selectedRevision, setSelectedRevision] = useState<HistoryEntry | LegacyHistoryEntry>() const [selectedRevision, setSelectedRevision] = useState<HistoryEntry | LegacyHistoryEntry>()
@@ -92,7 +92,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
setIsFetchingRemoteHistory(false) setIsFetchingRemoteHistory(false)
} }
} }
}, [application.historyManager, note]) }, [application, note])
useEffect(() => { useEffect(() => {
if (!remoteHistory?.length) { if (!remoteHistory?.length) {

View File

@@ -29,7 +29,7 @@ export const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentP
componentViewer.lockReadonly = true componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision componentViewer.overrideContextItem = templateNoteForRevision
return componentViewer return componentViewer
}, [application.componentManager, editorForCurrentNote, templateNoteForRevision]) }, [application, editorForCurrentNote, templateNoteForRevision])
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -37,7 +37,7 @@ export const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentP
application.componentManager.destroyComponentViewer(componentViewer) application.componentManager.destroyComponentViewer(componentViewer)
} }
} }
}, [application.componentManager, componentViewer]) }, [application, componentViewer])
return ( return (
<div className="flex flex-col h-full overflow-hidden"> <div className="flex flex-col h-full overflow-hidden">

View File

@@ -2,6 +2,7 @@ import { AppState } from '@/UIModels/AppState'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import Bubble from '@/Components/Bubble' import Bubble from '@/Components/Bubble'
import { useCallback } from 'preact/hooks'
type Props = { type Props = {
appState: AppState appState: AppState
@@ -13,9 +14,9 @@ export const SearchOptions = observer(({ appState }: Props) => {
const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions
async function toggleIncludeProtectedContents() { const toggleIncludeProtectedContents = useCallback(async () => {
await searchOptions.toggleIncludeProtectedContents() await searchOptions.toggleIncludeProtectedContents()
} }, [searchOptions])
return ( return (
<div role="tablist" className="search-options justify-center" onMouseDown={(e) => e.preventDefault()}> <div role="tablist" className="search-options justify-center" onMouseDown={(e) => e.preventDefault()}>

View File

@@ -1,6 +1,6 @@
import { FunctionalComponent } from 'preact' import { FunctionalComponent } from 'preact'
import { useRef, useState } from 'preact/hooks' import { useRef, useState } from 'preact/hooks'
import { ArrowDownCheckmarkIcon } from '@standardnotes/stylekit' import { ArrowDownCheckmarkIcon } from '@standardnotes/icons'
import { Title } from '@/Components/Preferences/PreferencesComponents' import { Title } from '@/Components/Preferences/PreferencesComponents'
type Props = { type Props = {

View File

@@ -1,6 +1,6 @@
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@node_modules/@reach/alert-dialog' import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog'
import { useRef } from '@node_modules/preact/hooks' import { useRef } from 'preact/hooks'
export const ModalDialog: FunctionComponent = ({ children }) => { export const ModalDialog: FunctionComponent = ({ children }) => {
const ldRef = useRef<HTMLButtonElement>(null) const ldRef = useRef<HTMLButtonElement>(null)

View File

@@ -2,15 +2,16 @@ import { CustomCheckboxContainer, CustomCheckboxInput, CustomCheckboxInputProps
import '@reach/checkbox/styles.css' import '@reach/checkbox/styles.css'
import { ComponentChildren, FunctionalComponent } from 'preact' import { ComponentChildren, FunctionalComponent } from 'preact'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
import { HTMLProps } from 'react'
export type SwitchProps = HTMLProps<HTMLInputElement> & { export type SwitchProps = {
checked?: boolean checked?: boolean
// Optional in case it is wrapped in a button (e.g. a menu item) // Optional in case it is wrapped in a button (e.g. a menu item)
onChange?: (checked: boolean) => void onChange?: (checked: boolean) => void
className?: string className?: string
children?: ComponentChildren children?: ComponentChildren
role?: string role?: string
disabled?: boolean
tabIndex?: number
} }
export const Switch: FunctionalComponent<SwitchProps> = (props: SwitchProps) => { export const Switch: FunctionalComponent<SwitchProps> = (props: SwitchProps) => {

View File

@@ -1,6 +1,6 @@
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useRef, useEffect } from 'preact/hooks' import { useRef, useEffect, useCallback } from 'preact/hooks'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
type Props = { type Props = {
@@ -15,36 +15,42 @@ export const AutocompleteTagHint = observer(({ appState, closeOnBlur }: Props) =
const { autocompleteSearchQuery, autocompleteTagResults } = appState.noteTags const { autocompleteSearchQuery, autocompleteTagResults } = appState.noteTags
const onTagHintClick = async () => { const onTagHintClick = useCallback(async () => {
await appState.noteTags.createAndAddNewTag() await appState.noteTags.createAndAddNewTag()
appState.noteTags.setAutocompleteInputFocused(true) appState.noteTags.setAutocompleteInputFocused(true)
} }, [appState])
const onFocus = () => { const onFocus = useCallback(() => {
appState.noteTags.setAutocompleteTagHintFocused(true) appState.noteTags.setAutocompleteTagHintFocused(true)
} }, [appState])
const onBlur = (event: FocusEvent) => { const onBlur = useCallback(
closeOnBlur(event) (event: FocusEvent) => {
appState.noteTags.setAutocompleteTagHintFocused(false) closeOnBlur(event)
} appState.noteTags.setAutocompleteTagHintFocused(false)
},
[appState, closeOnBlur],
)
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = useCallback(
if (event.key === 'ArrowUp') { (event: KeyboardEvent) => {
if (autocompleteTagResults.length > 0) { if (event.key === 'ArrowUp') {
const lastTagResult = autocompleteTagResults[autocompleteTagResults.length - 1] if (autocompleteTagResults.length > 0) {
appState.noteTags.setFocusedTagResultUuid(lastTagResult.uuid) const lastTagResult = autocompleteTagResults[autocompleteTagResults.length - 1]
} else { appState.noteTags.setFocusedTagResultUuid(lastTagResult.uuid)
appState.noteTags.setAutocompleteInputFocused(true) } else {
appState.noteTags.setAutocompleteInputFocused(true)
}
} }
} },
} [appState, autocompleteTagResults],
)
useEffect(() => { useEffect(() => {
if (autocompleteTagHintFocused) { if (autocompleteTagHintFocused) {
hintRef.current?.focus() hintRef.current?.focus()
} }
}, [appState.noteTags, autocompleteTagHintFocused]) }, [appState, autocompleteTagHintFocused])
return ( return (
<> <>

View File

@@ -94,7 +94,7 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
if (autocompleteInputFocused) { if (autocompleteInputFocused) {
inputRef.current?.focus() inputRef.current?.focus()
} }
}, [appState.noteTags, autocompleteInputFocused]) }, [appState, autocompleteInputFocused])
return ( return (
<div ref={containerRef}> <div ref={containerRef}>

View File

@@ -64,7 +64,7 @@ export const AutocompleteTagResult = observer(({ appState, tagResult, closeOnBlu
tagResultRef.current?.focus() tagResultRef.current?.focus()
appState.noteTags.setFocusedTagResultUuid(undefined) appState.noteTags.setFocusedTagResultUuid(undefined)
} }
}, [appState.noteTags, focusedTagResultUuid, tagResult]) }, [appState, focusedTagResultUuid, tagResult])
return ( return (
<button <button

View File

@@ -1,4 +1,5 @@
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { SmartViewsListItem } from './SmartViewsListItem' import { SmartViewsListItem } from './SmartViewsListItem'
@@ -7,7 +8,11 @@ type Props = {
appState: AppState appState: AppState
} }
export const SmartViewsList: FunctionComponent<Props> = observer(({ appState }) => { export const SmartViewsList: FunctionComponent<Props> = observer(({ appState }: Props) => {
if (isStateDealloced(appState)) {
return null
}
const allViews = appState.tags.smartViews const allViews = appState.tags.smartViews
return ( return (

View File

@@ -8,12 +8,17 @@ import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem'
import { usePremiumModal } from '@/Hooks/usePremiumModal' import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { SNTag } from '@standardnotes/snjs' import { SNTag } from '@standardnotes/snjs'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = { type Props = {
appState: AppState appState: AppState
} }
export const TagsContextMenu: FunctionComponent<Props> = observer(({ appState }) => { export const TagsContextMenu: FunctionComponent<Props> = observer(({ appState }: Props) => {
if (isStateDealloced(appState)) {
return null
}
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const selectedTag = appState.tags.selected const selectedTag = appState.tags.selected
@@ -28,7 +33,7 @@ export const TagsContextMenu: FunctionComponent<Props> = observer(({ appState })
const reloadContextMenuLayout = useCallback(() => { const reloadContextMenuLayout = useCallback(() => {
appState.tags.reloadContextMenuLayout() appState.tags.reloadContextMenuLayout()
}, [appState.tags]) }, [appState])
useEffect(() => { useEffect(() => {
window.addEventListener('resize', reloadContextMenuLayout) window.addEventListener('resize', reloadContextMenuLayout)
@@ -45,16 +50,16 @@ export const TagsContextMenu: FunctionComponent<Props> = observer(({ appState })
appState.tags.setContextMenuOpen(false) appState.tags.setContextMenuOpen(false)
appState.tags.setAddingSubtagTo(selectedTag) appState.tags.setAddingSubtagTo(selectedTag)
}, [appState.features.hasFolders, appState.tags, premiumModal, selectedTag]) }, [appState, selectedTag, premiumModal])
const onClickRename = useCallback(() => { const onClickRename = useCallback(() => {
appState.tags.setContextMenuOpen(false) appState.tags.setContextMenuOpen(false)
appState.tags.editingTag = selectedTag appState.tags.editingTag = selectedTag
}, [appState.tags, selectedTag]) }, [appState, selectedTag])
const onClickDelete = useCallback(() => { const onClickDelete = useCallback(() => {
appState.tags.remove(selectedTag, true).catch(console.error) appState.tags.remove(selectedTag, true).catch(console.error)
}, [appState.tags, selectedTag]) }, [appState, selectedTag])
return contextMenuOpen ? ( return contextMenuOpen ? (
<div <div

View File

@@ -1,8 +1,10 @@
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
import { isMobile } from '@/Utils' import { isMobile } from '@/Utils'
import { SNTag } from '@standardnotes/snjs' import { SNTag } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { useCallback } from 'preact/hooks'
import { DndProvider } from 'react-dnd' import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend' import { HTML5Backend } from 'react-dnd-html5-backend'
import { TouchBackend } from 'react-dnd-touch-backend' import { TouchBackend } from 'react-dnd-touch-backend'
@@ -13,25 +15,35 @@ type Props = {
appState: AppState appState: AppState
} }
export const TagsList: FunctionComponent<Props> = observer(({ appState }) => { export const TagsList: FunctionComponent<Props> = observer(({ appState }: Props) => {
if (isStateDealloced(appState)) {
return null
}
const tagsState = appState.tags const tagsState = appState.tags
const allTags = tagsState.allLocalRootTags const allTags = tagsState.allLocalRootTags
const backend = isMobile({ tablet: true }) ? TouchBackend : HTML5Backend const backend = isMobile({ tablet: true }) ? TouchBackend : HTML5Backend
const openTagContextMenu = (posX: number, posY: number) => { const openTagContextMenu = useCallback(
appState.tags.setContextMenuClickLocation({ (posX: number, posY: number) => {
x: posX, appState.tags.setContextMenuClickLocation({
y: posY, x: posX,
}) y: posY,
appState.tags.reloadContextMenuLayout() })
appState.tags.setContextMenuOpen(true) appState.tags.reloadContextMenuLayout()
} appState.tags.setContextMenuOpen(true)
},
[appState],
)
const onContextMenu = (tag: SNTag, posX: number, posY: number) => { const onContextMenu = useCallback(
appState.tags.selected = tag (tag: SNTag, posX: number, posY: number) => {
openTagContextMenu(posX, posY) appState.tags.selected = tag
} openTagContextMenu(posX, posY)
},
[appState, openTagContextMenu],
)
return ( return (
<DndProvider backend={backend}> <DndProvider backend={backend}>

View File

@@ -165,7 +165,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(({ tag, features,
const readyToDrop = isOver && canDrop const readyToDrop = isOver && canDrop
const toggleContextMenu = () => { const toggleContextMenu = useCallback(() => {
if (!menuButtonRef.current) { if (!menuButtonRef.current) {
return return
} }
@@ -178,7 +178,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(({ tag, features,
} else { } else {
onContextMenu(tag, menuButtonRect.right, menuButtonRect.top) onContextMenu(tag, menuButtonRect.right, menuButtonRect.top)
} }
} }, [onContextMenu, tagsState, tag])
return ( return (
<> <>

View File

@@ -16,16 +16,20 @@ export const TagsSection: FunctionComponent<Props> = observer(({ appState }) =>
const checkIfMigrationNeeded = useCallback(() => { const checkIfMigrationNeeded = useCallback(() => {
setHasMigration(appState.application.items.hasTagsNeedingFoldersMigration()) setHasMigration(appState.application.items.hasTagsNeedingFoldersMigration())
}, [appState.application]) }, [appState])
useEffect(() => { useEffect(() => {
appState.application.addEventObserver(async (event) => { const removeObserver = appState.application.addEventObserver(async (event) => {
const events = [ApplicationEvent.CompletedInitialSync, ApplicationEvent.SignedIn] const events = [ApplicationEvent.CompletedInitialSync, ApplicationEvent.SignedIn]
if (events.includes(event)) { if (events.includes(event)) {
checkIfMigrationNeeded() checkIfMigrationNeeded()
} }
}) })
}, [appState.application, checkIfMigrationNeeded])
return () => {
removeObserver()
}
}, [appState, checkIfMigrationNeeded])
const runMigration = useCallback(async () => { const runMigration = useCallback(async () => {
if ( if (
@@ -46,7 +50,7 @@ export const TagsSection: FunctionComponent<Props> = observer(({ appState }) =>
}) })
.catch(console.error) .catch(console.error)
} }
}, [appState.application, checkIfMigrationNeeded]) }, [appState, checkIfMigrationNeeded])
return ( return (
<section> <section>

View File

@@ -2,6 +2,8 @@ import { Environment, RawKeychainValue } from '@standardnotes/snjs'
import { WebOrDesktopDevice } from './WebOrDesktopDevice' import { WebOrDesktopDevice } from './WebOrDesktopDevice'
const KEYCHAIN_STORAGE_KEY = 'keychain' const KEYCHAIN_STORAGE_KEY = 'keychain'
const DESTROYED_DEVICE_URL_PARAM = 'destroyed'
const DESTROYED_DEVICE_URL_VALUE = 'true'
export class WebDevice extends WebOrDesktopDevice { export class WebDevice extends WebOrDesktopDevice {
environment = Environment.Web environment = Environment.Web
@@ -23,4 +25,17 @@ export class WebDevice extends WebOrDesktopDevice {
async clearRawKeychainValue(): Promise<void> { async clearRawKeychainValue(): Promise<void> {
localStorage.removeItem(KEYCHAIN_STORAGE_KEY) localStorage.removeItem(KEYCHAIN_STORAGE_KEY)
} }
async performHardReset(): Promise<void> {
const url = new URL(window.location.href)
const params = url.searchParams
params.append(DESTROYED_DEVICE_URL_PARAM, DESTROYED_DEVICE_URL_VALUE)
window.location.replace(url.href)
}
public isDeviceDestroyed(): boolean {
const url = new URL(window.location.href)
const params = url.searchParams
return params.get(DESTROYED_DEVICE_URL_PARAM) === DESTROYED_DEVICE_URL_VALUE
}
} }

View File

@@ -181,4 +181,12 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface
abstract setKeychainValue(value: unknown): Promise<void> abstract setKeychainValue(value: unknown): Promise<void>
abstract clearRawKeychainValue(): Promise<void> abstract clearRawKeychainValue(): Promise<void>
abstract isDeviceDestroyed(): boolean
abstract performHardReset(): Promise<void>
async performSoftReset(): Promise<void> {
window.location.reload()
}
} }

View File

@@ -1,9 +1,9 @@
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState' import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionalComponent } from 'preact' import { ComponentChildren, FunctionalComponent, createContext } from 'preact'
import { useContext } from 'preact/hooks' import { useCallback, useContext } from 'preact/hooks'
import { createContext } from 'react'
import { PremiumFeaturesModal } from '@/Components/PremiumFeaturesModal' import { PremiumFeaturesModal } from '@/Components/PremiumFeaturesModal'
type PremiumModalContextData = { type PremiumModalContextData = {
@@ -27,33 +27,50 @@ export const usePremiumModal = (): PremiumModalContextData => {
interface Props { interface Props {
application: WebApplication application: WebApplication
appState: AppState appState: AppState
children: ComponentChildren | ComponentChildren[]
} }
export const PremiumModalProvider: FunctionalComponent<Props> = observer(({ application, appState, children }) => { export const PremiumModalProvider: FunctionalComponent<Props> = observer(
const featureName = appState.features.premiumAlertFeatureName ({ application, appState, children }: Props) => {
const activate = appState.features.showPremiumAlert const dealloced = !appState || appState.dealloced == undefined
const close = appState.features.closePremiumAlert if (dealloced) {
return null
}
const showModal = !!featureName const featureName = appState.features.premiumAlertFeatureName || ''
const hasSubscription = Boolean( const showModal = !!featureName
appState.subscription.userSubscription &&
!appState.subscription.isUserSubscriptionExpired &&
!appState.subscription.isUserSubscriptionCanceled,
)
return ( const hasSubscription = Boolean(
<> appState.subscription.userSubscription &&
{showModal && ( !appState.subscription.isUserSubscriptionExpired &&
<PremiumFeaturesModal !appState.subscription.isUserSubscriptionCanceled,
application={application} )
featureName={featureName}
hasSubscription={hasSubscription} const activate = useCallback(
onClose={close} (feature: string) => {
showModal={!!featureName} appState.features.showPremiumAlert(feature).catch(console.error)
/> },
)} [appState],
<PremiumModalProvider_ value={{ activate }}>{children}</PremiumModalProvider_> )
</>
) const close = useCallback(() => {
}) appState.features.closePremiumAlert()
}, [appState])
return (
<>
{showModal && (
<PremiumFeaturesModal
application={application}
featureName={featureName}
hasSubscription={hasSubscription}
onClose={close}
showModal={!!featureName}
/>
)}
<PremiumModalProvider_ value={{ activate }}>{children}</PremiumModalProvider_>
</>
)
},
)

View File

@@ -62,6 +62,7 @@ export class IOService {
if (!modifier) { if (!modifier) {
return return
} }
switch (modifier) { switch (modifier) {
case KeyboardModifier.Meta: { case KeyboardModifier.Meta: {
if (this.isMac) { if (this.isMac) {
@@ -197,8 +198,10 @@ export class IOService {
addKeyObserver(observer: KeyboardObserver): () => void { addKeyObserver(observer: KeyboardObserver): () => void {
this.observers.push(observer) this.observers.push(observer)
const thislessObservers = this.observers
return () => { return () => {
removeFromArray(this.observers, observer) removeFromArray(thislessObservers, observer)
} }
} }
} }

View File

@@ -38,6 +38,7 @@ export class ThemeManager extends ApplicationService {
override async onAppEvent(event: ApplicationEvent) { override async onAppEvent(event: ApplicationEvent) {
super.onAppEvent(event).catch(console.error) super.onAppEvent(event).catch(console.error)
switch (event) { switch (event) {
case ApplicationEvent.SignedOut: { case ApplicationEvent.SignedOut: {
this.deactivateAllThemes() this.deactivateAllThemes()
@@ -91,6 +92,7 @@ export class ThemeManager extends ApplicationService {
;(this.unregisterStream as unknown) = undefined ;(this.unregisterStream as unknown) = undefined
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.colorSchemeEventHandler) window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.colorSchemeEventHandler)
super.deinit() super.deinit()
} }
@@ -226,6 +228,7 @@ export class ThemeManager extends ApplicationService {
public deactivateAllThemes() { public deactivateAllThemes() {
const activeThemes = this.activeThemes.slice() const activeThemes = this.activeThemes.slice()
for (const uuid of activeThemes) { for (const uuid of activeThemes) {
this.deactivateTheme(uuid) this.deactivateTheme(uuid)
} }

View File

@@ -0,0 +1,23 @@
import { DeinitSource } from '@standardnotes/snjs'
import { WebApplication } from '../Application'
export function isStateDealloced(state: AbstractState): boolean {
return state.dealloced == undefined || state.dealloced === true
}
export abstract class AbstractState {
application: WebApplication
appState?: AbstractState
dealloced = false
constructor(application: WebApplication, appState?: AbstractState) {
this.application = application
this.appState = appState
}
deinit(_source: DeinitSource): void {
this.dealloced = true
;(this.application as unknown) = undefined
;(this.appState as unknown) = undefined
}
}

View File

@@ -1,8 +1,9 @@
import { isDev } from '@/Utils' import { destroyAllObjectProperties, isDev } from '@/Utils'
import { action, computed, makeObservable, observable, runInAction } from 'mobx' import { action, computed, makeObservable, observable, runInAction } from 'mobx'
import { ApplicationEvent, ContentType, SNNote, SNTag } from '@standardnotes/snjs' import { ApplicationEvent, ContentType, DeinitSource, SNNote, SNTag } from '@standardnotes/snjs'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { AccountMenuPane } from '@/Components/AccountMenu' import { AccountMenuPane } from '@/Components/AccountMenu'
import { AbstractState } from './AbstractState'
type StructuredItemsCount = { type StructuredItemsCount = {
notes: number notes: number
@@ -11,7 +12,7 @@ type StructuredItemsCount = {
archived: number archived: number
} }
export class AccountMenuState { export class AccountMenuState extends AbstractState {
show = false show = false
signingOut = false signingOut = false
otherSessionsSignOut = false otherSessionsSignOut = false
@@ -26,7 +27,15 @@ export class AccountMenuState {
shouldAnimateCloseMenu = false shouldAnimateCloseMenu = false
currentPane = AccountMenuPane.GeneralMenu currentPane = AccountMenuPane.GeneralMenu
constructor(private application: WebApplication, private appEventListeners: (() => void)[]) { override deinit(source: DeinitSource) {
super.deinit(source)
;(this.notesAndTags as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, private appEventListeners: (() => void)[]) {
super(application)
makeObservable(this, { makeObservable(this, {
show: observable, show: observable,
signingOut: observable, signingOut: observable,

View File

@@ -1,7 +1,7 @@
import { storage, StorageKey } from '@/Services/LocalStorage' import { storage, StorageKey } from '@/Services/LocalStorage'
import { WebApplication, WebAppEvent } from '@/UIModels/Application' import { WebApplication, WebAppEvent } from '@/UIModels/Application'
import { AccountMenuState } from '@/UIModels/AppState/AccountMenuState' import { AccountMenuState } from '@/UIModels/AppState/AccountMenuState'
import { isDesktopApplication } from '@/Utils' import { destroyAllObjectProperties, isDesktopApplication } from '@/Utils'
import { import {
ApplicationEvent, ApplicationEvent,
ContentType, ContentType,
@@ -33,6 +33,7 @@ import { SubscriptionState } from './SubscriptionState'
import { SyncState } from './SyncState' import { SyncState } from './SyncState'
import { TagsState } from './TagsState' import { TagsState } from './TagsState'
import { FilePreviewModalState } from './FilePreviewModalState' import { FilePreviewModalState } from './FilePreviewModalState'
import { AbstractState } from './AbstractState'
export enum AppStateEvent { export enum AppStateEvent {
TagChanged, TagChanged,
@@ -57,35 +58,34 @@ export enum EventSource {
type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void> type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void>
export class AppState { export class AppState extends AbstractState {
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
application: WebApplication
observers: ObserverCallback[] = [] observers: ObserverCallback[] = []
locked = true locked = true
unsubApp: any unsubAppEventObserver!: () => void
webAppEventDisposer?: () => void webAppEventDisposer?: () => void
onVisibilityChange: any onVisibilityChange: () => void
showBetaWarning: boolean showBetaWarning: boolean
private multiEditorSupport = false private multiEditorSupport = false
readonly quickSettingsMenu = new QuickSettingsState()
readonly accountMenu: AccountMenuState readonly accountMenu: AccountMenuState
readonly actionsMenu = new ActionsMenuState() readonly actionsMenu = new ActionsMenuState()
readonly features: FeaturesState
readonly filePreviewModal = new FilePreviewModalState()
readonly files: FilesState
readonly noAccountWarning: NoAccountWarningState
readonly notes: NotesState
readonly notesView: NotesViewState
readonly noteTags: NoteTagsState
readonly preferences = new PreferencesState() readonly preferences = new PreferencesState()
readonly purchaseFlow: PurchaseFlowState readonly purchaseFlow: PurchaseFlowState
readonly noAccountWarning: NoAccountWarningState readonly quickSettingsMenu = new QuickSettingsState()
readonly noteTags: NoteTagsState
readonly sync = new SyncState()
readonly searchOptions: SearchOptionsState readonly searchOptions: SearchOptionsState
readonly notes: NotesState
readonly features: FeaturesState
readonly tags: TagsState
readonly notesView: NotesViewState
readonly subscription: SubscriptionState readonly subscription: SubscriptionState
readonly files: FilesState readonly sync = new SyncState()
readonly filePreviewModal = new FilePreviewModalState() readonly tags: TagsState
isSessionsModalVisible = false isSessionsModalVisible = false
@@ -94,7 +94,8 @@ export class AppState {
private readonly tagChangedDisposer: IReactionDisposer private readonly tagChangedDisposer: IReactionDisposer
constructor(application: WebApplication, private device: WebOrDesktopDeviceInterface) { constructor(application: WebApplication, private device: WebOrDesktopDeviceInterface) {
this.application = application super(application)
this.notes = new NotesState( this.notes = new NotesState(
application, application,
this, this,
@@ -103,6 +104,7 @@ export class AppState {
}, },
this.appEventObserverRemovers, this.appEventObserverRemovers,
) )
this.noteTags = new NoteTagsState(application, this, this.appEventObserverRemovers) this.noteTags = new NoteTagsState(application, this, this.appEventObserverRemovers)
this.features = new FeaturesState(application, this.appEventObserverRemovers) this.features = new FeaturesState(application, this.appEventObserverRemovers)
this.tags = new TagsState(application, this.appEventObserverRemovers, this.features) this.tags = new TagsState(application, this.appEventObserverRemovers, this.features)
@@ -144,41 +146,72 @@ export class AppState {
this.tagChangedDisposer = this.tagChangedNotifier() this.tagChangedDisposer = this.tagChangedNotifier()
} }
deinit(source: DeinitSource): void { override deinit(source: DeinitSource): void {
super.deinit(source)
if (source === DeinitSource.SignOut) { if (source === DeinitSource.SignOut) {
storage.remove(StorageKey.ShowBetaWarning) storage.remove(StorageKey.ShowBetaWarning)
this.noAccountWarning.reset() this.noAccountWarning.reset()
} }
;(this.application as unknown) = undefined
this.actionsMenu.reset() this.unsubAppEventObserver?.()
this.unsubApp?.() ;(this.unsubAppEventObserver as unknown) = undefined
this.unsubApp = undefined
this.observers.length = 0 this.observers.length = 0
this.appEventObserverRemovers.forEach((remover) => remover()) this.appEventObserverRemovers.forEach((remover) => remover())
this.appEventObserverRemovers.length = 0 this.appEventObserverRemovers.length = 0
;(this.features as unknown) = undefined ;(this.device as unknown) = undefined
this.webAppEventDisposer?.() this.webAppEventDisposer?.()
this.webAppEventDisposer = undefined this.webAppEventDisposer = undefined
;(this.quickSettingsMenu as unknown) = undefined ;(this.filePreviewModal as unknown) = undefined
;(this.accountMenu as unknown) = undefined
;(this.actionsMenu as unknown) = undefined
;(this.preferences as unknown) = undefined ;(this.preferences as unknown) = undefined
;(this.purchaseFlow as unknown) = undefined ;(this.quickSettingsMenu as unknown) = undefined
;(this.noteTags as unknown) = undefined
;(this.sync as unknown) = undefined ;(this.sync as unknown) = undefined
;(this.searchOptions as unknown) = undefined
;(this.notes as unknown) = undefined this.actionsMenu.reset()
;(this.actionsMenu as unknown) = undefined
this.features.deinit(source)
;(this.features as unknown) = undefined ;(this.features as unknown) = undefined
;(this.tags as unknown) = undefined
this.accountMenu.deinit(source)
;(this.accountMenu as unknown) = undefined
this.files.deinit(source)
;(this.files as unknown) = undefined
this.noAccountWarning.deinit(source)
;(this.noAccountWarning as unknown) = undefined
this.notes.deinit(source)
;(this.notes as unknown) = undefined
this.notesView.deinit(source)
;(this.notesView as unknown) = undefined ;(this.notesView as unknown) = undefined
this.noteTags.deinit(source)
;(this.noteTags as unknown) = undefined
this.purchaseFlow.deinit(source)
;(this.purchaseFlow as unknown) = undefined
this.searchOptions.deinit(source)
;(this.searchOptions as unknown) = undefined
this.subscription.deinit(source)
;(this.subscription as unknown) = undefined
this.tags.deinit(source)
;(this.tags as unknown) = undefined
document.removeEventListener('visibilitychange', this.onVisibilityChange) document.removeEventListener('visibilitychange', this.onVisibilityChange)
this.onVisibilityChange = undefined ;(this.onVisibilityChange as unknown) = undefined
this.tagChangedDisposer() this.tagChangedDisposer()
;(this.tagChangedDisposer as unknown) = undefined ;(this.tagChangedDisposer as unknown) = undefined
destroyAllObjectProperties(this)
} }
openSessionsModal(): void { openSessionsModal(): void {
@@ -333,7 +366,7 @@ export class AppState {
} }
addAppEventObserver() { addAppEventObserver() {
this.unsubApp = this.application.addEventObserver(async (eventName) => { this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
switch (eventName) { switch (eventName) {
case ApplicationEvent.Started: case ApplicationEvent.Started:
this.locked = true this.locked = true
@@ -370,11 +403,12 @@ export class AppState {
} }
} }
/** @returns A function that unregisters this observer */ addObserver(callback: ObserverCallback): () => void {
addObserver(callback: ObserverCallback) {
this.observers.push(callback) this.observers.push(callback)
const thislessObservers = this.observers
return () => { return () => {
removeFromArray(this.observers, callback) removeFromArray(thislessObservers, callback)
} }
} }

View File

@@ -1,14 +1,30 @@
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { ApplicationEvent, FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs' import { destroyAllObjectProperties } from '@/Utils'
import { ApplicationEvent, DeinitSource, FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { action, makeObservable, observable, runInAction, when } from 'mobx' import { action, makeObservable, observable, runInAction, when } from 'mobx'
import { AbstractState } from './AbstractState'
export class FeaturesState { export class FeaturesState extends AbstractState {
hasFolders: boolean hasFolders: boolean
hasSmartViews: boolean hasSmartViews: boolean
hasFiles: boolean hasFiles: boolean
premiumAlertFeatureName: string | undefined premiumAlertFeatureName: string | undefined
constructor(private application: WebApplication, appObservers: (() => void)[]) { override deinit(source: DeinitSource) {
super.deinit(source)
;(this.showPremiumAlert as unknown) = undefined
;(this.closePremiumAlert as unknown) = undefined
;(this.hasFolders as unknown) = undefined
;(this.hasSmartViews as unknown) = undefined
;(this.hasFiles as unknown) = undefined
;(this.premiumAlertFeatureName as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
this.hasFolders = this.isEntitledToFolders() this.hasFolders = this.isEntitledToFolders()
this.hasSmartViews = this.isEntitledToSmartViews() this.hasSmartViews = this.isEntitledToSmartViews()
this.hasFiles = this.isEntitledToFiles() this.hasFiles = this.isEntitledToFiles()

View File

@@ -1,10 +1,10 @@
import { SNFile } from '@standardnotes/snjs/dist/@types' import { FileItem } from '@standardnotes/snjs'
import { action, makeObservable, observable } from 'mobx' import { action, makeObservable, observable } from 'mobx'
export class FilePreviewModalState { export class FilePreviewModalState {
isOpen = false isOpen = false
currentFile: SNFile | undefined = undefined currentFile: FileItem | undefined = undefined
otherFiles: SNFile[] = [] otherFiles: FileItem[] = []
constructor() { constructor() {
makeObservable(this, { makeObservable(this, {
@@ -18,11 +18,11 @@ export class FilePreviewModalState {
}) })
} }
setCurrentFile = (currentFile: SNFile) => { setCurrentFile = (currentFile: FileItem) => {
this.currentFile = currentFile this.currentFile = currentFile
} }
activate = (currentFile: SNFile, otherFiles: SNFile[]) => { activate = (currentFile: FileItem, otherFiles: FileItem[]) => {
this.currentFile = currentFile this.currentFile = currentFile
this.otherFiles = otherFiles this.otherFiles = otherFiles
this.isOpen = true this.isOpen = true

View File

@@ -7,14 +7,12 @@ import {
ClassicFileSaver, ClassicFileSaver,
parseFileName, parseFileName,
} from '@standardnotes/filepicker' } from '@standardnotes/filepicker'
import { ClientDisplayableError, SNFile } from '@standardnotes/snjs' import { ClientDisplayableError, FileItem } from '@standardnotes/snjs'
import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/stylekit' import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/stylekit'
import { WebApplication } from '../Application' import { AbstractState } from './AbstractState'
export class FilesState { export class FilesState extends AbstractState {
constructor(private application: WebApplication) {} public async downloadFile(file: FileItem): Promise<void> {
public async downloadFile(file: SNFile): Promise<void> {
let downloadingToastId = '' let downloadingToastId = ''
try { try {
@@ -102,7 +100,7 @@ export class FilesState {
return return
} }
const uploadedFiles: SNFile[] = [] const uploadedFiles: FileItem[] = []
for (const file of selectedFiles) { for (const file of selectedFiles) {
if (!shouldUseStreamingReader && maxFileSize && file.size >= maxFileSize) { if (!shouldUseStreamingReader && maxFileSize && file.size >= maxFileSize) {

View File

@@ -1,10 +1,15 @@
import { storage, StorageKey } from '@/Services/LocalStorage' import { storage, StorageKey } from '@/Services/LocalStorage'
import { SNApplication, ApplicationEvent } from '@standardnotes/snjs' import { ApplicationEvent } from '@standardnotes/snjs'
import { runInAction, makeObservable, observable, action } from 'mobx' import { runInAction, makeObservable, observable, action } from 'mobx'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export class NoAccountWarningState { export class NoAccountWarningState extends AbstractState {
show: boolean show: boolean
constructor(application: SNApplication, appObservers: (() => void)[]) {
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
this.show = application.hasAccount() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true this.show = application.hasAccount() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true
appObservers.push( appObservers.push(

View File

@@ -1,10 +1,12 @@
import { ElementIds } from '@/ElementIDs' import { ElementIds } from '@/ElementIDs'
import { ApplicationEvent, ContentType, PrefKey, SNNote, SNTag, UuidString } from '@standardnotes/snjs' import { destroyAllObjectProperties } from '@/Utils'
import { ApplicationEvent, ContentType, DeinitSource, PrefKey, SNNote, SNTag, UuidString } from '@standardnotes/snjs'
import { action, computed, makeObservable, observable } from 'mobx' import { action, computed, makeObservable, observable } from 'mobx'
import { WebApplication } from '../Application' import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
import { AppState } from './AppState' import { AppState } from './AppState'
export class NoteTagsState { export class NoteTagsState extends AbstractState {
autocompleteInputFocused = false autocompleteInputFocused = false
autocompleteSearchQuery = '' autocompleteSearchQuery = ''
autocompleteTagHintFocused = false autocompleteTagHintFocused = false
@@ -15,7 +17,17 @@ export class NoteTagsState {
tagsContainerMaxWidth: number | 'auto' = 0 tagsContainerMaxWidth: number | 'auto' = 0
addNoteToParentFolders: boolean addNoteToParentFolders: boolean
constructor(private application: WebApplication, private appState: AppState, appEventListeners: (() => void)[]) { override deinit(source: DeinitSource) {
super.deinit(source)
;(this.tags as unknown) = undefined
;(this.autocompleteTagResults as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, override appState: AppState, appEventListeners: (() => void)[]) {
super(application, appState)
makeObservable(this, { makeObservable(this, {
autocompleteInputFocused: observable, autocompleteInputFocused: observable,
autocompleteSearchQuery: observable, autocompleteSearchQuery: observable,

View File

@@ -1,3 +1,4 @@
import { destroyAllObjectProperties } from '@/Utils'
import { confirmDialog } from '@/Services/AlertService' import { confirmDialog } from '@/Services/AlertService'
import { KeyboardModifier } from '@/Services/IOService' import { KeyboardModifier } from '@/Services/IOService'
import { StringEmptyTrash, Strings, StringUtils } from '@/Strings' import { StringEmptyTrash, Strings, StringUtils } from '@/Strings'
@@ -10,12 +11,14 @@ import {
SNTag, SNTag,
ChallengeReason, ChallengeReason,
NoteViewController, NoteViewController,
DeinitSource,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { makeObservable, observable, action, computed, runInAction } from 'mobx' import { makeObservable, observable, action, computed, runInAction } from 'mobx'
import { WebApplication } from '../Application' import { WebApplication } from '../Application'
import { AppState } from './AppState' import { AppState } from './AppState'
import { AbstractState } from './AbstractState'
export class NotesState { export class NotesState extends AbstractState {
lastSelectedNote: SNNote | undefined lastSelectedNote: SNNote | undefined
selectedNotes: Record<UuidString, SNNote> = {} selectedNotes: Record<UuidString, SNNote> = {}
contextMenuOpen = false contextMenuOpen = false
@@ -28,12 +31,23 @@ export class NotesState {
showProtectedWarning = false showProtectedWarning = false
showRevisionHistoryModal = false showRevisionHistoryModal = false
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.lastSelectedNote as unknown) = undefined
;(this.selectedNotes as unknown) = undefined
;(this.onActiveEditorChanged as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor( constructor(
private application: WebApplication, application: WebApplication,
private appState: AppState, public override appState: AppState,
private onActiveEditorChanged: () => Promise<void>, private onActiveEditorChanged: () => Promise<void>,
appEventListeners: (() => void)[], appEventListeners: (() => void)[],
) { ) {
super(application, appState)
makeObservable(this, { makeObservable(this, {
selectedNotes: observable, selectedNotes: observable,
contextMenuOpen: observable, contextMenuOpen: observable,
@@ -75,6 +89,10 @@ export class NotesState {
} }
get selectedNotesCount(): number { get selectedNotesCount(): number {
if (this.dealloced) {
return 0
}
return Object.keys(this.selectedNotes).length return Object.keys(this.selectedNotes).length
} }

View File

@@ -1,8 +1,10 @@
import { destroyAllObjectProperties } from '@/Utils'
import { import {
ApplicationEvent, ApplicationEvent,
CollectionSort, CollectionSort,
CollectionSortProperty, CollectionSortProperty,
ContentType, ContentType,
DeinitSource,
findInArray, findInArray,
NotesDisplayCriteria, NotesDisplayCriteria,
PrefKey, PrefKey,
@@ -15,6 +17,7 @@ import {
import { action, autorun, computed, makeObservable, observable, reaction } from 'mobx' import { action, autorun, computed, makeObservable, observable, reaction } from 'mobx'
import { AppState, AppStateEvent } from '.' import { AppState, AppStateEvent } from '.'
import { WebApplication } from '../Application' import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
const MIN_NOTE_CELL_HEIGHT = 51.0 const MIN_NOTE_CELL_HEIGHT = 51.0
const DEFAULT_LIST_NUM_NOTES = 20 const DEFAULT_LIST_NUM_NOTES = 20
@@ -34,7 +37,7 @@ export type DisplayOptions = {
hideEditorIcon: boolean hideEditorIcon: boolean
} }
export class NotesViewState { export class NotesViewState extends AbstractState {
completedFullSync = false completedFullSync = false
noteFilterText = '' noteFilterText = ''
notes: SNNote[] = [] notes: SNNote[] = []
@@ -59,22 +62,34 @@ export class NotesViewState {
hideEditorIcon: false, hideEditorIcon: false,
} }
constructor(private application: WebApplication, private appState: AppState, appObservers: (() => void)[]) { override deinit(source: DeinitSource) {
super.deinit(source)
;(this.noteFilterText as unknown) = undefined
;(this.notes as unknown) = undefined
;(this.renderedNotes as unknown) = undefined
;(this.selectedNotes as unknown) = undefined
;(window.onresize as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, override appState: AppState, appObservers: (() => void)[]) {
super(application, appState)
this.resetPagination() this.resetPagination()
appObservers.push( appObservers.push(
application.streamItems<SNNote>(ContentType.Note, () => { application.streamItems<SNNote>(ContentType.Note, () => {
this.reloadNotes() this.reloadNotes()
const activeNote = this.appState.notes.activeNoteController?.note const activeNote = appState.notes.activeNoteController?.note
if (this.application.getAppState().notes.selectedNotesCount < 2) { if (appState.notes.selectedNotesCount < 2) {
if (activeNote) { if (activeNote) {
const browsingTrashedNotes = const browsingTrashedNotes =
this.appState.selectedTag instanceof SmartView && appState.selectedTag instanceof SmartView && appState.selectedTag?.uuid === SystemViewId.TrashedNotes
this.appState.selectedTag?.uuid === SystemViewId.TrashedNotes
if (activeNote.trashed && !browsingTrashedNotes && !this.appState?.searchOptions.includeTrashed) { if (activeNote.trashed && !browsingTrashedNotes && !appState?.searchOptions.includeTrashed) {
this.selectNextOrCreateNew() this.selectNextOrCreateNew()
} else if (!this.selectedNotes[activeNote.uuid]) { } else if (!this.selectedNotes[activeNote.uuid]) {
this.selectNote(activeNote).catch(console.error) this.selectNote(activeNote).catch(console.error)
@@ -91,7 +106,7 @@ export class NotesViewState {
this.reloadNotesDisplayOptions() this.reloadNotesDisplayOptions()
this.reloadNotes() this.reloadNotes()
if (this.appState.selectedTag && findInArray(tags, 'uuid', this.appState.selectedTag.uuid)) { if (appState.selectedTag && findInArray(tags, 'uuid', appState.selectedTag.uuid)) {
/** Tag title could have changed */ /** Tag title could have changed */
this.reloadPanelTitle() this.reloadPanelTitle()
} }
@@ -100,7 +115,7 @@ export class NotesViewState {
this.reloadPreferences() this.reloadPreferences()
}, ApplicationEvent.PreferencesChanged), }, ApplicationEvent.PreferencesChanged),
application.addEventObserver(async () => { application.addEventObserver(async () => {
this.appState.closeAllNoteControllers() appState.closeAllNoteControllers()
this.selectFirstNote() this.selectFirstNote()
this.setCompletedFullSync(false) this.setCompletedFullSync(false)
}, ApplicationEvent.SignedIn), }, ApplicationEvent.SignedIn),
@@ -108,20 +123,22 @@ export class NotesViewState {
this.reloadNotes() this.reloadNotes()
if ( if (
this.notes.length === 0 && this.notes.length === 0 &&
this.appState.selectedTag instanceof SmartView && appState.selectedTag instanceof SmartView &&
this.appState.selectedTag.uuid === SystemViewId.AllNotes && appState.selectedTag.uuid === SystemViewId.AllNotes &&
this.noteFilterText === '' && this.noteFilterText === '' &&
!this.appState.notes.activeNoteController !appState.notes.activeNoteController
) { ) {
this.createPlaceholderNote()?.catch(console.error) this.createPlaceholderNote()?.catch(console.error)
} }
this.setCompletedFullSync(true) this.setCompletedFullSync(true)
}, ApplicationEvent.CompletedFullSync), }, ApplicationEvent.CompletedFullSync),
autorun(() => { autorun(() => {
if (appState.notes.selectedNotes) { if (appState.notes.selectedNotes) {
this.syncSelectedNotes() this.syncSelectedNotes()
} }
}), }),
reaction( reaction(
() => [ () => [
appState.searchOptions.includeProtectedContents, appState.searchOptions.includeProtectedContents,
@@ -133,6 +150,7 @@ export class NotesViewState {
this.reloadNotes() this.reloadNotes()
}, },
), ),
appState.addObserver(async (eventName) => { appState.addObserver(async (eventName) => {
if (eventName === AppStateEvent.TagChanged) { if (eventName === AppStateEvent.TagChanged) {
this.handleTagChange() this.handleTagChange()
@@ -414,6 +432,7 @@ export class NotesViewState {
selectPreviousNote = () => { selectPreviousNote = () => {
const displayableNotes = this.notes const displayableNotes = this.notes
if (this.activeEditorNote) { if (this.activeEditorNote) {
const currentIndex = displayableNotes.indexOf(this.activeEditorNote) const currentIndex = displayableNotes.indexOf(this.activeEditorNote)
if (currentIndex - 1 >= 0) { if (currentIndex - 1 >= 0) {
@@ -426,11 +445,13 @@ export class NotesViewState {
return false return false
} }
} }
return undefined return undefined
} }
setNoteFilterText = (text: string) => { setNoteFilterText = (text: string) => {
this.noteFilterText = text this.noteFilterText = text
this.handleFilterTextChanged()
} }
syncSelectedNotes = () => { syncSelectedNotes = () => {

View File

@@ -1,17 +1,20 @@
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper' import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { action, makeObservable, observable } from 'mobx' import { action, makeObservable, observable } from 'mobx'
import { WebApplication } from '../Application' import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export enum PurchaseFlowPane { export enum PurchaseFlowPane {
SignIn, SignIn,
CreateAccount, CreateAccount,
} }
export class PurchaseFlowState { export class PurchaseFlowState extends AbstractState {
isOpen = false isOpen = false
currentPane = PurchaseFlowPane.CreateAccount currentPane = PurchaseFlowPane.CreateAccount
constructor(private application: WebApplication) { constructor(application: WebApplication) {
super(application)
makeObservable(this, { makeObservable(this, {
isOpen: observable, isOpen: observable,
currentPane: observable, currentPane: observable,

View File

@@ -1,13 +1,16 @@
import { ApplicationEvent } from '@standardnotes/snjs' import { ApplicationEvent } from '@standardnotes/snjs'
import { makeObservable, observable, action, runInAction } from 'mobx' import { makeObservable, observable, action, runInAction } from 'mobx'
import { WebApplication } from '../Application' import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export class SearchOptionsState { export class SearchOptionsState extends AbstractState {
includeProtectedContents = false includeProtectedContents = false
includeArchived = false includeArchived = false
includeTrashed = false includeTrashed = false
constructor(private application: WebApplication, appObservers: (() => void)[]) { constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
makeObservable(this, { makeObservable(this, {
includeProtectedContents: observable, includeProtectedContents: observable,
includeTrashed: observable, includeTrashed: observable,

View File

@@ -1,6 +1,13 @@
import { ApplicationEvent, ClientDisplayableError, convertTimestampToMilliseconds } from '@standardnotes/snjs' import { destroyAllObjectProperties } from '@/Utils'
import {
ApplicationEvent,
ClientDisplayableError,
convertTimestampToMilliseconds,
DeinitSource,
} from '@standardnotes/snjs'
import { action, computed, makeObservable, observable } from 'mobx' import { action, computed, makeObservable, observable } from 'mobx'
import { WebApplication } from '../Application' import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
type Subscription = { type Subscription = {
planName: string planName: string
@@ -14,11 +21,21 @@ type AvailableSubscriptions = {
} }
} }
export class SubscriptionState { export class SubscriptionState extends AbstractState {
userSubscription: Subscription | undefined = undefined userSubscription: Subscription | undefined = undefined
availableSubscriptions: AvailableSubscriptions | undefined = undefined availableSubscriptions: AvailableSubscriptions | undefined = undefined
constructor(private application: WebApplication, appObservers: (() => void)[]) { override deinit(source: DeinitSource) {
super.deinit(source)
;(this.userSubscription as unknown) = undefined
;(this.availableSubscriptions as unknown) = undefined
destroyAllObjectProperties(this)
}
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
makeObservable(this, { makeObservable(this, {
userSubscription: observable, userSubscription: observable,
availableSubscriptions: observable, availableSubscriptions: observable,

View File

@@ -12,10 +12,13 @@ import {
UuidString, UuidString,
isSystemView, isSystemView,
FindItem, FindItem,
DeinitSource,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { action, computed, makeAutoObservable, makeObservable, observable, runInAction } from 'mobx' import { action, computed, makeAutoObservable, makeObservable, observable, runInAction } from 'mobx'
import { WebApplication } from '../Application' import { WebApplication } from '../Application'
import { FeaturesState } from './FeaturesState' import { FeaturesState } from './FeaturesState'
import { AbstractState } from './AbstractState'
import { destroyAllObjectProperties } from '@/Utils'
type AnyTag = SNTag | SmartView type AnyTag = SNTag | SmartView
@@ -56,7 +59,7 @@ const isValidFutureSiblings = (application: SNApplication, futureSiblings: SNTag
return true return true
} }
export class TagsState { export class TagsState extends AbstractState {
tags: SNTag[] = [] tags: SNTag[] = []
smartViews: SmartView[] = [] smartViews: SmartView[] = []
allNotesCount_ = 0 allNotesCount_ = 0
@@ -75,7 +78,9 @@ export class TagsState {
private readonly tagsCountsState: TagsCountsState private readonly tagsCountsState: TagsCountsState
constructor(private application: WebApplication, appEventListeners: (() => void)[], private features: FeaturesState) { constructor(application: WebApplication, appEventListeners: (() => void)[], private features: FeaturesState) {
super(application)
this.tagsCountsState = new TagsCountsState(this.application) this.tagsCountsState = new TagsCountsState(this.application)
this.selected_ = undefined this.selected_ = undefined
@@ -164,6 +169,19 @@ export class TagsState {
) )
} }
override deinit(source: DeinitSource) {
super.deinit(source)
;(this.features as unknown) = undefined
;(this.tags as unknown) = undefined
;(this.smartViews as unknown) = undefined
;(this.selected_ as unknown) = undefined
;(this.previouslySelected_ as unknown) = undefined
;(this.editing_ as unknown) = undefined
;(this.addingSubtagTo as unknown) = undefined
destroyAllObjectProperties(this)
}
async createSubtagAndAssignParent(parent: SNTag, title: string) { async createSubtagAndAssignParent(parent: SNTag, title: string) {
const hasEmptyTitle = title.length === 0 const hasEmptyTitle = title.length === 0

View File

@@ -17,6 +17,7 @@ import {
Runtime, Runtime,
DesktopDeviceInterface, DesktopDeviceInterface,
isDesktopDevice, isDesktopDevice,
DeinitMode,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
type WebServices = { type WebServices = {
@@ -68,13 +69,10 @@ export class WebApplication extends SNApplication {
this.iconsController = new IconsController() this.iconsController = new IconsController()
} }
override deinit(source: DeinitSource): void { override deinit(mode: DeinitMode, source: DeinitSource): void {
super.deinit(source) super.deinit(mode, source)
try {
if (source === DeinitSource.AppGroupUnload) {
this.getThemeService().deactivateAllThemes()
}
try {
for (const service of Object.values(this.webServices)) { for (const service of Object.values(this.webServices)) {
if (!service) { if (!service) {
continue continue
@@ -88,7 +86,10 @@ export class WebApplication extends SNApplication {
} }
this.webServices = {} as WebServices this.webServices = {} as WebServices
this.noteControllerGroup.deinit() this.noteControllerGroup.deinit()
;(this.noteControllerGroup as unknown) = undefined
this.webEventObservers.length = 0 this.webEventObservers.length = 0
} catch (error) { } catch (error) {
console.error('Error while deiniting application', error) console.error('Error while deiniting application', error)

View File

@@ -16,10 +16,47 @@ import { AutolockService } from '@/Services/AutolockService'
import { ThemeManager } from '@/Services/ThemeManager' import { ThemeManager } from '@/Services/ThemeManager'
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice' import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
const createApplication = (
descriptor: ApplicationDescriptor,
deviceInterface: WebOrDesktopDevice,
defaultSyncServerHost: string,
device: WebOrDesktopDevice,
runtime: Runtime,
webSocketUrl: string,
) => {
const platform = getPlatform()
const application = new WebApplication(
deviceInterface,
platform,
descriptor.identifier,
defaultSyncServerHost,
webSocketUrl,
runtime,
)
const appState = new AppState(application, device)
const archiveService = new ArchiveManager(application)
const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop)
const autolockService = new AutolockService(application, new InternalEventBus())
const themeService = new ThemeManager(application)
application.setWebServices({
appState,
archiveService,
desktopService: isDesktopDevice(device) ? new DesktopManager(application, device) : undefined,
io,
autolockService,
themeService,
})
return application
}
export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> { export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
constructor( constructor(
private defaultSyncServerHost: string, private defaultSyncServerHost: string,
private device: WebOrDesktopDevice, device: WebOrDesktopDevice,
private runtime: Runtime, private runtime: Runtime,
private webSocketUrl: string, private webSocketUrl: string,
) { ) {
@@ -27,8 +64,14 @@ export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
} }
override async initialize(): Promise<void> { override async initialize(): Promise<void> {
const defaultSyncServerHost = this.defaultSyncServerHost
const runtime = this.runtime
const webSocketUrl = this.webSocketUrl
await super.initialize({ await super.initialize({
applicationCreator: this.createApplication, applicationCreator: async (descriptor, device) => {
return createApplication(descriptor, device, defaultSyncServerHost, device, runtime, webSocketUrl)
},
}) })
if (isDesktopApplication()) { if (isDesktopApplication()) {
@@ -38,37 +81,15 @@ export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
} }
} }
override handleAllWorkspacesSignedOut(): void { override deinit() {
isDesktopDevice(this.deviceInterface) && this.deviceInterface.destroyAllData() super.deinit()
if (isDesktopApplication()) {
delete window.webClient
}
} }
private createApplication = (descriptor: ApplicationDescriptor, deviceInterface: WebOrDesktopDevice) => { override handleAllWorkspacesSignedOut(): void {
const platform = getPlatform() isDesktopDevice(this.device) && this.device.destroyAllData()
const application = new WebApplication(
deviceInterface,
platform,
descriptor.identifier,
this.defaultSyncServerHost,
this.webSocketUrl,
this.runtime,
)
const appState = new AppState(application, this.device)
const archiveService = new ArchiveManager(application)
const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop)
const autolockService = new AutolockService(application, new InternalEventBus())
const themeService = new ThemeManager(application)
application.setWebServices({
appState,
archiveService,
desktopService: isDesktopDevice(this.device) ? new DesktopManager(application, this.device) : undefined,
io,
autolockService,
themeService,
})
return application
} }
} }

View File

@@ -1,5 +1,11 @@
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
const isBackupRelatedFile = (item: DataTransferItem, application: WebApplication): boolean => {
const fileName = item.getAsFile()?.name || ''
const isBackupMetadataFile = application.files.isFileNameFileBackupRelated(fileName) !== false
return isBackupMetadataFile
}
export const isHandlingFileDrag = (event: DragEvent, application: WebApplication) => { export const isHandlingFileDrag = (event: DragEvent, application: WebApplication) => {
const items = event.dataTransfer?.items const items = event.dataTransfer?.items
@@ -8,10 +14,7 @@ export const isHandlingFileDrag = (event: DragEvent, application: WebApplication
} }
return Array.from(items).some((item) => { return Array.from(items).some((item) => {
const isFile = item.kind === 'file' return item.kind === 'file' && !isBackupRelatedFile(item, application)
const fileName = item.getAsFile()?.name || ''
const isBackupMetadataFile = application.files.isFileNameFileBackupMetadataFile(fileName)
return isFile && !isBackupMetadataFile
}) })
} }
@@ -23,9 +26,6 @@ export const isHandlingBackupDrag = (event: DragEvent, application: WebApplicati
} }
return Array.from(items).every((item) => { return Array.from(items).every((item) => {
const isFile = item.kind === 'file' return item.kind === 'file' && isBackupRelatedFile(item, application)
const fileName = item.getAsFile()?.name || ''
const isBackupMetadataFile = application.files.isFileNameFileBackupMetadataFile(fileName)
return isFile && isBackupMetadataFile
}) })
} }

Some files were not shown because too many files have changed in this diff Show More