diff --git a/app/assets/javascripts/App.tsx b/app/assets/javascripts/App.tsx index 1f13151db..9ca95133b 100644 --- a/app/assets/javascripts/App.tsx +++ b/app/assets/javascripts/App.tsx @@ -22,14 +22,13 @@ declare global { import { IsWebPlatform, WebAppVersion } from '@/Version' import { DesktopManagerInterface, SNLog } from '@standardnotes/snjs' -import { render } from 'preact' -import { ApplicationGroupView } from './Components/ApplicationGroupView/ApplicationGroupView' +import ApplicationGroupView from './Components/ApplicationGroupView/ApplicationGroupView' import { WebDevice } from './Device/WebDevice' import { StartApplication } from './Device/StartApplication' import { ApplicationGroup } from './UIModels/ApplicationGroup' import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice' import { WebApplication } from './UIModels/Application' -import { unmountComponentAtRoot } from './Utils/PreactUtils' +import { createRoot } from 'react-dom/client' let keyCount = 0 const getKey = () => { @@ -38,6 +37,11 @@ const getKey = () => { const RootId = 'app-group-root' +const rootElement = document.createElement('div') +rootElement.id = RootId +const appendedRootNode = document.body.appendChild(rootElement) +const root = createRoot(appendedRootNode) + const startApplication: StartApplication = async function startApplication( defaultSyncServerHost: string, device: WebOrDesktopDevice, @@ -48,19 +52,14 @@ const startApplication: StartApplication = async function startApplication( SNLog.onError = console.error const onDestroy = () => { - const root = document.getElementById(RootId) as HTMLElement - unmountComponentAtRoot(root) - root.remove() + const rootElement = document.getElementById(RootId) as HTMLElement + root.unmount() + rootElement.remove() renderApp() } const renderApp = () => { - const root = document.createElement('div') - root.id = RootId - - const parentNode = document.body.appendChild(root) - - render( + root.render( , - parentNode, ) } diff --git a/app/assets/javascripts/Components/Abstract/PureComponent.tsx b/app/assets/javascripts/Components/Abstract/PureComponent.tsx index 341b3737b..353c0f93f 100644 --- a/app/assets/javascripts/Components/Abstract/PureComponent.tsx +++ b/app/assets/javascripts/Components/Abstract/PureComponent.tsx @@ -2,9 +2,7 @@ import { ApplicationEvent } from '@standardnotes/snjs' import { WebApplication } from '@/UIModels/Application' import { AppState, AppStateEvent } from '@/UIModels/AppState' import { autorun, IReactionDisposer, IReactionPublic } from 'mobx' -import { Component } from 'preact' -import { findDOMNode, unmountComponentAtNode } from 'preact/compat' - +import { Component } from 'react' export type PureComponentState = Partial> export type PureComponentProps = Partial> @@ -36,20 +34,6 @@ export abstract class PureComponent

void): void { this.reactionDisposers.push(autorun(view)) } diff --git a/app/assets/javascripts/Components/AccountMenu/AccountMenu.tsx b/app/assets/javascripts/Components/AccountMenu/AccountMenu.tsx index 400eff7d7..1e5d60069 100644 --- a/app/assets/javascripts/Components/AccountMenu/AccountMenu.tsx +++ b/app/assets/javascripts/Components/AccountMenu/AccountMenu.tsx @@ -2,15 +2,10 @@ import { observer } from 'mobx-react-lite' import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' import { AppState } from '@/UIModels/AppState' import { WebApplication } from '@/UIModels/Application' -import { useCallback, useRef, useState } from 'preact/hooks' -import { GeneralAccountMenu } from './GeneralAccountMenu' -import { FunctionComponent } from 'preact' -import { SignInPane } from './SignIn' -import { CreateAccount } from './CreateAccount' -import { ConfirmPassword } from './ConfirmPassword' -import { JSXInternal } from 'preact/src/jsx' +import { useCallback, useRef, FunctionComponent, KeyboardEventHandler } from 'react' import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { AccountMenuPane } from './AccountMenuPane' +import MenuPaneSelector from './MenuPaneSelector' type Props = { appState: AppState @@ -19,114 +14,61 @@ type Props = { mainApplicationGroup: ApplicationGroup } -type PaneSelectorProps = { - appState: AppState - application: WebApplication - mainApplicationGroup: ApplicationGroup - menuPane: AccountMenuPane - setMenuPane: (pane: AccountMenuPane) => void - closeMenu: () => void +const AccountMenu: FunctionComponent = ({ application, appState, onClickOutside, mainApplicationGroup }) => { + 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(null) + useCloseOnClickOutside(ref, () => { + onClickOutside() + }) + + const handleKeyDown: KeyboardEventHandler = 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 ( +

