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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
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 { FunctionComponent } from 'preact'
import { useCallback, useEffect, useState } from 'preact/hooks'
@@ -17,13 +17,18 @@ type Props = {
}
export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }) => {
({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }: Props) => {
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([])
useEffect(() => {
const removeAppGroupObserver = mainApplicationGroup.addApplicationChangeObserver(() => {
const applicationDescriptors = mainApplicationGroup.getDescriptors()
setApplicationDescriptors(applicationDescriptors)
const applicationDescriptors = mainApplicationGroup.getDescriptors()
setApplicationDescriptors(applicationDescriptors)
const removeAppGroupObserver = mainApplicationGroup.addEventObserver((event) => {
if (event === ApplicationGroupEvent.DescriptorsDataChanged) {
const applicationDescriptors = mainApplicationGroup.getDescriptors()
setApplicationDescriptors(applicationDescriptors)
}
})
return () => {
@@ -42,20 +47,21 @@ export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
return
}
mainApplicationGroup.signOutAllWorkspaces().catch(console.error)
}, [mainApplicationGroup, appState.application.alertService])
}, [mainApplicationGroup, appState])
const destroyWorkspace = useCallback(() => {
appState.accountMenu.setSigningOut(true)
}, [appState])
return (
<Menu a11yLabel="Workspace switcher menu" className="px-0 focus:shadow-none" isOpen={isOpen}>
{applicationDescriptors.map((descriptor) => (
<WorkspaceMenuItem
key={descriptor.identifier}
descriptor={descriptor}
hideOptions={hideWorkspaceOptions}
onDelete={() => {
appState.accountMenu.setSigningOut(true)
}}
onClick={() => {
mainApplicationGroup.loadApplicationForDescriptor(descriptor)
}}
onDelete={destroyWorkspace}
onClick={() => void mainApplicationGroup.unloadCurrentAndActivateDescriptor(descriptor)}
renameDescriptor={(label: string) => mainApplicationGroup.renameDescriptor(descriptor, label)}
/>
))}
@@ -64,7 +70,7 @@ export const WorkspaceSwitcherMenu: FunctionComponent<Props> = observer(
<MenuItem
type={MenuItemType.IconButton}
onClick={() => {
mainApplicationGroup.addNewApplication()
void mainApplicationGroup.unloadCurrentAndCreateNewDescriptor()
}}
>
<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 { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { Icon } from '@/Components/Icon'
import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu'
@@ -19,7 +19,7 @@ export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(({ mai
const [isOpen, setIsOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
const toggleMenu = () => {
const toggleMenu = useCallback(() => {
if (!isOpen) {
const menuPosition = calculateSubmenuStyle(buttonRef.current)
if (menuPosition) {
@@ -28,7 +28,7 @@ export const WorkspaceSwitcherOption: FunctionComponent<Props> = observer(({ mai
}
setIsOpen(!isOpen)
}
}, [isOpen, setIsOpen])
useEffect(() => {
if (isOpen) {

View File

@@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { AppState } from '@/UIModels/AppState'
import { WebApplication } from '@/UIModels/Application'
import { useRef, useState } from 'preact/hooks'
import { useCallback, useRef, useState } from 'preact/hooks'
import { GeneralAccountMenu } from './GeneralAccountMenu'
import { FunctionComponent } from 'preact'
import { SignInPane } from './SignIn'
@@ -80,26 +80,40 @@ const MenuPaneSelector: FunctionComponent<PaneSelectorProps> = observer(
export const AccountMenu: FunctionComponent<Props> = observer(
({ application, appState, onClickOutside, mainApplicationGroup }) => {
const { currentPane, setCurrentPane, shouldAnimateCloseMenu, closeAccountMenu } = appState.accountMenu
const { currentPane, shouldAnimateCloseMenu } = appState.accountMenu
const closeAccountMenu = useCallback(() => {
appState.accountMenu.closeAccountMenu()
}, [appState])
const setCurrentPane = useCallback(
(pane: AccountMenuPane) => {
appState.accountMenu.setCurrentPane(pane)
},
[appState],
)
const ref = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(ref, () => {
onClickOutside()
})
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (event) => {
switch (event.key) {
case 'Escape':
if (currentPane === AccountMenuPane.GeneralMenu) {
closeAccountMenu()
} else if (currentPane === AccountMenuPane.ConfirmPassword) {
setCurrentPane(AccountMenuPane.Register)
} else {
setCurrentPane(AccountMenuPane.GeneralMenu)
}
break
}
}
const handleKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = useCallback(
(event) => {
switch (event.key) {
case 'Escape':
if (currentPane === AccountMenuPane.GeneralMenu) {
closeAccountMenu()
} else if (currentPane === AccountMenuPane.ConfirmPassword) {
setCurrentPane(AccountMenuPane.Register)
} else {
setCurrentPane(AccountMenuPane.GeneralMenu)
}
break
}
},
[closeAccountMenu, currentPane, setCurrentPane],
)
return (
<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 { Component } from 'preact'
import { ApplicationView } from '@/Components/ApplicationView'
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
import { ApplicationGroupEvent, Runtime } from '@standardnotes/snjs'
import { unmountComponentAtNode, findDOMNode } from 'preact/compat'
import { DialogContent, DialogOverlay } from '@reach/dialog'
import { isDesktopApplication } from '@/Utils'
type Props = {
server: string
device: WebOrDesktopDevice
enableUnfinished: boolean
websocketUrl: string
onDestroy: () => void
}
type State = {
activeApplication?: WebApplication
}
type Props = {
mainApplicationGroup: ApplicationGroup
dealloced?: boolean
deviceDestroyed?: boolean
}
export class ApplicationGroupView extends Component<Props, State> {
applicationObserverRemover?: () => void
private group?: ApplicationGroup
private application?: WebApplication
constructor(props: Props) {
super(props)
props.mainApplicationGroup.addApplicationChangeObserver(() => {
const activeApplication = props.mainApplicationGroup.primaryApplication as WebApplication
this.setState({ activeApplication })
if (props.device.isDeviceDestroyed()) {
this.state = {
deviceDestroyed: true,
}
return
}
this.group = new ApplicationGroup(
props.server,
props.device,
props.enableUnfinished ? Runtime.Dev : Runtime.Prod,
props.websocketUrl,
)
window.mainApplicationGroup = this.group
this.applicationObserverRemover = this.group.addEventObserver((event, data) => {
if (event === ApplicationGroupEvent.PrimaryApplicationSet) {
this.application = data?.primaryApplication as WebApplication
this.setState({ activeApplication: this.application })
} else if (event === ApplicationGroupEvent.DeviceWillRestart) {
this.setState({ dealloced: true })
}
})
props.mainApplicationGroup.initialize().catch(console.error)
this.state = {}
this.group.initialize().catch(console.error)
}
deinit() {
this.application = undefined
this.applicationObserverRemover?.()
;(this.applicationObserverRemover as unknown) = undefined
this.group?.deinit()
;(this.group as unknown) = undefined
this.setState({ dealloced: true, activeApplication: undefined })
const onDestroy = this.props.onDestroy
const node = findDOMNode(this) as Element
unmountComponentAtNode(node)
onDestroy()
}
render() {
const renderDialog = (message: string) => {
return (
<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 (
<>
{this.state.activeApplication && (
<div id={this.state.activeApplication.identifier}>
<ApplicationView
key={this.state.activeApplication.ephemeralIdentifier}
mainApplicationGroup={this.props.mainApplicationGroup}
application={this.state.activeApplication}
/>
</div>
)}
</>
<div id={this.state.activeApplication.identifier} key={this.state.activeApplication.ephemeralIdentifier}>
<ApplicationView
key={this.state.activeApplication.ephemeralIdentifier}
mainApplicationGroup={this.group}
application={this.state.activeApplication}
/>
</div>
)
}
}

View File

@@ -4,8 +4,7 @@ import { AppStateEvent, PanelResizedData } from '@/UIModels/AppState'
import { ApplicationEvent, Challenge, PermissionDialog, removeFromArray } from '@standardnotes/snjs'
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants'
import { alertDialog } from '@/Services/AlertService'
import { WebAppEvent, WebApplication } from '@/UIModels/Application'
import { PureComponent } from '@/Components/Abstract/PureComponent'
import { WebApplication } from '@/UIModels/Application'
import { Navigation } from '@/Components/Navigation'
import { NotesView } from '@/Components/NotesView'
import { NoteGroupView } from '@/Components/NoteGroupView'
@@ -15,7 +14,7 @@ import { PreferencesViewWrapper } from '@/Components/Preferences/PreferencesView
import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal'
import { NotesContextMenu } from '@/Components/NotesContextMenu'
import { PurchaseFlowWrapper } from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { render } from 'preact'
import { render, FunctionComponent } from 'preact'
import { PermissionsModal } from '@/Components/PermissionsModal'
import { RevisionHistoryModalWrapper } from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper'
import { PremiumModalProvider } from '@/Hooks/usePremiumModal'
@@ -23,199 +22,221 @@ import { ConfirmSignoutContainer } from '@/Components/ConfirmSignoutModal'
import { TagsContextMenu } from '@/Components/Tags/TagContextMenu'
import { ToastContainer } from '@standardnotes/stylekit'
import { FilePreviewModal } from '../Files/FilePreviewModal'
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = {
application: WebApplication
mainApplicationGroup: ApplicationGroup
}
type State = {
started?: boolean
launched?: boolean
needsUnlock?: boolean
appClass: string
challenges: Challenge[]
}
export const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicationGroup }) => {
const platformString = getPlatformString()
const [appClass, setAppClass] = useState('')
const [launched, setLaunched] = useState(false)
const [needsUnlock, setNeedsUnlock] = useState(true)
const [challenges, setChallenges] = useState<Challenge[]>([])
const [dealloced, setDealloced] = useState(false)
export class ApplicationView extends PureComponent<Props, State> {
public readonly platformString = getPlatformString()
const componentManager = application.componentManager
const appState = application.getAppState()
constructor(props: Props) {
super(props, props.application)
this.state = {
appClass: '',
challenges: [],
}
}
useEffect(() => {
setDealloced(application.dealloced)
}, [application.dealloced])
override deinit() {
;(this.application as unknown) = undefined
super.deinit()
}
override componentDidMount(): void {
super.componentDidMount()
void this.loadApplication()
}
async loadApplication() {
const desktopService = this.application.getDesktopService()
if (desktopService) {
this.application.componentManager.setDesktopManager(desktopService)
}
await this.application.prepareForLaunch({
receiveChallenge: async (challenge) => {
const challenges = this.state.challenges.slice()
challenges.push(challenge)
this.setState({ challenges: challenges })
},
})
await this.application.launch()
}
public removeChallenge = async (challenge: Challenge) => {
const challenges = this.state.challenges.slice()
removeFromArray(challenges, challenge)
this.setState({ challenges: challenges })
}
override async onAppStart() {
super.onAppStart().catch(console.error)
this.setState({
started: true,
needsUnlock: this.application.hasPasscode(),
})
this.application.componentManager.presentPermissionsDialog = this.presentPermissionsDialog
}
override async onAppLaunch() {
super.onAppLaunch().catch(console.error)
this.setState({
launched: true,
needsUnlock: false,
})
this.handleDemoSignInFromParams().catch(console.error)
}
onUpdateAvailable() {
this.application.notifyWebEvent(WebAppEvent.NewUpdateAvailable)
}
override async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName)
switch (eventName) {
case ApplicationEvent.LocalDatabaseReadError:
alertDialog({
text: 'Unable to load local database. Please restart the app and try again.',
}).catch(console.error)
break
case ApplicationEvent.LocalDatabaseWriteError:
alertDialog({
text: 'Unable to write to local database. Please restart the app and try again.',
}).catch(console.error)
break
}
}
override async onAppStateEvent(eventName: AppStateEvent, data?: unknown) {
if (eventName === AppStateEvent.PanelResized) {
const { panel, collapsed } = data as PanelResizedData
let appClass = ''
if (panel === PANEL_NAME_NOTES && collapsed) {
appClass += 'collapsed-notes'
}
if (panel === PANEL_NAME_NAVIGATION && collapsed) {
appClass += ' collapsed-navigation'
}
this.setState({ appClass })
} else if (eventName === AppStateEvent.WindowDidFocus) {
if (!(await this.application.isLocked())) {
this.application.sync.sync().catch(console.error)
}
}
}
async handleDemoSignInFromParams() {
const token = getWindowUrlParams().get('demo-token')
if (!token || this.application.hasAccount()) {
useEffect(() => {
if (dealloced) {
return
}
await this.application.sessions.populateSessionFromDemoShareToken(token)
}
const desktopService = application.getDesktopService()
presentPermissionsDialog = (dialog: PermissionDialog) => {
render(
<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>
if (desktopService) {
application.componentManager.setDesktopManager(desktopService)
}
const renderAppContents = !this.state.needsUnlock && this.state.launched
application
.prepareForLaunch({
receiveChallenge: async (challenge) => {
const challengesCopy = challenges.slice()
challengesCopy.push(challenge)
setChallenges(challengesCopy)
},
})
.then(() => {
void application.launch()
})
.catch(console.error)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [application, dealloced])
const removeChallenge = useCallback(
(challenge: Challenge) => {
const challengesCopy = challenges.slice()
removeFromArray(challengesCopy, challenge)
setChallenges(challengesCopy)
},
[challenges],
)
const presentPermissionsDialog = useCallback(
(dialog: PermissionDialog) => {
render(
<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 (
<PremiumModalProvider application={this.application} appState={this.appState}>
<div className={this.platformString + ' main-ui-view sn-component'}>
{renderAppContents && (
<div id="app" className={this.state.appClass + ' app app-column-container'}>
<Navigation application={this.application} />
<NotesView application={this.application} appState={this.appState} />
<NoteGroupView application={this.application} />
</div>
)}
{renderAppContents && (
<>
<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}
<>
{challenges.map((challenge) => {
return (
<div className="sk-modal">
<ChallengeModal
key={`${challenge.id}${application.ephemeralIdentifier}`}
application={application}
appState={appState}
mainApplicationGroup={mainApplicationGroup}
challenge={challenge}
onDismiss={removeChallenge}
/>
<ToastContainer />
<FilePreviewModal application={this.application} appState={this.appState} />
</>
)}
</div>
</PremiumModalProvider>
</div>
)
})}
</>
)
}, [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 { Icon } from '@/Components/Icon'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { ChallengeReason, CollectionSort, ContentType, SNFile, SNNote } from '@standardnotes/snjs'
import { ChallengeReason, CollectionSort, ContentType, FileItem, SNNote } from '@standardnotes/snjs'
import { confirmDialog } from '@/Services/AlertService'
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit'
import { StreamingFileReader } from '@standardnotes/filepicker'
@@ -17,6 +17,7 @@ import { AttachedFilesPopover } from './AttachedFilesPopover'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { PopoverTabs } from './PopoverTabs'
import { isHandlingFileDrag } from '@/Utils/DragTypeCheck'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = {
application: WebApplication
@@ -25,9 +26,12 @@ type Props = {
}
export const AttachedFilesButton: FunctionComponent<Props> = observer(
({ application, appState, onClickPreprocessing }) => {
const premiumModal = usePremiumModal()
({ application, appState, onClickPreprocessing }: Props) => {
if (isStateDealloced(appState)) {
return null
}
const premiumModal = usePremiumModal()
const note: SNNote | undefined = Object.values(appState.notes.selectedNotes)[0]
const [open, setOpen] = useState(false)
@@ -50,15 +54,15 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
}, [appState.filePreviewModal.isOpen, keepMenuOpen])
const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles)
const [allFiles, setAllFiles] = useState<SNFile[]>([])
const [attachedFiles, setAttachedFiles] = useState<SNFile[]>([])
const [allFiles, setAllFiles] = useState<FileItem[]>([])
const [attachedFiles, setAttachedFiles] = useState<FileItem[]>([])
const attachedFilesCount = attachedFiles.length
useEffect(() => {
application.items.setDisplayOptions(ContentType.File, CollectionSort.Title, 'dsc')
const unregisterFileStream = application.streamItems(ContentType.File, () => {
setAllFiles(application.items.getDisplayableItems<SNFile>(ContentType.File))
setAllFiles(application.items.getDisplayableItems<FileItem>(ContentType.File))
if (note) {
setAttachedFiles(application.items.getFilesForNote(note))
}
@@ -106,7 +110,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
await toggleAttachedFilesMenu()
}, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal])
const deleteFile = async (file: SNFile) => {
const deleteFile = async (file: FileItem) => {
const shouldDelete = await confirmDialog({
text: `Are you sure you want to permanently delete "${file.name}"?`,
confirmButtonStyle: 'danger',
@@ -125,12 +129,12 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
}
}
const downloadFile = async (file: SNFile) => {
const downloadFile = async (file: FileItem) => {
appState.files.downloadFile(file).catch(console.error)
}
const attachFileToNote = useCallback(
async (file: SNFile) => {
async (file: FileItem) => {
if (!note) {
addToast({
type: ToastType.Error,
@@ -144,7 +148,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
[application.items, note],
)
const detachFileFromNote = async (file: SNFile) => {
const detachFileFromNote = async (file: FileItem) => {
if (!note) {
addToast({
type: ToastType.Error,
@@ -155,8 +159,8 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
await application.items.disassociateFileWithNote(file, note)
}
const toggleFileProtection = async (file: SNFile) => {
let result: SNFile | undefined
const toggleFileProtection = async (file: FileItem) => {
let result: FileItem | undefined
if (file.protected) {
keepMenuOpen(true)
result = await application.mutator.unprotectFile(file)
@@ -169,13 +173,13 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
return isProtected
}
const authorizeProtectedActionForFile = async (file: SNFile, challengeReason: ChallengeReason) => {
const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => {
const authorizedFiles = await application.protections.authorizeProtectedActionForFiles([file], challengeReason)
const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file)
return isAuthorized
}
const renameFile = async (file: SNFile, fileName: string) => {
const renameFile = async (file: FileItem, fileName: string) => {
await application.items.renameFile(file, fileName)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ export const ChallengeModalPrompt: FunctionComponent<Props> = ({ prompt, values,
}, [isInvalid])
return (
<div className="w-full mb-3">
<div key={prompt.id} className="w-full mb-3">
{prompt.validation === ChallengeValidation.ProtectionSessionDuration ? (
<div className="min-w-76">
<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 { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { FunctionComponent } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { WorkspaceSwitcherMenu } from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu'
import { Button } from '@/Components/Button/Button'
import { Icon } from '@/Components/Icon'
@@ -22,7 +22,7 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainAppl
useCloseOnClickOutside(containerRef, () => setIsOpen(false))
const toggleMenu = () => {
const toggleMenu = useCallback(() => {
if (!isOpen) {
const menuPosition = calculateSubmenuStyle(buttonRef.current)
if (menuPosition) {
@@ -31,7 +31,7 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainAppl
}
setIsOpen(!isOpen)
}
}, [isOpen])
useEffect(() => {
if (isOpen) {

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { WebApplication } from '@/UIModels/Application'
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
import { DialogContent, DialogOverlay } from '@reach/dialog'
import { addToast, NoPreviewIllustration, ToastType } from '@standardnotes/stylekit'
import { addToast, ToastType } from '@standardnotes/stylekit'
import { NoPreviewIllustration } from '@standardnotes/icons'
import { FunctionComponent } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { getFileIconComponent } from '@/Components/AttachedFilesPopover/PopoverFileItem'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ export const NotesListItem: FunctionComponent<Props> = ({
const showModifiedDate = sortedBy === CollectionSort.UpdatedAt
const editorForNote = application.componentManager.editorForNote(note)
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 (
<div

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ export const AddTagOption: FunctionComponent<Props> = observer(({ appState }) =>
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
const toggleTagsMenu = () => {
const toggleTagsMenu = useCallback(() => {
if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) {
@@ -34,7 +34,7 @@ export const AddTagOption: FunctionComponent<Props> = observer(({ appState }) =>
}
setIsMenuOpen(!isMenuOpen)
}
}, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => {
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 { IconType, SNComponent, SNNote } from '@standardnotes/snjs'
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 { ChangeEditorMenu } from '@/Components/ChangeEditor/ChangeEditorMenu'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
@@ -48,7 +48,7 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
setIsVisible(open)
})
const toggleChangeEditorMenu = () => {
const toggleChangeEditorMenu = useCallback(() => {
if (!isOpen) {
const menuStyle = calculateSubmenuStyle(buttonRef.current)
if (menuStyle) {
@@ -57,7 +57,7 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
}
setIsOpen(!isOpen)
}
}, [isOpen])
useEffect(() => {
if (isOpen) {

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { PANEL_NAME_NOTES } from '@/Constants'
import { PrefKey } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { NoAccountWarning } from '@/Components/NoAccountWarning'
import { NotesList } from '@/Components/NotesList'
import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu'
@@ -13,45 +13,46 @@ import { SearchOptions } from '@/Components/SearchOptions'
import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
type Props = {
application: WebApplication
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 displayOptionsMenuRef = useRef<HTMLDivElement>(null)
const {
completedFullSync,
createNewNote,
displayOptions,
noteFilterText,
optionsSubtitle,
panelTitle,
renderedNotes,
selectedNotes,
setNoteFilterText,
searchBarElement,
selectNextNote,
selectPreviousNote,
onFilterEnter,
handleFilterTextChanged,
clearFilterText,
paginate,
panelWidth,
} = 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 [focusedSearch, setFocusedSearch] = useState(false)
const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu)
useEffect(() => {
handleFilterTextChanged()
}, [noteFilterText, handleFilterTextChanged])
useEffect(() => {
/**
* 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],
onKeyDown: (event) => {
event.preventDefault()
createNewNote().catch(console.error)
createNewNote()
},
})
@@ -102,34 +103,43 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
previousNoteKeyObserver()
searchKeyObserver()
}
}, [application.io, createNewNote, searchBarElement, selectNextNote, selectPreviousNote])
}, [application, createNewNote, selectPreviousNote, searchBarElement, selectNextNote])
const onNoteFilterTextChange = (e: Event) => {
setNoteFilterText((e.target as HTMLInputElement).value)
}
const onNoteFilterTextChange = useCallback(
(e: Event) => {
setNoteFilterText((e.target as HTMLInputElement).value)
},
[setNoteFilterText],
)
const onSearchFocused = () => setFocusedSearch(true)
const onSearchBlurred = () => setFocusedSearch(false)
const onSearchFocused = useCallback(() => setFocusedSearch(true), [])
const onSearchBlurred = useCallback(() => setFocusedSearch(false), [])
const onNoteFilterKeyUp = (e: KeyboardEvent) => {
if (e.key === KeyboardKey.Enter) {
onFilterEnter()
}
}
const onNoteFilterKeyUp = useCallback(
(e: KeyboardEvent) => {
if (e.key === KeyboardKey.Enter) {
onFilterEnter()
}
},
[onFilterEnter],
)
const panelResizeFinishCallback: ResizeFinishCallback = (width, _lastLeft, _isMaxWidth, isCollapsed) => {
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
(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.panelDidResize(PANEL_NAME_NOTES, isCollapsed)
}
}, [appState])
const panelWidthEventCallback = () => {
appState.noteTags.reloadTagsContainerMaxWidth()
}
const toggleDisplayOptionsMenu = () => {
const toggleDisplayOptionsMenu = useCallback(() => {
setShowDisplayOptionsMenu(!showDisplayOptionsMenu)
}
}, [showDisplayOptionsMenu])
return (
<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 { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState'
@@ -18,9 +18,10 @@ export const OtherSessionsSignOutContainer = observer((props: Props) => {
const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props) => {
const cancelRef = useRef<HTMLButtonElement>(null)
function closeDialog() {
const closeDialog = useCallback(() => {
appState.accountMenu.setOtherSessionsSignOut(false)
}
}, [appState])
return (
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { PreferencesSegment, Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents'
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
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 { EncryptionStatusItem } from '../../Security/Encryption'
import { Icon } from '@/Components/Icon'
@@ -16,7 +16,7 @@ type Props = {
export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
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 [isSavingAsDecrypted, setIsSavingAsDecrypted] = useState(false)
@@ -24,9 +24,9 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
useEffect(() => {
if (droppedFile) {
void application.files.decryptBackupMetadataFile(droppedFile).then(setDecryptedFileContent)
void application.files.decryptBackupMetadataFile(droppedFile).then(setDecryptedFileItem)
} else {
setDecryptedFileContent(undefined)
setDecryptedFileItem(undefined)
}
}, [droppedFile, application])
@@ -41,20 +41,20 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
}, [application, fileSystem])
const downloadBinaryFileAsDecrypted = useCallback(async () => {
if (!decryptedFileContent || !binaryFile) {
if (!decryptedFileItem || !binaryFile) {
return
}
setIsSavingAsDecrypted(true)
const result = await application.files.readBackupFileAndSaveDecrypted(binaryFile, decryptedFileContent, fileSystem)
const result = await application.files.readBackupFileAndSaveDecrypted(binaryFile, decryptedFileItem, fileSystem)
if (result === 'success') {
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)
setDecryptedFileContent(undefined)
setDecryptedFileItem(undefined)
setDroppedFile(undefined)
} else if (result === 'failed') {
void application.alertService.alert(
@@ -63,7 +63,7 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
}
setIsSavingAsDecrypted(false)
}, [decryptedFileContent, application, binaryFile, fileSystem])
}, [decryptedFileItem, application, binaryFile, fileSystem])
const handleDrag = useCallback(
(event: DragEvent) => {
@@ -123,6 +123,12 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
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 {
const metadata = JSON.parse(text) as FileBackupMetadataFile
setDroppedFile(metadata)
@@ -160,14 +166,14 @@ export const BackupsDropZone: FunctionComponent<Props> = ({ application }) => {
return (
<>
<PreferencesSegment>
{!decryptedFileContent && <Text>Attempting to decrypt metadata file...</Text>}
{!decryptedFileItem && <Text>Attempting to decrypt metadata file...</Text>}
{decryptedFileContent && (
{decryptedFileItem && (
<>
<Title>Backup Decryption</Title>
<EncryptionStatusItem
status={decryptedFileContent.name}
status={decryptedFileItem.name}
icon={[<Icon type="attachment-file" className="min-w-5 min-h-5" />]}
checkmark={true}
/>

View File

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

View File

@@ -69,7 +69,7 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
.componentsForArea(ComponentArea.Editor)
.map((editor): EditorOption => {
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 {
label: editor.name,

View File

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

View File

@@ -1,15 +1,14 @@
import { Icon } from '@/Components/Icon'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { FunctionComponent } from 'preact'
import { MouseEventHandler } from 'react'
import { useState, useRef, useEffect } from 'preact/hooks'
import { IconType } from '@standardnotes/snjs'
const DisclosureIconButton: FunctionComponent<{
className?: string
icon: IconType
onMouseEnter?: MouseEventHandler
onMouseLeave?: MouseEventHandler
onMouseEnter?: any
onMouseLeave?: any
}> = ({ className = '', icon, onMouseEnter, onMouseLeave }) => (
<DisclosureButton
onMouseEnter={onMouseEnter}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -139,7 +139,7 @@ export const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(({ appli
const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen)
const toggleThemesMenu = () => {
const toggleThemesMenu = useCallback(() => {
if (!themesMenuOpen && themesButtonRef.current) {
const themesButtonRect = themesButtonRef.current.getBoundingClientRect()
setThemesMenuPosition({
@@ -150,48 +150,57 @@ export const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(({ appli
} else {
setThemesMenuOpen(false)
}
}
}, [themesMenuOpen])
const openPreferences = () => {
const openPreferences = useCallback(() => {
closeQuickSettingsMenu()
appState.preferences.openPreferences()
}
}, [appState, closeQuickSettingsMenu])
const toggleComponent = (component: SNComponent) => {
if (component.isTheme()) {
application.mutator.toggleTheme(component).catch(console.error)
} else {
application.mutator.toggleComponent(component).catch(console.error)
}
}
const toggleComponent = useCallback(
(component: SNComponent) => {
if (component.isTheme()) {
application.mutator.toggleTheme(component).catch(console.error)
} else {
application.mutator.toggleComponent(component).catch(console.error)
}
},
[application],
)
const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (event) => {
switch (event.key) {
case 'Escape':
setThemesMenuOpen(false)
themesButtonRef.current?.focus()
break
case 'ArrowRight':
if (!themesMenuOpen) {
toggleThemesMenu()
}
}
}
const handleBtnKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = useCallback(
(event) => {
switch (event.key) {
case 'Escape':
setThemesMenuOpen(false)
themesButtonRef.current?.focus()
break
case 'ArrowRight':
if (!themesMenuOpen) {
toggleThemesMenu()
}
}
},
[themesMenuOpen, toggleThemesMenu],
)
const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = (event) => {
quickSettingsKeyDownHandler(closeQuickSettingsMenu, event, quickSettingsMenuRef, themesMenuOpen)
}
const handleQuickSettingsKeyDown: JSXInternal.KeyboardEventHandler<HTMLDivElement> = useCallback(
(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)
}
}, [])
const toggleDefaultTheme = () => {
const toggleDefaultTheme = useCallback(() => {
const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable())
if (activeTheme) {
application.mutator.toggleTheme(activeTheme).catch(console.error)
}
}
}, [application, themes])
return (
<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 { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import { StateUpdater, useCallback, useState } from 'preact/hooks'
import { useEffect } from 'react'
import { StateUpdater, useCallback, useState, useEffect } from 'preact/hooks'
import { LegacyHistoryList } from './LegacyHistoryList'
import { RemoteHistoryList } from './RemoteHistoryList'
import { SessionHistoryList } from './SessionHistoryList'
@@ -67,7 +66,7 @@ export const HistoryListContainer: FunctionComponent<Props> = observer(
}
fetchLegacyHistory().catch(console.error)
}, [application.actionsManager, note])
}, [application, note])
const TabButton: FunctionComponent<{
type: RevisionListTabType

View File

@@ -1,7 +1,7 @@
import { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import HistoryLockedIllustration from '../../../svg/il-history-locked.svg'
import { HistoryLockedIllustration } from '@standardnotes/icons'
import { Button } from '@/Components/Button/Button'
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 editorForCurrentNote = useMemo(() => {
return application.componentManager.editorForNote(note)
}, [application.componentManager, note])
}, [application, note])
const [isFetchingSelectedRevision, setIsFetchingSelectedRevision] = useState(false)
const [selectedRevision, setSelectedRevision] = useState<HistoryEntry | LegacyHistoryEntry>()
@@ -92,7 +92,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
setIsFetchingRemoteHistory(false)
}
}
}, [application.historyManager, note])
}, [application, note])
useEffect(() => {
if (!remoteHistory?.length) {

View File

@@ -29,7 +29,7 @@ export const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentP
componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision
return componentViewer
}, [application.componentManager, editorForCurrentNote, templateNoteForRevision])
}, [application, editorForCurrentNote, templateNoteForRevision])
useEffect(() => {
return () => {
@@ -37,7 +37,7 @@ export const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentP
application.componentManager.destroyComponentViewer(componentViewer)
}
}
}, [application.componentManager, componentViewer])
}, [application, componentViewer])
return (
<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 { observer } from 'mobx-react-lite'
import Bubble from '@/Components/Bubble'
import { useCallback } from 'preact/hooks'
type Props = {
appState: AppState
@@ -13,9 +14,9 @@ export const SearchOptions = observer(({ appState }: Props) => {
const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions
async function toggleIncludeProtectedContents() {
const toggleIncludeProtectedContents = useCallback(async () => {
await searchOptions.toggleIncludeProtectedContents()
}
}, [searchOptions])
return (
<div role="tablist" className="search-options justify-center" onMouseDown={(e) => e.preventDefault()}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { AppState } from '@/UIModels/AppState'
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'preact'
import { SmartViewsListItem } from './SmartViewsListItem'
@@ -7,7 +8,11 @@ type Props = {
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
return (

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,8 @@ import { Environment, RawKeychainValue } from '@standardnotes/snjs'
import { WebOrDesktopDevice } from './WebOrDesktopDevice'
const KEYCHAIN_STORAGE_KEY = 'keychain'
const DESTROYED_DEVICE_URL_PARAM = 'destroyed'
const DESTROYED_DEVICE_URL_VALUE = 'true'
export class WebDevice extends WebOrDesktopDevice {
environment = Environment.Web
@@ -23,4 +25,17 @@ export class WebDevice extends WebOrDesktopDevice {
async clearRawKeychainValue(): Promise<void> {
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 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 { AppState } from '@/UIModels/AppState'
import { observer } from 'mobx-react-lite'
import { FunctionalComponent } from 'preact'
import { useContext } from 'preact/hooks'
import { createContext } from 'react'
import { ComponentChildren, FunctionalComponent, createContext } from 'preact'
import { useCallback, useContext } from 'preact/hooks'
import { PremiumFeaturesModal } from '@/Components/PremiumFeaturesModal'
type PremiumModalContextData = {
@@ -27,33 +27,50 @@ export const usePremiumModal = (): PremiumModalContextData => {
interface Props {
application: WebApplication
appState: AppState
children: ComponentChildren | ComponentChildren[]
}
export const PremiumModalProvider: FunctionalComponent<Props> = observer(({ application, appState, children }) => {
const featureName = appState.features.premiumAlertFeatureName
const activate = appState.features.showPremiumAlert
const close = appState.features.closePremiumAlert
export const PremiumModalProvider: FunctionalComponent<Props> = observer(
({ application, appState, children }: Props) => {
const dealloced = !appState || appState.dealloced == undefined
if (dealloced) {
return null
}
const showModal = !!featureName
const featureName = appState.features.premiumAlertFeatureName || ''
const hasSubscription = Boolean(
appState.subscription.userSubscription &&
!appState.subscription.isUserSubscriptionExpired &&
!appState.subscription.isUserSubscriptionCanceled,
)
const showModal = !!featureName
return (
<>
{showModal && (
<PremiumFeaturesModal
application={application}
featureName={featureName}
hasSubscription={hasSubscription}
onClose={close}
showModal={!!featureName}
/>
)}
<PremiumModalProvider_ value={{ activate }}>{children}</PremiumModalProvider_>
</>
)
})
const hasSubscription = Boolean(
appState.subscription.userSubscription &&
!appState.subscription.isUserSubscriptionExpired &&
!appState.subscription.isUserSubscriptionCanceled,
)
const activate = useCallback(
(feature: string) => {
appState.features.showPremiumAlert(feature).catch(console.error)
},
[appState],
)
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) {
return
}
switch (modifier) {
case KeyboardModifier.Meta: {
if (this.isMac) {
@@ -197,8 +198,10 @@ export class IOService {
addKeyObserver(observer: KeyboardObserver): () => void {
this.observers.push(observer)
const thislessObservers = this.observers
return () => {
removeFromArray(this.observers, observer)
removeFromArray(thislessObservers, observer)
}
}
}

View File

@@ -38,6 +38,7 @@ export class ThemeManager extends ApplicationService {
override async onAppEvent(event: ApplicationEvent) {
super.onAppEvent(event).catch(console.error)
switch (event) {
case ApplicationEvent.SignedOut: {
this.deactivateAllThemes()
@@ -91,6 +92,7 @@ export class ThemeManager extends ApplicationService {
;(this.unregisterStream as unknown) = undefined
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.colorSchemeEventHandler)
super.deinit()
}
@@ -226,6 +228,7 @@ export class ThemeManager extends ApplicationService {
public deactivateAllThemes() {
const activeThemes = this.activeThemes.slice()
for (const uuid of activeThemes) {
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 { ApplicationEvent, ContentType, SNNote, SNTag } from '@standardnotes/snjs'
import { ApplicationEvent, ContentType, DeinitSource, SNNote, SNTag } from '@standardnotes/snjs'
import { WebApplication } from '@/UIModels/Application'
import { AccountMenuPane } from '@/Components/AccountMenu'
import { AbstractState } from './AbstractState'
type StructuredItemsCount = {
notes: number
@@ -11,7 +12,7 @@ type StructuredItemsCount = {
archived: number
}
export class AccountMenuState {
export class AccountMenuState extends AbstractState {
show = false
signingOut = false
otherSessionsSignOut = false
@@ -26,7 +27,15 @@ export class AccountMenuState {
shouldAnimateCloseMenu = false
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, {
show: observable,
signingOut: observable,

View File

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

View File

@@ -1,14 +1,30 @@
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 { AbstractState } from './AbstractState'
export class FeaturesState {
export class FeaturesState extends AbstractState {
hasFolders: boolean
hasSmartViews: boolean
hasFiles: boolean
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.hasSmartViews = this.isEntitledToSmartViews()
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'
export class FilePreviewModalState {
isOpen = false
currentFile: SNFile | undefined = undefined
otherFiles: SNFile[] = []
currentFile: FileItem | undefined = undefined
otherFiles: FileItem[] = []
constructor() {
makeObservable(this, {
@@ -18,11 +18,11 @@ export class FilePreviewModalState {
})
}
setCurrentFile = (currentFile: SNFile) => {
setCurrentFile = (currentFile: FileItem) => {
this.currentFile = currentFile
}
activate = (currentFile: SNFile, otherFiles: SNFile[]) => {
activate = (currentFile: FileItem, otherFiles: FileItem[]) => {
this.currentFile = currentFile
this.otherFiles = otherFiles
this.isOpen = true

View File

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

View File

@@ -1,10 +1,15 @@
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 { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export class NoAccountWarningState {
export class NoAccountWarningState extends AbstractState {
show: boolean
constructor(application: SNApplication, appObservers: (() => void)[]) {
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
this.show = application.hasAccount() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true
appObservers.push(

View File

@@ -1,10 +1,12 @@
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 { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
import { AppState } from './AppState'
export class NoteTagsState {
export class NoteTagsState extends AbstractState {
autocompleteInputFocused = false
autocompleteSearchQuery = ''
autocompleteTagHintFocused = false
@@ -15,7 +17,17 @@ export class NoteTagsState {
tagsContainerMaxWidth: number | 'auto' = 0
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, {
autocompleteInputFocused: observable,
autocompleteSearchQuery: observable,

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,16 @@
import { ApplicationEvent } from '@standardnotes/snjs'
import { makeObservable, observable, action, runInAction } from 'mobx'
import { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
export class SearchOptionsState {
export class SearchOptionsState extends AbstractState {
includeProtectedContents = false
includeArchived = false
includeTrashed = false
constructor(private application: WebApplication, appObservers: (() => void)[]) {
constructor(application: WebApplication, appObservers: (() => void)[]) {
super(application)
makeObservable(this, {
includeProtectedContents: 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 { WebApplication } from '../Application'
import { AbstractState } from './AbstractState'
type Subscription = {
planName: string
@@ -14,11 +21,21 @@ type AvailableSubscriptions = {
}
}
export class SubscriptionState {
export class SubscriptionState extends AbstractState {
userSubscription: Subscription | 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, {
userSubscription: observable,
availableSubscriptions: observable,

View File

@@ -12,10 +12,13 @@ import {
UuidString,
isSystemView,
FindItem,
DeinitSource,
} from '@standardnotes/snjs'
import { action, computed, makeAutoObservable, makeObservable, observable, runInAction } from 'mobx'
import { WebApplication } from '../Application'
import { FeaturesState } from './FeaturesState'
import { AbstractState } from './AbstractState'
import { destroyAllObjectProperties } from '@/Utils'
type AnyTag = SNTag | SmartView
@@ -56,7 +59,7 @@ const isValidFutureSiblings = (application: SNApplication, futureSiblings: SNTag
return true
}
export class TagsState {
export class TagsState extends AbstractState {
tags: SNTag[] = []
smartViews: SmartView[] = []
allNotesCount_ = 0
@@ -75,7 +78,9 @@ export class TagsState {
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.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) {
const hasEmptyTitle = title.length === 0

View File

@@ -17,6 +17,7 @@ import {
Runtime,
DesktopDeviceInterface,
isDesktopDevice,
DeinitMode,
} from '@standardnotes/snjs'
type WebServices = {
@@ -68,13 +69,10 @@ export class WebApplication extends SNApplication {
this.iconsController = new IconsController()
}
override deinit(source: DeinitSource): void {
super.deinit(source)
try {
if (source === DeinitSource.AppGroupUnload) {
this.getThemeService().deactivateAllThemes()
}
override deinit(mode: DeinitMode, source: DeinitSource): void {
super.deinit(mode, source)
try {
for (const service of Object.values(this.webServices)) {
if (!service) {
continue
@@ -88,7 +86,10 @@ export class WebApplication extends SNApplication {
}
this.webServices = {} as WebServices
this.noteControllerGroup.deinit()
;(this.noteControllerGroup as unknown) = undefined
this.webEventObservers.length = 0
} catch (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 { 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> {
constructor(
private defaultSyncServerHost: string,
private device: WebOrDesktopDevice,
device: WebOrDesktopDevice,
private runtime: Runtime,
private webSocketUrl: string,
) {
@@ -27,8 +64,14 @@ export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
}
override async initialize(): Promise<void> {
const defaultSyncServerHost = this.defaultSyncServerHost
const runtime = this.runtime
const webSocketUrl = this.webSocketUrl
await super.initialize({
applicationCreator: this.createApplication,
applicationCreator: async (descriptor, device) => {
return createApplication(descriptor, device, defaultSyncServerHost, device, runtime, webSocketUrl)
},
})
if (isDesktopApplication()) {
@@ -38,37 +81,15 @@ export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
}
}
override handleAllWorkspacesSignedOut(): void {
isDesktopDevice(this.deviceInterface) && this.deviceInterface.destroyAllData()
override deinit() {
super.deinit()
if (isDesktopApplication()) {
delete window.webClient
}
}
private createApplication = (descriptor: ApplicationDescriptor, deviceInterface: WebOrDesktopDevice) => {
const platform = getPlatform()
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
override handleAllWorkspacesSignedOut(): void {
isDesktopDevice(this.device) && this.device.destroyAllData()
}
}

View File

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

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