From dc3dcfba2b6c3ee4b61dd174f6023d0d80ef2b60 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sat, 26 Mar 2022 15:26:36 +0530 Subject: [PATCH 001/102] refactor: DecoratedInput and add DecoratedPasswordInput (#953) --- .../AccountMenu/AdvancedOptions.tsx | 42 +++--- .../AccountMenu/ConfirmPassword.tsx | 29 ++-- .../components/AccountMenu/CreateAccount.tsx | 44 ++---- .../components/AccountMenu/SignIn.tsx | 40 ++--- .../javascripts/components/DecoratedInput.tsx | 140 +++++++++++------- .../components/DecoratedPasswordInput.tsx | 42 ++++++ .../javascripts/components/InputWithIcon.tsx | 91 ------------ .../Preferences/panes/Extensions.tsx | 2 +- .../panes/account/offlineSubscription.tsx | 2 +- .../panes/security-segments/Encryption.tsx | 47 +++--- .../panes/two-factor-auth/SaveSecretKey.tsx | 2 +- .../panes/two-factor-auth/ScanQRCode.tsx | 2 +- .../panes/two-factor-auth/Verification.tsx | 3 +- app/assets/stylesheets/_sn.scss | 4 + 14 files changed, 210 insertions(+), 280 deletions(-) create mode 100644 app/assets/javascripts/components/DecoratedPasswordInput.tsx delete mode 100644 app/assets/javascripts/components/InputWithIcon.tsx diff --git a/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx b/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx index a09335b36..ec67e0ed3 100644 --- a/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx +++ b/app/assets/javascripts/components/AccountMenu/AdvancedOptions.tsx @@ -4,8 +4,8 @@ import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { useEffect, useState } from 'preact/hooks'; import { Checkbox } from '../Checkbox'; +import { DecoratedInput } from '../DecoratedInput'; import { Icon } from '../Icon'; -import { InputWithIcon } from '../InputWithIcon'; type Props = { application: WebApplication; @@ -63,16 +63,12 @@ export const AdvancedOptions: FunctionComponent = observer( setIsVault(!isVault); }; - const handleVaultNameChange = (e: Event) => { - if (e.target instanceof HTMLInputElement) { - setVaultName(e.target.value); - } + const handleVaultNameChange = (name: string) => { + setVaultName(name); }; - const handleVaultUserphraseChange = (e: Event) => { - if (e.target instanceof HTMLInputElement) { - setVaultUserphrase(e.target.value); - } + const handleVaultUserphraseChange = (userphrase: string) => { + setVaultUserphrase(userphrase); }; const handleServerOptionChange = (e: Event) => { @@ -81,11 +77,9 @@ export const AdvancedOptions: FunctionComponent = observer( } }; - const handleSyncServerChange = (e: Event) => { - if (e.target instanceof HTMLInputElement) { - setServer(e.target.value); - application.setCustomHost(e.target.value); - } + const handleSyncServerChange = (server: string) => { + setServer(server); + application.setCustomHost(server); }; const handleStrictSigninChange = () => { @@ -135,19 +129,19 @@ export const AdvancedOptions: FunctionComponent = observer( {appState.enableUnfinishedFeatures && isVault && ( <> - ]} + type="text" placeholder="Vault name" value={vaultName} onChange={handleVaultNameChange} disabled={disabled} /> - ]} + type="text" placeholder="Vault userphrase" value={vaultUserphrase} onChange={handleVaultUserphraseChange} @@ -183,9 +177,9 @@ export const AdvancedOptions: FunctionComponent = observer( onChange={handleServerOptionChange} disabled={disabled} /> - ]} placeholder="https://api.standardnotes.com" value={server} onChange={handleSyncServerChange} diff --git a/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx b/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx index 0a5575cb6..2aad0c0b1 100644 --- a/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx +++ b/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx @@ -7,9 +7,9 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import { AccountMenuPane } from '.'; import { Button } from '../Button'; import { Checkbox } from '../Checkbox'; +import { DecoratedPasswordInput } from '../DecoratedPasswordInput'; +import { Icon } from '../Icon'; import { IconButton } from '../IconButton'; -import { InputWithIcon } from '../InputWithIcon'; -import { AdvancedOptions } from './AdvancedOptions'; type Props = { appState: AppState; @@ -23,7 +23,6 @@ export const ConfirmPassword: FunctionComponent = observer( ({ application, appState, setMenuPane, email, password }) => { const { notesAndTagsCount } = appState.accountMenu; const [confirmPassword, setConfirmPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); const [isRegistering, setIsRegistering] = useState(false); const [isEphemeral, setIsEphemeral] = useState(false); const [shouldMergeLocal, setShouldMergeLocal] = useState(true); @@ -35,10 +34,8 @@ export const ConfirmPassword: FunctionComponent = observer( passwordInputRef.current?.focus(); }, []); - const handlePasswordChange = (e: Event) => { - if (e.target instanceof HTMLInputElement) { - setConfirmPassword(e.target.value); - } + const handlePasswordChange = (text: string) => { + setConfirmPassword(text); }; const handleEphemeralChange = () => { @@ -117,23 +114,15 @@ export const ConfirmPassword: FunctionComponent = observer( your data.
- ]} onChange={handlePasswordChange} onKeyDown={handleKeyDown} - toggle={{ - toggleOnIcon: 'eye-off', - toggleOffIcon: 'eye', - title: 'Show password', - toggled: showPassword, - onClick: setShowPassword, - }} + placeholder="Confirm password" ref={passwordInputRef} - disabled={isRegistering} + value={confirmPassword} /> {error ?
{error}
: null} diff --git a/app/assets/javascripts/components/Files/FilePreviewModal.tsx b/app/assets/javascripts/components/Files/FilePreviewModal.tsx index 92e1128d5..e1d0b4b0b 100644 --- a/app/assets/javascripts/components/Files/FilePreviewModal.tsx +++ b/app/assets/javascripts/components/Files/FilePreviewModal.tsx @@ -111,7 +111,7 @@ export const FilePreviewModal: FunctionComponent = ({
{objectUrl && (
{objectUrl && ( -
- {objectUrl ? ( - getPreviewComponentForFile(file, objectUrl) - ) : isLoadingFile ? ( -
- ) : ( -
- -
- This file can't be previewed. -
- {isFilePreviewable ? ( - <> -
- There was an error loading the file. Try again, or download - it and open it using another application. -
-
+
+
+ {objectUrl ? ( + getPreviewComponentForFile(file, objectUrl) + ) : isLoadingFile ? ( +
+ ) : ( +
+ +
+ This file can't be previewed. +
+ {isFilePreviewable ? ( + <> +
+ There was an error loading the file. Try again, or + download the file and open it using another application. +
+
+ + +
+ + ) : ( + <> +
+ To view this file, download it and open it using another + application. +
- -
- - ) : ( - <> -
- To view this file, download it and open it using another - application. -
- - - )} -
- )} + + )} +
+ )} +
+ {showFileInfoPanel && }
diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index f2e8bdcbd..266087998 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -473,6 +473,10 @@ border-right-width: 1px; } +.sn-component .border-l-1px { + border-left-width: 1px; +} + .sn-component .border-t-1px { border-top-width: 1px; } @@ -493,6 +497,10 @@ padding: 0.25rem; } +.p-1\.5 { + padding: 0.375rem; +} + .p-8 { padding: 2rem; } From 3a2ff2f440a5838649eca883f3e6689b4f5c05ff Mon Sep 17 00:00:00 2001 From: Mo Date: Mon, 11 Apr 2022 12:48:19 -0500 Subject: [PATCH 011/102] refactor: new snjs support (#967) --- .prettierrc | 2 +- .../components/AccountMenu/User.tsx | 2 +- .../AttachedFilesPopover.tsx | 26 +- app/assets/javascripts/components/Footer.tsx | 5 +- .../components/NoteView/NoteView.tsx | 270 ++++++++---------- .../javascripts/components/NotesListItem.tsx | 23 +- .../components/NotesListOptionsMenu.tsx | 8 +- .../NotesOptions/ListedActionsOption.tsx | 4 +- .../components/NotesOptions/NotesOptions.tsx | 21 -- .../changeEditor/ChangeEditorMenu.tsx | 2 +- .../changeEditor/createEditorMenuGroups.ts | 6 +- .../components/Preferences/PreferencesMenu.ts | 52 +--- .../Preferences/PreferencesView.tsx | 30 +- .../Preferences/panes/Appearance.tsx | 7 +- .../Preferences/panes/ExtensionPane.tsx | 79 ----- .../Preferences/panes/Extensions.tsx | 9 +- .../panes/backups-segments/EmailBackups.tsx | 14 +- .../cloud-backups/CloudBackupProvider.tsx | 5 +- .../backups-segments/cloud-backups/index.tsx | 9 +- .../ConfirmCustomExtension.tsx | 4 +- .../extensions-segments/ExtensionItem.tsx | 4 +- .../ExtensionsLatestVersions.ts | 7 +- .../panes/general-segments/Defaults.tsx | 4 +- .../panes/general-segments/Labs.tsx | 7 +- .../panes/security-segments/Privacy.tsx | 2 +- .../QuickSettingsMenu/QuickSettingsMenu.tsx | 38 ++- .../HistoryListContainer.tsx | 6 +- .../RevisionHistoryModalWrapper.tsx | 15 +- .../components/RevisionPreviewModal.tsx | 6 +- .../components/Tags/SmartViewsListItem.tsx | 52 ++-- .../components/Tags/TagsListItem.tsx | 99 +++---- .../javascripts/services/archiveManager.ts | 12 +- .../javascripts/services/desktopManager.ts | 17 +- .../javascripts/services/themeManager.ts | 17 +- .../ui_models/app_state/account_menu_state.ts | 9 +- .../ui_models/app_state/app_state.ts | 101 ++++--- .../ui_models/app_state/note_tags_state.ts | 2 +- .../ui_models/app_state/notes_state.ts | 42 +-- .../ui_models/app_state/notes_view_state.ts | 56 ++-- .../ui_models/app_state/tags_state.ts | 67 ++--- .../javascripts/ui_models/application.ts | 3 +- .../ui_models/application_group.ts | 2 +- package.json | 26 +- yarn.lock | 196 ++++++------- 44 files changed, 569 insertions(+), 799 deletions(-) delete mode 100644 app/assets/javascripts/components/Preferences/panes/ExtensionPane.tsx diff --git a/.prettierrc b/.prettierrc index 544138be4..9e74d98a6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,3 @@ { - "singleQuote": true + "singleQuote": true, } diff --git a/app/assets/javascripts/components/AccountMenu/User.tsx b/app/assets/javascripts/components/AccountMenu/User.tsx index 68f49df1e..7815d4e74 100644 --- a/app/assets/javascripts/components/AccountMenu/User.tsx +++ b/app/assets/javascripts/components/AccountMenu/User.tsx @@ -1,7 +1,7 @@ import { observer } from 'mobx-react-lite'; import { AppState } from '@/ui_models/app_state'; import { WebApplication } from '@/ui_models/application'; -import { User as UserType } from '@standardnotes/responses'; +import { User as UserType } from '@standardnotes/snjs'; type Props = { appState: AppState; diff --git a/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx b/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx index 82c1f2c14..770ea0277 100644 --- a/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx +++ b/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx @@ -169,20 +169,18 @@ export const AttachedFilesPopover: FunctionComponent = observer(
) : null} {filteredList.length > 0 ? ( - filteredList - .filter((file) => !file.deleted) - .map((file: SNFile) => { - return ( - - ); - }) + filteredList.map((file: SNFile) => { + return ( + + ); + }) ) : (
diff --git a/app/assets/javascripts/components/Footer.tsx b/app/assets/javascripts/components/Footer.tsx index 5965843c4..fa7667602 100644 --- a/app/assets/javascripts/components/Footer.tsx +++ b/app/assets/javascripts/components/Footer.tsx @@ -226,10 +226,7 @@ export class Footer extends PureComponent { this.application.items.setDisplayOptions( ContentType.Theme, CollectionSort.Title, - 'asc', - (theme: ItemInterface) => { - return !theme.errorDecrypting; - } + 'asc' ); } diff --git a/app/assets/javascripts/components/NoteView/NoteView.tsx b/app/assets/javascripts/components/NoteView/NoteView.tsx index e90eebcb1..6db18d81a 100644 --- a/app/assets/javascripts/components/NoteView/NoteView.tsx +++ b/app/assets/javascripts/components/NoteView/NoteView.tsx @@ -281,7 +281,7 @@ export class NoteView extends PureComponent { }); } - if (!note.deleted && note.locked !== this.state.noteLocked) { + if (note.locked !== this.state.noteLocked) { this.setState({ noteLocked: note.locked, }); @@ -421,7 +421,7 @@ export class NoteView extends PureComponent { streamItems() { this.removeComponentStreamObserver = this.application.streamItems( ContentType.Component, - async (_items, source) => { + async ({ source }) => { if ( isPayloadSourceInternalChange(source) || source === PayloadSource.InitialObserverRegistrationPush @@ -1003,7 +1003,7 @@ export class NoteView extends PureComponent { )}
- {this.note && !this.note.errorDecrypting && ( + {this.note && (
{
)} - {!this.note.errorDecrypting && ( -
- {this.state.marginResizersEnabled && - this.editorContentRef.current ? ( - - ) : null} +
+ {this.state.marginResizersEnabled && + this.editorContentRef.current ? ( + + ) : null} - {this.state.editorComponentViewer && ( -
- -
+ {this.state.editorComponentViewer && ( +
+ +
+ )} + + {this.state.editorStateDidLoad && + !this.state.editorComponentViewer && + !this.state.textareaUnloading && ( + )} - {this.state.editorStateDidLoad && - !this.state.editorComponentViewer && - !this.state.textareaUnloading && ( - - )} + {this.state.marginResizersEnabled && + this.editorContentRef.current ? ( + + ) : null} +
- {this.state.marginResizersEnabled && - this.editorContentRef.current ? ( - - ) : null} -
- )} - - {this.note.errorDecrypting && ( -
-
-
-
-
- {this.note.waitingForKey - ? 'Waiting for Key' - : 'Unable to Decrypt'} -
-
-
-
- {this.note.waitingForKey && ( -

- This note is awaiting its encryption key to be ready. - Please wait for syncing to complete for this note to - be decrypted. -

- )} - {!this.note.waitingForKey && ( -

- There was an error decrypting this item. Ensure you - are running the latest version of this app, then sign - out and sign back in to try again. -

- )} -
-
-
-
-
- )} - - {!this.note.errorDecrypting && ( -
- {this.state.availableStackComponents.length > 0 && ( -
-
- {this.state.availableStackComponents.map((component) => { - return ( -
{ - this.toggleStackComponent(component); - }} - className="sk-app-bar-item" - > -
-
-
-
-
{component.name}
-
+
+ {this.state.availableStackComponents.length > 0 && ( +
+
+ {this.state.availableStackComponents.map((component) => { + return ( +
{ + this.toggleStackComponent(component); + }} + className="sk-app-bar-item" + > +
+
- ); - })} -
+
+
{component.name}
+
+
+ ); + })}
- )} - -
- {this.state.stackComponentViewers.map((viewer) => { - return ( -
- -
- ); - })}
+ )} + +
+ {this.state.stackComponentViewers.map((viewer) => { + return ( +
+ +
+ ); + })}
- )} +
); diff --git a/app/assets/javascripts/components/NotesListItem.tsx b/app/assets/javascripts/components/NotesListItem.tsx index 1928308bd..cd11880ca 100644 --- a/app/assets/javascripts/components/NotesListItem.tsx +++ b/app/assets/javascripts/components/NotesListItem.tsx @@ -1,6 +1,7 @@ import { WebApplication } from '@/ui_models/application'; import { CollectionSort, + CollectionSortProperty, sanitizeHtmlString, SNNote, } from '@standardnotes/snjs'; @@ -18,7 +19,7 @@ type Props = { onClick: () => void; onContextMenu: (e: MouseEvent) => void; selected: boolean; - sortedBy?: CollectionSort; + sortedBy?: CollectionSortProperty; }; type NoteFlag = { @@ -34,25 +35,7 @@ const flagsForNote = (note: SNNote) => { class: 'danger', }); } - if (note.errorDecrypting) { - if (note.waitingForKey) { - flags.push({ - text: 'Waiting For Keys', - class: 'info', - }); - } else { - flags.push({ - text: 'Missing Keys', - class: 'danger', - }); - } - } - if (note.deleted) { - flags.push({ - text: 'Deletion Pending Sync', - class: 'danger', - }); - } + return flags; }; diff --git a/app/assets/javascripts/components/NotesListOptionsMenu.tsx b/app/assets/javascripts/components/NotesListOptionsMenu.tsx index a701d6df0..9002ab7ee 100644 --- a/app/assets/javascripts/components/NotesListOptionsMenu.tsx +++ b/app/assets/javascripts/components/NotesListOptionsMenu.tsx @@ -1,5 +1,9 @@ import { WebApplication } from '@/ui_models/application'; -import { CollectionSort, PrefKey } from '@standardnotes/snjs'; +import { + CollectionSort, + CollectionSortProperty, + PrefKey, +} from '@standardnotes/snjs'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { useState } from 'preact/hooks'; @@ -52,7 +56,7 @@ export const NotesListOptionsMenu: FunctionComponent = observer( setSortReverse(!sortReverse); }; - const toggleSortBy = (sort: CollectionSort) => { + const toggleSortBy = (sort: CollectionSortProperty) => { if (sortBy === sort) { toggleSortReverse(); } else { diff --git a/app/assets/javascripts/components/NotesOptions/ListedActionsOption.tsx b/app/assets/javascripts/components/NotesOptions/ListedActionsOption.tsx index 5fc9bef1a..f9cdf0c49 100644 --- a/app/assets/javascripts/components/NotesOptions/ListedActionsOption.tsx +++ b/app/assets/javascripts/components/NotesOptions/ListedActionsOption.tsx @@ -104,7 +104,7 @@ const ListedActionsMenu: FunctionComponent = ({ const updatedGroup: ListedMenuGroup = { name: updatedAccountInfo.display_name, account: group.account, - actions: updatedAccountInfo.actions, + actions: updatedAccountInfo.actions as Action[], }; const updatedGroups = menuGroups.map((group) => { @@ -145,7 +145,7 @@ const ListedActionsMenu: FunctionComponent = ({ menuGroups.push({ name: accountInfo.display_name, account, - actions: accountInfo.actions, + actions: accountInfo.actions as Action[], }); } else { menuGroups.push({ diff --git a/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx b/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx index 3fb22b7a0..3c0c8da56 100644 --- a/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx +++ b/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx @@ -219,7 +219,6 @@ export const NotesOptions = observer( const notTrashed = notes.some((note) => !note.trashed); const pinned = notes.some((note) => note.pinned); const unpinned = notes.some((note) => !note.pinned); - const errored = notes.some((note) => note.errorDecrypting); useEffect(() => { const removeAltKeyObserver = application.io.addKeyObserver({ @@ -278,26 +277,6 @@ export const NotesOptions = observer( }); }; - if (errored) { - return ( - <> - {notes.length === 1 ? ( -
-
- Note ID: {notes[0].uuid} -
-
- ) : null} - { - await appState.notes.deleteNotesPermanently(); - }} - /> - - ); - } - const openRevisionHistoryModal = () => { appState.notes.setShowRevisionHistoryModal(true); }; diff --git a/app/assets/javascripts/components/NotesOptions/changeEditor/ChangeEditorMenu.tsx b/app/assets/javascripts/components/NotesOptions/changeEditor/ChangeEditorMenu.tsx index 7a6f217c2..cd35f31b5 100644 --- a/app/assets/javascripts/components/NotesOptions/changeEditor/ChangeEditorMenu.tsx +++ b/app/assets/javascripts/components/NotesOptions/changeEditor/ChangeEditorMenu.tsx @@ -86,7 +86,7 @@ export const ChangeEditorMenu: FunctionComponent = ({ ) => { if (component) { if (component.conflictOf) { - application.mutator.changeAndSaveItem(component.uuid, (mutator) => { + application.mutator.changeAndSaveItem(component, (mutator) => { mutator.conflictOf = undefined; }); } diff --git a/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts b/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts index 1b59c6358..d69be1d46 100644 --- a/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts +++ b/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts @@ -1,11 +1,13 @@ import { WebApplication } from '@/ui_models/application'; import { + ContentType, + FeatureStatus, + SNComponent, ComponentArea, FeatureDescription, GetFeatures, NoteType, -} from '@standardnotes/features'; -import { ContentType, FeatureStatus, SNComponent } from '@standardnotes/snjs'; +} from '@standardnotes/snjs'; import { EditorMenuItem, EditorMenuGroup } from '../ChangeEditorOption'; export const PLAIN_EDITOR_NAME = 'Plain Editor'; diff --git a/app/assets/javascripts/components/Preferences/PreferencesMenu.ts b/app/assets/javascripts/components/Preferences/PreferencesMenu.ts index b9b474f63..f2a2843fb 100644 --- a/app/assets/javascripts/components/Preferences/PreferencesMenu.ts +++ b/app/assets/javascripts/components/Preferences/PreferencesMenu.ts @@ -1,12 +1,6 @@ import { action, makeAutoObservable, observable } from 'mobx'; import { ExtensionsLatestVersions } from '@/components/Preferences/panes/extensions-segments'; -import { - ComponentArea, - ContentType, - FeatureIdentifier, - SNComponent, - IconType, -} from '@standardnotes/snjs'; +import { FeatureIdentifier, IconType } from '@standardnotes/snjs'; import { WebApplication } from '@/ui_models/application'; const PREFERENCE_IDS = [ @@ -61,7 +55,6 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ export class PreferencesMenu { private _selectedPane: PreferenceId | FeatureIdentifier = 'account'; - private _extensionPanes: SNComponent[] = []; private _menu: PreferencesMenuItem[]; private _extensionLatestVersions: ExtensionsLatestVersions = new ExtensionsLatestVersions(new Map()); @@ -74,7 +67,6 @@ export class PreferencesMenu { ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS; - this.loadExtensionsPanes(); this.loadLatestVersions(); makeAutoObservable< @@ -105,64 +97,24 @@ export class PreferencesMenu { return this._extensionLatestVersions; } - loadExtensionsPanes(): void { - const excludedComponents = [ - FeatureIdentifier.TwoFactorAuthManager, - 'org.standardnotes.batch-manager', - 'org.standardnotes.extensions-manager', - FeatureIdentifier.CloudLink, - ]; - this._extensionPanes = ( - this.application.items.getItems([ - ContentType.ActionsExtension, - ContentType.Component, - ContentType.Theme, - ]) as SNComponent[] - ).filter( - (extension) => - extension.area === ComponentArea.Modal && - !excludedComponents.includes(extension.package_info.identifier) - ); - } - get menuItems(): SelectableMenuItem[] { const menuItems = this._menu.map((preference) => ({ ...preference, selected: preference.id === this._selectedPane, })); - const extensionsMenuItems: SelectableMenuItem[] = this._extensionPanes.map( - (extension) => { - return { - icon: 'window', - id: extension.package_info.identifier, - label: extension.name, - selected: extension.package_info.identifier === this._selectedPane, - }; - } - ); - return menuItems.concat(extensionsMenuItems); + return menuItems; } get selectedMenuItem(): PreferencesMenuItem | undefined { return this._menu.find((item) => item.id === this._selectedPane); } - get selectedExtension(): SNComponent | undefined { - return this._extensionPanes.find( - (extension) => extension.package_info.identifier === this._selectedPane - ); - } - get selectedPaneId(): PreferenceId | FeatureIdentifier { if (this.selectedMenuItem != undefined) { return this.selectedMenuItem.id; } - if (this.selectedExtension != undefined) { - return this.selectedExtension.package_info.identifier; - } - return 'account'; } diff --git a/app/assets/javascripts/components/Preferences/PreferencesView.tsx b/app/assets/javascripts/components/Preferences/PreferencesView.tsx index dd615570a..f808c6458 100644 --- a/app/assets/javascripts/components/Preferences/PreferencesView.tsx +++ b/app/assets/javascripts/components/Preferences/PreferencesView.tsx @@ -1,6 +1,8 @@ import { RoundIconButton } from '@/components/RoundIconButton'; import { TitleBar, Title } from '@/components/TitleBar'; import { FunctionComponent } from 'preact'; +import { observer } from 'mobx-react-lite'; + import { AccountPreferences, HelpAndFeedback, @@ -8,15 +10,12 @@ import { General, Security, } from './panes'; -import { observer } from 'mobx-react-lite'; - import { PreferencesMenu } from './PreferencesMenu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; import { MfaProps } from './panes/two-factor-auth/MfaProps'; import { AppState } from '@/ui_models/app_state'; import { useEffect, useMemo } from 'preact/hooks'; -import { ExtensionPane } from './panes/ExtensionPane'; import { Backups } from '@/components/Preferences/panes/Backups'; import { Appearance } from './panes/Appearance'; @@ -66,24 +65,13 @@ const PaneSelector: FunctionComponent< case 'help-feedback': return ; default: - if (menu.selectedExtension != undefined) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); } }); diff --git a/app/assets/javascripts/components/Preferences/panes/Appearance.tsx b/app/assets/javascripts/components/Preferences/panes/Appearance.tsx index 3ca731a9b..5f7f16259 100644 --- a/app/assets/javascripts/components/Preferences/panes/Appearance.tsx +++ b/app/assets/javascripts/components/Preferences/panes/Appearance.tsx @@ -4,12 +4,12 @@ import { sortThemes } from '@/components/QuickSettingsMenu/QuickSettingsMenu'; import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator'; import { Switch } from '@/components/Switch'; import { WebApplication } from '@/ui_models/application'; -import { GetFeatures } from '@standardnotes/features'; import { ContentType, FeatureIdentifier, FeatureStatus, PrefKey, + GetFeatures, SNTheme, } from '@standardnotes/snjs'; import { observer } from 'mobx-react-lite'; @@ -61,9 +61,8 @@ export const Appearance: FunctionComponent = observer( ); useEffect(() => { - const themesAsItems: DropdownItem[] = ( - application.items.getDisplayableItems(ContentType.Theme) as SNTheme[] - ) + const themesAsItems: DropdownItem[] = application.items + .getDisplayableItems(ContentType.Theme) .filter((theme) => !theme.isLayerable()) .sort(sortThemes) .map((theme) => { diff --git a/app/assets/javascripts/components/Preferences/panes/ExtensionPane.tsx b/app/assets/javascripts/components/Preferences/panes/ExtensionPane.tsx deleted file mode 100644 index cac365104..000000000 --- a/app/assets/javascripts/components/Preferences/panes/ExtensionPane.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { - PreferencesGroup, - PreferencesSegment, -} from '@/components/Preferences/components'; -import { WebApplication } from '@/ui_models/application'; -import { ComponentViewer, SNComponent } from '@standardnotes/snjs'; -import { FeatureIdentifier } from '@standardnotes/features'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { ExtensionItem } from './extensions-segments'; -import { ComponentView } from '@/components/ComponentView'; -import { AppState } from '@/ui_models/app_state'; -import { PreferencesMenu } from '@/components/Preferences/PreferencesMenu'; -import { useEffect, useState } from 'preact/hooks'; - -interface IProps { - application: WebApplication; - appState: AppState; - extension: SNComponent; - preferencesMenu: PreferencesMenu; -} - -const urlOverrideForExtension = (extension: SNComponent) => { - if (extension.identifier === FeatureIdentifier.CloudLink) { - return 'https://extensions.standardnotes.org/components/cloudlink'; - } else { - return undefined; - } -}; - -export const ExtensionPane: FunctionComponent = observer( - ({ extension, application, appState, preferencesMenu }) => { - const [componentViewer] = useState( - application.componentManager.createComponentViewer( - extension, - undefined, - undefined, - urlOverrideForExtension(extension) - ) - ); - const latestVersion = - preferencesMenu.extensionsLatestVersions.getVersion(extension); - - useEffect(() => { - return () => { - application.componentManager.destroyComponentViewer(componentViewer); - }; - }, [application, componentViewer]); - - return ( -
-
-
- - - application.mutator - .deleteItem(extension) - .then(() => preferencesMenu.loadExtensionsPanes()) - } - latestVersion={latestVersion} - /> - - - - -
-
-
- ); - } -); diff --git a/app/assets/javascripts/components/Preferences/panes/Extensions.tsx b/app/assets/javascripts/components/Preferences/panes/Extensions.tsx index 8c7c9a8e0..99ef28307 100644 --- a/app/assets/javascripts/components/Preferences/panes/Extensions.tsx +++ b/app/assets/javascripts/components/Preferences/panes/Extensions.tsx @@ -13,10 +13,11 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import { observer } from 'mobx-react-lite'; const loadExtensions = (application: WebApplication) => - application.items.getItems( - [ContentType.ActionsExtension, ContentType.Component, ContentType.Theme], - true - ) as SNComponent[]; + application.items.getItems([ + ContentType.ActionsExtension, + ContentType.Component, + ContentType.Theme, + ]) as SNComponent[]; export const Extensions: FunctionComponent<{ application: WebApplication; diff --git a/app/assets/javascripts/components/Preferences/panes/backups-segments/EmailBackups.tsx b/app/assets/javascripts/components/Preferences/panes/backups-segments/EmailBackups.tsx index b3286718c..16d634bcd 100644 --- a/app/assets/javascripts/components/Preferences/panes/backups-segments/EmailBackups.tsx +++ b/app/assets/javascripts/components/Preferences/panes/backups-segments/EmailBackups.tsx @@ -13,16 +13,16 @@ import { Text, Title, } from '../../components'; -import { - EmailBackupFrequency, - MuteFailedBackupsEmailsOption, - SettingName, -} from '@standardnotes/settings'; import { Dropdown, DropdownItem } from '@/components/Dropdown'; import { Switch } from '@/components/Switch'; import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator'; -import { FeatureIdentifier } from '@standardnotes/features'; -import { FeatureStatus } from '@standardnotes/snjs'; +import { + FeatureStatus, + FeatureIdentifier, + EmailBackupFrequency, + MuteFailedBackupsEmailsOption, + SettingName, +} from '@standardnotes/snjs'; type Props = { application: WebApplication; diff --git a/app/assets/javascripts/components/Preferences/panes/backups-segments/cloud-backups/CloudBackupProvider.tsx b/app/assets/javascripts/components/Preferences/panes/backups-segments/cloud-backups/CloudBackupProvider.tsx index 64806d0ec..938c73911 100644 --- a/app/assets/javascripts/components/Preferences/panes/backups-segments/cloud-backups/CloudBackupProvider.tsx +++ b/app/assets/javascripts/components/Preferences/panes/backups-segments/cloud-backups/CloudBackupProvider.tsx @@ -1,11 +1,12 @@ import { useCallback, useEffect, useState } from 'preact/hooks'; -import { ButtonType, SettingName } from '@standardnotes/snjs'; import { + ButtonType, + SettingName, CloudProvider, DropboxBackupFrequency, GoogleDriveBackupFrequency, OneDriveBackupFrequency, -} from '@standardnotes/settings'; +} from '@standardnotes/snjs'; import { WebApplication } from '@/ui_models/application'; import { Button } from '@/components/Button'; import { isDev, openInNewTab } from '@/utils'; diff --git a/app/assets/javascripts/components/Preferences/panes/backups-segments/cloud-backups/index.tsx b/app/assets/javascripts/components/Preferences/panes/backups-segments/cloud-backups/index.tsx index 1ce07b1cb..a19b481fd 100644 --- a/app/assets/javascripts/components/Preferences/panes/backups-segments/cloud-backups/index.tsx +++ b/app/assets/javascripts/components/Preferences/panes/backups-segments/cloud-backups/index.tsx @@ -9,14 +9,15 @@ import { Title, } from '@/components/Preferences/components'; import { HorizontalSeparator } from '@/components/Shared/HorizontalSeparator'; -import { FeatureIdentifier } from '@standardnotes/features'; -import { FeatureStatus } from '@standardnotes/snjs'; -import { FunctionComponent } from 'preact'; import { + FeatureStatus, + FeatureIdentifier, CloudProvider, MuteFailedCloudBackupsEmailsOption, SettingName, -} from '@standardnotes/settings'; +} from '@standardnotes/snjs'; +import { FunctionComponent } from 'preact'; + import { Switch } from '@/components/Switch'; import { convertStringifiedBooleanToBoolean } from '@/utils'; import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/strings'; diff --git a/app/assets/javascripts/components/Preferences/panes/extensions-segments/ConfirmCustomExtension.tsx b/app/assets/javascripts/components/Preferences/panes/extensions-segments/ConfirmCustomExtension.tsx index 362d3d24c..60333b60d 100644 --- a/app/assets/javascripts/components/Preferences/panes/extensions-segments/ConfirmCustomExtension.tsx +++ b/app/assets/javascripts/components/Preferences/panes/extensions-segments/ConfirmCustomExtension.tsx @@ -1,4 +1,4 @@ -import { displayStringForContentType, SNComponent } from '@standardnotes/snjs'; +import { DisplayStringForContentType, SNComponent } from '@standardnotes/snjs'; import { Button } from '@/components/Button'; import { FunctionComponent } from 'preact'; import { Title, Text, Subtitle, PreferencesSegment } from '../../components'; @@ -30,7 +30,7 @@ export const ConfirmCustomExtension: FunctionComponent<{ }, { label: 'Extension Type', - value: displayStringForContentType(component.content_type), + value: DisplayStringForContentType(component.content_type), }, ]; diff --git a/app/assets/javascripts/components/Preferences/panes/extensions-segments/ExtensionItem.tsx b/app/assets/javascripts/components/Preferences/panes/extensions-segments/ExtensionItem.tsx index 4dab16adb..316ee59a7 100644 --- a/app/assets/javascripts/components/Preferences/panes/extensions-segments/ExtensionItem.tsx +++ b/app/assets/javascripts/components/Preferences/panes/extensions-segments/ExtensionItem.tsx @@ -47,7 +47,7 @@ export const ExtensionItem: FunctionComponent = ({ const newOfflineOnly = !offlineOnly; setOfflineOnly(newOfflineOnly); application.mutator - .changeAndSaveItem(extension.uuid, (m: any) => { + .changeAndSaveItem(extension, (m: any) => { if (m.content == undefined) m.content = {}; m.content.offlineOnly = newOfflineOnly; }) @@ -63,7 +63,7 @@ export const ExtensionItem: FunctionComponent = ({ const changeExtensionName = (newName: string) => { setExtensionName(newName); application.mutator - .changeAndSaveItem(extension.uuid, (m: any) => { + .changeAndSaveItem(extension, (m: any) => { if (m.content == undefined) m.content = {}; m.content.name = newName; }) diff --git a/app/assets/javascripts/components/Preferences/panes/extensions-segments/ExtensionsLatestVersions.ts b/app/assets/javascripts/components/Preferences/panes/extensions-segments/ExtensionsLatestVersions.ts index 5ac51a842..6757dd813 100644 --- a/app/assets/javascripts/components/Preferences/panes/extensions-segments/ExtensionsLatestVersions.ts +++ b/app/assets/javascripts/components/Preferences/panes/extensions-segments/ExtensionsLatestVersions.ts @@ -1,6 +1,9 @@ import { WebApplication } from '@/ui_models/application'; -import { FeatureDescription } from '@standardnotes/features'; -import { SNComponent, ClientDisplayableError } from '@standardnotes/snjs'; +import { + SNComponent, + ClientDisplayableError, + FeatureDescription, +} from '@standardnotes/snjs'; import { makeAutoObservable, observable } from 'mobx'; export class ExtensionsLatestVersions { diff --git a/app/assets/javascripts/components/Preferences/panes/general-segments/Defaults.tsx b/app/assets/javascripts/components/Preferences/panes/general-segments/Defaults.tsx index 003cc9a39..fa5bd6c4e 100644 --- a/app/assets/javascripts/components/Preferences/panes/general-segments/Defaults.tsx +++ b/app/assets/javascripts/components/Preferences/panes/general-segments/Defaults.tsx @@ -34,7 +34,7 @@ const makeEditorDefault = ( if (currentDefault) { removeEditorDefault(application, currentDefault); } - application.mutator.changeAndSaveItem(component.uuid, (m) => { + application.mutator.changeAndSaveItem(component, (m) => { const mutator = m as ComponentMutator; mutator.defaultEditor = true; }); @@ -44,7 +44,7 @@ const removeEditorDefault = ( application: WebApplication, component: SNComponent ) => { - application.mutator.changeAndSaveItem(component.uuid, (m) => { + application.mutator.changeAndSaveItem(component, (m) => { const mutator = m as ComponentMutator; mutator.defaultEditor = false; }); diff --git a/app/assets/javascripts/components/Preferences/panes/general-segments/Labs.tsx b/app/assets/javascripts/components/Preferences/panes/general-segments/Labs.tsx index 9c36f06ba..0c14f55fb 100644 --- a/app/assets/javascripts/components/Preferences/panes/general-segments/Labs.tsx +++ b/app/assets/javascripts/components/Preferences/panes/general-segments/Labs.tsx @@ -1,4 +1,3 @@ -import { FindNativeFeature } from '@standardnotes/features'; import { Switch } from '@/components/Switch'; import { PreferencesGroup, @@ -8,7 +7,11 @@ import { Title, } from '@/components/Preferences/components'; import { WebApplication } from '@/ui_models/application'; -import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'; +import { + FeatureIdentifier, + FeatureStatus, + FindNativeFeature, +} from '@standardnotes/snjs'; import { FunctionComponent } from 'preact'; import { useCallback, useEffect, useState } from 'preact/hooks'; import { usePremiumModal } from '@/components/Premium'; diff --git a/app/assets/javascripts/components/Preferences/panes/security-segments/Privacy.tsx b/app/assets/javascripts/components/Preferences/panes/security-segments/Privacy.tsx index 78277a96a..5ae3843e2 100644 --- a/app/assets/javascripts/components/Preferences/panes/security-segments/Privacy.tsx +++ b/app/assets/javascripts/components/Preferences/panes/security-segments/Privacy.tsx @@ -12,7 +12,7 @@ import { MuteSignInEmailsOption, LogSessionUserAgentOption, SettingName, -} from '@standardnotes/settings'; +} from '@standardnotes/snjs'; import { observer } from 'mobx-react-lite'; import { FunctionalComponent } from 'preact'; import { useCallback, useEffect, useState } from 'preact/hooks'; diff --git a/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx b/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx index d2cbdcf52..daf69a3b2 100644 --- a/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/app/assets/javascripts/components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -103,15 +103,15 @@ export const QuickSettingsMenu: FunctionComponent = observer( }, [focusModeEnabled]); const reloadThemes = useCallback(() => { - const themes = ( - application.items.getDisplayableItems(ContentType.Theme) as SNTheme[] - ).map((item) => { - return { - name: item.name, - identifier: item.identifier, - component: item, - }; - }) as ThemeItem[]; + const themes = application.items + .getDisplayableItems(ContentType.Theme) + .map((item) => { + return { + name: item.name, + identifier: item.identifier, + component: item, + }; + }) as ThemeItem[]; GetFeatures() .filter( @@ -140,17 +140,15 @@ export const QuickSettingsMenu: FunctionComponent = observer( }, [application]); const reloadToggleableComponents = useCallback(() => { - const toggleableComponents = ( - application.items.getDisplayableItems( - ContentType.Component - ) as SNComponent[] - ).filter( - (component) => - [ComponentArea.EditorStack, ComponentArea.TagsList].includes( - component.area - ) && - component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent - ); + const toggleableComponents = application.items + .getDisplayableItems(ContentType.Component) + .filter( + (component) => + [ComponentArea.EditorStack].includes(component.area) && + component.identifier !== + FeatureIdentifier.DeprecatedFoldersComponent + ); + setToggleableComponents(toggleableComponents); }, [application]); diff --git a/app/assets/javascripts/components/RevisionHistoryModal/HistoryListContainer.tsx b/app/assets/javascripts/components/RevisionHistoryModal/HistoryListContainer.tsx index 55748187a..6f16c6a21 100644 --- a/app/assets/javascripts/components/RevisionHistoryModal/HistoryListContainer.tsx +++ b/app/assets/javascripts/components/RevisionHistoryModal/HistoryListContainer.tsx @@ -134,7 +134,7 @@ export const HistoryListContainer: FunctionComponent = observer( throw new Error('Could not fetch revision'); } - setSelectedRevision(response.item as HistoryEntry); + setSelectedRevision(response.item as unknown as HistoryEntry); } catch (error) { console.error(error); setSelectedRevision(undefined); @@ -165,7 +165,7 @@ export const HistoryListContainer: FunctionComponent = observer( try { const remoteRevision = await application.historyManager.fetchRemoteRevision( - note.uuid, + note, revisionListEntry ); setSelectedRevision(remoteRevision); @@ -182,7 +182,7 @@ export const HistoryListContainer: FunctionComponent = observer( }, [ application, - note.uuid, + note, setIsFetchingSelectedRevision, setSelectedRemoteEntry, setSelectedRevision, diff --git a/app/assets/javascripts/components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx b/app/assets/javascripts/components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx index 2ec440528..78ddfc537 100644 --- a/app/assets/javascripts/components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx +++ b/app/assets/javascripts/components/RevisionHistoryModal/RevisionHistoryModalWrapper.tsx @@ -8,7 +8,6 @@ import { ButtonType, ContentType, HistoryEntry, - PayloadContent, PayloadSource, RevisionListEntry, SNNote, @@ -148,7 +147,7 @@ export const RevisionHistoryModal: FunctionComponent }).then((confirmed) => { if (confirmed) { application.mutator.changeAndSaveItem( - selectedRevision.payload.uuid, + originalNote, (mutator) => { mutator.unsafe_setCustomContent( selectedRevision.payload.content @@ -165,14 +164,14 @@ export const RevisionHistoryModal: FunctionComponent const restoreAsCopy = async () => { if (selectedRevision) { - const originalNote = application.items.findItem( + const originalNote = application.items.findSureItem( selectedRevision.payload.uuid - ) as SNNote; + ); const duplicatedItem = await application.mutator.duplicateItem( originalNote, { - ...(selectedRevision.payload.content as PayloadContent), + ...selectedRevision.payload.content, title: selectedRevision.payload.content.title ? selectedRevision.payload.content.title + ' (copy)' : undefined, @@ -188,10 +187,10 @@ export const RevisionHistoryModal: FunctionComponent useEffect(() => { const fetchTemplateNote = async () => { if (selectedRevision) { - const newTemplateNote = (await application.mutator.createTemplateItem( + const newTemplateNote = application.mutator.createTemplateItem( ContentType.Note, selectedRevision.payload.content - )) as SNNote; + ) as SNNote; setTemplateNoteForRevision(newTemplateNote); } @@ -218,7 +217,7 @@ export const RevisionHistoryModal: FunctionComponent setIsDeletingRevision(true); application.historyManager - .deleteRemoteRevision(note.uuid, selectedRemoteEntry) + .deleteRemoteRevision(note, selectedRemoteEntry) .then((res) => { if (res.error?.message) { throw new Error(res.error.message); diff --git a/app/assets/javascripts/components/RevisionPreviewModal.tsx b/app/assets/javascripts/components/RevisionPreviewModal.tsx index 2889e1498..204f65e1a 100644 --- a/app/assets/javascripts/components/RevisionPreviewModal.tsx +++ b/app/assets/javascripts/components/RevisionPreviewModal.tsx @@ -4,7 +4,7 @@ import { PayloadSource, SNNote, ComponentViewer, - PayloadContent, + NoteContent, } from '@standardnotes/snjs'; import { confirmDialog } from '@/services/alertService'; import { STRING_RESTORE_LOCKED_ATTEMPT } from '@/strings'; @@ -13,7 +13,7 @@ import { ComponentView } from './ComponentView'; interface Props { application: WebApplication; - content: PayloadContent; + content: NoteContent; title?: string; uuid: string; } @@ -74,7 +74,7 @@ export class RevisionPreviewModal extends PureComponent { }); } else { this.application.mutator.changeAndSaveItem( - this.props.uuid, + this.originalNote, (mutator) => { mutator.unsafe_setCustomContent(this.props.content); }, diff --git a/app/assets/javascripts/components/Tags/SmartViewsListItem.tsx b/app/assets/javascripts/components/Tags/SmartViewsListItem.tsx index 841c7c540..0b8f22c3e 100644 --- a/app/assets/javascripts/components/Tags/SmartViewsListItem.tsx +++ b/app/assets/javascripts/components/Tags/SmartViewsListItem.tsx @@ -110,30 +110,29 @@ export const SmartViewsListItem: FunctionComponent = observer( paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`, }} > - {!view.errorDecrypting ? ( -
-
- -
- +
+ -
- {view.uuid === SystemViewId.AllNotes && tagsState.allNotesCount} -
- ) : null} + +
+ {view.uuid === SystemViewId.AllNotes && tagsState.allNotesCount} +
+
+ {!isSystemView(view) && (
{view.conflictOf && ( @@ -141,14 +140,7 @@ export const SmartViewsListItem: FunctionComponent = observer( Conflicted Copy {view.conflictOf}
)} - {view.errorDecrypting && !view.waitingForKey && ( -
Missing Keys
- )} - {view.errorDecrypting && view.waitingForKey && ( -
- Waiting For Keys -
- )} + {isSelected && (
{!isEditing && ( diff --git a/app/assets/javascripts/components/Tags/TagsListItem.tsx b/app/assets/javascripts/components/Tags/TagsListItem.tsx index 5e3cf1018..93b44e5d1 100644 --- a/app/assets/javascripts/components/Tags/TagsListItem.tsx +++ b/app/assets/javascripts/components/Tags/TagsListItem.tsx @@ -152,7 +152,7 @@ export const TagsListItem: FunctionComponent = observer( () => ({ accept: ItemTypes.TAG, canDrop: (item) => { - return tagsState.isValidTagParent(tag.uuid, item.uuid); + return tagsState.isValidTagParent(tag, item as SNTag); }, drop: (item) => { if (!hasFolders) { @@ -202,70 +202,61 @@ export const TagsListItem: FunctionComponent = observer( onContextMenu(tag, e.clientX, e.clientY); }} > - {!tag.errorDecrypting ? ( -
- {hasAtLeastOneFolder && ( -
- -
- )} -
- -
- -
+
+ {hasAtLeastOneFolder && ( +
-
{noteCounts.get()}
+ )} +
+
- ) : null} + +
+ +
{noteCounts.get()}
+
+
+
{tag.conflictOf && (
Conflicted Copy {tag.conflictOf}
)} - {tag.errorDecrypting && !tag.waitingForKey && ( -
Missing Keys
- )} - {tag.errorDecrypting && tag.waitingForKey && ( -
Waiting For Keys
- )}
{isAddingSubtag && ( diff --git a/app/assets/javascripts/services/archiveManager.ts b/app/assets/javascripts/services/archiveManager.ts index 78d31e2f9..bc9ca5438 100644 --- a/app/assets/javascripts/services/archiveManager.ts +++ b/app/assets/javascripts/services/archiveManager.ts @@ -1,11 +1,10 @@ import { WebApplication } from '@/ui_models/application'; import { parseFileName } from '@standardnotes/filepicker'; import { - EncryptionIntent, ContentType, - SNNote, BackupFile, - PayloadContent, + BackupFileDecryptedContextualPayload, + NoteContent, } from '@standardnotes/snjs'; function sanitizeFileName(name: string): string { @@ -79,6 +78,7 @@ export class ArchiveManager { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'text/plain', }); + const fileName = zippableFileName('Standard Notes Backup and Import File'); await zipWriter.add(fileName, new zip.BlobReader(blob)); @@ -88,9 +88,9 @@ export class ArchiveManager { let name, contents; if (item.content_type === ContentType.Note) { - const note = item as SNNote; - name = (note.content as PayloadContent).title; - contents = (note.content as PayloadContent).text; + const note = item as BackupFileDecryptedContextualPayload; + name = note.content.title; + contents = note.content.text; } else { name = item.content_type; contents = JSON.stringify(item.content, null, 2); diff --git a/app/assets/javascripts/services/desktopManager.ts b/app/assets/javascripts/services/desktopManager.ts index c32ae85dd..412cba298 100644 --- a/app/assets/javascripts/services/desktopManager.ts +++ b/app/assets/javascripts/services/desktopManager.ts @@ -8,13 +8,11 @@ import { removeFromArray, DesktopManagerInterface, PayloadSource, - EncryptionIntent, - CreateIntentPayloadFromObject, + InternalEventBus, } from '@standardnotes/snjs'; import { WebAppEvent, WebApplication } from '@/ui_models/application'; import { isDesktopApplication } from '@/utils'; import { Bridge, ElectronDesktopCallbacks } from './bridge'; -import { InternalEventBus } from '@standardnotes/services'; /** * An interface used by the Desktop application to interact with SN @@ -68,10 +66,7 @@ export class DesktopManager * Keys are not passed into ItemParams, so the result is not encrypted */ convertComponentForTransmission(component: SNComponent) { - return CreateIntentPayloadFromObject( - component.payloadRepresentation(), - EncryptionIntent.FileDecrypted - ); + return component.payloadRepresentation().ejected(); } // All `components` should be installed @@ -84,11 +79,7 @@ export class DesktopManager return this.convertComponentForTransmission(component); }) ).then((payloads) => { - this.bridge.syncComponents( - payloads.filter( - (payload) => !payload.errorDecrypting && !payload.waitingForKey - ) - ); + this.bridge.syncComponents(payloads); }); } @@ -137,7 +128,7 @@ export class DesktopManager return; } const updatedComponent = await this.application.mutator.changeAndSaveItem( - component.uuid, + component, (m) => { const mutator = m as ComponentMutator; if (error) { diff --git a/app/assets/javascripts/services/themeManager.ts b/app/assets/javascripts/services/themeManager.ts index b598499d0..68031c36b 100644 --- a/app/assets/javascripts/services/themeManager.ts +++ b/app/assets/javascripts/services/themeManager.ts @@ -1,7 +1,6 @@ import { WebApplication } from '@/ui_models/application'; import { StorageValueModes, - EncryptionIntent, ApplicationService, SNTheme, removeFromArray, @@ -11,9 +10,9 @@ import { FeatureStatus, PayloadSource, PrefKey, - CreateIntentPayloadFromObject, + CreateDecryptedLocalStorageContextPayload, + InternalEventBus, } from '@standardnotes/snjs'; -import { InternalEventBus } from '@standardnotes/services'; const CACHED_THEMES_KEY = 'cachedThemes'; @@ -156,9 +155,9 @@ export class ThemeManager extends ApplicationService { const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier; - const themes = this.application.items.getDisplayableItems( + const themes = this.application.items.getDisplayableItems( ContentType.Theme - ) as SNTheme[]; + ); const enableDefaultTheme = () => { const activeTheme = themes.find( @@ -206,7 +205,8 @@ export class ThemeManager extends ApplicationService { this.unregisterStream = this.application.streamItems( ContentType.Theme, - (items, source) => { + ({ changed, inserted, source }) => { + const items = changed.concat(inserted); const themes = items as SNTheme[]; for (const theme of themes) { if (theme.active) { @@ -275,10 +275,7 @@ export class ThemeManager extends ApplicationService { const mapped = themes.map((theme) => { const payload = theme.payloadRepresentation(); - return CreateIntentPayloadFromObject( - payload, - EncryptionIntent.LocalStorageDecrypted - ); + return CreateDecryptedLocalStorageContextPayload(payload); }); return this.application.setValue( diff --git a/app/assets/javascripts/ui_models/app_state/account_menu_state.ts b/app/assets/javascripts/ui_models/app_state/account_menu_state.ts index ce9064880..5bb879a6b 100644 --- a/app/assets/javascripts/ui_models/app_state/account_menu_state.ts +++ b/app/assets/javascripts/ui_models/app_state/account_menu_state.ts @@ -6,7 +6,12 @@ import { observable, runInAction, } from 'mobx'; -import { ApplicationEvent, ContentType, SNItem } from '@standardnotes/snjs'; +import { + ApplicationEvent, + ContentType, + SNNote, + SNTag, +} from '@standardnotes/snjs'; import { WebApplication } from '@/ui_models/application'; import { AccountMenuPane } from '@/components/AccountMenu'; @@ -23,7 +28,7 @@ export class AccountMenuState { otherSessionsSignOut = false; server: string | undefined = undefined; enableServerOption = false; - notesAndTags: SNItem[] = []; + notesAndTags: (SNNote | SNTag)[] = []; isEncryptionEnabled = false; encryptionStatusString = ''; isBackupEncrypted = false; diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index 8c6dc4e27..ac1e97623 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -8,13 +8,14 @@ import { ContentType, DeinitSource, NoteViewController, - PayloadSource, PrefKey, SNNote, SmartView, SNTag, SystemViewId, removeFromArray, + PayloadSource, + Uuid, } from '@standardnotes/snjs'; import { action, @@ -276,9 +277,9 @@ export class AppState { this.application.noteControllerGroup.closeAllNoteViews(); } - noteControllerForNote(note: SNNote) { + noteControllerForNote(uuid: Uuid) { for (const controller of this.getNoteControllers()) { - if (controller.note.uuid === note.uuid) { + if (controller.note.uuid === uuid) { return controller; } } @@ -328,43 +329,61 @@ export class AppState { } streamNotesAndTags() { - this.application.streamItems( + this.application.streamItems( [ContentType.Note, ContentType.Tag], - async (items, source) => { + async ({ changed, inserted, removed, source }) => { + if ( + ![PayloadSource.PreSyncSave, PayloadSource.RemoteRetrieved].includes( + source + ) + ) { + return; + } + + const removedNotes = removed.filter( + (i) => i.content_type === ContentType.Note + ); + + for (const removedNote of removedNotes) { + const noteController = this.noteControllerForNote(removedNote.uuid); + if (noteController) { + this.closeNoteController(noteController); + } + } + + const changedOrInserted = [...changed, ...inserted].filter( + (i) => i.content_type === ContentType.Note + ); + const selectedTag = this.tags.selected; - /** Close any note controllers for deleted/trashed/archived notes */ - if (source === PayloadSource.PreSyncSave) { - const notes = items.filter( - (candidate) => candidate.content_type === ContentType.Note - ) as SNNote[]; - for (const note of notes) { - const noteController = this.noteControllerForNote(note); - if (!noteController) { - continue; - } - if (note.deleted) { - this.closeNoteController(noteController); - } else if ( - note.trashed && - !( - selectedTag instanceof SmartView && - selectedTag.uuid === SystemViewId.TrashedNotes - ) && - !this.searchOptions.includeTrashed - ) { - this.closeNoteController(noteController); - } else if ( - note.archived && - !( - selectedTag instanceof SmartView && - selectedTag.uuid === SystemViewId.ArchivedNotes - ) && - !this.searchOptions.includeArchived && - !this.application.getPreference(PrefKey.NotesShowArchived, false) - ) { - this.closeNoteController(noteController); - } + for (const note of changedOrInserted) { + const noteController = this.noteControllerForNote(note.uuid); + if (!noteController) { + continue; + } + + const isBrowswingTrashedNotes = + selectedTag instanceof SmartView && + selectedTag.uuid === SystemViewId.TrashedNotes; + + const isBrowsingArchivedNotes = + selectedTag instanceof SmartView && + selectedTag.uuid === SystemViewId.ArchivedNotes; + + if ( + note.trashed && + !isBrowswingTrashedNotes && + !this.searchOptions.includeTrashed + ) { + this.closeNoteController(noteController); + } else if ( + note.archived && + !isBrowsingArchivedNotes && + !this.searchOptions.includeArchived && + !this.application.getPreference(PrefKey.NotesShowArchived, false) + ) { + this.closeNoteController(noteController); } } } @@ -436,11 +455,9 @@ export class AppState { /** Returns the tags that are referncing this note */ public getNoteTags(note: SNNote) { - return this.application.items - .itemsReferencingItem(note.uuid) - .filter((ref) => { - return ref.content_type === ContentType.Tag; - }) as SNTag[]; + return this.application.items.itemsReferencingItem(note).filter((ref) => { + return ref.content_type === ContentType.Tag; + }) as SNTag[]; } panelDidResize(name: string, collapsed: boolean) { diff --git a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts index 238da65aa..a5f26c786 100644 --- a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts +++ b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts @@ -215,7 +215,7 @@ export class NoteTagsState { async removeTagFromActiveNote(tag: SNTag): Promise { const { activeNote } = this; if (activeNote) { - await this.application.mutator.changeItem(tag.uuid, (mutator) => { + await this.application.mutator.changeItem(tag, (mutator) => { mutator.removeItemAsRelationship(activeNote); }); this.application.sync.sync(); diff --git a/app/assets/javascripts/ui_models/app_state/notes_state.ts b/app/assets/javascripts/ui_models/app_state/notes_state.ts index 0dec3b8d0..c01ecc378 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -60,15 +60,22 @@ export class NotesState { }); appEventListeners.push( - application.streamItems(ContentType.Note, (notes) => { - runInAction(() => { - for (const note of notes) { - if (this.selectedNotes[note.uuid]) { - this.selectedNotes[note.uuid] = note as SNNote; + application.streamItems( + ContentType.Note, + ({ changed, inserted, removed }) => { + runInAction(() => { + for (const removedNote of removed) { + delete this.selectedNotes[removedNote.uuid]; } - } - }); - }) + + for (const note of [...changed, ...inserted]) { + if (this.selectedNotes[note.uuid]) { + this.selectedNotes[note.uuid] = note; + } + } + }); + } + ) ); } @@ -85,9 +92,8 @@ export class NotesState { } private async selectNotesRange(selectedNote: SNNote): Promise { - const notes = this.application.items.getDisplayableItems( - ContentType.Note - ) as SNNote[]; + const notes = this.application.items.getDisplayableNotes(); + const lastSelectedNoteIndex = notes.findIndex( (note) => note.uuid == this.lastSelectedNote?.uuid ); @@ -179,10 +185,6 @@ export class NotesState { this.appState.noteTags.reloadTags(); await this.onActiveEditorChanged(); - - if (note.waitingForKey) { - this.application.presentKeyRecoveryWizard(); - } } setContextMenuOpen(open: boolean): void { @@ -263,7 +265,7 @@ export class NotesState { mutate: (mutator: NoteMutator) => void ): Promise { await this.application.mutator.changeItems( - Object.keys(this.selectedNotes), + Object.values(this.selectedNotes), mutate, false ); @@ -399,7 +401,7 @@ export class NotesState { async toggleGlobalSpellcheckForNote(note: SNNote) { await this.application.mutator.changeItem( - note.uuid, + note, (mutator) => { mutator.toggleSpellcheck(); }, @@ -410,11 +412,11 @@ export class NotesState { async addTagToSelectedNotes(tag: SNTag): Promise { const selectedNotes = Object.values(this.selectedNotes); - const parentChainTags = this.application.items.getTagParentChain(tag.uuid); + const parentChainTags = this.application.items.getTagParentChain(tag); const tagsToAdd = [...parentChainTags, tag]; await Promise.all( tagsToAdd.map(async (tag) => { - await this.application.mutator.changeItem(tag.uuid, (mutator) => { + await this.application.mutator.changeItem(tag, (mutator) => { for (const note of selectedNotes) { mutator.addItemAsRelationship(note); } @@ -426,7 +428,7 @@ export class NotesState { async removeTagFromSelectedNotes(tag: SNTag): Promise { const selectedNotes = Object.values(this.selectedNotes); - await this.application.mutator.changeItem(tag.uuid, (mutator) => { + await this.application.mutator.changeItem(tag, (mutator) => { for (const note of selectedNotes) { mutator.removeItemAsRelationship(note); } diff --git a/app/assets/javascripts/ui_models/app_state/notes_view_state.ts b/app/assets/javascripts/ui_models/app_state/notes_view_state.ts index f2dd350c8..1130d3534 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_view_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_view_state.ts @@ -1,6 +1,7 @@ import { ApplicationEvent, CollectionSort, + CollectionSortProperty, ContentType, findInArray, NotesDisplayCriteria, @@ -28,7 +29,7 @@ const ELEMENT_ID_SEARCH_BAR = 'search-bar'; const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable'; export type DisplayOptions = { - sortBy: CollectionSort; + sortBy: CollectionSortProperty; sortReverse: boolean; hidePinned: boolean; showArchived: boolean; @@ -73,18 +74,20 @@ export class NotesViewState { this.resetPagination(); appObservers.push( - application.streamItems(ContentType.Note, () => { + application.streamItems(ContentType.Note, () => { this.reloadNotes(); + const activeNote = this.appState.notes.activeNoteController?.note; + if (this.application.getAppState().notes.selectedNotesCount < 2) { if (activeNote) { - const discarded = activeNote.deleted || activeNote.trashed; + const browsingTrashedNotes = + this.appState.selectedTag instanceof SmartView && + this.appState.selectedTag?.uuid === SystemViewId.TrashedNotes; + if ( - discarded && - !( - this.appState.selectedTag instanceof SmartView && - this.appState.selectedTag?.uuid === SystemViewId.TrashedNotes - ) && + activeNote.trashed && + !browsingTrashedNotes && !this.appState?.searchOptions.includeTrashed ) { this.selectNextOrCreateNew(); @@ -96,19 +99,24 @@ export class NotesViewState { } } }), - application.streamItems([ContentType.Tag], async (items) => { - const tags = items as SNTag[]; - /** A tag could have changed its relationships, so we need to reload the filter */ - this.reloadNotesDisplayOptions(); - this.reloadNotes(); - if ( - this.appState.selectedTag && - findInArray(tags, 'uuid', this.appState.selectedTag.uuid) - ) { - /** Tag title could have changed */ - this.reloadPanelTitle(); + + application.streamItems( + [ContentType.Tag], + async ({ changed, inserted }) => { + const tags = [...changed, ...inserted]; + /** A tag could have changed its relationships, so we need to reload the filter */ + this.reloadNotesDisplayOptions(); + this.reloadNotes(); + + if ( + this.appState.selectedTag && + findInArray(tags, 'uuid', this.appState.selectedTag.uuid) + ) { + /** Tag title could have changed */ + this.reloadPanelTitle(); + } } - }), + ), application.addEventObserver(async () => { this.reloadPreferences(); }, ApplicationEvent.PreferencesChanged), @@ -223,9 +231,7 @@ export class NotesViewState { if (!tag) { return; } - const notes = this.application.items.getDisplayableItems( - ContentType.Note - ) as SNNote[]; + const notes = this.application.items.getDisplayableNotes(); const renderedNotes = notes.slice(0, this.notesToDisplay); this.notes = notes; @@ -250,7 +256,7 @@ export class NotesViewState { } const criteria = NotesDisplayCriteria.Create({ - sortProperty: this.displayOptions.sortBy as CollectionSort, + sortProperty: this.displayOptions.sortBy, sortDirection: this.displayOptions.sortReverse ? 'asc' : 'dsc', tags: tag instanceof SNTag ? [tag] : [], views: tag instanceof SmartView ? [tag] : [], @@ -498,7 +504,7 @@ export class NotesViewState { handleEditorChange = async () => { const activeNote = this.appState.getActiveNoteController()?.note; if (activeNote && activeNote.conflictOf) { - this.application.mutator.changeAndSaveItem(activeNote.uuid, (mutator) => { + this.application.mutator.changeAndSaveItem(activeNote, (mutator) => { mutator.conflictOf = undefined; }); } diff --git a/app/assets/javascripts/ui_models/app_state/tags_state.ts b/app/assets/javascripts/ui_models/app_state/tags_state.ts index e03dc419c..acf1fee80 100644 --- a/app/assets/javascripts/ui_models/app_state/tags_state.ts +++ b/app/assets/javascripts/ui_models/app_state/tags_state.ts @@ -14,6 +14,7 @@ import { TagMutator, UuidString, isSystemView, + FindItem, } from '@standardnotes/snjs'; import { action, @@ -29,11 +30,9 @@ import { FeaturesState, SMART_TAGS_FEATURE_NAME } from './features_state'; type AnyTag = SNTag | SmartView; const rootTags = (application: SNApplication): SNTag[] => { - const hasNoParent = (tag: SNTag) => !application.items.getTagParent(tag.uuid); + const hasNoParent = (tag: SNTag) => !application.items.getTagParent(tag); - const allTags = application.items.getDisplayableItems( - ContentType.Tag - ) as SNTag[]; + const allTags = application.items.getDisplayableItems(ContentType.Tag); const rootTags = allTags.filter(hasNoParent); return rootTags; @@ -44,10 +43,10 @@ const tagSiblings = (application: SNApplication, tag: SNTag): SNTag[] => { tags.filter((other) => other.uuid !== tag.uuid); const isTemplateTag = application.items.isTemplateItem(tag); - const parentTag = !isTemplateTag && application.items.getTagParent(tag.uuid); + const parentTag = !isTemplateTag && application.items.getTagParent(tag); if (parentTag) { - const siblingsAndTag = application.items.getTagChildren(parentTag.uuid); + const siblingsAndTag = application.items.getTagChildren(parentTag); return withoutCurrentTag(siblingsAndTag); } @@ -148,24 +147,24 @@ export class TagsState { appEventListeners.push( this.application.streamItems( [ContentType.Tag, ContentType.SmartView], - (items) => { + ({ changed, removed }) => { runInAction(() => { this.tags = this.application.items.getDisplayableItems( ContentType.Tag ); + this.smartViews = this.application.items.getSmartViews(); const selectedTag = this.selected_; + if (selectedTag && !isSystemView(selectedTag as SmartView)) { - const matchingTag = items.find( - (candidate) => candidate.uuid === selectedTag.uuid - ) as AnyTag; - if (matchingTag) { - if (matchingTag.deleted) { - this.selected_ = this.smartViews[0]; - } else { - this.selected_ = matchingTag; - } + if (FindItem(removed, selectedTag.uuid)) { + this.selected_ = this.smartViews[0]; + } + + const updated = FindItem(changed, selectedTag.uuid); + if (updated) { + this.selected_ = updated as AnyTag; } } else { this.selected_ = this.smartViews[0]; @@ -202,7 +201,7 @@ export class TagsState { title )) as SNTag; - const futureSiblings = this.application.items.getTagChildren(parent.uuid); + const futureSiblings = this.application.items.getTagChildren(parent); if (!isValidFutureSiblings(this.application, futureSiblings, createdTag)) { this.setAddingSubtagTo(undefined); @@ -319,7 +318,7 @@ export class TagsState { return []; } - const children = this.application.items.getTagChildren(tag.uuid); + const children = this.application.items.getTagChildren(tag); const childrenUuids = children.map((childTag) => childTag.uuid); const childrenTags = this.tags.filter((tag) => @@ -328,8 +327,8 @@ export class TagsState { return childrenTags; } - isValidTagParent(parentUuid: UuidString, tagUuid: UuidString): boolean { - return this.application.items.isValidTagParent(parentUuid, tagUuid); + isValidTagParent(parent: SNTag, tag: SNTag): boolean { + return this.application.items.isValidTagParent(parent, tag); } public hasParent(tagUuid: UuidString): boolean { @@ -343,7 +342,7 @@ export class TagsState { ): Promise { const tag = this.application.items.findItem(tagUuid) as SNTag; - const currentParent = this.application.items.getTagParent(tag.uuid); + const currentParent = this.application.items.getTagParent(tag); const currentParentUuid = currentParent?.uuid; if (currentParentUuid === futureParentUuid) { @@ -361,9 +360,8 @@ export class TagsState { } await this.application.mutator.unsetTagParent(tag); } else { - const futureSiblings = this.application.items.getTagChildren( - futureParent.uuid - ); + const futureSiblings = + this.application.items.getTagChildren(futureParent); if (!isValidFutureSiblings(this.application, futureSiblings, tag)) { return; } @@ -374,9 +372,7 @@ export class TagsState { } get rootTags(): SNTag[] { - return this.tags.filter( - (tag) => !this.application.items.getTagParent(tag.uuid) - ); + return this.tags.filter((tag) => !this.application.items.getTagParent(tag)); } get tagsCount(): number { @@ -401,7 +397,7 @@ export class TagsState { public set selected(tag: AnyTag | undefined) { if (tag && tag.conflictOf) { - this.application.mutator.changeAndSaveItem(tag.uuid, (mutator) => { + this.application.mutator.changeAndSaveItem(tag, (mutator) => { mutator.conflictOf = undefined; }); } @@ -417,12 +413,9 @@ export class TagsState { } public setExpanded(tag: SNTag, expanded: boolean) { - this.application.mutator.changeAndSaveItem( - tag.uuid, - (mutator) => { - mutator.expanded = expanded; - } - ); + this.application.mutator.changeAndSaveItem(tag, (mutator) => { + mutator.expanded = expanded; + }); } public get selectedUuid(): UuidString | undefined { @@ -527,7 +520,7 @@ export class TagsState { }); } else { await this.application.mutator.changeAndSaveItem( - tag.uuid, + tag, (mutator) => { mutator.title = newTitle; } @@ -563,9 +556,7 @@ export class TagsState { } public get hasAtLeastOneFolder(): boolean { - return this.tags.some( - (tag) => !!this.application.items.getTagParent(tag.uuid) - ); + return this.tags.some((tag) => !!this.application.items.getTagParent(tag)); } } diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts index 8d06cd055..e51ca5cfa 100644 --- a/app/assets/javascripts/ui_models/application.ts +++ b/app/assets/javascripts/ui_models/application.ts @@ -75,15 +75,16 @@ export class WebApplication extends SNApplication { if (source === DeinitSource.AppGroupUnload) { this.getThemeService().deactivateAllThemes(); } + for (const service of Object.values(this.webServices)) { if ('deinit' in service) { service.deinit?.(source); } (service as any).application = undefined; } + this.webServices = {} as WebServices; this.noteControllerGroup.deinit(); - this.iconsController.deinit(); this.webEventObservers.length = 0; if (source === DeinitSource.SignOut) { diff --git a/app/assets/javascripts/ui_models/application_group.ts b/app/assets/javascripts/ui_models/application_group.ts index 8feb4c7c4..f391b333b 100644 --- a/app/assets/javascripts/ui_models/application_group.ts +++ b/app/assets/javascripts/ui_models/application_group.ts @@ -6,6 +6,7 @@ import { DeviceInterface, Platform, Runtime, + InternalEventBus, } from '@standardnotes/snjs'; import { AppState } from '@/ui_models/app_state'; import { Bridge } from '@/services/bridge'; @@ -16,7 +17,6 @@ import { IOService } from '@/services/ioService'; import { AutolockService } from '@/services/autolock_service'; import { StatusManager } from '@/services/statusManager'; import { ThemeManager } from '@/services/themeManager'; -import { InternalEventBus } from '@standardnotes/services'; export class ApplicationGroup extends SNApplicationGroup { constructor( diff --git a/package.json b/package.json index ae404118b..f26895e8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.14.0", + "version": "3.15.0", "license": "AGPL-3.0-or-later", "repository": { "type": "git", @@ -26,11 +26,6 @@ "@babel/plugin-transform-react-jsx": "^7.17.3", "@babel/preset-env": "^7.16.11", "@babel/preset-typescript": "^7.16.7", - "@reach/disclosure": "^0.16.2", - "@reach/visually-hidden": "^0.16.0", - "@standardnotes/responses": "1.5.1", - "@standardnotes/services": "1.7.1", - "@standardnotes/stylekit": "5.20.0", "@svgr/webpack": "^6.2.1", "@types/jest": "^27.4.1", "@types/react": "^17.0.42", @@ -65,26 +60,27 @@ "webpack-merge": "^5.8.0" }, "dependencies": { - "@reach/alert": "^0.16.0", "@reach/alert-dialog": "^0.16.2", + "@reach/alert": "^0.16.0", "@reach/checkbox": "^0.16.0", "@reach/dialog": "^0.16.2", + "@reach/disclosure": "^0.16.2", "@reach/listbox": "^0.16.2", "@reach/tooltip": "^0.16.2", + "@reach/visually-hidden": "^0.16.0", "@standardnotes/components": "1.7.14", - "@standardnotes/features": "1.36.3", - "@standardnotes/filepicker": "1.10.4", - "@standardnotes/settings": "1.13.2", - "@standardnotes/sncrypto-web": "1.8.1", - "@standardnotes/snjs": "2.93.3", + "@standardnotes/filepicker": "1.10.5", + "@standardnotes/sncrypto-web": "1.8.2", + "@standardnotes/snjs": "2.94.3", + "@standardnotes/stylekit": "5.21.3", "@zip.js/zip.js": "^2.4.7", - "mobx": "^6.5.0", "mobx-react-lite": "^3.3.0", + "mobx": "^6.5.0", "preact": "^10.6.6", "qrcode.react": "^2.0.0", - "react-dnd": "^15.1.1", "react-dnd-html5-backend": "^15.1.2", - "react-dnd-touch-backend": "^15.1.1" + "react-dnd-touch-backend": "^15.1.1", + "react-dnd": "^15.1.1" }, "lint-staged": { "app/**/*.{js,ts,jsx,tsx}": "eslint --cache --fix", diff --git a/yarn.lock b/yarn.lock index 771c0a0db..b457e95f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2388,132 +2388,122 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@standardnotes/auth@^3.18.3": - version "3.18.3" - resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.18.3.tgz#2c2aa19ad56fc5f3f286dbef56bd8496d1e5380f" - integrity sha512-crdpVHJpnY574IIwBM1QcqNMULyvQ8nOnkH4DhFNcfWF7R+A+S4lMg2KxN9bR+j2gM/WWhbfcU8owJrz1+vsqA== +"@standardnotes/auth@^3.18.5": + version "3.18.5" + resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.18.5.tgz#11087e0bc31c42e489a7f546d317bbbcbd48d573" + integrity sha512-NSeU3A9iDyZFLLfv6jOyaweykMbjUUe0XoZo6lVifQucsRW0VJ4R6m3aeXqx6/zDwmHIBqVVzrX2bqQ7r2s9SA== dependencies: - "@standardnotes/common" "^1.19.1" + "@standardnotes/common" "^1.19.3" jsonwebtoken "^8.5.1" -"@standardnotes/common@^1.19.1": - version "1.19.1" - resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.19.1.tgz#45f0837b40bc1c583c22552a53429fe26c5ef498" - integrity sha512-O114ZdOvur6U1mmiPfvEo/TZjKRxHWBsAAAn+XTtS70E+2VqgvCwgiYT6bG1oUcoyrIiCdRPPyiTC/UPF4gkXg== +"@standardnotes/common@^1.19.3": + version "1.19.3" + resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.19.3.tgz#ae2ca493492eb5e0d9663abf736a9d7a26365c52" + integrity sha512-nMqH+grkIgnODr5EncbG9yHqIxSBHLVTiAb+NZ2EvkhLBiuVhP2UEDNs8KcZKfgNVGGDIHQbjEXZKGog9J6gZw== "@standardnotes/components@1.7.14": version "1.7.14" resolved "https://registry.yarnpkg.com/@standardnotes/components/-/components-1.7.14.tgz#ddd5b4d5787d3f90a4e1a88cfd95995f9267b1d1" integrity sha512-NDzP8/lmzgFBjxfmaE3OSOjPfozs3vednFfrjmjri5kCXlfClEISL6b/kh6B6wv/x9DIFveOsRj2Lrov+5IXdw== -"@standardnotes/domain-events@^2.26.6": - version "2.26.6" - resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.26.6.tgz#6d19099b147f60dfe69a458463aa81759846507a" - integrity sha512-CiF7MlFlZgwzMbOkcHGOy8FL1iGlSff9s7eUOlgOcYoidolFOF8HHJJn+d8+iwEvwANDwMJmrrGcFf5X9f0wzA== +"@standardnotes/domain-events@^2.26.9": + version "2.26.9" + resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.26.9.tgz#09d3ac547e263ff957a9a645a6c1f0039aa85ccd" + integrity sha512-iQCiCdLpMw3Yp3Z9q7GU9Z48tK2jAMI32VjsQtbyqOmjR+aVmo4e2oGyshm/2+5ymKUiG7xlx3Hl63RlQbGf1A== dependencies: - "@standardnotes/auth" "^3.18.3" - "@standardnotes/features" "^1.36.3" + "@standardnotes/auth" "^3.18.5" + "@standardnotes/features" "^1.37.2" -"@standardnotes/encryption@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.1.3.tgz#7554a6268d8f274c59bb9dd55117ba7dffefb5cd" - integrity sha512-0oelxlDMCR8I6l33R/CsdU9z25R+kCvYLt0FbypPCRaDOMTcjITR1R+VsnKznw/GbcMxcD1TTnDGRRQyLVmDmw== +"@standardnotes/encryption@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.2.2.tgz#27325dff46875871c26e208d77651532573a7afc" + integrity sha512-B9RjWltLuURg6qUdFTQZeQRKX2DoP6VL/6kS2Ay3ZkrEEcCNvxeuqr9yvXbZn2kKS0dUDfzfbYWOvoVxyyUwYA== dependencies: - "@standardnotes/models" "^1.1.1" - "@standardnotes/payloads" "^1.5.1" - "@standardnotes/responses" "^1.5.1" - "@standardnotes/services" "^1.7.1" + "@standardnotes/models" "^1.2.2" + "@standardnotes/responses" "^1.6.2" + "@standardnotes/services" "^1.8.2" -"@standardnotes/features@1.36.3", "@standardnotes/features@^1.36.3": - version "1.36.3" - resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.36.3.tgz#c3a2c2451fe2f036c5c94e51e86c804872028763" - integrity sha512-/38iTb22NULjtL8MitUeqsZdGwZgyP6t8p2LPc5sLFmeyU/CgZ22sh55GKAW5P/fm/oWyBqK6zjsVvtIH6AQug== +"@standardnotes/features@^1.37.2": + version "1.37.2" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.37.2.tgz#bedae2935a5659aa1fd30849e47ed167d45a7d3a" + integrity sha512-kNUCRMuLPmjjxWvE/OjHAewGZzv5/NAsqw/cHj+O4sbhRBWSv1dq+UzLzUxnj2kUsKObPjKqYeqo6WrXOnl3BQ== dependencies: - "@standardnotes/auth" "^3.18.3" - "@standardnotes/common" "^1.19.1" + "@standardnotes/auth" "^3.18.5" + "@standardnotes/common" "^1.19.3" -"@standardnotes/filepicker@1.10.4": - version "1.10.4" - resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.10.4.tgz#a69822381dcb7910ce7ce9040bb0466b3b950985" - integrity sha512-BnfKi+HFXX1e/fprZ2awvcrBSQkVZs1VN/hk7e9hH8qhQtbktfURKEEyOZoQnDu14BMa1tSdAVTjhXpxlbRRcA== +"@standardnotes/filepicker@1.10.5": + version "1.10.5" + resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.10.5.tgz#28c7cc68cb3337940a52e19b1dea76af343f2852" + integrity sha512-Tl9mCW/qAzupOtQAYlbPHgZU16ljtRGV0awOHT7+uulPYoyKJJ4cqlKRzFaaONTAN6GpICEeSqidcB/lb5e5uQ== -"@standardnotes/models@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@standardnotes/models/-/models-1.1.1.tgz#1a54253ded5c759ac8eeb1cb2878da73da2bd8a3" - integrity sha512-tl6Yjv/hlPd1xARF4o8vHNPyfoV0d5V/IkdlPA0I2GQh7pLPxHirwD5E2+rRnv59HPiV4ws+XwLtnnPZaZ38bg== +"@standardnotes/models@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@standardnotes/models/-/models-1.2.2.tgz#d67086319ad12ec5f6d267f77d353a0bd3740983" + integrity sha512-o762wf4pOobk4GcHJR0+lFVJeP/TifiP4OCM9ko23wkE6d6tWpM5SyvOUyZga2NNp5He8Pkm06Oo1ObxBYaWIw== dependencies: - "@standardnotes/payloads" "^1.5.1" + "@standardnotes/features" "^1.37.2" + "@standardnotes/responses" "^1.6.2" + "@standardnotes/utils" "^1.4.8" -"@standardnotes/payloads@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@standardnotes/payloads/-/payloads-1.5.1.tgz#f0cbe309721a14811d4b11cfce9a66d6fb9c2171" - integrity sha512-wqL31WPGFAQPMXICCf5Kxh3UOiaGbMu6ZE8cYmuZsMhti2h8FgIE04rcrzAh0/dSYtufrfHUnjhMmkPXZQAV2g== +"@standardnotes/responses@^1.6.2": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.2.tgz#665702c01bd25f10763ef0230fe9df775996c424" + integrity sha512-w/SNFITk7aipmN1nahH456xe/xKGPp+/dqpKF+LDoSl6C+UZ17NOtSVP/Q4nVIvGy57QOD8OvN7inCpSrLCEyw== dependencies: - "@standardnotes/common" "^1.19.1" - "@standardnotes/features" "^1.36.3" - "@standardnotes/utils" "^1.4.6" + "@standardnotes/auth" "^3.18.5" + "@standardnotes/common" "^1.19.3" + "@standardnotes/features" "^1.37.2" -"@standardnotes/responses@1.5.1", "@standardnotes/responses@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.5.1.tgz#a88171e0834ad363ce2284256f8630f8adee4ae3" - integrity sha512-8BodIxtIfSG9IEzCSQy2dmB9RR1FeoKYBwOrO4MKyX1TpvVPlr0dtGUxWtbt20kI49FSLU9QDeWeexNiXcDSrA== +"@standardnotes/services@^1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.8.2.tgz#3a95af1573636dc288dc4efd52de8568c2cd68fe" + integrity sha512-+iLVngtw0avyoUM4Sn0Kpmh99KTWYbhX/HiJLbnFoW5LJrqA2FarZRPUqYpiKuEcfcdZ4brmm5ipts45n1DXXw== dependencies: - "@standardnotes/auth" "^3.18.3" - "@standardnotes/common" "^1.19.1" - "@standardnotes/features" "^1.36.3" - "@standardnotes/payloads" "^1.5.1" + "@standardnotes/common" "^1.19.3" + "@standardnotes/models" "^1.2.2" + "@standardnotes/responses" "^1.6.2" + "@standardnotes/utils" "^1.4.8" -"@standardnotes/services@1.7.1", "@standardnotes/services@^1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.7.1.tgz#7c3f8f379478dfeeec3b9c87868a380213947533" - integrity sha512-xW5C/nqUomwxnN2zSS22t845PPRceevUNqtwFtPzodeaDNZpCMwLvHPqJhbXW6cYuPeVbPGn4ify8EnrGqu+/w== +"@standardnotes/settings@^1.13.3": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.13.3.tgz#6f46927f5cb85e3070ccd2bc6a91deadf1b9ef9f" + integrity sha512-hLb5ba8qFgKdhSYqJLX+7L5K2ZPwoqGlMrDDQTph+rQe08FnDoupCzz+uFOO1EJITI3XzrbvSW79GQYV0/5AkA== + +"@standardnotes/sncrypto-common@^1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.7.5.tgz#2c4ca3923970f5360f8ae01d40e2eaa957b75c12" + integrity sha512-VfGNDAlju4h7Ai7Z7shds41Lvl7JQruYc4pemkAzcmYgrXLHy/wHMUA0kanGTW7fVr3AYmbYCqO6lycldAmrwQ== + +"@standardnotes/sncrypto-web@1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.8.2.tgz#cf60a2ad85240e3ef56ccd5d1b45c29d16300966" + integrity sha512-C/2EkZrYBKTgnW4uluhJ3+GOnbbOL6ixg/c9pa2LZ9zT1vBRaS3vFIHC5hIMwrOsPE1QmIaJyiGtdzMA7A3VMw== dependencies: - "@standardnotes/common" "^1.19.1" - "@standardnotes/models" "^1.1.1" - "@standardnotes/responses" "^1.5.1" - "@standardnotes/utils" "^1.4.6" - -"@standardnotes/settings@1.13.2", "@standardnotes/settings@^1.13.2": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.13.2.tgz#32509823e9f1b885282d5f6e82a6fed5178e5ec2" - integrity sha512-kA2K2/xfS3dlPF//yy2GitH6bfFf4EmvNQ77OWMn1qZdLlTl4HTGps1Q5mX/BrLfC9HNutxqnLFr1N94vxdWlQ== - -"@standardnotes/sncrypto-common@^1.7.4": - version "1.7.4" - resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.7.4.tgz#970fbe84edbdc9d632c8774ec201f1e140f693d6" - integrity sha512-6O4RjyfNyCFV32sYCO7jxDt/UelPfbvaMQXYPJ3F58r5gyFdc9K/PxjWmKhzRVxTprNLr+4uWbf1FUERTdTuhg== - -"@standardnotes/sncrypto-web@1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.8.1.tgz#4e8192be81c2b6da2d39d4119ea95ceeb24c49a0" - integrity sha512-lTuFu8WNmUh/9+OApgBj0IU55svXEK5gQRpE9Wdn49mzPZUQx7FrrkQ8piKDBrkpxFoaeQXXgNNWk3CwGQ8WCQ== - dependencies: - "@standardnotes/sncrypto-common" "^1.7.4" + "@standardnotes/sncrypto-common" "^1.7.5" buffer "^6.0.3" libsodium-wrappers "^0.7.9" -"@standardnotes/snjs@2.93.3": - version "2.93.3" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.93.3.tgz#528c257994b8602666af0d80930e55421d9d5946" - integrity sha512-XZS8vBDqc80lEYKndyNIiyZSPme1DU7H6XNCUBH5mt41CF8qvxv76eTFSxmi2oZNrNNujl/Ez1jb23F7oUEMrw== +"@standardnotes/snjs@2.94.3": + version "2.94.3" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.94.3.tgz#93918769025f4883f80555df61c88a838de1c4c3" + integrity sha512-wfJg8n/p1QisPlxWSyW9dYqN79/Zr16lugryHG0Tc1vXRyLcDNbaYxvo5XR3gGjsml8zARn5NdXJEkBBdPN2xA== dependencies: - "@standardnotes/auth" "^3.18.3" - "@standardnotes/common" "^1.19.1" - "@standardnotes/domain-events" "^2.26.6" - "@standardnotes/encryption" "^1.1.3" - "@standardnotes/features" "^1.36.3" - "@standardnotes/models" "^1.1.1" - "@standardnotes/payloads" "^1.5.1" - "@standardnotes/responses" "^1.5.1" - "@standardnotes/services" "^1.7.1" - "@standardnotes/settings" "^1.13.2" - "@standardnotes/sncrypto-common" "^1.7.4" - "@standardnotes/utils" "^1.4.6" + "@standardnotes/auth" "^3.18.5" + "@standardnotes/common" "^1.19.3" + "@standardnotes/domain-events" "^2.26.9" + "@standardnotes/encryption" "^1.2.2" + "@standardnotes/features" "^1.37.2" + "@standardnotes/models" "^1.2.2" + "@standardnotes/responses" "^1.6.2" + "@standardnotes/services" "^1.8.2" + "@standardnotes/settings" "^1.13.3" + "@standardnotes/sncrypto-common" "^1.7.5" + "@standardnotes/utils" "^1.4.8" -"@standardnotes/stylekit@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@standardnotes/stylekit/-/stylekit-5.20.0.tgz#84ad262b39cbe307a9e06ffa8eb05119db8a7adb" - integrity sha512-R926S0NlzB97wq6PyX978s1g21KSymX3CjQCmbM3tfV7KFXfNjjDdWSTVetC8exZj/BPvsudfOtNA5bVsJ8RkA== +"@standardnotes/stylekit@5.21.3": + version "5.21.3" + resolved "https://registry.yarnpkg.com/@standardnotes/stylekit/-/stylekit-5.21.3.tgz#32d136328e7ec04f983fe635ec507a05084f3aac" + integrity sha512-0ILhE/xtuGBbBuMRurdWDuHrtMvIoJoMwu/2wp+2WNlYzWbSzq/TQ3MYYBUUS76K/nerxJwr7kBtZeJpCdWccg== dependencies: "@nanostores/preact" "^0.1.3" "@reach/listbox" "^0.16.2" @@ -2524,12 +2514,12 @@ nanostores "^0.5.10" prop-types "^15.8.1" -"@standardnotes/utils@^1.4.6": - version "1.4.6" - resolved "https://registry.yarnpkg.com/@standardnotes/utils/-/utils-1.4.6.tgz#19fadae46b594e301731989a836e1e9025ec4749" - integrity sha512-8k/ucqtfZd/tl7zBA5E988Kbo+9RiGq70ZNiXjvGhud8hX2JUfUtVqnpjTCIEChKyJhOJR3uVQZBHq9H9S0+lg== +"@standardnotes/utils@^1.4.8": + version "1.4.8" + resolved "https://registry.yarnpkg.com/@standardnotes/utils/-/utils-1.4.8.tgz#4c984220903bf64733961c50633de6adf62cb78f" + integrity sha512-sGROUIUdDncSyAb4w1O+qadJOTp18+9XO8MX08jOEPEMTfmVNFV1xYu6ZWe/0Ae4wCghBr8XeCCfVeE1XNyjTg== dependencies: - "@standardnotes/common" "^1.19.1" + "@standardnotes/common" "^1.19.3" dompurify "^2.3.6" lodash "^4.17.21" From c16f23a75fa82949fd87d2939cd2524e97259dc3 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Tue, 12 Apr 2022 00:05:08 +0530 Subject: [PATCH 012/102] feat: new lock screen and challenge modal design (#957) --- .../components/ApplicationView.tsx | 2 +- app/assets/javascripts/components/Button.tsx | 8 +- .../javascripts/components/ChallengeModal.tsx | 371 ------------------ .../ChallengeModal/ChallengeModal.tsx | 270 +++++++++++++ .../ChallengeModal/ChallengePrompt.tsx | 103 +++++ .../javascripts/components/DecoratedInput.tsx | 2 +- .../components/DecoratedPasswordInput.tsx | 2 +- .../javascripts/components/IconButton.tsx | 1 + .../javascripts/components/Menu/Menu.tsx | 3 +- app/assets/stylesheets/_modals.scss | 6 +- app/assets/stylesheets/_reach-sub.scss | 3 + app/assets/stylesheets/_sn.scss | 48 ++- 12 files changed, 434 insertions(+), 385 deletions(-) delete mode 100644 app/assets/javascripts/components/ChallengeModal.tsx create mode 100644 app/assets/javascripts/components/ChallengeModal/ChallengeModal.tsx create mode 100644 app/assets/javascripts/components/ChallengeModal/ChallengePrompt.tsx diff --git a/app/assets/javascripts/components/ApplicationView.tsx b/app/assets/javascripts/components/ApplicationView.tsx index 9f5191c1c..56c522bf0 100644 --- a/app/assets/javascripts/components/ApplicationView.tsx +++ b/app/assets/javascripts/components/ApplicationView.tsx @@ -17,7 +17,7 @@ import { NoteGroupView } from '@/components/NoteGroupView'; import { Footer } from '@/components/Footer'; import { SessionsModal } from '@/components/SessionsModal'; import { PreferencesViewWrapper } from '@/components/Preferences/PreferencesViewWrapper'; -import { ChallengeModal } from '@/components/ChallengeModal'; +import { ChallengeModal } from '@/components/ChallengeModal/ChallengeModal'; import { NotesContextMenu } from '@/components/NotesContextMenu'; import { PurchaseFlowWrapper } from '@/components/PurchaseFlow/PurchaseFlowWrapper'; import { render } from 'preact'; diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx index f91177d0b..1afc15904 100644 --- a/app/assets/javascripts/components/Button.tsx +++ b/app/assets/javascripts/components/Button.tsx @@ -22,8 +22,8 @@ const getClassName = ( let focusHoverStates = variant === 'normal' - ? 'focus:bg-contrast hover:bg-contrast' - : 'hover:brightness-130 focus:brightness-130'; + ? 'focus:bg-contrast focus:outline-none hover:bg-contrast' + : 'hover:brightness-130 focus:outline-none focus:brightness-130'; if (danger) { colors = @@ -39,8 +39,8 @@ const getClassName = ( : 'bg-grey-2 color-info-contrast'; focusHoverStates = variant === 'normal' - ? 'focus:bg-default hover:bg-default' - : 'focus:brightness-default hover:brightness-default'; + ? 'focus:bg-default focus:outline-none hover:bg-default' + : 'focus:brightness-default focus:outline-none hover:brightness-default'; } return `${baseClass} ${colors} ${borders} ${focusHoverStates} ${cursor}`; diff --git a/app/assets/javascripts/components/ChallengeModal.tsx b/app/assets/javascripts/components/ChallengeModal.tsx deleted file mode 100644 index ec98b8426..000000000 --- a/app/assets/javascripts/components/ChallengeModal.tsx +++ /dev/null @@ -1,371 +0,0 @@ -import { WebApplication } from '@/ui_models/application'; -import { Dialog } from '@reach/dialog'; -import { - ChallengeValue, - removeFromArray, - Challenge, - ChallengeReason, - ChallengePrompt, - ChallengeValidation, - ProtectionSessionDurations, -} from '@standardnotes/snjs'; -import { confirmDialog } from '@/services/alertService'; -import { STRING_SIGN_OUT_CONFIRMATION } from '@/strings'; -import { createRef } from 'preact'; -import { PureComponent } from '@/components/Abstract/PureComponent'; - -type InputValue = { - prompt: ChallengePrompt; - value: string | number | boolean; - invalid: boolean; -}; - -type Values = Record; - -type State = { - prompts: ChallengePrompt[]; - values: Partial; - processing: boolean; - forgotPasscode: boolean; - showForgotPasscodeLink: boolean; - processingPrompts: ChallengePrompt[]; - hasAccount: boolean; - protectedNoteAccessDuration: number; -}; - -type Props = { - challenge: Challenge; - application: WebApplication; - onDismiss: (challenge: Challenge) => void; -}; - -export class ChallengeModal extends PureComponent { - submitting = false; - protectionsSessionDurations = ProtectionSessionDurations; - protectionsSessionValidation = ChallengeValidation.ProtectionSessionDuration; - private initialFocusRef = createRef(); - - constructor(props: Props) { - super(props, props.application); - - const values = {} as Values; - const prompts = this.props.challenge.prompts; - for (const prompt of prompts) { - values[prompt.id] = { - prompt, - value: prompt.initialValue ?? '', - invalid: false, - }; - } - const showForgotPasscodeLink = [ - ChallengeReason.ApplicationUnlock, - ChallengeReason.Migration, - ].includes(this.props.challenge.reason); - this.state = { - prompts, - values, - processing: false, - forgotPasscode: false, - showForgotPasscodeLink, - hasAccount: this.application.hasAccount(), - processingPrompts: [], - protectedNoteAccessDuration: ProtectionSessionDurations[0].valueInSeconds, - }; - } - - componentDidMount(): void { - super.componentDidMount(); - - this.application.addChallengeObserver(this.props.challenge, { - onValidValue: (value) => { - this.state.values[value.prompt.id]!.invalid = false; - removeFromArray(this.state.processingPrompts, value.prompt); - this.reloadProcessingStatus(); - this.afterStateChange(); - }, - onInvalidValue: (value) => { - this.state.values[value.prompt.id]!.invalid = true; - /** If custom validation, treat all values together and not individually */ - if (!value.prompt.validates) { - this.setState({ processingPrompts: [], processing: false }); - } else { - removeFromArray(this.state.processingPrompts, value.prompt); - this.reloadProcessingStatus(); - } - this.afterStateChange(); - }, - onComplete: () => { - this.dismiss(); - }, - onCancel: () => { - this.dismiss(); - }, - }); - } - - deinit() { - (this.application as unknown) = undefined; - (this.props.challenge as unknown) = undefined; - super.deinit(); - } - - reloadProcessingStatus() { - return this.setState({ - processing: this.state.processingPrompts.length > 0, - }); - } - - destroyLocalData = async () => { - if ( - await confirmDialog({ - text: STRING_SIGN_OUT_CONFIRMATION, - confirmButtonStyle: 'danger', - }) - ) { - this.dismiss(); - this.application.user.signOut(); - } - }; - - cancel = () => { - if (this.props.challenge.cancelable) { - this.application.cancelChallenge(this.props.challenge); - } - }; - - onForgotPasscodeClick = () => { - this.setState({ - forgotPasscode: true, - }); - }; - - onTextValueChange = (prompt: ChallengePrompt) => { - const values = this.state.values; - values[prompt.id]!.invalid = false; - this.setState({ values }); - }; - - onNumberValueChange(prompt: ChallengePrompt, value: number) { - const values = this.state.values; - values[prompt.id]!.invalid = false; - values[prompt.id]!.value = value; - this.setState({ values }); - } - - validate() { - let failed = 0; - for (const prompt of this.state.prompts) { - const value = this.state.values[prompt.id]!; - if (typeof value.value === 'string' && value.value.length === 0) { - this.state.values[prompt.id]!.invalid = true; - failed++; - } - } - return failed === 0; - } - - submit = async () => { - if (!this.validate()) { - return; - } - if (this.submitting || this.state.processing) { - return; - } - this.submitting = true; - await this.setState({ processing: true }); - const values: ChallengeValue[] = []; - for (const inputValue of Object.values(this.state.values)) { - const rawValue = inputValue!.value; - const value = new ChallengeValue(inputValue!.prompt, rawValue); - values.push(value); - } - const processingPrompts = values.map((v) => v.prompt); - await this.setState({ - processingPrompts: processingPrompts, - processing: processingPrompts.length > 0, - }); - /** - * Unfortunately neccessary to wait 50ms so that the above setState call completely - * updates the UI to change processing state, before we enter into UI blocking operation - * (crypto key generation) - */ - setTimeout(() => { - if (values.length > 0) { - this.application.submitValuesForChallenge(this.props.challenge, values); - } else { - this.setState({ processing: false }); - } - this.submitting = false; - }, 50); - }; - - afterStateChange() { - this.render(); - } - - dismiss = () => { - this.props.onDismiss(this.props.challenge); - }; - - private renderChallengePrompts() { - return this.state.prompts.map((prompt, index) => ( - <> - {/** ProtectionSessionDuration can't just be an input field */} - {prompt.validation === ChallengeValidation.ProtectionSessionDuration ? ( -
-
-
Allow protected access for
- {ProtectionSessionDurations.map((option) => ( - { - event.preventDefault(); - this.onNumberValueChange(prompt, option.valueInSeconds); - }} - > - {option.label} - - ))} -
-
- ) : ( -
-
{ - event.preventDefault(); - this.submit(); - }} - > - { - const value = (event.target as HTMLInputElement).value; - this.state.values[prompt.id]!.value = value; - this.onTextValueChange(prompt); - }} - ref={index === 0 ? this.initialFocusRef : undefined} - placeholder={prompt.title} - type={prompt.secureTextEntry ? 'password' : 'text'} - /> -
-
- )} - - {this.state.values[prompt.id]!.invalid && ( -
- -
- )} - - )); - } - - render() { - if (!this.state.prompts) { - return <>; - } - return ( - { - if (this.props.challenge.cancelable) { - this.cancel(); - } - }} - > -
-
-
-
-
- {this.props.challenge.modalTitle} -
-
-
-
-
- {this.props.challenge.heading} -
- {this.props.challenge.subheading && ( -
- {this.props.challenge.subheading} -
- )} -
- -
- {this.renderChallengePrompts()} -
-
-
- - {this.props.challenge.cancelable && ( - <> -
- this.cancel()} - > - Cancel - - - )} -
- {this.state.showForgotPasscodeLink && ( -
- {this.state.forgotPasscode ? ( - <> -

- {this.state.hasAccount - ? 'If you forgot your application passcode, your ' + - 'only option is to clear your local data from this ' + - 'device and sign back in to your account.' - : 'If you forgot your application passcode, your ' + - 'only option is to delete your data.'} -

- { - this.destroyLocalData(); - }} - > - Delete Local Data - - - ) : ( - this.onForgotPasscodeClick()} - > - Forgot your passcode? - - )} -
-
- )} -
-
-
-
- ); - } -} diff --git a/app/assets/javascripts/components/ChallengeModal/ChallengeModal.tsx b/app/assets/javascripts/components/ChallengeModal/ChallengeModal.tsx new file mode 100644 index 000000000..4f006c1db --- /dev/null +++ b/app/assets/javascripts/components/ChallengeModal/ChallengeModal.tsx @@ -0,0 +1,270 @@ +import { WebApplication } from '@/ui_models/application'; +import { DialogContent, DialogOverlay } from '@reach/dialog'; +import { + ButtonType, + Challenge, + ChallengePrompt, + ChallengeReason, + ChallengeValue, + removeFromArray, +} from '@standardnotes/snjs'; +import { ProtectedIllustration } from '@standardnotes/stylekit'; +import { FunctionComponent } from 'preact'; +import { useCallback, useEffect, useState } from 'preact/hooks'; +import { Button } from '../Button'; +import { Icon } from '../Icon'; +import { ChallengeModalPrompt } from './ChallengePrompt'; + +type InputValue = { + prompt: ChallengePrompt; + value: string | number | boolean; + invalid: boolean; +}; + +export type ChallengeModalValues = Record; + +type Props = { + application: WebApplication; + challenge: Challenge; + onDismiss: (challenge: Challenge) => Promise; +}; + +const validateValues = ( + values: ChallengeModalValues, + prompts: ChallengePrompt[] +): ChallengeModalValues | undefined => { + let hasInvalidValues = false; + const validatedValues = { ...values }; + for (const prompt of prompts) { + const value = validatedValues[prompt.id]; + if (typeof value.value === 'string' && value.value.length === 0) { + validatedValues[prompt.id].invalid = true; + hasInvalidValues = true; + } + } + if (!hasInvalidValues) { + return validatedValues; + } +}; + +export const ChallengeModal: FunctionComponent = ({ + application, + challenge, + onDismiss, +}) => { + const [values, setValues] = useState(() => { + const values = {} as ChallengeModalValues; + for (const prompt of challenge.prompts) { + values[prompt.id] = { + prompt, + value: prompt.initialValue ?? '', + invalid: false, + }; + } + return values; + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [, setProcessingPrompts] = useState([]); + const [bypassModalFocusLock, setBypassModalFocusLock] = useState(false); + const shouldShowForgotPasscode = [ + ChallengeReason.ApplicationUnlock, + ChallengeReason.Migration, + ].includes(challenge.reason); + + const submit = async () => { + const validatedValues = validateValues(values, challenge.prompts); + if (!validatedValues) { + return; + } + if (isSubmitting || isProcessing) { + return; + } + setIsSubmitting(true); + setIsProcessing(true); + const valuesToProcess: ChallengeValue[] = []; + for (const inputValue of Object.values(validatedValues)) { + const rawValue = inputValue.value; + const value = new ChallengeValue(inputValue.prompt, rawValue); + valuesToProcess.push(value); + } + const processingPrompts = valuesToProcess.map((v) => v.prompt); + setIsProcessing(processingPrompts.length > 0); + setProcessingPrompts(processingPrompts); + /** + * Unfortunately neccessary to wait 50ms so that the above setState call completely + * updates the UI to change processing state, before we enter into UI blocking operation + * (crypto key generation) + */ + setTimeout(() => { + if (valuesToProcess.length > 0) { + application.submitValuesForChallenge(challenge, valuesToProcess); + } else { + setIsProcessing(false); + } + setIsSubmitting(false); + }, 50); + }; + + const onValueChange = useCallback( + (value: string | number, prompt: ChallengePrompt) => { + const newValues = { ...values }; + newValues[prompt.id].invalid = false; + newValues[prompt.id].value = value; + setValues(newValues); + }, + [values] + ); + + const closeModal = () => { + if (challenge.cancelable) { + onDismiss(challenge); + } + }; + + useEffect(() => { + const removeChallengeObserver = application.addChallengeObserver( + challenge, + { + onValidValue: (value) => { + setValues((values) => { + const newValues = { ...values }; + newValues[value.prompt.id].invalid = false; + return newValues; + }); + setProcessingPrompts((currentlyProcessingPrompts) => { + const processingPrompts = currentlyProcessingPrompts.slice(); + removeFromArray(processingPrompts, value.prompt); + setIsProcessing(processingPrompts.length > 0); + return processingPrompts; + }); + }, + onInvalidValue: (value) => { + setValues((values) => { + const newValues = { ...values }; + newValues[value.prompt.id].invalid = true; + return newValues; + }); + /** If custom validation, treat all values together and not individually */ + if (!value.prompt.validates) { + setProcessingPrompts([]); + setIsProcessing(false); + } else { + setProcessingPrompts((currentlyProcessingPrompts) => { + const processingPrompts = currentlyProcessingPrompts.slice(); + removeFromArray(processingPrompts, value.prompt); + setIsProcessing(processingPrompts.length > 0); + return processingPrompts; + }); + } + }, + onComplete: () => { + onDismiss(challenge); + }, + onCancel: () => { + onDismiss(challenge); + }, + } + ); + + return () => { + removeChallengeObserver(); + }; + }, [application, challenge, onDismiss]); + + if (!challenge.prompts) { + return null; + } + + return ( + + + {challenge.cancelable && ( + + )} + +
+ {challenge.heading} +
+
+ {challenge.subheading} +
+
{ + e.preventDefault(); + submit(); + }} + > + {challenge.prompts.map((prompt, index) => ( + + ))} + + + {shouldShowForgotPasscode && ( + + )} +
+
+ ); +}; diff --git a/app/assets/javascripts/components/ChallengeModal/ChallengePrompt.tsx b/app/assets/javascripts/components/ChallengeModal/ChallengePrompt.tsx new file mode 100644 index 000000000..24be05e2c --- /dev/null +++ b/app/assets/javascripts/components/ChallengeModal/ChallengePrompt.tsx @@ -0,0 +1,103 @@ +import { + ChallengePrompt, + ChallengeValidation, + ProtectionSessionDurations, +} from '@standardnotes/snjs'; +import { FunctionComponent } from 'preact'; +import { useEffect, useRef } from 'preact/hooks'; +import { DecoratedInput } from '../DecoratedInput'; +import { DecoratedPasswordInput } from '../DecoratedPasswordInput'; +import { ChallengeModalValues } from './ChallengeModal'; + +type Props = { + prompt: ChallengePrompt; + values: ChallengeModalValues; + index: number; + onValueChange: (value: string | number, prompt: ChallengePrompt) => void; + isInvalid: boolean; +}; + +export const ChallengeModalPrompt: FunctionComponent = ({ + prompt, + values, + index, + onValueChange, + isInvalid, +}) => { + const inputRef = useRef(null); + + useEffect(() => { + if (index === 0) { + inputRef.current?.focus(); + } + }, [index]); + + useEffect(() => { + if (isInvalid) { + inputRef.current?.focus(); + } + }, [isInvalid]); + + return ( + <> + {prompt.validation === ChallengeValidation.ProtectionSessionDuration ? ( +
+
+ Allow protected access for +
+
+ {ProtectionSessionDurations.map((option) => { + const selected = + option.valueInSeconds === values[prompt.id].value; + return ( + + ); + })} +
+
+ ) : prompt.secureTextEntry ? ( + onValueChange(value, prompt)} + /> + ) : ( + onValueChange(value, prompt)} + /> + )} + {isInvalid && ( +
+ Invalid authentication, please try again. +
+ )} + + ); +}; diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx index 5307106c7..a2e23229c 100644 --- a/app/assets/javascripts/components/DecoratedInput.tsx +++ b/app/assets/javascripts/components/DecoratedInput.tsx @@ -23,7 +23,7 @@ const getClassNames = ( container: `flex items-stretch position-relative bg-default border-1 border-solid border-main rounded focus-within:ring-info overflow-hidden ${ !hasLeftDecorations && !hasRightDecorations ? 'px-2 py-1.5' : '' }`, - input: `w-full border-0 focus:shadow-none ${ + input: `w-full border-0 focus:shadow-none bg-transparent ${ !hasLeftDecorations && hasRightDecorations ? 'pl-2' : '' } ${hasRightDecorations ? 'pr-2' : ''}`, disabled: 'bg-grey-5 cursor-not-allowed', diff --git a/app/assets/javascripts/components/DecoratedPasswordInput.tsx b/app/assets/javascripts/components/DecoratedPasswordInput.tsx index 32e21a56a..22fdaf514 100644 --- a/app/assets/javascripts/components/DecoratedPasswordInput.tsx +++ b/app/assets/javascripts/components/DecoratedPasswordInput.tsx @@ -9,7 +9,7 @@ const Toggle: FunctionComponent<{ setIsToggled: StateUpdater; }> = ({ isToggled, setIsToggled }) => ( = ({ const focusableClass = focusable ? '' : 'focus:shadow-none'; return (
) : null} - ); - } -); + ) + }, +) diff --git a/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx b/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx similarity index 60% rename from app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx rename to app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx index cbb981c8c..bf633af22 100644 --- a/app/assets/javascripts/components/AccountMenu/ConfirmPassword.tsx +++ b/app/assets/javascripts/Components/AccountMenu/ConfirmPassword.tsx @@ -1,96 +1,96 @@ -import { STRING_NON_MATCHING_PASSWORDS } from '@/strings'; -import { WebApplication } from '@/ui_models/application'; -import { AppState } from '@/ui_models/app_state'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; -import { AccountMenuPane } from '.'; -import { Button } from '../Button'; -import { Checkbox } from '../Checkbox'; -import { DecoratedPasswordInput } from '../DecoratedPasswordInput'; -import { Icon } from '../Icon'; -import { IconButton } from '../IconButton'; +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 { useEffect, useRef, useState } from 'preact/hooks' +import { AccountMenuPane } from '.' +import { Button } from '@/Components/Button/Button' +import { Checkbox } from '@/Components/Checkbox' +import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput' +import { Icon } from '@/Components/Icon' +import { IconButton } from '@/Components/Button/IconButton' type Props = { - appState: AppState; - application: WebApplication; - setMenuPane: (pane: AccountMenuPane) => void; - email: string; - password: string; -}; + appState: AppState + application: WebApplication + setMenuPane: (pane: AccountMenuPane) => void + email: string + 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 { 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(); - }, []); + passwordInputRef.current?.focus() + }, []) const handlePasswordChange = (text: string) => { - setConfirmPassword(text); - }; + setConfirmPassword(text) + } const handleEphemeralChange = () => { - setIsEphemeral(!isEphemeral); - }; + setIsEphemeral(!isEphemeral) + } const handleShouldMergeChange = () => { - setShouldMergeLocal(!shouldMergeLocal); - }; + setShouldMergeLocal(!shouldMergeLocal) + } const handleKeyDown = (e: KeyboardEvent) => { if (error.length) { - setError(''); + setError('') } if (e.key === 'Enter') { - handleConfirmFormSubmit(e); + handleConfirmFormSubmit(e) } - }; + } const handleConfirmFormSubmit = (e: Event) => { - e.preventDefault(); + e.preventDefault() if (!password) { - passwordInputRef.current?.focus(); - return; + passwordInputRef.current?.focus() + return } if (password === confirmPassword) { - setIsRegistering(true); + setIsRegistering(true) application .register(email, password, isEphemeral, shouldMergeLocal) .then((res) => { if (res.error) { - throw new Error(res.error.message); + throw new Error(res.error.message) } - appState.accountMenu.closeAccountMenu(); - appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu); + appState.accountMenu.closeAccountMenu() + appState.accountMenu.setCurrentPane(AccountMenuPane.GeneralMenu) }) .catch((err) => { - console.error(err); - setError(err.message); + console.error(err) + setError(err.message) }) .finally(() => { - setIsRegistering(false); - }); + setIsRegistering(false) + }) } else { - setError(STRING_NON_MATCHING_PASSWORDS); - setConfirmPassword(''); - passwordInputRef.current?.focus(); + setError(STRING_NON_MATCHING_PASSWORDS) + setConfirmPassword('') + passwordInputRef.current?.focus() } - }; + } const handleGoBack = () => { - setMenuPane(AccountMenuPane.Register); - }; + setMenuPane(AccountMenuPane.Register) + } return ( <> @@ -110,8 +110,7 @@ export const ConfirmPassword: FunctionComponent = observer( Standard Notes does not have a password reset option - . If you forget your password, you will permanently lose access to - your data. + . If you forget your password, you will permanently lose access to your data.
= observer( {error ?
{error}
: null}
)} -
+
@@ -102,8 +97,8 @@ export const GeneralAccountMenu: FunctionComponent = observer( <>
- You’re offline. Sign in to sync your notes and preferences - across all your devices and enable end-to-end encryption. + You’re offline. Sign in to sync your notes and preferences across all your devices + and enable end-to-end encryption.
@@ -116,9 +111,7 @@ export const GeneralAccountMenu: FunctionComponent = observer( isOpen={appState.accountMenu.show} a11yLabel="General account menu" closeMenu={closeMenu} - initialFocus={ - !application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX - } + initialFocus={!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX} > = observer( { - appState.accountMenu.closeAccountMenu(); - appState.preferences.setCurrentPane('account'); - appState.preferences.openPreferences(); + appState.accountMenu.closeAccountMenu() + appState.preferences.setCurrentPane('account') + appState.preferences.openPreferences() }} > @@ -143,7 +136,7 @@ export const GeneralAccountMenu: FunctionComponent = observer( { - setMenuPane(AccountMenuPane.Register); + setMenuPane(AccountMenuPane.Register) }} > @@ -152,7 +145,7 @@ export const GeneralAccountMenu: FunctionComponent = observer( { - setMenuPane(AccountMenuPane.SignIn); + setMenuPane(AccountMenuPane.SignIn) }} > @@ -164,9 +157,9 @@ export const GeneralAccountMenu: FunctionComponent = observer( className="justify-between" type={MenuItemType.IconButton} onClick={() => { - appState.accountMenu.closeAccountMenu(); - appState.preferences.setCurrentPane('help-feedback'); - appState.preferences.openPreferences(); + appState.accountMenu.closeAccountMenu() + appState.preferences.setCurrentPane('help-feedback') + appState.preferences.openPreferences() }} >
@@ -181,7 +174,7 @@ export const GeneralAccountMenu: FunctionComponent = observer( { - appState.accountMenu.setSigningOut(true); + appState.accountMenu.setSigningOut(true) }} > @@ -191,6 +184,6 @@ export const GeneralAccountMenu: FunctionComponent = observer( ) : null} - ); - } -); + ) + }, +) diff --git a/app/assets/javascripts/components/AccountMenu/SignIn.tsx b/app/assets/javascripts/Components/AccountMenu/SignIn.tsx similarity index 62% rename from app/assets/javascripts/components/AccountMenu/SignIn.tsx rename to app/assets/javascripts/Components/AccountMenu/SignIn.tsx index 0d4deacaa..7df3b8260 100644 --- a/app/assets/javascripts/components/AccountMenu/SignIn.tsx +++ b/app/assets/javascripts/Components/AccountMenu/SignIn.tsx @@ -1,134 +1,134 @@ -import { WebApplication } from '@/ui_models/application'; -import { AppState } from '@/ui_models/app_state'; -import { isDev } from '@/utils'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; -import { AccountMenuPane } from '.'; -import { Button } from '../Button'; -import { Checkbox } from '../Checkbox'; -import { DecoratedInput } from '../DecoratedInput'; -import { DecoratedPasswordInput } from '../DecoratedPasswordInput'; -import { Icon } from '../Icon'; -import { IconButton } from '../IconButton'; -import { AdvancedOptions } from './AdvancedOptions'; +import { WebApplication } from '@/UIModels/Application' +import { AppState } from '@/UIModels/AppState' +import { isDev } from '@/Utils' +import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'preact' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { AccountMenuPane } from '.' +import { Button } from '@/Components/Button/Button' +import { Checkbox } from '@/Components/Checkbox' +import { DecoratedInput } from '@/Components/Input/DecoratedInput' +import { DecoratedPasswordInput } from '@/Components/Input/DecoratedPasswordInput' +import { Icon } from '@/Components/Icon' +import { IconButton } from '@/Components/Button/IconButton' +import { AdvancedOptions } from './AdvancedOptions' type Props = { - appState: AppState; - application: WebApplication; - setMenuPane: (pane: AccountMenuPane) => void; -}; + appState: AppState + application: WebApplication + setMenuPane: (pane: AccountMenuPane) => void +} export const SignInPane: FunctionComponent = observer( ({ application, appState, setMenuPane }) => { - const { notesAndTagsCount } = appState.accountMenu; - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const [isEphemeral, setIsEphemeral] = useState(false); + const { notesAndTagsCount } = appState.accountMenu + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [isEphemeral, setIsEphemeral] = useState(false) - const [isStrictSignin, setIsStrictSignin] = useState(false); - const [isSigningIn, setIsSigningIn] = useState(false); - const [shouldMergeLocal, setShouldMergeLocal] = useState(true); - const [isVault, setIsVault] = useState(false); + const [isStrictSignin, setIsStrictSignin] = useState(false) + const [isSigningIn, setIsSigningIn] = useState(false) + const [shouldMergeLocal, setShouldMergeLocal] = useState(true) + const [isVault, setIsVault] = useState(false) - const emailInputRef = useRef(null); - const passwordInputRef = useRef(null); + const emailInputRef = useRef(null) + const passwordInputRef = useRef(null) useEffect(() => { if (emailInputRef?.current) { - emailInputRef.current?.focus(); + emailInputRef.current?.focus() } if (isDev && window.devAccountEmail) { - setEmail(window.devAccountEmail); - setPassword(window.devAccountPassword as string); + setEmail(window.devAccountEmail) + setPassword(window.devAccountPassword as string) } - }, []); + }, []) const resetInvalid = () => { if (error.length) { - setError(''); + setError('') } - }; + } const handleEmailChange = (text: string) => { - setEmail(text); - }; + setEmail(text) + } const handlePasswordChange = (text: string) => { if (error.length) { - setError(''); + setError('') } - setPassword(text); - }; + setPassword(text) + } const handleEphemeralChange = () => { - setIsEphemeral(!isEphemeral); - }; + setIsEphemeral(!isEphemeral) + } const handleStrictSigninChange = () => { - setIsStrictSignin(!isStrictSignin); - }; + setIsStrictSignin(!isStrictSignin) + } const handleShouldMergeChange = () => { - setShouldMergeLocal(!shouldMergeLocal); - }; + setShouldMergeLocal(!shouldMergeLocal) + } const signIn = () => { - setIsSigningIn(true); - emailInputRef?.current?.blur(); - passwordInputRef?.current?.blur(); + setIsSigningIn(true) + emailInputRef?.current?.blur() + passwordInputRef?.current?.blur() application .signIn(email, password, isStrictSignin, isEphemeral, shouldMergeLocal) .then((res) => { if (res.error) { - throw new Error(res.error.message); + throw new Error(res.error.message) } - appState.accountMenu.closeAccountMenu(); + appState.accountMenu.closeAccountMenu() }) .catch((err) => { - console.error(err); - setError(err.message ?? err.toString()); - setPassword(''); - passwordInputRef?.current?.blur(); + console.error(err) + setError(err.message ?? err.toString()) + setPassword('') + passwordInputRef?.current?.blur() }) .finally(() => { - setIsSigningIn(false); - }); - }; + setIsSigningIn(false) + }) + } const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { - handleSignInFormSubmit(e); + handleSignInFormSubmit(e) } - }; + } const onVaultChange = useCallback( (newIsVault: boolean, vaultedEmail?: string) => { - setIsVault(newIsVault); + setIsVault(newIsVault) if (newIsVault && vaultedEmail) { - setEmail(vaultedEmail); + setEmail(vaultedEmail) } }, - [setEmail] - ); + [setEmail], + ) const handleSignInFormSubmit = (e: Event) => { - e.preventDefault(); + e.preventDefault() if (!email || email.length === 0) { - emailInputRef?.current?.focus(); - return; + emailInputRef?.current?.focus() + return } if (!password || password.length === 0) { - passwordInputRef?.current?.focus(); - return; + passwordInputRef?.current?.focus() + return } - signIn(); - }; + signIn() + } return ( <> @@ -201,6 +201,6 @@ export const SignInPane: FunctionComponent = observer( onStrictSignInChange={handleStrictSigninChange} /> - ); - } -); + ) + }, +) diff --git a/app/assets/javascripts/components/AccountMenu/User.tsx b/app/assets/javascripts/Components/AccountMenu/User.tsx similarity index 68% rename from app/assets/javascripts/components/AccountMenu/User.tsx rename to app/assets/javascripts/Components/AccountMenu/User.tsx index 7815d4e74..2752a0504 100644 --- a/app/assets/javascripts/components/AccountMenu/User.tsx +++ b/app/assets/javascripts/Components/AccountMenu/User.tsx @@ -1,16 +1,16 @@ -import { observer } from 'mobx-react-lite'; -import { AppState } from '@/ui_models/app_state'; -import { WebApplication } from '@/ui_models/application'; -import { User as UserType } from '@standardnotes/snjs'; +import { observer } from 'mobx-react-lite' +import { AppState } from '@/UIModels/AppState' +import { WebApplication } from '@/UIModels/Application' +import { User as UserType } from '@standardnotes/snjs' type Props = { - appState: AppState; - application: WebApplication; -}; + appState: AppState + application: WebApplication +} const User = observer(({ appState, application }: Props) => { - const { server } = appState.accountMenu; - const user = application.getUser() as UserType; + const { server } = appState.accountMenu + const user = application.getUser() as UserType return (
@@ -18,8 +18,7 @@ const User = observer(({ appState, application }: Props) => {
Sync Unreachable
- Hmm...we can't seem to sync your account. The reason:{' '} - {appState.sync.errorMessage} + Hmm...we can't seem to sync your account. The reason: {appState.sync.errorMessage}
{
- ); -}); + ) +}) -export default User; +export default User diff --git a/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx similarity index 67% rename from app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx rename to app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx index 5f222726f..11026f65b 100644 --- a/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx +++ b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceMenuItem.tsx @@ -1,16 +1,16 @@ -import { Icon } from '@/components/Icon'; -import { MenuItem, MenuItemType } from '@/components/Menu/MenuItem'; -import { KeyboardKey } from '@/services/ioService'; -import { ApplicationDescriptor } from '@standardnotes/snjs/dist/@types'; -import { FunctionComponent } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; +import { Icon } from '@/Components/Icon' +import { MenuItem, MenuItemType } from '@/Components/Menu/MenuItem' +import { KeyboardKey } from '@/Services/IOService' +import { ApplicationDescriptor } from '@standardnotes/snjs/dist/@types' +import { FunctionComponent } from 'preact' +import { useEffect, useRef, useState } from 'preact/hooks' type Props = { - descriptor: ApplicationDescriptor; - onClick: () => void; - onDelete: () => void; - renameDescriptor: (label: string) => void; -}; + descriptor: ApplicationDescriptor + onClick: () => void + onDelete: () => void + renameDescriptor: (label: string) => void +} export const WorkspaceMenuItem: FunctionComponent = ({ descriptor, @@ -18,26 +18,26 @@ export const WorkspaceMenuItem: FunctionComponent = ({ onDelete, renameDescriptor, }) => { - const [isRenaming, setIsRenaming] = useState(false); - const inputRef = useRef(null); + const [isRenaming, setIsRenaming] = useState(false) + const inputRef = useRef(null) useEffect(() => { if (isRenaming) { - inputRef.current?.focus(); + inputRef.current?.focus() } - }, [isRenaming]); + }, [isRenaming]) const handleInputKeyDown = (event: KeyboardEvent) => { if (event.key === KeyboardKey.Enter) { - inputRef.current?.blur(); + inputRef.current?.blur() } - }; + } const handleInputBlur = (event: FocusEvent) => { - const name = (event.target as HTMLInputElement).value; - renameDescriptor(name); - setIsRenaming(false); - }; + const name = (event.target as HTMLInputElement).value + renameDescriptor(name) + setIsRenaming(false) + } return ( = ({
- ); -}; + ) +} diff --git a/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx new file mode 100644 index 000000000..876b4c291 --- /dev/null +++ b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx @@ -0,0 +1,64 @@ +import { ApplicationGroup } from '@/UIModels/ApplicationGroup' +import { AppState } from '@/UIModels/AppState' +import { ApplicationDescriptor } from '@standardnotes/snjs' +import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'preact' +import { useEffect, useState } from 'preact/hooks' +import { Icon } from '@/Components/Icon' +import { Menu } from '@/Components/Menu/Menu' +import { MenuItem, MenuItemSeparator, MenuItemType } from '@/Components/Menu/MenuItem' +import { WorkspaceMenuItem } from './WorkspaceMenuItem' + +type Props = { + mainApplicationGroup: ApplicationGroup + appState: AppState + isOpen: boolean +} + +export const WorkspaceSwitcherMenu: FunctionComponent = observer( + ({ mainApplicationGroup, appState, isOpen }) => { + const [applicationDescriptors, setApplicationDescriptors] = useState( + [], + ) + + useEffect(() => { + const removeAppGroupObserver = mainApplicationGroup.addApplicationChangeObserver(() => { + const applicationDescriptors = mainApplicationGroup.getDescriptors() + setApplicationDescriptors(applicationDescriptors) + }) + + return () => { + removeAppGroupObserver() + } + }, [mainApplicationGroup]) + + return ( + + {applicationDescriptors.map((descriptor) => ( + { + appState.accountMenu.setSigningOut(true) + }} + onClick={() => { + mainApplicationGroup.loadApplicationForDescriptor(descriptor).catch(console.error) + }} + renameDescriptor={(label: string) => + mainApplicationGroup.renameDescriptor(descriptor, label) + } + /> + ))} + + { + mainApplicationGroup.addNewApplication().catch(console.error) + }} + > + + Add another workspace + + + ) + }, +) diff --git a/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx similarity index 55% rename from app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx rename to app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx index d369c3977..284e0a400 100644 --- a/app/assets/javascripts/components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx +++ b/app/assets/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx @@ -1,53 +1,47 @@ -import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants'; -import { ApplicationGroup } from '@/ui_models/application_group'; -import { AppState } from '@/ui_models/app_state'; -import { - calculateSubmenuStyle, - SubmenuStyle, -} from '@/utils/calculateSubmenuStyle'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; -import { Icon } from '../../Icon'; -import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu'; +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' +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 { useEffect, useRef, useState } from 'preact/hooks' +import { Icon } from '@/Components/Icon' +import { WorkspaceSwitcherMenu } from './WorkspaceSwitcherMenu' type Props = { - mainApplicationGroup: ApplicationGroup; - appState: AppState; -}; + mainApplicationGroup: ApplicationGroup + appState: AppState +} export const WorkspaceSwitcherOption: FunctionComponent = observer( ({ mainApplicationGroup, appState }) => { - const buttonRef = useRef(null); - const menuRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - const [menuStyle, setMenuStyle] = useState(); + const buttonRef = useRef(null) + const menuRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const [menuStyle, setMenuStyle] = useState() const toggleMenu = () => { if (!isOpen) { - const menuPosition = calculateSubmenuStyle(buttonRef.current); + const menuPosition = calculateSubmenuStyle(buttonRef.current) if (menuPosition) { - setMenuStyle(menuPosition); + setMenuStyle(menuPosition) } } - setIsOpen(!isOpen); - }; + setIsOpen(!isOpen) + } useEffect(() => { if (isOpen) { setTimeout(() => { - const newMenuPosition = calculateSubmenuStyle( - buttonRef.current, - menuRef.current - ); + const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current) if (newMenuPosition) { - setMenuStyle(newMenuPosition); + setMenuStyle(newMenuPosition) } - }); + }) } - }, [isOpen]); + }, [isOpen]) return ( <> @@ -78,6 +72,6 @@ export const WorkspaceSwitcherOption: FunctionComponent = observer(
)} - ); - } -); + ) + }, +) diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/Components/AccountMenu/index.tsx similarity index 54% rename from app/assets/javascripts/components/AccountMenu/index.tsx rename to app/assets/javascripts/Components/AccountMenu/index.tsx index 9e2e1381a..cf4ae5772 100644 --- a/app/assets/javascripts/components/AccountMenu/index.tsx +++ b/app/assets/javascripts/Components/AccountMenu/index.tsx @@ -1,15 +1,15 @@ -import { observer } from 'mobx-react-lite'; -import { useCloseOnClickOutside } from '@/components/utils'; -import { AppState } from '@/ui_models/app_state'; -import { WebApplication } from '@/ui_models/application'; -import { 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 { ApplicationGroup } from '@/ui_models/application_group'; +import { observer } from 'mobx-react-lite' +import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' +import { AppState } from '@/UIModels/AppState' +import { WebApplication } from '@/UIModels/Application' +import { useRef, useState } from 'preact/hooks' +import { 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 { ApplicationGroup } from '@/UIModels/ApplicationGroup' export enum AccountMenuPane { GeneralMenu, @@ -19,32 +19,25 @@ export enum AccountMenuPane { } type Props = { - appState: AppState; - application: WebApplication; - onClickOutside: () => void; - mainApplicationGroup: ApplicationGroup; -}; + appState: AppState + application: WebApplication + onClickOutside: () => void + mainApplicationGroup: ApplicationGroup +} type PaneSelectorProps = { - appState: AppState; - application: WebApplication; - mainApplicationGroup: ApplicationGroup; - menuPane: AccountMenuPane; - setMenuPane: (pane: AccountMenuPane) => void; - closeMenu: () => void; -}; + appState: AppState + application: WebApplication + mainApplicationGroup: ApplicationGroup + menuPane: AccountMenuPane + setMenuPane: (pane: AccountMenuPane) => void + closeMenu: () => void +} const MenuPaneSelector: FunctionComponent = observer( - ({ - application, - appState, - menuPane, - setMenuPane, - closeMenu, - mainApplicationGroup, - }) => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + ({ application, appState, menuPane, setMenuPane, closeMenu, mainApplicationGroup }) => { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') switch (menuPane) { case AccountMenuPane.GeneralMenu: @@ -56,15 +49,11 @@ const MenuPaneSelector: FunctionComponent = observer( setMenuPane={setMenuPane} closeMenu={closeMenu} /> - ); + ) case AccountMenuPane.SignIn: return ( - - ); + + ) case AccountMenuPane.Register: return ( = observer( password={password} setPassword={setPassword} /> - ); + ) case AccountMenuPane.ConfirmPassword: return ( = observer( email={email} password={password} /> - ); + ) } - } -); + }, +) export const AccountMenu: FunctionComponent = observer( ({ application, appState, onClickOutside, mainApplicationGroup }) => { - const { - currentPane, - setCurrentPane, - shouldAnimateCloseMenu, - closeAccountMenu, - } = appState.accountMenu; + const { currentPane, setCurrentPane, shouldAnimateCloseMenu, closeAccountMenu } = + appState.accountMenu - const ref = useRef(null); + const ref = useRef(null) useCloseOnClickOutside(ref, () => { - onClickOutside(); - }); + onClickOutside() + }) - const handleKeyDown: JSXInternal.KeyboardEventHandler = ( - event - ) => { + const handleKeyDown: JSXInternal.KeyboardEventHandler = (event) => { switch (event.key) { case 'Escape': if (currentPane === AccountMenuPane.GeneralMenu) { - closeAccountMenu(); + closeAccountMenu() } else if (currentPane === AccountMenuPane.ConfirmPassword) { - setCurrentPane(AccountMenuPane.Register); + setCurrentPane(AccountMenuPane.Register) } else { - setCurrentPane(AccountMenuPane.GeneralMenu); + setCurrentPane(AccountMenuPane.GeneralMenu) } - break; + break } - }; + } return (
@@ -141,6 +122,6 @@ export const AccountMenu: FunctionComponent = observer( />
- ); - } -); + ) + }, +) diff --git a/app/assets/javascripts/components/ApplicationGroupView.tsx b/app/assets/javascripts/Components/ApplicationGroupView/index.tsx similarity index 54% rename from app/assets/javascripts/components/ApplicationGroupView.tsx rename to app/assets/javascripts/Components/ApplicationGroupView/index.tsx index fd84b890b..570673766 100644 --- a/app/assets/javascripts/components/ApplicationGroupView.tsx +++ b/app/assets/javascripts/Components/ApplicationGroupView/index.tsx @@ -1,27 +1,26 @@ -import { ApplicationGroup } from '@/ui_models/application_group'; -import { WebApplication } from '@/ui_models/application'; -import { Component } from 'preact'; -import { ApplicationView } from './ApplicationView'; +import { ApplicationGroup } from '@/UIModels/ApplicationGroup' +import { WebApplication } from '@/UIModels/Application' +import { Component } from 'preact' +import { ApplicationView } from '@/Components/ApplicationView' type State = { - activeApplication?: WebApplication; -}; + activeApplication?: WebApplication +} type Props = { - mainApplicationGroup: ApplicationGroup; -}; + mainApplicationGroup: ApplicationGroup +} export class ApplicationGroupView extends Component { constructor(props: Props) { - super(props); + super(props) props.mainApplicationGroup.addApplicationChangeObserver(() => { - const activeApplication = props.mainApplicationGroup - .primaryApplication as WebApplication; - this.setState({ activeApplication }); - }); + const activeApplication = props.mainApplicationGroup.primaryApplication as WebApplication + this.setState({ activeApplication }) + }) - props.mainApplicationGroup.initialize(); + props.mainApplicationGroup.initialize().catch(console.error) } render() { @@ -37,6 +36,6 @@ export class ApplicationGroupView extends Component {
)} - ); + ) } } diff --git a/app/assets/javascripts/Components/ApplicationView/index.tsx b/app/assets/javascripts/Components/ApplicationView/index.tsx new file mode 100644 index 000000000..b6b408a94 --- /dev/null +++ b/app/assets/javascripts/Components/ApplicationView/index.tsx @@ -0,0 +1,216 @@ +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 { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants' +import { alertDialog } from '@/Services/AlertService' +import { WebAppEvent, WebApplication } from '@/UIModels/Application' +import { PureComponent } from '@/Components/Abstract/PureComponent' +import { Navigation } from '@/Components/Navigation' +import { NotesView } from '@/Components/NotesView' +import { NoteGroupView } from '@/Components/NoteGroupView' +import { Footer } from '@/Components/Footer' +import { SessionsModal } from '@/Components/SessionsModal' +import { PreferencesViewWrapper } from '@/Components/Preferences/PreferencesViewWrapper' +import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal' +import { NotesContextMenu } from '@/Components/NotesContextMenu' +import { PurchaseFlowWrapper } from '@/Components/PurchaseFlow/PurchaseFlowWrapper' +import { render } from 'preact' +import { PermissionsModal } from '@/Components/PermissionsModal' +import { RevisionHistoryModalWrapper } from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper' +import { PremiumModalProvider } from '@/Hooks/usePremiumModal' +import { ConfirmSignoutContainer } from '@/Components/ConfirmSignoutModal' +import { TagsContextMenu } from '@/Components/Tags/TagContextMenu' +import { ToastContainer } from '@standardnotes/stylekit' +import { FilePreviewModalProvider } from '@/Components/Files/FilePreviewModalProvider' + +type Props = { + application: WebApplication + mainApplicationGroup: ApplicationGroup +} + +type State = { + started?: boolean + launched?: boolean + needsUnlock?: boolean + appClass: string + challenges: Challenge[] +} + +export class ApplicationView extends PureComponent { + public readonly platformString = getPlatformString() + + constructor(props: Props) { + super(props, props.application) + this.state = { + appClass: '', + challenges: [], + } + } + + override deinit() { + ;(this.application as unknown) = undefined + super.deinit() + } + + override componentDidMount(): void { + super.componentDidMount() + this.loadApplication().catch(console.error) + } + + async loadApplication() { + this.application.componentManager.setDesktopManager(this.application.getDesktopService()) + await this.application.prepareForLaunch({ + receiveChallenge: async (challenge) => { + const challenges = this.state.challenges.slice() + challenges.push(challenge) + this.setState({ challenges: challenges }) + }, + }) + await this.application.launch() + } + + public removeChallenge = async (challenge: Challenge) => { + const challenges = this.state.challenges.slice() + removeFromArray(challenges, challenge) + this.setState({ challenges: challenges }) + } + + override async onAppStart() { + super.onAppStart().catch(console.error) + this.setState({ + started: true, + needsUnlock: this.application.hasPasscode(), + }) + + this.application.componentManager.presentPermissionsDialog = this.presentPermissionsDialog + } + + override async onAppLaunch() { + super.onAppLaunch().catch(console.error) + this.setState({ + launched: true, + needsUnlock: false, + }) + this.handleDemoSignInFromParams().catch(console.error) + } + + onUpdateAvailable() { + this.application.notifyWebEvent(WebAppEvent.NewUpdateAvailable) + } + + override async onAppEvent(eventName: ApplicationEvent) { + super.onAppEvent(eventName) + switch (eventName) { + case ApplicationEvent.LocalDatabaseReadError: + alertDialog({ + text: 'Unable to load local database. Please restart the app and try again.', + }).catch(console.error) + break + case ApplicationEvent.LocalDatabaseWriteError: + alertDialog({ + text: 'Unable to write to local database. Please restart the app and try again.', + }).catch(console.error) + break + } + } + + override async onAppStateEvent(eventName: AppStateEvent, data?: unknown) { + if (eventName === AppStateEvent.PanelResized) { + const { panel, collapsed } = data as PanelResizedData + let appClass = '' + if (panel === PANEL_NAME_NOTES && collapsed) { + appClass += 'collapsed-notes' + } + if (panel === PANEL_NAME_NAVIGATION && collapsed) { + appClass += ' collapsed-navigation' + } + this.setState({ appClass }) + } else if (eventName === AppStateEvent.WindowDidFocus) { + if (!(await this.application.isLocked())) { + this.application.sync.sync().catch(console.error) + } + } + } + + async handleDemoSignInFromParams() { + const token = getWindowUrlParams().get('demo-token') + if (!token || this.application.hasAccount()) { + return + } + + await this.application.sessions.populateSessionFromDemoShareToken(token) + } + + presentPermissionsDialog = (dialog: PermissionDialog) => { + render( + , + document.body.appendChild(document.createElement('div')), + ) + } + + override render() { + if (this.application['dealloced'] === true) { + console.error('Attempting to render dealloced application') + return
+ } + + const renderAppContents = !this.state.needsUnlock && this.state.launched + + return ( + + +
+ {renderAppContents && ( +
+ + + +
+ )} + {renderAppContents && ( + <> +
+ + + + + )} + {this.state.challenges.map((challenge) => { + return ( +
+ +
+ ) + })} + {renderAppContents && ( + <> + + + + + + + )} +
+
+
+ ) + } +} diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx new file mode 100644 index 000000000..af2c3f1ce --- /dev/null +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx @@ -0,0 +1,368 @@ +import { WebApplication } from '@/UIModels/Application' +import { AppState } from '@/UIModels/AppState' +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' +import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' +import { + ChallengeReason, + ContentType, + FeatureIdentifier, + FeatureStatus, + SNFile, +} 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, PopoverTabs } from './AttachedFilesPopover' +import { usePremiumModal } from '@/Hooks/usePremiumModal' + +type Props = { + application: WebApplication + appState: AppState + onClickPreprocessing?: () => Promise +} + +const createDragOverlay = () => { + if (document.getElementById('drag-overlay')) { + return + } + + const overlayElementTemplate = + '
' + const overlayFragment = document.createRange().createContextualFragment(overlayElementTemplate) + document.body.appendChild(overlayFragment) +} + +const removeDragOverlay = () => { + document.getElementById('drag-overlay')?.remove() +} + +export const AttachedFilesButton: FunctionComponent = observer( + ({ application, appState, onClickPreprocessing }) => { + const premiumModal = usePremiumModal() + const note = Object.values(appState.notes.selectedNotes)[0] + + 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) + + const [attachedFilesCount, setAttachedFilesCount] = useState( + note ? application.items.getFilesForNote(note).length : 0, + ) + + const reloadAttachedFilesCount = useCallback(() => { + setAttachedFilesCount(note ? application.items.getFilesForNote(note).length : 0) + }, [application.items, note]) + + useEffect(() => { + const unregisterFileStream = application.streamItems(ContentType.File, () => { + reloadAttachedFilesCount() + }) + + return () => { + unregisterFileStream() + } + }, [application, reloadAttachedFilesCount]) + + const toggleAttachedFilesMenu = useCallback(async () => { + if ( + application.features.getFeatureStatus(FeatureIdentifier.Files) !== FeatureStatus.Entitled + ) { + premiumModal.activate('Files') + return + } + + 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 = !open + if (newOpenState && onClickPreprocessing) { + await onClickPreprocessing() + } + + setOpen(newOpenState) + } + }, [application.features, onClickPreprocessing, open, premiumModal]) + + const deleteFile = async (file: SNFile) => { + 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}"...`, + }) + await application.files.deleteFile(file) + addToast({ + type: ToastType.Success, + message: `Deleted file "${file.name}"`, + }) + dismissToast(deletingToastId) + } + } + + const downloadFile = async (file: SNFile) => { + appState.files.downloadFile(file).catch(console.error) + } + + const attachFileToNote = useCallback( + async (file: SNFile) => { + await application.items.associateFileWithNote(file, note) + }, + [application.items, note], + ) + + const detachFileFromNote = async (file: SNFile) => { + await application.items.disassociateFileWithNote(file, note) + } + + const toggleFileProtection = async (file: SNFile) => { + let result: SNFile | 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: SNFile, + challengeReason: ChallengeReason, + ) => { + const authorizedFiles = await application.protections.authorizeProtectedActionForFiles( + [file], + challengeReason, + ) + const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) + return isAuthorized + } + + const renameFile = async (file: SNFile, fileName: string) => { + 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() + } + + 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 + } + + application.sync.sync().catch(console.error) + + return true + } + + const [isDraggingFiles, setIsDraggingFiles] = useState(false) + const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles) + const dragCounter = useRef(0) + + const handleDrag = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + } + + const handleDragIn = useCallback( + (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + dragCounter.current = dragCounter.current + 1 + + if (event.dataTransfer?.items.length) { + setIsDraggingFiles(true) + createDragOverlay() + if (!open) { + toggleAttachedFilesMenu().catch(console.error) + } + } + }, + [open, toggleAttachedFilesMenu], + ) + + const handleDragOut = (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + dragCounter.current = dragCounter.current - 1 + + if (dragCounter.current > 0) { + return + } + + removeDragOverlay() + + setIsDraggingFiles(false) + } + + const handleDrop = useCallback( + (event: DragEvent) => { + event.preventDefault() + event.stopPropagation() + + setIsDraggingFiles(false) + removeDragOverlay() + + 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, attachFileToNote, currentTab], + ) + + 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]) + + 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 && ( + + )} + + +
+ ) + }, +) diff --git a/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx similarity index 64% rename from app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx rename to app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx index 770ea0277..f363a8865 100644 --- a/app/assets/javascripts/components/AttachedFilesPopover/AttachedFilesPopover.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx @@ -1,18 +1,15 @@ -import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants'; -import { WebApplication } from '@/ui_models/application'; -import { AppState } from '@/ui_models/app_state'; -import { ContentType, SNFile, SNNote } from '@standardnotes/snjs'; -import { FilesIllustration } from '@standardnotes/stylekit'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks'; -import { Button } from '../Button'; -import { Icon } from '../Icon'; -import { PopoverFileItem } from './PopoverFileItem'; -import { - PopoverFileItemAction, - PopoverFileItemActionType, -} from './PopoverFileItemAction'; +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' +import { WebApplication } from '@/UIModels/Application' +import { AppState } from '@/UIModels/AppState' +import { ContentType, SNFile, SNNote } from '@standardnotes/snjs' +import { FilesIllustration } from '@standardnotes/stylekit' +import { observer } from 'mobx-react-lite' +import { FunctionComponent } from 'preact' +import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks' +import { Button } from '@/Components/Button/Button' +import { Icon } from '@/Components/Icon' +import { PopoverFileItem } from './PopoverFileItem' +import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction' export enum PopoverTabs { AttachedFiles, @@ -20,15 +17,15 @@ export enum PopoverTabs { } type Props = { - application: WebApplication; - appState: AppState; - currentTab: PopoverTabs; - closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void; - handleFileAction: (action: PopoverFileItemAction) => Promise; - isDraggingFiles: boolean; - note: SNNote; - setCurrentTab: StateUpdater; -}; + application: WebApplication + appState: AppState + currentTab: PopoverTabs + closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void + handleFileAction: (action: PopoverFileItemAction) => Promise + isDraggingFiles: boolean + note: SNNote + setCurrentTab: StateUpdater +} export const AttachedFilesPopover: FunctionComponent = observer( ({ @@ -41,70 +38,61 @@ export const AttachedFilesPopover: FunctionComponent = observer( note, setCurrentTab, }) => { - const [attachedFiles, setAttachedFiles] = useState([]); - const [allFiles, setAllFiles] = useState([]); - const [searchQuery, setSearchQuery] = useState(''); - const searchInputRef = useRef(null); + const [attachedFiles, setAttachedFiles] = useState([]) + const [allFiles, setAllFiles] = useState([]) + 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 + (file) => file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1, ) - : filesList; + : filesList useEffect(() => { - const unregisterFileStream = application.streamItems( - ContentType.File, - () => { - setAttachedFiles( - application.items - .getFilesForNote(note) - .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)) - ); + const unregisterFileStream = application.streamItems(ContentType.File, () => { + setAttachedFiles( + application.items + .getFilesForNote(note) + .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)), + ) - setAllFiles( - application.items - .getItems(ContentType.File) - .sort((a, b) => - a.created_at < b.created_at ? 1 : -1 - ) as SNFile[] - ); - } - ); + setAllFiles( + application.items + .getItems(ContentType.File) + .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)) as SNFile[], + ) + }) return () => { - unregisterFileStream(); - }; - }, [application, note]); + unregisterFileStream() + } + }, [application, note]) const handleAttachFilesClick = async () => { - const uploadedFiles = await appState.files.uploadNewFile(); + const uploadedFiles = await appState.files.uploadNewFile() if (!uploadedFiles) { - return; + return } if (currentTab === PopoverTabs.AttachedFiles) { uploadedFiles.forEach((file) => { handleFileAction({ type: PopoverFileItemActionType.AttachFileToNote, payload: file, - }); - }); + }).catch(console.error) + }) } - }; + } return (
@@ -115,7 +103,7 @@ export const AttachedFilesPopover: FunctionComponent = observer( : 'color-text' }`} onClick={() => { - setCurrentTab(PopoverTabs.AttachedFiles); + setCurrentTab(PopoverTabs.AttachedFiles) }} onBlur={closeOnBlur} > @@ -128,7 +116,7 @@ export const AttachedFilesPopover: FunctionComponent = observer( : 'color-text' }`} onClick={() => { - setCurrentTab(PopoverTabs.AllFiles); + setCurrentTab(PopoverTabs.AllFiles) }} onBlur={closeOnBlur} > @@ -145,7 +133,7 @@ export const AttachedFilesPopover: FunctionComponent = observer( placeholder="Search files..." value={searchQuery} onInput={(e) => { - setSearchQuery((e.target as HTMLInputElement).value); + setSearchQuery((e.target as HTMLInputElement).value) }} onBlur={closeOnBlur} ref={searchInputRef} @@ -154,15 +142,12 @@ export const AttachedFilesPopover: FunctionComponent = observer( )}
@@ -179,7 +164,7 @@ export const AttachedFilesPopover: FunctionComponent = observer( getIconType={application.iconsController.getIconForFileType} closeOnBlur={closeOnBlur} /> - ); + ) }) ) : (
@@ -193,17 +178,10 @@ export const AttachedFilesPopover: FunctionComponent = observer( ? 'No files attached to this note' : 'No files found in this account'}
- -
- Or drop your files here -
+
Or drop your files here
)}
@@ -214,13 +192,10 @@ export const AttachedFilesPopover: FunctionComponent = observer( onBlur={closeOnBlur} > - {currentTab === PopoverTabs.AttachedFiles - ? 'Attach' - : 'Upload'}{' '} - files + {currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files )}
- ); - } -); + ) + }, +) diff --git a/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItem.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx similarity index 62% rename from app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItem.tsx rename to app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx index 1f6981f47..d5b61ceb3 100644 --- a/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileItem.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx @@ -1,29 +1,26 @@ -import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/constants'; -import { KeyboardKey } from '@/services/ioService'; -import { formatSizeToReadableString } from '@standardnotes/filepicker'; -import { IconType, SNFile } from '@standardnotes/snjs'; -import { FunctionComponent } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; -import { Icon, ICONS } from '../Icon'; -import { - PopoverFileItemAction, - PopoverFileItemActionType, -} from './PopoverFileItemAction'; -import { PopoverFileSubmenu } from './PopoverFileSubmenu'; +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' +import { KeyboardKey } from '@/Services/IOService' +import { formatSizeToReadableString } from '@standardnotes/filepicker' +import { IconType, SNFile } from '@standardnotes/snjs' +import { FunctionComponent } from 'preact' +import { useEffect, useRef, useState } from 'preact/hooks' +import { Icon, ICONS } from '@/Components/Icon' +import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction' +import { PopoverFileSubmenu } from './PopoverFileSubmenu' export const getFileIconComponent = (iconType: string, className: string) => { - const IconComponent = ICONS[iconType as keyof typeof ICONS]; + const IconComponent = ICONS[iconType as keyof typeof ICONS] - return ; -}; + return +} export type PopoverFileItemProps = { - file: SNFile; - isAttachedToNote: boolean; - handleFileAction: (action: PopoverFileItemAction) => Promise; - getIconType(type: string): IconType; - closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void; -}; + file: SNFile + isAttachedToNote: boolean + handleFileAction: (action: PopoverFileItemAction) => Promise + getIconType(type: string): IconType + closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void +} export const PopoverFileItem: FunctionComponent = ({ file, @@ -32,16 +29,16 @@ export const PopoverFileItem: FunctionComponent = ({ getIconType, closeOnBlur, }) => { - const [fileName, setFileName] = useState(file.name); - const [isRenamingFile, setIsRenamingFile] = useState(false); - const itemRef = useRef(null); - const fileNameInputRef = useRef(null); + const [fileName, setFileName] = useState(file.name) + const [isRenamingFile, setIsRenamingFile] = useState(false) + const itemRef = useRef(null) + const fileNameInputRef = useRef(null) useEffect(() => { if (isRenamingFile) { - fileNameInputRef.current?.focus(); + fileNameInputRef.current?.focus() } - }, [isRenamingFile]); + }, [isRenamingFile]) const renameFile = async (file: SNFile, name: string) => { await handleFileAction({ @@ -50,23 +47,23 @@ export const PopoverFileItem: FunctionComponent = ({ file, name, }, - }); - setIsRenamingFile(false); - }; + }) + setIsRenamingFile(false) + } const handleFileNameInput = (event: Event) => { - setFileName((event.target as HTMLInputElement).value); - }; + setFileName((event.target as HTMLInputElement).value) + } const handleFileNameInputKeyDown = (event: KeyboardEvent) => { if (event.key === KeyboardKey.Enter) { - itemRef.current?.focus(); + itemRef.current?.focus() } - }; + } const handleFileNameInputBlur = () => { - renameFile(file, fileName); - }; + renameFile(file, fileName).catch(console.error) + } return (
= ({ tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} >
- {getFileIconComponent( - getIconType(file.mimeType), - 'w-8 h-8 flex-shrink-0' - )} + {getFileIconComponent(getIconType(file.mimeType), 'w-8 h-8 flex-shrink-0')}
{isRenamingFile ? ( = ({
)}
- {file.created_at.toLocaleString()} ·{' '} - {formatSizeToReadableString(file.size)} + {file.created_at.toLocaleString()} · {formatSizeToReadableString(file.size)}
@@ -115,5 +108,5 @@ export const PopoverFileItem: FunctionComponent = ({ closeOnBlur={closeOnBlur} />
- ); -}; + ) +} diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx new file mode 100644 index 000000000..b774c6b90 --- /dev/null +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx @@ -0,0 +1,31 @@ +import { SNFile } from '@standardnotes/snjs' + +export enum PopoverFileItemActionType { + AttachFileToNote, + DetachFileToNote, + DeleteFile, + DownloadFile, + RenameFile, + ToggleFileProtection, +} + +export type PopoverFileItemAction = + | { + type: Exclude< + PopoverFileItemActionType, + PopoverFileItemActionType.RenameFile | PopoverFileItemActionType.ToggleFileProtection + > + payload: SNFile + } + | { + type: PopoverFileItemActionType.ToggleFileProtection + payload: SNFile + callback: (isProtected: boolean) => void + } + | { + type: PopoverFileItemActionType.RenameFile + payload: { + file: SNFile + name: string + } + } diff --git a/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileSubmenu.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx similarity index 74% rename from app/assets/javascripts/components/AttachedFilesPopover/PopoverFileSubmenu.tsx rename to app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx index d9183779d..fe5a94e5f 100644 --- a/app/assets/javascripts/components/AttachedFilesPopover/PopoverFileSubmenu.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx @@ -1,31 +1,18 @@ -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 '../Icon'; -import { Switch } from '../Switch'; -import { useCloseOnBlur } from '../utils'; -import { useFilePreviewModal } from '../Files/FilePreviewModalProvider'; -import { PopoverFileItemProps } from './PopoverFileItem'; -import { PopoverFileItemActionType } from './PopoverFileItemAction'; +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' +import { Switch } from '@/Components/Switch' +import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' +import { useFilePreviewModal } from '@/Components/Files/FilePreviewModalProvider' +import { PopoverFileItemProps } from './PopoverFileItem' +import { PopoverFileItemActionType } from './PopoverFileItemAction' type Props = Omit & { - setIsRenamingFile: StateUpdater; -}; + setIsRenamingFile: StateUpdater +} export const PopoverFileSubmenu: FunctionComponent = ({ file, @@ -33,54 +20,51 @@ export const PopoverFileSubmenu: FunctionComponent = ({ handleFileAction, setIsRenamingFile, }) => { - const filePreviewModal = useFilePreviewModal(); + const filePreviewModal = useFilePreviewModal() - const menuContainerRef = useRef(null); - const menuButtonRef = useRef(null); - const menuRef = useRef(null); + const menuContainerRef = useRef(null) + const menuButtonRef = useRef(null) + const menuRef = useRef(null) - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isFileProtected, setIsFileProtected] = useState(file.protected); + const [isMenuOpen, setIsMenuOpen] = useState(false) + const [isFileProtected, setIsFileProtected] = useState(file.protected) const [menuStyle, setMenuStyle] = useState({ right: 0, bottom: 0, maxHeight: 'auto', - }); - const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen); + }) + const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen) const closeMenu = () => { - setIsMenuOpen(false); - }; + setIsMenuOpen(false) + } const toggleMenu = () => { if (!isMenuOpen) { - const menuPosition = calculateSubmenuStyle(menuButtonRef.current); + const menuPosition = calculateSubmenuStyle(menuButtonRef.current) if (menuPosition) { - setMenuStyle(menuPosition); + setMenuStyle(menuPosition) } } - setIsMenuOpen(!isMenuOpen); - }; + setIsMenuOpen(!isMenuOpen) + } const recalculateMenuStyle = useCallback(() => { - const newMenuPosition = calculateSubmenuStyle( - menuButtonRef.current, - menuRef.current - ); + const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current) if (newMenuPosition) { - setMenuStyle(newMenuPosition); + setMenuStyle(newMenuPosition) } - }, []); + }, []) useEffect(() => { if (isMenuOpen) { setTimeout(() => { - recalculateMenuStyle(); - }); + recalculateMenuStyle() + }) } - }, [isMenuOpen, recalculateMenuStyle]); + }, [isMenuOpen, recalculateMenuStyle]) return (
@@ -106,8 +90,8 @@ export const PopoverFileSubmenu: FunctionComponent = ({ onBlur={closeOnBlur} className="sn-dropdown-item focus:bg-info-backdrop" onClick={() => { - filePreviewModal.activate(file); - closeMenu(); + filePreviewModal.activate(file) + closeMenu() }} > @@ -121,8 +105,8 @@ export const PopoverFileSubmenu: FunctionComponent = ({ handleFileAction({ type: PopoverFileItemActionType.DetachFileToNote, payload: file, - }); - closeMenu(); + }).catch(console.error) + closeMenu() }} > @@ -136,8 +120,8 @@ export const PopoverFileSubmenu: FunctionComponent = ({ handleFileAction({ type: PopoverFileItemActionType.AttachFileToNote, payload: file, - }); - closeMenu(); + }).catch(console.error) + closeMenu() }} > @@ -152,9 +136,9 @@ export const PopoverFileSubmenu: FunctionComponent = ({ type: PopoverFileItemActionType.ToggleFileProtection, payload: file, callback: (isProtected: boolean) => { - setIsFileProtected(isProtected); + setIsFileProtected(isProtected) }, - }); + }).catch(console.error) }} onBlur={closeOnBlur} > @@ -176,8 +160,8 @@ export const PopoverFileSubmenu: FunctionComponent = ({ handleFileAction({ type: PopoverFileItemActionType.DownloadFile, payload: file, - }); - closeMenu(); + }).catch(console.error) + closeMenu() }} > @@ -187,7 +171,7 @@ export const PopoverFileSubmenu: FunctionComponent = ({ onBlur={closeOnBlur} className="sn-dropdown-item focus:bg-info-backdrop" onClick={() => { - setIsRenamingFile(true); + setIsRenamingFile(true) }} > @@ -200,8 +184,8 @@ export const PopoverFileSubmenu: FunctionComponent = ({ handleFileAction({ type: PopoverFileItemActionType.DeleteFile, payload: file, - }); - closeMenu(); + }).catch(console.error) + closeMenu() }} > @@ -212,5 +196,5 @@ export const PopoverFileSubmenu: FunctionComponent = ({
- ); -}; + ) +} diff --git a/app/assets/javascripts/components/Bubble.tsx b/app/assets/javascripts/Components/Bubble/index.tsx similarity index 70% rename from app/assets/javascripts/components/Bubble.tsx rename to app/assets/javascripts/Components/Bubble/index.tsx index e164a4f50..5a2146823 100644 --- a/app/assets/javascripts/components/Bubble.tsx +++ b/app/assets/javascripts/Components/Bubble/index.tsx @@ -1,25 +1,23 @@ interface BubbleProperties { - label: string; - selected: boolean; - onSelect: () => void; + label: string + selected: boolean + onSelect: () => void } const styles = { base: 'px-2 py-1.5 text-center rounded-full cursor-pointer transition border-1 border-solid active:border-info active:bg-info active:color-neutral-contrast', unselected: 'color-neutral border-secondary', selected: 'border-info bg-info color-neutral-contrast', -}; +} const Bubble = ({ label, selected, onSelect }: BubbleProperties) => ( {label} -); +) -export default Bubble; +export default Bubble diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/Components/Button/Button.tsx similarity index 54% rename from app/assets/javascripts/components/Button.tsx rename to app/assets/javascripts/Components/Button/Button.tsx index 1afc15904..eca0e336e 100644 --- a/app/assets/javascripts/components/Button.tsx +++ b/app/assets/javascripts/Components/Button/Button.tsx @@ -1,58 +1,44 @@ -import { JSXInternal } from 'preact/src/jsx'; -import { ComponentChildren, FunctionComponent, Ref } from 'preact'; -import { forwardRef } from 'preact/compat'; +import { JSXInternal } from 'preact/src/jsx' +import { ComponentChildren, FunctionComponent, Ref } from 'preact' +import { forwardRef } from 'preact/compat' -const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`; +const baseClass = 'rounded px-4 py-1.75 font-bold text-sm fit-content' -type ButtonVariant = 'normal' | 'primary'; +type ButtonVariant = 'normal' | 'primary' -const getClassName = ( - variant: ButtonVariant, - danger: boolean, - disabled: boolean -) => { - const borders = - variant === 'normal' ? 'border-solid border-main border-1' : 'no-border'; - const cursor = disabled ? 'cursor-not-allowed' : 'cursor-pointer'; +const getClassName = (variant: ButtonVariant, danger: boolean, disabled: boolean) => { + const borders = variant === 'normal' ? 'border-solid border-main border-1' : 'no-border' + const cursor = disabled ? 'cursor-not-allowed' : 'cursor-pointer' - let colors = - variant === 'normal' - ? 'bg-default color-text' - : 'bg-info color-info-contrast'; + let colors = variant === 'normal' ? 'bg-default color-text' : 'bg-info color-info-contrast' let focusHoverStates = variant === 'normal' ? 'focus:bg-contrast focus:outline-none hover:bg-contrast' - : 'hover:brightness-130 focus:outline-none focus:brightness-130'; + : 'hover:brightness-130 focus:outline-none focus:brightness-130' if (danger) { - colors = - variant === 'normal' - ? 'bg-default color-danger' - : 'bg-danger color-info-contrast'; + colors = variant === 'normal' ? 'bg-default color-danger' : 'bg-danger color-info-contrast' } if (disabled) { - colors = - variant === 'normal' - ? 'bg-default color-grey-2' - : 'bg-grey-2 color-info-contrast'; + colors = variant === 'normal' ? 'bg-default color-grey-2' : 'bg-grey-2 color-info-contrast' focusHoverStates = variant === 'normal' ? 'focus:bg-default focus:outline-none hover:bg-default' - : 'focus:brightness-default focus:outline-none hover:brightness-default'; + : 'focus:brightness-default focus:outline-none hover:brightness-default' } - return `${baseClass} ${colors} ${borders} ${focusHoverStates} ${cursor}`; -}; + return `${baseClass} ${colors} ${borders} ${focusHoverStates} ${cursor}` +} type ButtonProps = JSXInternal.HTMLAttributes & { - children?: ComponentChildren; - className?: string; - variant?: ButtonVariant; - dangerStyle?: boolean; - label?: string; -}; + children?: ComponentChildren + className?: string + variant?: ButtonVariant + dangerStyle?: boolean + label?: string +} export const Button: FunctionComponent = forwardRef( ( @@ -65,7 +51,7 @@ export const Button: FunctionComponent = forwardRef( children, ...props }: ButtonProps, - ref: Ref + ref: Ref, ) => { return ( - ); - } -); + ) + }, +) diff --git a/app/assets/javascripts/components/IconButton.tsx b/app/assets/javascripts/Components/Button/IconButton.tsx similarity index 66% rename from app/assets/javascripts/components/IconButton.tsx rename to app/assets/javascripts/Components/Button/IconButton.tsx index 6349be7bc..f74bec08c 100644 --- a/app/assets/javascripts/components/IconButton.tsx +++ b/app/assets/javascripts/Components/Button/IconButton.tsx @@ -1,27 +1,27 @@ -import { FunctionComponent } from 'preact'; -import { Icon } from './Icon'; -import { IconType } from '@standardnotes/snjs'; +import { FunctionComponent } from 'preact' +import { Icon } from '@/Components/Icon' +import { IconType } from '@standardnotes/snjs' interface Props { /** * onClick - preventDefault is handled within the component */ - onClick: () => void; + onClick: () => void - className?: string; + className?: string - icon: IconType; + icon: IconType - iconClassName?: string; + iconClassName?: string /** * Button tooltip */ - title: string; + title: string - focusable: boolean; + focusable: boolean - disabled?: boolean; + disabled?: boolean } /** @@ -38,10 +38,10 @@ export const IconButton: FunctionComponent = ({ disabled = false, }) => { const click = (e: MouseEvent) => { - e.preventDefault(); - onClick(); - }; - const focusableClass = focusable ? '' : 'focus:shadow-none'; + e.preventDefault() + onClick() + } + const focusableClass = focusable ? '' : 'focus:shadow-none' return ( - ); -}; + ) +} diff --git a/app/assets/javascripts/Components/Button/RoundIconButton.tsx b/app/assets/javascripts/Components/Button/RoundIconButton.tsx new file mode 100644 index 000000000..ee3d20325 --- /dev/null +++ b/app/assets/javascripts/Components/Button/RoundIconButton.tsx @@ -0,0 +1,40 @@ +import { FunctionComponent } from 'preact' +import { Icon } from '@/Components/Icon' +import { IconType } from '@standardnotes/snjs' + +type ButtonType = 'normal' | 'primary' + +interface Props { + /** + * onClick - preventDefault is handled within the component + */ + 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) => { + e.preventDefault() + onClick() + } + const classes = type === 'primary' ? 'info ' : '' + return ( + + ) +} diff --git a/app/assets/javascripts/components/ChallengeModal/ChallengeModal.tsx b/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx similarity index 54% rename from app/assets/javascripts/components/ChallengeModal/ChallengeModal.tsx rename to app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx index 4f006c1db..c3990d784 100644 --- a/app/assets/javascripts/components/ChallengeModal/ChallengeModal.tsx +++ b/app/assets/javascripts/Components/ChallengeModal/ChallengeModal.tsx @@ -1,5 +1,5 @@ -import { WebApplication } from '@/ui_models/application'; -import { DialogContent, DialogOverlay } from '@reach/dialog'; +import { WebApplication } from '@/UIModels/Application' +import { DialogContent, DialogOverlay } from '@reach/dialog' import { ButtonType, Challenge, @@ -7,90 +7,87 @@ import { ChallengeReason, ChallengeValue, removeFromArray, -} from '@standardnotes/snjs'; -import { ProtectedIllustration } from '@standardnotes/stylekit'; -import { FunctionComponent } from 'preact'; -import { useCallback, useEffect, useState } from 'preact/hooks'; -import { Button } from '../Button'; -import { Icon } from '../Icon'; -import { ChallengeModalPrompt } from './ChallengePrompt'; +} from '@standardnotes/snjs' +import { ProtectedIllustration } from '@standardnotes/stylekit' +import { FunctionComponent } from 'preact' +import { useCallback, useEffect, useState } from 'preact/hooks' +import { Button } from '@/Components/Button/Button' +import { Icon } from '@/Components/Icon' +import { ChallengeModalPrompt } from './ChallengePrompt' type InputValue = { - prompt: ChallengePrompt; - value: string | number | boolean; - invalid: boolean; -}; + prompt: ChallengePrompt + value: string | number | boolean + invalid: boolean +} -export type ChallengeModalValues = Record; +export type ChallengeModalValues = Record type Props = { - application: WebApplication; - challenge: Challenge; - onDismiss: (challenge: Challenge) => Promise; -}; + application: WebApplication + challenge: Challenge + onDismiss: (challenge: Challenge) => Promise +} const validateValues = ( values: ChallengeModalValues, - prompts: ChallengePrompt[] + prompts: ChallengePrompt[], ): ChallengeModalValues | undefined => { - let hasInvalidValues = false; - const validatedValues = { ...values }; + let hasInvalidValues = false + const validatedValues = { ...values } for (const prompt of prompts) { - const value = validatedValues[prompt.id]; + const value = validatedValues[prompt.id] if (typeof value.value === 'string' && value.value.length === 0) { - validatedValues[prompt.id].invalid = true; - hasInvalidValues = true; + validatedValues[prompt.id].invalid = true + hasInvalidValues = true } } if (!hasInvalidValues) { - return validatedValues; + return validatedValues } -}; + return undefined +} -export const ChallengeModal: FunctionComponent = ({ - application, - challenge, - onDismiss, -}) => { +export const ChallengeModal: FunctionComponent = ({ application, challenge, onDismiss }) => { const [values, setValues] = useState(() => { - const values = {} as ChallengeModalValues; + const values = {} as ChallengeModalValues for (const prompt of challenge.prompts) { values[prompt.id] = { prompt, value: prompt.initialValue ?? '', invalid: false, - }; + } } - return values; - }); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isProcessing, setIsProcessing] = useState(false); - const [, setProcessingPrompts] = useState([]); - const [bypassModalFocusLock, setBypassModalFocusLock] = useState(false); + return values + }) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + const [, setProcessingPrompts] = useState([]) + const [bypassModalFocusLock, setBypassModalFocusLock] = useState(false) const shouldShowForgotPasscode = [ ChallengeReason.ApplicationUnlock, ChallengeReason.Migration, - ].includes(challenge.reason); + ].includes(challenge.reason) const submit = async () => { - const validatedValues = validateValues(values, challenge.prompts); + const validatedValues = validateValues(values, challenge.prompts) if (!validatedValues) { - return; + return } if (isSubmitting || isProcessing) { - return; + return } - setIsSubmitting(true); - setIsProcessing(true); - const valuesToProcess: ChallengeValue[] = []; + setIsSubmitting(true) + setIsProcessing(true) + const valuesToProcess: ChallengeValue[] = [] for (const inputValue of Object.values(validatedValues)) { - const rawValue = inputValue.value; - const value = new ChallengeValue(inputValue.prompt, rawValue); - valuesToProcess.push(value); + const rawValue = inputValue.value + const value = new ChallengeValue(inputValue.prompt, rawValue) + valuesToProcess.push(value) } - const processingPrompts = valuesToProcess.map((v) => v.prompt); - setIsProcessing(processingPrompts.length > 0); - setProcessingPrompts(processingPrompts); + const processingPrompts = valuesToProcess.map((v) => v.prompt) + setIsProcessing(processingPrompts.length > 0) + setProcessingPrompts(processingPrompts) /** * Unfortunately neccessary to wait 50ms so that the above setState call completely * updates the UI to change processing state, before we enter into UI blocking operation @@ -98,90 +95,85 @@ export const ChallengeModal: FunctionComponent = ({ */ setTimeout(() => { if (valuesToProcess.length > 0) { - application.submitValuesForChallenge(challenge, valuesToProcess); + application.submitValuesForChallenge(challenge, valuesToProcess).catch(console.error) } else { - setIsProcessing(false); + setIsProcessing(false) } - setIsSubmitting(false); - }, 50); - }; + setIsSubmitting(false) + }, 50) + } const onValueChange = useCallback( (value: string | number, prompt: ChallengePrompt) => { - const newValues = { ...values }; - newValues[prompt.id].invalid = false; - newValues[prompt.id].value = value; - setValues(newValues); + const newValues = { ...values } + newValues[prompt.id].invalid = false + newValues[prompt.id].value = value + setValues(newValues) }, - [values] - ); + [values], + ) const closeModal = () => { if (challenge.cancelable) { - onDismiss(challenge); + onDismiss(challenge).catch(console.error) } - }; + } useEffect(() => { - const removeChallengeObserver = application.addChallengeObserver( - challenge, - { - onValidValue: (value) => { - setValues((values) => { - const newValues = { ...values }; - newValues[value.prompt.id].invalid = false; - return newValues; - }); + const removeChallengeObserver = application.addChallengeObserver(challenge, { + onValidValue: (value) => { + setValues((values) => { + const newValues = { ...values } + newValues[value.prompt.id].invalid = false + return newValues + }) + setProcessingPrompts((currentlyProcessingPrompts) => { + const processingPrompts = currentlyProcessingPrompts.slice() + removeFromArray(processingPrompts, value.prompt) + setIsProcessing(processingPrompts.length > 0) + return processingPrompts + }) + }, + onInvalidValue: (value) => { + setValues((values) => { + const newValues = { ...values } + newValues[value.prompt.id].invalid = true + return newValues + }) + /** If custom validation, treat all values together and not individually */ + if (!value.prompt.validates) { + setProcessingPrompts([]) + setIsProcessing(false) + } else { setProcessingPrompts((currentlyProcessingPrompts) => { - const processingPrompts = currentlyProcessingPrompts.slice(); - removeFromArray(processingPrompts, value.prompt); - setIsProcessing(processingPrompts.length > 0); - return processingPrompts; - }); - }, - onInvalidValue: (value) => { - setValues((values) => { - const newValues = { ...values }; - newValues[value.prompt.id].invalid = true; - return newValues; - }); - /** If custom validation, treat all values together and not individually */ - if (!value.prompt.validates) { - setProcessingPrompts([]); - setIsProcessing(false); - } else { - setProcessingPrompts((currentlyProcessingPrompts) => { - const processingPrompts = currentlyProcessingPrompts.slice(); - removeFromArray(processingPrompts, value.prompt); - setIsProcessing(processingPrompts.length > 0); - return processingPrompts; - }); - } - }, - onComplete: () => { - onDismiss(challenge); - }, - onCancel: () => { - onDismiss(challenge); - }, - } - ); + const processingPrompts = currentlyProcessingPrompts.slice() + removeFromArray(processingPrompts, value.prompt) + setIsProcessing(processingPrompts.length > 0) + return processingPrompts + }) + } + }, + onComplete: () => { + onDismiss(challenge).catch(console.error) + }, + onCancel: () => { + onDismiss(challenge).catch(console.error) + }, + }) return () => { - removeChallengeObserver(); - }; - }, [application, challenge, onDismiss]); + removeChallengeObserver() + } + }, [application, challenge, onDismiss]) if (!challenge.prompts) { - return null; + return null } return ( = ({ )} -
- {challenge.heading} -
-
- {challenge.subheading} -
+
{challenge.heading}
+
{challenge.subheading}
{ - e.preventDefault(); - submit(); + e.preventDefault() + submit().catch(console.error) }} > {challenge.prompts.map((prompt, index) => ( @@ -232,7 +220,7 @@ export const ChallengeModal: FunctionComponent = ({ disabled={isProcessing} className="min-w-76 mb-3.5" onClick={() => { - submit(); + submit().catch(console.error) }} > {isProcessing ? 'Generating Keys...' : 'Unlock'} @@ -241,23 +229,23 @@ export const ChallengeModal: FunctionComponent = ({
{group.items.map((item) => { const onClickEditorItem = () => { - selectEditor(item); - }; + selectEditor(item).catch(console.error) + } return ( @@ -236,11 +205,11 @@ export const ChangeEditorMenu: FunctionComponent = ({ {!item.isEntitled && }
- ); + ) })} - ); + ) })} - ); -}; + ) +} diff --git a/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts b/app/assets/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts similarity index 70% rename from app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts rename to app/assets/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts index d69be1d46..0ec3dab33 100644 --- a/app/assets/javascripts/components/NotesOptions/changeEditor/createEditorMenuGroups.ts +++ b/app/assets/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts @@ -1,4 +1,4 @@ -import { WebApplication } from '@/ui_models/application'; +import { WebApplication } from '@/UIModels/Application' import { ContentType, FeatureStatus, @@ -7,37 +7,32 @@ import { FeatureDescription, GetFeatures, NoteType, -} from '@standardnotes/snjs'; -import { EditorMenuItem, EditorMenuGroup } from '../ChangeEditorOption'; +} from '@standardnotes/snjs' +import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption' -export const PLAIN_EDITOR_NAME = 'Plain Editor'; +export const PLAIN_EDITOR_NAME = 'Plain Editor' -type EditorGroup = NoteType | 'plain' | 'others'; +type EditorGroup = NoteType | 'plain' | 'others' -const getEditorGroup = ( - featureDescription: FeatureDescription -): EditorGroup => { +const getEditorGroup = (featureDescription: FeatureDescription): EditorGroup => { if (featureDescription.note_type) { - return featureDescription.note_type; + return featureDescription.note_type } else if (featureDescription.file_type) { switch (featureDescription.file_type) { case 'txt': - return 'plain'; + return 'plain' case 'html': - return NoteType.RichText; + return NoteType.RichText case 'md': - return NoteType.Markdown; + return NoteType.Markdown default: - return 'others'; + return 'others' } } - return 'others'; -}; + return 'others' +} -export const createEditorMenuGroups = ( - application: WebApplication, - editors: SNComponent[] -) => { +export const createEditorMenuGroups = (application: WebApplication, editors: SNComponent[]) => { const editorItems: Record = { plain: [ { @@ -52,40 +47,34 @@ export const createEditorMenuGroups = ( spreadsheet: [], authentication: [], others: [], - }; + } GetFeatures() .filter( (feature) => - feature.content_type === ContentType.Component && - feature.area === ComponentArea.Editor + feature.content_type === ContentType.Component && feature.area === ComponentArea.Editor, ) .forEach((editorFeature) => { - const notInstalled = !editors.find( - (editor) => editor.identifier === editorFeature.identifier - ); - const isExperimental = application.features.isExperimentalFeature( - editorFeature.identifier - ); + const notInstalled = !editors.find((editor) => editor.identifier === editorFeature.identifier) + const isExperimental = application.features.isExperimentalFeature(editorFeature.identifier) if (notInstalled && !isExperimental) { editorItems[getEditorGroup(editorFeature)].push({ name: editorFeature.name as string, isEntitled: false, - }); + }) } - }); + }) editors.forEach((editor) => { const editorItem: EditorMenuItem = { name: editor.name, component: editor, isEntitled: - application.features.getFeatureStatus(editor.identifier) === - FeatureStatus.Entitled, - }; + application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled, + } - editorItems[getEditorGroup(editor.package_info)].push(editorItem); - }); + editorItems[getEditorGroup(editor.package_info)].push(editorItem) + }) const editorMenuGroups: EditorMenuGroup[] = [ { @@ -136,7 +125,7 @@ export const createEditorMenuGroups = ( title: 'Others', items: editorItems.others, }, - ]; + ] - return editorMenuGroups; -}; + return editorMenuGroups +} diff --git a/app/assets/javascripts/components/Checkbox.tsx b/app/assets/javascripts/Components/Checkbox/index.tsx similarity index 73% rename from app/assets/javascripts/components/Checkbox.tsx rename to app/assets/javascripts/Components/Checkbox/index.tsx index e453f55d6..11bb910e7 100644 --- a/app/assets/javascripts/components/Checkbox.tsx +++ b/app/assets/javascripts/Components/Checkbox/index.tsx @@ -1,12 +1,12 @@ -import { FunctionComponent } from 'preact'; +import { FunctionComponent } from 'preact' type CheckboxProps = { - name: string; - checked: boolean; - onChange: (e: Event) => void; - disabled?: boolean; - label: string; -}; + name: string + checked: boolean + onChange: (e: Event) => void + disabled?: boolean + label: string +} export const Checkbox: FunctionComponent = ({ name, @@ -28,5 +28,5 @@ export const Checkbox: FunctionComponent = ({ /> {label} - ); -}; + ) +} diff --git a/app/assets/javascripts/components/ComponentView/IsDeprecated.tsx b/app/assets/javascripts/Components/ComponentView/IsDeprecated.tsx similarity index 57% rename from app/assets/javascripts/components/ComponentView/IsDeprecated.tsx rename to app/assets/javascripts/Components/ComponentView/IsDeprecated.tsx index f65e80008..a1f4150c1 100644 --- a/app/assets/javascripts/components/ComponentView/IsDeprecated.tsx +++ b/app/assets/javascripts/Components/ComponentView/IsDeprecated.tsx @@ -1,14 +1,14 @@ -import { FunctionalComponent } from 'preact'; +import { FunctionalComponent } from 'preact' interface IProps { - deprecationMessage: string | undefined; - dismissDeprecationMessage: () => void; + deprecationMessage: string | undefined + dismissDeprecationMessage: () => void } export const IsDeprecated: FunctionalComponent = ({ - deprecationMessage, - dismissDeprecationMessage - }) => { + deprecationMessage, + dismissDeprecationMessage, +}) => { return (
@@ -21,12 +21,10 @@ export const IsDeprecated: FunctionalComponent = ({
- +
- ); -}; + ) +} diff --git a/app/assets/javascripts/components/ComponentView/IsExpired.tsx b/app/assets/javascripts/Components/ComponentView/IsExpired.tsx similarity index 53% rename from app/assets/javascripts/components/ComponentView/IsExpired.tsx rename to app/assets/javascripts/Components/ComponentView/IsExpired.tsx index ce95e0750..d2df6cee1 100644 --- a/app/assets/javascripts/components/ComponentView/IsExpired.tsx +++ b/app/assets/javascripts/Components/ComponentView/IsExpired.tsx @@ -1,29 +1,25 @@ -import { FeatureStatus } from '@standardnotes/snjs'; -import { FunctionalComponent } from 'preact'; +import { FeatureStatus } from '@standardnotes/snjs' +import { FunctionalComponent } from 'preact' interface IProps { - expiredDate: string; - componentName: string; - featureStatus: FeatureStatus; - manageSubscription: () => void; + expiredDate: string + componentName: string + featureStatus: FeatureStatus + manageSubscription: () => void } -const statusString = ( - featureStatus: FeatureStatus, - expiredDate: string, - componentName: string -) => { +const statusString = (featureStatus: FeatureStatus, expiredDate: string, componentName: string) => { switch (featureStatus) { case FeatureStatus.InCurrentPlanButExpired: - return `Your subscription expired on ${expiredDate}`; + return `Your subscription expired on ${expiredDate}` case FeatureStatus.NoUserSubscription: - return `You do not have an active subscription`; + return 'You do not have an active subscription' case FeatureStatus.NotInCurrentPlan: - return `Please upgrade your plan to access ${componentName}`; + return `Please upgrade your plan to access ${componentName}` default: - return `${componentName} is valid and you should not be seeing this message`; + return `${componentName} is valid and you should not be seeing this message` } -}; +} export const IsExpired: FunctionalComponent = ({ expiredDate, @@ -41,27 +37,18 @@ export const IsExpired: FunctionalComponent = ({
- - {statusString(featureStatus, expiredDate, componentName)} - -
- {componentName} is in a read-only state. -
+ {statusString(featureStatus, expiredDate, componentName)} +
{componentName} is in a read-only state.
-
manageSubscription()} - > - +
manageSubscription()}> +
- ); -}; + ) +} diff --git a/app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx b/app/assets/javascripts/Components/ComponentView/IssueOnLoading.tsx similarity index 59% rename from app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx rename to app/assets/javascripts/Components/ComponentView/IssueOnLoading.tsx index b2c70b408..69f3c48ca 100644 --- a/app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx +++ b/app/assets/javascripts/Components/ComponentView/IssueOnLoading.tsx @@ -1,22 +1,17 @@ -import { FunctionalComponent } from 'preact'; +import { FunctionalComponent } from 'preact' interface IProps { - componentName: string; - reloadIframe: () => void; + componentName: string + reloadIframe: () => void } -export const IssueOnLoading: FunctionalComponent = ({ - componentName, - reloadIframe, -}) => { +export const IssueOnLoading: FunctionalComponent = ({ componentName, reloadIframe }) => { return (
-
- There was an issue loading {componentName}. -
+
There was an issue loading {componentName}.
@@ -26,5 +21,5 @@ export const IssueOnLoading: FunctionalComponent = ({
- ); -}; + ) +} diff --git a/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx b/app/assets/javascripts/Components/ComponentView/OfflineRestricted.tsx similarity index 69% rename from app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx rename to app/assets/javascripts/Components/ComponentView/OfflineRestricted.tsx index a60ee0de3..290ef05d0 100644 --- a/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx +++ b/app/assets/javascripts/Components/ComponentView/OfflineRestricted.tsx @@ -1,4 +1,4 @@ -import { FunctionalComponent } from 'preact'; +import { FunctionalComponent } from 'preact' export const OfflineRestricted: FunctionalComponent = () => { return ( @@ -11,21 +11,17 @@ export const OfflineRestricted: FunctionalComponent = () => { You have restricted this component to not use a hosted version.
- Locally-installed components are not available in the web - application. + Locally-installed components are not available in the web application.
-
- To continue, choose from the following options: -
+
To continue, choose from the following options:
  • - Enable the Hosted option for this component by opening the - Preferences {'>'} General {'>'} Advanced Settings menu and{' '} - toggling 'Use hosted when local is unavailable' under this - component's options. Then press Reload. + Enable the Hosted option for this component by opening the Preferences {'>'}{' '} + General {'>'} Advanced Settings menu and toggling 'Use hosted when local is + unavailable' under this component's options. Then press Reload.
  • Use the desktop application.
@@ -35,5 +31,5 @@ export const OfflineRestricted: FunctionalComponent = () => {
- ); -}; + ) +} diff --git a/app/assets/javascripts/components/ComponentView/UrlMissing.tsx b/app/assets/javascripts/Components/ComponentView/UrlMissing.tsx similarity index 60% rename from app/assets/javascripts/components/ComponentView/UrlMissing.tsx rename to app/assets/javascripts/Components/ComponentView/UrlMissing.tsx index fa6a56af2..c077671f6 100644 --- a/app/assets/javascripts/components/ComponentView/UrlMissing.tsx +++ b/app/assets/javascripts/Components/ComponentView/UrlMissing.tsx @@ -1,7 +1,7 @@ -import { FunctionalComponent } from 'preact'; +import { FunctionalComponent } from 'preact' interface IProps { - componentName: string; + componentName: string } export const UrlMissing: FunctionalComponent = ({ componentName }) => { @@ -14,16 +14,14 @@ export const UrlMissing: FunctionalComponent = ({ componentName }) => { This extension is missing its URL property.

- In order to access your note immediately, - please switch from {componentName} to the Plain Editor. -

-
-

- Please contact help@standardnotes.com to remedy this issue. + In order to access your note immediately, please switch from {componentName} to the + Plain Editor.

+
+

Please contact help@standardnotes.com to remedy this issue.

- ); -}; + ) +} diff --git a/app/assets/javascripts/Components/ComponentView/index.tsx b/app/assets/javascripts/Components/ComponentView/index.tsx new file mode 100644 index 000000000..be26e251d --- /dev/null +++ b/app/assets/javascripts/Components/ComponentView/index.tsx @@ -0,0 +1,221 @@ +import { + ComponentAction, + FeatureStatus, + SNComponent, + dateToLocalizedString, + ComponentViewer, + ComponentViewerEvent, + ComponentViewerError, +} from '@standardnotes/snjs' +import { WebApplication } from '@/UIModels/Application' +import { FunctionalComponent } from 'preact' +import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks' +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 { AppState } from '@/UIModels/AppState' +import { openSubscriptionDashboard } from '@/Utils/ManageSubscription' + +interface IProps { + application: WebApplication + appState: AppState + componentViewer: ComponentViewer + requestReload?: (viewer: ComponentViewer, force?: boolean) => void + onLoad?: (component: SNComponent) => void + manualDealloc?: boolean +} + +/** + * The maximum amount of time we'll wait for a component + * to load before displaying error + */ +const MaxLoadThreshold = 4000 +const VisibilityChangeKey = 'visibilitychange' +const MSToWaitAfterIframeLoadToAvoidFlicker = 35 + +export const ComponentView: FunctionalComponent = observer( + ({ application, onLoad, componentViewer, requestReload }) => { + const iframeRef = useRef(null) + const excessiveLoadingTimeout = useRef | 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 component = componentViewer.component + + const manageSubscription = useCallback(() => { + openSubscriptionDashboard(application) + }, [application]) + + 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) + } + + const onVisibilityChange = useCallback(() => { + if (document.visibilityState === 'hidden') { + return + } + if (hasIssueLoading) { + requestReload?.(componentViewer) + } + }, [hasIssueLoading, componentViewer, requestReload]) + + const handleIframeTakingTooLongToLoad = useCallback(async () => { + setIsLoading(false) + setHasIssueLoading(true) + + if (!didAttemptReload) { + setDidAttemptReload(true) + requestReload?.(componentViewer) + } else { + document.addEventListener(VisibilityChangeKey, onVisibilityChange) + } + }, [didAttemptReload, onVisibilityChange, componentViewer, requestReload]) + + useMemo(() => { + const loadTimeout = setTimeout(() => { + handleIframeTakingTooLongToLoad().catch(console.error) + }, MaxLoadThreshold) + + excessiveLoadingTimeout.current = loadTimeout + + return () => { + excessiveLoadingTimeout.current && clearTimeout(excessiveLoadingTimeout.current) + } + }, [handleIframeTakingTooLongToLoad]) + + const onIframeLoad = useCallback(() => { + const iframe = iframeRef.current as HTMLIFrameElement + const contentWindow = iframe.contentWindow as Window + excessiveLoadingTimeout.current && clearTimeout(excessiveLoadingTimeout.current) + + componentViewer.setWindow(contentWindow).catch(console.error) + + setTimeout(() => { + setIsLoading(false) + setHasIssueLoading(false) + onLoad?.(component) + }, MSToWaitAfterIframeLoadToAvoidFlicker) + }, [componentViewer, onLoad, component, excessiveLoadingTimeout]) + + useEffect(() => { + const removeFeaturesChangedObserver = componentViewer.addEventObserver((event) => { + if (event === ComponentViewerEvent.FeatureStatusUpdated) { + setFeatureStatus(componentViewer.getFeatureStatus()) + } + }) + + return () => { + removeFeaturesChangedObserver() + } + }, [componentViewer]) + + 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 () => { + 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 &&
} + + ) + }, +) diff --git a/app/assets/javascripts/Components/ConfirmSignoutModal/index.tsx b/app/assets/javascripts/Components/ConfirmSignoutModal/index.tsx new file mode 100644 index 000000000..132b42574 --- /dev/null +++ b/app/assets/javascripts/Components/ConfirmSignoutModal/index.tsx @@ -0,0 +1,98 @@ +import { useEffect, useRef, useState } from 'preact/hooks' +import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog' +import { STRING_SIGN_OUT_CONFIRMATION } from '@/Strings' +import { WebApplication } from '@/UIModels/Application' +import { AppState } from '@/UIModels/AppState' +import { observer } from 'mobx-react-lite' + +type Props = { + application: WebApplication + appState: AppState +} + +export const ConfirmSignoutContainer = observer((props: Props) => { + if (!props.appState.accountMenu.signingOut) { + return null + } + return +}) + +export const ConfirmSignoutModal = observer(({ application, appState }: Props) => { + const [deleteLocalBackups, setDeleteLocalBackups] = useState(false) + + const cancelRef = useRef(null) + function closeDialog() { + appState.accountMenu.setSigningOut(false) + } + + const [localBackupsCount, setLocalBackupsCount] = useState(0) + + useEffect(() => { + application.bridge.localBackupsCount().then(setLocalBackupsCount).catch(console.error) + }, [appState.accountMenu.signingOut, application.bridge]) + + return ( + +
+
+
+
+
+ + Sign out workspace? + + +

{STRING_SIGN_OUT_CONFIRMATION}

+
+ {localBackupsCount > 0 && ( +
+
+ + +
+ )} +
+ + +
+
+
+
+
+
+
+ ) +}) diff --git a/app/assets/javascripts/components/Dropdown.tsx b/app/assets/javascripts/Components/Dropdown/index.tsx similarity index 74% rename from app/assets/javascripts/components/Dropdown.tsx rename to app/assets/javascripts/Components/Dropdown/index.tsx index 837697520..9bc9b6b22 100644 --- a/app/assets/javascripts/components/Dropdown.tsx +++ b/app/assets/javascripts/Components/Dropdown/index.tsx @@ -5,32 +5,32 @@ import { ListboxList, ListboxOption, ListboxPopover, -} from '@reach/listbox'; -import VisuallyHidden from '@reach/visually-hidden'; -import { FunctionComponent } from 'preact'; -import { Icon } from './Icon'; -import { IconType } from '@standardnotes/snjs'; +} from '@reach/listbox' +import VisuallyHidden from '@reach/visually-hidden' +import { FunctionComponent } from 'preact' +import { Icon } from '@/Components/Icon' +import { IconType } from '@standardnotes/snjs' export type DropdownItem = { - icon?: IconType; - iconClassName?: string; - label: string; - value: string; - disabled?: boolean; -}; + icon?: IconType + iconClassName?: string + label: string + value: string + disabled?: boolean +} type DropdownProps = { - id: string; - label: string; - items: DropdownItem[]; - value: string; - onChange: (value: string, item: DropdownItem) => void; - disabled?: boolean; -}; + id: string + label: string + items: DropdownItem[] + value: string + onChange: (value: string, item: DropdownItem) => void + disabled?: boolean +} type ListboxButtonProps = DropdownItem & { - isExpanded: boolean; -}; + isExpanded: boolean +} const CustomDropdownButton: FunctionComponent = ({ label, @@ -47,15 +47,11 @@ const CustomDropdownButton: FunctionComponent = ({ ) : null}
{label}
- + -); +) export const Dropdown: FunctionComponent = ({ id, @@ -65,15 +61,13 @@ export const Dropdown: FunctionComponent = ({ onChange, disabled, }) => { - const labelId = `${id}-label`; + const labelId = `${id}-label` const handleChange = (value: string) => { - const selectedItem = items.find( - (item) => item.value === value - ) as DropdownItem; + const selectedItem = items.find((item) => item.value === value) as DropdownItem - onChange(value, selectedItem); - }; + onChange(value, selectedItem) + } return ( <> @@ -87,16 +81,16 @@ export const Dropdown: FunctionComponent = ({ { - const current = items.find((item) => item.value === value); - const icon = current ? current?.icon : null; - const iconClassName = current ? current?.iconClassName : null; + const current = items.find((item) => item.value === value) + const icon = current ? current?.icon : null + const iconClassName = current ? current?.iconClassName : null return CustomDropdownButton({ value: value ? value : label.toLowerCase(), label, isExpanded, ...(icon ? { icon } : null), ...(iconClassName ? { iconClassName } : null), - }); + }) }} /> @@ -125,5 +119,5 @@ export const Dropdown: FunctionComponent = ({ - ); -}; + ) +} diff --git a/app/assets/javascripts/components/Files/FilePreviewInfoPanel.tsx b/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx similarity index 70% rename from app/assets/javascripts/components/Files/FilePreviewInfoPanel.tsx rename to app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx index cf215c80d..5a12704e9 100644 --- a/app/assets/javascripts/components/Files/FilePreviewInfoPanel.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewInfoPanel.tsx @@ -1,11 +1,11 @@ -import { formatSizeToReadableString } from '@standardnotes/filepicker'; -import { SNFile } from '@standardnotes/snjs'; -import { FunctionComponent } from 'preact'; -import { Icon } from '../Icon'; +import { formatSizeToReadableString } from '@standardnotes/filepicker' +import { SNFile } from '@standardnotes/snjs' +import { FunctionComponent } from 'preact' +import { Icon } from '@/Components/Icon' type Props = { - file: SNFile; -}; + file: SNFile +} export const FilePreviewInfoPanel: FunctionComponent = ({ file }) => { return ( @@ -18,12 +18,10 @@ export const FilePreviewInfoPanel: FunctionComponent = ({ file }) => { Type: {file.mimeType}
- Size:{' '} - {formatSizeToReadableString(file.size)} + Size: {formatSizeToReadableString(file.size)}
- Created:{' '} - {file.created_at.toLocaleString()} + Created: {file.created_at.toLocaleString()}
Last Modified:{' '} @@ -33,5 +31,5 @@ export const FilePreviewInfoPanel: FunctionComponent = ({ file }) => { File ID: {file.uuid}
- ); -}; + ) +} diff --git a/app/assets/javascripts/components/Files/FilePreviewModal.tsx b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx similarity index 68% rename from app/assets/javascripts/components/Files/FilePreviewModal.tsx rename to app/assets/javascripts/Components/Files/FilePreviewModal.tsx index 6ea7f46e0..f709fde05 100644 --- a/app/assets/javascripts/components/Files/FilePreviewModal.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx @@ -1,88 +1,81 @@ -import { WebApplication } from '@/ui_models/application'; -import { concatenateUint8Arrays } from '@/utils/concatenateUint8Arrays'; -import { DialogContent, DialogOverlay } from '@reach/dialog'; -import { SNFile } from '@standardnotes/snjs'; -import { NoPreviewIllustration } from '@standardnotes/stylekit'; -import { FunctionComponent } from 'preact'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; -import { getFileIconComponent } from '../AttachedFilesPopover/PopoverFileItem'; -import { Button } from '../Button'; -import { Icon } from '../Icon'; -import { FilePreviewInfoPanel } from './FilePreviewInfoPanel'; -import { isFileTypePreviewable } from './isFilePreviewable'; +import { WebApplication } from '@/UIModels/Application' +import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays' +import { DialogContent, DialogOverlay } from '@reach/dialog' +import { SNFile } from '@standardnotes/snjs' +import { NoPreviewIllustration } from '@standardnotes/stylekit' +import { FunctionComponent } from 'preact' +import { useCallback, useEffect, useRef, useState } from 'preact/hooks' +import { getFileIconComponent } from '@/Components/AttachedFilesPopover/PopoverFileItem' +import { Button } from '@/Components/Button/Button' +import { Icon } from '@/Components/Icon' +import { FilePreviewInfoPanel } from './FilePreviewInfoPanel' +import { isFileTypePreviewable } from './isFilePreviewable' type Props = { - application: WebApplication; - file: SNFile; - onDismiss: () => void; -}; + application: WebApplication + file: SNFile + onDismiss: () => void +} const getPreviewComponentForFile = (file: SNFile, objectUrl: string) => { if (file.mimeType.startsWith('image/')) { - return ; + return } if (file.mimeType.startsWith('video/')) { - return