+
+ +
+
+ ) } -const MenuPaneSelector: FunctionComponent = observer( - ({ application, appState, menuPane, setMenuPane, closeMenu, mainApplicationGroup }) => { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - - switch (menuPane) { - case AccountMenuPane.GeneralMenu: - return ( - - ) - case AccountMenuPane.SignIn: - return - case AccountMenuPane.Register: - return ( - - ) - case AccountMenuPane.ConfirmPassword: - return ( - - ) - } - }, -) - -export const AccountMenu: FunctionComponent = observer( - ({ application, appState, onClickOutside, mainApplicationGroup }) => { - 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(null) - useCloseOnClickOutside(ref, () => { - onClickOutside() - }) - - const handleKeyDown: JSXInternal.KeyboardEventHandler = 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 ( -
-
- -
-
- ) - }, -) +export default observer(AccountMenu) diff --git a/app/assets/javascripts/Components/AccountMenu/AdvancedOptions.tsx b/app/assets/javascripts/Components/AccountMenu/AdvancedOptions.tsx index 97b900d2a..a0d6bda88 100644 --- a/app/assets/javascripts/Components/AccountMenu/AdvancedOptions.tsx +++ b/app/assets/javascripts/Components/AccountMenu/AdvancedOptions.tsx @@ -1,11 +1,10 @@ import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useState } from 'preact/hooks' -import { Checkbox } from '@/Components/Checkbox/Checkbox' -import { DecoratedInput } from '@/Components/Input/DecoratedInput' -import { Icon } from '@/Components/Icon/Icon' +import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useState } from 'react' +import Checkbox from '@/Components/Checkbox/Checkbox' +import DecoratedInput from '@/Components/Input/DecoratedInput' +import Icon from '@/Components/Icon/Icon' type Props = { application: WebApplication @@ -15,170 +14,177 @@ type Props = { onStrictSignInChange?: (isStrictSignIn: boolean) => void } -export const AdvancedOptions: FunctionComponent = observer( - ({ appState, application, disabled = false, onPrivateWorkspaceChange, onStrictSignInChange, children }) => { - const { server, setServer, enableServerOption, setEnableServerOption } = appState.accountMenu - const [showAdvanced, setShowAdvanced] = useState(false) +const AdvancedOptions: FunctionComponent = ({ + appState, + application, + disabled = false, + onPrivateWorkspaceChange, + onStrictSignInChange, + children, +}) => { + const { server, setServer, enableServerOption, setEnableServerOption } = appState.accountMenu + const [showAdvanced, setShowAdvanced] = useState(false) - const [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false) - const [privateWorkspaceName, setPrivateWorkspaceName] = useState('') - const [privateWorkspaceUserphrase, setPrivateWorkspaceUserphrase] = useState('') + const [isPrivateWorkspace, setIsPrivateWorkspace] = useState(false) + const [privateWorkspaceName, setPrivateWorkspaceName] = useState('') + const [privateWorkspaceUserphrase, setPrivateWorkspaceUserphrase] = useState('') - const [isStrictSignin, setIsStrictSignin] = useState(false) + const [isStrictSignin, setIsStrictSignin] = useState(false) - useEffect(() => { - const recomputePrivateWorkspaceIdentifier = async () => { - const identifier = await application.computePrivateWorkspaceIdentifier( - privateWorkspaceName, - privateWorkspaceUserphrase, - ) + useEffect(() => { + const recomputePrivateWorkspaceIdentifier = async () => { + const identifier = await application.computePrivateWorkspaceIdentifier( + privateWorkspaceName, + privateWorkspaceUserphrase, + ) - if (!identifier) { - if (privateWorkspaceName?.length > 0 && privateWorkspaceUserphrase?.length > 0) { - application.alertService.alert('Unable to compute private workspace name.').catch(console.error) - } - return + if (!identifier) { + if (privateWorkspaceName?.length > 0 && privateWorkspaceUserphrase?.length > 0) { + application.alertService.alert('Unable to compute private workspace name.').catch(console.error) } - onPrivateWorkspaceChange?.(true, identifier) + return } + onPrivateWorkspaceChange?.(true, identifier) + } - if (privateWorkspaceName && privateWorkspaceUserphrase) { - recomputePrivateWorkspaceIdentifier().catch(console.error) + if (privateWorkspaceName && privateWorkspaceUserphrase) { + recomputePrivateWorkspaceIdentifier().catch(console.error) + } + }, [privateWorkspaceName, privateWorkspaceUserphrase, application, onPrivateWorkspaceChange]) + + useEffect(() => { + onPrivateWorkspaceChange?.(isPrivateWorkspace) + }, [isPrivateWorkspace, onPrivateWorkspaceChange]) + + const handleIsPrivateWorkspaceChange = useCallback(() => { + setIsPrivateWorkspace(!isPrivateWorkspace) + }, [isPrivateWorkspace]) + + const handlePrivateWorkspaceNameChange = useCallback((name: string) => { + setPrivateWorkspaceName(name) + }, []) + + const handlePrivateWorkspaceUserphraseChange = useCallback((userphrase: string) => { + setPrivateWorkspaceUserphrase(userphrase) + }, []) + + const handleServerOptionChange: ChangeEventHandler = useCallback( + (e) => { + if (e.target instanceof HTMLInputElement) { + setEnableServerOption(e.target.checked) } - }, [privateWorkspaceName, privateWorkspaceUserphrase, application, onPrivateWorkspaceChange]) + }, + [setEnableServerOption], + ) - useEffect(() => { - onPrivateWorkspaceChange?.(isPrivateWorkspace) - }, [isPrivateWorkspace, onPrivateWorkspaceChange]) + const handleSyncServerChange = useCallback( + (server: string) => { + setServer(server) + application.setCustomHost(server).catch(console.error) + }, + [application, setServer], + ) - const handleIsPrivateWorkspaceChange = useCallback(() => { - setIsPrivateWorkspace(!isPrivateWorkspace) - }, [isPrivateWorkspace]) + const handleStrictSigninChange = useCallback(() => { + const newValue = !isStrictSignin + setIsStrictSignin(newValue) + onStrictSignInChange?.(newValue) + }, [isStrictSignin, onStrictSignInChange]) - const handlePrivateWorkspaceNameChange = useCallback((name: string) => { - setPrivateWorkspaceName(name) - }, []) + const toggleShowAdvanced = useCallback(() => { + setShowAdvanced(!showAdvanced) + }, [showAdvanced]) - const handlePrivateWorkspaceUserphraseChange = useCallback((userphrase: string) => { - setPrivateWorkspaceUserphrase(userphrase) - }, []) + return ( + <> + + {showAdvanced ? ( +
+ {children} - const handleServerOptionChange = useCallback( - (e: Event) => { - if (e.target instanceof HTMLInputElement) { - setEnableServerOption(e.target.checked) - } - }, - [setEnableServerOption], - ) - - const handleSyncServerChange = useCallback( - (server: string) => { - setServer(server) - application.setCustomHost(server).catch(console.error) - }, - [application, setServer], - ) - - const handleStrictSigninChange = useCallback(() => { - const newValue = !isStrictSignin - setIsStrictSignin(newValue) - onStrictSignInChange?.(newValue) - }, [isStrictSignin, onStrictSignInChange]) - - const toggleShowAdvanced = useCallback(() => { - setShowAdvanced(!showAdvanced) - }, [showAdvanced]) - - return ( - <> - - {showAdvanced ? ( -
- {children} + {isPrivateWorkspace && ( + <> + ]} + type="text" + placeholder="Userphrase" + value={privateWorkspaceUserphrase} + onChange={handlePrivateWorkspaceUserphraseChange} + disabled={disabled} + /> + ]} + type="text" + placeholder="Name" + value={privateWorkspaceName} + onChange={handlePrivateWorkspaceNameChange} + disabled={disabled} + /> + + )} + + {onStrictSignInChange && ( + )} - {isPrivateWorkspace && ( - <> - ]} - type="text" - placeholder="Userphrase" - value={privateWorkspaceUserphrase} - onChange={handlePrivateWorkspaceUserphraseChange} - disabled={disabled} - /> - ]} - type="text" - placeholder="Name" - value={privateWorkspaceName} - onChange={handlePrivateWorkspaceNameChange} - disabled={disabled} - /> - - )} + + ]} + placeholder="https://api.standardnotes.com" + value={server} + onChange={handleSyncServerChange} + disabled={!enableServerOption && !disabled} + /> +
+ ) : null} + + ) +} - {onStrictSignInChange && ( -
- - - - -
- )} - - - ]} - placeholder="https://api.standardnotes.com" - value={server} - onChange={handleSyncServerChange} - disabled={!enableServerOption && !disabled} - /> -
- ) : null} - - ) - }, -) +export default observer(AdvancedOptions) diff --git a/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx b/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx index e1dbc3cfb..84570701f 100644 --- a/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx +++ b/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx @@ -2,14 +2,13 @@ import { STRING_NON_MATCHING_PASSWORDS } from '@/Strings' import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react' import { AccountMenuPane } from './AccountMenuPane' -import { Button } from '@/Components/Button/Button' -import { Checkbox } from '@/Components/Checkbox/Checkbox' -import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput' -import { Icon } from '@/Components/Icon/Icon' -import { IconButton } from '@/Components/Button/IconButton' +import Button from '@/Components/Button/Button' +import Checkbox from '@/Components/Checkbox/Checkbox' +import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput' +import Icon from '@/Components/Icon/Icon' +import IconButton from '@/Components/Button/IconButton' type Props = { appState: AppState @@ -19,140 +18,140 @@ type Props = { password: string } -export const ConfirmPassword: FunctionComponent = observer( - ({ application, appState, setMenuPane, email, password }) => { - const { notesAndTagsCount } = appState.accountMenu - const [confirmPassword, setConfirmPassword] = useState('') - const [isRegistering, setIsRegistering] = useState(false) - const [isEphemeral, setIsEphemeral] = useState(false) - const [shouldMergeLocal, setShouldMergeLocal] = useState(true) - const [error, setError] = useState('') +const ConfirmPassword: FunctionComponent = ({ application, appState, setMenuPane, email, password }) => { + const { notesAndTagsCount } = appState.accountMenu + const [confirmPassword, setConfirmPassword] = useState('') + const [isRegistering, setIsRegistering] = useState(false) + const [isEphemeral, setIsEphemeral] = useState(false) + const [shouldMergeLocal, setShouldMergeLocal] = useState(true) + const [error, setError] = useState('') - const passwordInputRef = useRef(null) + const passwordInputRef = useRef(null) - useEffect(() => { - passwordInputRef.current?.focus() - }, []) + useEffect(() => { + passwordInputRef.current?.focus() + }, []) - const handlePasswordChange = useCallback((text: string) => { - setConfirmPassword(text) - }, []) + const handlePasswordChange = useCallback((text: string) => { + setConfirmPassword(text) + }, []) - const handleEphemeralChange = useCallback(() => { - setIsEphemeral(!isEphemeral) - }, [isEphemeral]) + const handleEphemeralChange = useCallback(() => { + setIsEphemeral(!isEphemeral) + }, [isEphemeral]) - const handleShouldMergeChange = useCallback(() => { - setShouldMergeLocal(!shouldMergeLocal) - }, [shouldMergeLocal]) + const handleShouldMergeChange = useCallback(() => { + setShouldMergeLocal(!shouldMergeLocal) + }, [shouldMergeLocal]) - const handleConfirmFormSubmit = useCallback( - (e: Event) => { - e.preventDefault() + const handleConfirmFormSubmit = useCallback( + (e) => { + 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() + } + }, + [appState, application, confirmPassword, email, isEphemeral, password, shouldMergeLocal], + ) - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (error.length) { - setError('') - } - if (e.key === 'Enter') { - handleConfirmFormSubmit(e) - } - }, - [handleConfirmFormSubmit, error], - ) + const handleKeyDown: KeyboardEventHandler = useCallback( + (e) => { + if (error.length) { + setError('') + } + if (e.key === 'Enter') { + handleConfirmFormSubmit(e) + } + }, + [handleConfirmFormSubmit, error], + ) - const handleGoBack = useCallback(() => { - setMenuPane(AccountMenuPane.Register) - }, [setMenuPane]) + const handleGoBack = useCallback(() => { + setMenuPane(AccountMenuPane.Register) + }, [setMenuPane]) - return ( - <> -
- -
Confirm password
-
-
- Because your notes are encrypted using your password,{' '} - Standard Notes does not have a password reset option. If you forget your - password, you will permanently lose access to your data. -
-
- ]} - onChange={handlePasswordChange} - onKeyDown={handleKeyDown} - placeholder="Confirm password" - ref={passwordInputRef} - value={confirmPassword} - /> - {error ?
{error}
: null} - - + )} ) } + +export default WorkspaceMenuItem diff --git a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx index f9300e65f..c8484bb61 100644 --- a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx +++ b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx @@ -2,12 +2,13 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { AppState } from '@/UIModels/AppState' import { ApplicationDescriptor, ApplicationGroupEvent, ButtonType } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' -import { Menu } from '@/Components/Menu/Menu' -import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem' -import { WorkspaceMenuItem } from './WorkspaceMenuItem' +import { FunctionComponent, useCallback, useEffect, useState } from 'react' +import Icon from '@/Components/Icon/Icon' +import Menu from '@/Components/Menu/Menu' +import MenuItem from '@/Components/Menu/MenuItem' +import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator' +import { MenuItemType } from '@/Components/Menu/MenuItemType' +import WorkspaceMenuItem from './WorkspaceMenuItem' type Props = { mainApplicationGroup: ApplicationGroup @@ -16,74 +17,79 @@ type Props = { hideWorkspaceOptions?: boolean } -export const WorkspaceSwitcherMenu: FunctionComponent = observer( - ({ mainApplicationGroup, appState, isOpen, hideWorkspaceOptions = false }: Props) => { - const [applicationDescriptors, setApplicationDescriptors] = useState([]) +const WorkspaceSwitcherMenu: FunctionComponent = ({ + mainApplicationGroup, + appState, + isOpen, + hideWorkspaceOptions = false, +}: Props) => { + const [applicationDescriptors, setApplicationDescriptors] = useState([]) - useEffect(() => { - const applicationDescriptors = mainApplicationGroup.getDescriptors() - setApplicationDescriptors(applicationDescriptors) + useEffect(() => { + const applicationDescriptors = mainApplicationGroup.getDescriptors() + setApplicationDescriptors(applicationDescriptors) - const removeAppGroupObserver = mainApplicationGroup.addEventObserver((event) => { - if (event === ApplicationGroupEvent.DescriptorsDataChanged) { - const applicationDescriptors = mainApplicationGroup.getDescriptors() - setApplicationDescriptors(applicationDescriptors) - } - }) - - return () => { - removeAppGroupObserver() + const removeAppGroupObserver = mainApplicationGroup.addEventObserver((event) => { + if (event === ApplicationGroupEvent.DescriptorsDataChanged) { + const applicationDescriptors = mainApplicationGroup.getDescriptors() + setApplicationDescriptors(applicationDescriptors) } - }, [mainApplicationGroup]) + }) - const signoutAll = useCallback(async () => { - const confirmed = await appState.application.alertService.confirm( - 'Are you sure you want to sign out of all workspaces on this device?', - undefined, - 'Sign out all', - ButtonType.Danger, - ) - if (!confirmed) { - return - } - mainApplicationGroup.signOutAllWorkspaces().catch(console.error) - }, [mainApplicationGroup, appState]) + return () => { + removeAppGroupObserver() + } + }, [mainApplicationGroup]) - const destroyWorkspace = useCallback(() => { - appState.accountMenu.setSigningOut(true) - }, [appState]) - - return ( - - {applicationDescriptors.map((descriptor) => ( - void mainApplicationGroup.unloadCurrentAndActivateDescriptor(descriptor)} - renameDescriptor={(label: string) => mainApplicationGroup.renameDescriptor(descriptor, label)} - /> - ))} - - - { - void mainApplicationGroup.unloadCurrentAndCreateNewDescriptor() - }} - > - - Add another workspace - - - {!hideWorkspaceOptions && ( - - - Sign out all workspaces - - )} - + const signoutAll = useCallback(async () => { + const confirmed = await appState.application.alertService.confirm( + 'Are you sure you want to sign out of all workspaces on this device?', + undefined, + 'Sign out all', + ButtonType.Danger, ) - }, -) + if (!confirmed) { + return + } + mainApplicationGroup.signOutAllWorkspaces().catch(console.error) + }, [mainApplicationGroup, appState]) + + const destroyWorkspace = useCallback(() => { + appState.accountMenu.setSigningOut(true) + }, [appState]) + + return ( + + {applicationDescriptors.map((descriptor) => ( + void mainApplicationGroup.unloadCurrentAndActivateDescriptor(descriptor)} + renameDescriptor={(label: string) => mainApplicationGroup.renameDescriptor(descriptor, label)} + /> + ))} + + + { + void mainApplicationGroup.unloadCurrentAndCreateNewDescriptor() + }} + > + + Add another workspace + + + {!hideWorkspaceOptions && ( + + + Sign out all workspaces + + )} + + ) +} + +export default observer(WorkspaceSwitcherMenu) diff --git a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx index 312669ff2..95a788196 100644 --- a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx +++ b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx @@ -3,17 +3,16 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { AppState } from '@/UIModels/AppState' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' -import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' +import WorkspaceSwitcherMenu from './WorkspaceSwitcherMenu' type Props = { mainApplicationGroup: ApplicationGroup appState: AppState } -export const WorkspaceSwitcherOption: FunctionComponent = observer(({ mainApplicationGroup, appState }) => { +const WorkspaceSwitcherOption: FunctionComponent = ({ mainApplicationGroup, appState }) => { const buttonRef = useRef(null) const menuRef = useRef(null) const [isOpen, setIsOpen] = useState(false) @@ -64,4 +63,6 @@ export const WorkspaceSwitcherOption: FunctionComponent = observer(({ mai )} ) -}) +} + +export default observer(WorkspaceSwitcherOption) diff --git a/app/assets/javascripts/Components/ApplicationGroupView/ApplicationGroupView.tsx b/app/assets/javascripts/Components/ApplicationGroupView/ApplicationGroupView.tsx index eb9f66e1e..467debdaf 100644 --- a/app/assets/javascripts/Components/ApplicationGroupView/ApplicationGroupView.tsx +++ b/app/assets/javascripts/Components/ApplicationGroupView/ApplicationGroupView.tsx @@ -1,12 +1,12 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { WebApplication } from '@/UIModels/Application' -import { Component } from 'preact' -import { ApplicationView } from '@/Components/ApplicationView/ApplicationView' +import { Component } from 'react' +import ApplicationView from '@/Components/ApplicationView/ApplicationView' import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice' import { ApplicationGroupEvent, ApplicationGroupEventData, DeinitSource } from '@standardnotes/snjs' -import { unmountComponentAtNode, findDOMNode } from 'preact/compat' import { DialogContent, DialogOverlay } from '@reach/dialog' import { isDesktopApplication } from '@/Utils' +import DeallocateHandler from '../DeallocateHandler/DeallocateHandler' type Props = { server: string @@ -23,7 +23,7 @@ type State = { deviceDestroyed?: boolean } -export class ApplicationGroupView extends Component { +class ApplicationGroupView extends Component { applicationObserverRemover?: () => void private group?: ApplicationGroup private application?: WebApplication @@ -74,17 +74,15 @@ export class ApplicationGroupView extends Component { const onDestroy = this.props.onDestroy - const node = findDOMNode(this) as Element - unmountComponentAtNode(node) - onDestroy() } - render() { + override render() { const renderDialog = (message: string) => { return ( { return (
- + + +
) } } + +export default ApplicationGroupView diff --git a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx index 967efbb9e..1f2e8d1c0 100644 --- a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -1,56 +1,44 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { getPlatformString, getWindowUrlParams } from '@/Utils' import { AppStateEvent, PanelResizedData } from '@/UIModels/AppState' -import { ApplicationEvent, Challenge, PermissionDialog, removeFromArray } from '@standardnotes/snjs' +import { ApplicationEvent, Challenge, removeFromArray } from '@standardnotes/snjs' import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants' import { alertDialog } from '@/Services/AlertService' import { WebApplication } from '@/UIModels/Application' -import { Navigation } from '@/Components/Navigation/Navigation' -import { NoteGroupView } from '@/Components/NoteGroupView/NoteGroupView' -import { Footer } from '@/Components/Footer/Footer' -import { SessionsModal } from '@/Components/SessionsModal/SessionsModal' -import { PreferencesViewWrapper } from '@/Components/Preferences/PreferencesViewWrapper' -import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal' -import { NotesContextMenu } from '@/Components/NotesContextMenu/NotesContextMenu' -import { PurchaseFlowWrapper } from '@/Components/PurchaseFlow/PurchaseFlowWrapper' -import { render, FunctionComponent } from 'preact' -import { PermissionsModal } from '@/Components/PermissionsModal/PermissionsModal' -import { RevisionHistoryModalWrapper } from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper' -import { PremiumModalProvider } from '@/Hooks/usePremiumModal' -import { ConfirmSignoutContainer } from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal' -import { TagsContextMenu } from '@/Components/Tags/TagContextMenu' +import Navigation from '@/Components/Navigation/Navigation' +import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView' +import Footer from '@/Components/Footer/Footer' +import SessionsModal from '@/Components/SessionsModal/SessionsModal' +import PreferencesViewWrapper from '@/Components/Preferences/PreferencesViewWrapper' +import ChallengeModal from '@/Components/ChallengeModal/ChallengeModal' +import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu' +import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper' +import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react' +import RevisionHistoryModalWrapper from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper' +import PremiumModalProvider from '@/Hooks/usePremiumModal' +import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal' +import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu' import { ToastContainer } from '@standardnotes/stylekit' -import { FilePreviewModalWrapper } from '@/Components/Files/FilePreviewModal' -import { useCallback, useEffect, useMemo, useState } from 'preact/hooks' -import { isStateDealloced } from '@/UIModels/AppState/AbstractState' -import { ContentListView } from '@/Components/ContentListView/ContentListView' -import { FileContextMenu } from '@/Components/FileContextMenu/FileContextMenu' +import FilePreviewModalWrapper from '@/Components/Files/FilePreviewModal' +import ContentListView from '@/Components/ContentListView/ContentListView' +import FileContextMenuWrapper from '@/Components/FileContextMenu/FileContextMenu' +import PermissionsModalWrapper from '@/Components/PermissionsModal/PermissionsModalWrapper' type Props = { application: WebApplication mainApplicationGroup: ApplicationGroup } -export const ApplicationView: FunctionComponent = ({ application, mainApplicationGroup }) => { +const ApplicationView: FunctionComponent = ({ application, mainApplicationGroup }) => { const platformString = getPlatformString() const [appClass, setAppClass] = useState('') const [launched, setLaunched] = useState(false) const [needsUnlock, setNeedsUnlock] = useState(true) const [challenges, setChallenges] = useState([]) - const [dealloced, setDealloced] = useState(false) - const componentManager = application.componentManager const appState = application.getAppState() useEffect(() => { - setDealloced(application.dealloced) - }, [application.dealloced]) - - useEffect(() => { - if (dealloced) { - return - } - const desktopService = application.getDesktopService() if (desktopService) { @@ -70,7 +58,7 @@ export const ApplicationView: FunctionComponent = ({ application, mainApp }) .catch(console.error) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [application, dealloced]) + }, [application]) const removeChallenge = useCallback( (challenge: Challenge) => { @@ -81,29 +69,9 @@ export const ApplicationView: FunctionComponent = ({ application, mainApp [challenges], ) - const presentPermissionsDialog = useCallback( - (dialog: PermissionDialog) => { - render( - , - 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]) + }, [application]) const handleDemoSignInFromParams = useCallback(() => { const token = getWindowUrlParams().get('demo-token') @@ -183,7 +151,7 @@ export const ApplicationView: FunctionComponent = ({ application, mainApp <> {challenges.map((challenge) => { return ( -
+
= ({ application, mainApp ) }, [appState, challenges, mainApplicationGroup, removeChallenge, application]) - if (dealloced || isStateDealloced(appState)) { - return null - } - if (!renderAppContents) { return renderChallenges() } @@ -227,8 +191,8 @@ export const ApplicationView: FunctionComponent = ({ application, mainApp <> - - + + = ({ application, mainApp /> +
) } + +export default ApplicationView diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx index eba0ed061..a945f5e89 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx @@ -4,20 +4,18 @@ import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import VisuallyHidden from '@reach/visually-hidden' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { ChallengeReason, ContentType, FileItem, SNNote } from '@standardnotes/snjs' import { confirmDialog } from '@/Services/AlertService' import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit' import { StreamingFileReader } from '@standardnotes/filepicker' import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction' -import { AttachedFilesPopover } from './AttachedFilesPopover' +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,128 +23,109 @@ type Props = { onClickPreprocessing?: () => Promise } -export const AttachedFilesButton: FunctionComponent = observer( - ({ application, appState, onClickPreprocessing }: Props) => { - if (isStateDealloced(appState)) { - return null +const AttachedFilesButton: FunctionComponent = ({ application, appState, onClickPreprocessing }: Props) => { + const premiumModal = usePremiumModal() + const note: SNNote | undefined = appState.notes.firstSelectedNote + + const [open, setOpen] = useState(false) + const [position, setPosition] = useState({ + top: 0, + right: 0, + }) + const [maxHeight, setMaxHeight] = useState('auto') + const buttonRef = useRef(null) + const panelRef = useRef(null) + const containerRef = useRef(null) + const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen) + + useEffect(() => { + if (appState.filePreviewModal.isOpen) { + keepMenuOpen(true) + } else { + keepMenuOpen(false) } + }, [appState.filePreviewModal.isOpen, keepMenuOpen]) - const premiumModal = usePremiumModal() - const note: SNNote | undefined = appState.notes.firstSelectedNote + const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles) + const [allFiles, setAllFiles] = useState([]) + const [attachedFiles, setAttachedFiles] = useState([]) + const attachedFilesCount = attachedFiles.length - const [open, setOpen] = useState(false) - const [position, setPosition] = useState({ - top: 0, - right: 0, + useEffect(() => { + const unregisterFileStream = application.streamItems(ContentType.File, () => { + setAllFiles(application.items.getDisplayableFiles()) + if (note) { + setAttachedFiles(application.items.getFilesForNote(note)) + } }) - const [maxHeight, setMaxHeight] = useState('auto') - const buttonRef = useRef(null) - const panelRef = useRef(null) - const containerRef = useRef(null) - const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen) - useEffect(() => { - if (appState.filePreviewModal.isOpen) { - keepMenuOpen(true) - } else { - keepMenuOpen(false) + return () => { + unregisterFileStream() + } + }, [application, note]) + + const toggleAttachedFilesMenu = useCallback(async () => { + const rect = buttonRef.current?.getBoundingClientRect() + if (rect) { + const { clientHeight } = document.documentElement + const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect() + const footerHeightInPx = footerElementRect?.height + + if (footerHeightInPx) { + setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER) } - }, [appState.filePreviewModal.isOpen, keepMenuOpen]) - const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles) - const [allFiles, setAllFiles] = useState([]) - const [attachedFiles, setAttachedFiles] = useState([]) - const attachedFilesCount = attachedFiles.length - - useEffect(() => { - const unregisterFileStream = application.streamItems(ContentType.File, () => { - setAllFiles(application.items.getDisplayableFiles()) - if (note) { - setAttachedFiles(application.items.getFilesForNote(note)) - } + setPosition({ + top: rect.bottom, + right: document.body.clientWidth - rect.right, }) - return () => { - unregisterFileStream() + const newOpenState = !open + if (newOpenState && onClickPreprocessing) { + await onClickPreprocessing() } - }, [application, note]) - const toggleAttachedFilesMenu = useCallback(async () => { - const rect = buttonRef.current?.getBoundingClientRect() - if (rect) { - const { clientHeight } = document.documentElement - const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect() - const footerHeightInPx = footerElementRect?.height + setOpen(newOpenState) + } + }, [onClickPreprocessing, open]) - if (footerHeightInPx) { - setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER) - } + const prospectivelyShowFilesPremiumModal = useCallback(() => { + if (!appState.features.hasFiles) { + premiumModal.activate('Files') + } + }, [appState.features.hasFiles, premiumModal]) - setPosition({ - top: rect.bottom, - right: document.body.clientWidth - rect.right, - }) + const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => { + prospectivelyShowFilesPremiumModal() - const newOpenState = !open - if (newOpenState && onClickPreprocessing) { - await onClickPreprocessing() - } + await toggleAttachedFilesMenu() + }, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal]) - setOpen(newOpenState) - } - }, [onClickPreprocessing, open]) - - const prospectivelyShowFilesPremiumModal = useCallback(() => { - if (!appState.features.hasFiles) { - premiumModal.activate('Files') - } - }, [appState.features.hasFiles, premiumModal]) - - const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => { - prospectivelyShowFilesPremiumModal() - - await toggleAttachedFilesMenu() - }, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal]) - - const deleteFile = async (file: FileItem) => { - const shouldDelete = await confirmDialog({ - text: `Are you sure you want to permanently delete "${file.name}"?`, - confirmButtonStyle: 'danger', + const deleteFile = async (file: FileItem) => { + const shouldDelete = await confirmDialog({ + text: `Are you sure you want to permanently delete "${file.name}"?`, + confirmButtonStyle: 'danger', + }) + if (shouldDelete) { + const deletingToastId = addToast({ + type: ToastType.Loading, + message: `Deleting file "${file.name}"...`, }) - if (shouldDelete) { - const deletingToastId = addToast({ - type: ToastType.Loading, - message: `Deleting file "${file.name}"...`, - }) - await application.files.deleteFile(file) - addToast({ - type: ToastType.Success, - message: `Deleted file "${file.name}"`, - }) - dismissToast(deletingToastId) - } + await application.files.deleteFile(file) + addToast({ + type: ToastType.Success, + message: `Deleted file "${file.name}"`, + }) + dismissToast(deletingToastId) } + } - const downloadFile = async (file: FileItem) => { - appState.files.downloadFile(file).catch(console.error) - } + const downloadFile = async (file: FileItem) => { + appState.files.downloadFile(file).catch(console.error) + } - const attachFileToNote = useCallback( - async (file: FileItem) => { - if (!note) { - addToast({ - type: ToastType.Error, - message: 'Could not attach file because selected note was deleted', - }) - return - } - - await application.items.associateFileWithNote(file, note) - }, - [application.items, note], - ) - - const detachFileFromNote = async (file: FileItem) => { + const attachFileToNote = useCallback( + async (file: FileItem) => { if (!note) { addToast({ type: ToastType.Error, @@ -154,268 +133,283 @@ export const AttachedFilesButton: FunctionComponent = observer( }) return } - await application.items.disassociateFileWithNote(file, note) + + await application.items.associateFileWithNote(file, note) + }, + [application.items, note], + ) + + const detachFileFromNote = async (file: FileItem) => { + if (!note) { + addToast({ + type: ToastType.Error, + message: 'Could not attach file because selected note was deleted', + }) + return + } + await application.items.disassociateFileWithNote(file, note) + } + + const toggleFileProtection = async (file: FileItem) => { + let result: FileItem | undefined + if (file.protected) { + keepMenuOpen(true) + result = await application.mutator.unprotectFile(file) + keepMenuOpen(false) + buttonRef.current?.focus() + } else { + result = await application.mutator.protectFile(file) + } + const isProtected = result ? result.protected : file.protected + return isProtected + } + + const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => { + const authorizedFiles = await application.protections.authorizeProtectedActionForItems([file], challengeReason) + const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) + return isAuthorized + } + + const renameFile = async (file: FileItem, fileName: string) => { + await application.items.renameFile(file, fileName) + } + + const handleFileAction = async (action: PopoverFileItemAction) => { + const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file + let isAuthorizedForAction = true + + if (file.protected && action.type !== PopoverFileItemActionType.ToggleFileProtection) { + keepMenuOpen(true) + isAuthorizedForAction = await authorizeProtectedActionForFile(file, ChallengeReason.AccessProtectedFile) + keepMenuOpen(false) + buttonRef.current?.focus() } - const toggleFileProtection = async (file: FileItem) => { - let result: FileItem | undefined - if (file.protected) { + if (!isAuthorizedForAction) { + return false + } + + switch (action.type) { + case PopoverFileItemActionType.AttachFileToNote: + await attachFileToNote(file) + break + case PopoverFileItemActionType.DetachFileToNote: + await detachFileFromNote(file) + break + case PopoverFileItemActionType.DeleteFile: + await deleteFile(file) + break + case PopoverFileItemActionType.DownloadFile: + await downloadFile(file) + break + case PopoverFileItemActionType.ToggleFileProtection: { + const isProtected = await toggleFileProtection(file) + action.callback(isProtected) + break + } + case PopoverFileItemActionType.RenameFile: + await renameFile(file, action.payload.name) + break + case PopoverFileItemActionType.PreviewFile: { keepMenuOpen(true) - result = await application.mutator.unprotectFile(file) - keepMenuOpen(false) - buttonRef.current?.focus() - } else { - result = await application.mutator.protectFile(file) + const otherFiles = currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles + appState.filePreviewModal.activate( + file, + otherFiles.filter((file) => !file.protected), + ) + break } - const isProtected = result ? result.protected : file.protected - return isProtected } - const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => { - const authorizedFiles = await application.protections.authorizeProtectedActionForItems([file], challengeReason) - const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) - return isAuthorized + if ( + action.type !== PopoverFileItemActionType.DownloadFile && + action.type !== PopoverFileItemActionType.PreviewFile + ) { + application.sync.sync().catch(console.error) } - const renameFile = async (file: FileItem, fileName: string) => { - await application.items.renameFile(file, fileName) - } + return true + } - const handleFileAction = async (action: PopoverFileItemAction) => { - const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file - let isAuthorizedForAction = true - - if (file.protected && action.type !== PopoverFileItemActionType.ToggleFileProtection) { - keepMenuOpen(true) - isAuthorizedForAction = await authorizeProtectedActionForFile(file, ChallengeReason.AccessProtectedFile) - keepMenuOpen(false) - buttonRef.current?.focus() - } - - if (!isAuthorizedForAction) { - return false - } - - switch (action.type) { - case PopoverFileItemActionType.AttachFileToNote: - await attachFileToNote(file) - break - case PopoverFileItemActionType.DetachFileToNote: - await detachFileFromNote(file) - break - case PopoverFileItemActionType.DeleteFile: - await deleteFile(file) - break - case PopoverFileItemActionType.DownloadFile: - await downloadFile(file) - break - case PopoverFileItemActionType.ToggleFileProtection: { - const isProtected = await toggleFileProtection(file) - action.callback(isProtected) - break - } - case PopoverFileItemActionType.RenameFile: - await renameFile(file, action.payload.name) - break - case PopoverFileItemActionType.PreviewFile: { - keepMenuOpen(true) - const otherFiles = currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles - appState.filePreviewModal.activate( - file, - otherFiles.filter((file) => !file.protected), - ) - break - } - } - - if ( - action.type !== PopoverFileItemActionType.DownloadFile && - action.type !== PopoverFileItemActionType.PreviewFile - ) { - application.sync.sync().catch(console.error) - } - - return true - } - - const [isDraggingFiles, setIsDraggingFiles] = useState(false) - const dragCounter = useRef(0) - - const handleDrag = useCallback( - (event: DragEvent) => { - if (isHandlingFileDrag(event, application)) { - event.preventDefault() - event.stopPropagation() - } - }, - [application], - ) - - const handleDragIn = useCallback( - (event: DragEvent) => { - if (!isHandlingFileDrag(event, application)) { - return - } + const [isDraggingFiles, setIsDraggingFiles] = useState(false) + const dragCounter = useRef(0) + const handleDrag = useCallback( + (event: DragEvent) => { + if (isHandlingFileDrag(event, application)) { event.preventDefault() event.stopPropagation() + } + }, + [application], + ) - switch ((event.target as HTMLElement).id) { - case PopoverTabs.AllFiles: - setCurrentTab(PopoverTabs.AllFiles) - break - case PopoverTabs.AttachedFiles: - setCurrentTab(PopoverTabs.AttachedFiles) - break + const handleDragIn = useCallback( + (event: DragEvent) => { + if (!isHandlingFileDrag(event, application)) { + return + } + + event.preventDefault() + event.stopPropagation() + + switch ((event.target as HTMLElement).id) { + case PopoverTabs.AllFiles: + setCurrentTab(PopoverTabs.AllFiles) + break + case PopoverTabs.AttachedFiles: + setCurrentTab(PopoverTabs.AttachedFiles) + break + } + + dragCounter.current = dragCounter.current + 1 + + if (event.dataTransfer?.items.length) { + setIsDraggingFiles(true) + if (!open) { + toggleAttachedFilesMenu().catch(console.error) } + } + }, + [open, toggleAttachedFilesMenu, application], + ) - dragCounter.current = dragCounter.current + 1 + const handleDragOut = useCallback( + (event: DragEvent) => { + if (!isHandlingFileDrag(event, application)) { + return + } - if (event.dataTransfer?.items.length) { - setIsDraggingFiles(true) - if (!open) { - toggleAttachedFilesMenu().catch(console.error) + event.preventDefault() + event.stopPropagation() + + dragCounter.current = dragCounter.current - 1 + + if (dragCounter.current > 0) { + return + } + + setIsDraggingFiles(false) + }, + [application], + ) + + const handleDrop = useCallback( + (event: DragEvent) => { + if (!isHandlingFileDrag(event, application)) { + return + } + + event.preventDefault() + event.stopPropagation() + + setIsDraggingFiles(false) + + if (!appState.features.hasFiles) { + prospectivelyShowFilesPremiumModal() + return + } + + if (event.dataTransfer?.items.length) { + Array.from(event.dataTransfer.items).forEach(async (item) => { + const fileOrHandle = StreamingFileReader.available() + ? ((await item.getAsFileSystemHandle()) as FileSystemFileHandle) + : item.getAsFile() + + if (!fileOrHandle) { + return } - } - }, - [open, toggleAttachedFilesMenu, application], - ) - const handleDragOut = useCallback( - (event: DragEvent) => { - if (!isHandlingFileDrag(event, application)) { - return - } + const uploadedFiles = await appState.files.uploadNewFile(fileOrHandle) - event.preventDefault() - event.stopPropagation() + if (!uploadedFiles) { + return + } - dragCounter.current = dragCounter.current - 1 + if (currentTab === PopoverTabs.AttachedFiles) { + uploadedFiles.forEach((file) => { + attachFileToNote(file).catch(console.error) + }) + } + }) - if (dragCounter.current > 0) { - return - } - - setIsDraggingFiles(false) - }, - [application], - ) - - const handleDrop = useCallback( - (event: DragEvent) => { - if (!isHandlingFileDrag(event, application)) { - return - } - - event.preventDefault() - event.stopPropagation() - - setIsDraggingFiles(false) - - if (!appState.features.hasFiles) { - prospectivelyShowFilesPremiumModal() - return - } - - if (event.dataTransfer?.items.length) { - Array.from(event.dataTransfer.items).forEach(async (item) => { - const fileOrHandle = StreamingFileReader.available() - ? ((await item.getAsFileSystemHandle()) as FileSystemFileHandle) - : item.getAsFile() - - if (!fileOrHandle) { - return - } - - const uploadedFiles = await appState.files.uploadNewFile(fileOrHandle) - - if (!uploadedFiles) { - return - } - - if (currentTab === PopoverTabs.AttachedFiles) { - uploadedFiles.forEach((file) => { - attachFileToNote(file).catch(console.error) - }) - } - }) - - event.dataTransfer.clearData() - dragCounter.current = 0 - } - }, - [ - appState.files, - appState.features.hasFiles, - attachFileToNote, - currentTab, - application, - prospectivelyShowFilesPremiumModal, - ], - ) - - useEffect(() => { - window.addEventListener('dragenter', handleDragIn) - window.addEventListener('dragleave', handleDragOut) - window.addEventListener('dragover', handleDrag) - window.addEventListener('drop', handleDrop) - - return () => { - window.removeEventListener('dragenter', handleDragIn) - window.removeEventListener('dragleave', handleDragOut) - window.removeEventListener('dragover', handleDrag) - window.removeEventListener('drop', handleDrop) + event.dataTransfer.clearData() + dragCounter.current = 0 } - }, [handleDragIn, handleDrop, handleDrag, handleDragOut]) + }, + [ + appState.files, + appState.features.hasFiles, + attachFileToNote, + currentTab, + application, + prospectivelyShowFilesPremiumModal, + ], + ) - return ( -
- - { - if (event.key === 'Escape') { - setOpen(false) - } - }} - ref={buttonRef} - className={`sn-icon-button border-contrast ${attachedFilesCount > 0 ? 'py-1 px-3' : ''}`} - onBlur={closeOnBlur} - > - Attached files - - {attachedFilesCount > 0 && {attachedFilesCount}} - - { - if (event.key === 'Escape') { - setOpen(false) - buttonRef.current?.focus() - } - }} - ref={panelRef} - style={{ - ...position, - maxHeight, - }} - className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed" - onBlur={closeOnBlur} - > - {open && ( - - )} - - -
- ) - }, -) + useEffect(() => { + window.addEventListener('dragenter', handleDragIn) + window.addEventListener('dragleave', handleDragOut) + window.addEventListener('dragover', handleDrag) + window.addEventListener('drop', handleDrop) + + return () => { + window.removeEventListener('dragenter', handleDragIn) + window.removeEventListener('dragleave', handleDragOut) + window.removeEventListener('dragover', handleDrag) + window.removeEventListener('drop', handleDrop) + } + }, [handleDragIn, handleDrop, handleDrag, handleDragOut]) + + return ( +
+ + { + if (event.key === 'Escape') { + setOpen(false) + } + }} + ref={buttonRef} + className={`sn-icon-button border-contrast ${attachedFilesCount > 0 ? 'py-1 px-3' : ''}`} + onBlur={closeOnBlur} + > + Attached files + + {attachedFilesCount > 0 && {attachedFilesCount}} + + { + if (event.key === 'Escape') { + setOpen(false) + buttonRef.current?.focus() + } + }} + ref={panelRef} + style={{ + ...position, + maxHeight, + }} + className="sn-dropdown sn-dropdown--animated min-w-80 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed" + onBlur={closeOnBlur} + > + {open && ( + + )} + + +
+ ) +} + +export default observer(AttachedFilesButton) diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx index 947e29f17..57f2dc9db 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx @@ -4,11 +4,10 @@ import { AppState } from '@/UIModels/AppState' 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' -import { Button } from '@/Components/Button/Button' -import { Icon } from '@/Components/Icon/Icon' -import { PopoverFileItem } from './PopoverFileItem' +import { Dispatch, FunctionComponent, SetStateAction, useRef, useState } from 'react' +import Button from '@/Components/Button/Button' +import Icon from '@/Components/Icon/Icon' +import PopoverFileItem from './PopoverFileItem' import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction' import { PopoverTabs } from './PopoverTabs' @@ -21,153 +20,153 @@ type Props = { currentTab: PopoverTabs handleFileAction: (action: PopoverFileItemAction) => Promise isDraggingFiles: boolean - setCurrentTab: StateUpdater + setCurrentTab: Dispatch> } -export const AttachedFilesPopover: FunctionComponent = observer( - ({ - application, - appState, - allFiles, - attachedFiles, - closeOnBlur, - currentTab, - handleFileAction, - isDraggingFiles, - setCurrentTab, - }) => { - const [searchQuery, setSearchQuery] = useState('') - const searchInputRef = useRef(null) +const AttachedFilesPopover: FunctionComponent = ({ + application, + appState, + allFiles, + attachedFiles, + closeOnBlur, + currentTab, + handleFileAction, + isDraggingFiles, + setCurrentTab, +}) => { + const [searchQuery, setSearchQuery] = useState('') + const searchInputRef = useRef(null) - const filesList = currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles + const filesList = currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles - const filteredList = - searchQuery.length > 0 - ? filesList.filter((file) => file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1) - : filesList + const filteredList = + searchQuery.length > 0 + ? filesList.filter((file) => file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1) + : filesList - const handleAttachFilesClick = async () => { - const uploadedFiles = await appState.files.uploadNewFile() - if (!uploadedFiles) { - return - } - if (currentTab === PopoverTabs.AttachedFiles) { - uploadedFiles.forEach((file) => { - handleFileAction({ - type: PopoverFileItemActionType.AttachFileToNote, - payload: file, - }).catch(console.error) - }) - } + const handleAttachFilesClick = async () => { + const uploadedFiles = await appState.files.uploadNewFile() + if (!uploadedFiles) { + return } + if (currentTab === PopoverTabs.AttachedFiles) { + uploadedFiles.forEach((file) => { + handleFileAction({ + type: PopoverFileItemActionType.AttachFileToNote, + payload: file, + }).catch(console.error) + }) + } + } - return ( -
-
- - -
-
- {filteredList.length > 0 || searchQuery.length > 0 ? ( -
-
- { - setSearchQuery((e.target as HTMLInputElement).value) + return ( +
+
+ + +
+
+ {filteredList.length > 0 || searchQuery.length > 0 ? ( +
+
+ { + setSearchQuery((e.target as HTMLInputElement).value) + }} + onBlur={closeOnBlur} + ref={searchInputRef} + /> + {searchQuery.length > 0 && ( + - )} -
+ > + + + )}
- ) : null} - {filteredList.length > 0 ? ( - filteredList.map((file: FileItem) => { - return ( - - ) - }) - ) : ( -
-
- -
-
- {searchQuery.length > 0 - ? 'No result found' - : currentTab === PopoverTabs.AttachedFiles - ? 'No files attached to this note' - : 'No files found in this account'} -
- -
Or drop your files here
+
+ ) : null} + {filteredList.length > 0 ? ( + filteredList.map((file: FileItem) => { + return ( + + ) + }) + ) : ( +
+
+
- )} -
- {filteredList.length > 0 && ( - +
+ {searchQuery.length > 0 + ? 'No result found' + : currentTab === PopoverTabs.AttachedFiles + ? 'No files attached to this note' + : 'No files found in this account'} +
+ +
Or drop your files here
+
)}
- ) - }, -) + {filteredList.length > 0 && ( + + )} +
+ ) +} + +export default observer(AttachedFilesPopover) diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx index 5985f4bf7..61692e579 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx @@ -1,28 +1,15 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' import { KeyboardKey } from '@/Services/IOService' import { formatSizeToReadableString } from '@standardnotes/filepicker' -import { IconType, FileItem } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { useEffect, useRef, useState } from 'preact/hooks' -import { Icon, ICONS } from '@/Components/Icon/Icon' -import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction' -import { PopoverFileSubmenu } from './PopoverFileSubmenu' +import { FileItem } from '@standardnotes/snjs' +import { FormEventHandler, FunctionComponent, KeyboardEventHandler, useEffect, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' +import { PopoverFileItemActionType } from './PopoverFileItemAction' +import PopoverFileSubmenu from './PopoverFileSubmenu' +import { getFileIconComponent } from './getFileIconComponent' +import { PopoverFileItemProps } from './PopoverFileItemProps' -export const getFileIconComponent = (iconType: string, className: string) => { - const IconComponent = ICONS[iconType as keyof typeof ICONS] - - return -} - -export type PopoverFileItemProps = { - file: FileItem - isAttachedToNote: boolean - handleFileAction: (action: PopoverFileItemAction) => Promise - getIconType(type: string): IconType - closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void -} - -export const PopoverFileItem: FunctionComponent = ({ +const PopoverFileItem: FunctionComponent = ({ file, isAttachedToNote, handleFileAction, @@ -51,11 +38,11 @@ export const PopoverFileItem: FunctionComponent = ({ setIsRenamingFile(false) } - const handleFileNameInput = (event: Event) => { + const handleFileNameInput: FormEventHandler = (event) => { setFileName((event.target as HTMLInputElement).value) } - const handleFileNameInputKeyDown = (event: KeyboardEvent) => { + const handleFileNameInputKeyDown: KeyboardEventHandler = (event) => { if (event.key === KeyboardKey.Enter) { itemRef.current?.focus() } @@ -115,3 +102,5 @@ export const PopoverFileItem: FunctionComponent = ({
) } + +export default PopoverFileItem diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx new file mode 100644 index 000000000..35cc40679 --- /dev/null +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx @@ -0,0 +1,10 @@ +import { IconType, FileItem } from '@standardnotes/snjs' +import { PopoverFileItemAction } from './PopoverFileItemAction' + +export type PopoverFileItemProps = { + file: FileItem + isAttachedToNote: boolean + handleFileAction: (action: PopoverFileItemAction) => Promise + getIconType(type: string): IconType + closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void +} diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx index f46485834..c0604c38d 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx @@ -1,20 +1,19 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' -import { FunctionComponent } from 'preact' -import { StateUpdater, useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' -import { Switch } from '@/Components/Switch/Switch' +import { Dispatch, FunctionComponent, SetStateAction, useCallback, useEffect, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' +import Switch from '@/Components/Switch/Switch' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' -import { PopoverFileItemProps } from './PopoverFileItem' +import { PopoverFileItemProps } from './PopoverFileItemProps' import { PopoverFileItemActionType } from './PopoverFileItemAction' type Props = Omit & { - setIsRenamingFile: StateUpdater + setIsRenamingFile: Dispatch> previewHandler: () => void } -export const PopoverFileSubmenu: FunctionComponent = ({ +const PopoverFileSubmenu: FunctionComponent = ({ file, isAttachedToNote, handleFileAction, @@ -197,3 +196,5 @@ export const PopoverFileSubmenu: FunctionComponent = ({
) } + +export default PopoverFileSubmenu diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/getFileIconComponent.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/getFileIconComponent.tsx new file mode 100644 index 000000000..bd1e0e867 --- /dev/null +++ b/app/assets/javascripts/Components/AttachedFilesPopover/getFileIconComponent.tsx @@ -0,0 +1,7 @@ +import { ICONS } from '@/Components/Icon/Icon' + +export const getFileIconComponent = (iconType: string, className: string) => { + const IconComponent = ICONS[iconType as keyof typeof ICONS] + + return +} diff --git a/app/assets/javascripts/Components/Bubble/Bubble.tsx b/app/assets/javascripts/Components/Bubble/Bubble.tsx index 5a2146823..99c8f7bd8 100644 --- a/app/assets/javascripts/Components/Bubble/Bubble.tsx +++ b/app/assets/javascripts/Components/Bubble/Bubble.tsx @@ -1,4 +1,6 @@ -interface BubbleProperties { +import { FunctionComponent } from 'react' + +type Props = { label: string selected: boolean onSelect: () => void @@ -10,7 +12,7 @@ const styles = { selected: 'border-info bg-info color-neutral-contrast', } -const Bubble = ({ label, selected, onSelect }: BubbleProperties) => ( +const Bubble: FunctionComponent = ({ label, selected, onSelect }) => ( & { - children?: ComponentChildren +interface ButtonProps extends ComponentPropsWithoutRef<'button'> { + children?: ReactNode className?: string variant?: ButtonVariant dangerStyle?: boolean label?: string } -export const Button: FunctionComponent = forwardRef( +const Button = forwardRef( ( { variant = 'normal', @@ -66,3 +64,5 @@ export const Button: FunctionComponent = forwardRef( ) }, ) + +export default Button diff --git a/app/assets/javascripts/Components/Button/IconButton.tsx b/app/assets/javascripts/Components/Button/IconButton.tsx index 95b45a4d2..726c5564c 100644 --- a/app/assets/javascripts/Components/Button/IconButton.tsx +++ b/app/assets/javascripts/Components/Button/IconButton.tsx @@ -1,34 +1,18 @@ -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' +import { FunctionComponent, MouseEventHandler } from 'react' +import Icon from '@/Components/Icon/Icon' import { IconType } from '@standardnotes/snjs' -interface Props { - /** - * onClick - preventDefault is handled within the component - */ +type Props = { onClick: () => void - className?: string - icon: IconType - iconClassName?: string - - /** - * Button tooltip - */ title: string - focusable: boolean - disabled?: boolean } -/** - * IconButton component with an icon - * preventDefault is already handled within the component - */ -export const IconButton: FunctionComponent = ({ +const IconButton: FunctionComponent = ({ onClick, className = '', icon, @@ -37,7 +21,7 @@ export const IconButton: FunctionComponent = ({ iconClassName = '', disabled = false, }) => { - const click = (e: MouseEvent) => { + const click: MouseEventHandler = (e) => { e.preventDefault() onClick() } @@ -55,3 +39,5 @@ export const IconButton: FunctionComponent = ({ ) } + +export default IconButton diff --git a/app/assets/javascripts/Components/Button/RoundIconButton.tsx b/app/assets/javascripts/Components/Button/RoundIconButton.tsx index 31bb848c5..91ca24ca7 100644 --- a/app/assets/javascripts/Components/Button/RoundIconButton.tsx +++ b/app/assets/javascripts/Components/Button/RoundIconButton.tsx @@ -1,28 +1,18 @@ -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' +import { FunctionComponent, MouseEventHandler } from 'react' +import Icon from '@/Components/Icon/Icon' import { IconType } from '@standardnotes/snjs' type ButtonType = 'normal' | 'primary' -interface Props { - /** - * onClick - preventDefault is handled within the component - */ +type Props = { onClick: () => void - type: ButtonType - className?: string - icon: IconType } -/** - * IconButton component with an icon - * preventDefault is already handled within the component - */ -export const RoundIconButton: FunctionComponent = ({ onClick, type, className, icon: iconType }) => { - const click = (e: MouseEvent) => { +const RoundIconButton: FunctionComponent = ({ onClick, type, className, icon: iconType }) => { + const click: MouseEventHandler = (e) => { e.preventDefault() onClick() } @@ -33,3 +23,5 @@ export const RoundIconButton: FunctionComponent = ({ onClick, type, class ) } + +export default RoundIconButton diff --git a/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx b/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx index b29e8edce..9b305f863 100644 --- a/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx +++ b/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx @@ -9,22 +9,14 @@ import { removeFromArray, } from '@standardnotes/snjs' import { ProtectedIllustration } from '@standardnotes/icons' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useState } from 'preact/hooks' -import { Button } from '@/Components/Button/Button' -import { Icon } from '@/Components/Icon/Icon' -import { ChallengeModalPrompt } from './ChallengePrompt' -import { LockscreenWorkspaceSwitcher } from './LockscreenWorkspaceSwitcher' +import { FunctionComponent, useCallback, useEffect, useState } from 'react' +import Button from '@/Components/Button/Button' +import Icon from '@/Components/Icon/Icon' +import ChallengeModalPrompt from './ChallengePrompt' +import LockscreenWorkspaceSwitcher from './LockscreenWorkspaceSwitcher' import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { AppState } from '@/UIModels/AppState' - -type InputValue = { - prompt: ChallengePrompt - value: string | number | boolean - invalid: boolean -} - -export type ChallengeModalValues = Record +import { ChallengeModalValues } from './ChallengeModalValues' type Props = { application: WebApplication @@ -50,7 +42,7 @@ const validateValues = (values: ChallengeModalValues, prompts: ChallengePrompt[] return undefined } -export const ChallengeModal: FunctionComponent = ({ +const ChallengeModal: FunctionComponent = ({ application, appState, mainApplicationGroup, @@ -191,6 +183,7 @@ export const ChallengeModal: FunctionComponent = ({ key={challenge.id} > = ({ ) } + +export default ChallengeModal diff --git a/app/assets/javascripts/Components/ChallengeModal/ChallengeModalValues.tsx b/app/assets/javascripts/Components/ChallengeModal/ChallengeModalValues.tsx new file mode 100644 index 000000000..458c6007e --- /dev/null +++ b/app/assets/javascripts/Components/ChallengeModal/ChallengeModalValues.tsx @@ -0,0 +1,4 @@ +import { ChallengePrompt } from '@standardnotes/snjs' +import { InputValue } from './InputValue' + +export type ChallengeModalValues = Record diff --git a/app/assets/javascripts/Components/ChallengeModal/ChallengePrompt.tsx b/app/assets/javascripts/Components/ChallengeModal/ChallengePrompt.tsx index 8b1d34212..c1d07b476 100644 --- a/app/assets/javascripts/Components/ChallengeModal/ChallengePrompt.tsx +++ b/app/assets/javascripts/Components/ChallengeModal/ChallengePrompt.tsx @@ -1,9 +1,8 @@ import { ChallengePrompt, ChallengeValidation, ProtectionSessionDurations } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { useEffect, useRef } from 'preact/hooks' -import { DecoratedInput } from '@/Components/Input/DecoratedInput' -import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput' -import { ChallengeModalValues } from './ChallengeModal' +import { FunctionComponent, useEffect, useRef } from 'react' +import DecoratedInput from '@/Components/Input/DecoratedInput' +import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput' +import { ChallengeModalValues } from './ChallengeModalValues' type Props = { prompt: ChallengePrompt @@ -13,7 +12,7 @@ type Props = { isInvalid: boolean } -export const ChallengeModalPrompt: FunctionComponent = ({ prompt, values, index, onValueChange, isInvalid }) => { +const ChallengeModalPrompt: FunctionComponent = ({ prompt, values, index, onValueChange, isInvalid }) => { const inputRef = useRef(null) useEffect(() => { @@ -38,6 +37,7 @@ export const ChallengeModalPrompt: FunctionComponent = ({ prompt, values, const selected = option.valueInSeconds === values[prompt.id].value return (
) } + +export default ChallengeModalPrompt diff --git a/app/assets/javascripts/Components/ChallengeModal/InputValue.tsx b/app/assets/javascripts/Components/ChallengeModal/InputValue.tsx new file mode 100644 index 000000000..fa327652b --- /dev/null +++ b/app/assets/javascripts/Components/ChallengeModal/InputValue.tsx @@ -0,0 +1,7 @@ +import { ChallengePrompt } from '@standardnotes/snjs' + +export type InputValue = { + prompt: ChallengePrompt + value: string | number | boolean + invalid: boolean +} diff --git a/app/assets/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx b/app/assets/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx index 117633c74..6bd8b77ec 100644 --- a/app/assets/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx +++ b/app/assets/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx @@ -1,11 +1,10 @@ import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { AppState } from '@/UIModels/AppState' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' -import { FunctionComponent } from 'preact' -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/Icon' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' +import WorkspaceSwitcherMenu from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu' +import Button from '@/Components/Button/Button' +import Icon from '@/Components/Icon/Icon' import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' type Props = { @@ -13,7 +12,7 @@ type Props = { appState: AppState } -export const LockscreenWorkspaceSwitcher: FunctionComponent = ({ mainApplicationGroup, appState }) => { +const LockscreenWorkspaceSwitcher: FunctionComponent = ({ mainApplicationGroup, appState }) => { const buttonRef = useRef(null) const menuRef = useRef(null) const containerRef = useRef(null) @@ -65,3 +64,5 @@ export const LockscreenWorkspaceSwitcher: FunctionComponent = ({ mainAppl
) } + +export default LockscreenWorkspaceSwitcher diff --git a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx index 169fb734f..c7e2411cb 100644 --- a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx +++ b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx @@ -4,12 +4,10 @@ import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import VisuallyHidden from '@reach/visually-hidden' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useRef, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' -import { ChangeEditorMenu } from './ChangeEditorMenu' +import { FunctionComponent, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' +import ChangeEditorMenu from './ChangeEditorMenu' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' -import { isStateDealloced } from '@/UIModels/AppState/AbstractState' type Props = { application: WebApplication @@ -17,98 +15,94 @@ type Props = { onClickPreprocessing?: () => Promise } -export const ChangeEditorButton: FunctionComponent = observer( - ({ application, appState, onClickPreprocessing }: Props) => { - if (isStateDealloced(appState)) { - return null - } +const ChangeEditorButton: FunctionComponent = ({ application, appState, onClickPreprocessing }: Props) => { + const note = appState.notes.firstSelectedNote + const [isOpen, setIsOpen] = useState(false) + const [isVisible, setIsVisible] = useState(false) + const [position, setPosition] = useState({ + top: 0, + right: 0, + }) + const [maxHeight, setMaxHeight] = useState('auto') + const buttonRef = useRef(null) + const panelRef = useRef(null) + const containerRef = useRef(null) + const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen) - const note = appState.notes.firstSelectedNote - const [isOpen, setIsOpen] = useState(false) - const [isVisible, setIsVisible] = useState(false) - const [position, setPosition] = useState({ - top: 0, - right: 0, - }) - const [maxHeight, setMaxHeight] = useState('auto') - const buttonRef = useRef(null) - const panelRef = useRef(null) - const containerRef = useRef(null) - const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen) + const toggleChangeEditorMenu = async () => { + const rect = buttonRef.current?.getBoundingClientRect() + if (rect) { + const { clientHeight } = document.documentElement + const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect() + const footerHeightInPx = footerElementRect?.height - const toggleChangeEditorMenu = async () => { - const rect = buttonRef.current?.getBoundingClientRect() - if (rect) { - const { clientHeight } = document.documentElement - const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect() - const footerHeightInPx = footerElementRect?.height - - if (footerHeightInPx) { - setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER) - } - - setPosition({ - top: rect.bottom, - right: document.body.clientWidth - rect.right, - }) - - const newOpenState = !isOpen - if (newOpenState && onClickPreprocessing) { - await onClickPreprocessing() - } - - setIsOpen(newOpenState) - setTimeout(() => { - setIsVisible(newOpenState) - }) + if (footerHeightInPx) { + setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER) } - } - return ( -
- - { - if (event.key === 'Escape') { + setPosition({ + top: rect.bottom, + right: document.body.clientWidth - rect.right, + }) + + const newOpenState = !isOpen + if (newOpenState && onClickPreprocessing) { + await onClickPreprocessing() + } + + setIsOpen(newOpenState) + setTimeout(() => { + setIsVisible(newOpenState) + }) + } + } + + return ( +
+ + { + if (event.key === 'Escape') { + setIsOpen(false) + } + }} + onBlur={closeOnBlur} + ref={buttonRef} + className="sn-icon-button border-contrast" + > + Change note type + + + { + if (event.key === 'Escape') { + setIsOpen(false) + buttonRef.current?.focus() + } + }} + ref={panelRef} + style={{ + ...position, + maxHeight, + }} + className="sn-dropdown sn-dropdown--animated min-w-68 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed" + onBlur={closeOnBlur} + > + {isOpen && ( + { setIsOpen(false) - } - }} - onBlur={closeOnBlur} - ref={buttonRef} - className="sn-icon-button border-contrast" - > - Change note type - - - { - if (event.key === 'Escape') { - setIsOpen(false) - buttonRef.current?.focus() - } - }} - ref={panelRef} - style={{ - ...position, - maxHeight, - }} - className="sn-dropdown sn-dropdown--animated min-w-68 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed" - onBlur={closeOnBlur} - > - {isOpen && ( - { - setIsOpen(false) - }} - /> - )} - - -
- ) - }, -) + }} + /> + )} + +
+
+ ) +} + +export default observer(ChangeEditorButton) diff --git a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index 24c08cc7d..dae1095b3 100644 --- a/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -1,6 +1,7 @@ -import { Icon } from '@/Components/Icon/Icon' -import { Menu } from '@/Components/Menu/Menu' -import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem' +import Icon from '@/Components/Icon/Icon' +import Menu from '@/Components/Menu/Menu' +import MenuItem from '@/Components/Menu/MenuItem' +import { MenuItemType } from '@/Components/Menu/MenuItemType' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Strings' import { WebApplication } from '@/UIModels/Application' @@ -13,9 +14,9 @@ import { SNNote, TransactionalMutation, } from '@standardnotes/snjs' -import { Fragment, FunctionComponent } from 'preact' -import { useCallback, useEffect, useState } from 'preact/hooks' -import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption' +import { Fragment, FunctionComponent, useCallback, useEffect, useState } from 'react' +import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup' +import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem' import { createEditorMenuGroups } from './createEditorMenuGroups' import { PLAIN_EDITOR_NAME } from '@/Constants' import { @@ -34,7 +35,7 @@ type ChangeEditorMenuProps = { const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-') -export const ChangeEditorMenu: FunctionComponent = ({ +const ChangeEditorMenu: FunctionComponent = ({ application, closeOnBlur, closeMenu, @@ -189,6 +190,7 @@ export const ChangeEditorMenu: FunctionComponent = ({ } return ( = ({ ) } + +export default ChangeEditorMenu diff --git a/app/assets/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts b/app/assets/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts index 4ac1ad471..0ad8f50cb 100644 --- a/app/assets/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts +++ b/app/assets/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts @@ -8,7 +8,8 @@ import { GetFeatures, NoteType, } from '@standardnotes/snjs' -import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption' +import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup' +import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem' import { PLAIN_EDITOR_NAME } from '@/Constants' type EditorGroup = NoteType | 'plain' | 'others' diff --git a/app/assets/javascripts/Components/Checkbox/Checkbox.tsx b/app/assets/javascripts/Components/Checkbox/Checkbox.tsx index 4217c543b..f06a73fa8 100644 --- a/app/assets/javascripts/Components/Checkbox/Checkbox.tsx +++ b/app/assets/javascripts/Components/Checkbox/Checkbox.tsx @@ -1,14 +1,14 @@ -import { FunctionComponent } from 'preact' +import { ChangeEventHandler, FunctionComponent } from 'react' type CheckboxProps = { name: string checked: boolean - onChange: (e: Event) => void + onChange: ChangeEventHandler disabled?: boolean label: string } -export const Checkbox: FunctionComponent = ({ name, checked, onChange, disabled, label }) => { +const Checkbox: FunctionComponent = ({ name, checked, onChange, disabled, label }) => { return ( ) } + +export default Checkbox diff --git a/app/assets/javascripts/Components/ComponentView/ComponentView.tsx b/app/assets/javascripts/Components/ComponentView/ComponentView.tsx index 68e3e2f7a..33ff00cae 100644 --- a/app/assets/javascripts/Components/ComponentView/ComponentView.tsx +++ b/app/assets/javascripts/Components/ComponentView/ComponentView.tsx @@ -8,14 +8,13 @@ import { ComponentViewerError, } from '@standardnotes/snjs' import { WebApplication } from '@/UIModels/Application' -import { FunctionalComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { observer } from 'mobx-react-lite' -import { OfflineRestricted } from '@/Components/ComponentView/OfflineRestricted' -import { UrlMissing } from '@/Components/ComponentView/UrlMissing' -import { IsDeprecated } from '@/Components/ComponentView/IsDeprecated' -import { IsExpired } from '@/Components/ComponentView/IsExpired' -import { IssueOnLoading } from '@/Components/ComponentView/IssueOnLoading' +import OfflineRestricted from '@/Components/ComponentView/OfflineRestricted' +import UrlMissing from '@/Components/ComponentView/UrlMissing' +import IsDeprecated from '@/Components/ComponentView/IsDeprecated' +import IsExpired from '@/Components/ComponentView/IsExpired' +import IssueOnLoading from '@/Components/ComponentView/IssueOnLoading' import { AppState } from '@/UIModels/AppState' import { openSubscriptionDashboard } from '@/Utils/ManageSubscription' @@ -35,187 +34,187 @@ const MaxLoadThreshold = 4000 const VisibilityChangeKey = 'visibilitychange' const MSToWaitAfterIframeLoadToAvoidFlicker = 35 -export const ComponentView: FunctionalComponent = observer( - ({ application, onLoad, componentViewer, requestReload }) => { - const iframeRef = useRef(null) - const [loadTimeout, setLoadTimeout] = useState | undefined>(undefined) +const ComponentView: FunctionComponent = ({ application, onLoad, componentViewer, requestReload }) => { + const iframeRef = useRef(null) + const [loadTimeout, setLoadTimeout] = useState | undefined>(undefined) - const [hasIssueLoading, setHasIssueLoading] = useState(false) - const [isLoading, setIsLoading] = useState(true) - const [featureStatus, setFeatureStatus] = useState(componentViewer.getFeatureStatus()) - const [isComponentValid, setIsComponentValid] = useState(true) - const [error, setError] = useState(undefined) - const [deprecationMessage, setDeprecationMessage] = useState(undefined) - const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false) - const [didAttemptReload, setDidAttemptReload] = useState(false) + const [hasIssueLoading, setHasIssueLoading] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const [featureStatus, setFeatureStatus] = useState(componentViewer.getFeatureStatus()) + const [isComponentValid, setIsComponentValid] = useState(true) + const [error, setError] = useState(undefined) + const [deprecationMessage, setDeprecationMessage] = useState(undefined) + const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false) + const [didAttemptReload, setDidAttemptReload] = useState(false) - const component: SNComponent = componentViewer.component + const component: SNComponent = componentViewer.component - const manageSubscription = useCallback(() => { - openSubscriptionDashboard(application) - }, [application]) + const manageSubscription = useCallback(() => { + openSubscriptionDashboard(application) + }, [application]) - const reloadValidityStatus = useCallback(() => { - setFeatureStatus(componentViewer.getFeatureStatus()) - if (!componentViewer.lockReadonly) { - componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled) - } - setIsComponentValid(componentViewer.shouldRender()) + const reloadValidityStatus = useCallback(() => { + setFeatureStatus(componentViewer.getFeatureStatus()) + if (!componentViewer.lockReadonly) { + componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled) + } + setIsComponentValid(componentViewer.shouldRender()) - if (isLoading && !isComponentValid) { - setIsLoading(false) - } - - setError(componentViewer.getError()) - setDeprecationMessage(component.deprecationMessage) - }, [componentViewer, component.deprecationMessage, featureStatus, isComponentValid, isLoading]) - - useEffect(() => { - reloadValidityStatus() - }, [reloadValidityStatus]) - - const dismissDeprecationMessage = () => { - setIsDeprecationMessageDismissed(true) + if (isLoading && !isComponentValid) { + setIsLoading(false) } - const onVisibilityChange = useCallback(() => { - if (document.visibilityState === 'hidden') { - return - } - if (hasIssueLoading) { + setError(componentViewer.getError()) + setDeprecationMessage(component.deprecationMessage) + }, [componentViewer, component.deprecationMessage, featureStatus, isComponentValid, isLoading]) + + useEffect(() => { + reloadValidityStatus() + }, [reloadValidityStatus]) + + const dismissDeprecationMessage = () => { + setIsDeprecationMessageDismissed(true) + } + + const onVisibilityChange = useCallback(() => { + if (document.visibilityState === 'hidden') { + return + } + if (hasIssueLoading) { + requestReload?.(componentViewer) + } + }, [hasIssueLoading, componentViewer, requestReload]) + + useEffect(() => { + const loadTimeout = setTimeout(() => { + setIsLoading(false) + setHasIssueLoading(true) + + if (!didAttemptReload) { + setDidAttemptReload(true) requestReload?.(componentViewer) + } else { + document.addEventListener(VisibilityChangeKey, onVisibilityChange) } - }, [hasIssueLoading, componentViewer, requestReload]) + }, MaxLoadThreshold) - useEffect(() => { - const loadTimeout = setTimeout(() => { - setIsLoading(false) - setHasIssueLoading(true) - - if (!didAttemptReload) { - setDidAttemptReload(true) - requestReload?.(componentViewer) - } else { - document.addEventListener(VisibilityChangeKey, onVisibilityChange) - } - }, MaxLoadThreshold) - - setLoadTimeout(loadTimeout) - - return () => { - if (loadTimeout) { - clearTimeout(loadTimeout) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [componentViewer]) - - const onIframeLoad = useCallback(() => { - const iframe = iframeRef.current as HTMLIFrameElement - const contentWindow = iframe.contentWindow as Window + setLoadTimeout(loadTimeout) + return () => { if (loadTimeout) { clearTimeout(loadTimeout) } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [componentViewer]) - try { - componentViewer.setWindow(contentWindow) - } catch (error) { - console.error(error) + const onIframeLoad = useCallback(() => { + const iframe = iframeRef.current as HTMLIFrameElement + const contentWindow = iframe.contentWindow as Window + + if (loadTimeout) { + clearTimeout(loadTimeout) + } + + try { + componentViewer.setWindow(contentWindow) + } catch (error) { + console.error(error) + } + + setTimeout(() => { + setIsLoading(false) + setHasIssueLoading(false) + onLoad?.(component) + }, MSToWaitAfterIframeLoadToAvoidFlicker) + }, [componentViewer, onLoad, component, loadTimeout]) + + useEffect(() => { + const removeFeaturesChangedObserver = componentViewer.addEventObserver((event) => { + if (event === ComponentViewerEvent.FeatureStatusUpdated) { + setFeatureStatus(componentViewer.getFeatureStatus()) } + }) - setTimeout(() => { - setIsLoading(false) - setHasIssueLoading(false) - onLoad?.(component) - }, MSToWaitAfterIframeLoadToAvoidFlicker) - }, [componentViewer, onLoad, component, loadTimeout]) + return () => { + removeFeaturesChangedObserver() + } + }, [componentViewer]) - useEffect(() => { - const removeFeaturesChangedObserver = componentViewer.addEventObserver((event) => { - if (event === ComponentViewerEvent.FeatureStatusUpdated) { - setFeatureStatus(componentViewer.getFeatureStatus()) + useEffect(() => { + const removeActionObserver = componentViewer.addActionObserver((action, data) => { + switch (action) { + case ComponentAction.KeyDown: + application.io.handleComponentKeyDown(data.keyboardModifier) + break + case ComponentAction.KeyUp: + application.io.handleComponentKeyUp(data.keyboardModifier) + break + case ComponentAction.Click: + application.getAppState().notes.setContextMenuOpen(false) + break + default: + return + } + }) + return () => { + removeActionObserver() + } + }, [componentViewer, application]) + + useEffect(() => { + const unregisterDesktopObserver = application + .getDesktopService() + ?.registerUpdateObserver((updatedComponent: SNComponent) => { + if (updatedComponent.uuid === component.uuid && updatedComponent.active) { + requestReload?.(componentViewer) } }) - return () => { - removeFeaturesChangedObserver() - } - }, [componentViewer]) + return () => { + unregisterDesktopObserver?.() + } + }, [application, requestReload, componentViewer, component.uuid]) - useEffect(() => { - const removeActionObserver = componentViewer.addActionObserver((action, data) => { - switch (action) { - case ComponentAction.KeyDown: - application.io.handleComponentKeyDown(data.keyboardModifier) - break - case ComponentAction.KeyUp: - application.io.handleComponentKeyUp(data.keyboardModifier) - break - case ComponentAction.Click: - application.getAppState().notes.setContextMenuOpen(false) - break - default: - return - } - }) - return () => { - removeActionObserver() - } - }, [componentViewer, application]) + return ( + <> + {hasIssueLoading && ( + { + reloadValidityStatus(), requestReload?.(componentViewer, true) + }} + /> + )} - useEffect(() => { - const unregisterDesktopObserver = application - .getDesktopService() - ?.registerUpdateObserver((updatedComponent: SNComponent) => { - if (updatedComponent.uuid === component.uuid && updatedComponent.active) { - requestReload?.(componentViewer) - } - }) + {featureStatus !== FeatureStatus.Entitled && ( + + )} + {deprecationMessage && !isDeprecationMessageDismissed && ( + + )} + {error === ComponentViewerError.OfflineRestricted && } + {error === ComponentViewerError.MissingUrl && } + {component.uuid && isComponentValid && ( + + )} + {isLoading &&
} + + ) +} - return () => { - unregisterDesktopObserver?.() - } - }, [application, requestReload, componentViewer, component.uuid]) - - return ( - <> - {hasIssueLoading && ( - { - reloadValidityStatus(), requestReload?.(componentViewer, true) - }} - /> - )} - - {featureStatus !== FeatureStatus.Entitled && ( - - )} - {deprecationMessage && !isDeprecationMessageDismissed && ( - - )} - {error === ComponentViewerError.OfflineRestricted && } - {error === ComponentViewerError.MissingUrl && } - {component.uuid && isComponentValid && ( - - )} - {isLoading &&
} - - ) - }, -) +export default observer(ComponentView) diff --git a/app/assets/javascripts/Components/ComponentView/IsDeprecated.tsx b/app/assets/javascripts/Components/ComponentView/IsDeprecated.tsx index fab2a95f8..ee0125870 100644 --- a/app/assets/javascripts/Components/ComponentView/IsDeprecated.tsx +++ b/app/assets/javascripts/Components/ComponentView/IsDeprecated.tsx @@ -1,11 +1,11 @@ -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' -interface IProps { +type Props = { deprecationMessage: string | undefined dismissDeprecationMessage: () => void } -export const IsDeprecated: FunctionalComponent = ({ deprecationMessage, dismissDeprecationMessage }) => { +const IsDeprecated: FunctionComponent = ({ deprecationMessage, dismissDeprecationMessage }) => { return (
@@ -23,3 +23,5 @@ export const IsDeprecated: FunctionalComponent = ({ deprecationMessage,
) } + +export default IsDeprecated diff --git a/app/assets/javascripts/Components/ComponentView/IsExpired.tsx b/app/assets/javascripts/Components/ComponentView/IsExpired.tsx index d2df6cee1..61d88aa54 100644 --- a/app/assets/javascripts/Components/ComponentView/IsExpired.tsx +++ b/app/assets/javascripts/Components/ComponentView/IsExpired.tsx @@ -1,7 +1,7 @@ import { FeatureStatus } from '@standardnotes/snjs' -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' -interface IProps { +type Props = { expiredDate: string componentName: string featureStatus: FeatureStatus @@ -21,12 +21,7 @@ const statusString = (featureStatus: FeatureStatus, expiredDate: string, compone } } -export const IsExpired: FunctionalComponent = ({ - expiredDate, - featureStatus, - componentName, - manageSubscription, -}) => { +const IsExpired: FunctionComponent = ({ expiredDate, featureStatus, componentName, manageSubscription }) => { return (
@@ -52,3 +47,5 @@ export const IsExpired: FunctionalComponent = ({
) } + +export default IsExpired diff --git a/app/assets/javascripts/Components/ComponentView/IssueOnLoading.tsx b/app/assets/javascripts/Components/ComponentView/IssueOnLoading.tsx index 69f3c48ca..3f9a0f4c0 100644 --- a/app/assets/javascripts/Components/ComponentView/IssueOnLoading.tsx +++ b/app/assets/javascripts/Components/ComponentView/IssueOnLoading.tsx @@ -1,11 +1,11 @@ -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' -interface IProps { +type Props = { componentName: string reloadIframe: () => void } -export const IssueOnLoading: FunctionalComponent = ({ componentName, reloadIframe }) => { +const IssueOnLoading: FunctionComponent = ({ componentName, reloadIframe }) => { return (
@@ -23,3 +23,5 @@ export const IssueOnLoading: FunctionalComponent = ({ componentName, rel
) } + +export default IssueOnLoading diff --git a/app/assets/javascripts/Components/ComponentView/OfflineRestricted.tsx b/app/assets/javascripts/Components/ComponentView/OfflineRestricted.tsx index ff2fa1063..bb9258c9e 100644 --- a/app/assets/javascripts/Components/ComponentView/OfflineRestricted.tsx +++ b/app/assets/javascripts/Components/ComponentView/OfflineRestricted.tsx @@ -1,6 +1,6 @@ -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' -export const OfflineRestricted: FunctionalComponent = () => { +const OfflineRestricted: FunctionComponent = () => { return (
@@ -29,3 +29,5 @@ export const OfflineRestricted: FunctionalComponent = () => {
) } + +export default OfflineRestricted diff --git a/app/assets/javascripts/Components/ComponentView/UrlMissing.tsx b/app/assets/javascripts/Components/ComponentView/UrlMissing.tsx index ee70ebde6..cd74f3bcc 100644 --- a/app/assets/javascripts/Components/ComponentView/UrlMissing.tsx +++ b/app/assets/javascripts/Components/ComponentView/UrlMissing.tsx @@ -1,10 +1,10 @@ -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' -interface IProps { +type Props = { componentName: string } -export const UrlMissing: FunctionalComponent = ({ componentName }) => { +const UrlMissing: FunctionComponent = ({ componentName }) => { return (
@@ -20,3 +20,5 @@ export const UrlMissing: FunctionalComponent = ({ componentName }) => {
) } + +export default UrlMissing diff --git a/app/assets/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx b/app/assets/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx index ed8679b15..98e99bc33 100644 --- a/app/assets/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx +++ b/app/assets/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'preact/hooks' +import { FunctionComponent, useEffect, useRef, useState } from 'react' import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog' import { STRING_SIGN_OUT_CONFIRMATION } from '@/Strings' import { WebApplication } from '@/UIModels/Application' @@ -13,14 +13,7 @@ type Props = { applicationGroup: ApplicationGroup } -export const ConfirmSignoutContainer = observer((props: Props) => { - if (!props.appState.accountMenu.signingOut) { - return null - } - return -}) - -export const ConfirmSignoutModal = observer(({ application, appState, applicationGroup }: Props) => { +const ConfirmSignoutModal: FunctionComponent = ({ application, appState, applicationGroup }) => { const [deleteLocalBackups, setDeleteLocalBackups] = useState(false) const cancelRef = useRef(null) @@ -114,4 +107,15 @@ export const ConfirmSignoutModal = observer(({ application, appState, applicatio
) -}) +} + +ConfirmSignoutModal.displayName = 'ConfirmSignoutModal' + +const ConfirmSignoutContainer = (props: Props) => { + if (!props.appState.accountMenu.signingOut) { + return null + } + return +} + +export default observer(ConfirmSignoutContainer) diff --git a/app/assets/javascripts/Components/ContentListView/ContentList.tsx b/app/assets/javascripts/Components/ContentListView/ContentList.tsx index b249e6429..e084539bd 100644 --- a/app/assets/javascripts/Components/ContentListView/ContentList.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentList.tsx @@ -3,11 +3,10 @@ import { KeyboardKey } from '@/Services/IOService' import { AppState } from '@/UIModels/AppState' import { UuidString } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' +import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react' import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants' import { ListableContentItem } from './Types/ListableContentItem' -import { ContentListItem } from './ContentListItem' -import { useCallback } from 'preact/hooks' +import ContentListItem from './ContentListItem' type Props = { application: WebApplication @@ -17,59 +16,59 @@ type Props = { paginate: () => void } -export const ContentList: FunctionComponent = observer( - ({ application, appState, items, selectedItems, paginate }) => { - const { selectPreviousItem, selectNextItem } = appState.contentListView - const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = appState.contentListView.webDisplayOptions - const { sortBy } = appState.contentListView.displayOptions +const ContentList: FunctionComponent = ({ application, appState, items, selectedItems, paginate }) => { + const { selectPreviousItem, selectNextItem } = appState.contentListView + const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = appState.contentListView.webDisplayOptions + const { sortBy } = appState.contentListView.displayOptions - 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 onScroll: UIEventHandler = useCallback( + (e) => { + const offset = NOTES_LIST_SCROLL_THRESHOLD + const element = e.target as HTMLElement + if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) { + paginate() + } + }, + [paginate], + ) - const onKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === KeyboardKey.Up) { - e.preventDefault() - selectPreviousItem() - } else if (e.key === KeyboardKey.Down) { - e.preventDefault() - selectNextItem() - } - }, - [selectNextItem, selectPreviousItem], - ) + const onKeyDown: KeyboardEventHandler = useCallback( + (e) => { + if (e.key === KeyboardKey.Up) { + e.preventDefault() + selectPreviousItem() + } else if (e.key === KeyboardKey.Down) { + e.preventDefault() + selectNextItem() + } + }, + [selectNextItem, selectPreviousItem], + ) - return ( -
- {items.map((item) => ( - - ))} -
- ) - }, -) + return ( +
+ {items.map((item) => ( + + ))} +
+ ) +} + +export default observer(ContentList) diff --git a/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx b/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx index df29c4870..4a92569c9 100644 --- a/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentListItem.tsx @@ -1,10 +1,10 @@ import { ContentType, SNTag } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { FileListItem } from './FileListItem' -import { NoteListItem } from './NoteListItem' +import { FunctionComponent } from 'react' +import FileListItem from './FileListItem' +import NoteListItem from './NoteListItem' import { AbstractListItemProps } from './Types/AbstractListItemProps' -export const ContentListItem: FunctionComponent = (props) => { +const ContentListItem: FunctionComponent = (props) => { const getTags = () => { if (props.hideTags) { return [] @@ -34,3 +34,5 @@ export const ContentListItem: FunctionComponent = (props) return null } } + +export default ContentListItem diff --git a/app/assets/javascripts/Components/ContentListView/ContentListView.tsx b/app/assets/javascripts/Components/ContentListView/ContentListView.tsx index cf5796d42..cb793be26 100644 --- a/app/assets/javascripts/Components/ContentListView/ContentListView.tsx +++ b/app/assets/javascripts/Components/ContentListView/ContentListView.tsx @@ -4,27 +4,29 @@ import { AppState } from '@/UIModels/AppState' import { PANEL_NAME_NOTES } from '@/Constants' import { PrefKey } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { ContentList } from '@/Components/ContentListView/ContentList' -import { NotesListOptionsMenu } from '@/Components/ContentListView/NotesListOptionsMenu' -import { NoAccountWarning } from '@/Components/NoAccountWarning/NoAccountWarning' -import { SearchOptions } from '@/Components/SearchOptions/SearchOptions' -import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' +import { + ChangeEventHandler, + FunctionComponent, + KeyboardEventHandler, + useCallback, + useEffect, + useRef, + useState, +} from 'react' +import ContentList from '@/Components/ContentListView/ContentList' +import NotesListOptionsMenu from '@/Components/ContentListView/NotesListOptionsMenu' +import NoAccountWarningWrapper from '@/Components/NoAccountWarning/NoAccountWarning' +import SearchOptions from '@/Components/SearchOptions/SearchOptions' +import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/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 ContentListView: FunctionComponent = observer(({ application, appState }) => { - if (isStateDealloced(appState)) { - return null - } - +const ContentListView: FunctionComponent = ({ application, appState }) => { const itemsViewPanelRef = useRef(null) const displayOptionsMenuRef = useRef(null) @@ -104,9 +106,9 @@ export const ContentListView: FunctionComponent = observer(({ application } }, [application.io, createNewNote, searchBarElement, selectNextItem, selectPreviousItem]) - const onNoteFilterTextChange = useCallback( - (e: Event) => { - setNoteFilterText((e.target as HTMLInputElement).value) + const onNoteFilterTextChange: ChangeEventHandler = useCallback( + (e) => { + setNoteFilterText(e.target.value) }, [setNoteFilterText], ) @@ -114,8 +116,8 @@ export const ContentListView: FunctionComponent = observer(({ application const onSearchFocused = useCallback(() => setFocusedSearch(true), []) const onSearchBlurred = useCallback(() => setFocusedSearch(false), []) - const onNoteFilterKeyUp = useCallback( - (e: KeyboardEvent) => { + const onNoteFilterKeyUp: KeyboardEventHandler = useCallback( + (e) => { if (e.key === KeyboardKey.Enter) { onFilterEnter() } @@ -176,10 +178,10 @@ export const ContentListView: FunctionComponent = observer(({ application onKeyUp={onNoteFilterKeyUp} onFocus={onSearchFocused} onBlur={onSearchBlurred} - autocomplete="off" + autoComplete="off" /> {noteFilterText && ( - )} @@ -191,7 +193,7 @@ export const ContentListView: FunctionComponent = observer(({ application
)}
- +
@@ -253,4 +255,6 @@ export const ContentListView: FunctionComponent = observer(({ application )}
) -}) +} + +export default observer(ContentListView) diff --git a/app/assets/javascripts/Components/ContentListView/FileListItem.tsx b/app/assets/javascripts/Components/ContentListView/FileListItem.tsx index 33bc666c6..e8358298f 100644 --- a/app/assets/javascripts/Components/ContentListView/FileListItem.tsx +++ b/app/assets/javascripts/Components/ContentListView/FileListItem.tsx @@ -1,81 +1,89 @@ import { FileItem } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback } from 'preact/hooks' -import { getFileIconComponent } from '../AttachedFilesPopover/PopoverFileItem' -import { ListItemConflictIndicator } from './ListItemConflictIndicator' -import { ListItemFlagIcons } from './ListItemFlagIcons' -import { ListItemTags } from './ListItemTags' -import { ListItemMetadata } from './ListItemMetadata' +import { FunctionComponent, useCallback } from 'react' +import { getFileIconComponent } from '../AttachedFilesPopover/getFileIconComponent' +import ListItemConflictIndicator from './ListItemConflictIndicator' +import ListItemFlagIcons from './ListItemFlagIcons' +import ListItemTags from './ListItemTags' +import ListItemMetadata from './ListItemMetadata' import { DisplayableListItemProps } from './Types/DisplayableListItemProps' -export const FileListItem: FunctionComponent = observer( - ({ application, appState, hideDate, hideIcon, hideTags, item, selected, sortBy, tags }) => { - const openFileContextMenu = useCallback( - (posX: number, posY: number) => { - appState.files.setFileContextMenuLocation({ - x: posX, - y: posY, - }) - appState.files.setShowFileContextMenu(true) - }, - [appState.files], - ) - - const openContextMenu = useCallback( - (posX: number, posY: number) => { - void appState.contentListView.selectItemWithScrollHandling(item, { - userTriggered: true, - scrollIntoView: false, - }) - openFileContextMenu(posX, posY) - }, - [appState.contentListView, item, openFileContextMenu], - ) - - const onClick = useCallback(() => { - void appState.selectedItems.selectItem(item.uuid, true).then(({ didSelect }) => { - if (didSelect && appState.selectedItems.selectedItemsCount < 2) { - appState.filePreviewModal.activate(item as FileItem, appState.files.allFiles) - } +const FileListItem: FunctionComponent = ({ + application, + appState, + hideDate, + hideIcon, + hideTags, + item, + selected, + sortBy, + tags, +}) => { + const openFileContextMenu = useCallback( + (posX: number, posY: number) => { + appState.files.setFileContextMenuLocation({ + x: posX, + y: posY, }) - }, [appState.filePreviewModal, appState.files.allFiles, appState.selectedItems, item]) + appState.files.setShowFileContextMenu(true) + }, + [appState.files], + ) - const IconComponent = () => - getFileIconComponent( - application.iconsController.getIconForFileType((item as FileItem).mimeType), - 'w-5 h-5 flex-shrink-0', - ) + const openContextMenu = useCallback( + async (posX: number, posY: number) => { + const { didSelect } = await appState.selectedItems.selectItem(item.uuid) + if (didSelect) { + openFileContextMenu(posX, posY) + } + }, + [appState.selectedItems, item.uuid, openFileContextMenu], + ) - return ( -
{ - event.preventDefault() - openContextMenu(event.clientX, event.clientY) - }} - > - {!hideIcon ? ( -
- -
- ) : ( -
- )} -
-
-
{item.title}
-
- - - -
- -
+ const onClick = useCallback(() => { + void appState.selectedItems.selectItem(item.uuid, true).then(({ didSelect }) => { + if (didSelect && appState.selectedItems.selectedItemsCount < 2) { + appState.filePreviewModal.activate(item as FileItem, appState.files.allFiles) + } + }) + }, [appState.filePreviewModal, appState.files.allFiles, appState.selectedItems, item]) + + const IconComponent = () => + getFileIconComponent( + application.iconsController.getIconForFileType((item as FileItem).mimeType), + 'w-5 h-5 flex-shrink-0', ) - }, -) + + return ( +
{ + event.preventDefault() + void openContextMenu(event.clientX, event.clientY) + }} + > + {!hideIcon ? ( +
+ +
+ ) : ( +
+ )} +
+
+
{item.title}
+
+ + + +
+ +
+ ) +} + +export default observer(FileListItem) diff --git a/app/assets/javascripts/Components/ContentListView/ListItemConflictIndicator.tsx b/app/assets/javascripts/Components/ContentListView/ListItemConflictIndicator.tsx index b1146b707..1849b286d 100644 --- a/app/assets/javascripts/Components/ContentListView/ListItemConflictIndicator.tsx +++ b/app/assets/javascripts/Components/ContentListView/ListItemConflictIndicator.tsx @@ -1,11 +1,13 @@ -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' import { ListableContentItem } from './Types/ListableContentItem' -export const ListItemConflictIndicator: FunctionComponent<{ +type Props = { item: { conflictOf?: ListableContentItem['conflictOf'] } -}> = ({ item }) => { +} + +const ListItemConflictIndicator: FunctionComponent = ({ item }) => { return item.conflictOf ? (
@@ -14,3 +16,5 @@ export const ListItemConflictIndicator: FunctionComponent<{
) : null } + +export default ListItemConflictIndicator diff --git a/app/assets/javascripts/Components/ContentListView/ListItemFlagIcons.tsx b/app/assets/javascripts/Components/ContentListView/ListItemFlagIcons.tsx index 1cfb57dd3..9fd7b2d28 100644 --- a/app/assets/javascripts/Components/ContentListView/ListItemFlagIcons.tsx +++ b/app/assets/javascripts/Components/ContentListView/ListItemFlagIcons.tsx @@ -1,5 +1,5 @@ -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' +import { FunctionComponent } from 'react' +import Icon from '@/Components/Icon/Icon' import { ListableContentItem } from './Types/ListableContentItem' type Props = { @@ -12,7 +12,7 @@ type Props = { hasFiles?: boolean } -export const ListItemFlagIcons: FunctionComponent = ({ item, hasFiles = false }) => { +const ListItemFlagIcons: FunctionComponent = ({ item, hasFiles = false }) => { return (
{item.locked && ( @@ -43,3 +43,5 @@ export const ListItemFlagIcons: FunctionComponent = ({ item, hasFiles = f
) } + +export default ListItemFlagIcons diff --git a/app/assets/javascripts/Components/ContentListView/ListItemMetadata.tsx b/app/assets/javascripts/Components/ContentListView/ListItemMetadata.tsx index 94f54a2af..51c812542 100644 --- a/app/assets/javascripts/Components/ContentListView/ListItemMetadata.tsx +++ b/app/assets/javascripts/Components/ContentListView/ListItemMetadata.tsx @@ -1,5 +1,5 @@ import { CollectionSort, SortableItem } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' import { ListableContentItem } from './Types/ListableContentItem' type Props = { @@ -12,7 +12,7 @@ type Props = { sortBy: keyof SortableItem | undefined } -export const ListItemMetadata: FunctionComponent = ({ item, hideDate, sortBy }) => { +const ListItemMetadata: FunctionComponent = ({ item, hideDate, sortBy }) => { const showModifiedDate = sortBy === CollectionSort.UpdatedAt if (hideDate && !item.protected) { @@ -27,3 +27,5 @@ export const ListItemMetadata: FunctionComponent = ({ item, hideDate, sor
) } + +export default ListItemMetadata diff --git a/app/assets/javascripts/Components/ContentListView/ListItemTags.tsx b/app/assets/javascripts/Components/ContentListView/ListItemTags.tsx index 4d9258c85..31c040ecb 100644 --- a/app/assets/javascripts/Components/ContentListView/ListItemTags.tsx +++ b/app/assets/javascripts/Components/ContentListView/ListItemTags.tsx @@ -1,10 +1,12 @@ -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' +import { FunctionComponent } from 'react' +import Icon from '@/Components/Icon/Icon' -export const ListItemTags: FunctionComponent<{ +type Props = { hideTags: boolean tags: string[] -}> = ({ hideTags, tags }) => { +} + +const ListItemTags: FunctionComponent = ({ hideTags, tags }) => { if (hideTags || !tags.length) { return null } @@ -12,7 +14,10 @@ export const ListItemTags: FunctionComponent<{ return (
{tags.map((tag) => ( - + {tag} @@ -20,3 +25,5 @@ export const ListItemTags: FunctionComponent<{
) } + +export default ListItemTags diff --git a/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx b/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx index f51b530bf..b6fa0e960 100644 --- a/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx +++ b/app/assets/javascripts/Components/ContentListView/NoteListItem.tsx @@ -1,84 +1,97 @@ import { PLAIN_EDITOR_NAME } from '@/Constants' import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' -import { ListItemConflictIndicator } from './ListItemConflictIndicator' -import { ListItemFlagIcons } from './ListItemFlagIcons' -import { ListItemTags } from './ListItemTags' -import { ListItemMetadata } from './ListItemMetadata' +import { FunctionComponent } from 'react' +import Icon from '@/Components/Icon/Icon' +import ListItemConflictIndicator from './ListItemConflictIndicator' +import ListItemFlagIcons from './ListItemFlagIcons' +import ListItemTags from './ListItemTags' +import ListItemMetadata from './ListItemMetadata' import { DisplayableListItemProps } from './Types/DisplayableListItemProps' -export const NoteListItem: FunctionComponent = observer( - ({ application, appState, hideDate, hideIcon, hideTags, hidePreview, item, selected, sortBy, tags }) => { - const editorForNote = application.componentManager.editorForNote(item as SNNote) - const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME - const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type) - const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0 +const NoteListItem: FunctionComponent = ({ + application, + appState, + hideDate, + hideIcon, + hideTags, + hidePreview, + item, + selected, + sortBy, + tags, +}) => { + const editorForNote = application.componentManager.editorForNote(item as SNNote) + const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME + const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type) + const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0 - const openNoteContextMenu = (posX: number, posY: number) => { - appState.notes.setContextMenuClickLocation({ - x: posX, - y: posY, - }) - appState.notes.reloadContextMenuLayout() - appState.notes.setContextMenuOpen(true) - } + const openNoteContextMenu = (posX: number, posY: number) => { + appState.notes.setContextMenuClickLocation({ + x: posX, + y: posY, + }) + appState.notes.reloadContextMenuLayout() + appState.notes.setContextMenuOpen(true) + } - const openContextMenu = (posX: number, posY: number) => { - void appState.selectedItems.selectItem(item.uuid, true) + const openContextMenu = async (posX: number, posY: number) => { + const { didSelect } = await appState.selectedItems.selectItem(item.uuid, true) + if (didSelect) { openNoteContextMenu(posX, posY) } + } - return ( -
{ - void appState.selectedItems.selectItem(item.uuid, true) - }} - onContextMenu={(event) => { - event.preventDefault() - openContextMenu(event.clientX, event.clientY) - }} - > - {!hideIcon ? ( -
- -
- ) : ( -
- )} -
-
-
{item.title}
-
- {!hidePreview && !item.hidePreview && !item.protected && ( -
- {item.preview_html && ( -
- )} - {!item.preview_html && item.preview_plain && ( -
{item.preview_plain}
- )} - {!item.preview_html && !item.preview_plain && item.text && ( -
{item.text}
- )} -
- )} - - - + return ( +
{ + void appState.selectedItems.selectItem(item.uuid, true) + }} + onContextMenu={(event) => { + event.preventDefault() + void openContextMenu(event.clientX, event.clientY) + }} + > + {!hideIcon ? ( +
+
- + ) : ( +
+ )} +
+
+
{item.title}
+
+ {!hidePreview && !item.hidePreview && !item.protected && ( +
+ {item.preview_html && ( +
+ )} + {!item.preview_html && item.preview_plain && ( +
{item.preview_plain}
+ )} + {!item.preview_html && !item.preview_plain && item.text && ( +
{item.text}
+ )} +
+ )} + + +
- ) - }, -) + +
+ ) +} + +export default observer(NoteListItem) diff --git a/app/assets/javascripts/Components/ContentListView/NotesListOptionsMenu.tsx b/app/assets/javascripts/Components/ContentListView/NotesListOptionsMenu.tsx index 23bd29e1b..569b8650f 100644 --- a/app/assets/javascripts/Components/ContentListView/NotesListOptionsMenu.tsx +++ b/app/assets/javascripts/Components/ContentListView/NotesListOptionsMenu.tsx @@ -1,11 +1,12 @@ import { WebApplication } from '@/UIModels/Application' import { CollectionSort, CollectionSortProperty, PrefKey } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' -import { Menu } from '@/Components/Menu/Menu' -import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem' +import { FunctionComponent, useCallback, useState } from 'react' +import Icon from '@/Components/Icon/Icon' +import Menu from '@/Components/Menu/Menu' +import MenuItem from '@/Components/Menu/MenuItem' +import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator' +import { MenuItemType } from '@/Components/Menu/MenuItemType' type Props = { application: WebApplication @@ -14,235 +15,238 @@ type Props = { isOpen: boolean } -export const NotesListOptionsMenu: FunctionComponent = observer( - ({ closeDisplayOptionsMenu, closeOnBlur, application, isOpen }) => { - const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt)) - const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false)) - const [hidePreview, setHidePreview] = useState(() => application.getPreference(PrefKey.NotesHideNotePreview, false)) - const [hideDate, setHideDate] = useState(() => application.getPreference(PrefKey.NotesHideDate, false)) - const [hideTags, setHideTags] = useState(() => application.getPreference(PrefKey.NotesHideTags, true)) - const [hidePinned, setHidePinned] = useState(() => application.getPreference(PrefKey.NotesHidePinned, false)) - const [showArchived, setShowArchived] = useState(() => application.getPreference(PrefKey.NotesShowArchived, false)) - const [showTrashed, setShowTrashed] = useState(() => application.getPreference(PrefKey.NotesShowTrashed, false)) - const [hideProtected, setHideProtected] = useState(() => - application.getPreference(PrefKey.NotesHideProtected, false), - ) - const [hideEditorIcon, setHideEditorIcon] = useState(() => - application.getPreference(PrefKey.NotesHideEditorIcon, false), - ) +const NotesListOptionsMenu: FunctionComponent = ({ + closeDisplayOptionsMenu, + closeOnBlur, + application, + isOpen, +}) => { + const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt)) + const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false)) + const [hidePreview, setHidePreview] = useState(() => application.getPreference(PrefKey.NotesHideNotePreview, false)) + const [hideDate, setHideDate] = useState(() => application.getPreference(PrefKey.NotesHideDate, false)) + const [hideTags, setHideTags] = useState(() => application.getPreference(PrefKey.NotesHideTags, true)) + const [hidePinned, setHidePinned] = useState(() => application.getPreference(PrefKey.NotesHidePinned, false)) + const [showArchived, setShowArchived] = useState(() => application.getPreference(PrefKey.NotesShowArchived, false)) + const [showTrashed, setShowTrashed] = useState(() => application.getPreference(PrefKey.NotesShowTrashed, false)) + const [hideProtected, setHideProtected] = useState(() => application.getPreference(PrefKey.NotesHideProtected, false)) + const [hideEditorIcon, setHideEditorIcon] = useState(() => + application.getPreference(PrefKey.NotesHideEditorIcon, false), + ) - const toggleSortReverse = useCallback(() => { - application.setPreference(PrefKey.SortNotesReverse, !sortReverse).catch(console.error) - setSortReverse(!sortReverse) - }, [application, sortReverse]) + const toggleSortReverse = useCallback(() => { + application.setPreference(PrefKey.SortNotesReverse, !sortReverse).catch(console.error) + setSortReverse(!sortReverse) + }, [application, sortReverse]) - const toggleSortBy = useCallback( - (sort: CollectionSortProperty) => { - if (sortBy === sort) { - toggleSortReverse() - } else { - setSortBy(sort) - application.setPreference(PrefKey.SortNotesBy, sort).catch(console.error) - } - }, - [application, sortBy, toggleSortReverse], - ) + 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 = useCallback(() => { - toggleSortBy(CollectionSort.UpdatedAt) - }, [toggleSortBy]) + const toggleSortByDateModified = useCallback(() => { + toggleSortBy(CollectionSort.UpdatedAt) + }, [toggleSortBy]) - const toggleSortByCreationDate = useCallback(() => { - toggleSortBy(CollectionSort.CreatedAt) - }, [toggleSortBy]) + const toggleSortByCreationDate = useCallback(() => { + toggleSortBy(CollectionSort.CreatedAt) + }, [toggleSortBy]) - const toggleSortByTitle = useCallback(() => { - toggleSortBy(CollectionSort.Title) - }, [toggleSortBy]) + const toggleSortByTitle = useCallback(() => { + toggleSortBy(CollectionSort.Title) + }, [toggleSortBy]) - const toggleHidePreview = useCallback(() => { - setHidePreview(!hidePreview) - application.setPreference(PrefKey.NotesHideNotePreview, !hidePreview).catch(console.error) - }, [application, hidePreview]) + const toggleHidePreview = useCallback(() => { + setHidePreview(!hidePreview) + application.setPreference(PrefKey.NotesHideNotePreview, !hidePreview).catch(console.error) + }, [application, hidePreview]) - const toggleHideDate = useCallback(() => { - setHideDate(!hideDate) - application.setPreference(PrefKey.NotesHideDate, !hideDate).catch(console.error) - }, [application, hideDate]) + const toggleHideDate = useCallback(() => { + setHideDate(!hideDate) + application.setPreference(PrefKey.NotesHideDate, !hideDate).catch(console.error) + }, [application, hideDate]) - const toggleHideTags = useCallback(() => { - setHideTags(!hideTags) - application.setPreference(PrefKey.NotesHideTags, !hideTags).catch(console.error) - }, [application, hideTags]) + const toggleHideTags = useCallback(() => { + setHideTags(!hideTags) + application.setPreference(PrefKey.NotesHideTags, !hideTags).catch(console.error) + }, [application, hideTags]) - const toggleHidePinned = useCallback(() => { - setHidePinned(!hidePinned) - application.setPreference(PrefKey.NotesHidePinned, !hidePinned).catch(console.error) - }, [application, hidePinned]) + const toggleHidePinned = useCallback(() => { + setHidePinned(!hidePinned) + application.setPreference(PrefKey.NotesHidePinned, !hidePinned).catch(console.error) + }, [application, hidePinned]) - const toggleShowArchived = useCallback(() => { - setShowArchived(!showArchived) - application.setPreference(PrefKey.NotesShowArchived, !showArchived).catch(console.error) - }, [application, showArchived]) + const toggleShowArchived = useCallback(() => { + setShowArchived(!showArchived) + application.setPreference(PrefKey.NotesShowArchived, !showArchived).catch(console.error) + }, [application, showArchived]) - const toggleShowTrashed = useCallback(() => { - setShowTrashed(!showTrashed) - application.setPreference(PrefKey.NotesShowTrashed, !showTrashed).catch(console.error) - }, [application, showTrashed]) + const toggleShowTrashed = useCallback(() => { + setShowTrashed(!showTrashed) + application.setPreference(PrefKey.NotesShowTrashed, !showTrashed).catch(console.error) + }, [application, showTrashed]) - const toggleHideProtected = useCallback(() => { - setHideProtected(!hideProtected) - application.setPreference(PrefKey.NotesHideProtected, !hideProtected).catch(console.error) - }, [application, hideProtected]) + const toggleHideProtected = useCallback(() => { + setHideProtected(!hideProtected) + application.setPreference(PrefKey.NotesHideProtected, !hideProtected).catch(console.error) + }, [application, hideProtected]) - const toggleEditorIcon = useCallback(() => { - setHideEditorIcon(!hideEditorIcon) - application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon).catch(console.error) - }, [application, hideEditorIcon]) + const toggleEditorIcon = useCallback(() => { + setHideEditorIcon(!hideEditorIcon) + application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon).catch(console.error) + }, [application, hideEditorIcon]) - return ( - +
Sort by
+ -
Sort by
- -
- Date modified - {sortBy === CollectionSort.UpdatedAt ? ( - sortReverse ? ( - - ) : ( - - ) - ) : null} -
-
- -
- Creation date - {sortBy === CollectionSort.CreatedAt ? ( - sortReverse ? ( - - ) : ( - - ) - ) : null} -
-
- -
- Title - {sortBy === CollectionSort.Title ? ( - sortReverse ? ( - - ) : ( - - ) - ) : null} -
-
- -
View
- -
Show note preview
-
- - Show date - - - Show tags - - - Show editor icon - -
-
Other
- - Show pinned notes - - - Show protected notes - - - Show archived notes - - - Show trashed notes - -
- ) - }, -) +
+ Date modified + {sortBy === CollectionSort.UpdatedAt ? ( + sortReverse ? ( + + ) : ( + + ) + ) : null} +
+ + +
+ Creation date + {sortBy === CollectionSort.CreatedAt ? ( + sortReverse ? ( + + ) : ( + + ) + ) : null} +
+
+ +
+ Title + {sortBy === CollectionSort.Title ? ( + sortReverse ? ( + + ) : ( + + ) + ) : null} +
+
+ +
View
+ +
Show note preview
+
+ + Show date + + + Show tags + + + Show editor icon + +
+
Other
+ + Show pinned notes + + + Show protected notes + + + Show archived notes + + + Show trashed notes + + + ) +} + +export default observer(NotesListOptionsMenu) diff --git a/app/assets/javascripts/Components/DeallocateHandler/DeallocateHandler.tsx b/app/assets/javascripts/Components/DeallocateHandler/DeallocateHandler.tsx new file mode 100644 index 000000000..770c4cc12 --- /dev/null +++ b/app/assets/javascripts/Components/DeallocateHandler/DeallocateHandler.tsx @@ -0,0 +1,17 @@ +import { WebApplication } from '@/UIModels/Application' +import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'react' + +type Props = { + application: WebApplication +} + +const DeallocateHandler: FunctionComponent = ({ application, children }) => { + if (application.dealloced) { + return null + } + + return <>{children} +} + +export default observer(DeallocateHandler) diff --git a/app/assets/javascripts/Components/Dropdown/Dropdown.tsx b/app/assets/javascripts/Components/Dropdown/Dropdown.tsx index abf75f76d..806be187f 100644 --- a/app/assets/javascripts/Components/Dropdown/Dropdown.tsx +++ b/app/assets/javascripts/Components/Dropdown/Dropdown.tsx @@ -1,16 +1,8 @@ import { ListboxArrow, ListboxButton, ListboxInput, ListboxList, ListboxOption, ListboxPopover } from '@reach/listbox' import VisuallyHidden from '@reach/visually-hidden' -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' -import { IconType } from '@standardnotes/snjs' - -export type DropdownItem = { - icon?: IconType - iconClassName?: string - label: string - value: string - disabled?: boolean -} +import { FunctionComponent } from 'react' +import Icon from '@/Components/Icon/Icon' +import { DropdownItem } from './DropdownItem' type DropdownProps = { id: string @@ -46,7 +38,7 @@ const CustomDropdownButton: FunctionComponent = ({ ) -export const Dropdown: FunctionComponent = ({ id, label, items, value, onChange, disabled }) => { +const Dropdown: FunctionComponent = ({ id, label, items, value, onChange, disabled }) => { const labelId = `${id}-label` const handleChange = (value: string) => { @@ -79,6 +71,7 @@ export const Dropdown: FunctionComponent = ({ id, label, items, v {items.map((item) => ( = ({ id, label, items, v ) } + +export default Dropdown diff --git a/app/assets/javascripts/Components/Dropdown/DropdownItem.tsx b/app/assets/javascripts/Components/Dropdown/DropdownItem.tsx new file mode 100644 index 000000000..05aaf556d --- /dev/null +++ b/app/assets/javascripts/Components/Dropdown/DropdownItem.tsx @@ -0,0 +1,9 @@ +import { IconType } from '@standardnotes/snjs' + +export type DropdownItem = { + icon?: IconType + iconClassName?: string + label: string + value: string + disabled?: boolean +} diff --git a/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx b/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx index ec27d4636..1243073b0 100644 --- a/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx +++ b/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx @@ -3,18 +3,16 @@ import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import React from 'react' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { PopoverFileItemAction } from '../AttachedFilesPopover/PopoverFileItemAction' import { PopoverTabs } from '../AttachedFilesPopover/PopoverTabs' -import { FileMenuOptions } from './FileMenuOptions' +import FileMenuOptions from './FileMenuOptions' type Props = { appState: AppState } -export const FileContextMenu: FunctionComponent = observer(({ appState }) => { +const FileContextMenu: FunctionComponent = observer(({ appState }) => { const { selectedFiles, showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = appState.files const [contextMenuStyle, setContextMenuStyle] = useState({ @@ -28,9 +26,6 @@ export const FileContextMenu: FunctionComponent = observer(({ appState }) useCloseOnClickOutside(contextMenuRef, () => appState.files.setShowFileContextMenu(false)) const selectedFile = selectedFiles[0] - if (!showFileContextMenu || !selectedFile) { - return null - } const reloadContextMenuLayout = useCallback(() => { const { clientHeight } = document.documentElement @@ -118,3 +113,19 @@ export const FileContextMenu: FunctionComponent = observer(({ appState })
) }) + +FileContextMenu.displayName = 'FileContextMenu' + +const FileContextMenuWrapper: FunctionComponent = ({ appState }) => { + const { selectedFiles, showFileContextMenu } = appState.files + + const selectedFile = selectedFiles[0] + + if (!showFileContextMenu || !selectedFile) { + return null + } + + return +} + +export default observer(FileContextMenuWrapper) diff --git a/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx b/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx index 17bcbcb2b..01909bee6 100644 --- a/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx +++ b/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx @@ -1,9 +1,9 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' import { FileItem } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' import { PopoverFileItemAction, PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction' -import { Icon } from '@/Components/Icon/Icon' -import { Switch } from '@/Components/Switch/Switch' +import Icon from '@/Components/Icon/Icon' +import Switch from '@/Components/Switch/Switch' type Props = { closeMenu: () => void @@ -17,7 +17,7 @@ type Props = { shouldShowAttachOption: boolean } -export const FileMenuOptions: FunctionComponent = ({ +const FileMenuOptions: FunctionComponent = ({ closeMenu, closeOnBlur, file, @@ -139,3 +139,5 @@ export const FileMenuOptions: FunctionComponent = ({ ) } + +export default FileMenuOptions diff --git a/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx b/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx index c12f9e356..20afa53d2 100644 --- a/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx @@ -1,13 +1,13 @@ import { formatSizeToReadableString } from '@standardnotes/filepicker' import { FileItem } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' +import { FunctionComponent } from 'react' +import Icon from '@/Components/Icon/Icon' type Props = { file: FileItem } -export const FilePreviewInfoPanel: FunctionComponent = ({ file }) => { +const FilePreviewInfoPanel: FunctionComponent = ({ file }) => { return (
@@ -35,3 +35,5 @@ export const FilePreviewInfoPanel: FunctionComponent = ({ file }) => {
) } + +export default FilePreviewInfoPanel diff --git a/app/assets/javascripts/Components/Files/FilePreviewModal.tsx b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx index b9795bd6a..503f9d76d 100644 --- a/app/assets/javascripts/Components/Files/FilePreviewModal.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx @@ -3,14 +3,13 @@ import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays' import { DialogContent, DialogOverlay } from '@reach/dialog' import { addToast, ToastType } from '@standardnotes/stylekit' import { NoPreviewIllustration } from '@standardnotes/icons' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' -import { getFileIconComponent } from '@/Components/AttachedFilesPopover/PopoverFileItem' -import { Button } from '@/Components/Button/Button' -import { Icon } from '@/Components/Icon/Icon' -import { FilePreviewInfoPanel } from './FilePreviewInfoPanel' +import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { getFileIconComponent } from '@/Components/AttachedFilesPopover/getFileIconComponent' +import Button from '@/Components/Button/Button' +import Icon from '@/Components/Icon/Icon' +import FilePreviewInfoPanel from './FilePreviewInfoPanel' import { isFileTypePreviewable } from './isFilePreviewable' -import { PreviewComponent } from './PreviewComponent' +import PreviewComponent from './PreviewComponent' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' import { KeyboardKey } from '@/Services/IOService' import { AppState } from '@/UIModels/AppState' @@ -21,10 +20,6 @@ type Props = { appState: AppState } -export const FilePreviewModalWrapper: FunctionComponent = observer(({ application, appState }) => { - return appState.filePreviewModal.isOpen ? : null -}) - const FilePreviewModal: FunctionComponent = observer(({ application, appState }) => { const { currentFile, setCurrentFile, otherFiles, dismiss } = appState.filePreviewModal @@ -91,8 +86,8 @@ const FilePreviewModal: FunctionComponent = observer(({ application, appS } }, [currentFile, getObjectUrl, objectUrl]) - const keyDownHandler = useCallback( - (event: KeyboardEvent) => { + const keyDownHandler: KeyboardEventHandler = useCallback( + (event) => { if (event.key !== KeyboardKey.Left && event.key !== KeyboardKey.Right) { return } @@ -141,6 +136,7 @@ const FilePreviewModal: FunctionComponent = observer(({ application, appS dangerouslyBypassScrollLock > = observer(({ application, appS ) }) + +FilePreviewModal.displayName = 'FilePreviewModal' + +const FilePreviewModalWrapper: FunctionComponent = ({ application, appState }) => { + return appState.filePreviewModal.isOpen ? : null +} + +export default observer(FilePreviewModalWrapper) diff --git a/app/assets/javascripts/Components/Files/ImagePreview.tsx b/app/assets/javascripts/Components/Files/ImagePreview.tsx index a07d75d29..6d4e4fb82 100644 --- a/app/assets/javascripts/Components/Files/ImagePreview.tsx +++ b/app/assets/javascripts/Components/Files/ImagePreview.tsx @@ -1,13 +1,12 @@ import { IconType } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { useRef, useState } from 'preact/hooks' -import { IconButton } from '../Button/IconButton' +import { FunctionComponent, useRef, useState } from 'react' +import IconButton from '../Button/IconButton' type Props = { objectUrl: string } -export const ImagePreview: FunctionComponent = ({ objectUrl }) => { +const ImagePreview: FunctionComponent = ({ objectUrl }) => { const initialImgHeightRef = useRef() const [imageZoomPercent, setImageZoomPercent] = useState(100) @@ -21,8 +20,8 @@ export const ImagePreview: FunctionComponent = ({ objectUrl }) => { height: `${imageZoomPercent}%`, ...(imageZoomPercent <= 100 ? { - 'min-width': '100%', - 'object-fit': 'contain', + minWidth: '100%', + objectFit: 'contain', } : { position: 'absolute', @@ -69,3 +68,5 @@ export const ImagePreview: FunctionComponent = ({ objectUrl }) => {
) } + +export default ImagePreview diff --git a/app/assets/javascripts/Components/Files/PreviewComponent.tsx b/app/assets/javascripts/Components/Files/PreviewComponent.tsx index 4ab0e352e..b0729bbd0 100644 --- a/app/assets/javascripts/Components/Files/PreviewComponent.tsx +++ b/app/assets/javascripts/Components/Files/PreviewComponent.tsx @@ -1,13 +1,13 @@ import { FileItem } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { ImagePreview } from './ImagePreview' +import { FunctionComponent } from 'react' +import ImagePreview from './ImagePreview' type Props = { file: FileItem objectUrl: string } -export const PreviewComponent: FunctionComponent = ({ file, objectUrl }) => { +const PreviewComponent: FunctionComponent = ({ file, objectUrl }) => { if (file.mimeType.startsWith('image/')) { return } @@ -22,3 +22,5 @@ export const PreviewComponent: FunctionComponent = ({ file, objectUrl }) return } + +export default PreviewComponent diff --git a/app/assets/javascripts/Components/Footer/Footer.tsx b/app/assets/javascripts/Components/Footer/Footer.tsx index cc3a80f6b..71403bfa4 100644 --- a/app/assets/javascripts/Components/Footer/Footer.tsx +++ b/app/assets/javascripts/Components/Footer/Footer.tsx @@ -11,12 +11,12 @@ import { STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON, } from '@/Strings' import { alertDialog, confirmDialog } from '@/Services/AlertService' -import { AccountMenu } from '@/Components/AccountMenu/AccountMenu' +import AccountMenu from '@/Components/AccountMenu/AccountMenu' import { AppStateEvent, EventSource } from '@/UIModels/AppState' -import { Icon } from '@/Components/Icon/Icon' -import { QuickSettingsMenu } from '@/Components/QuickSettingsMenu/QuickSettingsMenu' -import { SyncResolutionMenu } from '@/Components/SyncResolutionMenu/SyncResolutionMenu' -import { Fragment } from 'preact' +import Icon from '@/Components/Icon/Icon' +import QuickSettingsMenu from '@/Components/QuickSettingsMenu/QuickSettingsMenu' +import SyncResolutionMenu from '@/Components/SyncResolutionMenu/SyncResolutionMenu' +import { Fragment } from 'react' import { AccountMenuPane } from '../AccountMenu/AccountMenuPane' type Props = { @@ -39,7 +39,7 @@ type State = { arbitraryStatusMessage?: string } -export class Footer extends PureComponent { +class Footer extends PureComponent { public user?: unknown private didCheckForOffline = false private completedInitialSync = false @@ -455,3 +455,5 @@ export class Footer extends PureComponent { ) } } + +export default Footer diff --git a/app/assets/javascripts/Components/Icon/Icon.tsx b/app/assets/javascripts/Components/Icon/Icon.tsx index cb3593814..c9a0ce930 100644 --- a/app/assets/javascripts/Components/Icon/Icon.tsx +++ b/app/assets/javascripts/Components/Icon/Icon.tsx @@ -1,4 +1,4 @@ -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' import { IconType } from '@standardnotes/snjs' import { @@ -187,7 +187,7 @@ type Props = { ariaLabel?: string } -export const Icon: FunctionalComponent = ({ type, className = '', ariaLabel }) => { +const Icon: FunctionComponent = ({ type, className = '', ariaLabel }) => { const IconComponent = ICONS[type as keyof typeof ICONS] if (!IconComponent) { return null @@ -200,3 +200,5 @@ export const Icon: FunctionalComponent = ({ type, className = '', ariaLab /> ) } + +export default Icon diff --git a/app/assets/javascripts/Components/Input/DecoratedInput.tsx b/app/assets/javascripts/Components/Input/DecoratedInput.tsx index 6c738d418..21919ed64 100644 --- a/app/assets/javascripts/Components/Input/DecoratedInput.tsx +++ b/app/assets/javascripts/Components/Input/DecoratedInput.tsx @@ -1,5 +1,4 @@ -import { FunctionalComponent, Ref } from 'preact' -import { forwardRef } from 'preact/compat' +import { forwardRef, Fragment, Ref } from 'react' import { DecoratedInputProps } from './DecoratedInputProps' const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean) => { @@ -17,7 +16,7 @@ const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean /** * Input that can be decorated on the left and right side */ -export const DecoratedInput: FunctionalComponent = forwardRef( +const DecoratedInput = forwardRef( ( { type = 'text', @@ -42,8 +41,8 @@ export const DecoratedInput: FunctionalComponent = forwardR
{left && (
- {left.map((leftChild) => ( - <>{leftChild} + {left.map((leftChild, index) => ( + {leftChild} ))}
)} @@ -58,14 +57,16 @@ export const DecoratedInput: FunctionalComponent = forwardR onFocus={onFocus} onKeyDown={onKeyDown} data-lpignore={type !== 'password' ? true : false} - autocomplete={autocomplete ? 'on' : 'off'} + autoComplete={autocomplete ? 'on' : 'off'} ref={ref} /> {right && (
{right.map((rightChild, index) => ( -
0 ? 'ml-3' : ''}>{rightChild}
+
0 ? 'ml-3' : ''} key={index}> + {rightChild} +
))}
)} @@ -73,3 +74,5 @@ export const DecoratedInput: FunctionalComponent = forwardR ) }, ) + +export default DecoratedInput diff --git a/app/assets/javascripts/Components/Input/DecoratedInputProps.ts b/app/assets/javascripts/Components/Input/DecoratedInputProps.ts index 6499f000e..05272efdb 100644 --- a/app/assets/javascripts/Components/Input/DecoratedInputProps.ts +++ b/app/assets/javascripts/Components/Input/DecoratedInputProps.ts @@ -1,15 +1,15 @@ -import { ComponentChild } from 'preact' +import { FocusEventHandler, KeyboardEventHandler, ReactNode } from 'react' export type DecoratedInputProps = { type?: 'text' | 'email' | 'password' className?: string disabled?: boolean - left?: ComponentChild[] - right?: ComponentChild[] + left?: ReactNode[] + right?: ReactNode[] value?: string placeholder?: string onChange?: (text: string) => void - onFocus?: (event: FocusEvent) => void - onKeyDown?: (event: KeyboardEvent) => void + onFocus?: FocusEventHandler + onKeyDown?: KeyboardEventHandler autocomplete?: boolean } diff --git a/app/assets/javascripts/Components/Input/DecoratedPasswordInput.tsx b/app/assets/javascripts/Components/Input/DecoratedPasswordInput.tsx index 7c1db6294..31ddd09c0 100644 --- a/app/assets/javascripts/Components/Input/DecoratedPasswordInput.tsx +++ b/app/assets/javascripts/Components/Input/DecoratedPasswordInput.tsx @@ -1,13 +1,11 @@ -import { FunctionComponent, Ref } from 'preact' -import { forwardRef } from 'preact/compat' -import { StateUpdater, useState } from 'preact/hooks' -import { DecoratedInput } from './DecoratedInput' -import { IconButton } from '@/Components/Button/IconButton' +import { Dispatch, FunctionComponent, Ref, SetStateAction, forwardRef, useState } from 'react' +import DecoratedInput from './DecoratedInput' +import IconButton from '@/Components/Button/IconButton' import { DecoratedInputProps } from './DecoratedInputProps' const Toggle: FunctionComponent<{ isToggled: boolean - setIsToggled: StateUpdater + setIsToggled: Dispatch> }> = ({ isToggled, setIsToggled }) => ( > = forwardRef( - (props, ref: Ref) => { - const [isToggled, setIsToggled] = useState(false) +const DecoratedPasswordInput = forwardRef((props: DecoratedInputProps, ref: Ref) => { + const [isToggled, setIsToggled] = useState(false) - const rightSideDecorations = props.right ? [...props.right] : [] + const rightSideDecorations = props.right ? [...props.right] : [] - return ( - ]} - /> - ) - }, -) + return ( + ]} + /> + ) +}) + +export default DecoratedPasswordInput diff --git a/app/assets/javascripts/Components/Input/FloatingLabelInput.tsx b/app/assets/javascripts/Components/Input/FloatingLabelInput.tsx index d554f80ef..049ac9f04 100644 --- a/app/assets/javascripts/Components/Input/FloatingLabelInput.tsx +++ b/app/assets/javascripts/Components/Input/FloatingLabelInput.tsx @@ -1,14 +1,11 @@ -import { FunctionComponent, Ref } from 'preact' -import { JSXInternal } from 'preact/src/jsx' -import { forwardRef } from 'preact/compat' -import { useState } from 'preact/hooks' +import { ChangeEventHandler, Ref, forwardRef, useState } from 'react' type Props = { id: string type: 'text' | 'email' | 'password' label: string value: string - onChange: JSXInternal.GenericEventHandler + onChange: ChangeEventHandler disabled?: boolean className?: string labelClassName?: string @@ -16,7 +13,7 @@ type Props = { isInvalid?: boolean } -export const FloatingLabelInput: FunctionComponent = forwardRef( +const FloatingLabelInput = forwardRef( ( { id, @@ -71,3 +68,5 @@ export const FloatingLabelInput: FunctionComponent = forwardRef( ) }, ) + +export default FloatingLabelInput diff --git a/app/assets/javascripts/Components/Input/Input.tsx b/app/assets/javascripts/Components/Input/Input.tsx index a5fc45476..8edf7a4fa 100644 --- a/app/assets/javascripts/Components/Input/Input.tsx +++ b/app/assets/javascripts/Components/Input/Input.tsx @@ -1,4 +1,4 @@ -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' interface Props { text?: string @@ -6,9 +6,11 @@ interface Props { className?: string } -export const Input: FunctionalComponent = ({ className = '', disabled = false, text }) => { +const Input: FunctionComponent = ({ className = '', disabled = false, text }) => { const base = 'rounded py-1.5 px-3 text-input my-1 h-8 bg-contrast' const stateClasses = disabled ? 'no-border' : 'border-solid border-1 border-main' const classes = `${base} ${stateClasses} ${className}` return } + +export default Input diff --git a/app/assets/javascripts/Components/Menu/Menu.tsx b/app/assets/javascripts/Components/Menu/Menu.tsx index 74410c2f9..61b714f32 100644 --- a/app/assets/javascripts/Components/Menu/Menu.tsx +++ b/app/assets/javascripts/Components/Menu/Menu.tsx @@ -1,21 +1,26 @@ -import { JSX, FunctionComponent, ComponentChildren, VNode, RefCallback, ComponentChild, toChildArray } from 'preact' -import { useCallback, useEffect, useRef } from 'preact/hooks' -import { JSXInternal } from 'preact/src/jsx' -import { MenuItem, MenuItemListElement } from './MenuItem' +import { + CSSProperties, + FunctionComponent, + KeyboardEventHandler, + ReactNode, + useCallback, + useEffect, + useRef, +} from 'react' import { KeyboardKey } from '@/Services/IOService' import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation' type MenuProps = { className?: string - style?: string | JSX.CSSProperties | undefined + style?: CSSProperties | undefined a11yLabel: string - children: ComponentChildren + children: ReactNode closeMenu?: () => void isOpen: boolean initialFocus?: number } -export const Menu: FunctionComponent = ({ +const Menu: FunctionComponent = ({ children, className = '', style, @@ -24,16 +29,10 @@ export const Menu: FunctionComponent = ({ isOpen, initialFocus, }: MenuProps) => { - const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]) - const menuElementRef = useRef(null) - const handleKeyDown: JSXInternal.KeyboardEventHandler = useCallback( + const handleKeyDown: KeyboardEventHandler = useCallback( (event) => { - if (!menuItemRefs.current) { - return - } - if (event.key === KeyboardKey.Escape) { closeMenu?.() return @@ -45,58 +44,13 @@ export const Menu: FunctionComponent = ({ useListKeyboardNavigation(menuElementRef, initialFocus) useEffect(() => { - if (isOpen && menuItemRefs.current.length > 0) { + if (isOpen) { setTimeout(() => { menuElementRef.current?.focus() }) } }, [isOpen]) - const pushRefToArray: RefCallback = useCallback((instance) => { - if (instance && instance.children) { - Array.from(instance.children).forEach((child) => { - if ( - child.getAttribute('role')?.includes('menuitem') && - !menuItemRefs.current.includes(child as HTMLButtonElement) - ) { - menuItemRefs.current.push(child as HTMLButtonElement) - } - }) - } - }, []) - - 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) - } - - const _child = child as VNode - const isFirstMenuItem = index === array.findIndex((child) => (child as VNode).type === MenuItem) - - const hasMultipleItems = Array.isArray(_child.props.children) - ? Array.from(_child.props.children as ComponentChild[]).some( - (child) => (child as VNode).type === MenuItem, - ) - : false - - const items = hasMultipleItems ? [...(_child.props.children as ComponentChild[])] : [_child] - - return items.map((child) => { - return ( - - {child} - - ) - }) - }, - [pushRefToArray], - ) - return ( = ({ style={style} aria-label={a11yLabel} > - {toChildArray(children).map(mapMenuItems)} + {children} ) } + +export default Menu diff --git a/app/assets/javascripts/Components/Menu/MenuItem.tsx b/app/assets/javascripts/Components/Menu/MenuItem.tsx index aa2be8a30..afdae1e3a 100644 --- a/app/assets/javascripts/Components/Menu/MenuItem.tsx +++ b/app/assets/javascripts/Components/Menu/MenuItem.tsx @@ -1,21 +1,15 @@ -import { ComponentChildren, FunctionComponent, VNode } from 'preact' -import { forwardRef, Ref } from 'preact/compat' -import { JSXInternal } from 'preact/src/jsx' -import { Icon } from '@/Components/Icon/Icon' -import { Switch, SwitchProps } from '@/Components/Switch/Switch' +import { forwardRef, MouseEventHandler, ReactNode, Ref } from 'react' +import Icon from '@/Components/Icon/Icon' +import Switch from '@/Components/Switch/Switch' +import { SwitchProps } from '@/Components/Switch/SwitchProps' import { IconType } from '@standardnotes/snjs' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' - -export enum MenuItemType { - IconButton, - RadioButton, - SwitchButton, -} +import { MenuItemType } from './MenuItemType' type MenuItemProps = { type: MenuItemType - children: ComponentChildren - onClick?: JSXInternal.MouseEventHandler + children: ReactNode + onClick?: MouseEventHandler onChange?: SwitchProps['onChange'] onBlur?: (event: { relatedTarget: EventTarget | null }) => void className?: string @@ -25,7 +19,7 @@ type MenuItemProps = { tabIndex?: number } -export const MenuItem: FunctionComponent = forwardRef( +const MenuItem = forwardRef( ( { children, @@ -42,63 +36,42 @@ export const MenuItem: FunctionComponent = forwardRef( ref: Ref, ) => { return type === MenuItemType.SwitchButton && typeof onChange === 'function' ? ( - +
  • + +
  • ) : ( - - ) - }, -) - -export const MenuItemSeparator: FunctionComponent = () =>
    - -type ListElementProps = { - isFirstMenuItem: boolean - children: ComponentChildren -} - -export const MenuItemListElement: FunctionComponent = forwardRef( - ({ children, isFirstMenuItem }: ListElementProps, ref: Ref) => { - const child = children as VNode - return ( -
  • - {{ - ...child, - props: { - ...(child.props ? { ...child.props } : {}), - ...(child.type === MenuItem - ? { - tabIndex: isFirstMenuItem ? 0 : -1, - } - : {}), - }, - }} +
  • +
  • ) }, ) + +export default MenuItem diff --git a/app/assets/javascripts/Components/Menu/MenuItemSeparator.tsx b/app/assets/javascripts/Components/Menu/MenuItemSeparator.tsx new file mode 100644 index 000000000..00a6d97b3 --- /dev/null +++ b/app/assets/javascripts/Components/Menu/MenuItemSeparator.tsx @@ -0,0 +1,9 @@ +import { FunctionComponent } from 'react' + +const MenuItemSeparator: FunctionComponent = () => ( +
  • +
    +
  • +) + +export default MenuItemSeparator diff --git a/app/assets/javascripts/Components/Menu/MenuItemType.ts b/app/assets/javascripts/Components/Menu/MenuItemType.ts new file mode 100644 index 000000000..cfeb9eb53 --- /dev/null +++ b/app/assets/javascripts/Components/Menu/MenuItemType.ts @@ -0,0 +1,5 @@ +export enum MenuItemType { + IconButton, + RadioButton, + SwitchButton, +} diff --git a/app/assets/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx b/app/assets/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx index b22577124..cc8bb217b 100644 --- a/app/assets/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx +++ b/app/assets/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx @@ -1,18 +1,18 @@ import { AppState } from '@/UIModels/AppState' import { IlNotesIcon } from '@standardnotes/icons' import { observer } from 'mobx-react-lite' -import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel' +import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel' import { WebApplication } from '@/UIModels/Application' -import { PinNoteButton } from '@/Components/PinNoteButton/PinNoteButton' -import { Button } from '../Button/Button' -import { useCallback } from 'preact/hooks' +import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton' +import Button from '../Button/Button' +import { useCallback } from 'react' type Props = { application: WebApplication appState: AppState } -export const MultipleSelectedNotes = observer(({ application, appState }: Props) => { +const MultipleSelectedNotes = ({ application, appState }: Props) => { const count = appState.notes.selectedNotesCount const cancelMultipleSelection = useCallback(() => { @@ -40,4 +40,6 @@ export const MultipleSelectedNotes = observer(({ application, appState }: Props)
    ) -}) +} + +export default observer(MultipleSelectedNotes) diff --git a/app/assets/javascripts/Components/Navigation/Navigation.tsx b/app/assets/javascripts/Components/Navigation/Navigation.tsx index 648a9890f..f88ef3ff5 100644 --- a/app/assets/javascripts/Components/Navigation/Navigation.tsx +++ b/app/assets/javascripts/Components/Navigation/Navigation.tsx @@ -1,18 +1,17 @@ -import { SmartViewsSection } from '@/Components/Tags/SmartViewsSection' -import { TagsSection } from '@/Components/Tags/TagsSection' +import SmartViewsSection from '@/Components/Tags/SmartViewsSection' +import TagsSection from '@/Components/Tags/TagsSection' import { WebApplication } from '@/UIModels/Application' import { PANEL_NAME_NAVIGATION } from '@/Constants' import { ApplicationEvent, PrefKey } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useMemo, useState } from 'preact/hooks' -import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' +import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react' +import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' type Props = { application: WebApplication } -export const Navigation: FunctionComponent = observer(({ application }) => { +const Navigation: FunctionComponent = ({ application }) => { const appState = useMemo(() => application.getAppState(), [application]) const [ref, setRef] = useState() const [panelWidth, setPanelWidth] = useState(0) @@ -79,4 +78,6 @@ export const Navigation: FunctionComponent = observer(({ application }) = )} ) -}) +} + +export default observer(Navigation) diff --git a/app/assets/javascripts/Components/NoAccountWarning/NoAccountWarning.tsx b/app/assets/javascripts/Components/NoAccountWarning/NoAccountWarning.tsx index 52b30c563..2792fc803 100644 --- a/app/assets/javascripts/Components/NoAccountWarning/NoAccountWarning.tsx +++ b/app/assets/javascripts/Components/NoAccountWarning/NoAccountWarning.tsx @@ -1,18 +1,13 @@ -import { Icon } from '@/Components/Icon/Icon' +import Icon from '@/Components/Icon/Icon' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { useCallback } from 'preact/hooks' +import { MouseEventHandler, useCallback } from 'react' type Props = { appState: AppState } -export const NoAccountWarning = observer(({ appState }: Props) => { - const canShow = appState.noAccountWarning.show - if (!canShow) { - return null - } - - const showAccountMenu = useCallback( - (event: Event) => { +const NoAccountWarning = observer(({ appState }: Props) => { + const showAccountMenu: MouseEventHandler = useCallback( + (event) => { event.stopPropagation() appState.accountMenu.setShow(true) }, @@ -32,9 +27,9 @@ export const NoAccountWarning = observer(({ appState }: Props) => { ) -}) +} + +export default observer(NoteTag) diff --git a/app/assets/javascripts/Components/NoteTags/NoteTagsContainer.tsx b/app/assets/javascripts/Components/NoteTags/NoteTagsContainer.tsx index 3ce97c92c..c3f392b65 100644 --- a/app/assets/javascripts/Components/NoteTags/NoteTagsContainer.tsx +++ b/app/assets/javascripts/Components/NoteTags/NoteTagsContainer.tsx @@ -1,19 +1,14 @@ import { AppState } from '@/UIModels/AppState' 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' +import AutocompleteTagInput from '@/Components/TagAutocomplete/AutocompleteTagInput' +import NoteTag from './NoteTag' +import { useEffect } from 'react' type Props = { appState: AppState } -export const NoteTagsContainer = observer(({ appState }: Props) => { - if (isStateDealloced(appState)) { - return null - } - +const NoteTagsContainer = ({ appState }: Props) => { const { tags, tagsContainerMaxWidth } = appState.noteTags useEffect(() => { @@ -33,4 +28,6 @@ export const NoteTagsContainer = observer(({ appState }: Props) => { ) -}) +} + +export default observer(NoteTagsContainer) diff --git a/app/assets/javascripts/Components/NoteView/EditingDisabledBanner.tsx b/app/assets/javascripts/Components/NoteView/EditingDisabledBanner.tsx index 3584b3218..bb47c4842 100644 --- a/app/assets/javascripts/Components/NoteView/EditingDisabledBanner.tsx +++ b/app/assets/javascripts/Components/NoteView/EditingDisabledBanner.tsx @@ -1,5 +1,5 @@ -import { FunctionComponent } from 'preact' -import { Icon } from '../Icon/Icon' +import { FunctionComponent } from 'react' +import Icon from '../Icon/Icon' type Props = { onMouseLeave: () => void @@ -9,7 +9,7 @@ type Props = { lockText: string } -export const EditingDisabledBanner: FunctionComponent = ({ +const EditingDisabledBanner: FunctionComponent = ({ onMouseLeave, onMouseOver, onClick, @@ -36,3 +36,5 @@ export const EditingDisabledBanner: FunctionComponent = ({ ) } + +export default EditingDisabledBanner diff --git a/app/assets/javascripts/Components/NoteView/NoteView.test.ts b/app/assets/javascripts/Components/NoteView/NoteView.test.ts index 2e6317cce..0ec6d0a1e 100644 --- a/app/assets/javascripts/Components/NoteView/NoteView.test.ts +++ b/app/assets/javascripts/Components/NoteView/NoteView.test.ts @@ -12,7 +12,7 @@ import { SNNote, } from '@standardnotes/snjs' -import { NoteView } from './NoteView' +import NoteView from './NoteView' describe('NoteView', () => { let noteViewController: NoteViewController diff --git a/app/assets/javascripts/Components/NoteView/NoteView.tsx b/app/assets/javascripts/Components/NoteView/NoteView.tsx index 7e44b11be..2be437fe7 100644 --- a/app/assets/javascripts/Components/NoteView/NoteView.tsx +++ b/app/assets/javascripts/Components/NoteView/NoteView.tsx @@ -1,4 +1,4 @@ -import { createRef, JSX, RefObject } from 'preact' +import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react' import { ApplicationEvent, isPayloadSourceRetrieved, @@ -19,16 +19,16 @@ import { KeyboardModifier, KeyboardKey } from '@/Services/IOService' import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Strings' import { confirmDialog } from '@/Services/AlertService' import { PureComponent } from '@/Components/Abstract/PureComponent' -import { ProtectedNoteOverlay } from '@/Components/ProtectedNoteOverlay/ProtectedNoteOverlay' -import { PinNoteButton } from '@/Components/PinNoteButton/PinNoteButton' -import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel' -import { NoteTagsContainer } from '@/Components/NoteTags/NoteTagsContainer' -import { ComponentView } from '@/Components/ComponentView/ComponentView' -import { PanelSide, PanelResizer, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' +import ProtectedNoteOverlay from '@/Components/ProtectedNoteOverlay/ProtectedNoteOverlay' +import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton' +import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel' +import NoteTagsContainer from '@/Components/NoteTags/NoteTagsContainer' +import ComponentView from '@/Components/ComponentView/ComponentView' +import PanelResizer, { PanelSide, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' import { ElementIds } from '@/ElementIDs' -import { ChangeEditorButton } from '@/Components/ChangeEditor/ChangeEditorButton' -import { AttachedFilesButton } from '@/Components/AttachedFilesPopover/AttachedFilesButton' -import { EditingDisabledBanner } from './EditingDisabledBanner' +import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton' +import AttachedFilesButton from '@/Components/AttachedFilesPopover/AttachedFilesButton' +import EditingDisabledBanner from './EditingDisabledBanner' import { transactionForAssociateComponentWithCurrentNote, transactionForDisassociateComponentWithCurrentNote, @@ -78,7 +78,7 @@ type State = { rightResizerOffset: number } -export class NoteView extends PureComponent { +class NoteView extends PureComponent { readonly controller!: NoteViewController private statusTimeout?: NodeJS.Timeout @@ -528,7 +528,7 @@ export class NoteView extends PureComponent { } } - onTextAreaChange = ({ currentTarget }: JSX.TargetedEvent) => { + onTextAreaChange: ChangeEventHandler = ({ currentTarget }) => { const text = currentTarget.value this.setState({ editorText: text, @@ -548,12 +548,16 @@ export class NoteView extends PureComponent { .catch(console.error) } - onTitleEnter = ({ currentTarget }: JSX.TargetedEvent) => { + onTitleEnter: KeyboardEventHandler = ({ key, currentTarget }) => { + if (key !== KeyboardKey.Enter) { + return + } + currentTarget.blur() this.focusEditor() } - onTitleChange = ({ currentTarget }: JSX.TargetedEvent) => { + onTitleChange: ChangeEventHandler = ({ currentTarget }) => { const title = currentTarget.value this.setState({ editorTitle: title, @@ -911,12 +915,12 @@ export class NoteView extends PureComponent { id={ElementIds.NoteTitleEditor} onChange={this.onTitleChange} onFocus={(event) => { - ;(event.target as HTMLTextAreaElement).select() + event.target.select() }} - onKeyUp={(event) => event.keyCode == 13 && this.onTitleEnter(event)} - spellcheck={false} + onKeyUp={this.onTitleEnter} + spellCheck={false} value={this.state.editorTitle} - autocomplete="off" + autoComplete="off" /> @@ -996,15 +1000,15 @@ export class NoteView extends PureComponent { {this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading && ( )} @@ -1059,7 +1063,7 @@ export class NoteView extends PureComponent {
    {this.state.stackComponentViewers.map((viewer) => { return ( -
    +
    { ) } } + +export default NoteView diff --git a/app/assets/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx b/app/assets/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx index d3968d2ab..33f0a1be6 100644 --- a/app/assets/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx +++ b/app/assets/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx @@ -2,8 +2,8 @@ import { AppState } from '@/UIModels/AppState' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' import { observer } from 'mobx-react-lite' -import { NotesOptions } from '@/Components/NotesOptions/NotesOptions' -import { useCallback, useEffect, useRef } from 'preact/hooks' +import NotesOptions from '@/Components/NotesOptions/NotesOptions' +import { useCallback, useEffect, useRef } from 'react' import { WebApplication } from '@/UIModels/Application' type Props = { @@ -11,7 +11,7 @@ type Props = { appState: AppState } -export const NotesContextMenu = observer(({ application, appState }: Props) => { +const NotesContextMenu = ({ application, appState }: Props) => { const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = appState.notes const contextMenuRef = useRef(null) @@ -42,4 +42,6 @@ export const NotesContextMenu = observer(({ application, appState }: Props) => {
    ) : null -}) +} + +export default observer(NotesContextMenu) diff --git a/app/assets/javascripts/Components/NotesOptions/AccordionMenuGroup.tsx b/app/assets/javascripts/Components/NotesOptions/AccordionMenuGroup.tsx new file mode 100644 index 000000000..afd41da6d --- /dev/null +++ b/app/assets/javascripts/Components/NotesOptions/AccordionMenuGroup.tsx @@ -0,0 +1,8 @@ +import { IconType } from '@standardnotes/snjs' + +export type AccordionMenuGroup = { + icon?: IconType + iconClassName?: string + title: string + items: Array +} diff --git a/app/assets/javascripts/Components/NotesOptions/AddTagOption.tsx b/app/assets/javascripts/Components/NotesOptions/AddTagOption.tsx index f409eb62a..1eb63d3f7 100644 --- a/app/assets/javascripts/Components/NotesOptions/AddTagOption.tsx +++ b/app/assets/javascripts/Components/NotesOptions/AddTagOption.tsx @@ -2,16 +2,15 @@ import { AppState } from '@/UIModels/AppState' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' type Props = { appState: AppState } -export const AddTagOption: FunctionComponent = observer(({ appState }) => { +const AddTagOption: FunctionComponent = ({ appState }) => { const menuContainerRef = useRef(null) const menuRef = useRef(null) const menuButtonRef = useRef(null) @@ -87,7 +86,7 @@ export const AddTagOption: FunctionComponent = observer(({ appState }) => > {appState.tags.tags.map((tag) => (
    ) -}) +} + +export default observer(AddTagOption) diff --git a/app/assets/javascripts/Components/NotesOptions/ChangeEditorOption.tsx b/app/assets/javascripts/Components/NotesOptions/ChangeEditorOption.tsx index 344661488..4d6ab456e 100644 --- a/app/assets/javascripts/Components/NotesOptions/ChangeEditorOption.tsx +++ b/app/assets/javascripts/Components/NotesOptions/ChangeEditorOption.tsx @@ -2,11 +2,10 @@ import { KeyboardKey } from '@/Services/IOService' import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' -import { IconType, SNComponent, SNNote } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' -import { ChangeEditorMenu } from '@/Components/ChangeEditor/ChangeEditorMenu' +import { SNNote } from '@standardnotes/snjs' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' +import ChangeEditorMenu from '@/Components/ChangeEditor/ChangeEditorMenu' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' @@ -16,22 +15,7 @@ type ChangeEditorOptionProps = { note: SNNote } -type AccordionMenuGroup = { - icon?: IconType - iconClassName?: string - title: string - items: Array -} - -export type EditorMenuItem = { - name: string - component?: SNComponent - isEntitled: boolean -} - -export type EditorMenuGroup = AccordionMenuGroup - -export const ChangeEditorOption: FunctionComponent = ({ application, note }) => { +const ChangeEditorOption: FunctionComponent = ({ application, note }) => { const [isOpen, setIsOpen] = useState(false) const [isVisible, setIsVisible] = useState(false) const [menuStyle, setMenuStyle] = useState({ @@ -121,3 +105,5 @@ export const ChangeEditorOption: FunctionComponent = ({
    ) } + +export default ChangeEditorOption diff --git a/app/assets/javascripts/Components/NotesOptions/EditorMenuGroup.tsx b/app/assets/javascripts/Components/NotesOptions/EditorMenuGroup.tsx new file mode 100644 index 000000000..cf541458b --- /dev/null +++ b/app/assets/javascripts/Components/NotesOptions/EditorMenuGroup.tsx @@ -0,0 +1,4 @@ +import { EditorMenuItem } from './EditorMenuItem' +import { AccordionMenuGroup } from './AccordionMenuGroup' + +export type EditorMenuGroup = AccordionMenuGroup diff --git a/app/assets/javascripts/Components/NotesOptions/EditorMenuItem.tsx b/app/assets/javascripts/Components/NotesOptions/EditorMenuItem.tsx new file mode 100644 index 000000000..d39a2850d --- /dev/null +++ b/app/assets/javascripts/Components/NotesOptions/EditorMenuItem.tsx @@ -0,0 +1,7 @@ +import { SNComponent } from '@standardnotes/snjs' + +export type EditorMenuItem = { + name: string + component?: SNComponent + isEntitled: boolean +} diff --git a/app/assets/javascripts/Components/NotesOptions/ListedActionsOption.tsx b/app/assets/javascripts/Components/NotesOptions/ListedActionsOption.tsx index 176bd9d4e..985bebb3c 100644 --- a/app/assets/javascripts/Components/NotesOptions/ListedActionsOption.tsx +++ b/app/assets/javascripts/Components/NotesOptions/ListedActionsOption.tsx @@ -2,9 +2,8 @@ import { WebApplication } from '@/UIModels/Application' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { Action, ListedAccount, SNNote } from '@standardnotes/snjs' -import { Fragment, FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' +import { Fragment, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' type Props = { @@ -206,7 +205,7 @@ const ListedActionsMenu: FunctionComponent = ({ applicat ) } -export const ListedActionsOption: FunctionComponent = ({ application, note }) => { +const ListedActionsOption: FunctionComponent = ({ application, note }) => { const menuContainerRef = useRef(null) const menuRef = useRef(null) const menuButtonRef = useRef(null) @@ -273,3 +272,5 @@ export const ListedActionsOption: FunctionComponent = ({ application, not ) } + +export default ListedActionsOption diff --git a/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx b/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx index 793f2d27d..0fbb9af06 100644 --- a/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -1,23 +1,16 @@ import { AppState } from '@/UIModels/AppState' -import { Icon } from '@/Components/Icon/Icon' -import { Switch } from '@/Components/Switch/Switch' +import Icon from '@/Components/Icon/Icon' +import Switch from '@/Components/Switch/Switch' import { observer } from 'mobx-react-lite' -import { useState, useEffect, useMemo, useCallback } from 'preact/hooks' +import { useState, useEffect, useMemo, useCallback, FunctionComponent } from 'react' import { SNApplication, SNNote } from '@standardnotes/snjs' -import { WebApplication } from '@/UIModels/Application' import { KeyboardModifier } from '@/Services/IOService' -import { FunctionComponent } from 'preact' -import { ChangeEditorOption } from './ChangeEditorOption' +import ChangeEditorOption from './ChangeEditorOption' import { BYTES_IN_ONE_MEGABYTE } from '@/Constants' -import { ListedActionsOption } from './ListedActionsOption' -import { AddTagOption } from './AddTagOption' +import ListedActionsOption from './ListedActionsOption' +import AddTagOption from './AddTagOption' import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit' - -export type NotesOptionsProps = { - application: WebApplication - appState: AppState - closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void -} +import { NotesOptionsProps } from './NotesOptionsProps' type DeletePermanentlyButtonProps = { closeOnBlur: NotesOptionsProps['closeOnBlur'] @@ -176,7 +169,7 @@ const NoteSizeWarning: FunctionComponent<{ ) : null } -export const NotesOptions = observer(({ application, appState, closeOnBlur }: NotesOptionsProps) => { +const NotesOptions = ({ application, appState, closeOnBlur }: NotesOptionsProps) => { const [altKeyDown, setAltKeyDown] = useState(false) const toggleOn = (condition: (note: SNNote) => boolean) => { @@ -440,4 +433,6 @@ export const NotesOptions = observer(({ application, appState, closeOnBlur }: No ) : null} ) -}) +} + +export default observer(NotesOptions) diff --git a/app/assets/javascripts/Components/NotesOptions/NotesOptionsPanel.tsx b/app/assets/javascripts/Components/NotesOptions/NotesOptionsPanel.tsx index 33c26269b..23b0a50bc 100644 --- a/app/assets/javascripts/Components/NotesOptions/NotesOptionsPanel.tsx +++ b/app/assets/javascripts/Components/NotesOptions/NotesOptionsPanel.tsx @@ -1,11 +1,11 @@ import { AppState } from '@/UIModels/AppState' -import { Icon } from '@/Components/Icon/Icon' +import Icon from '@/Components/Icon/Icon' import VisuallyHidden from '@reach/visually-hidden' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' -import { useRef, useState } from 'preact/hooks' +import { useRef, useState } from 'react' import { observer } from 'mobx-react-lite' -import { NotesOptions } from './NotesOptions' +import NotesOptions from './NotesOptions' import { WebApplication } from '@/UIModels/Application' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' @@ -15,7 +15,7 @@ type Props = { onClickPreprocessing?: () => Promise } -export const NotesOptionsPanel = observer(({ application, appState, onClickPreprocessing }: Props) => { +const NotesOptionsPanel = ({ application, appState, onClickPreprocessing }: Props) => { const [open, setOpen] = useState(false) const [position, setPosition] = useState({ top: 0, @@ -83,4 +83,6 @@ export const NotesOptionsPanel = observer(({ application, appState, onClickPrepr ) -}) +} + +export default observer(NotesOptionsPanel) diff --git a/app/assets/javascripts/Components/NotesOptions/NotesOptionsProps.ts b/app/assets/javascripts/Components/NotesOptions/NotesOptionsProps.ts new file mode 100644 index 000000000..2a969165c --- /dev/null +++ b/app/assets/javascripts/Components/NotesOptions/NotesOptionsProps.ts @@ -0,0 +1,8 @@ +import { WebApplication } from '@/UIModels/Application' +import { AppState } from '@/UIModels/AppState' + +export type NotesOptionsProps = { + application: WebApplication + appState: AppState + closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void +} diff --git a/app/assets/javascripts/Components/OtherSessionsSignOut/OtherSessionsSignOut.tsx b/app/assets/javascripts/Components/OtherSessionsSignOut/OtherSessionsSignOut.tsx index b5fa46311..c10ce1f16 100644 --- a/app/assets/javascripts/Components/OtherSessionsSignOut/OtherSessionsSignOut.tsx +++ b/app/assets/javascripts/Components/OtherSessionsSignOut/OtherSessionsSignOut.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'preact/hooks' +import { useCallback, useRef } from 'react' import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog' import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' @@ -9,13 +9,6 @@ type Props = { appState: AppState } -export const OtherSessionsSignOutContainer = observer((props: Props) => { - if (!props.appState.accountMenu.otherSessionsSignOut) { - return null - } - return -}) - const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props) => { const cancelRef = useRef(null) @@ -65,3 +58,14 @@ const ConfirmOtherSessionsSignOut = observer(({ application, appState }: Props) ) }) + +ConfirmOtherSessionsSignOut.displayName = 'ConfirmOtherSessionsSignOut' + +const OtherSessionsSignOutContainer = (props: Props) => { + if (!props.appState.accountMenu.otherSessionsSignOut) { + return null + } + return +} + +export default observer(OtherSessionsSignOutContainer) diff --git a/app/assets/javascripts/Components/PanelResizer/PanelResizer.tsx b/app/assets/javascripts/Components/PanelResizer/PanelResizer.tsx index d150bb6bd..6de7a49ab 100644 --- a/app/assets/javascripts/Components/PanelResizer/PanelResizer.tsx +++ b/app/assets/javascripts/Components/PanelResizer/PanelResizer.tsx @@ -1,4 +1,4 @@ -import { Component, createRef } from 'preact' +import { Component, createRef, MouseEventHandler } from 'react' import { debounce } from '@/Utils' export type ResizeFinishCallback = ( @@ -38,7 +38,7 @@ type State = { pressed: boolean } -export class PanelResizer extends Component { +class PanelResizer extends Component { private overlay?: HTMLDivElement private resizerElementRef = createRef() private debouncedResizeHandler: () => void @@ -76,6 +76,10 @@ export class PanelResizer extends Component { } } + override componentDidMount() { + this.resizerElementRef.current?.addEventListener('dblclick', this.onDblClick) + } + override componentDidUpdate(prevProps: Props) { if (this.props.width != prevProps.width) { this.setWidth(this.props.width) @@ -92,6 +96,7 @@ export class PanelResizer extends Component { } override componentWillUnmount() { + this.resizerElementRef.current?.removeEventListener('dblclick', this.onDblClick) document.removeEventListener('mouseup', this.onMouseUp) document.removeEventListener('mousemove', this.onMouseMove) window.removeEventListener('resize', this.debouncedResizeHandler) @@ -241,7 +246,7 @@ export class PanelResizer extends Component { this.finishSettingWidth() } - onMouseDown = (event: MouseEvent) => { + onMouseDown: MouseEventHandler = (event) => { this.addInvisibleOverlay() this.lastDownX = event.clientX this.startWidth = this.props.panel.scrollWidth @@ -299,16 +304,17 @@ export class PanelResizer extends Component { } } - render() { + override render() { return (
    ) } } + +export default PanelResizer diff --git a/app/assets/javascripts/Components/PasswordWizard/PasswordWizard.tsx b/app/assets/javascripts/Components/PasswordWizard/PasswordWizard.tsx index ad020ccaa..2cb0d1180 100644 --- a/app/assets/javascripts/Components/PasswordWizard/PasswordWizard.tsx +++ b/app/assets/javascripts/Components/PasswordWizard/PasswordWizard.tsx @@ -1,9 +1,10 @@ import { WebApplication } from '@/UIModels/Application' -import { createRef, JSX } from 'preact' +import { ChangeEventHandler, createRef } from 'react' import { PureComponent } from '@/Components/Abstract/PureComponent' interface Props { application: WebApplication + dismissModal: () => void } type State = { @@ -31,7 +32,7 @@ type FormData = { status?: string } -export class PasswordWizard extends PureComponent { +class PasswordWizard extends PureComponent { private currentPasswordInput = createRef() constructor(props: Props) { @@ -188,7 +189,7 @@ export class PasswordWizard extends PureComponent { if (this.state.lockContinue) { this.application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error) } else { - this.dismissModal() + this.props.dismissModal() } } @@ -201,19 +202,19 @@ export class PasswordWizard extends PureComponent { }) } - handleCurrentPasswordInputChange = ({ currentTarget }: JSX.TargetedEvent) => { + handleCurrentPasswordInputChange: ChangeEventHandler = ({ currentTarget }) => { this.setFormDataState({ currentPassword: currentTarget.value, }).catch(console.error) } - handleNewPasswordInputChange = ({ currentTarget }: JSX.TargetedEvent) => { + handleNewPasswordInputChange: ChangeEventHandler = ({ currentTarget }) => { this.setFormDataState({ newPassword: currentTarget.value, }).catch(console.error) } - handleNewPasswordConfirmationInputChange = ({ currentTarget }: JSX.TargetedEvent) => { + handleNewPasswordConfirmationInputChange: ChangeEventHandler = ({ currentTarget }) => { this.setFormDataState({ newPasswordConfirmation: currentTarget.value, }).catch(console.error) @@ -310,3 +311,5 @@ export class PasswordWizard extends PureComponent { ) } } + +export default PasswordWizard diff --git a/app/assets/javascripts/Components/PermissionsModal/PermissionsModal.tsx b/app/assets/javascripts/Components/PermissionsModal/PermissionsModal.tsx index c9f146c57..0682c8960 100644 --- a/app/assets/javascripts/Components/PermissionsModal/PermissionsModal.tsx +++ b/app/assets/javascripts/Components/PermissionsModal/PermissionsModal.tsx @@ -1,45 +1,27 @@ import { WebApplication } from '@/UIModels/Application' import { SNComponent } from '@standardnotes/snjs' -import { Component } from 'preact' -import { findDOMNode, unmountComponentAtNode } from 'preact/compat' +import { Component } from 'react' interface Props { application: WebApplication callback: (approved: boolean) => void + dismiss: () => void component: SNComponent permissionsString: string } -export class PermissionsModal extends Component { - getElement(): Element | null { - return findDOMNode(this) - } - - dismiss = () => { - const elem = this.getElement() - if (!elem) { - return - } - - const parent = elem.parentElement - if (!parent) { - return - } - parent.remove() - unmountComponentAtNode(parent) - } - +class PermissionsModal extends Component { accept = () => { this.props.callback(true) - this.dismiss() + this.props.dismiss() } deny = () => { this.props.callback(false) - this.dismiss() + this.props.dismiss() } - render() { + override render() { return (
    @@ -88,3 +70,5 @@ export class PermissionsModal extends Component { ) } } + +export default PermissionsModal diff --git a/app/assets/javascripts/Components/PermissionsModal/PermissionsModalWrapper.tsx b/app/assets/javascripts/Components/PermissionsModal/PermissionsModalWrapper.tsx new file mode 100644 index 000000000..308074fff --- /dev/null +++ b/app/assets/javascripts/Components/PermissionsModal/PermissionsModalWrapper.tsx @@ -0,0 +1,56 @@ +import { WebApplication } from '@/UIModels/Application' +import { ApplicationEvent, PermissionDialog } from '@standardnotes/snjs' +import { FunctionComponent, useCallback, useEffect, useState } from 'react' +import PermissionsModal from './PermissionsModal' + +type Props = { + application: WebApplication +} + +const PermissionsModalWrapper: FunctionComponent = ({ application }) => { + const [dialog, setDialog] = useState() + + const presentPermissionsDialog = useCallback((permissionDialog: PermissionDialog) => { + setDialog(permissionDialog) + }, []) + + const dismissPermissionsDialog = useCallback(() => { + setDialog(undefined) + }, []) + + const onAppStart = useCallback(() => { + application.componentManager.presentPermissionsDialog = presentPermissionsDialog + + return () => { + ;(application.componentManager.presentPermissionsDialog as unknown) = undefined + } + }, [application, presentPermissionsDialog]) + + useEffect(() => { + if (application.isStarted()) { + onAppStart() + } + + const removeAppObserver = application.addEventObserver(async (eventName) => { + if (eventName === ApplicationEvent.Started) { + onAppStart() + } + }) + + return () => { + removeAppObserver() + } + }, [application, onAppStart]) + + return dialog ? ( + + ) : null +} + +export default PermissionsModalWrapper diff --git a/app/assets/javascripts/Components/PinNoteButton/PinNoteButton.tsx b/app/assets/javascripts/Components/PinNoteButton/PinNoteButton.tsx index 75c5d9929..6738391a5 100644 --- a/app/assets/javascripts/Components/PinNoteButton/PinNoteButton.tsx +++ b/app/assets/javascripts/Components/PinNoteButton/PinNoteButton.tsx @@ -1,10 +1,8 @@ import { AppState } from '@/UIModels/AppState' import VisuallyHidden from '@reach/visually-hidden' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { Icon } from '@/Components/Icon/Icon' -import { useCallback } from 'preact/hooks' -import { isStateDealloced } from '@/UIModels/AppState/AbstractState' +import { FunctionComponent, useCallback } from 'react' +import Icon from '@/Components/Icon/Icon' type Props = { appState: AppState @@ -12,34 +10,27 @@ type Props = { onClickPreprocessing?: () => Promise } -export const PinNoteButton: FunctionComponent = observer( - ({ appState, className = '', onClickPreprocessing }: Props) => { - if (isStateDealloced(appState)) { - return null +const PinNoteButton: FunctionComponent = ({ appState, className = '', onClickPreprocessing }: Props) => { + const notes = appState.notes.selectedNotes + const pinned = notes.some((note) => note.pinned) + + const togglePinned = useCallback(async () => { + if (onClickPreprocessing) { + await onClickPreprocessing() } + if (!pinned) { + appState.notes.setPinSelectedNotes(true) + } else { + appState.notes.setPinSelectedNotes(false) + } + }, [appState, onClickPreprocessing, pinned]) - const notes = appState.notes.selectedNotes - const pinned = notes.some((note) => note.pinned) + return ( + + ) +} - const togglePinned = useCallback(async () => { - if (onClickPreprocessing) { - await onClickPreprocessing() - } - if (!pinned) { - appState.notes.setPinSelectedNotes(true) - } else { - appState.notes.setPinSelectedNotes(false) - } - }, [appState, onClickPreprocessing, pinned]) - - return ( - - ) - }, -) +export default observer(PinNoteButton) diff --git a/app/assets/javascripts/Components/Preferences/PaneSelector.tsx b/app/assets/javascripts/Components/Preferences/PaneSelector.tsx new file mode 100644 index 000000000..3452816da --- /dev/null +++ b/app/assets/javascripts/Components/Preferences/PaneSelector.tsx @@ -0,0 +1,60 @@ +import { FunctionComponent } from 'react' +import { observer } from 'mobx-react-lite' +import { PreferencesMenu } from './PreferencesMenu' +import Backups from '@/Components/Preferences/Panes/Backups/Backups' +import Appearance from './Panes/Appearance' +import General from './Panes/General/General' +import AccountPreferences from './Panes/Account/AccountPreferences' +import Security from './Panes/Security/Security' +import Listed from './Panes/Listed/Listed' +import HelpAndFeedback from './Panes/HelpFeedback' +import { PreferencesProps } from './PreferencesProps' + +const PaneSelector: FunctionComponent = ({ + menu, + appState, + application, + mfaProvider, + userProvider, +}) => { + switch (menu.selectedPaneId) { + case 'general': + return ( + + ) + case 'account': + return + case 'appearance': + return + case 'security': + return ( + + ) + case 'backups': + return + case 'listed': + return + case 'shortcuts': + return null + case 'accessibility': + return null + case 'get-free-month': + return null + case 'help-feedback': + return + default: + return ( + + ) + } +} + +export default observer(PaneSelector) diff --git a/app/assets/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx b/app/assets/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx index bed91f9a0..6e832c9c9 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx @@ -1,20 +1,20 @@ -import { PreferencesPane } from '@/Components/Preferences/PreferencesComponents' import { observer } from 'mobx-react-lite' import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' -import { Authentication } from './Authentication' -import { Credentials } from './Credentials' -import { Sync } from './Sync' -import { Subscription } from './Subscription/Subscription' -import { SignOutWrapper } from './SignOutView' -import { FilesSection } from './Files' +import Authentication from './Authentication' +import Credentials from './Credentials' +import Sync from './Sync' +import Subscription from './Subscription/Subscription' +import SignOutWrapper from './SignOutView' +import FilesSection from './Files' +import PreferencesPane from '../../PreferencesComponents/PreferencesPane' type Props = { application: WebApplication appState: AppState } -export const AccountPreferences = observer(({ application, appState }: Props) => ( +const AccountPreferences = ({ application, appState }: Props) => ( {!application.hasAccount() ? ( @@ -28,4 +28,6 @@ export const AccountPreferences = observer(({ application, appState }: Props) => {application.hasAccount() && appState.features.hasFiles && } -)) +) + +export default observer(AccountPreferences) diff --git a/app/assets/javascripts/Components/Preferences/Panes/Account/Advanced.tsx b/app/assets/javascripts/Components/Preferences/Panes/Account/Advanced.tsx index ce00ff781..ed16a7af5 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Account/Advanced.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Account/Advanced.tsx @@ -1,20 +1,21 @@ -import { FunctionalComponent } from 'preact' -import { PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents' -import { OfflineSubscription } from '@/Components/Preferences/Panes/Account/OfflineSubscription' +import { FunctionComponent } from 'react' +import OfflineSubscription from '@/Components/Preferences/Panes/Account/OfflineSubscription' import { WebApplication } from '@/UIModels/Application' import { observer } from 'mobx-react-lite' import { AppState } from '@/UIModels/AppState' -import { Extensions } from '@/Components/Preferences/Panes/Extensions/Extensions' +import Extensions from '@/Components/Preferences/Panes/Extensions/Extensions' import { ExtensionsLatestVersions } from '@/Components/Preferences/Panes/Extensions/ExtensionsLatestVersions' -import { AccordionItem } from '@/Components/Shared/AccordionItem' +import AccordionItem from '@/Components/Shared/AccordionItem' +import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' -interface IProps { +type Props = { application: WebApplication appState: AppState extensionsLatestVersions: ExtensionsLatestVersions } -export const Advanced: FunctionalComponent = observer(({ application, appState, extensionsLatestVersions }) => { +const Advanced: FunctionComponent = ({ application, appState, extensionsLatestVersions }) => { return ( @@ -33,4 +34,6 @@ export const Advanced: FunctionalComponent = observer(({ application, ap ) -}) +} + +export default observer(Advanced) diff --git a/app/assets/javascripts/Components/Preferences/Panes/Account/Authentication.tsx b/app/assets/javascripts/Components/Preferences/Panes/Account/Authentication.tsx index 4d40cb020..f12cb7628 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Account/Authentication.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Account/Authentication.tsx @@ -1,16 +1,20 @@ -import { Button } from '@/Components/Button/Button' -import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/Components/Preferences/PreferencesComponents' +import Button from '@/Components/Button/Button' +import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' import { AccountIllustration } from '@standardnotes/icons' import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane' +import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' -export const Authentication: FunctionComponent<{ +type Props = { application: WebApplication appState: AppState -}> = observer(({ appState }) => { +} + +const Authentication: FunctionComponent = ({ appState }) => { const clickSignIn = () => { appState.preferences.closePreferences() appState.accountMenu.setCurrentPane(AccountMenuPane.SignIn) @@ -43,4 +47,6 @@ export const Authentication: FunctionComponent<{ ) -}) +} + +export default observer(Authentication) diff --git a/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx b/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx index 4ca8e618a..217e6df42 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx @@ -1,16 +1,13 @@ -import { useState } from '@node_modules/preact/hooks' -import { - ModalDialog, - ModalDialogButtons, - ModalDialogDescription, - ModalDialogLabel, -} from '@/Components/Shared/ModalDialog' -import { Button } from '@/Components/Button/Button' -import { FunctionalComponent } from 'preact' +import ModalDialog from '@/Components/Shared/ModalDialog' +import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons' +import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription' +import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' +import Button from '@/Components/Button/Button' +import { FunctionComponent, useState } from 'react' import { WebApplication } from '@/UIModels/Application' import { useBeforeUnload } from '@/Hooks/useBeforeUnload' -import { ChangeEmailForm } from './ChangeEmailForm' -import { ChangeEmailSuccess } from './ChangeEmailSuccess' +import ChangeEmailForm from './ChangeEmailForm' +import ChangeEmailSuccess from './ChangeEmailSuccess' import { isEmailValid } from '@/Utils' enum SubmitButtonTitles { @@ -29,7 +26,7 @@ type Props = { application: WebApplication } -export const ChangeEmail: FunctionalComponent = ({ onCloseDialog, application }) => { +const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) => { const [currentPassword, setCurrentPassword] = useState('') const [newEmail, setNewEmail] = useState('') const [isContinuing, setIsContinuing] = useState(false) @@ -158,3 +155,5 @@ export const ChangeEmail: FunctionalComponent = ({ onCloseDialog, applica
    ) } + +export default ChangeEmail diff --git a/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailForm.tsx b/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailForm.tsx index 6f94004b6..6526fdbb1 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailForm.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailForm.tsx @@ -1,16 +1,15 @@ -import { StateUpdater } from 'preact/hooks' -import { FunctionalComponent } from 'preact' +import { Dispatch, SetStateAction, FunctionComponent } from 'react' type Props = { - setNewEmail: StateUpdater - setCurrentPassword: StateUpdater + setNewEmail: Dispatch> + setCurrentPassword: Dispatch> } const labelClassName = 'block mb-1' const inputClassName = 'sk-input contrast' -export const ChangeEmailForm: FunctionalComponent = ({ setNewEmail, setCurrentPassword }) => { +const ChangeEmailForm: FunctionComponent = ({ setNewEmail, setCurrentPassword }) => { return (
    @@ -42,3 +41,5 @@ export const ChangeEmailForm: FunctionalComponent = ({ setNewEmail, setCu
    ) } + +export default ChangeEmailForm diff --git a/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailSuccess.tsx b/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailSuccess.tsx index 1e735eae0..f6ca3f81b 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailSuccess.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailSuccess.tsx @@ -1,6 +1,6 @@ -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' -export const ChangeEmailSuccess: FunctionalComponent = () => { +const ChangeEmailSuccess: FunctionComponent = () => { return (
    Your email has been successfully changed.
    @@ -11,3 +11,5 @@ export const ChangeEmailSuccess: FunctionalComponent = () => {
    ) } + +export default ChangeEmailSuccess diff --git a/app/assets/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx b/app/assets/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx new file mode 100644 index 000000000..da3764d50 --- /dev/null +++ b/app/assets/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx @@ -0,0 +1,30 @@ +import Button from '@/Components/Button/Button' +import { AppState } from '@/UIModels/AppState' +import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'react' +import { Title, Text } from '../../PreferencesComponents/Content' +import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' + +const ClearSessionDataView: FunctionComponent<{ + appState: AppState +}> = ({ appState }) => { + return ( + + + Clear workspace + Remove all data related to the current workspace from the application. +
    +
    + This workspace Remove all data related to the current workspace from the application. @@ -54,33 +55,13 @@ const SignOutView: FunctionComponent<{ ) }) -const ClearSessionDataView: FunctionComponent<{ - appState: AppState -}> = observer(({ appState }) => { - return ( - - - Clear workspace - Remove all data related to the current workspace from the application. -
    -
    - +
    Automatic Light Theme Theme to be used for system light mode: @@ -135,7 +131,7 @@ export const Appearance: FunctionComponent = observer(({ application }) = />
    - +
    Automatic Dark Theme Theme to be used for system dark mode: @@ -155,4 +151,6 @@ export const Appearance: FunctionComponent = observer(({ application }) = ) -}) +} + +export default observer(Appearance) diff --git a/app/assets/javascripts/Components/Preferences/Panes/Backups/Backups.tsx b/app/assets/javascripts/Components/Preferences/Panes/Backups/Backups.tsx index 13e0d3a7c..0671fa0b5 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Backups/Backups.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Backups/Backups.tsx @@ -1,24 +1,27 @@ import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' -import { FunctionComponent } from 'preact' -import { PreferencesPane } from '@/Components/Preferences/PreferencesComponents' -import { CloudLink } from './CloudBackups/CloudBackups' -import { DataBackups } from './DataBackups' -import { EmailBackups } from './EmailBackups' -import { FileBackups } from './Files/FileBackups' +import { FunctionComponent } from 'react' +import PreferencesPane from '@/Components/Preferences/PreferencesComponents/PreferencesPane' +import CloudLink from './CloudBackups/CloudBackups' +import DataBackups from './DataBackups' +import EmailBackups from './EmailBackups' +import FileBackupsCrossPlatform from './Files/FileBackupsCrossPlatform' +import { observer } from 'mobx-react-lite' -interface Props { +type Props = { appState: AppState application: WebApplication } -export const Backups: FunctionComponent = ({ application, appState }) => { +const Backups: FunctionComponent = ({ application, appState }) => { return ( - + ) } + +export default observer(Backups) diff --git a/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackupProvider.tsx b/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackupProvider.tsx index aa66e0058..b7d99e38d 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackupProvider.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackupProvider.tsx @@ -1,4 +1,12 @@ -import { useCallback, useEffect, useState } from 'preact/hooks' +import { + useCallback, + useEffect, + useState, + FunctionComponent, + KeyboardEventHandler, + ChangeEventHandler, + MouseEventHandler, +} from 'react' import { ButtonType, SettingName, @@ -8,11 +16,10 @@ import { OneDriveBackupFrequency, } from '@standardnotes/snjs' import { WebApplication } from '@/UIModels/Application' -import { Button } from '@/Components/Button/Button' +import Button from '@/Components/Button/Button' import { isDev, openInNewTab } from '@/Utils' -import { Subtitle } from '@/Components/Preferences/PreferencesComponents' +import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content' import { KeyboardKey } from '@/Services/IOService' -import { FunctionComponent } from 'preact' type Props = { application: WebApplication @@ -20,17 +27,13 @@ type Props = { isEntitledToCloudBackups: boolean } -export const CloudBackupProvider: FunctionComponent = ({ - application, - providerName, - isEntitledToCloudBackups, -}) => { +const CloudBackupProvider: FunctionComponent = ({ application, providerName, isEntitledToCloudBackups }) => { const [authBegan, setAuthBegan] = useState(false) const [successfullyInstalled, setSuccessfullyInstalled] = useState(false) const [backupFrequency, setBackupFrequency] = useState(undefined) const [confirmation, setConfirmation] = useState('') - const disable = async (event: Event) => { + const disable: MouseEventHandler = async (event) => { event.stopPropagation() try { @@ -52,7 +55,7 @@ export const CloudBackupProvider: FunctionComponent = ({ } } - const installIntegration = (event: Event) => { + const installIntegration: MouseEventHandler = (event) => { if (!isEntitledToCloudBackups) { return } @@ -117,7 +120,7 @@ export const CloudBackupProvider: FunctionComponent = ({ return urlSearchParams.get(integrationTokenKeyInUrl) } - const handleKeyPress = async (event: KeyboardEvent) => { + const handleKeyPress: KeyboardEventHandler = async (event) => { if (event.key === KeyboardKey.Enter) { try { const decryptedCode = atob(confirmation) @@ -145,8 +148,8 @@ export const CloudBackupProvider: FunctionComponent = ({ } } - const handleChange = (event: Event) => { - setConfirmation((event.target as HTMLInputElement).value) + const handleChange: ChangeEventHandler = (event) => { + setConfirmation(event.target.value) } const getIntegrationStatus = useCallback(async () => { @@ -219,3 +222,5 @@ export const CloudBackupProvider: FunctionComponent = ({
    ) } + +export default CloudBackupProvider diff --git a/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackups.tsx b/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackups.tsx index baa8a0b2a..f7794647e 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackups.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackups.tsx @@ -1,14 +1,8 @@ -import { CloudBackupProvider } from './CloudBackupProvider' -import { useCallback, useEffect, useState } from 'preact/hooks' +import CloudBackupProvider from './CloudBackupProvider' +import { useCallback, useEffect, useState, FunctionComponent, Fragment } from 'react' import { WebApplication } from '@/UIModels/Application' -import { - PreferencesGroup, - PreferencesSegment, - Subtitle, - Text, - Title, -} from '@/Components/Preferences/PreferencesComponents' -import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' +import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' +import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import { FeatureStatus, FeatureIdentifier, @@ -16,11 +10,12 @@ import { MuteFailedCloudBackupsEmailsOption, SettingName, } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { Switch } from '@/Components/Switch/Switch' +import Switch from '@/Components/Switch/Switch' import { convertStringifiedBooleanToBoolean } from '@/Utils' import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Strings' +import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment' const providerData = [{ name: CloudProvider.Dropbox }, { name: CloudProvider.Google }, { name: CloudProvider.OneDrive }] @@ -28,7 +23,7 @@ type Props = { application: WebApplication } -export const CloudLink: FunctionComponent = ({ application }) => { +const CloudLink: FunctionComponent = ({ application }) => { const [isEntitledToCloudBackups, setIsEntitledToCloudBackups] = useState(false) const [isFailedCloudBackupEmailMuted, setIsFailedCloudBackupEmailMuted] = useState(true) const [isLoading, setIsLoading] = useState(false) @@ -121,14 +116,14 @@ export const CloudLink: FunctionComponent = ({ application }) => {
    {providerData.map(({ name }) => ( - <> + - + ))}
    @@ -155,3 +150,5 @@ export const CloudLink: FunctionComponent = ({ application }) => { ) } + +export default CloudLink diff --git a/app/assets/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx b/app/assets/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx index 1763af116..1067dafe9 100644 --- a/app/assets/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx +++ b/app/assets/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx @@ -11,27 +11,22 @@ import { STRING_ENC_NOT_ENABLED, } from '@/Strings' import { BackupFile } from '@standardnotes/snjs' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react' import { WebApplication } from '@/UIModels/Application' -import { JSXInternal } from 'preact/src/jsx' -import TargetedEvent = JSXInternal.TargetedEvent import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { - PreferencesGroup, - PreferencesSegment, - Title, - Text, - Subtitle, -} from '@/Components/Preferences/PreferencesComponents' -import { Button } from '@/Components/Button/Button' +import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content' +import Button from '@/Components/Button/Button' +import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' +import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' type Props = { application: WebApplication appState: AppState } -export const DataBackups = observer(({ application, appState }: Props) => { +const DataBackups = ({ application, appState }: Props) => { const fileInputRef = useRef(null) const [isImportDataLoading, setIsImportDataLoading] = useState(false) const { @@ -109,8 +104,8 @@ export const DataBackups = observer(({ application, appState }: Props) => { }) } - const importFileSelected = async (event: TargetedEvent) => { - const { files } = event.target as HTMLInputElement + const importFileSelected: ChangeEventHandler = async (event) => { + const { files } = event.target if (!files) { return @@ -136,7 +131,7 @@ export const DataBackups = observer(({ application, appState }: Props) => { } // Whenever "Import Backup" is either clicked or key-pressed, proceed the import - const handleImportFile = (event: TargetedEvent | KeyboardEvent) => { + const handleImportFile: MouseEventHandler = (event) => { if (event instanceof KeyboardEvent) { const { code } = event @@ -158,7 +153,7 @@ export const DataBackups = observer(({ application, appState }: Props) => { Data Backups - {!isDesktopApplication() && ( + {isDesktopApplication() && ( Backups are automatically created on desktop and can be managed via the "Backups" top-level menu. @@ -183,10 +178,11 @@ export const DataBackups = observer(({ application, appState }: Props) => { ) } + +export default ThemesMenuButton diff --git a/app/assets/javascripts/Components/RevisionHistoryModal/HistoryListContainer.tsx b/app/assets/javascripts/Components/RevisionHistoryModal/HistoryListContainer.tsx index 871d7765c..57e8bd5d6 100644 --- a/app/assets/javascripts/Components/RevisionHistoryModal/HistoryListContainer.tsx +++ b/app/assets/javascripts/Components/RevisionHistoryModal/HistoryListContainer.tsx @@ -1,11 +1,10 @@ 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, useEffect } from 'preact/hooks' -import { LegacyHistoryList } from './LegacyHistoryList' -import { RemoteHistoryList } from './RemoteHistoryList' -import { SessionHistoryList } from './SessionHistoryList' +import { FunctionComponent, useCallback, useState, useEffect, SetStateAction, Dispatch } from 'react' +import LegacyHistoryList from './LegacyHistoryList' +import RemoteHistoryList from './RemoteHistoryList' +import SessionHistoryList from './SessionHistoryList' import { LegacyHistoryEntry, RemoteRevisionListGroup, sortRevisionListIntoGroups } from './utils' export enum RevisionListTabType { @@ -19,169 +18,169 @@ type Props = { isFetchingRemoteHistory: boolean note: SNNote remoteHistory: RemoteRevisionListGroup[] | undefined - setIsFetchingSelectedRevision: StateUpdater - setSelectedRemoteEntry: StateUpdater - setSelectedRevision: StateUpdater - setShowContentLockedScreen: StateUpdater + setIsFetchingSelectedRevision: Dispatch> + setSelectedRemoteEntry: Dispatch> + setSelectedRevision: Dispatch> + setShowContentLockedScreen: Dispatch> } -export const HistoryListContainer: FunctionComponent = observer( - ({ - application, - isFetchingRemoteHistory, - note, - remoteHistory, - setIsFetchingSelectedRevision, - setSelectedRemoteEntry, - setSelectedRevision, - setShowContentLockedScreen, - }) => { - const sessionHistory = sortRevisionListIntoGroups( - application.historyManager.sessionHistoryForItem(note) as NoteHistoryEntry[], - ) - const [legacyHistory, setLegacyHistory] = useState() +const HistoryListContainer: FunctionComponent = ({ + application, + isFetchingRemoteHistory, + note, + remoteHistory, + setIsFetchingSelectedRevision, + setSelectedRemoteEntry, + setSelectedRevision, + setShowContentLockedScreen, +}) => { + const sessionHistory = sortRevisionListIntoGroups( + application.historyManager.sessionHistoryForItem(note) as NoteHistoryEntry[], + ) + const [legacyHistory, setLegacyHistory] = useState() - const [selectedTab, setSelectedTab] = useState(RevisionListTabType.Remote) + const [selectedTab, setSelectedTab] = useState(RevisionListTabType.Remote) - useEffect(() => { - const fetchLegacyHistory = async () => { - const actionExtensions = application.actionsManager.getExtensions() - actionExtensions.forEach(async (ext) => { - const actionExtension = await application.actionsManager.loadExtensionInContextOfItem(ext, note) + useEffect(() => { + const fetchLegacyHistory = async () => { + const actionExtensions = application.actionsManager.getExtensions() + actionExtensions.forEach(async (ext) => { + const actionExtension = await application.actionsManager.loadExtensionInContextOfItem(ext, note) - if (!actionExtension) { - return - } + if (!actionExtension) { + return + } - const isLegacyNoteHistoryExt = actionExtension?.actions.some((action) => action.verb === ActionVerb.Nested) + const isLegacyNoteHistoryExt = actionExtension?.actions.some((action) => action.verb === ActionVerb.Nested) - if (!isLegacyNoteHistoryExt) { - return - } + if (!isLegacyNoteHistoryExt) { + return + } - const legacyHistoryEntries = actionExtension.actions.filter((action) => action.subactions?.[0]) + const legacyHistoryEntries = actionExtension.actions.filter((action) => action.subactions?.[0]) - setLegacyHistory(legacyHistoryEntries) - }) - } - - fetchLegacyHistory().catch(console.error) - }, [application, note]) - - const TabButton: FunctionComponent<{ - type: RevisionListTabType - }> = ({ type }) => { - const isSelected = selectedTab === type - - return ( - - ) + setLegacyHistory(legacyHistoryEntries) + }) } - const fetchAndSetLegacyRevision = useCallback( - async (revisionListEntry: Action) => { - setSelectedRemoteEntry(undefined) + fetchLegacyHistory().catch(console.error) + }, [application, note]) + + const TabButton: FunctionComponent<{ + type: RevisionListTabType + }> = ({ type }) => { + const isSelected = selectedTab === type + + return ( + + ) + } + + const fetchAndSetLegacyRevision = useCallback( + async (revisionListEntry: Action) => { + setSelectedRemoteEntry(undefined) + setSelectedRevision(undefined) + setIsFetchingSelectedRevision(true) + + try { + if (!revisionListEntry.subactions?.[0]) { + throw new Error('Could not find revision action url') + } + + const response = await application.actionsManager.runAction(revisionListEntry.subactions[0], note) + + if (!response) { + throw new Error('Could not fetch revision') + } + + setSelectedRevision(response.item as unknown as HistoryEntry) + } catch (error) { + console.error(error) setSelectedRevision(undefined) + } finally { + setIsFetchingSelectedRevision(false) + } + }, + [application.actionsManager, note, setIsFetchingSelectedRevision, setSelectedRemoteEntry, setSelectedRevision], + ) + + const fetchAndSetRemoteRevision = useCallback( + async (revisionListEntry: RevisionListEntry) => { + setShowContentLockedScreen(false) + + if (application.features.hasMinimumRole(revisionListEntry.required_role)) { setIsFetchingSelectedRevision(true) + setSelectedRevision(undefined) + setSelectedRemoteEntry(undefined) try { - if (!revisionListEntry.subactions?.[0]) { - throw new Error('Could not find revision action url') - } - - const response = await application.actionsManager.runAction(revisionListEntry.subactions[0], note) - - if (!response) { - throw new Error('Could not fetch revision') - } - - setSelectedRevision(response.item as unknown as HistoryEntry) - } catch (error) { - console.error(error) - setSelectedRevision(undefined) + const remoteRevision = await application.historyManager.fetchRemoteRevision(note, revisionListEntry) + setSelectedRevision(remoteRevision) + setSelectedRemoteEntry(revisionListEntry) + } catch (err) { + console.error(err) } finally { setIsFetchingSelectedRevision(false) } - }, - [application.actionsManager, note, setIsFetchingSelectedRevision, setSelectedRemoteEntry, setSelectedRevision], - ) + } else { + setShowContentLockedScreen(true) + setSelectedRevision(undefined) + } + }, + [ + application, + note, + setIsFetchingSelectedRevision, + setSelectedRemoteEntry, + setSelectedRevision, + setShowContentLockedScreen, + ], + ) - const fetchAndSetRemoteRevision = useCallback( - async (revisionListEntry: RevisionListEntry) => { - setShowContentLockedScreen(false) - - if (application.features.hasMinimumRole(revisionListEntry.required_role)) { - setIsFetchingSelectedRevision(true) - setSelectedRevision(undefined) - setSelectedRemoteEntry(undefined) - - try { - const remoteRevision = await application.historyManager.fetchRemoteRevision(note, revisionListEntry) - setSelectedRevision(remoteRevision) - setSelectedRemoteEntry(revisionListEntry) - } catch (err) { - console.error(err) - } finally { - setIsFetchingSelectedRevision(false) - } - } else { - setShowContentLockedScreen(true) - setSelectedRevision(undefined) - } - }, - [ - application, - note, - setIsFetchingSelectedRevision, - setSelectedRemoteEntry, - setSelectedRevision, - setShowContentLockedScreen, - ], - ) - - return ( -
    -
    - - - {legacyHistory && legacyHistory.length > 0 && } -
    -
    - {selectedTab === RevisionListTabType.Session && ( - - )} - {selectedTab === RevisionListTabType.Remote && ( - - )} - {selectedTab === RevisionListTabType.Legacy && ( - - )} -
    + return ( +
    +
    + + + {legacyHistory && legacyHistory.length > 0 && }
    - ) - }, -) +
    + {selectedTab === RevisionListTabType.Session && ( + + )} + {selectedTab === RevisionListTabType.Remote && ( + + )} + {selectedTab === RevisionListTabType.Legacy && ( + + )} +
    +
    + ) +} + +export default observer(HistoryListContainer) diff --git a/app/assets/javascripts/Components/RevisionHistoryModal/HistoryListItem.tsx b/app/assets/javascripts/Components/RevisionHistoryModal/HistoryListItem.tsx index 4ee116cbe..6b9821366 100644 --- a/app/assets/javascripts/Components/RevisionHistoryModal/HistoryListItem.tsx +++ b/app/assets/javascripts/Components/RevisionHistoryModal/HistoryListItem.tsx @@ -1,12 +1,12 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' type HistoryListItemProps = { isSelected: boolean onClick: () => void } -export const HistoryListItem: FunctionComponent = ({ children, isSelected, onClick }) => { +const HistoryListItem: FunctionComponent = ({ children, isSelected, onClick }) => { return ( ) } + +export default HistoryListItem diff --git a/app/assets/javascripts/Components/RevisionHistoryModal/LegacyHistoryList.tsx b/app/assets/javascripts/Components/RevisionHistoryModal/LegacyHistoryList.tsx index 2b9c468e5..57d09ab8c 100644 --- a/app/assets/javascripts/Components/RevisionHistoryModal/LegacyHistoryList.tsx +++ b/app/assets/javascripts/Components/RevisionHistoryModal/LegacyHistoryList.tsx @@ -1,18 +1,17 @@ import { Action, HistoryEntry, RevisionListEntry } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { StateUpdater, useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' +import { Dispatch, FunctionComponent, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation' -import { HistoryListItem } from './HistoryListItem' +import HistoryListItem from './HistoryListItem' import { LegacyHistoryEntry } from './utils' type Props = { legacyHistory: Action[] | undefined - setSelectedRevision: StateUpdater - setSelectedRemoteEntry: StateUpdater + setSelectedRevision: Dispatch> + setSelectedRemoteEntry: Dispatch> fetchAndSetLegacyRevision: (revisionListEntry: Action) => Promise } -export const LegacyHistoryList: FunctionComponent = ({ +const LegacyHistoryList: FunctionComponent = ({ legacyHistory, setSelectedRevision, setSelectedRemoteEntry, @@ -72,3 +71,5 @@ export const LegacyHistoryList: FunctionComponent = ({
    ) } + +export default LegacyHistoryList diff --git a/app/assets/javascripts/Components/RevisionHistoryModal/RemoteHistoryList.tsx b/app/assets/javascripts/Components/RevisionHistoryModal/RemoteHistoryList.tsx index c940e4793..2c392e1ea 100644 --- a/app/assets/javascripts/Components/RevisionHistoryModal/RemoteHistoryList.tsx +++ b/app/assets/javascripts/Components/RevisionHistoryModal/RemoteHistoryList.tsx @@ -1,11 +1,10 @@ import { WebApplication } from '@/UIModels/Application' import { RevisionListEntry } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { Fragment, FunctionComponent } from 'preact' -import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' +import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import Icon from '@/Components/Icon/Icon' import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation' -import { HistoryListItem } from './HistoryListItem' +import HistoryListItem from './HistoryListItem' import { previewHistoryEntryTitle, RemoteRevisionListGroup } from './utils' type RemoteHistoryListProps = { @@ -15,76 +14,78 @@ type RemoteHistoryListProps = { fetchAndSetRemoteRevision: (revisionListEntry: RevisionListEntry) => Promise } -export const RemoteHistoryList: FunctionComponent = observer( - ({ application, remoteHistory, isFetchingRemoteHistory, fetchAndSetRemoteRevision }) => { - const remoteHistoryListRef = useRef(null) +const RemoteHistoryList: FunctionComponent = ({ + application, + remoteHistory, + isFetchingRemoteHistory, + fetchAndSetRemoteRevision, +}) => { + const remoteHistoryListRef = useRef(null) - useListKeyboardNavigation(remoteHistoryListRef) + useListKeyboardNavigation(remoteHistoryListRef) - const remoteHistoryLength = useMemo( - () => remoteHistory?.map((group) => group.entries).flat().length, - [remoteHistory], - ) + const remoteHistoryLength = useMemo(() => remoteHistory?.map((group) => group.entries).flat().length, [remoteHistory]) - const [selectedEntryUuid, setSelectedEntryUuid] = useState('') + const [selectedEntryUuid, setSelectedEntryUuid] = useState('') - const firstEntry = useMemo(() => { - return remoteHistory?.find((group) => group.entries?.length)?.entries?.[0] - }, [remoteHistory]) + const firstEntry = useMemo(() => { + return remoteHistory?.find((group) => group.entries?.length)?.entries?.[0] + }, [remoteHistory]) - const selectFirstEntry = useCallback(() => { - if (firstEntry) { - setSelectedEntryUuid(firstEntry.uuid) - fetchAndSetRemoteRevision(firstEntry).catch(console.error) - } - }, [fetchAndSetRemoteRevision, firstEntry]) + const selectFirstEntry = useCallback(() => { + if (firstEntry) { + setSelectedEntryUuid(firstEntry.uuid) + fetchAndSetRemoteRevision(firstEntry).catch(console.error) + } + }, [fetchAndSetRemoteRevision, firstEntry]) - useEffect(() => { - if (firstEntry && !selectedEntryUuid.length) { - selectFirstEntry() - } - }, [fetchAndSetRemoteRevision, firstEntry, remoteHistory, selectFirstEntry, selectedEntryUuid.length]) + useEffect(() => { + if (firstEntry && !selectedEntryUuid.length) { + selectFirstEntry() + } + }, [fetchAndSetRemoteRevision, firstEntry, remoteHistory, selectFirstEntry, selectedEntryUuid.length]) - return ( -
    - {isFetchingRemoteHistory &&
    } - {remoteHistory?.map((group) => { - if (group.entries && group.entries.length) { - return ( - -
    - {group.title} -
    - {group.entries.map((entry) => ( - { - setSelectedEntryUuid(entry.uuid) - fetchAndSetRemoteRevision(entry).catch(console.error) - }} - > -
    -
    {previewHistoryEntryTitle(entry)}
    - {!application.features.hasMinimumRole(entry.required_role) && } -
    -
    - ))} -
    - ) - } else { - return null - } - })} - {!remoteHistoryLength && !isFetchingRemoteHistory && ( -
    No remote history found
    - )} -
    - ) - }, -) + return ( +
    + {isFetchingRemoteHistory &&
    } + {remoteHistory?.map((group) => { + if (group.entries && group.entries.length) { + return ( + +
    + {group.title} +
    + {group.entries.map((entry) => ( + { + setSelectedEntryUuid(entry.uuid) + fetchAndSetRemoteRevision(entry).catch(console.error) + }} + > +
    +
    {previewHistoryEntryTitle(entry)}
    + {!application.features.hasMinimumRole(entry.required_role) && } +
    +
    + ))} +
    + ) + } else { + return null + } + })} + {!remoteHistoryLength && !isFetchingRemoteHistory && ( +
    No remote history found
    + )} +
    + ) +} + +export default observer(RemoteHistoryList) diff --git a/app/assets/javascripts/Components/RevisionHistoryModal/RevisionContentLocked.tsx b/app/assets/javascripts/Components/RevisionHistoryModal/RevisionContentLocked.tsx index f22885185..52edcbf95 100644 --- a/app/assets/javascripts/Components/RevisionHistoryModal/RevisionContentLocked.tsx +++ b/app/assets/javascripts/Components/RevisionHistoryModal/RevisionContentLocked.tsx @@ -1,8 +1,8 @@ import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' import { HistoryLockedIllustration } from '@standardnotes/icons' -import { Button } from '@/Components/Button/Button' +import Button from '@/Components/Button/Button' const getPlanHistoryDuration = (planName: string | undefined) => { switch (planName) { @@ -19,16 +19,18 @@ const getPremiumContentCopy = (planName: string | undefined) => { return `Version history is limited to ${getPlanHistoryDuration(planName)} in the ${planName} plan` } -export const RevisionContentLocked: FunctionComponent<{ +type Props = { appState: AppState -}> = observer(({ appState }) => { +} + +const RevisionContentLocked: FunctionComponent = ({ appState }) => { const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } = appState.subscription return (
    -
    Can't access this version
    +
    Can't access this version
    {getPremiumContentCopy( !isUserSubscriptionCanceled && !isUserSubscriptionExpired && userSubscriptionName @@ -49,4 +51,6 @@ export const RevisionContentLocked: FunctionComponent<{
    ) -}) +} + +export default observer(RevisionContentLocked) diff --git a/app/assets/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx b/app/assets/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx index 5b483b67c..24d54382a 100644 --- a/app/assets/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx +++ b/app/assets/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx @@ -13,12 +13,11 @@ import { SNNote, } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' -import { Button } from '@/Components/Button/Button' -import { HistoryListContainer } from './HistoryListContainer' -import { RevisionContentLocked } from './RevisionContentLocked' -import { SelectedRevisionContent } from './SelectedRevisionContent' +import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import Button from '@/Components/Button/Button' +import HistoryListContainer from './HistoryListContainer' +import RevisionContentLocked from './RevisionContentLocked' +import SelectedRevisionContent from './SelectedRevisionContent' import { LegacyHistoryEntry, RemoteRevisionListGroup, sortRevisionListIntoGroups } from './utils' type RevisionHistoryModalProps = { @@ -209,6 +208,7 @@ export const RevisionHistoryModal: FunctionComponent aria-label="Note revision history" > />
    {selectedRevision && ( -
    +
    {selectedRemoteEntry && (
    ) -}) +} + +export default observer(SearchOptions) diff --git a/app/assets/javascripts/Components/SessionsModal/SessionsModal.tsx b/app/assets/javascripts/Components/SessionsModal/SessionsModal.tsx index df6ff0f17..833cf791d 100644 --- a/app/assets/javascripts/Components/SessionsModal/SessionsModal.tsx +++ b/app/assets/javascripts/Components/SessionsModal/SessionsModal.tsx @@ -1,7 +1,6 @@ import { AppState } from '@/UIModels/AppState' import { SNApplication, SessionStrings, UuidString, isNullOrUndefined, RemoteSession } from '@standardnotes/snjs' -import { FunctionComponent } from 'preact' -import { useState, useEffect, useRef, useMemo } from 'preact/hooks' +import { FunctionComponent, useState, useEffect, useRef, useMemo } from 'react' import { Dialog } from '@reach/dialog' import { Alert } from '@reach/alert' import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog' @@ -103,23 +102,23 @@ const SessionsModalContent: FunctionComponent<{ <>
    -
    -
    -
    -
    Active Sessions
    +
    +
    +
    +
    Active Sessions
    - -
    -
    +
    {refreshing ? ( <> -
    +

    Loading sessions

    ) : ( @@ -128,7 +127,7 @@ const SessionsModalContent: FunctionComponent<{ {sessions.length > 0 && (
      {sessions.map((session) => ( -
    • +
    • {session.device_info}

      {session.current ? ( Current session @@ -202,13 +201,15 @@ const SessionsModalContent: FunctionComponent<{ ) } -export const SessionsModal: FunctionComponent<{ +const SessionsModal: FunctionComponent<{ appState: AppState application: WebApplication -}> = observer(({ appState, application }) => { +}> = ({ appState, application }) => { if (appState.isSessionsModalVisible) { return } else { return null } -}) +} + +export default observer(SessionsModal) diff --git a/app/assets/javascripts/Components/Shared/AccordionItem.tsx b/app/assets/javascripts/Components/Shared/AccordionItem.tsx index c33dcb692..45449010e 100644 --- a/app/assets/javascripts/Components/Shared/AccordionItem.tsx +++ b/app/assets/javascripts/Components/Shared/AccordionItem.tsx @@ -1,14 +1,13 @@ -import { FunctionalComponent } from 'preact' -import { useRef, useState } from 'preact/hooks' +import { FunctionComponent, useRef, useState } from 'react' import { ArrowDownCheckmarkIcon } from '@standardnotes/icons' -import { Title } from '@/Components/Preferences/PreferencesComponents' +import { Title } from '@/Components/Preferences/PreferencesComponents/Content' type Props = { title: string | JSX.Element className?: string } -export const AccordionItem: FunctionalComponent = ({ title, className = '', children }) => { +const AccordionItem: FunctionComponent = ({ title, className = '', children }) => { const elementRef = useRef(null) const [isExpanded, setIsExpanded] = useState(false) @@ -34,3 +33,5 @@ export const AccordionItem: FunctionalComponent = ({ title, className = '
    ) } + +export default AccordionItem diff --git a/app/assets/javascripts/Components/Shared/HorizontalSeparator.tsx b/app/assets/javascripts/Components/Shared/HorizontalSeparator.tsx index bf5ac0de8..43739ffef 100644 --- a/app/assets/javascripts/Components/Shared/HorizontalSeparator.tsx +++ b/app/assets/javascripts/Components/Shared/HorizontalSeparator.tsx @@ -1,8 +1,10 @@ -import { FunctionalComponent } from 'preact' +import { FunctionComponent } from 'react' type Props = { classes?: string } -export const HorizontalSeparator: FunctionalComponent = ({ classes = '' }) => { +const HorizontalSeparator: FunctionComponent = ({ classes = '' }) => { return
    } + +export default HorizontalSeparator diff --git a/app/assets/javascripts/Components/Shared/ModalDialog.tsx b/app/assets/javascripts/Components/Shared/ModalDialog.tsx index 11af27610..522896254 100644 --- a/app/assets/javascripts/Components/Shared/ModalDialog.tsx +++ b/app/assets/javascripts/Components/Shared/ModalDialog.tsx @@ -1,8 +1,7 @@ -import { FunctionComponent } from 'preact' -import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog' -import { useRef } from 'preact/hooks' +import { FunctionComponent, useRef } from 'react' +import { AlertDialog } from '@reach/alert-dialog' -export const ModalDialog: FunctionComponent = ({ children }) => { +const ModalDialog: FunctionComponent = ({ children }) => { const ldRef = useRef(null) return ( @@ -20,43 +19,4 @@ export const ModalDialog: FunctionComponent = ({ children }) => { ) } -export const ModalDialogLabel: FunctionComponent<{ - closeDialog: () => void - className?: string -}> = ({ children, closeDialog, className }) => ( - -
    -
    {children}
    -
    - Close -
    -
    -
    -
    -) - -export const ModalDialogDescription: FunctionComponent<{ - className?: string -}> = ({ children, className = '' }) => ( - - {children} - -) - -export const ModalDialogButtons: FunctionComponent<{ className?: string }> = ({ children, className }) => ( - <> -
    -
    - {children != undefined && Array.isArray(children) - ? children.map((child, idx, arr) => ( - <> - {child} - {idx < arr.length - 1 ?
    : undefined} - - )) - : children} -
    - -) - export default ModalDialog diff --git a/app/assets/javascripts/Components/Shared/ModalDialogButtons.tsx b/app/assets/javascripts/Components/Shared/ModalDialogButtons.tsx new file mode 100644 index 000000000..1ab16496d --- /dev/null +++ b/app/assets/javascripts/Components/Shared/ModalDialogButtons.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent } from 'react' + +type Props = { + className?: string +} + +const ModalDialogButtons: FunctionComponent = ({ children, className }) => ( + <> +
    +
    + {children != undefined && Array.isArray(children) + ? children.map((child, idx, arr) => ( + <> + {child} + {idx < arr.length - 1 ?
    : undefined} + + )) + : children} +
    + +) + +export default ModalDialogButtons diff --git a/app/assets/javascripts/Components/Shared/ModalDialogDescription.tsx b/app/assets/javascripts/Components/Shared/ModalDialogDescription.tsx new file mode 100644 index 000000000..99675626c --- /dev/null +++ b/app/assets/javascripts/Components/Shared/ModalDialogDescription.tsx @@ -0,0 +1,14 @@ +import { FunctionComponent } from 'react' +import { AlertDialogDescription } from '@reach/alert-dialog' + +type Props = { + className?: string +} + +const ModalDialogDescription: FunctionComponent = ({ children, className = '' }) => ( + + {children} + +) + +export default ModalDialogDescription diff --git a/app/assets/javascripts/Components/Shared/ModalDialogLabel.tsx b/app/assets/javascripts/Components/Shared/ModalDialogLabel.tsx new file mode 100644 index 000000000..74ec5a2c3 --- /dev/null +++ b/app/assets/javascripts/Components/Shared/ModalDialogLabel.tsx @@ -0,0 +1,21 @@ +import { FunctionComponent } from 'react' +import { AlertDialogLabel } from '@reach/alert-dialog' + +type Props = { + closeDialog: () => void + className?: string +} + +const ModalDialogLabel: FunctionComponent = ({ children, closeDialog, className }) => ( + +
    +
    {children}
    +
    + Close +
    +
    +
    +
    +) + +export default ModalDialogLabel diff --git a/app/assets/javascripts/Components/Switch/Switch.tsx b/app/assets/javascripts/Components/Switch/Switch.tsx index b07a54e3c..86e68ea1f 100644 --- a/app/assets/javascripts/Components/Switch/Switch.tsx +++ b/app/assets/javascripts/Components/Switch/Switch.tsx @@ -1,20 +1,9 @@ import { CustomCheckboxContainer, CustomCheckboxInput, CustomCheckboxInputProps } from '@reach/checkbox' import '@reach/checkbox/styles.css' -import { ComponentChildren, FunctionalComponent } from 'preact' -import { useState } from 'preact/hooks' +import { FunctionComponent, useState } from 'react' +import { SwitchProps } from './SwitchProps' -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 = (props: SwitchProps) => { +const Switch: FunctionComponent = (props: SwitchProps) => { const [checkedState, setChecked] = useState(props.checked || false) const checked = props.checked ?? checkedState const className = props.className ?? '' @@ -51,3 +40,5 @@ export const Switch: FunctionalComponent = (props: SwitchProps) => ) } + +export default Switch diff --git a/app/assets/javascripts/Components/Switch/SwitchProps.tsx b/app/assets/javascripts/Components/Switch/SwitchProps.tsx new file mode 100644 index 000000000..0e5e02e68 --- /dev/null +++ b/app/assets/javascripts/Components/Switch/SwitchProps.tsx @@ -0,0 +1,11 @@ +import { ReactNode } from 'react' + +export type SwitchProps = { + checked?: boolean + onChange?: (checked: boolean) => void + className?: string + children?: ReactNode + role?: string + disabled?: boolean + tabIndex?: number +} diff --git a/app/assets/javascripts/Components/SyncResolutionMenu/SyncResolutionMenu.tsx b/app/assets/javascripts/Components/SyncResolutionMenu/SyncResolutionMenu.tsx index 0e3c45350..ecce71632 100644 --- a/app/assets/javascripts/Components/SyncResolutionMenu/SyncResolutionMenu.tsx +++ b/app/assets/javascripts/Components/SyncResolutionMenu/SyncResolutionMenu.tsx @@ -6,7 +6,7 @@ type Props = { close: () => void } -export class SyncResolutionMenu extends PureComponent { +class SyncResolutionMenu extends PureComponent { constructor(props: Props) { super(props, props.application) } @@ -55,3 +55,5 @@ export class SyncResolutionMenu extends PureComponent { ) } } + +export default SyncResolutionMenu diff --git a/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagHint.tsx b/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagHint.tsx index 093045639..504ffdb0d 100644 --- a/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagHint.tsx +++ b/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagHint.tsx @@ -1,14 +1,14 @@ import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { useRef, useEffect, useCallback } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' +import { useRef, useEffect, useCallback, FocusEventHandler, KeyboardEventHandler } from 'react' +import Icon from '@/Components/Icon/Icon' type Props = { appState: AppState closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void } -export const AutocompleteTagHint = observer(({ appState, closeOnBlur }: Props) => { +const AutocompleteTagHint = ({ appState, closeOnBlur }: Props) => { const { autocompleteTagHintFocused } = appState.noteTags const hintRef = useRef(null) @@ -24,16 +24,16 @@ export const AutocompleteTagHint = observer(({ appState, closeOnBlur }: Props) = appState.noteTags.setAutocompleteTagHintFocused(true) }, [appState]) - const onBlur = useCallback( - (event: FocusEvent) => { + const onBlur: FocusEventHandler = useCallback( + (event) => { closeOnBlur(event) appState.noteTags.setAutocompleteTagHintFocused(false) }, [appState, closeOnBlur], ) - const onKeyDown = useCallback( - (event: KeyboardEvent) => { + const onKeyDown: KeyboardEventHandler = useCallback( + (event) => { if (event.key === 'ArrowUp') { if (autocompleteTagResults.length > 0) { const lastTagResult = autocompleteTagResults[autocompleteTagResults.length - 1] @@ -75,4 +75,6 @@ export const AutocompleteTagHint = observer(({ appState, closeOnBlur }: Props) = ) -}) +} + +export default observer(AutocompleteTagHint) diff --git a/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagInput.tsx b/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagInput.tsx index 07215243b..9f58fa47c 100644 --- a/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagInput.tsx @@ -1,9 +1,17 @@ -import { useEffect, useRef, useState } from 'preact/hooks' +import { + ChangeEventHandler, + FocusEventHandler, + FormEventHandler, + KeyboardEventHandler, + useEffect, + useRef, + useState, +} from 'react' import { Disclosure, DisclosurePanel } from '@reach/disclosure' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { AppState } from '@/UIModels/AppState' -import { AutocompleteTagResult } from './AutocompleteTagResult' -import { AutocompleteTagHint } from './AutocompleteTagHint' +import AutocompleteTagResult from './AutocompleteTagResult' +import AutocompleteTagHint from './AutocompleteTagHint' import { observer } from 'mobx-react-lite' import { SNTag } from '@standardnotes/snjs' @@ -11,7 +19,7 @@ type Props = { appState: AppState } -export const AutocompleteTagInput = observer(({ appState }: Props) => { +const AutocompleteTagInput = ({ appState }: Props) => { const { autocompleteInputFocused, autocompleteSearchQuery, @@ -41,8 +49,8 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { } } - const onSearchQueryChange = (event: Event) => { - const query = (event.target as HTMLInputElement).value + const onSearchQueryChange: ChangeEventHandler = (event) => { + const query = event.target.value if (query === '') { appState.noteTags.clearAutocompleteSearch() @@ -52,14 +60,14 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { } } - const onFormSubmit = async (event: Event) => { + const onFormSubmit: FormEventHandler = async (event) => { event.preventDefault() if (autocompleteSearchQuery !== '') { await appState.noteTags.createAndAddNewTag() } } - const onKeyDown = (event: KeyboardEvent) => { + const onKeyDown: KeyboardEventHandler = (event) => { switch (event.key) { case 'Backspace': case 'ArrowLeft': @@ -85,7 +93,7 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { appState.noteTags.setAutocompleteInputFocused(true) } - const onBlur = (event: FocusEvent) => { + const onBlur: FocusEventHandler = (event) => { closeOnBlur(event) appState.noteTags.setAutocompleteInputFocused(false) } @@ -139,4 +147,6 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => {
    ) -}) +} + +export default observer(AutocompleteTagInput) diff --git a/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagResult.tsx b/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagResult.tsx index cce905368..64dd5f0e1 100644 --- a/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagResult.tsx +++ b/app/assets/javascripts/Components/TagAutocomplete/AutocompleteTagResult.tsx @@ -2,8 +2,8 @@ import { AppState } from '@/UIModels/AppState' import { splitQueryInString } from '@/Utils/StringUtils' import { SNTag } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { useEffect, useRef } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' +import { FocusEventHandler, KeyboardEventHandler, useEffect, useRef } from 'react' +import Icon from '@/Components/Icon/Icon' type Props = { appState: AppState @@ -11,7 +11,7 @@ type Props = { closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void } -export const AutocompleteTagResult = observer(({ appState, tagResult, closeOnBlur }: Props) => { +const AutocompleteTagResult = ({ appState, tagResult, closeOnBlur }: Props) => { const { autocompleteSearchQuery, autocompleteTagHintVisible, autocompleteTagResults, focusedTagResultUuid } = appState.noteTags @@ -26,7 +26,7 @@ export const AutocompleteTagResult = observer(({ appState, tagResult, closeOnBlu appState.noteTags.setAutocompleteInputFocused(true) } - const onKeyDown = (event: KeyboardEvent) => { + const onKeyDown: KeyboardEventHandler = (event) => { const tagResultIndex = appState.noteTags.getTagIndex(tagResult, autocompleteTagResults) switch (event.key) { case 'ArrowUp': @@ -54,7 +54,7 @@ export const AutocompleteTagResult = observer(({ appState, tagResult, closeOnBlu appState.noteTags.setFocusedTagResultUuid(tagResult.uuid) } - const onBlur = (event: FocusEvent) => { + const onBlur: FocusEventHandler = (event) => { closeOnBlur(event) appState.noteTags.setFocusedTagResultUuid(undefined) } @@ -97,4 +97,6 @@ export const AutocompleteTagResult = observer(({ appState, tagResult, closeOnBlu ) -}) +} + +export default observer(AutocompleteTagResult) diff --git a/app/assets/javascripts/Components/Tags/RootTagDropZone.tsx b/app/assets/javascripts/Components/Tags/RootTagDropZone.tsx index a1c43c378..bcefc5b66 100644 --- a/app/assets/javascripts/Components/Tags/RootTagDropZone.tsx +++ b/app/assets/javascripts/Components/Tags/RootTagDropZone.tsx @@ -1,8 +1,9 @@ -import { Icon } from '@/Components/Icon/Icon' +import Icon from '@/Components/Icon/Icon' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { FeaturesState } from '@/UIModels/AppState/FeaturesState' import { TagsState } from '@/UIModels/AppState/TagsState' import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'react' import { useDrop } from 'react-dnd' import { DropItem, DropProps, ItemTypes } from './DragNDrop' @@ -11,7 +12,7 @@ type Props = { featuresState: FeaturesState } -export const RootTagDropZone: React.FC = observer(({ tagsState }) => { +const RootTagDropZone: FunctionComponent = ({ tagsState }) => { const premiumModal = usePremiumModal() const [{ isOver, canDrop }, dropRef] = useDrop( @@ -40,4 +41,6 @@ export const RootTagDropZone: React.FC = observer(({ tagsState }) => {

    ) -}) +} + +export default observer(RootTagDropZone) diff --git a/app/assets/javascripts/Components/Tags/SmartViewsList.tsx b/app/assets/javascripts/Components/Tags/SmartViewsList.tsx index 381dadba5..d524fdf9f 100644 --- a/app/assets/javascripts/Components/Tags/SmartViewsList.tsx +++ b/app/assets/javascripts/Components/Tags/SmartViewsList.tsx @@ -1,18 +1,13 @@ 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' +import { FunctionComponent } from 'react' +import SmartViewsListItem from './SmartViewsListItem' type Props = { appState: AppState } -export const SmartViewsList: FunctionComponent = observer(({ appState }: Props) => { - if (isStateDealloced(appState)) { - return null - } - +const SmartViewsList: FunctionComponent = ({ appState }: Props) => { const allViews = appState.tags.smartViews return ( @@ -22,4 +17,6 @@ export const SmartViewsList: FunctionComponent = observer(({ appState }: })} ) -}) +} + +export default observer(SmartViewsList) diff --git a/app/assets/javascripts/Components/Tags/SmartViewsListItem.tsx b/app/assets/javascripts/Components/Tags/SmartViewsListItem.tsx index 3d2f2a1ed..0c0feea47 100644 --- a/app/assets/javascripts/Components/Tags/SmartViewsListItem.tsx +++ b/app/assets/javascripts/Components/Tags/SmartViewsListItem.tsx @@ -1,11 +1,18 @@ -import { Icon } from '@/Components/Icon/Icon' +import Icon from '@/Components/Icon/Icon' import { FeaturesState } from '@/UIModels/AppState/FeaturesState' import { TagsState } from '@/UIModels/AppState/TagsState' import '@reach/tooltip/styles.css' import { SmartView, SystemViewId, IconType, isSystemView } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { + FormEventHandler, + FunctionComponent, + KeyboardEventHandler, + useCallback, + useEffect, + useRef, + useState, +} from 'react' type Props = { view: SmartView @@ -36,7 +43,7 @@ const smartViewIconType = (view: SmartView): IconType => { return 'hashtag' } -export const SmartViewsListItem: FunctionComponent = observer(({ view, tagsState }) => { +const SmartViewsListItem: FunctionComponent = ({ view, tagsState }) => { const [title, setTitle] = useState(view.title || '') const inputRef = useRef(null) @@ -57,16 +64,16 @@ export const SmartViewsListItem: FunctionComponent = observer(({ view, ta setTitle(view.title) }, [tagsState, view, title, setTitle]) - const onInput = useCallback( - (e: Event) => { + const onInput: FormEventHandler = useCallback( + (e) => { const value = (e.target as HTMLInputElement).value setTitle(value) }, [setTitle], ) - const onKeyUp = useCallback( - (e: KeyboardEvent) => { + const onKeyUp: KeyboardEventHandler = useCallback( + (e) => { if (e.code === 'Enter') { inputRef.current?.blur() e.preventDefault() @@ -149,4 +156,6 @@ export const SmartViewsListItem: FunctionComponent = observer(({ view, ta
    ) -}) +} + +export default observer(SmartViewsListItem) diff --git a/app/assets/javascripts/Components/Tags/SmartViewsSection.tsx b/app/assets/javascripts/Components/Tags/SmartViewsSection.tsx index 872056e0f..60c2e3a44 100644 --- a/app/assets/javascripts/Components/Tags/SmartViewsSection.tsx +++ b/app/assets/javascripts/Components/Tags/SmartViewsSection.tsx @@ -1,16 +1,18 @@ import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { SmartViewsList } from './SmartViewsList' +import { FunctionComponent } from 'react' +import SmartViewsList from './SmartViewsList' type Props = { appState: AppState } -export const SmartViewsSection: FunctionComponent = observer(({ appState }) => { +const SmartViewsSection: FunctionComponent = ({ appState }) => { return (
    ) -}) +} + +export default observer(SmartViewsSection) diff --git a/app/assets/javascripts/Components/Tags/TagContextMenu.tsx b/app/assets/javascripts/Components/Tags/TagContextMenu.tsx index fc7f6b1f1..02804f9f7 100644 --- a/app/assets/javascripts/Components/Tags/TagContextMenu.tsx +++ b/app/assets/javascripts/Components/Tags/TagContextMenu.tsx @@ -1,30 +1,25 @@ import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useRef } from 'preact/hooks' -import { Icon } from '@/Components/Icon/Icon' -import { Menu } from '@/Components/Menu/Menu' -import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem' +import { useCallback, useEffect, useRef } from 'react' +import Icon from '@/Components/Icon/Icon' +import Menu from '@/Components/Menu/Menu' +import MenuItem from '@/Components/Menu/MenuItem' +import { MenuItemType } from '@/Components/Menu/MenuItemType' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { SNTag } from '@standardnotes/snjs' import { isStateDealloced } from '@/UIModels/AppState/AbstractState' -type Props = { +type WrapperProps = { appState: AppState } -export const TagsContextMenu: FunctionComponent = observer(({ appState }: Props) => { - if (isStateDealloced(appState)) { - return null - } +type ContextMenuProps = WrapperProps & { + selectedTag: SNTag +} +const TagsContextMenu = observer(({ appState, selectedTag }: ContextMenuProps) => { const premiumModal = usePremiumModal() - const selectedTag = appState.tags.selected - - if (!selectedTag || !(selectedTag instanceof SNTag)) { - return null - } const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = appState.tags @@ -101,3 +96,21 @@ export const TagsContextMenu: FunctionComponent = observer(({ appState }:
    ) : null }) + +TagsContextMenu.displayName = 'TagsContextMenu' + +const TagsContextMenuWrapper = ({ appState }: WrapperProps) => { + if (isStateDealloced(appState)) { + return null + } + + const selectedTag = appState.tags.selected + + if (!selectedTag || !(selectedTag instanceof SNTag)) { + return null + } + + return +} + +export default observer(TagsContextMenuWrapper) diff --git a/app/assets/javascripts/Components/Tags/TagsList.tsx b/app/assets/javascripts/Components/Tags/TagsList.tsx index 2bb8db491..3dca17030 100644 --- a/app/assets/javascripts/Components/Tags/TagsList.tsx +++ b/app/assets/javascripts/Components/Tags/TagsList.tsx @@ -1,25 +1,19 @@ 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 { FunctionComponent, useCallback } from 'react' import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' import { TouchBackend } from 'react-dnd-touch-backend' -import { RootTagDropZone } from './RootTagDropZone' +import RootTagDropZone from './RootTagDropZone' import { TagsListItem } from './TagsListItem' type Props = { appState: AppState } -export const TagsList: FunctionComponent = observer(({ appState }: Props) => { - if (isStateDealloced(appState)) { - return null - } - +const TagsList: FunctionComponent = ({ appState }: Props) => { const tagsState = appState.tags const allTags = tagsState.allLocalRootTags @@ -68,4 +62,6 @@ export const TagsList: FunctionComponent = observer(({ appState }: Props) )} ) -}) +} + +export default observer(TagsList) diff --git a/app/assets/javascripts/Components/Tags/TagsListItem.tsx b/app/assets/javascripts/Components/Tags/TagsListItem.tsx index 40db65e0c..fd4bd92f4 100644 --- a/app/assets/javascripts/Components/Tags/TagsListItem.tsx +++ b/app/assets/javascripts/Components/Tags/TagsListItem.tsx @@ -1,4 +1,4 @@ -import { Icon } from '@/Components/Icon/Icon' +import Icon from '@/Components/Icon/Icon' import { TAG_FOLDERS_FEATURE_NAME } from '@/Constants' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { KeyboardKey } from '@/Services/IOService' @@ -8,8 +8,16 @@ import '@reach/tooltip/styles.css' import { SNTag } from '@standardnotes/snjs' import { computed } from 'mobx' import { observer } from 'mobx-react-lite' -import { FunctionComponent, JSX } from 'preact' -import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { + FormEventHandler, + FunctionComponent, + KeyboardEventHandler, + MouseEventHandler, + useCallback, + useEffect, + useRef, + useState, +} from 'react' import { useDrag, useDrop } from 'react-dnd' import { DropItem, DropProps, ItemTypes } from './DragNDrop' @@ -29,7 +37,7 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features, const [subtagTitle, setSubtagTitle] = useState('') const inputRef = useRef(null) const subtagInputRef = useRef(null) - const menuButtonRef = useRef(null) + const menuButtonRef = useRef(null) const isSelected = tagsState.selected === tag const isEditing = tagsState.editingTag === tag @@ -58,8 +66,8 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features, setTitle(tag.title || '') }, [setTitle, tag]) - const toggleChildren = useCallback( - (e: MouseEvent) => { + const toggleChildren: MouseEventHandler = useCallback( + (e) => { e.stopPropagation() setShowChildren((x) => { tagsState.setExpanded(tag, !x) @@ -78,16 +86,16 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features, setTitle(tag.title) }, [tagsState, tag, title, setTitle]) - const onInput = useCallback( - (e: JSX.TargetedEvent) => { + const onInput: FormEventHandler = useCallback( + (e) => { const value = (e.target as HTMLInputElement).value setTitle(value) }, [setTitle], ) - const onKeyDown = useCallback( - (e: KeyboardEvent) => { + const onKeyDown: KeyboardEventHandler = useCallback( + (e) => { if (e.key === KeyboardKey.Enter) { inputRef.current?.blur() e.preventDefault() @@ -102,7 +110,7 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features, } }, [inputRef, isEditing]) - const onSubtagInput = useCallback((e: JSX.TargetedEvent) => { + const onSubtagInput = useCallback((e) => { const value = (e.target as HTMLInputElement).value setSubtagTitle(value) }, []) @@ -112,8 +120,8 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features, setSubtagTitle('') }, [subtagTitle, tag, tagsState]) - const onSubtagKeyDown = useCallback( - (e: KeyboardEvent) => { + const onSubtagKeyDown: KeyboardEventHandler = useCallback( + (e) => { if (e.key === KeyboardKey.Enter) { e.preventDefault() subtagInputRef.current?.blur() @@ -189,7 +197,7 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features, style={{ paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`, }} - onContextMenu={(e: MouseEvent) => { + onContextMenu={(e) => { e.preventDefault() onContextMenu(tag, e.clientX, e.clientY) }} @@ -197,14 +205,15 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features,
    {hasAtLeastOneFolder && (
    - +
    )}
    @@ -222,7 +231,8 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features, ref={inputRef} />
    - +
    {noteCounts.get()}
    @@ -282,3 +292,5 @@ export const TagsListItem: FunctionComponent = observer(({ tag, features, ) }) + +TagsListItem.displayName = 'TagsListItem' diff --git a/app/assets/javascripts/Components/Tags/TagsSection.tsx b/app/assets/javascripts/Components/Tags/TagsSection.tsx index 5d3b158bd..acf67966c 100644 --- a/app/assets/javascripts/Components/Tags/TagsSection.tsx +++ b/app/assets/javascripts/Components/Tags/TagsSection.tsx @@ -1,17 +1,16 @@ -import { TagsList } from '@/Components/Tags/TagsList' +import TagsList from '@/Components/Tags/TagsList' import { AppState } from '@/UIModels/AppState' import { ApplicationEvent } from '@/__mocks__/@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback, useEffect, useState } from 'preact/hooks' -import { TagsSectionAddButton } from './TagsSectionAddButton' -import { TagsSectionTitle } from './TagsSectionTitle' +import { FunctionComponent, useCallback, useEffect, useState } from 'react' +import TagsSectionAddButton from './TagsSectionAddButton' +import TagsSectionTitle from './TagsSectionTitle' type Props = { appState: AppState } -export const TagsSection: FunctionComponent = observer(({ appState }) => { +const TagsSection: FunctionComponent = ({ appState }) => { const [hasMigration, setHasMigration] = useState(false) const checkIfMigrationNeeded = useCallback(() => { @@ -63,4 +62,6 @@ export const TagsSection: FunctionComponent = observer(({ appState }) => ) -}) +} + +export default observer(TagsSection) diff --git a/app/assets/javascripts/Components/Tags/TagsSectionAddButton.tsx b/app/assets/javascripts/Components/Tags/TagsSectionAddButton.tsx index 01a60517d..1b54363b0 100644 --- a/app/assets/javascripts/Components/Tags/TagsSectionAddButton.tsx +++ b/app/assets/javascripts/Components/Tags/TagsSectionAddButton.tsx @@ -1,15 +1,15 @@ -import { IconButton } from '@/Components/Button/IconButton' +import IconButton from '@/Components/Button/IconButton' import { FeaturesState } from '@/UIModels/AppState/FeaturesState' import { TagsState } from '@/UIModels/AppState/TagsState' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' type Props = { tags: TagsState features: FeaturesState } -export const TagsSectionAddButton: FunctionComponent = observer(({ tags }) => { +const TagsSectionAddButton: FunctionComponent = ({ tags }) => { return ( = observer(({ tags } onClick={() => tags.createNewTemplate()} /> ) -}) +} + +export default observer(TagsSectionAddButton) diff --git a/app/assets/javascripts/Components/Tags/TagsSectionTitle.tsx b/app/assets/javascripts/Components/Tags/TagsSectionTitle.tsx index 8dbaec464..211ed9d53 100644 --- a/app/assets/javascripts/Components/Tags/TagsSectionTitle.tsx +++ b/app/assets/javascripts/Components/Tags/TagsSectionTitle.tsx @@ -3,8 +3,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal' import { FeaturesState } from '@/UIModels/AppState/FeaturesState' import { Tooltip } from '@reach/tooltip' import { observer } from 'mobx-react-lite' -import { FunctionComponent } from 'preact' -import { useCallback } from 'preact/hooks' +import { FunctionComponent, useCallback } from 'react' type Props = { features: FeaturesState @@ -12,7 +11,7 @@ type Props = { onClickMigration: () => void } -export const TagsSectionTitle: FunctionComponent = observer(({ features, hasMigration, onClickMigration }) => { +const TagsSectionTitle: FunctionComponent = ({ features, hasMigration, onClickMigration }) => { const entitledToFolders = features.hasFolders const modal = usePremiumModal() @@ -47,4 +46,6 @@ export const TagsSectionTitle: FunctionComponent = observer(({ features,
    ) -}) +} + +export default observer(TagsSectionTitle) diff --git a/app/assets/javascripts/Components/TitleBar/Title.tsx b/app/assets/javascripts/Components/TitleBar/Title.tsx index 178766141..c6c5b09d1 100644 --- a/app/assets/javascripts/Components/TitleBar/Title.tsx +++ b/app/assets/javascripts/Components/TitleBar/Title.tsx @@ -1,5 +1,11 @@ -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' -export const Title: FunctionComponent<{ className?: string }> = ({ children, className }) => { +type Props = { + className?: string +} + +const Title: FunctionComponent = ({ children, className }) => { return
    {children}
    } + +export default Title diff --git a/app/assets/javascripts/Components/TitleBar/TitleBar.tsx b/app/assets/javascripts/Components/TitleBar/TitleBar.tsx index 725513eab..55a6adb5b 100644 --- a/app/assets/javascripts/Components/TitleBar/TitleBar.tsx +++ b/app/assets/javascripts/Components/TitleBar/TitleBar.tsx @@ -1,5 +1,11 @@ -import { FunctionComponent } from 'preact' +import { FunctionComponent } from 'react' -export const TitleBar: FunctionComponent<{ className?: string }> = ({ children, className }) => ( +type Props = { + className?: string +} + +const TitleBar: FunctionComponent = ({ children, className }) => (
    {children}
    ) + +export default TitleBar diff --git a/app/assets/javascripts/Hooks/useBeforeUnload.tsx b/app/assets/javascripts/Hooks/useBeforeUnload.tsx index 1fcc2391e..32b722354 100644 --- a/app/assets/javascripts/Hooks/useBeforeUnload.tsx +++ b/app/assets/javascripts/Hooks/useBeforeUnload.tsx @@ -1,4 +1,4 @@ -import { useEffect } from '@node_modules/preact/hooks' +import { useEffect } from 'react' export const useBeforeUnload = (): void => { useEffect(() => { diff --git a/app/assets/javascripts/Hooks/useCloseOnBlur.ts b/app/assets/javascripts/Hooks/useCloseOnBlur.ts index 0396859c5..80da10296 100644 --- a/app/assets/javascripts/Hooks/useCloseOnBlur.ts +++ b/app/assets/javascripts/Hooks/useCloseOnBlur.ts @@ -1,4 +1,4 @@ -import { StateUpdater, useCallback, useState } from 'preact/hooks' +import { Dispatch, SetStateAction, useCallback, useState } from 'react' /** * @returns a callback that will close a dropdown if none of its children has @@ -8,7 +8,7 @@ import { StateUpdater, useCallback, useState } from 'preact/hooks' export function useCloseOnBlur( container: { current?: HTMLDivElement | null }, setOpen: (open: boolean) => void, -): [(event: { relatedTarget: EventTarget | null }) => void, StateUpdater] { +): [(event: { relatedTarget: EventTarget | null }) => void, Dispatch>] { const [locked, setLocked] = useState(false) return [ useCallback( diff --git a/app/assets/javascripts/Hooks/useCloseOnClickOutside.ts b/app/assets/javascripts/Hooks/useCloseOnClickOutside.ts index b9b86c562..261e6f455 100644 --- a/app/assets/javascripts/Hooks/useCloseOnClickOutside.ts +++ b/app/assets/javascripts/Hooks/useCloseOnClickOutside.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'preact/hooks' +import { useCallback, useEffect } from 'react' export function useCloseOnClickOutside(container: { current: HTMLDivElement | null }, callback: () => void): void { const closeOnClickOutside = useCallback( diff --git a/app/assets/javascripts/Hooks/useListKeyboardNavigation.ts b/app/assets/javascripts/Hooks/useListKeyboardNavigation.ts index 7f9945b3c..491437215 100644 --- a/app/assets/javascripts/Hooks/useListKeyboardNavigation.ts +++ b/app/assets/javascripts/Hooks/useListKeyboardNavigation.ts @@ -1,8 +1,8 @@ import { KeyboardKey } from '@/Services/IOService' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' -import { useCallback, useState, useEffect, Ref } from 'preact/hooks' +import { useCallback, useState, useEffect, RefObject } from 'react' -export const useListKeyboardNavigation = (container: Ref, initialFocus = 0) => { +export const useListKeyboardNavigation = (container: RefObject, initialFocus = 0) => { const [listItems, setListItems] = useState() const [focusedItemIndex, setFocusedItemIndex] = useState(initialFocus) diff --git a/app/assets/javascripts/Hooks/usePremiumModal.tsx b/app/assets/javascripts/Hooks/usePremiumModal.tsx index 6a0405499..8e170a800 100644 --- a/app/assets/javascripts/Hooks/usePremiumModal.tsx +++ b/app/assets/javascripts/Hooks/usePremiumModal.tsx @@ -1,10 +1,8 @@ import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' import { observer } from 'mobx-react-lite' -import { ComponentChildren, FunctionalComponent, createContext } from 'preact' -import { useCallback, useContext } from 'preact/hooks' - -import { PremiumFeaturesModal } from '@/Components/PremiumFeaturesModal/PremiumFeaturesModal' +import { FunctionComponent, createContext, useCallback, useContext, ReactNode } from 'react' +import PremiumFeaturesModal from '@/Components/PremiumFeaturesModal/PremiumFeaturesModal' type PremiumModalContextData = { activate: (featureName: string) => void @@ -27,50 +25,51 @@ export const usePremiumModal = (): PremiumModalContextData => { interface Props { application: WebApplication appState: AppState - children: ComponentChildren | ComponentChildren[] + children: ReactNode } -export const PremiumModalProvider: FunctionalComponent = observer( - ({ application, appState, children }: Props) => { - const dealloced = !appState || appState.dealloced == undefined - if (dealloced) { - return null - } +const PremiumModalProvider: FunctionComponent = observer(({ application, appState, children }: Props) => { + const featureName = appState.features.premiumAlertFeatureName || '' - const featureName = appState.features.premiumAlertFeatureName || '' + const showModal = !!featureName - const showModal = !!featureName + const hasSubscription = Boolean( + appState.subscription.userSubscription && + !appState.subscription.isUserSubscriptionExpired && + !appState.subscription.isUserSubscriptionCanceled, + ) - const hasSubscription = Boolean( - appState.subscription.userSubscription && - !appState.subscription.isUserSubscriptionExpired && - !appState.subscription.isUserSubscriptionCanceled, - ) + const activate = useCallback( + (feature: string) => { + appState.features.showPremiumAlert(feature).catch(console.error) + }, + [appState], + ) - const activate = useCallback( - (feature: string) => { - appState.features.showPremiumAlert(feature).catch(console.error) - }, - [appState], - ) + const close = useCallback(() => { + appState.features.closePremiumAlert() + }, [appState]) - const close = useCallback(() => { - appState.features.closePremiumAlert() - }, [appState]) + return ( + <> + {showModal && ( + + )} + {children} + + ) +}) - return ( - <> - {showModal && ( - - )} - {children} - - ) - }, -) +PremiumModalProvider.displayName = 'PremiumModalProvider' + +const PremiumModalProviderWithDeallocateHandling: FunctionComponent = ({ application, appState, children }) => { + return +} + +export default observer(PremiumModalProviderWithDeallocateHandling) diff --git a/app/assets/javascripts/UIModels/Application.ts b/app/assets/javascripts/UIModels/Application.ts index c3308e24e..33d24c6cb 100644 --- a/app/assets/javascripts/UIModels/Application.ts +++ b/app/assets/javascripts/UIModels/Application.ts @@ -18,6 +18,7 @@ import { isDesktopDevice, DeinitMode, } from '@standardnotes/snjs' +import { makeObservable, observable } from 'mobx' type WebServices = { appState: AppState @@ -62,6 +63,10 @@ export class WebApplication extends SNApplication { supportsFileNavigation: window.enabledUnfinishedFeatures, }) + makeObservable(this, { + dealloced: observable, + }) + deviceInterface.setApplication(this) this.noteControllerGroup = new NoteGroupController(this) this.iconsController = new IconsController() diff --git a/app/assets/javascripts/Utils/FormatLastSyncDate.ts b/app/assets/javascripts/Utils/FormatLastSyncDate.ts new file mode 100644 index 000000000..0d10d18c4 --- /dev/null +++ b/app/assets/javascripts/Utils/FormatLastSyncDate.ts @@ -0,0 +1,5 @@ +import { dateToLocalizedString } from '@standardnotes/snjs/' + +export const formatLastSyncDate = (lastUpdatedDate: Date) => { + return dateToLocalizedString(lastUpdatedDate) +} diff --git a/app/assets/javascripts/Utils/PreactUtils.ts b/app/assets/javascripts/Utils/PreactUtils.ts deleted file mode 100644 index 20bf725be..000000000 --- a/app/assets/javascripts/Utils/PreactUtils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { render } from 'preact' - -/** - * Source: https://stackoverflow.com/questions/50946950/how-to-destroy-root-preact-node - * For some reason importing `import { unmountComponentAtNode } from 'preact/compat'` inside of app/index.tsx - * results in the app failing to compile. - */ -export function unmountComponentAtRoot(root: HTMLElement) { - render(null, root) -} diff --git a/app/assets/javascripts/jest.config.js b/app/assets/javascripts/jest.config.js index f7dfda989..9125ac632 100644 --- a/app/assets/javascripts/jest.config.js +++ b/app/assets/javascripts/jest.config.js @@ -11,8 +11,6 @@ module.exports = { ...pathsToModuleNameMapper(pathsFromTsconfig, { prefix: '', }), - '^react$': ['preact/compat'], - '^react-dom$': 'preact', '\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, globals: { diff --git a/app/assets/javascripts/tsconfig.json b/app/assets/javascripts/tsconfig.json index 8a5ac4bce..555d51e32 100644 --- a/app/assets/javascripts/tsconfig.json +++ b/app/assets/javascripts/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../../node_modules/@standardnotes/config/src/tsconfig.json", "compilerOptions": { + "skipLibCheck": true, "target": "ES2019", "module": "commonjs", "moduleResolution": "node", @@ -15,15 +16,29 @@ "declarationDir": "../../../dist/@types", "baseUrl": ".", "jsx": "react-jsx", - "jsxImportSource": "preact", - "typeRoots": ["./@types", "../../../node_modules/@types"], + "typeRoots": [ + "./@types", + "../../../node_modules/@types" + ], "paths": { - "%/*": ["../templates/*"], - "@/*": ["./*"], - "@Controllers/*": ["./controllers/*"], - "@Views/*": ["./views/*"], - "@Services/*": ["Services/*"], - "@node_modules/*": ["../../../node_modules/*"] + "%/*": [ + "../templates/*" + ], + "@/*": [ + "./*" + ], + "@Controllers/*": [ + "./controllers/*" + ], + "@Views/*": [ + "./views/*" + ], + "@Services/*": [ + "Services/*" + ], + "@node_modules/*": [ + "../../../node_modules/*" + ] } } } \ No newline at end of file diff --git a/package.json b/package.json index 590f6d6ef..fad70660c 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@standardnotes/config": "^2.4.3", "@types/jest": "^27.4.1", "@types/react": "^17.0.42", + "@types/react-dom": "^18.0.5", "@types/wicg-file-system-access": "^2020.9.5", "@typescript-eslint/eslint-plugin": "^5.20.0", "@typescript-eslint/parser": "^5.20.0", @@ -74,16 +75,17 @@ "@standardnotes/icons": "^1.1.8", "@standardnotes/services": "^1.13.3", "@standardnotes/sncrypto-web": "1.10.1", - "@standardnotes/snjs": "^2.114.2", + "@standardnotes/snjs": "^2.114.5", "@standardnotes/stylekit": "5.29.2", "@zip.js/zip.js": "^2.4.10", "mobx": "^6.5.0", "mobx-react-lite": "^3.3.0", - "preact": "^10.7.1", "qrcode.react": "^3.0.1", + "react": "^18.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", - "react-dnd-touch-backend": "^16.0.1" + "react-dnd-touch-backend": "^16.0.1", + "react-dom": "^18.1.0" }, "lint-staged": { "app/**/*.{js,ts,jsx,tsx}": "eslint --cache --fix", diff --git a/webpack.config.js b/webpack.config.js index 12185cdf3..52a1622d3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -50,9 +50,6 @@ module.exports = (env) => { '@Views': path.resolve(__dirname, 'app/assets/javascripts/views'), '@Services': path.resolve(__dirname, 'app/assets/javascripts/services'), '@node_modules': path.resolve(__dirname, 'node_modules'), - react: 'preact/compat', - 'react-dom/test-utils': 'preact/test-utils', - 'react-dom': 'preact/compat', }, }, module: { diff --git a/yarn.lock b/yarn.lock index 9de3df1c2..3b8b4d600 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2358,6 +2358,14 @@ "@standardnotes/common" "^1.22.0" jsonwebtoken "^8.5.1" +"@standardnotes/auth@^3.19.1": + version "3.19.1" + resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.19.1.tgz#09b1bf2a7c22bfb351b3f5af5a193566bfe4410c" + integrity sha512-ozzCan/uSdN8oQ2YSid5kVGts+13f63eKMyuiGesS1a/l0ezKWJHaFa3ulXETobhjJEF60yNQIjQ7sfdv+4L9A== + dependencies: + "@standardnotes/common" "^1.22.0" + jsonwebtoken "^8.5.1" + "@standardnotes/common@^1.22.0": version "1.22.0" resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.22.0.tgz#397604fb4b92901bac276940a2647509b70a7ad2" @@ -2379,22 +2387,22 @@ eslint-plugin-prettier "^4.0.0" prettier "^2.6.2" -"@standardnotes/domain-events@^2.28.13": - version "2.28.13" - resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.28.13.tgz#ee8434544ad175e6bfd7e9e59d6fea148dcaee1e" - integrity sha512-gKYfo1Yv0TKcG1CZJtjg2my3NyLddfWjdniPXCyFxZ7kHVMa2wGUjoxNpds8WQwSk9ktSlQV/8b7hv1Cjb0eXw== +"@standardnotes/domain-events@^2.28.14": + version "2.28.14" + resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.28.14.tgz#58de063de3ba93d016bdb7169b978d43b9c43df8" + integrity sha512-rMGQie66EK09AqRmxXOcVQSILt0TXdOGeVTRkKjZzGpAM+Y8YpOfG0/WOkBsB4pyMWBrSJvMA1aQtl7buslDrw== dependencies: - "@standardnotes/auth" "^3.19.0" - "@standardnotes/features" "^1.44.4" + "@standardnotes/auth" "^3.19.1" + "@standardnotes/features" "^1.44.5" -"@standardnotes/encryption@^1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.8.3.tgz#ae46c995f034e1b5f3fa04672fd65e37c09037f5" - integrity sha512-oPaUakSQtOyEVrJxz/O4CQNnQYDeIZN9FBjzoIsrdg1rpl7to+5lUYo0vufc4fVLXCXVKg3UQiS9pYotIEo7og== +"@standardnotes/encryption@^1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.8.5.tgz#48ea926e9c707e9d27e4c7698c634b5bfa85d662" + integrity sha512-Ih6HCnreQNkR8YTmcem2PvKtxTUadDYx20Tp4UF7TSpeaUL4ETLipamJ6aqYasKYeuf4uuoiQb7LLYiad37NyQ== dependencies: - "@standardnotes/models" "^1.10.2" - "@standardnotes/responses" "^1.6.27" - "@standardnotes/services" "^1.13.3" + "@standardnotes/models" "^1.11.0" + "@standardnotes/responses" "^1.6.28" + "@standardnotes/services" "^1.13.5" "@standardnotes/features@^1.44.4": version "1.44.4" @@ -2404,7 +2412,15 @@ "@standardnotes/auth" "^3.19.0" "@standardnotes/common" "^1.22.0" -"@standardnotes/filepicker@1.16.2", "@standardnotes/filepicker@^1.16.2": +"@standardnotes/features@^1.44.5": + version "1.44.5" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.44.5.tgz#c1dd2d8b46387765c7bef76ee916b0698c1153b5" + integrity sha512-yMh9W6KP/HfqEJEGh469Vh6PlH/U0GSif4amt5paBIdhvZt2Rmqv81wt+ac9kPp7HkDxZhr8+Ly68OVznCeVjw== + dependencies: + "@standardnotes/auth" "^3.19.1" + "@standardnotes/common" "^1.22.0" + +"@standardnotes/filepicker@1.16.2": version "1.16.2" resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.16.2.tgz#d6fda94b5578f30e6b4f792c874e3eb11ce58453" integrity sha512-B290nRn2aJzVX1VXMTHhu66tbj1oZFaksTSLQxiEWeLQ76AFAQ3/5frVH/6Y8ahIvBn3XYyotOOyXeL9HkBu8g== @@ -2413,16 +2429,25 @@ "@standardnotes/services" "^1.13.3" "@standardnotes/utils" "^1.6.10" -"@standardnotes/files@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@standardnotes/files/-/files-1.3.2.tgz#efa676cd80a3178300f5e2f62c0fb9b21a625ec5" - integrity sha512-53jGUeYwS/EcIcJAXms+4bPAYVp6sI1kZmdRkS1+3T7+lsBB7wLth3wKIsNEmGLXHzOR0JwsjHND4VQABymUcA== +"@standardnotes/filepicker@^1.16.4": + version "1.16.4" + resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.16.4.tgz#742023dab908c477e28bf472183cae8309639d95" + integrity sha512-a8RHKtLgTG3WmgXvRafMNj5utGKaIZvgQhqyRfNt0EuIK1uSvdvt3jLA8VGeehP1JE0gc3XfZKCmE7vgNz8wDg== dependencies: - "@standardnotes/encryption" "^1.8.3" - "@standardnotes/filepicker" "^1.16.2" - "@standardnotes/models" "^1.10.2" - "@standardnotes/responses" "^1.6.27" - "@standardnotes/services" "^1.13.3" + "@standardnotes/common" "^1.22.0" + "@standardnotes/services" "^1.13.5" + "@standardnotes/utils" "^1.6.10" + +"@standardnotes/files@^1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@standardnotes/files/-/files-1.3.4.tgz#0b8c820b2aad2d24d5b837935999979e09a2a6bb" + integrity sha512-kSb8jhvnnLX43FlCL3JzMZxT3KjI+9I37DyZi9FR/s+8gQ6TyuCtk4JjCZxn1i+cRbdKyzeVPi2T24cvQEVs/g== + dependencies: + "@standardnotes/encryption" "^1.8.5" + "@standardnotes/filepicker" "^1.16.4" + "@standardnotes/models" "^1.11.0" + "@standardnotes/responses" "^1.6.28" + "@standardnotes/services" "^1.13.5" "@standardnotes/utils" "^1.6.10" "@standardnotes/icons@^1.1.8": @@ -2439,6 +2464,15 @@ "@standardnotes/responses" "^1.6.27" "@standardnotes/utils" "^1.6.10" +"@standardnotes/models@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@standardnotes/models/-/models-1.11.0.tgz#35647d0f8b1bb0a431298bd040c91343f4ffc2b1" + integrity sha512-5T4WBQeNfWpX9F5NUhI68ah5nAk+0+Umz3YphRjpa8MrmDiA32VH37LG43+Tp17vSxQeG2shVAduC15QmGch3g== + dependencies: + "@standardnotes/features" "^1.44.5" + "@standardnotes/responses" "^1.6.28" + "@standardnotes/utils" "^1.6.10" + "@standardnotes/responses@^1.6.27": version "1.6.27" resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.27.tgz#3a440090e5cee09f2980df5cc57a60f76adff735" @@ -2448,6 +2482,15 @@ "@standardnotes/common" "^1.22.0" "@standardnotes/features" "^1.44.4" +"@standardnotes/responses@^1.6.28": + version "1.6.28" + resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.28.tgz#846e56968d104bac360aa9080627123326f70af9" + integrity sha512-oaxoH1Qw9VrIF7Fgak5NGayGDQZl5lOuuxyXh8ngCOP7Qpgrr/5whm5ZnKTMYwD8kE9/Vi22G9oxVTz1JOErkA== + dependencies: + "@standardnotes/auth" "^3.19.1" + "@standardnotes/common" "^1.22.0" + "@standardnotes/features" "^1.44.5" + "@standardnotes/services@^1.13.3": version "1.13.3" resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.13.3.tgz#b857a6ed42b15d40c2e9d08b41739f5fb6b0d3e0" @@ -2459,6 +2502,17 @@ "@standardnotes/responses" "^1.6.27" "@standardnotes/utils" "^1.6.10" +"@standardnotes/services@^1.13.5": + version "1.13.5" + resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.13.5.tgz#644533f1a30b6d7459c295152c222a725c9a4f33" + integrity sha512-YDG8JgCI29Sa3s8f90rxeuakG7/MDuKSM8OytKKl16WDioKcKEysPlqo4blmj3ntK5mTR7aV/CGh4J+gNak+jw== + dependencies: + "@standardnotes/auth" "^3.19.1" + "@standardnotes/common" "^1.22.0" + "@standardnotes/models" "^1.11.0" + "@standardnotes/responses" "^1.6.28" + "@standardnotes/utils" "^1.6.10" + "@standardnotes/settings@^1.14.3": version "1.14.3" resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.14.3.tgz#021085e8c383a9893a2c49daa74cc0754ccd67b5" @@ -2478,21 +2532,21 @@ buffer "^6.0.3" libsodium-wrappers "^0.7.9" -"@standardnotes/snjs@^2.114.2": - version "2.114.2" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.114.2.tgz#c1560c538127cff6b008348cef24ff1767d117ba" - integrity sha512-0qAzkfQDe1yeO+la4wNxzQI2N7F7RaFBpQrVCwkBxnq3s+DDXK+BnmUWxDNL54cchxLCTdqhayIoDFmsG0j8xg== +"@standardnotes/snjs@^2.114.5": + version "2.114.5" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.114.5.tgz#1fee7e7c7558d591cfa99bc47a514eb08c67357d" + integrity sha512-SEriP3YsKoIn8idPbKNUnTocQtLt8jXngZ1O1DQrC0fL/aUxHeaVUinSjYyr8WXQqQatoA2Jngcouv3zpvmAkQ== dependencies: - "@standardnotes/auth" "^3.19.0" + "@standardnotes/auth" "^3.19.1" "@standardnotes/common" "^1.22.0" - "@standardnotes/domain-events" "^2.28.13" - "@standardnotes/encryption" "^1.8.3" - "@standardnotes/features" "^1.44.4" - "@standardnotes/filepicker" "^1.16.2" - "@standardnotes/files" "^1.3.2" - "@standardnotes/models" "^1.10.2" - "@standardnotes/responses" "^1.6.27" - "@standardnotes/services" "^1.13.3" + "@standardnotes/domain-events" "^2.28.14" + "@standardnotes/encryption" "^1.8.5" + "@standardnotes/features" "^1.44.5" + "@standardnotes/filepicker" "^1.16.4" + "@standardnotes/files" "^1.3.4" + "@standardnotes/models" "^1.11.0" + "@standardnotes/responses" "^1.6.28" + "@standardnotes/services" "^1.13.5" "@standardnotes/settings" "^1.14.3" "@standardnotes/sncrypto-common" "^1.9.0" "@standardnotes/utils" "^1.6.10" @@ -2740,6 +2794,22 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/react-dom@^18.0.5": + version "18.0.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.5.tgz#330b2d472c22f796e5531446939eacef8378444a" + integrity sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "18.0.9" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" + integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/react@^17.0.42": version "17.0.42" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.42.tgz#8242b9219bf8a911c47f248e327206fea3f4ee5a" @@ -7155,7 +7225,7 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" -loose-envify@^1.0.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -8291,11 +8361,6 @@ postcss@^8.4.7: picocolors "^1.0.0" source-map-js "^1.0.2" -preact@^10.7.1: - version "10.7.1" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.7.1.tgz#bdd2b2dce91a5842c3b9b34dfe050e5401068c9e" - integrity sha512-MufnRFz39aIhs9AMFisonjzTud1PK1bY+jcJLo6m2T9Uh8AqjD77w11eAAawmjUogoGOnipECq7e/1RClIKsxg== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -8532,6 +8597,14 @@ react-dnd@^16.0.1: fast-deep-equal "^3.1.3" hoist-non-react-statics "^3.3.2" +react-dom@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f" + integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.22.0" + react-focus-lock@^2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.5.2.tgz#f1e4db5e25cd8789351f2bd5ebe91e9dcb9c2922" @@ -8582,6 +8655,13 @@ react-style-singleton@^2.1.0: invariant "^2.2.4" tslib "^1.0.0" +react@^18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890" + integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ== + dependencies: + loose-envify "^1.1.0" + read-package-json-fast@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz#323ca529630da82cb34b36cc0b996693c98c2b83" @@ -8947,6 +9027,13 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" +scheduler@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8" + integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ== + dependencies: + loose-envify "^1.1.0" + schema-utils@^2.6.5: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"