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}

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