chore: app group optimizations (#1027)
This commit is contained in:
4
app/assets/javascripts/@types/Svg.d.ts
vendored
Normal file
4
app/assets/javascripts/@types/Svg.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.svg' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
declare module '*.svg' {
|
||||
export default function SvgComponent(props: React.SVGProps<SVGSVGElement>): JSX.Element
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -89,7 +89,7 @@ import {
|
||||
WarningIcon,
|
||||
WindowIcon,
|
||||
SubtractIcon,
|
||||
} from '@standardnotes/stylekit'
|
||||
} from '@standardnotes/icons'
|
||||
|
||||
export const ICONS = {
|
||||
'account-circle': AccountCircleIcon,
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
{{
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,7 +34,7 @@ export const FilesSection: FunctionComponent<Props> = ({ application }) => {
|
||||
}
|
||||
|
||||
getFilesQuota().catch(console.error)
|
||||
}, [application.settings])
|
||||
}, [application])
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,7 +40,7 @@ export const LabsPane: FunctionComponent<Props> = ({ application }) => {
|
||||
}
|
||||
})
|
||||
setExperimentalFeatures(experimentalFeatures)
|
||||
}, [application.features])
|
||||
}, [application])
|
||||
|
||||
useEffect(() => {
|
||||
reloadExperimentalFeatures()
|
||||
|
||||
@@ -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}
|
||||
@@ -51,7 +50,7 @@ export const AuthAppInfoTooltip: FunctionComponent = () => {
|
||||
/>
|
||||
<DisclosurePanel>
|
||||
<div
|
||||
className={`bg-inverted-default color-inverted-default text-center rounded shadow-overlay
|
||||
className={`bg-inverted-default color-inverted-default text-center rounded shadow-overlay
|
||||
py-1.5 px-2 absolute w-103 -top-10 -left-51`}
|
||||
>
|
||||
Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -94,7 +94,7 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
|
||||
if (autocompleteInputFocused) {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}, [appState.noteTags, autocompleteInputFocused])
|
||||
}, [appState, autocompleteInputFocused])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
23
app/assets/javascripts/UIModels/AppState/AbstractState.ts
Normal file
23
app/assets/javascripts/UIModels/AppState/AbstractState.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user