chore: app group optimizations (#1027)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user