From d8d4052a527243cd38125a0eff516c449a5bf36d Mon Sep 17 00:00:00 2001 From: Mo Date: Sat, 5 Aug 2023 12:48:39 -0500 Subject: [PATCH] refactor(web): dependency management (#2386) --- packages/mobile/ios/Podfile.lock | 4 +- .../Domain/Api/LegacyApiServiceInterface.ts | 4 + .../Application/ApplicationInterface.ts | 73 +- .../Application/Options/ApplicationOptions.ts | 0 .../Domain}/Application/Options/Defaults.ts | 0 .../Application/Options/OptionalOptions.ts | 0 .../Application/Options/RequiredOptions.ts | 0 .../Challenge/ChallengeServiceInterface.ts | 2 - .../Domain/Device/DesktopManagerInterface.ts | 3 + .../Encryption/EncryptionProviderInterface.ts | 2 + .../Domain/Encryption/EncryptionService.ts | 3 +- .../src/Domain/Event/ApplicationEvent.ts | 2 - .../src/Domain/Item/ItemManagerInterface.ts | 6 + .../src/Domain/Mfa/MfaServiceInterface.ts} | 6 +- .../Preferences/PreferenceServiceInterface.ts | 2 +- .../Protection/ProtectionClientInterface.ts | 7 +- .../src/Domain/Protection/ProtectionEvent.ts | 6 + .../Domain/Session/SessionsClientInterface.ts | 1 + .../src/Domain/UseCase/ChangeAndSaveItem.ts | 33 + .../services/src/Domain/UseCase/GetHost.ts | 10 + .../services/src/Domain/UseCase/SetHost.ts | 18 + packages/services/src/Domain/index.ts | 15 +- packages/snjs/lib/Application/Application.ts | 202 +---- .../Application/Dependencies/Dependencies.ts | 28 +- .../lib/Application/Dependencies/Types.ts | 3 + packages/snjs/lib/Application/LiveItem.ts | 8 +- packages/snjs/lib/Application/index.ts | 1 - .../Services/Challenge/ChallengeService.ts | 6 +- .../UseCase/GetFeatureUrl.spec.ts | 4 +- .../snjs/lib/Services/Items/ItemManager.ts | 32 + packages/snjs/lib/Services/Mfa/MfaService.ts | 4 +- ...essionExpirationDuringActiveInteraction.ts | 1 + .../Services/Protection/ProtectionService.ts | 124 +-- .../Protection/ProtectionSessionDurations.ts | 20 + .../UnprotectedAccessSecondsDuration.ts | 6 + .../snjs/lib/Services/Protection/index.ts | 4 + .../isValidProtectionSessionLength.ts | 5 + .../lib/Services/Session/SessionManager.ts | 5 + .../snjs/lib/Services/Sync/SyncService.ts | 2 +- packages/snjs/mocha/application.test.js | 4 +- packages/snjs/mocha/auth-fringe-cases.test.js | 2 +- packages/snjs/mocha/features.test.js | 2 +- packages/snjs/mocha/history.test.js | 20 +- .../snjs/mocha/model_tests/importing.test.js | 2 +- packages/snjs/mocha/model_tests/items.test.js | 204 ++--- .../snjs/mocha/model_tests/notes_tags.test.js | 40 +- packages/snjs/mocha/protection.test.js | 6 +- .../snjs/mocha/sync_tests/conflicting.test.js | 91 +-- .../snjs/mocha/sync_tests/offline.test.js | 4 +- packages/snjs/mocha/sync_tests/online.test.js | 4 +- .../AegisToAuthenticatorConverter.spec.ts | 18 +- .../AegisToAuthenticatorConverter.ts | 8 +- .../EvernoteConverter.spec.ts | 16 +- .../EvernoteConverter/EvernoteConverter.ts | 10 +- .../GoogleKeepConverter.spec.ts | 16 +- .../GoogleKeepConverter.ts | 9 +- packages/ui-services/src/Import/Importer.ts | 30 +- .../PlaintextConverter/PlaintextConverter.ts | 6 +- .../SimplenoteConverter.spec.ts | 14 +- .../SimplenoteConverter.ts | 8 +- .../src/Security/AutolockService.ts | 4 +- .../src/StatePersistence/StatePersistence.ts | 4 +- .../ui-services/src/Theme/ThemeManager.ts | 12 +- .../ui-services/src/UseCase/GetItemTags.ts | 15 + .../src/UseCase/IsGlobalSpellcheckEnabled.ts | 11 + .../ui-services/src/UseCase/IsMobileDevice.ts | 11 + .../ui-services/src/UseCase/IsNativeIOS.ts | 13 + .../src/UseCase/IsNativeMobileWeb.ts | 10 + packages/ui-services/src/Utils/Utils.ts | 20 + .../src/Vaults/VaultDisplayService.ts | 8 +- .../WebApplication/WebApplicationInterface.ts | 12 +- packages/ui-services/src/index.ts | 7 + .../Domain/Dependency/DependencyContainer.ts | 50 ++ .../src/Domain/Dependency}/isDeinitable.ts | 0 packages/utils/src/Domain/index.ts | 2 + .../Application/Dependencies/Types.ts | 55 ++ .../Dependencies/WebDependencies.ts | 390 ++++++++++ .../src/javascripts/Application/DevMode.ts | 2 +- .../Application/Device/DesktopManager.ts | 32 +- .../Application/UseCase/GetPurchaseFlowUrl.ts | 25 + .../UseCase/IsTabletOrMobileScreen.ts | 32 + .../UseCase/LoadPurchaseFlowUrl.ts | 40 + .../UseCase/OpenSubscriptionDashboard.ts | 33 + .../Application/UseCase/PanesForLayout.ts | 35 + .../javascripts/Application/WebApplication.ts | 427 +++++++---- .../Application/WebApplicationGroup.ts | 2 +- .../javascripts/Application/WebServices.ts | 23 - .../Components/Abstract/PureComponent.tsx | 9 +- .../Components/AccountMenu/AccountMenu.tsx | 25 +- .../AccountMenu/AdvancedOptions.tsx | 11 +- .../AccountMenu/ConfirmPassword.tsx | 23 +- .../Components/AccountMenu/CreateAccount.tsx | 20 +- .../AccountMenu/GeneralAccountMenu.tsx | 50 +- .../AccountMenu/MenuPaneSelector.tsx | 31 +- .../Components/AccountMenu/SignIn.tsx | 21 +- .../Components/AccountMenu/User.tsx | 19 +- .../WorkspaceSwitcherMenu.tsx | 14 +- .../WorkspaceSwitcherOption.tsx | 10 +- .../ApplicationView/ApplicationView.tsx | 112 +-- .../ChallengeModal/BiometricsPrompt.tsx | 2 +- .../ChallengeModal/ChallengeModal.tsx | 17 +- .../ChallengeModal/ChallengePrompt.tsx | 2 +- .../LockscreenWorkspaceSwitcher.tsx | 5 +- .../Components/ChallengeModal/U2FPrompt.tsx | 10 +- .../ChangeEditor/ChangeEditorButton.tsx | 10 +- .../ChangeEditor/ChangeEditorMenu.tsx | 6 +- .../ChangeEditor/ChangeEditorMultipleMenu.tsx | 4 +- .../ClipperView/ClippedNoteView.tsx | 12 +- .../Components/ClipperView/ClipperView.tsx | 25 +- .../ComponentView/IframeFeatureView.tsx | 10 +- .../ComponentView/NotEntitledBanner.tsx | 3 +- .../ConfirmDeleteAccountModal.tsx | 10 +- .../ConfirmSignoutModal.tsx | 10 +- .../ContentListView/ContentList.tsx | 33 +- .../ContentListView/ContentListView.tsx | 75 +- .../Daily/DailyContentList.tsx | 3 +- .../Header/DisplayOptionsMenu.tsx | 6 +- .../Header/NewNotePreferences.tsx | 2 +- .../ContentTableView/ContentTableView.tsx | 129 +--- .../EditorWidthSelectionModal.tsx | 2 +- .../FileContextMenu/FileContextMenu.tsx | 77 +- .../FileContextMenu/FileMenuOptions.tsx | 24 +- .../FileContextMenu/FileOptionsPanel.tsx | 22 +- .../Components/FileDragNDropProvider.tsx | 12 +- .../Components/FilePreview/FilePreview.tsx | 2 +- .../FilePreview/FilePreviewModal.tsx | 22 +- .../FilePreview/PreviewComponent.tsx | 2 +- .../Components/FileView/FileView.tsx | 12 +- .../Components/FileView/FileViewProps.tsx | 2 - .../FileView/FileViewWithoutProtection.tsx | 23 +- .../Components/Footer/AccountMenuButton.tsx | 18 +- .../javascripts/Components/Footer/Footer.tsx | 42 +- .../Components/ImportModal/ImportModal.tsx | 7 +- .../LinkedItems/LinkedItemsButton.tsx | 14 +- .../LinkedItems/LinkedItemsPanel.tsx | 41 +- .../MultipleSelectedFiles.tsx | 30 +- .../MultipleSelectedNotes.tsx | 32 +- .../NoteGroupView/NoteGroupView.tsx | 49 +- .../NoteView/Controller/FileViewController.ts | 32 +- .../Controller/ItemGroupController.ts | 54 +- .../Controller/NoteViewController.spec.ts | 72 +- .../NoteView/Controller/NoteViewController.ts | 53 +- .../NoteConflictResolutionModal.tsx | 2 +- .../Components/NoteView/NoteView.test.ts | 13 +- .../Components/NoteView/NoteView.tsx | 44 +- .../NoteView/NoteViewFileDropTarget.tsx | 2 +- .../NoteView/PlainEditor/PlainEditor.tsx | 15 +- .../NotesContextMenu/NotesContextMenu.tsx | 31 +- .../Components/NotesOptions/NotesOptions.tsx | 62 +- .../NotesOptions/NotesOptionsPanel.tsx | 22 +- .../NotesOptions/NotesOptionsProps.ts | 10 - .../OtherSessionsSignOut.tsx | 10 +- .../Components/Panes/PanesSystemComponent.tsx | 14 - .../PasswordWizard/PasswordWizard.tsx | 2 +- .../Components/Preferences/PaneSelector.tsx | 32 +- .../Panes/Account/AccountPreferences.tsx | 16 +- .../Panes/Account/Authentication.tsx | 16 +- .../Panes/Account/ClearSessionDataView.tsx | 10 +- .../Preferences/Panes/Account/Credentials.tsx | 4 +- .../Panes/Account/DeleteAccount.tsx | 6 +- .../Preferences/Panes/Account/Email/Email.tsx | 2 +- .../Preferences/Panes/Account/SignOutView.tsx | 18 +- .../Account/Subscription/Subscription.tsx | 6 +- .../Subscription/SubscriptionInformation.tsx | 3 +- .../SubscriptionSharing.tsx | 7 +- .../Preferences/Panes/Backups/Backups.tsx | 6 +- .../Preferences/Panes/Backups/DataBackups.tsx | 34 +- .../Panes/Backups/EmailBackups.tsx | 2 +- .../TextBackups/TextBackupsDesktop.tsx | 2 +- .../General/Advanced/AdvancedSection.tsx | 6 +- .../General/Advanced/OfflineSubscription.tsx | 4 +- .../Advanced/Packages/PackageEntry.tsx | 16 +- .../Preferences/Panes/General/General.tsx | 12 +- .../Preferences/Panes/General/Persistence.tsx | 4 +- .../EditSmartViewModalController.tsx | 2 +- .../Panes/General/SmartViews/SmartViews.tsx | 2 +- .../Preferences/Panes/HelpFeedback.tsx | 2 +- .../Panes/HomeServer/HomeServerSettings.tsx | 2 - .../HomeServer/Status/StatusIndicator.tsx | 2 +- .../Preferences/Panes/Listed/Listed.tsx | 5 +- .../Preferences/Panes/Security/Encryption.tsx | 7 +- .../Panes/Security/ErroredItems.tsx | 6 +- .../Panes/Security/MultitaskingPrivacy.tsx | 2 +- .../Panes/Security/PasscodeLock.tsx | 15 +- .../Preferences/Panes/Security/Privacy.tsx | 2 +- .../Preferences/Panes/Security/Security.tsx | 18 +- .../Panes/Security/TwoFactorAuth/MfaProps.ts | 3 - .../TwoFactorAuth/TwoFactorActivation.ts | 6 +- .../Security/TwoFactorAuth/TwoFactorAuth.ts | 18 +- .../TwoFactorAuth/TwoFactorAuthWrapper.tsx | 2 +- .../Panes/Security/U2F/U2FAddDeviceView.tsx | 16 +- .../Panes/Security/U2F/U2FProps.ts | 2 - .../Security/U2F/U2FView/U2FDescription.tsx | 9 +- .../Panes/Security/U2F/U2FView/U2FTitle.tsx | 10 +- .../Panes/Security/U2F/U2FView/U2FView.tsx | 9 +- .../Panes/Security/U2F/U2FWrapper.tsx | 2 +- .../Preferences/Panes/Vaults/Vaults.tsx | 2 +- .../Preferences/PreferencesProps.tsx | 2 - .../Preferences/PreferencesView.tsx | 25 +- .../Preferences/PreferencesViewWrapper.tsx | 20 +- .../PreferencesViewWrapperProps.tsx | 2 - .../Preferences/Providers/UserProvider.ts | 3 - .../Components/Preferences/Providers/index.ts | 2 - .../Subviews/UpgradePrompt.tsx | 3 +- .../PurchaseFlow/Panes/CreateAccount.tsx | 12 +- .../Components/PurchaseFlow/Panes/SignIn.tsx | 10 +- .../PurchaseFlow/PurchaseFlowFunctions.ts | 45 -- .../PurchaseFlow/PurchaseFlowView.tsx | 24 +- .../PurchaseFlow/PurchaseFlowWrapper.tsx | 6 +- .../PurchaseFlow/PurchaseFlowWrapperProps.tsx | 2 - .../QuickSettingsMenu/QuickSettingsMenu.tsx | 4 +- .../HistoryModalContentPane.tsx | 3 +- .../HistoryModalDialogContent.tsx | 29 +- .../RevisionHistoryModal.tsx | 22 +- .../RevisionHistoryModalProps.tsx | 7 +- .../SessionsModal/SessionsModal.tsx | 22 +- .../Plugins/EncryptedFilePlugin/FilePlugin.ts | 2 +- .../Plugins/ExportPlugin/ExportPlugin.ts | 8 +- .../Plugins/ReadonlyPlugin/ReadonlyPlugin.tsx | 2 +- .../RemoteImageComponent.tsx | 5 +- .../SuperEditor/SuperNoteConverter.tsx | 12 +- .../SuperEditor/SuperNoteImporter.tsx | 12 +- .../Components/Tags/Navigation.tsx | 21 +- .../Components/Tags/SmartViewsListItem.tsx | 2 +- .../Components/Tags/TagContextMenu.tsx | 5 +- .../javascripts/Components/Tags/TagsList.tsx | 31 +- .../Components/Tags/TagsSection.tsx | 37 +- .../Abstract/AbstractViewController.ts | 7 +- .../Abstract/PersistenceService.ts | 121 ++- .../AccountMenu/AccountMenuController.ts | 52 +- .../Controllers/CrossControllerEvent.ts | 11 +- .../Controllers/FeaturesController.ts | 68 +- .../Controllers/FilePreviewModalController.ts | 7 +- .../Controllers/FilesController.ts | 98 ++- .../Controllers/ImportModalController.ts | 20 +- .../ItemList/ItemListController.spec.ts | 65 +- .../ItemList/ItemListController.ts | 710 ++++++++++++++---- .../Controllers/LinkingController.spec.ts | 48 +- .../Controllers/LinkingController.tsx | 105 +-- .../Controllers/Moments/MomentsService.ts | 93 ++- .../Navigation/NavigationController.ts | 134 ++-- .../Controllers/Navigation/TagsCountsState.ts | 7 +- .../Controllers/Navigation/Utils.ts | 22 +- .../Controllers/NoAccountWarningController.ts | 56 +- .../NoteHistory/HistoryModalController.ts | 11 +- .../NoteHistory/NoteHistoryController.ts | 98 +-- .../Controllers/NoteHistory/Types.ts | 20 + .../Controllers/NoteSyncController.ts | 31 +- .../NotesController/NotesController.ts | 160 ++-- .../PaneController/PaneController.ts | 51 +- .../PaneController/panesForLayout.ts | 30 - .../Controllers/PreferencesController.ts | 12 +- .../PurchaseFlow/PurchaseFlowController.ts | 53 +- .../Controllers/QuickSettingsController.ts | 5 +- .../Controllers/SearchOptionsController.ts | 40 +- .../Controllers/SelectedItemsController.ts | 407 ---------- .../Subscription/SubscriptionController.ts | 95 +-- .../VaultSelectionMenuController.ts | 5 +- .../Controllers/ViewControllerManager.ts | 322 -------- .../Event/ApplicationEventObserver.ts | 4 +- .../javascripts/Hooks/useContextMenuEvent.tsx | 2 +- .../src/javascripts/Hooks/useDocumentRect.ts | 2 +- .../Hooks/useIsTabletOrMobileScreen.tsx | 27 +- packages/web/src/javascripts/Hooks/useItem.ts | 2 +- .../web/src/javascripts/Hooks/useItemLinks.ts | 2 +- .../src/javascripts/Hooks/usePremiumModal.tsx | 68 +- .../NativeMobileWeb/DownloadBlobOnAndroid.tsx | 10 +- .../DownloadSelectedNotesOnAndroid.tsx | 8 +- .../NativeMobileWeb/ShareBlobOnMobile.ts | 13 +- .../NativeMobileWeb/ShareSelectedNotes.tsx | 9 +- .../NativeMobileWeb/useAndroidBackHandler.tsx | 2 +- .../Utils/DownloadOrShareBasedOnPlatform.ts | 31 +- .../javascripts/Utils/ManageSubscription.ts | 24 - packages/web/src/javascripts/Utils/Utils.ts | 9 +- 274 files changed, 4065 insertions(+), 3873 deletions(-) rename packages/{snjs/lib => services/src/Domain}/Application/Options/ApplicationOptions.ts (100%) rename packages/{snjs/lib => services/src/Domain}/Application/Options/Defaults.ts (100%) rename packages/{snjs/lib => services/src/Domain}/Application/Options/OptionalOptions.ts (100%) rename packages/{snjs/lib => services/src/Domain}/Application/Options/RequiredOptions.ts (100%) rename packages/{web/src/javascripts/Components/Preferences/Providers/MfaProvider.ts => services/src/Domain/Mfa/MfaServiceInterface.ts} (84%) create mode 100644 packages/services/src/Domain/Protection/ProtectionEvent.ts create mode 100644 packages/services/src/Domain/UseCase/ChangeAndSaveItem.ts create mode 100644 packages/services/src/Domain/UseCase/GetHost.ts create mode 100644 packages/services/src/Domain/UseCase/SetHost.ts create mode 100644 packages/snjs/lib/Services/Protection/ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction.ts create mode 100644 packages/snjs/lib/Services/Protection/ProtectionSessionDurations.ts create mode 100644 packages/snjs/lib/Services/Protection/UnprotectedAccessSecondsDuration.ts create mode 100644 packages/snjs/lib/Services/Protection/isValidProtectionSessionLength.ts create mode 100644 packages/ui-services/src/UseCase/GetItemTags.ts create mode 100644 packages/ui-services/src/UseCase/IsGlobalSpellcheckEnabled.ts create mode 100644 packages/ui-services/src/UseCase/IsMobileDevice.ts create mode 100644 packages/ui-services/src/UseCase/IsNativeIOS.ts create mode 100644 packages/ui-services/src/UseCase/IsNativeMobileWeb.ts create mode 100644 packages/ui-services/src/Utils/Utils.ts create mode 100644 packages/utils/src/Domain/Dependency/DependencyContainer.ts rename packages/{snjs/lib/Application/Dependencies => utils/src/Domain/Dependency}/isDeinitable.ts (100%) create mode 100644 packages/web/src/javascripts/Application/Dependencies/Types.ts create mode 100644 packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts create mode 100644 packages/web/src/javascripts/Application/UseCase/GetPurchaseFlowUrl.ts create mode 100644 packages/web/src/javascripts/Application/UseCase/IsTabletOrMobileScreen.ts create mode 100644 packages/web/src/javascripts/Application/UseCase/LoadPurchaseFlowUrl.ts create mode 100644 packages/web/src/javascripts/Application/UseCase/OpenSubscriptionDashboard.ts create mode 100644 packages/web/src/javascripts/Application/UseCase/PanesForLayout.ts delete mode 100644 packages/web/src/javascripts/Application/WebServices.ts delete mode 100644 packages/web/src/javascripts/Components/Preferences/Providers/UserProvider.ts delete mode 100644 packages/web/src/javascripts/Components/Preferences/Providers/index.ts delete mode 100644 packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts create mode 100644 packages/web/src/javascripts/Controllers/NoteHistory/Types.ts delete mode 100644 packages/web/src/javascripts/Controllers/SelectedItemsController.ts delete mode 100644 packages/web/src/javascripts/Controllers/ViewControllerManager.ts delete mode 100644 packages/web/src/javascripts/Utils/ManageSubscription.ts diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 98c9e2a32..502c74129 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -723,7 +723,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: 57d2868c099736d80fcd648bf211b4431e51a558 + boost: a7c83b31436843459a1961bfd74b96033dc77234 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 4cce221dd782d3ff7c4172167bba09d58af67ccb @@ -743,7 +743,7 @@ SPEC CHECKSUMS: MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18 RCTTypeSafety: cb09f3e4747b6d18331a15eb05271de7441ca0b3 React: 13109005b5353095c052f26af37413340ccf7a5d diff --git a/packages/services/src/Domain/Api/LegacyApiServiceInterface.ts b/packages/services/src/Domain/Api/LegacyApiServiceInterface.ts index 1940a443d..149c05054 100644 --- a/packages/services/src/Domain/Api/LegacyApiServiceInterface.ts +++ b/packages/services/src/Domain/Api/LegacyApiServiceInterface.ts @@ -10,6 +10,8 @@ export interface LegacyApiServiceInterface extends AbstractService, FilesApiInterface { isThirdPartyHostUsed(): boolean + setHost(host: string): Promise + getHost(): string downloadOfflineFeaturesFromRepo( repo: SNFeatureRepo, @@ -24,4 +26,6 @@ export interface LegacyApiServiceInterface limit: number, sharedVaultUuids?: string[], ): HttpRequest + + getNewSubscriptionToken(): Promise } diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index 38799ac5e..6ff85a093 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -1,24 +1,27 @@ -import { VaultUserServiceInterface, VaultInviteServiceInterface } from '@standardnotes/services' +import { + VaultUserServiceInterface, + VaultInviteServiceInterface, + StorageServiceInterface, + SyncServiceInterface, + FullyResolvedApplicationOptions, + ProtectionsClientInterface, + ChangeAndSaveItem, + GetHost, + SetHost, + LegacyApiServiceInterface, + StatusServiceInterface, + MfaServiceInterface, +} from '@standardnotes/services' import { VaultLockServiceInterface } from './../VaultLock/VaultLockServiceInterface' import { HistoryServiceInterface } from './../History/HistoryServiceInterface' import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface' import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface' -import { SyncOptions } from './../Sync/SyncOptions' import { ImportDataReturnType } from './../Mutator/ImportDataUseCase' import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface' import { VaultServiceInterface } from '../Vault/VaultServiceInterface' import { ApplicationIdentifier } from '@standardnotes/common' -import { - BackupFile, - DecryptedItemInterface, - DecryptedItemMutator, - ItemStream, - PayloadEmitSource, - Platform, - PrefKey, - PrefValue, -} from '@standardnotes/models' +import { BackupFile, Environment, Platform, PrefKey, PrefValue } from '@standardnotes/models' import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files' import { AlertService } from '../Alert/AlertService' @@ -37,7 +40,6 @@ import { DeinitSource } from './DeinitSource' import { UserServiceInterface } from '../User/UserServiceInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface' -import { User } from '@standardnotes/responses' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' export interface ApplicationInterface { @@ -53,49 +55,24 @@ export interface ApplicationInterface { createDecryptedBackupFile(): Promise hasPasscode(): boolean lock(): Promise - softLockBiometrics(): void setValue(key: string, value: unknown, mode?: StorageValueModes): void getValue(key: string, mode?: StorageValueModes): T removeValue(key: string, mode?: StorageValueModes): Promise - isLocked(): Promise getPreference(key: K): PrefValue[K] | undefined getPreference(key: K, defaultValue: PrefValue[K]): PrefValue[K] getPreference(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined setPreference(key: K, value: PrefValue[K]): Promise - streamItems( - contentType: string | string[], - stream: ItemStream, - ): () => void - getUser(): User | undefined hasAccount(): boolean setCustomHost(host: string): Promise isThirdPartyHostUsed(): boolean isUsingHomeServer(): Promise - getNewSubscriptionToken(): Promise importData(data: BackupFile, awaitSync?: boolean): Promise - /** - * Mutates a pre-existing item, marks it as dirty, and syncs it - */ - changeAndSaveItem( - itemToLookupUuidFor: DecryptedItemInterface, - mutate: (mutator: M) => void, - updateTimestamps?: boolean, - emitSource?: PayloadEmitSource, - syncOptions?: SyncOptions, - ): Promise - /** - * Mutates pre-existing items, marks them as dirty, and syncs - */ - changeAndSaveItems( - itemsToLookupUuidsFor: DecryptedItemInterface[], - mutate: (mutator: M) => void, - updateTimestamps?: boolean, - emitSource?: PayloadEmitSource, - syncOptions?: SyncOptions, - ): Promise + get changeAndSaveItem(): ChangeAndSaveItem + get getHost(): GetHost + get setHost(): SetHost get alerts(): AlertService get asymmetric(): AsymmetricMessageServiceInterface @@ -109,16 +86,24 @@ export interface ApplicationInterface { get history(): HistoryServiceInterface get homeServer(): HomeServerServiceInterface | undefined get items(): ItemManagerInterface + get legacyApi(): LegacyApiServiceInterface + get mfa(): MfaServiceInterface get mutator(): MutatorClientInterface get preferences(): PreferenceServiceInterface + get protections(): ProtectionsClientInterface get sessions(): SessionsClientInterface + get status(): StatusServiceInterface + get storage(): StorageServiceInterface get subscriptions(): SubscriptionManagerInterface + get sync(): SyncServiceInterface get user(): UserServiceInterface - get vaults(): VaultServiceInterface - get vaultLocks(): VaultLockServiceInterface - get vaultUsers(): VaultUserServiceInterface get vaultInvites(): VaultInviteServiceInterface + get vaultLocks(): VaultLockServiceInterface + get vaults(): VaultServiceInterface + get vaultUsers(): VaultUserServiceInterface + readonly options: FullyResolvedApplicationOptions + readonly environment: Environment readonly identifier: ApplicationIdentifier readonly platform: Platform device: DeviceInterface diff --git a/packages/snjs/lib/Application/Options/ApplicationOptions.ts b/packages/services/src/Domain/Application/Options/ApplicationOptions.ts similarity index 100% rename from packages/snjs/lib/Application/Options/ApplicationOptions.ts rename to packages/services/src/Domain/Application/Options/ApplicationOptions.ts diff --git a/packages/snjs/lib/Application/Options/Defaults.ts b/packages/services/src/Domain/Application/Options/Defaults.ts similarity index 100% rename from packages/snjs/lib/Application/Options/Defaults.ts rename to packages/services/src/Domain/Application/Options/Defaults.ts diff --git a/packages/snjs/lib/Application/Options/OptionalOptions.ts b/packages/services/src/Domain/Application/Options/OptionalOptions.ts similarity index 100% rename from packages/snjs/lib/Application/Options/OptionalOptions.ts rename to packages/services/src/Domain/Application/Options/OptionalOptions.ts diff --git a/packages/snjs/lib/Application/Options/RequiredOptions.ts b/packages/services/src/Domain/Application/Options/RequiredOptions.ts similarity index 100% rename from packages/snjs/lib/Application/Options/RequiredOptions.ts rename to packages/services/src/Domain/Application/Options/RequiredOptions.ts diff --git a/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts b/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts index 51a31946a..2bbee34bb 100644 --- a/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts +++ b/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts @@ -14,8 +14,6 @@ export interface ChallengeServiceInterface extends AbstractService { submitValuesForChallenge(challenge: ChallengeInterface, values: ChallengeValue[]): Promise cancelChallenge(challenge: ChallengeInterface): void - isPasscodeLocked(): Promise - /** * Resolves when the challenge has been completed. * For non-validated challenges, will resolve when the first value is submitted. diff --git a/packages/services/src/Domain/Device/DesktopManagerInterface.ts b/packages/services/src/Domain/Device/DesktopManagerInterface.ts index 68affc8d0..7fd490b71 100644 --- a/packages/services/src/Domain/Device/DesktopManagerInterface.ts +++ b/packages/services/src/Domain/Device/DesktopManagerInterface.ts @@ -4,4 +4,7 @@ export interface DesktopManagerInterface { syncComponentsInstallation(components: ComponentInterface[]): void registerUpdateObserver(callback: (component: ComponentInterface) => void): () => void getExtServerHost(): string + saveDesktopBackup(): Promise + searchText(text?: string): void + redoSearch(): void } diff --git a/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts b/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts index fc9292a1e..529caf5a6 100644 --- a/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts +++ b/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts @@ -24,6 +24,8 @@ import { export interface EncryptionProviderInterface { initialize(): Promise + isPasscodeLocked(): Promise + encryptSplitSingle(split: KeyedEncryptionSplit): Promise encryptSplit(split: KeyedEncryptionSplit): Promise decryptSplitSingle< diff --git a/packages/services/src/Domain/Encryption/EncryptionService.ts b/packages/services/src/Domain/Encryption/EncryptionService.ts index 3afa0cf43..b19b53678 100644 --- a/packages/services/src/Domain/Encryption/EncryptionService.ts +++ b/packages/services/src/Domain/Encryption/EncryptionService.ts @@ -191,6 +191,7 @@ export class EncryptionService return ProtocolVersionLatest } + /** Unlike SessionManager.isSignedIn, hasAccount can be read before the application is unlocked and is based on the key state */ public hasAccount() { return this.rootKeyManager.hasAccount() } @@ -625,7 +626,7 @@ export class EncryptionService /** * @returns True if the root key has not yet been unwrapped (passcode locked). */ - public async isPasscodeLocked() { + public async isPasscodeLocked(): Promise { return (await this.rootKeyManager.hasRootKeyWrapper()) && this.rootKeyManager.getRootKey() == undefined } diff --git a/packages/services/src/Domain/Event/ApplicationEvent.ts b/packages/services/src/Domain/Event/ApplicationEvent.ts index 05cc78c77..7557fb240 100644 --- a/packages/services/src/Domain/Event/ApplicationEvent.ts +++ b/packages/services/src/Domain/Event/ApplicationEvent.ts @@ -55,7 +55,5 @@ export enum ApplicationEvent { UnprotectedSessionExpired = 'Application:UnprotectedSessionExpired', /** Called when the app first launches and after first sync request made after sign in */ CompletedInitialSync = 'Application:CompletedInitialSync', - BiometricsSoftLockEngaged = 'Application:BiometricsSoftLockEngaged', - BiometricsSoftLockDisengaged = 'Application:BiometricsSoftLockDisengaged', DidPurchaseSubscription = 'Application:DidPurchaseSubscription', } diff --git a/packages/services/src/Domain/Item/ItemManagerInterface.ts b/packages/services/src/Domain/Item/ItemManagerInterface.ts index edb26c970..9d4e05186 100644 --- a/packages/services/src/Domain/Item/ItemManagerInterface.ts +++ b/packages/services/src/Domain/Item/ItemManagerInterface.ts @@ -22,6 +22,7 @@ import { NotesAndFilesDisplayControllerOptions, ThemeInterface, ComponentInterface, + ItemStream, } from '@standardnotes/models' import { AbstractService } from '../Service/AbstractService' @@ -57,6 +58,11 @@ export interface ItemManagerInterface extends AbstractService { callback: ItemManagerChangeObserverCallback, ): () => void + streamItems( + contentType: string | string[], + stream: ItemStream, + ): () => void + get items(): DecryptedItemInterface[] getItems(contentType: string | string[]): T[] diff --git a/packages/web/src/javascripts/Components/Preferences/Providers/MfaProvider.ts b/packages/services/src/Domain/Mfa/MfaServiceInterface.ts similarity index 84% rename from packages/web/src/javascripts/Components/Preferences/Providers/MfaProvider.ts rename to packages/services/src/Domain/Mfa/MfaServiceInterface.ts index 9a002749c..a68222376 100644 --- a/packages/web/src/javascripts/Components/Preferences/Providers/MfaProvider.ts +++ b/packages/services/src/Domain/Mfa/MfaServiceInterface.ts @@ -1,11 +1,7 @@ -export interface MfaProvider { +export interface MfaServiceInterface { isMfaActivated(): Promise - generateMfaSecret(): Promise - getOtpToken(secret: string): Promise - enableMfa(secret: string, otpToken: string): Promise - disableMfa(): Promise } diff --git a/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts b/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts index 8c3db5bac..cf3be4846 100644 --- a/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts +++ b/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts @@ -6,9 +6,9 @@ export enum PreferencesServiceEvent { } export interface PreferenceServiceInterface extends AbstractService { - getValue(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined getValue(key: K, defaultValue: PrefValue[K]): PrefValue[K] getValue(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined + getValue(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined setValue(key: K, value: PrefValue[K]): Promise /** Set value without triggering sync or event notifications */ diff --git a/packages/services/src/Domain/Protection/ProtectionClientInterface.ts b/packages/services/src/Domain/Protection/ProtectionClientInterface.ts index e7a97e29e..8f8aad6e3 100644 --- a/packages/services/src/Domain/Protection/ProtectionClientInterface.ts +++ b/packages/services/src/Domain/Protection/ProtectionClientInterface.ts @@ -1,9 +1,14 @@ +import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface' import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models' import { ChallengeInterface, ChallengeReason } from '../Challenge' import { MobileUnlockTiming } from './MobileUnlockTiming' import { TimingDisplayOption } from './TimingDisplayOption' +import { ProtectionEvent } from './ProtectionEvent' + +export interface ProtectionsClientInterface extends ApplicationServiceInterface { + isLocked(): Promise + softLockBiometrics(): void -export interface ProtectionsClientInterface { createLaunchChallenge(): ChallengeInterface | undefined authorizeProtectedActionForItems(files: T[], challengeReason: ChallengeReason): Promise authorizeItemAccess(item: DecryptedItem): Promise diff --git a/packages/services/src/Domain/Protection/ProtectionEvent.ts b/packages/services/src/Domain/Protection/ProtectionEvent.ts new file mode 100644 index 000000000..82b3349f0 --- /dev/null +++ b/packages/services/src/Domain/Protection/ProtectionEvent.ts @@ -0,0 +1,6 @@ +export enum ProtectionEvent { + UnprotectedSessionBegan = 'Protection:UnprotectedSessionBegan', + UnprotectedSessionExpired = 'Protection:UnprotectedSessionExpired', + BiometricsSoftLockEngaged = 'Protection:BiometricsSoftLockEngaged', + BiometricsSoftLockDisengaged = 'Protection:BiometricsSoftLockDisengaged', +} diff --git a/packages/services/src/Domain/Session/SessionsClientInterface.ts b/packages/services/src/Domain/Session/SessionsClientInterface.ts index 37d54bf94..fe87cdf3c 100644 --- a/packages/services/src/Domain/Session/SessionsClientInterface.ts +++ b/packages/services/src/Domain/Session/SessionsClientInterface.ts @@ -20,6 +20,7 @@ export interface SessionsClientInterface { getUser(): User | undefined isSignedIn(): boolean + isSignedOut(): boolean get userUuid(): string getSureUser(): User isSignedIntoFirstPartyServer(): boolean diff --git a/packages/services/src/Domain/UseCase/ChangeAndSaveItem.ts b/packages/services/src/Domain/UseCase/ChangeAndSaveItem.ts new file mode 100644 index 000000000..62c55fd4c --- /dev/null +++ b/packages/services/src/Domain/UseCase/ChangeAndSaveItem.ts @@ -0,0 +1,33 @@ +import { SyncOptions } from './../Sync/SyncOptions' +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' +import { SyncServiceInterface } from './../Sync/SyncServiceInterface' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' +import { DecryptedItemInterface, DecryptedItemMutator, MutationType, PayloadEmitSource } from '@standardnotes/models' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' + +export class ChangeAndSaveItem implements UseCaseInterface { + constructor( + private readonly items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + ) {} + + async execute( + itemToLookupUuidFor: DecryptedItemInterface, + mutate: (mutator: M) => void, + updateTimestamps = true, + emitSource?: PayloadEmitSource, + syncOptions?: SyncOptions, + ): Promise> { + await this.mutator.changeItems( + [itemToLookupUuidFor], + mutate, + updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, + emitSource, + ) + + await this.sync.sync(syncOptions) + + return Result.ok(this.items.findItem(itemToLookupUuidFor.uuid)) + } +} diff --git a/packages/services/src/Domain/UseCase/GetHost.ts b/packages/services/src/Domain/UseCase/GetHost.ts new file mode 100644 index 000000000..44daebd92 --- /dev/null +++ b/packages/services/src/Domain/UseCase/GetHost.ts @@ -0,0 +1,10 @@ +import { LegacyApiServiceInterface } from './../Api/LegacyApiServiceInterface' +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' + +export class GetHost implements SyncUseCaseInterface { + constructor(private legacyApi: LegacyApiServiceInterface) {} + + execute(): Result { + return Result.ok(this.legacyApi.getHost()) + } +} diff --git a/packages/services/src/Domain/UseCase/SetHost.ts b/packages/services/src/Domain/UseCase/SetHost.ts new file mode 100644 index 000000000..664f95dee --- /dev/null +++ b/packages/services/src/Domain/UseCase/SetHost.ts @@ -0,0 +1,18 @@ +import { HttpServiceInterface } from '@standardnotes/api' +import { LegacyApiServiceInterface } from '../Api/LegacyApiServiceInterface' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' + +export class SetHost implements UseCaseInterface { + constructor( + private http: HttpServiceInterface, + private legacyApi: LegacyApiServiceInterface, + ) {} + + async execute(host: string): Promise> { + this.http.setHost(host) + + await this.legacyApi.setHost(host) + + return Result.ok() + } +} diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index d18610473..1a57b56c2 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -10,6 +10,10 @@ export * from './Application/ApplicationStage' export * from './Application/DeinitCallback' export * from './Application/DeinitMode' export * from './Application/DeinitSource' +export * from './Application/Options/ApplicationOptions' +export * from './Application/Options/Defaults' +export * from './Application/Options/OptionalOptions' +export * from './Application/Options/RequiredOptions' export * from './AsymmetricMessage/AsymmetricMessageService' export * from './AsymmetricMessage/AsymmetricMessageServiceInterface' export * from './AsymmetricMessage/UseCase/GetInboundMessages' @@ -117,12 +121,14 @@ export * from './Item/StaticItemCounter' export * from './ItemsEncryption/ItemsEncryption' export * from './ItemsEncryption/ItemsEncryption' export * from './KeySystem/KeySystemKeyManager' +export * from './Mfa/MfaServiceInterface' export * from './Mutator/ImportDataUseCase' export * from './Mutator/MutatorClientInterface' export * from './Payloads/PayloadManagerInterface' export * from './Preferences/PreferenceServiceInterface' export * from './Protection/MobileUnlockTiming' export * from './Protection/ProtectionClientInterface' +export * from './Protection/ProtectionEvent' export * from './Protection/TimingDisplayOption' export * from './Revision/RevisionClientInterface' export * from './Revision/RevisionManager' @@ -170,20 +176,23 @@ export * from './Sync/SyncOptions' export * from './Sync/SyncQueueStrategy' export * from './Sync/SyncServiceInterface' export * from './Sync/SyncSource' +export * from './UseCase/ChangeAndSaveItem' export * from './UseCase/DiscardItemsLocally' +export * from './UseCase/GetHost' +export * from './UseCase/SetHost' export * from './User/AccountEvent' export * from './User/AccountEventData' export * from './User/CredentialsChangeFunctionResponse' export * from './User/SignedInOrRegisteredEventPayload' export * from './User/SignedOutEventPayload' -export * from './User/UserServiceInterface' -export * from './User/UserServiceInterface' export * from './User/UserService' +export * from './User/UserServiceInterface' +export * from './User/UserServiceInterface' export * from './UserEvent/NotificationService' export * from './UserEvent/NotificationServiceEvent' -export * from './Vault/UseCase/ChangeVaultStorageMode' export * from './Vault/UseCase/ChangeVaultKeyOptions' export * from './Vault/UseCase/ChangeVaultKeyOptionsDTO' +export * from './Vault/UseCase/ChangeVaultStorageMode' export * from './Vault/UseCase/CreateVault' export * from './Vault/UseCase/DeleteVault' export * from './Vault/UseCase/GetVault' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 9ddb17ed1..bd8eef1b1 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -1,4 +1,4 @@ -import { SNMfaService } from './../Services/Mfa/MfaService' +import { MfaService } from './../Services/Mfa/MfaService' import { KeyRecoveryService } from './../Services/KeyRecovery/KeyRecoveryService' import { WebSocketsService } from './../Services/Api/WebsocketsService' import { MigrationService } from './../Services/Migration/MigrationService' @@ -20,7 +20,6 @@ import { ApplicationStageChangedEventPayload, StorageValueModes, ChallengeObserver, - SyncOptions, ImportDataReturnType, ImportDataUseCase, StoragePersistencePolicies, @@ -57,7 +56,6 @@ import { ApplicationInterface, EncryptionService, EncryptionServiceEvent, - ChallengePrompt, Challenge, ErrorAlertStrings, SessionsClientInterface, @@ -75,32 +73,33 @@ import { VaultInviteServiceInterface, NotificationServiceEvent, VaultLockServiceInterface, + ApplicationConstructorOptions, + FullyResolvedApplicationOptions, + ApplicationOptionsDefaults, + ChangeAndSaveItem, + ProtectionEvent, + GetHost, + SetHost, + MfaServiceInterface, } from '@standardnotes/services' import { - PayloadEmitSource, SNNote, PrefKey, PrefValue, - DecryptedItemMutator, BackupFile, - DecryptedItemInterface, EncryptedItemInterface, Environment, - ItemStream, Platform, - MutationType, } from '@standardnotes/models' import { HttpResponse, SessionListResponse, - User, SignInResponse, ClientDisplayableError, SessionListEntry, } from '@standardnotes/responses' import { SyncService, - ProtectionEvent, SettingsService, ActionsService, ChallengeResponse, @@ -116,14 +115,13 @@ import { UuidGenerator, useBoolean, LoggerInterface, + canBlockDeinit, } from '@standardnotes/utils' import { UuidString, ApplicationEventPayload } from '../Types' import { applicationEventForSyncEvent } from '@Lib/Application/Event' import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files' import { ComputePrivateUsername } from '@standardnotes/encryption' import { SNLog } from '../Log' -import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions' -import { ApplicationOptionsDefaults } from './Options/Defaults' import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes' import { UseCaseContainerInterface } from '@Lib/Domain/UseCase/UseCaseContainerInterface' import { GetRecoveryCodes } from '@Lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes' @@ -137,7 +135,6 @@ import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetA import { GetAuthenticatorAuthenticationOptions } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions' import { Dependencies } from './Dependencies/Dependencies' import { TYPES } from './Dependencies/Types' -import { canBlockDeinit } from './Dependencies/isDeinitable' /** How often to automatically sync, in milliseconds */ const DEFAULT_AUTO_SYNC_INTERVAL = 30_000 @@ -165,7 +162,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private eventHandlers: ApplicationObserver[] = [] - private streamRemovers: ObserverRemover[] = [] private serviceObservers: ObserverRemover[] = [] private managedSubscribers: ObserverRemover[] = [] private autoSyncInterval!: ReturnType @@ -178,7 +174,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private launched = false /** Whether the application has been destroyed via .deinit() */ public dealloced = false - private isBiometricsSoftLockEngaged = false + private revokingSession = false private handledFullSyncStage = false @@ -561,13 +557,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli void this.migrations.handleApplicationEvent(event) } - /** - * Whether the local database has completed loading local items. - */ - public isDatabaseLoaded(): boolean { - return this.sync.isDatabaseLoaded() - } - public getSessions(): Promise> { return this.sessions.getSessionsList() } @@ -594,65 +583,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return compareVersions(userVersion, ProtocolVersion.V004) >= 0 } - /** - * Begin streaming items to display in the UI. The stream callback will be called - * immediately with the present items that match the constraint, and over time whenever - * items matching the constraint are added, changed, or deleted. - */ - public streamItems( - contentType: string | string[], - stream: ItemStream, - ): () => void { - const removeItemManagerObserver = this.items.addObserver( - contentType, - ({ changed, inserted, removed, source }) => { - stream({ changed, inserted, removed, source }) - }, - ) - - const matches = this.items.getItems(contentType) - stream({ - inserted: matches, - changed: [], - removed: [], - source: PayloadEmitSource.InitialObserverRegistrationPush, - }) - - this.streamRemovers.push(removeItemManagerObserver) - - return () => { - removeItemManagerObserver() - - removeFromArray(this.streamRemovers, removeItemManagerObserver) - } - } - - /** - * Set the server's URL - */ - public async setHost(host: string): Promise { - this.http.setHost(host) - - await this.legacyApi.setHost(host) - } - - public getHost(): string { - return this.legacyApi.getHost() - } - public async setCustomHost(host: string): Promise { - await this.setHost(host) + await this.setHost.execute(host) this.sockets.setWebSocketUrl(undefined) } - public getUser(): User | undefined { - if (!this.launched) { - throw Error('Attempting to access user before application unlocked') - } - return this.sessions.getUser() - } - public getUserPasswordCreationDate(): Date | undefined { return this.encryption.getPasswordCreatedDate() } @@ -699,10 +635,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return result } - public noAccount(): boolean { - return !this.hasAccount() - } - public hasAccount(): boolean { return this.encryption.hasAccount() } @@ -715,10 +647,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.protections.hasProtectionSources() } - public hasUnprotectedAccessSession(): boolean { - return this.protections.hasUnprotectedAccessSession() - } - /** * When a user specifies a non-zero remember duration on a protection * challenge, a session will be started during which protections are disabled. @@ -746,10 +674,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.protections.authorizeAutolockIntervalChange() } - public authorizeSearchingProtectedNotesText(): Promise { - return this.protections.authorizeSearchingProtectedNotesText() - } - public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise { return this.encryption.createEncryptedBackupFile() } @@ -852,7 +776,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.serviceObservers.length = 0 this.managedSubscribers.length = 0 - this.streamRemovers.length = 0 this.started = false @@ -921,39 +844,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli }) } - public async changeAndSaveItem( - itemToLookupUuidFor: DecryptedItemInterface, - mutate: (mutator: M) => void, - updateTimestamps = true, - emitSource?: PayloadEmitSource, - syncOptions?: SyncOptions, - ): Promise { - await this.mutator.changeItems( - [itemToLookupUuidFor], - mutate, - updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, - emitSource, - ) - await this.sync.sync(syncOptions) - return this.items.findItem(itemToLookupUuidFor.uuid) - } - - public async changeAndSaveItems( - itemsToLookupUuidsFor: DecryptedItemInterface[], - mutate: (mutator: M) => void, - updateTimestamps = true, - emitSource?: PayloadEmitSource, - syncOptions?: SyncOptions, - ): Promise { - await this.mutator.changeItems( - itemsToLookupUuidsFor, - mutate, - updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, - emitSource, - ) - await this.sync.sync(syncOptions) - } - public async importData(data: BackupFile, awaitSync = false): Promise { const usecase = this.dependencies.get(TYPES.ImportDataUseCase) return usecase.execute(data, awaitSync) @@ -991,42 +881,18 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.encryption.hasPasscode() } - async isLocked(): Promise { - if (!this.started) { - return Promise.resolve(true) - } - const isPasscodeLocked = await this.challenges.isPasscodeLocked() - return isPasscodeLocked || this.isBiometricsSoftLockEngaged - } - public async lock(): Promise { - /** Because locking is a critical operation, we want to try to do it safely, - * but only up to a certain limit. */ + /** + * Because locking is a critical operation, we want to try to do it safely, + * but only up to a certain limit. + */ const MaximumWaitTime = 500 + await this.prepareForDeinit(MaximumWaitTime) + return this.deinit(this.getDeinitMode(), DeinitSource.Lock) } - public softLockBiometrics(): void { - const challenge = new Challenge( - [new ChallengePrompt(ChallengeValidation.Biometric)], - ChallengeReason.ApplicationUnlock, - false, - ) - - void this.challenges.promptForChallengeResponse(challenge) - - this.isBiometricsSoftLockEngaged = true - void this.notifyEvent(ApplicationEvent.BiometricsSoftLockEngaged) - - this.addChallengeObserver(challenge, { - onComplete: () => { - this.isBiometricsSoftLockEngaged = false - void this.notifyEvent(ApplicationEvent.BiometricsSoftLockDisengaged) - }, - }) - } - isNativeMobileWeb() { return this.environment === Environment.Mobile } @@ -1102,10 +968,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli } } - public getNewSubscriptionToken(): Promise { - return this.legacyApi.getNewSubscriptionToken() - } - public isThirdPartyHostUsed(): boolean { return this.legacyApi.isThirdPartyHostUsed() } @@ -1117,7 +979,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return false } - return this.getHost() === (await homeServerService.getHomeServerUrl()) + return this.getHost.execute().getValue() === (await homeServerService.getHomeServerUrl()) } private createBackgroundDependencies() { @@ -1361,14 +1223,30 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.dependencies.get(TYPES.SharedVaultService) } - private get migrations(): MigrationService { - return this.dependencies.get(TYPES.MigrationService) + public get changeAndSaveItem(): ChangeAndSaveItem { + return this.dependencies.get(TYPES.ChangeAndSaveItem) } - private get legacyApi(): LegacyApiService { + public get getHost(): GetHost { + return this.dependencies.get(TYPES.GetHost) + } + + public get setHost(): SetHost { + return this.dependencies.get(TYPES.SetHost) + } + + public get legacyApi(): LegacyApiService { return this.dependencies.get(TYPES.LegacyApiService) } + public get mfa(): MfaServiceInterface { + return this.dependencies.get(TYPES.MfaService) + } + + private get migrations(): MigrationService { + return this.dependencies.get(TYPES.MigrationService) + } + private get http(): HttpServiceInterface { return this.dependencies.get(TYPES.HttpService) } @@ -1376,8 +1254,4 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private get sockets(): WebSocketsService { return this.dependencies.get(TYPES.WebSocketsService) } - - private get mfa(): SNMfaService { - return this.dependencies.get(TYPES.MfaService) - } } diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index ca9789ed0..6c2bf38ae 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -11,7 +11,7 @@ import { GetRecoveryCodes } from '../../Domain/UseCase/GetRecoveryCodes/GetRecov import { SignInWithRecoveryCodes } from '../../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes' import { ListedService } from '../../Services/Listed/ListedService' import { MigrationService } from '../../Services/Migration/MigrationService' -import { SNMfaService } from '../../Services/Mfa/MfaService' +import { MfaService } from '../../Services/Mfa/MfaService' import { SNComponentManager } from '../../Services/ComponentManager/ComponentManager' import { FeaturesService } from '@Lib/Services/Features/FeaturesService' import { SettingsService } from '../../Services/Settings/SNSettingsService' @@ -126,6 +126,10 @@ import { AlertService, DesktopDeviceInterface, ChangeVaultStorageMode, + ChangeAndSaveItem, + FullyResolvedApplicationOptions, + GetHost, + SetHost, } from '@standardnotes/services' import { ItemManager } from '../../Services/Items/ItemManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager' @@ -151,10 +155,8 @@ import { WebSocketApiService, WebSocketServer, } from '@standardnotes/api' -import { FullyResolvedApplicationOptions } from '../Options/ApplicationOptions' import { TYPES } from './Types' -import { isDeinitable } from './isDeinitable' -import { Logger, isNotUndefined } from '@standardnotes/utils' +import { Logger, isNotUndefined, isDeinitable } from '@standardnotes/utils' import { EncryptionOperators } from '@standardnotes/encryption' import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' @@ -219,6 +221,14 @@ export class Dependencies { ) }) + this.factory.set(TYPES.GetHost, () => { + return new GetHost(this.get(TYPES.LegacyApiService)) + }) + + this.factory.set(TYPES.SetHost, () => { + return new SetHost(this.get(TYPES.HttpService), this.get(TYPES.LegacyApiService)) + }) + this.factory.set(TYPES.GetKeyPairs, () => { return new GetKeyPairs(this.get(TYPES.RootKeyManager)) }) @@ -307,6 +317,14 @@ export class Dependencies { return new GetVaults(this.get(TYPES.ItemManager)) }) + this.factory.set(TYPES.ChangeAndSaveItem, () => { + return new ChangeAndSaveItem( + this.get(TYPES.ItemManager), + this.get(TYPES.MutatorService), + this.get(TYPES.SyncService), + ) + }) + this.factory.set(TYPES.GetSharedVaults, () => { return new GetSharedVaults(this.get(TYPES.GetVaults)) }) @@ -1080,7 +1098,7 @@ export class Dependencies { }) this.factory.set(TYPES.MfaService, () => { - return new SNMfaService( + return new MfaService( this.get(TYPES.SettingsService), this.get(TYPES.Crypto), this.get(TYPES.FeaturesService), diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 073a12c82..e01402355 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -156,6 +156,9 @@ export const TYPES = { DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'), GetKeyPairs: Symbol.for('GetKeyPairs'), ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'), + ChangeAndSaveItem: Symbol.for('ChangeAndSaveItem'), + GetHost: Symbol.for('GetHost'), + SetHost: Symbol.for('SetHost'), // Mappers SessionStorageMapper: Symbol.for('SessionStorageMapper'), diff --git a/packages/snjs/lib/Application/LiveItem.ts b/packages/snjs/lib/Application/LiveItem.ts index e159d685f..9edc880f3 100644 --- a/packages/snjs/lib/Application/LiveItem.ts +++ b/packages/snjs/lib/Application/LiveItem.ts @@ -1,17 +1,17 @@ import { DecryptedItemInterface } from '@standardnotes/models' -import { ApplicationInterface } from '@standardnotes/services' +import { ItemManagerInterface } from '@standardnotes/services' /** Keeps an item reference up to date with changes */ export class LiveItem { public item: T private removeObserver: () => void - constructor(uuid: string, application: ApplicationInterface, onChange?: (item: T) => void) { - this.item = application.items.findSureItem(uuid) + constructor(uuid: string, items: ItemManagerInterface, onChange?: (item: T) => void) { + this.item = items.findSureItem(uuid) onChange && onChange(this.item) - this.removeObserver = application.streamItems(this.item.content_type, ({ changed, inserted }) => { + this.removeObserver = items.streamItems(this.item.content_type, ({ changed, inserted }) => { const matchingItem = [...changed, ...inserted].find((item) => { return item.uuid === uuid }) diff --git a/packages/snjs/lib/Application/index.ts b/packages/snjs/lib/Application/index.ts index ea875adf3..29e6e3594 100644 --- a/packages/snjs/lib/Application/index.ts +++ b/packages/snjs/lib/Application/index.ts @@ -2,4 +2,3 @@ export * from './Application' export * from './Event' export * from './LiveItem' export * from './Platforms' -export * from './Options/Defaults' diff --git a/packages/snjs/lib/Services/Challenge/ChallengeService.ts b/packages/snjs/lib/Services/Challenge/ChallengeService.ts index eb2828712..8607f3d94 100644 --- a/packages/snjs/lib/Services/Challenge/ChallengeService.ts +++ b/packages/snjs/lib/Services/Challenge/ChallengeService.ts @@ -1,7 +1,7 @@ import { RootKeyInterface } from '@standardnotes/models' import { DiskStorageService } from '../Storage/DiskStorageService' import { removeFromArray } from '@standardnotes/utils' -import { isValidProtectionSessionLength } from '../Protection/ProtectionService' +import { isValidProtectionSessionLength } from '../Protection/isValidProtectionSessionLength' import { AbstractService, ChallengeServiceInterface, @@ -158,10 +158,6 @@ export class ChallengeService extends AbstractService implements ChallengeServic return { wrappingKey } } - public isPasscodeLocked(): Promise { - return this.encryptionService.isPasscodeLocked() - } - public addChallengeObserver(challenge: Challenge, observer: ChallengeObserver): () => void { const observers = this.challengeObservers[challenge.id] || [] diff --git a/packages/snjs/lib/Services/ComponentManager/UseCase/GetFeatureUrl.spec.ts b/packages/snjs/lib/Services/ComponentManager/UseCase/GetFeatureUrl.spec.ts index bace33193..b5321a2db 100644 --- a/packages/snjs/lib/Services/ComponentManager/UseCase/GetFeatureUrl.spec.ts +++ b/packages/snjs/lib/Services/ComponentManager/UseCase/GetFeatureUrl.spec.ts @@ -58,7 +58,7 @@ describe('GetFeatureUrl', () => { }) describe('desktop', () => { - let desktopManager: DesktopManagerInterface | undefined + let desktopManager: jest.Mocked beforeEach(() => { desktopManager = { @@ -69,7 +69,7 @@ describe('GetFeatureUrl', () => { getExtServerHost() { return desktopExtHost }, - } + } as unknown as jest.Mocked usecase = new GetFeatureUrl(desktopManager, Environment.Desktop, Platform.MacDesktop) }) diff --git a/packages/snjs/lib/Services/Items/ItemManager.ts b/packages/snjs/lib/Services/Items/ItemManager.ts index ed309f3a0..8f7a76e7a 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.ts @@ -28,6 +28,7 @@ export class ItemManager extends Services.AbstractService implements Services.It private collection!: Models.ItemCollection private systemSmartViews: Models.SmartView[] private itemCounter!: Models.ItemCounter + private streamDisposers: (() => void)[] = [] private navigationDisplayController!: Models.ItemDisplayController< Models.SNNote | Models.FileItem, @@ -230,6 +231,7 @@ export class ItemManager extends Services.AbstractService implements Services.It public override deinit(): void { this.unsubChangeObserver() + this.streamDisposers.length = 0 ;(this.unsubChangeObserver as unknown) = undefined ;(this.payloadManager as unknown) = undefined ;(this.collection as unknown) = undefined @@ -865,4 +867,34 @@ export class ItemManager extends Services.AbstractService implements Services.It getNoteLinkedFiles(note: Models.SNNote): Models.FileItem[] { return this.itemsReferencingItem(note).filter(Models.isFile) } + + /** + * Begin streaming items to display in the UI. The stream callback will be called + * immediately with the present items that match the constraint, and over time whenever + * items matching the constraint are added, changed, or deleted. + */ + public streamItems( + contentType: string | string[], + stream: Models.ItemStream, + ): () => void { + const removeItemManagerObserver = this.addObserver(contentType, ({ changed, inserted, removed, source }) => { + stream({ changed, inserted, removed, source }) + }) + + const matches = this.getItems(contentType) + stream({ + inserted: matches, + changed: [], + removed: [], + source: Models.PayloadEmitSource.InitialObserverRegistrationPush, + }) + + this.streamDisposers.push(removeItemManagerObserver) + + return () => { + removeItemManagerObserver() + + removeFromArray(this.streamDisposers, removeItemManagerObserver) + } + } } diff --git a/packages/snjs/lib/Services/Mfa/MfaService.ts b/packages/snjs/lib/Services/Mfa/MfaService.ts index aded3da96..48c1204d1 100644 --- a/packages/snjs/lib/Services/Mfa/MfaService.ts +++ b/packages/snjs/lib/Services/Mfa/MfaService.ts @@ -3,9 +3,9 @@ import { SettingName } from '@standardnotes/settings' import { SettingsService } from '../Settings' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { FeaturesService } from '../Features/FeaturesService' -import { AbstractService, InternalEventBusInterface, SignInStrings } from '@standardnotes/services' +import { AbstractService, InternalEventBusInterface, MfaServiceInterface, SignInStrings } from '@standardnotes/services' -export class SNMfaService extends AbstractService { +export class MfaService extends AbstractService implements MfaServiceInterface { constructor( private settingsService: SettingsService, private crypto: PureCryptoInterface, diff --git a/packages/snjs/lib/Services/Protection/ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction.ts b/packages/snjs/lib/Services/Protection/ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction.ts new file mode 100644 index 000000000..1ffbf3beb --- /dev/null +++ b/packages/snjs/lib/Services/Protection/ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction.ts @@ -0,0 +1 @@ +export const ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction = 30 diff --git a/packages/snjs/lib/Services/Protection/ProtectionService.ts b/packages/snjs/lib/Services/Protection/ProtectionService.ts index 167fc7a7b..0f90920b1 100644 --- a/packages/snjs/lib/Services/Protection/ProtectionService.ts +++ b/packages/snjs/lib/Services/Protection/ProtectionService.ts @@ -29,45 +29,11 @@ import { InternalEventInterface, ApplicationEvent, ApplicationStageChangedEventPayload, + ProtectionEvent, } from '@standardnotes/services' import { ContentType } from '@standardnotes/domain-core' - -export enum ProtectionEvent { - UnprotectedSessionBegan = 'UnprotectedSessionBegan', - UnprotectedSessionExpired = 'UnprotectedSessionExpired', -} - -export const ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction = 30 - -export enum UnprotectedAccessSecondsDuration { - OneMinute = 60, - FiveMinutes = 300, - OneHour = 3600, - OneWeek = 604800, -} - -export function isValidProtectionSessionLength(number: unknown): boolean { - return typeof number === 'number' && Object.values(UnprotectedAccessSecondsDuration).includes(number) -} - -export const ProtectionSessionDurations = [ - { - valueInSeconds: UnprotectedAccessSecondsDuration.OneMinute, - label: '1 Minute', - }, - { - valueInSeconds: UnprotectedAccessSecondsDuration.FiveMinutes, - label: '5 Minutes', - }, - { - valueInSeconds: UnprotectedAccessSecondsDuration.OneHour, - label: '1 Hour', - }, - { - valueInSeconds: UnprotectedAccessSecondsDuration.OneWeek, - label: '1 Week', - }, -] +import { isValidProtectionSessionLength } from './isValidProtectionSessionLength' +import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration' /** * Enforces certain actions to require extra authentication, @@ -82,11 +48,14 @@ export class ProtectionService private mobilePasscodeTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.OnQuit private mobileBiometricsTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.OnQuit + private isBiometricsSoftLockEngaged = false + private applicationStarted = false + constructor( - private encryptionService: EncryptionService, + private encryption: EncryptionService, private mutator: MutatorClientInterface, - private challengeService: ChallengeService, - private storageService: DiskStorageService, + private challenges: ChallengeService, + private storage: DiskStorageService, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -94,9 +63,9 @@ export class ProtectionService public override deinit(): void { clearTimeout(this.sessionExpiryTimeout) - ;(this.encryptionService as unknown) = undefined - ;(this.challengeService as unknown) = undefined - ;(this.storageService as unknown) = undefined + ;(this.encryption as unknown) = undefined + ;(this.challenges as unknown) = undefined + ;(this.storage as unknown) = undefined super.deinit() } @@ -108,11 +77,42 @@ export class ProtectionService this.mobilePasscodeTiming = this.getMobilePasscodeTiming() this.mobileBiometricsTiming = this.getMobileBiometricsTiming() } + } else if (event.type === ApplicationEvent.Started) { + this.applicationStarted = true } } + async isLocked(): Promise { + if (!this.applicationStarted) { + return true + } + + const isPasscodeLocked = await this.encryption.isPasscodeLocked() + return isPasscodeLocked || this.isBiometricsSoftLockEngaged + } + + public softLockBiometrics(): void { + const challenge = new Challenge( + [new ChallengePrompt(ChallengeValidation.Biometric)], + ChallengeReason.ApplicationUnlock, + false, + ) + + void this.challenges.promptForChallengeResponse(challenge) + + this.isBiometricsSoftLockEngaged = true + void this.notifyEvent(ProtectionEvent.BiometricsSoftLockEngaged) + + this.challenges.addChallengeObserver(challenge, { + onComplete: () => { + this.isBiometricsSoftLockEngaged = false + void this.notifyEvent(ProtectionEvent.BiometricsSoftLockDisengaged) + }, + }) + } + public hasProtectionSources(): boolean { - return this.encryptionService.hasAccount() || this.encryptionService.hasPasscode() || this.hasBiometricsEnabled() + return this.encryption.hasAccount() || this.encryption.hasPasscode() || this.hasBiometricsEnabled() } public hasUnprotectedAccessSession(): boolean { @@ -123,7 +123,7 @@ export class ProtectionService } public hasBiometricsEnabled(): boolean { - const biometricsState = this.storageService.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped) + const biometricsState = this.storage.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped) return Boolean(biometricsState) } @@ -133,7 +133,7 @@ export class ProtectionService return false } - this.storageService.setValue(StorageKey.BiometricsState, true, StorageValueModes.Nonwrapped) + this.storage.setValue(StorageKey.BiometricsState, true, StorageValueModes.Nonwrapped) return true } @@ -145,7 +145,7 @@ export class ProtectionService } if (await this.validateOrRenewSession(ChallengeReason.DisableBiometrics)) { - this.storageService.setValue(StorageKey.BiometricsState, false, StorageValueModes.Nonwrapped) + this.storage.setValue(StorageKey.BiometricsState, false, StorageValueModes.Nonwrapped) return true } else { return false @@ -157,7 +157,7 @@ export class ProtectionService if (this.hasBiometricsEnabled()) { prompts.push(new ChallengePrompt(ChallengeValidation.Biometric)) } - if (this.encryptionService.hasPasscode()) { + if (this.encryption.hasPasscode()) { prompts.push(new ChallengePrompt(ChallengeValidation.LocalPasscode)) } if (prompts.length > 0) { @@ -316,7 +316,7 @@ export class ProtectionService } getMobileBiometricsTiming(): MobileUnlockTiming | undefined { - return this.storageService.getValue( + return this.storage.getValue( StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped, MobileUnlockTiming.OnQuit, @@ -324,7 +324,7 @@ export class ProtectionService } getMobilePasscodeTiming(): MobileUnlockTiming | undefined { - return this.storageService.getValue( + return this.storage.getValue( StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped, MobileUnlockTiming.OnQuit, @@ -332,21 +332,21 @@ export class ProtectionService } setMobileBiometricsTiming(timing: MobileUnlockTiming): void { - this.storageService.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped) + this.storage.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped) this.mobileBiometricsTiming = timing } setMobilePasscodeTiming(timing: MobileUnlockTiming): void { - this.storageService.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped) + this.storage.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped) this.mobilePasscodeTiming = timing } setMobileScreenshotPrivacyEnabled(isEnabled: boolean) { - return this.storageService.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default) + return this.storage.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default) } getMobileScreenshotPrivacyEnabled(): boolean { - return this.storageService.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default, false) + return this.storage.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default, false) } private async validateOrRenewSession( @@ -363,19 +363,19 @@ export class ProtectionService prompts.push(new ChallengePrompt(ChallengeValidation.Biometric)) } - if (this.encryptionService.hasPasscode()) { + if (this.encryption.hasPasscode()) { prompts.push(new ChallengePrompt(ChallengeValidation.LocalPasscode)) } if (requireAccountPassword) { - if (!this.encryptionService.hasAccount()) { + if (!this.encryption.hasAccount()) { throw Error('Requiring account password for challenge with no account') } prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword)) } if (prompts.length === 0) { - if (fallBackToAccountPassword && this.encryptionService.hasAccount()) { + if (fallBackToAccountPassword && this.encryption.hasAccount()) { prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword)) } else { return true @@ -396,7 +396,7 @@ export class ProtectionService ), ) - const response = await this.challengeService.promptForChallengeResponse(new Challenge(prompts, reason, true)) + const response = await this.challenges.promptForChallengeResponse(new Challenge(prompts, reason, true)) if (response) { const length = response.values.find( @@ -414,7 +414,7 @@ export class ProtectionService } public getSessionExpiryDate(): Date { - const expiresAt = this.storageService.getValue(StorageKey.ProtectionExpirey) + const expiresAt = this.storage.getValue(StorageKey.ProtectionExpirey) if (expiresAt) { return new Date(expiresAt) } else { @@ -428,15 +428,15 @@ export class ProtectionService } private setSessionExpiryDate(date: Date) { - this.storageService.setValue(StorageKey.ProtectionExpirey, date) + this.storage.setValue(StorageKey.ProtectionExpirey, date) } private getLastSessionLength(): UnprotectedAccessSecondsDuration | undefined { - return this.storageService.getValue(StorageKey.ProtectionSessionLength) + return this.storage.getValue(StorageKey.ProtectionSessionLength) } private setSessionLength(length: UnprotectedAccessSecondsDuration): void { - this.storageService.setValue(StorageKey.ProtectionSessionLength, length) + this.storage.setValue(StorageKey.ProtectionSessionLength, length) const expiresAt = new Date() expiresAt.setSeconds(expiresAt.getSeconds() + length) this.setSessionExpiryDate(expiresAt) diff --git a/packages/snjs/lib/Services/Protection/ProtectionSessionDurations.ts b/packages/snjs/lib/Services/Protection/ProtectionSessionDurations.ts new file mode 100644 index 000000000..fd70e0e9b --- /dev/null +++ b/packages/snjs/lib/Services/Protection/ProtectionSessionDurations.ts @@ -0,0 +1,20 @@ +import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration' + +export const ProtectionSessionDurations = [ + { + valueInSeconds: UnprotectedAccessSecondsDuration.OneMinute, + label: '1 Minute', + }, + { + valueInSeconds: UnprotectedAccessSecondsDuration.FiveMinutes, + label: '5 Minutes', + }, + { + valueInSeconds: UnprotectedAccessSecondsDuration.OneHour, + label: '1 Hour', + }, + { + valueInSeconds: UnprotectedAccessSecondsDuration.OneWeek, + label: '1 Week', + }, +] diff --git a/packages/snjs/lib/Services/Protection/UnprotectedAccessSecondsDuration.ts b/packages/snjs/lib/Services/Protection/UnprotectedAccessSecondsDuration.ts new file mode 100644 index 000000000..469b02866 --- /dev/null +++ b/packages/snjs/lib/Services/Protection/UnprotectedAccessSecondsDuration.ts @@ -0,0 +1,6 @@ +export enum UnprotectedAccessSecondsDuration { + OneMinute = 60, + FiveMinutes = 300, + OneHour = 3600, + OneWeek = 604800, +} diff --git a/packages/snjs/lib/Services/Protection/index.ts b/packages/snjs/lib/Services/Protection/index.ts index b43e8b430..aec477432 100644 --- a/packages/snjs/lib/Services/Protection/index.ts +++ b/packages/snjs/lib/Services/Protection/index.ts @@ -1 +1,5 @@ export * from './ProtectionService' +export * from './ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction' +export * from './ProtectionSessionDurations' +export * from './UnprotectedAccessSecondsDuration' +export * from './isValidProtectionSessionLength' diff --git a/packages/snjs/lib/Services/Protection/isValidProtectionSessionLength.ts b/packages/snjs/lib/Services/Protection/isValidProtectionSessionLength.ts new file mode 100644 index 000000000..3008e3b14 --- /dev/null +++ b/packages/snjs/lib/Services/Protection/isValidProtectionSessionLength.ts @@ -0,0 +1,5 @@ +import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration' + +export function isValidProtectionSessionLength(number: unknown): boolean { + return typeof number === 'number' && Object.values(UnprotectedAccessSecondsDuration).includes(number) +} diff --git a/packages/snjs/lib/Services/Session/SessionManager.ts b/packages/snjs/lib/Services/Session/SessionManager.ts index 51bd8e692..bacca08d9 100644 --- a/packages/snjs/lib/Services/Session/SessionManager.ts +++ b/packages/snjs/lib/Services/Session/SessionManager.ts @@ -264,10 +264,15 @@ export class SessionManager } } + /** Unlike EncryptionService.hasAccount, isSignedIn can only be read once the application is unlocked */ public isSignedIn(): boolean { return this.getUser() != undefined } + public isSignedOut(): boolean { + return !this.isSignedIn() + } + public isSignedIntoFirstPartyServer(): boolean { return this.isSignedIn() && !this.apiService.isThirdPartyHostUsed() } diff --git a/packages/snjs/lib/Services/Sync/SyncService.ts b/packages/snjs/lib/Services/Sync/SyncService.ts index 5f91d85aa..bafe76072 100644 --- a/packages/snjs/lib/Services/Sync/SyncService.ts +++ b/packages/snjs/lib/Services/Sync/SyncService.ts @@ -83,6 +83,7 @@ import { SyncEventReceivedNotificationsData, SyncEventReceivedAsymmetricMessagesData, SyncOpStatus, + ApplicationSyncOptions, } from '@standardnotes/services' import { OfflineSyncResponse } from './Offline/Response' import { @@ -92,7 +93,6 @@ import { SplitPayloadsByEncryptionType, } from '@standardnotes/encryption' import { CreatePayloadFromRawServerItem } from './Account/Utilities' -import { ApplicationSyncOptions } from '@Lib/Application/Options/OptionalOptions' import { DecryptedServerConflictMap, TrustedServerConflictMap } from './Account/ServerConflictMap' import { ContentType } from '@standardnotes/domain-core' diff --git a/packages/snjs/mocha/application.test.js b/packages/snjs/mocha/application.test.js index a41a8837e..013c48242 100644 --- a/packages/snjs/mocha/application.test.js +++ b/packages/snjs/mocha/application.test.js @@ -79,8 +79,8 @@ describe('application instances', () => { }) await recreatedContext.launch() - expect(recreatedContext.application.getHost()).to.not.equal('http://nonsense.host') - expect(recreatedContext.application.getHost()).to.equal(Factory.getDefaultHost()) + expect(recreatedContext.application.getHost.execute().getValue()).to.not.equal('http://nonsense.host') + expect(recreatedContext.application.getHost.execute().getValue()).to.equal(Factory.getDefaultHost()) await recreatedContext.deinit() }) diff --git a/packages/snjs/mocha/auth-fringe-cases.test.js b/packages/snjs/mocha/auth-fringe-cases.test.js index 4de8801d2..aa8039ce2 100644 --- a/packages/snjs/mocha/auth-fringe-cases.test.js +++ b/packages/snjs/mocha/auth-fringe-cases.test.js @@ -85,7 +85,7 @@ describe('auth fringe cases', () => { const serverText = 'server text' - await context.application.changeAndSaveItem(firstVersionOfNote, (mutator) => { + await context.application.changeAndSaveItem.execute(firstVersionOfNote, (mutator) => { mutator.text = serverText }) diff --git a/packages/snjs/mocha/features.test.js b/packages/snjs/mocha/features.test.js index fba97f477..334315cb0 100644 --- a/packages/snjs/mocha/features.test.js +++ b/packages/snjs/mocha/features.test.js @@ -141,7 +141,7 @@ describe('features', () => { expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false) const extensionKey = UuidGenerator.GenerateUuid().split('-').join('') const promise = new Promise((resolve) => { - application.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => { + application.items.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => { for (const item of changed) { if (item.content.migratedToUserSetting) { resolve() diff --git a/packages/snjs/mocha/history.test.js b/packages/snjs/mocha/history.test.js index 08b515c74..421a7b54b 100644 --- a/packages/snjs/mocha/history.test.js +++ b/packages/snjs/mocha/history.test.js @@ -34,8 +34,8 @@ describe('history manager', () => { await Factory.safeDeinit(this.application) }) - function setTextAndSync(application, item, text) { - return application.changeAndSaveItem( + async function setTextAndSync(application, item, text) { + const result = await application.changeAndSaveItem.execute( item, (mutator) => { mutator.text = text @@ -44,6 +44,8 @@ describe('history manager', () => { undefined, syncOptions, ) + + return result.getValue() } function deleteCharsFromString(string, amount) { @@ -59,7 +61,7 @@ describe('history manager', () => { expect(this.history.sessionHistoryForItem(item).length).to.equal(0) /** Sync with different contents, should create new entry */ - await this.application.changeAndSaveItem( + await this.application.changeAndSaveItem.execute( item, (mutator) => { mutator.title = Math.random() @@ -79,7 +81,7 @@ describe('history manager', () => { const context = await Factory.createAppContext({ identifier }) await context.launch() expect(context.history.sessionHistoryForItem(item).length).to.equal(0) - await context.application.changeAndSaveItem( + await context.application.changeAndSaveItem.execute( item, (mutator) => { mutator.title = Math.random() @@ -103,7 +105,7 @@ describe('history manager', () => { await context.application.mutator.insertItem(item) expect(context.history.sessionHistoryForItem(item).length).to.equal(0) - await context.application.changeAndSaveItem( + await context.application.changeAndSaveItem.execute( item, (mutator) => { mutator.title = Math.random() @@ -243,7 +245,7 @@ describe('history manager', () => { const payload = Factory.createNotePayload() await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) const item = this.application.items.findItem(payload.uuid) - await this.application.changeAndSaveItem( + await this.application.changeAndSaveItem.execute( item, (mutator) => { mutator.title = Math.random() @@ -306,7 +308,7 @@ describe('history manager', () => { expect(itemHistory.length).to.equal(1) /** Sync with different contents, should not create a new entry */ - await this.application.changeAndSaveItem( + await this.application.changeAndSaveItem.execute( item, (mutator) => { mutator.title = Math.random() @@ -327,7 +329,7 @@ describe('history manager', () => { await Factory.sleep(Factory.ServerRevisionFrequency) /** Sync with different contents, should create new entry */ const newTitleAfterFirstChange = `The title should be: ${Math.random()}` - await this.application.changeAndSaveItem( + await this.application.changeAndSaveItem.execute( item, (mutator) => { mutator.title = newTitleAfterFirstChange @@ -411,7 +413,7 @@ describe('history manager', () => { await Factory.sleep(Factory.ServerRevisionFrequency) const changedText = `${Math.random()}` - await this.application.changeAndSaveItem(note, (mutator) => { + await this.application.changeAndSaveItem.execute(note, (mutator) => { mutator.title = changedText }) await Factory.markDirtyAndSyncItem(this.application, note) diff --git a/packages/snjs/mocha/model_tests/importing.test.js b/packages/snjs/mocha/model_tests/importing.test.js index b6398ff54..9a9a0a923 100644 --- a/packages/snjs/mocha/model_tests/importing.test.js +++ b/packages/snjs/mocha/model_tests/importing.test.js @@ -813,7 +813,7 @@ describe('importing', function () { }, }) await application.launch(false) - await application.setHost(Factory.getDefaultHost()) + await application.setHost.execute(Factory.getDefaultHost()) const backupFile = { items: [ diff --git a/packages/snjs/mocha/model_tests/items.test.js b/packages/snjs/mocha/model_tests/items.test.js index 81eeb723b..a466887ed 100644 --- a/packages/snjs/mocha/model_tests/items.test.js +++ b/packages/snjs/mocha/model_tests/items.test.js @@ -50,17 +50,19 @@ describe('items', () => { const item = this.application.items.items[0] expect(item.pinned).to.not.be.ok - const refreshedItem = await this.application.changeAndSaveItem( - item, - (mutator) => { - mutator.pinned = true - mutator.archived = true - mutator.locked = true - }, - undefined, - undefined, - syncOptions, - ) + const refreshedItem = ( + await this.application.changeAndSaveItem.execute( + item, + (mutator) => { + mutator.pinned = true + mutator.archived = true + mutator.locked = true + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(refreshedItem.pinned).to.equal(true) expect(refreshedItem.archived).to.equal(true) expect(refreshedItem.locked).to.equal(true) @@ -77,94 +79,110 @@ describe('items', () => { expect(item1.isItemContentEqualWith(item2)).to.equal(true) // items should ignore this field when checking for equality - item1 = await this.application.changeAndSaveItem( - item1, - (mutator) => { - mutator.userModifiedDate = new Date() - }, - undefined, - undefined, - syncOptions, - ) - item2 = await this.application.changeAndSaveItem( - item2, - (mutator) => { - mutator.userModifiedDate = undefined - }, - undefined, - undefined, - syncOptions, - ) + item1 = ( + await this.application.changeAndSaveItem.execute( + item1, + (mutator) => { + mutator.userModifiedDate = new Date() + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() + item2 = ( + await this.application.changeAndSaveItem.execute( + item2, + (mutator) => { + mutator.userModifiedDate = undefined + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(item1.isItemContentEqualWith(item2)).to.equal(true) - item1 = await this.application.changeAndSaveItem( - item1, - (mutator) => { - mutator.mutableContent.foo = 'bar' - }, - undefined, - undefined, - syncOptions, - ) + item1 = ( + await this.application.changeAndSaveItem.execute( + item1, + (mutator) => { + mutator.mutableContent.foo = 'bar' + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(item1.isItemContentEqualWith(item2)).to.equal(false) - item2 = await this.application.changeAndSaveItem( - item2, - (mutator) => { - mutator.mutableContent.foo = 'bar' - }, - undefined, - undefined, - syncOptions, - ) + item2 = ( + await this.application.changeAndSaveItem.execute( + item2, + (mutator) => { + mutator.mutableContent.foo = 'bar' + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(item1.isItemContentEqualWith(item2)).to.equal(true) expect(item2.isItemContentEqualWith(item1)).to.equal(true) - item1 = await this.application.changeAndSaveItem( - item1, - (mutator) => { - mutator.e2ePendingRefactor_addItemAsRelationship(item2) - }, - undefined, - undefined, - syncOptions, - ) - item2 = await this.application.changeAndSaveItem( - item2, - (mutator) => { - mutator.e2ePendingRefactor_addItemAsRelationship(item1) - }, - undefined, - undefined, - syncOptions, - ) + item1 = ( + await this.application.changeAndSaveItem.execute( + item1, + (mutator) => { + mutator.e2ePendingRefactor_addItemAsRelationship(item2) + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() + item2 = ( + await this.application.changeAndSaveItem.execute( + item2, + (mutator) => { + mutator.e2ePendingRefactor_addItemAsRelationship(item1) + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(item1.content.references.length).to.equal(1) expect(item2.content.references.length).to.equal(1) expect(item1.isItemContentEqualWith(item2)).to.equal(false) - item1 = await this.application.changeAndSaveItem( - item1, - (mutator) => { - mutator.removeItemAsRelationship(item2) - }, - undefined, - undefined, - syncOptions, - ) - item2 = await this.application.changeAndSaveItem( - item2, - (mutator) => { - mutator.removeItemAsRelationship(item1) - }, - undefined, - undefined, - syncOptions, - ) + item1 = ( + await this.application.changeAndSaveItem.execute( + item1, + (mutator) => { + mutator.removeItemAsRelationship(item2) + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() + item2 = ( + await this.application.changeAndSaveItem.execute( + item2, + (mutator) => { + mutator.removeItemAsRelationship(item1) + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(item1.isItemContentEqualWith(item2)).to.equal(true) expect(item1.content.references.length).to.equal(0) @@ -179,15 +197,17 @@ describe('items', () => { let item1 = this.application.items.getDisplayableNotes()[0] const item2 = this.application.items.getDisplayableNotes()[1] - item1 = await this.application.changeAndSaveItem( - item1, - (mutator) => { - mutator.mutableContent.foo = 'bar' - }, - undefined, - undefined, - syncOptions, - ) + item1 = ( + await this.application.changeAndSaveItem.execute( + item1, + (mutator) => { + mutator.mutableContent.foo = 'bar' + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(item1.content.foo).to.equal('bar') diff --git a/packages/snjs/mocha/model_tests/notes_tags.test.js b/packages/snjs/mocha/model_tests/notes_tags.test.js index a86504fea..ad0d2fa06 100644 --- a/packages/snjs/mocha/model_tests/notes_tags.test.js +++ b/packages/snjs/mocha/model_tests/notes_tags.test.js @@ -184,15 +184,17 @@ describe('notes and tags', () => { expect(note.content.references.length).to.equal(0) expect(tag.content.references.length).to.equal(1) - tag = await this.application.changeAndSaveItem( - tag, - (mutator) => { - mutator.removeItemAsRelationship(note) - }, - undefined, - undefined, - syncOptions, - ) + tag = ( + await this.application.changeAndSaveItem.execute( + tag, + (mutator) => { + mutator.removeItemAsRelationship(note) + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(this.application.items.itemsReferencingItem(note).length).to.equal(0) expect(tag.noteCount).to.equal(0) @@ -265,15 +267,17 @@ describe('notes and tags', () => { const notePayload = Factory.createNotePayload() await this.application.mutator.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged) let note = this.application.items.getItems([ContentType.TYPES.Note])[0] - note = await this.application.changeAndSaveItem( - note, - (mutator) => { - mutator.mutableContent.title = Math.random() - }, - undefined, - undefined, - syncOptions, - ) + note = ( + await this.application.changeAndSaveItem.execute( + note, + (mutator) => { + mutator.mutableContent.title = Math.random() + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() expect(note.content.title).to.not.equal(notePayload.content.title) }) diff --git a/packages/snjs/mocha/protection.test.js b/packages/snjs/mocha/protection.test.js index 74e1f3c2f..0ea3892ac 100644 --- a/packages/snjs/mocha/protection.test.js +++ b/packages/snjs/mocha/protection.test.js @@ -382,19 +382,19 @@ describe('protections', function () { this.foo = 'tar' application = await Factory.createInitAppWithFakeCrypto() await application.addPasscode('passcode') - expect(application.hasUnprotectedAccessSession()).to.be.false + expect(application.protections.hasUnprotectedAccessSession()).to.be.false }) it('should return true when session length has been set', async function () { application = await Factory.createInitAppWithFakeCrypto() await application.addPasscode('passcode') await application.protections.setSessionLength(UnprotectedAccessSecondsDuration.OneMinute) - expect(application.hasUnprotectedAccessSession()).to.be.true + expect(application.protections.hasUnprotectedAccessSession()).to.be.true }) it('should return true when there are no protection sources', async function () { application = await Factory.createInitAppWithFakeCrypto() - expect(application.hasUnprotectedAccessSession()).to.be.true + expect(application.protections.hasUnprotectedAccessSession()).to.be.true }) }) diff --git a/packages/snjs/mocha/sync_tests/conflicting.test.js b/packages/snjs/mocha/sync_tests/conflicting.test.js index 05b93c202..d25211693 100644 --- a/packages/snjs/mocha/sync_tests/conflicting.test.js +++ b/packages/snjs/mocha/sync_tests/conflicting.test.js @@ -298,7 +298,7 @@ describe('online conflict handling', function () { await this.application.mutator.setItemDirty(note) this.expectedItemCount++ - await this.application.changeAndSaveItem( + await this.application.changeAndSaveItem.execute( note, (mutator) => { // client A @@ -332,7 +332,7 @@ describe('online conflict handling', function () { await this.application.mutator.setItemDirty(note) this.expectedItemCount++ - await this.application.changeAndSaveItem( + await this.application.changeAndSaveItem.execute( note, (mutator) => { // client A @@ -602,15 +602,17 @@ describe('online conflict handling', function () { */ let tag = await Factory.createMappedTag(this.application) let note = await Factory.createMappedNote(this.application) - tag = await this.application.changeAndSaveItem( - tag, - (mutator) => { - mutator.e2ePendingRefactor_addItemAsRelationship(note) - }, - undefined, - undefined, - syncOptions, - ) + tag = ( + await this.application.changeAndSaveItem.execute( + tag, + (mutator) => { + mutator.e2ePendingRefactor_addItemAsRelationship(note) + }, + undefined, + undefined, + syncOptions, + ) + ).getValue() await this.application.mutator.setItemDirty(note) this.expectedItemCount += 2 @@ -732,39 +734,42 @@ describe('online conflict handling', function () { }) /** This test takes too long on Docker CI */ - it.skip('registering for account with bulk offline data belonging to another account should be error-free', async function () { - /** - * When performing a multi-page sync request where we are uploading data imported from a backup, - * if the first page of the sync request returns conflicted items keys, we rotate their UUID. - * The second page of sync waiting to be sent up is still encrypted with the old items key UUID. - * This causes a problem because when that second page is returned as conflicts, we will be looking - * for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire - * sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead - * take the approach of making sure the decryption function is liberal with regards to searching - * for the right items key. It will now consider (as a result of this test) an items key as being - * the correct key to decrypt an item if the itemskey.uuid == item.items_key_id OR if the itemsKey.duplicateOf - * value is equal to item.items_key_id. - */ + it.skip( + 'registering for account with bulk offline data belonging to another account should be error-free', + async function () { + /** + * When performing a multi-page sync request where we are uploading data imported from a backup, + * if the first page of the sync request returns conflicted items keys, we rotate their UUID. + * The second page of sync waiting to be sent up is still encrypted with the old items key UUID. + * This causes a problem because when that second page is returned as conflicts, we will be looking + * for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire + * sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead + * take the approach of making sure the decryption function is liberal with regards to searching + * for the right items key. It will now consider (as a result of this test) an items key as being + * the correct key to decrypt an item if the itemskey.uuid == item.items_key_id OR if the itemsKey.duplicateOf + * value is equal to item.items_key_id. + */ - /** Create bulk data belonging to another account and sync */ - const largeItemCount = SyncUpDownLimit + 10 - await Factory.createManyMappedNotes(this.application, largeItemCount) - await this.application.sync.sync(syncOptions) - const priorData = this.application.items.items + /** Create bulk data belonging to another account and sync */ + const largeItemCount = SyncUpDownLimit + 10 + await Factory.createManyMappedNotes(this.application, largeItemCount) + await this.application.sync.sync(syncOptions) + const priorData = this.application.items.items - /** Register new account and import this same data */ - const newApp = await Factory.signOutApplicationAndReturnNew(this.application) - await Factory.registerUserToApplication({ - application: newApp, - email: Utils.generateUuid(), - password: Utils.generateUuid(), - }) - await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload)) - await newApp.sync.markAllItemsAsNeedingSyncAndPersist() - await newApp.sync.sync(syncOptions) - expect(newApp.payloads.invalidPayloads.length).to.equal(0) - await Factory.safeDeinit(newApp) - }).timeout(80000) + /** Register new account and import this same data */ + const newApp = await Factory.signOutApplicationAndReturnNew(this.application) + await Factory.registerUserToApplication({ + application: newApp, + email: Utils.generateUuid(), + password: Utils.generateUuid(), + }) + await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload)) + await newApp.sync.markAllItemsAsNeedingSyncAndPersist() + await newApp.sync.sync(syncOptions) + expect(newApp.payloads.invalidPayloads.length).to.equal(0) + await Factory.safeDeinit(newApp) + }, + ).timeout(80000) it('importing data belonging to another account should not result in duplication', async function () { /** Create primary account and export data */ @@ -801,7 +806,7 @@ describe('online conflict handling', function () { await createSyncedNoteWithTag(this.application) const tag = this.application.items.getDisplayableTags()[0] const note2 = await Factory.createMappedNote(this.application) - await this.application.changeAndSaveItem(tag, (mutator) => { + await this.application.changeAndSaveItem.execute(tag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(note2) }) let backupFile = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() diff --git a/packages/snjs/mocha/sync_tests/offline.test.js b/packages/snjs/mocha/sync_tests/offline.test.js index e097d8d28..6456f13a5 100644 --- a/packages/snjs/mocha/sync_tests/offline.test.js +++ b/packages/snjs/mocha/sync_tests/offline.test.js @@ -98,7 +98,7 @@ describe('offline syncing', () => { this.expectedItemCount++ await this.application.sync.sync(syncOptions) this.application = await Factory.signOutApplicationAndReturnNew(this.application) - expect(this.application.noAccount()).to.equal(true) - expect(this.application.getUser()).to.not.be.ok + expect(this.application.sessions.isSignedIn()).to.equal(false) + expect(this.application.sessions.getUser()).to.not.be.ok }) }) diff --git a/packages/snjs/mocha/sync_tests/online.test.js b/packages/snjs/mocha/sync_tests/online.test.js index 63dc27ed9..ff7d8220b 100644 --- a/packages/snjs/mocha/sync_tests/online.test.js +++ b/packages/snjs/mocha/sync_tests/online.test.js @@ -600,7 +600,7 @@ describe('online syncing', function () { it('saving an item after sync should persist it with content property', async function () { const note = await Factory.createMappedNote(this.application) const text = Factory.randomString(10000) - await this.application.changeAndSaveItem( + await this.application.changeAndSaveItem.execute( note, (mutator) => { mutator.text = text @@ -1015,7 +1015,7 @@ describe('online syncing', function () { it('deleting an item permanently should include it in PayloadEmitSource.PreSyncSave item change observer', async function () { let conditionMet = false - this.application.streamItems([ContentType.TYPES.Note], async ({ removed, source }) => { + this.application.items.streamItems([ContentType.TYPES.Note], async ({ removed, source }) => { if (source === PayloadEmitSource.PreSyncSave && removed.length === 1) { conditionMet = true } diff --git a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts index 71bd24448..d8c1a6d40 100644 --- a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts +++ b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts @@ -1,19 +1,13 @@ -import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter' import data from './testData' +import { UuidGenerator } from '@standardnotes/utils' + +UuidGenerator.SetGenerator(() => String(Math.random())) describe('AegisConverter', () => { - let application: WebApplicationInterface - - beforeEach(() => { - application = { - generateUUID: jest.fn().mockReturnValue('test'), - } as unknown as WebApplicationInterface - }) - it('should parse entries', () => { - const converter = new AegisToAuthenticatorConverter(application) + const converter = new AegisToAuthenticatorConverter() const result = converter.parseEntries(data) @@ -34,7 +28,7 @@ describe('AegisConverter', () => { }) it('should create note from entries with editor info', () => { - const converter = new AegisToAuthenticatorConverter(application) + const converter = new AegisToAuthenticatorConverter() const parsedEntries = converter.parseEntries(data) @@ -61,7 +55,7 @@ describe('AegisConverter', () => { }) it('should create note from entries without editor info', () => { - const converter = new AegisToAuthenticatorConverter(application) + const converter = new AegisToAuthenticatorConverter() const parsedEntries = converter.parseEntries(data) diff --git a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts index 009b08000..76c79e104 100644 --- a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts +++ b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts @@ -1,8 +1,8 @@ import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { readFileAsText } from '../Utils' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' -import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { ContentType } from '@standardnotes/domain-core' +import { UuidGenerator } from '@standardnotes/utils' type AegisData = { db: { @@ -27,9 +27,11 @@ type AuthenticatorEntry = { } export class AegisToAuthenticatorConverter { - constructor(protected application: WebApplicationInterface) {} + constructor() {} + // eslint-disable-next-line @typescript-eslint/no-explicit-any static isValidAegisJson(json: any): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type)) } @@ -61,7 +63,7 @@ export class AegisToAuthenticatorConverter { created_at_timestamp: file.lastModified, updated_at: new Date(file.lastModified), updated_at_timestamp: file.lastModified, - uuid: this.application.generateUUID(), + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.TYPES.Note, content: { title: file.name.split('.')[0], diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts index 6ab411f88..116be7b08 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts @@ -6,7 +6,7 @@ import { ContentType } from '@standardnotes/domain-core' import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models' import { EvernoteConverter } from './EvernoteConverter' import data from './testData' -import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' +import { UuidGenerator } from '@standardnotes/utils' // Mock dayjs so dayjs.extend() doesn't throw an error in EvernoteConverter.ts jest.mock('dayjs', () => { @@ -21,17 +21,11 @@ jest.mock('dayjs', () => { } }) +UuidGenerator.SetGenerator(() => String(Math.random())) + describe('EvernoteConverter', () => { - let application: WebApplicationInterface - - beforeEach(() => { - application = { - generateUUID: jest.fn().mockReturnValue(Math.random()), - } as any as WebApplicationInterface - }) - it('should parse and strip html', () => { - const converter = new EvernoteConverter(application) + const converter = new EvernoteConverter() const result = converter.parseENEXData(data, true) @@ -51,7 +45,7 @@ describe('EvernoteConverter', () => { }) it('should parse and not strip html', () => { - const converter = new EvernoteConverter(application) + const converter = new EvernoteConverter() const result = converter.parseENEXData(data, false) diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts index d9f092da1..0020529da 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts @@ -3,15 +3,15 @@ import { readFileAsText } from '../Utils' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' import utc from 'dayjs/plugin/utc' -import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { ContentType } from '@standardnotes/domain-core' +import { UuidGenerator } from '@standardnotes/utils' dayjs.extend(customParseFormat) dayjs.extend(utc) const dateFormat = 'YYYYMMDDTHHmmss' export class EvernoteConverter { - constructor(protected application: WebApplicationInterface) {} + constructor() {} async convertENEXFileToNotesAndTags(file: File, stripHTML: boolean): Promise { const content = await readFileAsText(file) @@ -35,7 +35,7 @@ export class EvernoteConverter { created_at_timestamp: now.getTime(), updated_at: now, updated_at_timestamp: now.getTime(), - uuid: this.application.generateUUID(), + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.TYPES.Tag, content: { title: defaultTagName, @@ -88,7 +88,7 @@ export class EvernoteConverter { created_at_timestamp: createdAtDate.getTime(), updated_at: updatedAtDate, updated_at_timestamp: updatedAtDate.getTime(), - uuid: this.application.generateUUID(), + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.TYPES.Note, content: { title: !title ? `Imported note ${index + 1} from Evernote` : title, @@ -111,7 +111,7 @@ export class EvernoteConverter { if (!tag) { const now = new Date() tag = { - uuid: this.application.generateUUID(), + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.TYPES.Tag, created_at: now, created_at_timestamp: now.getTime(), diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts index cd973ef54..487980889 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts @@ -4,19 +4,13 @@ import { jsonTestData, htmlTestData } from './testData' import { GoogleKeepConverter } from './GoogleKeepConverter' -import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' +import { UuidGenerator } from '@standardnotes/utils' + +UuidGenerator.SetGenerator(() => String(Math.random())) describe('GoogleKeepConverter', () => { - let application: WebApplicationInterface - - beforeEach(() => { - application = { - generateUUID: jest.fn().mockReturnValue('uuid'), - } as unknown as WebApplicationInterface - }) - it('should parse json data', () => { - const converter = new GoogleKeepConverter(application) + const converter = new GoogleKeepConverter() const result = converter.tryParseAsJson(jsonTestData) @@ -33,7 +27,7 @@ describe('GoogleKeepConverter', () => { }) it('should parse html data', () => { - const converter = new GoogleKeepConverter(application) + const converter = new GoogleKeepConverter() const result = converter.tryParseAsHtml( htmlTestData, diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts index b5608d327..fdc805c66 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts @@ -1,7 +1,7 @@ import { ContentType } from '@standardnotes/domain-core' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { readFileAsText } from '../Utils' -import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' +import { UuidGenerator } from '@standardnotes/utils' type GoogleKeepJsonNote = { color: string @@ -14,7 +14,7 @@ type GoogleKeepJsonNote = { } export class GoogleKeepConverter { - constructor(protected application: WebApplicationInterface) {} + constructor() {} async convertGoogleKeepBackupFileToNote( file: File, @@ -66,7 +66,7 @@ export class GoogleKeepConverter { created_at_timestamp: date.getTime(), updated_at: date, updated_at_timestamp: date.getTime(), - uuid: this.application.generateUUID(), + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.TYPES.Note, content: { title: title, @@ -96,6 +96,7 @@ export class GoogleKeepConverter { return } + // eslint-disable-next-line @typescript-eslint/no-explicit-any static isValidGoogleKeepJson(json: any): boolean { return ( typeof json.title === 'string' && @@ -120,7 +121,7 @@ export class GoogleKeepConverter { created_at_timestamp: date.getTime(), updated_at: date, updated_at_timestamp: date.getTime(), - uuid: this.application.generateUUID(), + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.TYPES.Note, content: { title: parsed.title, diff --git a/packages/ui-services/src/Import/Importer.ts b/packages/ui-services/src/Import/Importer.ts index 6c959fcb5..5f0f10daa 100644 --- a/packages/ui-services/src/Import/Importer.ts +++ b/packages/ui-services/src/Import/Importer.ts @@ -1,5 +1,10 @@ import { parseFileName } from '@standardnotes/filepicker' -import { FeatureStatus } from '@standardnotes/services' +import { + FeatureStatus, + FeaturesClientInterface, + ItemManagerInterface, + MutatorClientInterface, +} from '@standardnotes/services' import { NativeFeatureIdentifier } from '@standardnotes/features' import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter' import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter' @@ -8,7 +13,6 @@ import { PlaintextConverter } from './PlaintextConverter/PlaintextConverter' import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter' import { readFileAsText } from './Utils' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' -import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis' @@ -19,12 +23,16 @@ export class Importer { plaintextConverter: PlaintextConverter evernoteConverter: EvernoteConverter - constructor(protected application: WebApplicationInterface) { - this.aegisConverter = new AegisToAuthenticatorConverter(application) - this.googleKeepConverter = new GoogleKeepConverter(application) - this.simplenoteConverter = new SimplenoteConverter(application) - this.plaintextConverter = new PlaintextConverter(application) - this.evernoteConverter = new EvernoteConverter(application) + constructor( + private features: FeaturesClientInterface, + private mutator: MutatorClientInterface, + private items: ItemManagerInterface, + ) { + this.aegisConverter = new AegisToAuthenticatorConverter() + this.googleKeepConverter = new GoogleKeepConverter() + this.simplenoteConverter = new SimplenoteConverter() + this.plaintextConverter = new PlaintextConverter() + this.evernoteConverter = new EvernoteConverter() } static detectService = async (file: File): Promise => { @@ -64,7 +72,7 @@ export class Importer { async getPayloadsFromFile(file: File, type: NoteImportType): Promise { if (type === 'aegis') { const isEntitledToAuthenticator = - this.application.features.getFeatureStatus( + this.features.getFeatureStatus( NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(), ) === FeatureStatus.Entitled return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)] @@ -85,7 +93,7 @@ export class Importer { const insertedItems = await Promise.all( payloads.map(async (payload) => { const content = payload.content as NoteContent - const note = this.application.items.createTemplateItem( + const note = this.items.createTemplateItem( payload.content_type, { text: content.text, @@ -100,7 +108,7 @@ export class Importer { uuid: payload.uuid, }, ) - return this.application.mutator.insertItem(note) + return this.mutator.insertItem(note) }), ) return insertedItems diff --git a/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts b/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts index 7d100d949..6581e73d4 100644 --- a/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts +++ b/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts @@ -2,11 +2,9 @@ import { ContentType } from '@standardnotes/domain-core' import { parseFileName } from '@standardnotes/filepicker' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { readFileAsText } from '../Utils' -import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' +import { UuidGenerator } from '@standardnotes/utils' export class PlaintextConverter { - constructor(protected application: WebApplicationInterface) {} - static isValidPlaintextFile(file: File): boolean { return file.type === 'text/plain' || file.type === 'text/markdown' } @@ -24,7 +22,7 @@ export class PlaintextConverter { created_at_timestamp: createdAtDate.getTime(), updated_at: updatedAtDate, updated_at_timestamp: updatedAtDate.getTime(), - uuid: this.application.generateUUID(), + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.TYPES.Note, content: { title: name, diff --git a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts index a5a622d09..c9458f596 100644 --- a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts +++ b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts @@ -1,18 +1,12 @@ -import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' +import { UuidGenerator } from '@standardnotes/utils' import { SimplenoteConverter } from './SimplenoteConverter' import data from './testData' +UuidGenerator.SetGenerator(() => String(Math.random())) + describe('SimplenoteConverter', () => { - let application: WebApplicationInterface - - beforeEach(() => { - application = { - generateUUID: jest.fn().mockReturnValue('uuid'), - } as any - }) - it('should parse', () => { - const converter = new SimplenoteConverter(application) + const converter = new SimplenoteConverter() const result = converter.parse(data) diff --git a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts index 0ee392d3e..347b5d6cf 100644 --- a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts +++ b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts @@ -1,7 +1,7 @@ import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { ContentType } from '@standardnotes/domain-core' import { readFileAsText } from '../Utils' -import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' +import { UuidGenerator } from '@standardnotes/utils' type SimplenoteItem = { creationDate: string @@ -14,11 +14,13 @@ type SimplenoteData = { trashedNotes: SimplenoteItem[] } +// eslint-disable-next-line @typescript-eslint/no-explicit-any const isSimplenoteEntry = (entry: any): boolean => entry.id && entry.content && entry.creationDate && entry.lastModified export class SimplenoteConverter { - constructor(protected application: WebApplicationInterface) {} + constructor() {} + // eslint-disable-next-line @typescript-eslint/no-explicit-any static isValidSimplenoteJson(json: any): boolean { return ( (json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) || @@ -53,7 +55,7 @@ export class SimplenoteConverter { created_at_timestamp: createdAtDate.getTime(), updated_at: updatedAtDate, updated_at_timestamp: updatedAtDate.getTime(), - uuid: this.application.generateUUID(), + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.TYPES.Note, content: { title, diff --git a/packages/ui-services/src/Security/AutolockService.ts b/packages/ui-services/src/Security/AutolockService.ts index bf15d9254..6d5102b01 100644 --- a/packages/ui-services/src/Security/AutolockService.ts +++ b/packages/ui-services/src/Security/AutolockService.ts @@ -21,7 +21,7 @@ const STORAGE_KEY_AUTOLOCK_INTERVAL = 'AutoLockIntervalKey' export class AutolockService extends AbstractService { private unsubApp!: () => void - private pollInterval: any + private pollInterval: ReturnType | undefined private lastFocusState?: 'hidden' | 'visible' private lockAfterDate?: Date @@ -100,7 +100,7 @@ export class AutolockService extends AbstractService { */ beginPolling() { this.pollInterval = setInterval(async () => { - const locked = await this.application.isLocked() + const locked = await this.application.protections.isLocked() if (!locked && this.lockAfterDate && new Date() > this.lockAfterDate) { this.lockApplication() } diff --git a/packages/ui-services/src/StatePersistence/StatePersistence.ts b/packages/ui-services/src/StatePersistence/StatePersistence.ts index ecd5a6a35..bf63bc7f3 100644 --- a/packages/ui-services/src/StatePersistence/StatePersistence.ts +++ b/packages/ui-services/src/StatePersistence/StatePersistence.ts @@ -1,5 +1,5 @@ export enum PersistenceKey { - SelectedItemsController = 'selected-items-controller', + ItemListController = 'selected-items-controller', NavigationController = 'navigation-controller', } @@ -12,6 +12,6 @@ export type NavigationControllerPersistableValue = { } export type PersistedStateValue = { - [PersistenceKey.SelectedItemsController]: SelectionControllerPersistableValue + [PersistenceKey.ItemListController]: SelectionControllerPersistableValue [PersistenceKey.NavigationController]: NavigationControllerPersistableValue } diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index 583a0cce5..625e320c7 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -60,7 +60,7 @@ export class ThemeManager extends AbstractUIServicee { } override async onAppStart() { - const desktopService = this.application.getDesktopService() + const desktopService = this.application.desktopManager if (desktopService) { this.eventDisposers.push( desktopService.registerUpdateObserver((component) => { @@ -167,7 +167,7 @@ export class ThemeManager extends AbstractUIServicee { const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false) if (useDeviceThemeSettings) { - const prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark' + const prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark' this.setThemeAsPerColorScheme(prefersDarkColorScheme) } } @@ -187,7 +187,7 @@ export class ThemeManager extends AbstractUIServicee { let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches if (this.application.isNativeMobileWeb()) { - prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark' + prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark' } this.setThemeAsPerColorScheme(prefersDarkColorScheme) @@ -340,9 +340,7 @@ export class ThemeManager extends AbstractUIServicee { if (this.application.isNativeMobileWeb() && !theme.layerable) { const packageInfo = theme.featureDescription setTimeout(() => { - this.application - .mobileDevice() - .handleThemeSchemeChange(packageInfo.isDark ?? false, this.getBackgroundColor()) + this.application.mobileDevice.handleThemeSchemeChange(packageInfo.isDark ?? false, this.getBackgroundColor()) }) } @@ -366,7 +364,7 @@ export class ThemeManager extends AbstractUIServicee { if (this.themesActiveInTheUI.isEmpty()) { if (this.application.isNativeMobileWeb()) { - this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff') + this.application.mobileDevice.handleThemeSchemeChange(false, '#ffffff') } this.toggleTranslucentUIColors() } diff --git a/packages/ui-services/src/UseCase/GetItemTags.ts b/packages/ui-services/src/UseCase/GetItemTags.ts new file mode 100644 index 000000000..e34d2f00f --- /dev/null +++ b/packages/ui-services/src/UseCase/GetItemTags.ts @@ -0,0 +1,15 @@ +import { ContentType, Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { DecryptedItemInterface, SNTag } from '@standardnotes/models' +import { ItemManagerInterface } from '@standardnotes/services' + +export class GetItemTags implements SyncUseCaseInterface { + constructor(private items: ItemManagerInterface) {} + + execute(item: DecryptedItemInterface): Result { + return Result.ok( + this.items.itemsReferencingItem(item).filter((ref) => { + return ref.content_type === ContentType.TYPES.Tag + }), + ) + } +} diff --git a/packages/ui-services/src/UseCase/IsGlobalSpellcheckEnabled.ts b/packages/ui-services/src/UseCase/IsGlobalSpellcheckEnabled.ts new file mode 100644 index 000000000..45469e8ba --- /dev/null +++ b/packages/ui-services/src/UseCase/IsGlobalSpellcheckEnabled.ts @@ -0,0 +1,11 @@ +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { PrefDefaults, PrefKey } from '@standardnotes/models' +import { PreferenceServiceInterface } from '@standardnotes/services' + +export class IsGlobalSpellcheckEnabled implements SyncUseCaseInterface { + constructor(private preferences: PreferenceServiceInterface) {} + + execute(): Result { + return Result.ok(this.preferences.getValue(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck])) + } +} diff --git a/packages/ui-services/src/UseCase/IsMobileDevice.ts b/packages/ui-services/src/UseCase/IsMobileDevice.ts new file mode 100644 index 000000000..3da970796 --- /dev/null +++ b/packages/ui-services/src/UseCase/IsMobileDevice.ts @@ -0,0 +1,11 @@ +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { IsNativeMobileWeb } from './IsNativeMobileWeb' +import { isAndroid, isIOS } from '../Utils/Utils' + +export class IsMobileDevice implements SyncUseCaseInterface { + constructor(private _isNativeMobileWeb: IsNativeMobileWeb) {} + + execute(): Result { + return Result.ok(this._isNativeMobileWeb.execute().getValue() || isIOS() || isAndroid()) + } +} diff --git a/packages/ui-services/src/UseCase/IsNativeIOS.ts b/packages/ui-services/src/UseCase/IsNativeIOS.ts new file mode 100644 index 000000000..dd94787bb --- /dev/null +++ b/packages/ui-services/src/UseCase/IsNativeIOS.ts @@ -0,0 +1,13 @@ +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { Environment, Platform } from '@standardnotes/models' + +export class IsNativeIOS implements SyncUseCaseInterface { + constructor( + private environment: Environment, + private platform: Platform, + ) {} + + execute(): Result { + return Result.ok(this.environment === Environment.Mobile && this.platform === Platform.Ios) + } +} diff --git a/packages/ui-services/src/UseCase/IsNativeMobileWeb.ts b/packages/ui-services/src/UseCase/IsNativeMobileWeb.ts new file mode 100644 index 000000000..17593edb0 --- /dev/null +++ b/packages/ui-services/src/UseCase/IsNativeMobileWeb.ts @@ -0,0 +1,10 @@ +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { Environment } from '@standardnotes/models' + +export class IsNativeMobileWeb implements SyncUseCaseInterface { + constructor(private environment: Environment) {} + + execute(): Result { + return Result.ok(this.environment === Environment.Mobile) + } +} diff --git a/packages/ui-services/src/Utils/Utils.ts b/packages/ui-services/src/Utils/Utils.ts new file mode 100644 index 000000000..345589840 --- /dev/null +++ b/packages/ui-services/src/Utils/Utils.ts @@ -0,0 +1,20 @@ +import { Platform } from '@standardnotes/models' + +declare global { + interface Document { + documentMode?: string + } + + interface Window { + MSStream?: unknown + platform?: Platform + } +} + +// https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885#9039885 +export const isIOS = () => + (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) || + (navigator.userAgent.includes('Mac') && 'ontouchend' in document && navigator.maxTouchPoints > 1) || + window.platform === Platform.Ios + +export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android') diff --git a/packages/ui-services/src/Vaults/VaultDisplayService.ts b/packages/ui-services/src/Vaults/VaultDisplayService.ts index 13be7355e..cc8dfa354 100644 --- a/packages/ui-services/src/Vaults/VaultDisplayService.ts +++ b/packages/ui-services/src/Vaults/VaultDisplayService.ts @@ -33,10 +33,6 @@ export class VaultDisplayService this.options = new VaultDisplayOptions({ exclude: [], locked: [] }) - internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultLocked) - internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultUnlocked) - internalEventBus.addEventHandler(this, ApplicationEvent.ApplicationStageChanged) - makeObservable(this, { options: observable, @@ -48,6 +44,10 @@ export class VaultDisplayService unhideVault: action, showOnlyVault: action, }) + + internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultLocked) + internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultUnlocked) + internalEventBus.addEventHandler(this, ApplicationEvent.ApplicationStageChanged) } async handleEvent(event: InternalEventInterface): Promise { diff --git a/packages/ui-services/src/WebApplication/WebApplicationInterface.ts b/packages/ui-services/src/WebApplication/WebApplicationInterface.ts index c874de4ec..fdd79a60a 100644 --- a/packages/ui-services/src/WebApplication/WebApplicationInterface.ts +++ b/packages/ui-services/src/WebApplication/WebApplicationInterface.ts @@ -1,14 +1,15 @@ import { ApplicationInterface, + DesktopDeviceInterface, DesktopManagerInterface, MobileDeviceInterface, WebAppEvent, } from '@standardnotes/services' import { KeyboardService } from '../Keyboard/KeyboardService' +import { RouteServiceInterface } from '../Route/RouteServiceInterface' export interface WebApplicationInterface extends ApplicationInterface { notifyWebEvent(event: WebAppEvent, data?: unknown): void - getDesktopService(): DesktopManagerInterface | undefined handleMobileEnteringBackgroundEvent(): Promise handleMobileGainingFocusEvent(): Promise handleMobileLosingFocusEvent(): Promise @@ -24,10 +25,17 @@ export interface WebApplicationInterface extends ApplicationInterface { handleReceivedTextEvent(item: { text: string; title?: string }): Promise handleReceivedLinkEvent(item: { link: string; title: string }): Promise isNativeMobileWeb(): boolean - mobileDevice(): MobileDeviceInterface handleAndroidBackButtonPressed(): void addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined setAndroidBackHandlerFallbackListener(listener: () => boolean): void + handleInitialMobileScreenshotPrivacy(): void generateUUID(): string + checkForSecurityUpdate(): Promise + + get desktopManager(): DesktopManagerInterface | undefined + get mobileDevice(): MobileDeviceInterface + get isMobileDevice(): boolean + get desktopDevice(): DesktopDeviceInterface | undefined get keyboardService(): KeyboardService + get routeService(): RouteServiceInterface } diff --git a/packages/ui-services/src/index.ts b/packages/ui-services/src/index.ts index 74cfb2bb7..f6de720b5 100644 --- a/packages/ui-services/src/index.ts +++ b/packages/ui-services/src/index.ts @@ -29,6 +29,12 @@ export * from './Route/RouteServiceEvent' export * from './Security/AutolockService' export * from './Storage/LocalStorage' +export * from './UseCase/IsGlobalSpellcheckEnabled' +export * from './UseCase/IsNativeMobileWeb' +export * from './UseCase/IsMobileDevice' +export * from './UseCase/IsNativeIOS' +export * from './UseCase/GetItemTags' + export * from './Theme/ThemeManager' export * from './Theme/GetAllThemesUseCase' @@ -42,3 +48,4 @@ export * from './Vaults/VaultDisplayServiceEvent' export * from './Vaults/VaultDisplayServiceInterface' export * from './WebApplication/WebApplicationInterface' +export * from './Utils/Utils' diff --git a/packages/utils/src/Domain/Dependency/DependencyContainer.ts b/packages/utils/src/Domain/Dependency/DependencyContainer.ts new file mode 100644 index 000000000..bd84f86b7 --- /dev/null +++ b/packages/utils/src/Domain/Dependency/DependencyContainer.ts @@ -0,0 +1,50 @@ +import { isNotUndefined } from '../Utils/Utils' +import { isDeinitable } from './isDeinitable' + +export class DependencyContainer { + private factory = new Map unknown>() + private dependencies = new Map() + + public deinit() { + this.factory.clear() + + const deps = this.getAll() + for (const dep of deps) { + if (isDeinitable(dep)) { + dep.deinit() + } + } + + this.dependencies.clear() + } + + public getAll(): unknown[] { + return Array.from(this.dependencies.values()).filter(isNotUndefined) + } + + public bind(sym: symbol, maker: () => T) { + this.factory.set(sym, maker) + } + + public get(sym: symbol): T { + const dep = this.dependencies.get(sym) + if (dep) { + return dep as T + } + + const maker = this.factory.get(sym) + if (!maker) { + throw new Error(`No dependency maker found for ${sym.toString()}`) + } + + const instance = maker() + if (!instance) { + /** Could be optional */ + return undefined as T + } + + this.dependencies.set(sym, instance) + + return instance as T + } +} diff --git a/packages/snjs/lib/Application/Dependencies/isDeinitable.ts b/packages/utils/src/Domain/Dependency/isDeinitable.ts similarity index 100% rename from packages/snjs/lib/Application/Dependencies/isDeinitable.ts rename to packages/utils/src/Domain/Dependency/isDeinitable.ts diff --git a/packages/utils/src/Domain/index.ts b/packages/utils/src/Domain/index.ts index 626591680..21fbfdbe7 100644 --- a/packages/utils/src/Domain/index.ts +++ b/packages/utils/src/Domain/index.ts @@ -1,5 +1,7 @@ export * from './Date/DateUtils' export * from './Deferred/Deferred' +export * from './Dependency/DependencyContainer' +export * from './Dependency/isDeinitable' export * from './Logger/Logger' export * from './Logger/LoggerInterface' export * from './Logger/LogLevel' diff --git a/packages/web/src/javascripts/Application/Dependencies/Types.ts b/packages/web/src/javascripts/Application/Dependencies/Types.ts new file mode 100644 index 000000000..6477b63a1 --- /dev/null +++ b/packages/web/src/javascripts/Application/Dependencies/Types.ts @@ -0,0 +1,55 @@ +export const Web_TYPES = { + Application: Symbol.for('Application'), + + // Services + AndroidBackHandler: Symbol.for('AndroidBackHandler'), + ArchiveManager: Symbol.for('ArchiveManager'), + AutolockService: Symbol.for('AutolockService'), + ChangelogService: Symbol.for('ChangelogService'), + DesktopManager: Symbol.for('DesktopManager'), + Importer: Symbol.for('Importer'), + ItemGroupController: Symbol.for('ItemGroupController'), + KeyboardService: Symbol.for('KeyboardService'), + MobileWebReceiver: Symbol.for('MobileWebReceiver'), + MomentsService: Symbol.for('MomentsService'), + PersistenceService: Symbol.for('PersistenceService'), + RouteService: Symbol.for('RouteService'), + ThemeManager: Symbol.for('ThemeManager'), + VaultDisplayService: Symbol.for('VaultDisplayService'), + + // Controllers + AccountMenuController: Symbol.for('AccountMenuController'), + ActionsMenuController: Symbol.for('ActionsMenuController'), + ApplicationEventObserver: Symbol.for('ApplicationEventObserver'), + FeaturesController: Symbol.for('FeaturesController'), + FilePreviewModalController: Symbol.for('FilePreviewModalController'), + FilesController: Symbol.for('FilesController'), + HistoryModalController: Symbol.for('HistoryModalController'), + ImportModalController: Symbol.for('ImportModalController'), + ItemListController: Symbol.for('ItemListController'), + LinkingController: Symbol.for('LinkingController'), + NavigationController: Symbol.for('NavigationController'), + NoAccountWarningController: Symbol.for('NoAccountWarningController'), + NotesController: Symbol.for('NotesController'), + PaneController: Symbol.for('PaneController'), + PreferencesController: Symbol.for('PreferencesController'), + PurchaseFlowController: Symbol.for('PurchaseFlowController'), + QuickSettingsController: Symbol.for('QuickSettingsController'), + SearchOptionsController: Symbol.for('SearchOptionsController'), + SubscriptionController: Symbol.for('SubscriptionController'), + SyncStatusController: Symbol.for('SyncStatusController'), + ToastService: Symbol.for('ToastService'), + VaultSelectionMenuController: Symbol.for('VaultSelectionMenuController'), + + // Use cases + GetItemTags: Symbol.for('GetItemTags'), + GetPurchaseFlowUrl: Symbol.for('GetPurchaseFlowUrl'), + IsGlobalSpellcheckEnabled: Symbol.for('IsGlobalSpellcheckEnabled'), + IsMobileDevice: Symbol.for('IsMobileDevice'), + IsNativeIOS: Symbol.for('IsNativeIOS'), + IsNativeMobileWeb: Symbol.for('IsNativeMobileWeb'), + IsTabletOrMobileScreen: Symbol.for('IsTabletOrMobileScreen'), + LoadPurchaseFlowUrl: Symbol.for('LoadPurchaseFlowUrl'), + OpenSubscriptionDashboard: Symbol.for('OpenSubscriptionDashboard'), + PanesForLayout: Symbol.for('PanesForLayout'), +} diff --git a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts new file mode 100644 index 000000000..6a3593971 --- /dev/null +++ b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts @@ -0,0 +1,390 @@ +import { + ArchiveManager, + AutolockService, + ChangelogService, + GetItemTags, + Importer, + IsGlobalSpellcheckEnabled, + IsMobileDevice, + IsNativeIOS, + IsNativeMobileWeb, + KeyboardService, + RouteService, + ThemeManager, + ToastService, + VaultDisplayService, + WebApplicationInterface, +} from '@standardnotes/ui-services' +import { DependencyContainer } from '@standardnotes/utils' +import { Web_TYPES } from './Types' +import { BackupServiceInterface, isDesktopDevice } from '@standardnotes/snjs' +import { DesktopManager } from '../Device/DesktopManager' +import { MomentsService } from '@/Controllers/Moments/MomentsService' +import { PersistenceService } from '@/Controllers/Abstract/PersistenceService' +import { FilePreviewModalController } from '@/Controllers/FilePreviewModalController' +import { QuickSettingsController } from '@/Controllers/QuickSettingsController' +import { VaultSelectionMenuController } from '@/Controllers/VaultSelectionMenuController' +import { PaneController } from '@/Controllers/PaneController/PaneController' +import { PreferencesController } from '@/Controllers/PreferencesController' +import { FeaturesController } from '@/Controllers/FeaturesController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' +import { NotesController } from '@/Controllers/NotesController/NotesController' +import { ItemListController } from '@/Controllers/ItemList/ItemListController' +import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController' +import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' +import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController' +import { PurchaseFlowController } from '@/Controllers/PurchaseFlow/PurchaseFlowController' +import { FilesController } from '@/Controllers/FilesController' +import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' +import { ImportModalController } from '@/Controllers/ImportModalController' +import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver' +import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { LinkingController } from '@/Controllers/LinkingController' +import { SyncStatusController } from '@/Controllers/SyncStatusController' +import { ActionsMenuController } from '@/Controllers/ActionsMenuController' +import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' +import { MobileWebReceiver } from '@/NativeMobileWeb/MobileWebReceiver' +import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler' +import { IsTabletOrMobileScreen } from '../UseCase/IsTabletOrMobileScreen' +import { PanesForLayout } from '../UseCase/PanesForLayout' +import { LoadPurchaseFlowUrl } from '../UseCase/LoadPurchaseFlowUrl' +import { GetPurchaseFlowUrl } from '../UseCase/GetPurchaseFlowUrl' +import { OpenSubscriptionDashboard } from '../UseCase/OpenSubscriptionDashboard' + +export class WebDependencies extends DependencyContainer { + constructor(private application: WebApplicationInterface) { + super() + + this.bind(Web_TYPES.Importer, () => { + return new Importer(application.features, application.mutator, application.items) + }) + + this.bind(Web_TYPES.IsNativeIOS, () => { + return new IsNativeIOS(application.environment, application.platform) + }) + + this.bind(Web_TYPES.OpenSubscriptionDashboard, () => { + return new OpenSubscriptionDashboard(application, application.legacyApi) + }) + + this.bind(Web_TYPES.IsNativeMobileWeb, () => { + return new IsNativeMobileWeb(application.environment) + }) + + this.bind(Web_TYPES.IsGlobalSpellcheckEnabled, () => { + return new IsGlobalSpellcheckEnabled(application.preferences) + }) + + this.bind(Web_TYPES.MobileWebReceiver, () => { + if (!application.isNativeMobileWeb()) { + return undefined + } + + return new MobileWebReceiver(application) + }) + + this.bind(Web_TYPES.AndroidBackHandler, () => { + if (!application.isNativeMobileWeb()) { + return undefined + } + + return new AndroidBackHandler() + }) + + this.bind(Web_TYPES.Application, () => this.application) + + this.bind(Web_TYPES.ItemGroupController, () => { + return new ItemGroupController( + application.items, + application.mutator, + application.sync, + application.sessions, + application.preferences, + application.componentManager, + application.alerts, + this.get(Web_TYPES.IsNativeMobileWeb), + ) + }) + + this.bind(Web_TYPES.RouteService, () => { + return new RouteService(this.application, this.application.events) + }) + + this.bind(Web_TYPES.KeyboardService, () => { + return new KeyboardService(application.platform, application.environment) + }) + + this.bind(Web_TYPES.ArchiveManager, () => { + return new ArchiveManager(this.get(Web_TYPES.Application)) + }) + + this.bind(Web_TYPES.ThemeManager, () => { + return new ThemeManager(application, application.preferences, application.componentManager, application.events) + }) + + this.bind(Web_TYPES.AutolockService, () => { + return application.isNativeMobileWeb() ? undefined : new AutolockService(application, application.events) + }) + + this.bind(Web_TYPES.DesktopManager, () => { + return isDesktopDevice(application.device) + ? new DesktopManager(application, application.device, application.fileBackups as BackupServiceInterface) + : undefined + }) + + this.bind(Web_TYPES.ChangelogService, () => { + return new ChangelogService(application.environment, application.storage) + }) + + this.bind(Web_TYPES.IsMobileDevice, () => { + return new IsMobileDevice(this.get(Web_TYPES.IsNativeMobileWeb)) + }) + + this.bind(Web_TYPES.MomentsService, () => { + return new MomentsService( + this.get(Web_TYPES.FilesController), + this.get(Web_TYPES.LinkingController), + application.storage, + application.preferences, + application.items, + application.protections, + application.desktopDevice, + this.get(Web_TYPES.IsMobileDevice), + application.events, + ) + }) + + this.bind(Web_TYPES.VaultDisplayService, () => { + return new VaultDisplayService(application, application.events) + }) + + this.bind(Web_TYPES.PersistenceService, () => { + return new PersistenceService( + this.get(Web_TYPES.ItemListController), + this.get(Web_TYPES.NavigationController), + application.storage, + application.items, + application.sync, + application.events, + ) + }) + + this.bind(Web_TYPES.FilePreviewModalController, () => { + return new FilePreviewModalController(application.items) + }) + + this.bind(Web_TYPES.QuickSettingsController, () => { + return new QuickSettingsController(application.events) + }) + + this.bind(Web_TYPES.VaultSelectionMenuController, () => { + return new VaultSelectionMenuController(application.events) + }) + + this.bind(Web_TYPES.PaneController, () => { + return new PaneController( + application.preferences, + this.get(Web_TYPES.KeyboardService), + this.get(Web_TYPES.IsTabletOrMobileScreen), + this.get(Web_TYPES.PanesForLayout), + application.events, + ) + }) + + this.bind(Web_TYPES.PanesForLayout, () => { + return new PanesForLayout(this.get(Web_TYPES.IsTabletOrMobileScreen)) + }) + + this.bind(Web_TYPES.IsTabletOrMobileScreen, () => { + return new IsTabletOrMobileScreen(application.environment) + }) + + this.bind(Web_TYPES.PreferencesController, () => { + return new PreferencesController(this.get(Web_TYPES.RouteService), application.events) + }) + + this.bind(Web_TYPES.FeaturesController, () => { + return new FeaturesController(application.features, application.events) + }) + + this.bind(Web_TYPES.NavigationController, () => { + return new NavigationController( + this.get(Web_TYPES.FeaturesController), + this.get(Web_TYPES.VaultDisplayService), + this.get(Web_TYPES.KeyboardService), + this.get(Web_TYPES.PaneController), + application.sync, + application.mutator, + application.items, + application.preferences, + application.alerts, + application.changeAndSaveItem, + application.events, + ) + }) + + this.bind(Web_TYPES.NotesController, () => { + return new NotesController( + this.get(Web_TYPES.ItemListController), + this.get(Web_TYPES.NavigationController), + this.get(Web_TYPES.ItemGroupController), + this.get(Web_TYPES.KeyboardService), + application.preferences, + application.items, + application.mutator, + application.sync, + application.protections, + application.alerts, + this.get(Web_TYPES.IsGlobalSpellcheckEnabled), + this.get(Web_TYPES.GetItemTags), + application.events, + ) + }) + + this.bind(Web_TYPES.GetItemTags, () => { + return new GetItemTags(application.items) + }) + + this.bind(Web_TYPES.SearchOptionsController, () => { + return new SearchOptionsController(application.protections, application.events) + }) + + this.bind(Web_TYPES.LinkingController, () => { + return new LinkingController( + this.get(Web_TYPES.ItemListController), + this.get(Web_TYPES.FilesController), + this.get(Web_TYPES.SubscriptionController), + this.get(Web_TYPES.NavigationController), + this.get(Web_TYPES.ItemGroupController), + this.get(Web_TYPES.VaultDisplayService), + application.preferences, + application.items, + application.mutator, + application.sync, + application.vaults, + application.events, + ) + }) + + this.bind(Web_TYPES.ItemListController, () => { + return new ItemListController( + this.get(Web_TYPES.KeyboardService), + this.get(Web_TYPES.PaneController), + this.get(Web_TYPES.NavigationController), + this.get(Web_TYPES.SearchOptionsController), + application.items, + application.preferences, + this.get(Web_TYPES.ItemGroupController), + this.get(Web_TYPES.VaultDisplayService), + this.get(Web_TYPES.DesktopManager), + application.protections, + application.options, + this.get(Web_TYPES.IsNativeMobileWeb), + application.changeAndSaveItem, + application.events, + ) + }) + + this.bind(Web_TYPES.NoAccountWarningController, () => { + return new NoAccountWarningController(application.sessions, application.events) + }) + + this.bind(Web_TYPES.AccountMenuController, () => { + return new AccountMenuController(application.items, application.getHost, application.events) + }) + + this.bind(Web_TYPES.SubscriptionController, () => { + return new SubscriptionController( + application.subscriptions, + application.sessions, + application.features, + application.events, + ) + }) + + this.bind(Web_TYPES.PurchaseFlowController, () => { + return new PurchaseFlowController( + application.sessions, + application.subscriptions, + application.legacyApi, + application.alerts, + application.mobileDevice, + this.get(Web_TYPES.LoadPurchaseFlowUrl), + this.get(Web_TYPES.IsNativeIOS), + application.events, + ) + }) + + this.bind(Web_TYPES.LoadPurchaseFlowUrl, () => { + return new LoadPurchaseFlowUrl(application, this.get(Web_TYPES.GetPurchaseFlowUrl)) + }) + + this.bind(Web_TYPES.GetPurchaseFlowUrl, () => { + return new GetPurchaseFlowUrl(application, application.legacyApi) + }) + + this.bind(Web_TYPES.SyncStatusController, () => { + return new SyncStatusController() + }) + + this.bind(Web_TYPES.ActionsMenuController, () => { + return new ActionsMenuController() + }) + + this.bind(Web_TYPES.FilesController, () => { + return new FilesController( + this.get(Web_TYPES.NotesController), + this.get(Web_TYPES.FilePreviewModalController), + this.get(Web_TYPES.ArchiveManager), + this.get(Web_TYPES.VaultDisplayService), + application.items, + application.files, + application.mutator, + application.sync, + application.protections, + application.alerts, + application.platform, + application.mobileDevice, + this.get(Web_TYPES.IsNativeMobileWeb), + application.events, + ) + }) + + this.bind(Web_TYPES.HistoryModalController, () => { + return new HistoryModalController( + this.get(Web_TYPES.NotesController), + this.get(Web_TYPES.KeyboardService), + application.events, + ) + }) + + this.bind(Web_TYPES.ImportModalController, () => { + return new ImportModalController( + this.get(Web_TYPES.Importer), + this.get(Web_TYPES.NavigationController), + application.items, + application.mutator, + ) + }) + + this.bind(Web_TYPES.ToastService, () => { + return new ToastService() + }) + + this.bind(Web_TYPES.ApplicationEventObserver, () => { + return new ApplicationEventObserver( + application, + application.routeService, + this.get(Web_TYPES.PurchaseFlowController), + this.get(Web_TYPES.AccountMenuController), + this.get(Web_TYPES.PreferencesController), + this.get(Web_TYPES.SyncStatusController), + application.sync, + application.sessions, + application.subscriptions, + this.get(Web_TYPES.ToastService), + application.user, + ) + }) + } +} diff --git a/packages/web/src/javascripts/Application/DevMode.ts b/packages/web/src/javascripts/Application/DevMode.ts index 180482589..f62fc7711 100644 --- a/packages/web/src/javascripts/Application/DevMode.ts +++ b/packages/web/src/javascripts/Application/DevMode.ts @@ -10,7 +10,7 @@ export class DevMode { /** Valid only when running a mock event publisher on port 3124 */ async purchaseMockSubscription() { const subscriptionId = 2002 - const email = this.application.getUser()?.email + const email = this.application.sessions.getUser()?.email const response = await fetch('http://localhost:3124/events', { method: 'POST', headers: { diff --git a/packages/web/src/javascripts/Application/Device/DesktopManager.ts b/packages/web/src/javascripts/Application/Device/DesktopManager.ts index 4fe1a151e..86d94a313 100644 --- a/packages/web/src/javascripts/Application/Device/DesktopManager.ts +++ b/packages/web/src/javascripts/Application/Device/DesktopManager.ts @@ -90,7 +90,7 @@ export class DesktopManager } } - async saveDesktopBackup() { + async saveDesktopBackup(): Promise { this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload) const data = await this.getBackupFile() @@ -149,12 +149,12 @@ export class DesktopManager } } - searchText(text?: string) { + searchText(text?: string): void { this.lastSearchedText = text this.device.onSearch(text) } - redoSearch() { + redoSearch(): void { if (this.lastSearchedText) { this.searchText(this.lastSearchedText) } @@ -188,18 +188,20 @@ export class DesktopManager return } - const updatedComponent = await this.application.changeAndSaveItem( - component, - (m) => { - const mutator = m as ComponentMutator - // eslint-disable-next-line camelcase - mutator.local_url = componentData.content.local_url as string - // eslint-disable-next-line camelcase - mutator.package_info = componentData.content.package_info - mutator.setAppDataItem(AppDataField.ComponentInstallError, undefined) - }, - undefined, - ) + const updatedComponent = ( + await this.application.changeAndSaveItem.execute( + component, + (m) => { + const mutator = m as ComponentMutator + // eslint-disable-next-line camelcase + mutator.local_url = componentData.content.local_url as string + // eslint-disable-next-line camelcase + mutator.package_info = componentData.content.package_info + mutator.setAppDataItem(AppDataField.ComponentInstallError, undefined) + }, + undefined, + ) + ).getValue() for (const observer of this.updateObservers) { observer.callback(updatedComponent as SNComponent) diff --git a/packages/web/src/javascripts/Application/UseCase/GetPurchaseFlowUrl.ts b/packages/web/src/javascripts/Application/UseCase/GetPurchaseFlowUrl.ts new file mode 100644 index 000000000..1524f1aec --- /dev/null +++ b/packages/web/src/javascripts/Application/UseCase/GetPurchaseFlowUrl.ts @@ -0,0 +1,25 @@ +import { isDesktopApplication } from '@/Utils' +import { ApplicationInterface, LegacyApiServiceInterface, Result, UseCaseInterface } from '@standardnotes/snjs' + +export class GetPurchaseFlowUrl implements UseCaseInterface { + constructor( + private application: ApplicationInterface, + private legacyApi: LegacyApiServiceInterface, + ) {} + + async execute(): Promise> { + const currentUrl = window.location.origin + const successUrl = isDesktopApplication() ? 'standardnotes://' : currentUrl + + if (this.application.sessions.isSignedOut() || this.application.isThirdPartyHostUsed()) { + return Result.ok(`${window.purchaseUrl}/offline?&success_url=${successUrl}`) + } + + const token = await this.legacyApi.getNewSubscriptionToken() + if (token) { + return Result.ok(`${window.purchaseUrl}?subscription_token=${token}&success_url=${successUrl}`) + } + + return Result.fail('Could not get purchase flow URL.') + } +} diff --git a/packages/web/src/javascripts/Application/UseCase/IsTabletOrMobileScreen.ts b/packages/web/src/javascripts/Application/UseCase/IsTabletOrMobileScreen.ts new file mode 100644 index 000000000..0b618c08d --- /dev/null +++ b/packages/web/src/javascripts/Application/UseCase/IsTabletOrMobileScreen.ts @@ -0,0 +1,32 @@ +import { isMobileScreen, isTabletOrMobileScreen, isTabletScreen } from '@/Utils' +import { Environment, Result, SyncUseCaseInterface } from '@standardnotes/snjs' +import { IsNativeMobileWeb } from '@standardnotes/ui-services' + +type ReturnType = { + isTabletOrMobile: boolean + isTablet: boolean + isMobile: boolean +} + +export class IsTabletOrMobileScreen implements SyncUseCaseInterface { + private _isNativeMobileWeb = new IsNativeMobileWeb(this.environment) + + constructor(private environment: Environment) {} + + execute(): Result { + const isNativeMobile = this._isNativeMobileWeb.execute().getValue() + const isTabletOrMobile = isTabletOrMobileScreen() || isNativeMobile + const isTablet = isTabletScreen() || (isNativeMobile && !isMobileScreen()) + const isMobile = isMobileScreen() || (isNativeMobile && !isTablet) + + if (isTablet && isMobile) { + throw Error('isTablet and isMobile cannot both be true') + } + + return Result.ok({ + isTabletOrMobile, + isTablet, + isMobile, + }) + } +} diff --git a/packages/web/src/javascripts/Application/UseCase/LoadPurchaseFlowUrl.ts b/packages/web/src/javascripts/Application/UseCase/LoadPurchaseFlowUrl.ts new file mode 100644 index 000000000..64e4d5304 --- /dev/null +++ b/packages/web/src/javascripts/Application/UseCase/LoadPurchaseFlowUrl.ts @@ -0,0 +1,40 @@ +import { Environment, Result, UseCaseInterface } from '@standardnotes/snjs' +import { GetPurchaseFlowUrl } from './GetPurchaseFlowUrl' +import { RouteType, WebApplicationInterface } from '@standardnotes/ui-services' + +export class LoadPurchaseFlowUrl implements UseCaseInterface { + constructor( + private application: WebApplicationInterface, + private _getPurchaseFlowUrl: GetPurchaseFlowUrl, + ) {} + + async execute(): Promise> { + const urlResult = await this._getPurchaseFlowUrl.execute() + if (urlResult.isFailed()) { + return urlResult + } + + const url = urlResult.getValue() + const route = this.application.routeService.getRoute() + const params = route.type === RouteType.Purchase ? route.purchaseParams : { period: null, plan: null } + const period = params.period ? `&period=${params.period}` : '' + const plan = params.plan ? `&plan=${params.plan}` : '' + + if (url) { + const finalUrl = `${url}${period}${plan}` + + if (this.application.isNativeMobileWeb()) { + this.application.mobileDevice.openUrl(finalUrl) + } else if (this.application.environment === Environment.Desktop) { + this.application.desktopDevice?.openUrl(finalUrl) + } else { + const windowProxy = window.open('', '_blank') + ;(windowProxy as WindowProxy).location = finalUrl + } + + return Result.ok() + } + + return Result.fail('Could not load purchase flow URL.') + } +} diff --git a/packages/web/src/javascripts/Application/UseCase/OpenSubscriptionDashboard.ts b/packages/web/src/javascripts/Application/UseCase/OpenSubscriptionDashboard.ts new file mode 100644 index 000000000..f2e6f37a6 --- /dev/null +++ b/packages/web/src/javascripts/Application/UseCase/OpenSubscriptionDashboard.ts @@ -0,0 +1,33 @@ +import { Environment, LegacyApiServiceInterface, Result, UseCaseInterface } from '@standardnotes/snjs' +import { WebApplicationInterface } from '@standardnotes/ui-services' + +export class OpenSubscriptionDashboard implements UseCaseInterface { + constructor( + private application: WebApplicationInterface, + private legacyApi: LegacyApiServiceInterface, + ) {} + + async execute(): Promise> { + const token = await this.legacyApi.getNewSubscriptionToken() + if (!token) { + return Result.fail('Could not get subscription token.') + } + + const url = `${window.dashboardUrl}?subscription_token=${token}` + + if (this.application.device.environment === Environment.Mobile) { + this.application.device.openUrl(url) + return Result.ok() + } + + if (this.application.device.environment === Environment.Desktop) { + window.open(url, '_blank') + return Result.ok() + } + + const windowProxy = window.open('', '_blank') + ;(windowProxy as WindowProxy).location = url + + return Result.ok() + } +} diff --git a/packages/web/src/javascripts/Application/UseCase/PanesForLayout.ts b/packages/web/src/javascripts/Application/UseCase/PanesForLayout.ts new file mode 100644 index 000000000..f8f0c121d --- /dev/null +++ b/packages/web/src/javascripts/Application/UseCase/PanesForLayout.ts @@ -0,0 +1,35 @@ +import { AppPaneId } from './../../Components/Panes/AppPaneMetadata' +import { PaneLayout } from './../../Controllers/PaneController/PaneLayout' +import { IsTabletOrMobileScreen } from './IsTabletOrMobileScreen' +import { Result, SyncUseCaseInterface } from '@standardnotes/snjs' + +export class PanesForLayout implements SyncUseCaseInterface { + constructor(private _isTabletOrMobileScreen: IsTabletOrMobileScreen) {} + + execute(layout: PaneLayout): Result { + const screen = this._isTabletOrMobileScreen.execute().getValue() + if (screen.isTablet) { + if (layout === PaneLayout.TagSelection || layout === PaneLayout.TableView) { + return Result.ok([AppPaneId.Navigation, AppPaneId.Items]) + } else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.Editing) { + return Result.ok([AppPaneId.Items, AppPaneId.Editor]) + } + } else if (screen.isMobile) { + if (layout === PaneLayout.TagSelection) { + return Result.ok([AppPaneId.Navigation]) + } else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.TableView) { + return Result.ok([AppPaneId.Navigation, AppPaneId.Items]) + } else if (layout === PaneLayout.Editing) { + return Result.ok([AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]) + } + } else { + if (layout === PaneLayout.TableView) { + return Result.ok([AppPaneId.Navigation, AppPaneId.Items]) + } else { + return Result.ok([AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor]) + } + } + + throw Error('Unhandled pane layout') + } +} diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts index 7932c48e2..9bd7f61a4 100644 --- a/packages/web/src/javascripts/Application/WebApplication.ts +++ b/packages/web/src/javascripts/Application/WebApplication.ts @@ -1,11 +1,9 @@ import { WebCrypto } from '@/Application/Crypto' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice' import { DeinitSource, Platform, SNApplication, - removeFromArray, DesktopDeviceInterface, isDesktopDevice, DeinitMode, @@ -19,57 +17,82 @@ import { DecryptedItem, Environment, ApplicationOptionsDefaults, - BackupServiceInterface, InternalFeatureService, InternalFeatureServiceInterface, - PrefDefaults, NoteContent, SNNote, + DesktopManagerInterface, } from '@standardnotes/snjs' -import { makeObservable, observable } from 'mobx' +import { action, computed, makeObservable, observable } from 'mobx' import { startAuthentication, startRegistration } from '@simplewebauthn/browser' import { PanelResizedData } from '@/Types/PanelResizedData' -import { getBlobFromBase64, isAndroid, isDesktopApplication, isDev, isIOS } from '@/Utils' -import { DesktopManager } from './Device/DesktopManager' +import { getBlobFromBase64, isDesktopApplication, isDev } from '@/Utils' import { ArchiveManager, AutolockService, ChangelogService, + Importer, + IsGlobalSpellcheckEnabled, + IsMobileDevice, + IsNativeIOS, + IsNativeMobileWeb, KeyboardService, PreferenceId, - RouteService, RouteServiceInterface, ThemeManager, - VaultDisplayService, VaultDisplayServiceInterface, WebAlertService, WebApplicationInterface, } from '@standardnotes/ui-services' import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver' -import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler' import { setCustomViewportHeight } from '@/setViewportHeightWithFallback' -import { WebServices } from './WebServices' import { FeatureName } from '@/Controllers/FeatureName' -import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' import { VisibilityObserver } from './VisibilityObserver' -import { MomentsService } from '@/Controllers/Moments/MomentsService' import { DevMode } from './DevMode' import { ToastType, addToast, dismissToast } from '@standardnotes/toast' +import { WebDependencies } from './Dependencies/WebDependencies' +import { Web_TYPES } from './Dependencies/Types' +import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver' +import { PaneController } from '@/Controllers/PaneController/PaneController' +import { LinkingController } from '@/Controllers/LinkingController' +import { MomentsService } from '@/Controllers/Moments/MomentsService' +import { FeaturesController } from '@/Controllers/FeaturesController' +import { FilesController } from '@/Controllers/FilesController' +import { ItemListController } from '@/Controllers/ItemList/ItemListController' +import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler' +import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController' +import { PurchaseFlowController } from '@/Controllers/PurchaseFlow/PurchaseFlowController' +import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' +import { PreferencesController } from '@/Controllers/PreferencesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' +import { ImportModalController } from '@/Controllers/ImportModalController' +import { SyncStatusController } from '@/Controllers/SyncStatusController' +import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' +import { FilePreviewModalController } from '@/Controllers/FilePreviewModalController' +import { OpenSubscriptionDashboard } from './UseCase/OpenSubscriptionDashboard' +import { QuickSettingsController } from '@/Controllers/QuickSettingsController' +import { VaultSelectionMenuController } from '@/Controllers/VaultSelectionMenuController' +import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' +import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController' +import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { PersistenceService } from '@/Controllers/Abstract/PersistenceService' +import { removeFromArray } from '@standardnotes/utils' export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void export class WebApplication extends SNApplication implements WebApplicationInterface { - public readonly itemControllerGroup: ItemGroupController - public readonly routeService: RouteServiceInterface + readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures - private readonly webServices!: WebServices + private readonly deps = new WebDependencies(this) + + private visibilityObserver?: VisibilityObserver private readonly webEventObservers: WebEventObserver[] = [] - private readonly mobileWebReceiver?: MobileWebReceiver - private readonly androidBackHandler?: AndroidBackHandler - private readonly visibilityObserver?: VisibilityObserver - private readonly mobileAppEventObserver?: () => void + private disposers: (() => void)[] = [] - public readonly devMode?: DevMode + public isSessionsModalVisible = false + + public devMode?: DevMode constructor( deviceInterface: WebOrDesktopDevice, @@ -102,48 +125,49 @@ export class WebApplication extends SNApplication implements WebApplicationInter ) => Promise>, }) + makeObservable(this, { + dealloced: observable, + + preferencesController: computed, + + isSessionsModalVisible: observable, + + openSessionsModal: action, + closeSessionsModal: action, + }) + + this.createBackgroundServices() + } + + private createBackgroundServices(): void { + void this.mobileWebReceiver + void this.autolockService + void this.persistence + void this.themeManager + void this.momentsService + void this.routeService + if (isDev) { this.devMode = new DevMode(this) } - makeObservable(this, { - dealloced: observable, - }) - if (!this.isNativeMobileWeb()) { - deviceInterface.setApplication(this) + this.webOrDesktopDevice.setApplication(this) } - this.itemControllerGroup = new ItemGroupController(this) - this.routeService = new RouteService(this, this.events) - - this.webServices = {} as WebServices - this.webServices.keyboardService = new KeyboardService(platform, this.environment) - this.webServices.archiveService = new ArchiveManager(this) - this.webServices.themeService = new ThemeManager(this, this.preferences, this.componentManager, this.events) - this.webServices.autolockService = this.isNativeMobileWeb() ? undefined : new AutolockService(this, this.events) - this.webServices.desktopService = isDesktopDevice(deviceInterface) - ? new DesktopManager(this, deviceInterface, this.fileBackups as BackupServiceInterface) - : undefined - this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface) - this.webServices.changelogService = new ChangelogService(this.environment, this.storage) - this.webServices.momentsService = new MomentsService( - this, - this.webServices.viewControllerManager.filesController, - this.events, - ) - this.webServices.vaultDisplayService = new VaultDisplayService(this, this.events) + const appEventObserver = this.deps.get(Web_TYPES.ApplicationEventObserver) + this.disposers.push(this.addEventObserver(appEventObserver.handle.bind(appEventObserver))) if (this.isNativeMobileWeb()) { - this.mobileWebReceiver = new MobileWebReceiver(this) - this.androidBackHandler = new AndroidBackHandler() - this.mobileAppEventObserver = this.addEventObserver(async (event) => { - this.mobileDevice().notifyApplicationEvent(event) - }) + this.disposers.push( + this.addEventObserver(async (event) => { + this.mobileDevice.notifyApplicationEvent(event) + }), + ) // eslint-disable-next-line no-console console.log = (...args) => { - this.mobileDevice().consoleLog(...args) + this.mobileDevice.consoleLog(...args) } } @@ -158,42 +182,23 @@ export class WebApplication extends SNApplication implements WebApplicationInter super.deinit(mode, source) if (!this.isNativeMobileWeb()) { - this.webOrDesktopDevice().removeApplication(this) + this.webOrDesktopDevice.removeApplication(this) } + for (const disposer of this.disposers) { + disposer() + } + this.disposers.length = 0 + + this.deps.deinit() + try { - for (const service of Object.values(this.webServices)) { - if (!service) { - continue - } - - if ('deinit' in service) { - service.deinit?.(source) - } - - ;(service as { application?: WebApplication }).application = undefined - } - - ;(this.webServices as unknown) = undefined - - this.itemControllerGroup.deinit() - ;(this.itemControllerGroup as unknown) = undefined - ;(this.mobileWebReceiver as unknown) = undefined - - this.routeService.deinit() - ;(this.routeService as unknown) = undefined - this.webEventObservers.length = 0 if (this.visibilityObserver) { this.visibilityObserver.deinit() ;(this.visibilityObserver as unknown) = undefined } - - if (this.mobileAppEventObserver) { - this.mobileAppEventObserver() - ;(this.mobileAppEventObserver as unknown) = undefined - } } catch (error) { console.error('Error while deiniting application', error) } @@ -225,46 +230,6 @@ export class WebApplication extends SNApplication implements WebApplicationInter this.notifyWebEvent(WebAppEvent.PanelResized, data) } - public get vaultDisplayService(): VaultDisplayServiceInterface { - return this.webServices.vaultDisplayService - } - - public get controllers(): ViewControllerManager { - return this.webServices.viewControllerManager - } - - public getDesktopService(): DesktopManager | undefined { - return this.webServices.desktopService - } - - public getAutolockService() { - return this.webServices.autolockService - } - - public getArchiveService() { - return this.webServices.archiveService - } - - public get paneController() { - return this.webServices.viewControllerManager.paneController - } - - public get linkingController() { - return this.webServices.viewControllerManager.linkingController - } - - public get changelogService() { - return this.webServices.changelogService - } - - public get momentsService() { - return this.webServices.momentsService - } - - public get featuresController() { - return this.controllers.featuresController - } - public get desktopDevice(): DesktopDeviceInterface | undefined { if (isDesktopDevice(this.device)) { return this.device @@ -277,53 +242,42 @@ export class WebApplication extends SNApplication implements WebApplicationInter return InternalFeatureService.get() } - isNativeIOS() { - return this.isNativeMobileWeb() && this.platform === Platform.Ios + isNativeIOS(): boolean { + return this.deps.get(Web_TYPES.IsNativeIOS).execute().getValue() } - get isMobileDevice() { - return this.isNativeMobileWeb() || isIOS() || isAndroid() + get isMobileDevice(): boolean { + return this.deps.get(Web_TYPES.IsMobileDevice).execute().getValue() } get hideOutboundSubscriptionLinks() { return this.isNativeIOS() } - mobileDevice(): MobileDeviceInterface { - if (!this.isNativeMobileWeb()) { - throw Error('Attempting to access device as mobile device on non mobile platform') - } + get mobileDevice(): MobileDeviceInterface { return this.device as MobileDeviceInterface } - webOrDesktopDevice(): WebOrDesktopDevice { + get webOrDesktopDevice(): WebOrDesktopDevice { return this.device as WebOrDesktopDevice } - public getThemeService() { - return this.webServices.themeService - } - - public get keyboardService() { - return this.webServices.keyboardService - } - - async checkForSecurityUpdate() { + async checkForSecurityUpdate(): Promise { return this.protocolUpgradeAvailable() } performDesktopTextBackup(): void | Promise { - return this.getDesktopService()?.saveDesktopBackup() + return this.desktopManager?.saveDesktopBackup() } isGlobalSpellcheckEnabled(): boolean { - return this.getPreference(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck]) + return this.deps.get(Web_TYPES.IsGlobalSpellcheckEnabled).execute().getValue() } public getItemTags(item: DecryptedItemInterface) { - return this.items.itemsReferencingItem(item).filter((ref) => { + return this.items.itemsReferencingItem(item).filter((ref) => { return ref.content_type === ContentType.TYPES.Tag - }) as SNTag[] + }) } public get version(): string { @@ -349,15 +303,15 @@ export class WebApplication extends SNApplication implements WebApplicationInter } if (this.protections.getMobileScreenshotPrivacyEnabled()) { - this.mobileDevice().setAndroidScreenshotPrivacy(true) + this.mobileDevice.setAndroidScreenshotPrivacy(true) } else { - this.mobileDevice().setAndroidScreenshotPrivacy(false) + this.mobileDevice.setAndroidScreenshotPrivacy(false) } } async handleMobileLosingFocusEvent(): Promise { if (this.protections.getMobileScreenshotPrivacyEnabled()) { - this.mobileDevice().stopHidingMobileInterfaceFromScreenshots() + this.mobileDevice.stopHidingMobileInterfaceFromScreenshots() } await this.lockApplicationAfterMobileEventIfApplicable() @@ -365,12 +319,20 @@ export class WebApplication extends SNApplication implements WebApplicationInter async handleMobileResumingFromBackgroundEvent(): Promise { if (this.protections.getMobileScreenshotPrivacyEnabled()) { - this.mobileDevice().hideMobileInterfaceFromScreenshots() + this.mobileDevice.hideMobileInterfaceFromScreenshots() } } handleMobileColorSchemeChangeEvent() { - void this.getThemeService().handleMobileColorSchemeChangeEvent() + void this.themeManager.handleMobileColorSchemeChangeEvent() + } + + openSessionsModal = () => { + this.isSessionsModalVisible = true + } + + closeSessionsModal = () => { + this.isSessionsModalVisible = false } handleMobileKeyboardWillChangeFrameEvent(frame: { @@ -392,14 +354,14 @@ export class WebApplication extends SNApplication implements WebApplicationInter } handleReceivedFileEvent(file: { name: string; mimeType: string; data: string }): void { - const filesController = this.controllers.filesController + const filesController = this.filesController const blob = getBlobFromBase64(file.data, file.mimeType) const mappedFile = new File([blob], file.name, { type: file.mimeType }) filesController.uploadNewFile(mappedFile, true).catch(console.error) } async handleReceivedTextEvent({ text, title }: { text: string; title?: string | undefined }) { - const titleForNote = title || this.controllers.itemListController.titleForNewNote() + const titleForNote = title || this.itemListController.titleForNewNote() const note = this.items.createTemplateItem(ContentType.TYPES.Note, { title: titleForNote, @@ -409,7 +371,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter const insertedNote = await this.mutator.insertItem(note) - this.controllers.selectionController.selectItem(insertedNote.uuid, true).catch(console.error) + this.itemListController.selectItem(insertedNote.uuid, true).catch(console.error) addToast({ type: ToastType.Success, @@ -437,7 +399,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter const file = new File([imgBlob], finalPath, { type: imgBlob.type, }) - this.controllers.filesController.uploadNewFile(file, true).catch(console.error) + this.filesController.uploadNewFile(file, true).catch(console.error) } catch (error) { console.error(error) } finally { @@ -453,7 +415,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter } private async lockApplicationAfterMobileEventIfApplicable(): Promise { - const isLocked = await this.isLocked() + const isLocked = await this.protections.isLocked() if (isLocked) { return } @@ -469,7 +431,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter if (passcodeLockImmediately) { await this.lock() } else if (biometricsLockImmediately) { - this.softLockBiometrics() + this.protections.softLockBiometrics() } } @@ -494,7 +456,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter isAuthorizedToRenderItem(item: DecryptedItem): boolean { if (item.protected && this.hasProtectionSources()) { - return this.hasUnprotectedAccessSession() + return this.protections.hasUnprotectedAccessSession() } return true @@ -505,19 +467,19 @@ export class WebApplication extends SNApplication implements WebApplicationInter } get entitledToFiles(): boolean { - return this.controllers.featuresController.entitledToFiles + return this.featuresController.entitledToFiles } showPremiumModal(featureName?: FeatureName): void { - void this.controllers.featuresController.showPremiumAlert(featureName) + void this.featuresController.showPremiumAlert(featureName) } hasValidFirstPartySubscription(): boolean { - return this.controllers.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription + return this.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription } async openPurchaseFlow() { - await this.controllers.purchaseFlowController.openPurchaseFlow() + await this.purchaseFlowController.openPurchaseFlow() } addNativeMobileEventListener = (listener: NativeMobileEventListener) => { @@ -529,11 +491,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter } showAccountMenu(): void { - this.controllers.accountMenuController.setShow(true) + this.accountMenuController.setShow(true) } hideAccountMenu(): void { - this.controllers.accountMenuController.setShow(false) + this.accountMenuController.setShow(false) } /** @@ -545,13 +507,158 @@ export class WebApplication extends SNApplication implements WebApplicationInter } openPreferences(pane?: PreferenceId): void { - this.controllers.preferencesController.openPreferences() + this.preferencesController.openPreferences() if (pane) { - this.controllers.preferencesController.setCurrentPane(pane) + this.preferencesController.setCurrentPane(pane) } } generateUUID(): string { return this.options.crypto.generateUUID() } + + /** + * Dependency + * Accessors + */ + + get routeService(): RouteServiceInterface { + return this.deps.get(Web_TYPES.RouteService) + } + + get androidBackHandler(): AndroidBackHandler { + return this.deps.get(Web_TYPES.AndroidBackHandler) + } + + get vaultDisplayService(): VaultDisplayServiceInterface { + return this.deps.get(Web_TYPES.VaultDisplayService) + } + + get desktopManager(): DesktopManagerInterface | undefined { + return this.deps.get(Web_TYPES.DesktopManager) + } + + get autolockService(): AutolockService | undefined { + return this.deps.get(Web_TYPES.AutolockService) + } + + get archiveService(): ArchiveManager { + return this.deps.get(Web_TYPES.ArchiveManager) + } + + get paneController(): PaneController { + return this.deps.get(Web_TYPES.PaneController) + } + + get linkingController(): LinkingController { + return this.deps.get(Web_TYPES.LinkingController) + } + + get changelogService(): ChangelogService { + return this.deps.get(Web_TYPES.ChangelogService) + } + + get momentsService(): MomentsService { + return this.deps.get(Web_TYPES.MomentsService) + } + + get themeManager(): ThemeManager { + return this.deps.get(Web_TYPES.ThemeManager) + } + + get keyboardService(): KeyboardService { + return this.deps.get(Web_TYPES.KeyboardService) + } + + get featuresController(): FeaturesController { + return this.deps.get(Web_TYPES.FeaturesController) + } + + get filesController(): FilesController { + return this.deps.get(Web_TYPES.FilesController) + } + + get filePreviewModalController(): FilePreviewModalController { + return this.deps.get(Web_TYPES.FilePreviewModalController) + } + + get notesController(): NotesController { + return this.deps.get(Web_TYPES.NotesController) + } + + get importModalController(): ImportModalController { + return this.deps.get(Web_TYPES.ImportModalController) + } + + get navigationController(): NavigationController { + return this.deps.get(Web_TYPES.NavigationController) + } + + get historyModalController(): HistoryModalController { + return this.deps.get(Web_TYPES.HistoryModalController) + } + + get syncStatusController(): SyncStatusController { + return this.deps.get(Web_TYPES.SyncStatusController) + } + + get itemListController(): ItemListController { + return this.deps.get(Web_TYPES.ItemListController) + } + + get importer(): Importer { + return this.deps.get(Web_TYPES.Importer) + } + + get subscriptionController(): SubscriptionController { + return this.deps.get(Web_TYPES.SubscriptionController) + } + + get purchaseFlowController(): PurchaseFlowController { + return this.deps.get(Web_TYPES.PurchaseFlowController) + } + + get quickSettingsMenuController(): QuickSettingsController { + return this.deps.get(Web_TYPES.QuickSettingsController) + } + + get persistence(): PersistenceService { + return this.deps.get(Web_TYPES.PersistenceService) + } + + get itemControllerGroup(): ItemGroupController { + return this.deps.get(Web_TYPES.ItemGroupController) + } + + get noAccountWarningController(): NoAccountWarningController { + return this.deps.get(Web_TYPES.NoAccountWarningController) + } + + get searchOptionsController(): SearchOptionsController { + return this.deps.get(Web_TYPES.SearchOptionsController) + } + + get vaultSelectionController(): VaultSelectionMenuController { + return this.deps.get(Web_TYPES.VaultSelectionMenuController) + } + + get openSubscriptionDashboard(): OpenSubscriptionDashboard { + return this.deps.get(Web_TYPES.OpenSubscriptionDashboard) + } + + get mobileWebReceiver(): MobileWebReceiver | undefined { + return this.deps.get(Web_TYPES.MobileWebReceiver) + } + + get accountMenuController(): AccountMenuController { + return this.deps.get(Web_TYPES.AccountMenuController) + } + + get preferencesController(): PreferencesController { + return this.deps.get(Web_TYPES.PreferencesController) + } + + get isNativeMobileWebUseCase(): IsNativeMobileWeb { + return this.deps.get(Web_TYPES.IsNativeMobileWeb) + } } diff --git a/packages/web/src/javascripts/Application/WebApplicationGroup.ts b/packages/web/src/javascripts/Application/WebApplicationGroup.ts index 883409f65..1ec278a0e 100644 --- a/packages/web/src/javascripts/Application/WebApplicationGroup.ts +++ b/packages/web/src/javascripts/Application/WebApplicationGroup.ts @@ -43,7 +43,7 @@ export class WebApplicationGroup extends SNApplicationGroup }) if (isDesktopApplication()) { - window.webClient = (this.primaryApplication as WebApplication).getDesktopService() + window.webClient = (this.primaryApplication as WebApplication).desktopManager } } diff --git a/packages/web/src/javascripts/Application/WebServices.ts b/packages/web/src/javascripts/Application/WebServices.ts deleted file mode 100644 index c47c3c0ba..000000000 --- a/packages/web/src/javascripts/Application/WebServices.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' -import { DesktopManager } from './Device/DesktopManager' -import { - ArchiveManager, - AutolockService, - ChangelogServiceInterface, - KeyboardService, - ThemeManager, - VaultDisplayServiceInterface, -} from '@standardnotes/ui-services' -import { MomentsService } from '@/Controllers/Moments/MomentsService' - -export type WebServices = { - viewControllerManager: ViewControllerManager - desktopService?: DesktopManager - autolockService?: AutolockService - archiveService: ArchiveManager - themeService: ThemeManager - keyboardService: KeyboardService - changelogService: ChangelogServiceInterface - momentsService: MomentsService - vaultDisplayService: VaultDisplayServiceInterface -} diff --git a/packages/web/src/javascripts/Components/Abstract/PureComponent.tsx b/packages/web/src/javascripts/Components/Abstract/PureComponent.tsx index f00b1c3a1..ca1c0b409 100644 --- a/packages/web/src/javascripts/Components/Abstract/PureComponent.tsx +++ b/packages/web/src/javascripts/Components/Abstract/PureComponent.tsx @@ -1,8 +1,7 @@ import { ApplicationEvent } from '@standardnotes/snjs' -import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { autorun, IReactionDisposer, IReactionPublic } from 'mobx' import { Component } from 'react' +import { WebApplication } from '@/Application/WebApplication' export type PureComponentState = Partial> export type PureComponentProps = Partial> @@ -13,7 +12,7 @@ export abstract class AbstractComponent

void): void { this.reactionDisposers.push(autorun(view)) } diff --git a/packages/web/src/javascripts/Components/AccountMenu/AccountMenu.tsx b/packages/web/src/javascripts/Components/AccountMenu/AccountMenu.tsx index ec513a105..f16d8d0b2 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/AccountMenu.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/AccountMenu.tsx @@ -1,35 +1,30 @@ import { observer } from 'mobx-react-lite' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' -import { WebApplication } from '@/Application/WebApplication' import { useCallback, FunctionComponent, KeyboardEventHandler } from 'react' import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { AccountMenuPane } from './AccountMenuPane' import MenuPaneSelector from './MenuPaneSelector' import { KeyboardKey } from '@standardnotes/ui-services' +import { useApplication } from '../ApplicationProvider' export type AccountMenuProps = { - viewControllerManager: ViewControllerManager - application: WebApplication onClickOutside: () => void mainApplicationGroup: WebApplicationGroup } -const AccountMenu: FunctionComponent = ({ - application, - viewControllerManager, - mainApplicationGroup, -}) => { - const { currentPane } = viewControllerManager.accountMenuController +const AccountMenu: FunctionComponent = ({ mainApplicationGroup }) => { + const application = useApplication() + + const { currentPane } = application.accountMenuController const closeAccountMenu = useCallback(() => { - viewControllerManager.accountMenuController.closeAccountMenu() - }, [viewControllerManager]) + application.accountMenuController.closeAccountMenu() + }, [application]) const setCurrentPane = useCallback( (pane: AccountMenuPane) => { - viewControllerManager.accountMenuController.setCurrentPane(pane) + application.accountMenuController.setCurrentPane(pane) }, - [viewControllerManager], + [application], ) const handleKeyDown: KeyboardEventHandler = useCallback( @@ -50,8 +45,6 @@ const AccountMenu: FunctionComponent = ({ return (

void onStrictSignInChange?: (isStrictSignIn: boolean) => void @@ -17,15 +14,15 @@ type Props = { } const AdvancedOptions: FunctionComponent = ({ - viewControllerManager, - application, disabled = false, onPrivateUsernameModeChange, onStrictSignInChange, onRecoveryCodesChange, children, }) => { - const { server, setServer, enableServerOption, setEnableServerOption } = viewControllerManager.accountMenuController + const application = useApplication() + + const { server, setServer, enableServerOption, setEnableServerOption } = application.accountMenuController const [showAdvanced, setShowAdvanced] = useState(false) const [isPrivateUsername, setIsPrivateUsername] = useState(false) diff --git a/packages/web/src/javascripts/Components/AccountMenu/ConfirmPassword.tsx b/packages/web/src/javascripts/Components/AccountMenu/ConfirmPassword.tsx index b0f73ec6b..9d6f03fda 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/ConfirmPassword.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/ConfirmPassword.tsx @@ -1,6 +1,4 @@ import { STRING_NON_MATCHING_PASSWORDS } from '@/Constants/Strings' -import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import { FormEventHandler, @@ -17,23 +15,18 @@ import Checkbox from '@/Components/Checkbox/Checkbox' import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput' import Icon from '@/Components/Icon/Icon' import IconButton from '@/Components/Button/IconButton' +import { useApplication } from '../ApplicationProvider' type Props = { - viewControllerManager: ViewControllerManager - application: WebApplication setMenuPane: (pane: AccountMenuPane) => void email: string password: string } -const ConfirmPassword: FunctionComponent = ({ - application, - viewControllerManager, - setMenuPane, - email, - password, -}) => { - const { notesAndTagsCount } = viewControllerManager.accountMenuController +const ConfirmPassword: FunctionComponent = ({ setMenuPane, email, password }) => { + const application = useApplication() + + const { notesAndTagsCount } = application.accountMenuController const [confirmPassword, setConfirmPassword] = useState('') const [isRegistering, setIsRegistering] = useState(false) const [isEphemeral, setIsEphemeral] = useState(false) @@ -72,8 +65,8 @@ const ConfirmPassword: FunctionComponent = ({ application .register(email, password, isEphemeral, shouldMergeLocal) .then(() => { - viewControllerManager.accountMenuController.closeAccountMenu() - viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu) + application.accountMenuController.closeAccountMenu() + application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu) }) .catch((err) => { console.error(err) @@ -88,7 +81,7 @@ const ConfirmPassword: FunctionComponent = ({ passwordInputRef.current?.focus() } }, - [viewControllerManager, application, confirmPassword, email, isEphemeral, password, shouldMergeLocal], + [application, confirmPassword, email, isEphemeral, password, shouldMergeLocal], ) const handleKeyDown: KeyboardEventHandler = useCallback( diff --git a/packages/web/src/javascripts/Components/AccountMenu/CreateAccount.tsx b/packages/web/src/javascripts/Components/AccountMenu/CreateAccount.tsx index 2e1a0f964..caa8cc610 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/CreateAccount.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/CreateAccount.tsx @@ -1,5 +1,3 @@ -import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import { FormEventHandler, @@ -20,8 +18,6 @@ import AdvancedOptions from './AdvancedOptions' import HorizontalSeparator from '../Shared/HorizontalSeparator' type Props = { - viewControllerManager: ViewControllerManager - application: WebApplication setMenuPane: (pane: AccountMenuPane) => void email: string setEmail: React.Dispatch> @@ -29,15 +25,7 @@ type Props = { setPassword: React.Dispatch> } -const CreateAccount: FunctionComponent = ({ - viewControllerManager, - application, - setMenuPane, - email, - setEmail, - password, - setPassword, -}) => { +const CreateAccount: FunctionComponent = ({ setMenuPane, email, setEmail, password, setPassword }) => { const emailInputRef = useRef(null) const passwordInputRef = useRef(null) const [isPrivateUsername, setIsPrivateUsername] = useState(false) @@ -145,11 +133,7 @@ const CreateAccount: FunctionComponent = ({ )} - {shouldShowWorkspaceSwitcher && ( - - )} + {shouldShowWorkspaceSwitcher && }
diff --git a/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx b/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx index 729edb23e..14d2ae486 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx @@ -39,7 +39,7 @@ const ChallengeModalPrompt: FunctionComponent = ({ const activatePrompt = useCallback(async () => { if (prompt.validation === ChallengeValidation.Biometric) { if (application.isNativeMobileWeb()) { - const appState = await application.mobileDevice().getAppState() + const appState = await application.mobileDevice.getAppState() if (appState !== 'active') { return diff --git a/packages/web/src/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx b/packages/web/src/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx index bd53957fb..ca1366053 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/LockscreenWorkspaceSwitcher.tsx @@ -1,5 +1,4 @@ import { WebApplicationGroup } from '@/Application/WebApplicationGroup' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { FunctionComponent, useCallback, useRef, useState } from 'react' import WorkspaceSwitcherMenu from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu' import Button from '@/Components/Button/Button' @@ -8,10 +7,9 @@ import Popover from '../Popover/Popover' type Props = { mainApplicationGroup: WebApplicationGroup - viewControllerManager: ViewControllerManager } -const LockscreenWorkspaceSwitcher: FunctionComponent = ({ mainApplicationGroup, viewControllerManager }) => { +const LockscreenWorkspaceSwitcher: FunctionComponent = ({ mainApplicationGroup }) => { const buttonRef = useRef(null) const containerRef = useRef(null) const [isOpen, setIsOpen] = useState(false) @@ -38,7 +36,6 @@ const LockscreenWorkspaceSwitcher: FunctionComponent = ({ mainApplication > diff --git a/packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx b/packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx index f939cd42a..9177c7080 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx @@ -3,13 +3,13 @@ import { ChallengePrompt } from '@standardnotes/services' import { RefObject, useState } from 'react' import { WebApplication } from '@/Application/WebApplication' -import { isAndroid } from '@/Utils' import Button from '../Button/Button' import Icon from '../Icon/Icon' import { InputValue } from './InputValue' import U2FPromptIframeContainer from './U2FPromptIframeContainer' +import { isAndroid } from '@standardnotes/ui-services' type Props = { application: WebApplication @@ -27,7 +27,7 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData return ( { onValueChange(response, prompt) }} @@ -60,9 +60,9 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData } const authenticatorOptions = authenticatorOptionsOrError.getValue() - authenticatorResponse = await application - .mobileDevice() - .authenticateWithU2F(JSON.stringify(authenticatorOptions)) + authenticatorResponse = await application.mobileDevice.authenticateWithU2F( + JSON.stringify(authenticatorOptions), + ) } else { const authenticatorResponseOrError = await application.getAuthenticatorAuthenticationResponse.execute({ username: username.value, diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx index d3c38019c..1835b4367 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx @@ -1,4 +1,3 @@ -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import ChangeEditorMenu from './ChangeEditorMenu' @@ -11,19 +10,14 @@ import { NoteViewController } from '../NoteView/Controller/NoteViewController' import { NoteType, noteTypeForEditorIdentifier } from '@standardnotes/snjs' type Props = { - viewControllerManager: ViewControllerManager noteViewController?: NoteViewController onClickPreprocessing?: () => Promise } -const ChangeEditorButton: FunctionComponent = ({ - viewControllerManager, - noteViewController, - onClickPreprocessing, -}: Props) => { +const ChangeEditorButton: FunctionComponent = ({ noteViewController, onClickPreprocessing }: Props) => { const application = useApplication() - const note = viewControllerManager.notesController.firstSelectedNote + const note = application.notesController.firstSelectedNote const [isOpen, setIsOpen] = useState(false) const buttonRef = useRef(null) const containerRef = useRef(null) diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index 75cabc46a..615498256 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -85,14 +85,14 @@ const ChangeEditorMenu: FunctionComponent = ({ const selectComponent = useCallback( async (uiFeature: UIFeature, note: SNNote) => { if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) { - void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => { + void application.changeAndSaveItem.execute(uiFeature.asComponent, (mutator) => { mutator.conflictOf = undefined }) } - await application.controllers.itemListController.insertCurrentIfTemplate() + await application.itemListController.insertCurrentIfTemplate() - await application.changeAndSaveItem(note, (mutator) => { + await application.changeAndSaveItem.execute(note, (mutator) => { const noteMutator = mutator as NoteMutator noteMutator.noteType = uiFeature.noteType noteMutator.editorIdentifier = uiFeature.featureIdentifier diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMultipleMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMultipleMenu.tsx index 5aeaf7268..d4b829525 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMultipleMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMultipleMenu.tsx @@ -42,12 +42,12 @@ const ChangeEditorMultipleMenu = ({ application, notes, setDisableClickOutside } const selectComponent = useCallback( async (uiFeature: UIFeature, note: SNNote) => { if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) { - void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => { + void application.changeAndSaveItem.execute(uiFeature.asComponent, (mutator) => { mutator.conflictOf = undefined }) } - await application.changeAndSaveItem(note, (mutator) => { + await application.changeAndSaveItem.execute(note, (mutator) => { const noteMutator = mutator as NoteMutator noteMutator.noteType = uiFeature.noteType noteMutator.editorIdentifier = uiFeature.featureIdentifier diff --git a/packages/web/src/javascripts/Components/ClipperView/ClippedNoteView.tsx b/packages/web/src/javascripts/Components/ClipperView/ClippedNoteView.tsx index 899859aba..39e5fa50d 100644 --- a/packages/web/src/javascripts/Components/ClipperView/ClippedNoteView.tsx +++ b/packages/web/src/javascripts/Components/ClipperView/ClippedNoteView.tsx @@ -24,7 +24,17 @@ const ClippedNoteView = ({ }) => { const application = useApplication() - const syncController = useRef(new NoteSyncController(application, note)) + const syncController = useRef( + new NoteSyncController( + note, + application.items, + application.mutator, + application.sessions, + application.sync, + application.alerts, + application.isNativeMobileWebUseCase, + ), + ) useEffect(() => { const currentController = syncController.current return () => { diff --git a/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx b/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx index 197e2fb1a..198232e07 100644 --- a/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx +++ b/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx @@ -1,5 +1,4 @@ import { WebApplicationGroup } from '@/Application/WebApplicationGroup' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { useCallback, useEffect, useState } from 'react' import { AccountMenuPane } from '../AccountMenu/AccountMenuPane' import MenuPaneSelector from '../AccountMenu/MenuPaneSelector' @@ -29,7 +28,7 @@ import { getSuperJSONFromClipPayload } from './getSuperJSONFromClipHTML' import ClippedNoteView from './ClippedNoteView' import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon' import Button from '../Button/Button' -import { openSubscriptionDashboard } from '@/Utils/ManageSubscription' + import { useStateRef } from '@/Hooks/useStateRef' import usePreference from '@/Hooks/usePreference' import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem' @@ -39,13 +38,7 @@ import StyledTooltip from '../StyledTooltip/StyledTooltip' import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem' import Spinner from '../Spinner/Spinner' -const ClipperView = ({ - viewControllerManager, - applicationGroup, -}: { - viewControllerManager: ViewControllerManager - applicationGroup: WebApplicationGroup -}) => { +const ClipperView = ({ applicationGroup }: { applicationGroup: WebApplicationGroup }) => { const application = useApplication() const [currentWindow, setCurrentWindow] = useState>>() @@ -61,7 +54,7 @@ const ClipperView = ({ }, []) const isFirefoxPopup = !!currentWindow && currentWindow.type === 'popup' && currentWindow.incognito === false - const [user, setUser] = useState(() => application.getUser()) + const [user, setUser] = useState(() => application.sessions.getUser()) const [isSyncing, setIsSyncing] = useState(false) const [hasSyncError, setHasSyncError] = useState(false) useEffect(() => { @@ -81,7 +74,7 @@ const ClipperView = ({ case ApplicationEvent.SignedIn: case ApplicationEvent.SignedOut: case ApplicationEvent.UserRolesChanged: - setUser(application.getUser()) + setUser(application.sessions.getUser()) setIsEntitled( application.features.getFeatureStatus( NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(), @@ -223,7 +216,7 @@ const ClipperView = ({ type: 'image/png', }) - const uploadedFile = await viewControllerManager.filesController.uploadNewFile(file).catch(console.error) + const uploadedFile = await application.filesController.uploadNewFile(file).catch(console.error) if (uploadedFile && defaultTagRef.current) { await application.linkingController.linkItems(uploadedFile, defaultTagRef.current) @@ -269,6 +262,7 @@ const ClipperView = ({ createNoteFromClip().catch(console.error) }, [ + application.filesController, application.items, application.linkingController, application.mutator, @@ -276,12 +270,11 @@ const ClipperView = ({ clipPayload, defaultTagRef, isEntitledRef, - viewControllerManager.filesController, ]) const upgradePlan = useCallback(async () => { if (hasSubscription) { - await openSubscriptionDashboard(application) + await application.openSubscriptionDashboard.execute() } else { await application.openPurchaseFlow() } @@ -317,7 +310,7 @@ const ClipperView = ({ @@ -328,8 +321,6 @@ const ClipperView = ({ return menuPane ? (
= ({ onLoad, componentViewer, application.keyboardService.handleComponentKeyUp(data.keyboardModifier) break case ComponentAction.Click: - application.controllers.notesController.setContextMenuOpen(false) + application.notesController.setContextMenuOpen(false) break default: return @@ -165,13 +165,13 @@ const IframeFeatureView: FunctionComponent = ({ onLoad, componentViewer, }, [componentViewer, application]) useEffect(() => { - const unregisterDesktopObserver = application - .getDesktopService() - ?.registerUpdateObserver((updatedComponent: ComponentInterface) => { + const unregisterDesktopObserver = application.desktopManager?.registerUpdateObserver( + (updatedComponent: ComponentInterface) => { if (updatedComponent.uuid === uiFeature.uniqueIdentifier.value) { requestReload?.(componentViewer) } - }) + }, + ) return () => { unregisterDesktopObserver?.() diff --git a/packages/web/src/javascripts/Components/ComponentView/NotEntitledBanner.tsx b/packages/web/src/javascripts/Components/ComponentView/NotEntitledBanner.tsx index de8d563ab..42569ca35 100644 --- a/packages/web/src/javascripts/Components/ComponentView/NotEntitledBanner.tsx +++ b/packages/web/src/javascripts/Components/ComponentView/NotEntitledBanner.tsx @@ -3,7 +3,6 @@ import { FunctionComponent, useCallback } from 'react' import Button from '@/Components/Button/Button' import { WarningCircle } from '../UIElements/WarningCircle' import { useApplication } from '../ApplicationProvider' -import { openSubscriptionDashboard } from '@/Utils/ManageSubscription' type Props = { feature: AnyFeatureDescription @@ -33,7 +32,7 @@ const NotEntitledBanner: FunctionComponent = ({ featureStatus, feature }) const expiredDate = application.subscriptions.userSubscriptionExpirationDate const manageSubscription = useCallback(() => { - void openSubscriptionDashboard(application) + void application.openSubscriptionDashboard.execute() }, [application]) return ( diff --git a/packages/web/src/javascripts/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal.tsx b/packages/web/src/javascripts/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal.tsx index d9d89d5e1..3756a0f1a 100644 --- a/packages/web/src/javascripts/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal.tsx +++ b/packages/web/src/javascripts/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal.tsx @@ -1,5 +1,4 @@ import { observer } from 'mobx-react-lite' -import { ViewControllerManager } from '@Controllers/ViewControllerManager' import { useCallback, useRef } from 'react' import { STRING_DELETE_ACCOUNT_CONFIRMATION } from '@/Constants/Strings' import Button from '@/Components/Button/Button' @@ -8,14 +7,13 @@ import Icon from '../Icon/Icon' import AlertDialog from '../AlertDialog/AlertDialog' type Props = { - viewControllerManager: ViewControllerManager application: WebApplication } -const ConfirmDeleteAccountModal = ({ application, viewControllerManager }: Props) => { +const ConfirmDeleteAccountModal = ({ application }: Props) => { const closeDialog = useCallback(() => { - viewControllerManager.accountMenuController.setDeletingAccount(false) - }, [viewControllerManager.accountMenuController]) + application.accountMenuController.setDeletingAccount(false) + }, [application.accountMenuController]) const cancelRef = useRef(null) @@ -52,7 +50,7 @@ const ConfirmDeleteAccountModal = ({ application, viewControllerManager }: Props ConfirmDeleteAccountModal.displayName = 'ConfirmDeleteAccountModal' const ConfirmDeleteAccountContainer = (props: Props) => { - if (!props.viewControllerManager.accountMenuController.deletingAccount) { + if (!props.application.accountMenuController.deletingAccount) { return null } return diff --git a/packages/web/src/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx b/packages/web/src/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx index 65abfb186..bd75a7a80 100644 --- a/packages/web/src/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx +++ b/packages/web/src/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx @@ -1,7 +1,6 @@ import { FunctionComponent, useCallback, useRef } from 'react' import { STRING_SIGN_OUT_CONFIRMATION } from '@/Constants/Strings' import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { isDesktopApplication } from '@/Utils' @@ -12,11 +11,10 @@ import HorizontalSeparator from '../Shared/HorizontalSeparator' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager applicationGroup: WebApplicationGroup } -const ConfirmSignoutModal: FunctionComponent = ({ application, viewControllerManager, applicationGroup }) => { +const ConfirmSignoutModal: FunctionComponent = ({ application, applicationGroup }) => { const hasAnyBackupsEnabled = application.fileBackups?.isFilesBackupsEnabled() || application.fileBackups?.isPlaintextBackupsEnabled() || @@ -24,8 +22,8 @@ const ConfirmSignoutModal: FunctionComponent = ({ application, viewContro const cancelRef = useRef(null) const closeDialog = useCallback(() => { - viewControllerManager.accountMenuController.setSigningOut(false) - }, [viewControllerManager.accountMenuController]) + application.accountMenuController.setSigningOut(false) + }, [application.accountMenuController]) const workspaces = applicationGroup.getDescriptors() const showWorkspaceWarning = workspaces.length > 1 && isDesktopApplication() @@ -98,7 +96,7 @@ const ConfirmSignoutModal: FunctionComponent = ({ application, viewContro ConfirmSignoutModal.displayName = 'ConfirmSignoutModal' const ConfirmSignoutContainer = (props: Props) => { - if (!props.viewControllerManager.accountMenuController.signingOut) { + if (!props.application.accountMenuController.signingOut) { return null } return diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx index e27532d22..24e11bf57 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx @@ -5,39 +5,22 @@ import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants' import { ListableContentItem } from './Types/ListableContentItem' import ContentListItem from './ContentListItem' -import { ItemListController } from '@/Controllers/ItemList/ItemListController' -import { FilesController } from '@/Controllers/FilesController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { NotesController } from '@/Controllers/NotesController/NotesController' import { ElementIds } from '@/Constants/ElementIDs' import { classNames } from '@standardnotes/utils' import { ContentType, SNTag } from '@standardnotes/snjs' +import { ItemListController } from '@/Controllers/ItemList/ItemListController' type Props = { application: WebApplication - filesController: FilesController - itemListController: ItemListController items: ListableContentItem[] - navigationController: NavigationController - notesController: NotesController - selectionController: SelectedItemsController - selectedUuids: SelectedItemsController['selectedUuids'] + selectedUuids: ItemListController['selectedUuids'] paginate: () => void } -const ContentList: FunctionComponent = ({ - application, - filesController, - itemListController, - items, - navigationController, - notesController, - selectionController, - selectedUuids, - paginate, -}) => { - const { selectPreviousItem, selectNextItem } = selectionController +const ContentList: FunctionComponent = ({ application, items, selectedUuids, paginate }) => { + const { filesController, itemListController, navigationController, notesController } = application + + const { selectPreviousItem, selectNextItem } = itemListController const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = itemListController.webDisplayOptions const { sortBy } = itemListController.displayOptions const selectedTag = navigationController.selected @@ -68,9 +51,9 @@ const ContentList: FunctionComponent = ({ const selectItem = useCallback( (item: ListableContentItem, userTriggered?: boolean) => { - return selectionController.selectItem(item.uuid, userTriggered) + return itemListController.selectItem(item.uuid, userTriggered) }, - [selectionController], + [itemListController], ) const getTagsForItem = useCallback( diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index 5d362b6bb..55e6100af 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -14,22 +14,13 @@ import { observer } from 'mobx-react-lite' import { forwardRef, useCallback, useEffect, useMemo } from 'react' import ContentList from '@/Components/ContentListView/ContentList' import NoAccountWarning from '@/Components/NoAccountWarning/NoAccountWarning' -import { ItemListController } from '@/Controllers/ItemList/ItemListController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { FilesController } from '@/Controllers/FilesController' -import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController' -import { NotesController } from '@/Controllers/NotesController/NotesController' -import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' import { ElementIds } from '@/Constants/ElementIDs' import ContentListHeader from './Header/ContentListHeader' import { AppPaneId } from '../Panes/AppPaneMetadata' import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider' import SearchBar from '../SearchBar/SearchBar' -import { SearchOptionsController } from '@/Controllers/SearchOptionsController' import { classNames } from '@standardnotes/utils' import { useFileDragNDrop } from '../FileDragNDropProvider' -import { LinkingController } from '@/Controllers/LinkingController' import DailyContentList from './Daily/DailyContentList' import { ListableContentItem } from './Types/ListableContentItem' import { FeatureName } from '@/Controllers/FeatureName' @@ -37,29 +28,14 @@ import { PanelResizedData } from '@/Types/PanelResizedData' import { useForwardedRef } from '@/Hooks/useForwardedRef' import FloatingAddButton from './FloatingAddButton' import ContentTableView from '../ContentTableView/ContentTableView' -import { FeaturesController } from '@/Controllers/FeaturesController' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' -import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' -import { PaneController } from '@/Controllers/PaneController/PaneController' import EmptyFilesView from './EmptyFilesView' import { PaneLayout } from '@/Controllers/PaneController/PaneLayout' import { usePaneSwipeGesture } from '../Panes/usePaneGesture' import { mergeRefs } from '@/Hooks/mergeRefs' type Props = { - accountMenuController: AccountMenuController application: WebApplication - filesController: FilesController - itemListController: ItemListController - navigationController: NavigationController - noAccountWarningController: NoAccountWarningController - notesController: NotesController - selectionController: SelectedItemsController - searchOptionsController: SearchOptionsController - linkingController: LinkingController - featuresController: FeaturesController - historyModalController: HistoryModalController - paneController: PaneController className?: string id: string children?: React.ReactNode @@ -67,30 +43,20 @@ type Props = { } const ContentListView = forwardRef( - ( - { + ({ application, className, id, children, onPanelWidthLoad }, ref) => { + const { + paneController, accountMenuController, - application, filesController, itemListController, navigationController, noAccountWarningController, - notesController, - selectionController, searchOptionsController, linkingController, - featuresController, - historyModalController, - paneController, - className, - id, - children, - onPanelWidthLoad, - }, - ref, - ) => { + } = application + const { setPaneLayout, panes } = useResponsiveAppPane() - const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController + const { selected: selectedTag, selectedAsTag } = navigationController const { completedFullSync, @@ -102,6 +68,9 @@ const ContentListView = forwardRef( items, isCurrentNoteTemplate, isTableViewEnabled, + selectedUuids, + selectNextItem, + selectPreviousItem, } = itemListController const innerRef = useForwardedRef(ref) @@ -260,7 +229,7 @@ const ContentListView = forwardRef( } event.preventDefault() - selectionController.selectAll() + itemListController.selectAll() }, }, ]) @@ -268,9 +237,9 @@ const ContentListView = forwardRef( addNewItem, application.keyboardService, createNewNote, + itemListController, selectNextItem, selectPreviousItem, - selectionController, shouldUseTableView, ]) @@ -289,13 +258,13 @@ const ContentListView = forwardRef( const handleDailyListSelection = useCallback( async (item: ListableContentItem, userTriggered: boolean) => { - await selectionController.selectItemWithScrollHandling(item, { + await itemListController.selectItemWithScrollHandling(item, { userTriggered: true, scrollIntoView: userTriggered === false, animated: false, }) }, - [selectionController], + [itemListController], ) useEffect(() => { @@ -371,29 +340,13 @@ const ContentListView = forwardRef( ) : null} {!dailyMode && renderedItems.length ? ( shouldUseTableView ? ( - + ) : ( ) ) : null} diff --git a/packages/web/src/javascripts/Components/ContentListView/Daily/DailyContentList.tsx b/packages/web/src/javascripts/Components/ContentListView/Daily/DailyContentList.tsx index 79264c022..3ffdc1bd6 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Daily/DailyContentList.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Daily/DailyContentList.tsx @@ -1,7 +1,6 @@ import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { ListableContentItem } from '../Types/ListableContentItem' import { ItemListController } from '@/Controllers/ItemList/ItemListController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { useResponsiveAppPane } from '../../Panes/ResponsivePaneProvider' import { AppPaneId } from '../../Panes/AppPaneMetadata' import { @@ -25,7 +24,7 @@ type Props = { items: ListableContentItem[] onSelect: (item: ListableContentItem, userTriggered: boolean) => Promise selectedTag: SNTag - selectedUuids: SelectedItemsController['selectedUuids'] + selectedUuids: ItemListController['selectedUuids'] } const PageSize = 10 diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx index aecc71209..1f9dfe688 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx @@ -93,7 +93,7 @@ const DisplayOptionsMenu: FunctionComponent = ({ : selectedTag.preferences const [currentMode, setCurrentMode] = useState(selectedTagPreferences ? 'tag' : 'global') const [preferences, setPreferences] = useState({}) - const hasSubscription = application.controllers.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription + const hasSubscription = application.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription const controlsDisabled = currentMode === 'tag' && !hasSubscription const isDailyEntry = selectedTagPreferences?.entryMode === 'daily' @@ -181,7 +181,7 @@ const DisplayOptionsMenu: FunctionComponent = ({ } else if (isSystemTag) { await changeSystemViewPreferences(properties) } else { - await application.changeAndSaveItem(selectedTag, (mutator) => { + await application.changeAndSaveItem.execute(selectedTag, (mutator) => { mutator.preferences = { ...mutator.preferences, ...properties, @@ -202,7 +202,7 @@ const DisplayOptionsMenu: FunctionComponent = ({ return } - void application.changeAndSaveItem(selectedTag, (mutator) => { + void application.changeAndSaveItem.execute(selectedTag, (mutator) => { mutator.preferences = undefined }) }, [application, isSystemTag, reloadPreferences, selectedTag]) diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx index ceda06beb..00aff6121 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx @@ -231,7 +231,7 @@ const NewNotePreferences: FunctionComponent = ({ onClick={(event) => { if (application.isNativeMobileWeb()) { event.preventDefault() - application.mobileDevice().openUrl(HelpPageUrl) + application.mobileDevice.openUrl(HelpPageUrl) } }} > diff --git a/packages/web/src/javascripts/Components/ContentTableView/ContentTableView.tsx b/packages/web/src/javascripts/Components/ContentTableView/ContentTableView.tsx index 0c151d775..9bf7f8d92 100644 --- a/packages/web/src/javascripts/Components/ContentTableView/ContentTableView.tsx +++ b/packages/web/src/javascripts/Components/ContentTableView/ContentTableView.tsx @@ -1,5 +1,4 @@ import { WebApplication } from '@/Application/WebApplication' -import { FilesController } from '@/Controllers/FilesController' import { formatDateForContextMenu } from '@/Utils/DateUtils' import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType' import { formatSizeToReadableString } from '@standardnotes/filepicker' @@ -28,38 +27,15 @@ import FileMenuOptions from '../FileContextMenu/FileMenuOptions' import Icon from '../Icon/Icon' import LinkedItemBubble from '../LinkedItems/LinkedItemBubble' import LinkedItemsPanel from '../LinkedItems/LinkedItemsPanel' -import { LinkingController } from '@/Controllers/LinkingController' -import { FeaturesController } from '@/Controllers/FeaturesController' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' import { useApplication } from '../ApplicationProvider' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType' import NotesOptions from '../NotesOptions/NotesOptions' -import { NotesController } from '@/Controllers/NotesController/NotesController' -import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' import { useItemLinks } from '@/Hooks/useItemLinks' import { ItemLink } from '@/Utils/Items/Search/ItemLink' -import { ItemListController } from '@/Controllers/ItemList/ItemListController' import ListItemVaultInfo from '../ContentListView/ListItemVaultInfo' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' -const ContextMenuCell = ({ - items, - filesController, - navigationController, - linkingController, - notesController, - historyModalController, - selectionController, -}: { - items: DecryptedItemInterface[] - filesController: FilesController - navigationController: NavigationController - linkingController: LinkingController - notesController: NotesController - historyModalController: HistoryModalController - selectionController: SelectedItemsController -}) => { +const ContextMenuCell = ({ items }: { items: DecryptedItemInterface[] }) => { const [contextMenuVisible, setContextMenuVisible] = useState(false) const anchorElementRef = useRef(null) @@ -102,12 +78,9 @@ const ContextMenuCell = ({ {allItemsAreFiles && ( { setContextMenuVisible(false) }} - filesController={filesController} shouldShowRenameOption={false} shouldShowAttachOption={false} selectedFiles={items as FileItem[]} @@ -116,11 +89,6 @@ const ContextMenuCell = ({ {allItemsAreNotes && ( { setContextMenuVisible(false) }} @@ -132,17 +100,7 @@ const ContextMenuCell = ({ ) } -const ItemLinksCell = ({ - item, - filesController, - linkingController, - featuresController, -}: { - item: DecryptedItemInterface - filesController: FilesController - linkingController: LinkingController - featuresController: FeaturesController -}) => { +const ItemLinksCell = ({ item }: { item: DecryptedItemInterface }) => { const [contextMenuVisible, setContextMenuVisible] = useState(false) const anchorElementRef = useRef(null) @@ -170,13 +128,7 @@ const ItemLinksCell = ({ align="start" className="py-2" > - + ) @@ -263,37 +215,18 @@ const AttachedToCell = ({ item }: { item: DecryptedItemInterface }) => { type Props = { application: WebApplication items: DecryptedItemInterface[] - filesController: FilesController - featuresController: FeaturesController - linkingController: LinkingController - navigationController: NavigationController - notesController: NotesController - historyModalController: HistoryModalController - itemListController: ItemListController - selectionController: SelectedItemsController } -const ContentTableView = ({ - application, - items, - filesController, - featuresController, - linkingController, - navigationController, - notesController, - historyModalController, - itemListController, - selectionController, -}: Props) => { +const ContentTableView = ({ application, items }: Props) => { const listHasFiles = items.some((item) => item instanceof FileItem) - const { sortBy, sortDirection } = itemListController.displayOptions + const { sortBy, sortDirection } = application.itemListController.displayOptions const sortReversed = sortDirection === 'asc' - const { hideDate, hideEditorIcon: hideIcon, hideTags } = itemListController.webDisplayOptions + const { hideDate, hideEditorIcon: hideIcon, hideTags } = application.itemListController.webDisplayOptions const onSortChange = useCallback( async (sortBy: keyof SortableItem, sortReversed: boolean) => { - const selectedTag = navigationController.selected + const selectedTag = application.navigationController.selected if (!selectedTag) { return @@ -320,7 +253,7 @@ const ContentTableView = ({ return } - await application.changeAndSaveItem(selectedTag, (mutator) => { + await application.changeAndSaveItem.execute(selectedTag, (mutator) => { mutator.preferences = { ...mutator.preferences, sortBy, @@ -328,7 +261,7 @@ const ContentTableView = ({ } }) }, - [application, navigationController.selected], + [application], ) const [contextMenuItem, setContextMenuItem] = useState(undefined) @@ -383,7 +316,7 @@ const ContentTableView = ({ enableMultipleRowSelection: true, onRowActivate(item) { if (item instanceof FileItem) { - void filesController.handleFileAction({ + void application.filesController.handleFileAction({ type: FileItemActionType.PreviewFile, payload: { file: item, @@ -399,35 +332,12 @@ const ContentTableView = ({ rowActions: (item) => { return (
- - + +
) }, - selectionActions: (itemIds) => ( - itemIds.includes(item.uuid))} - filesController={filesController} - linkingController={linkingController} - navigationController={navigationController} - notesController={notesController} - historyModalController={historyModalController} - selectionController={selectionController} - /> - ), + selectionActions: (itemIds) => itemIds.includes(item.uuid))} />, showSelectionActions: true, }) @@ -456,26 +366,15 @@ const ContentTableView = ({ )} {contextMenuItem instanceof SNNote && ( - + )} diff --git a/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx b/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx index 6fead8227..078cd288a 100644 --- a/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx +++ b/packages/web/src/javascripts/Components/EditorWidthSelectionModal/EditorWidthSelectionModal.tsx @@ -154,7 +154,7 @@ const EditorWidthSelectionModal = ({ const EditorWidthSelectionModalWrapper = () => { const application = useApplication() - const { notesController } = application.controllers + const { notesController } = application const [isOpen, setIsOpen] = useState(false) const [isGlobal, setIsGlobal] = useState(false) diff --git a/packages/web/src/javascripts/Components/FileContextMenu/FileContextMenu.tsx b/packages/web/src/javascripts/Components/FileContextMenu/FileContextMenu.tsx index 905c4d78e..ccf0cfe9b 100644 --- a/packages/web/src/javascripts/Components/FileContextMenu/FileContextMenu.tsx +++ b/packages/web/src/javascripts/Components/FileContextMenu/FileContextMenu.tsx @@ -1,60 +1,46 @@ import { FilesController } from '@/Controllers/FilesController' -import { LinkingController } from '@/Controllers/LinkingController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'react' import Menu from '../Menu/Menu' import Popover from '../Popover/Popover' import FileMenuOptions from './FileMenuOptions' +import { ItemListController } from '@/Controllers/ItemList/ItemListController' type Props = { filesController: FilesController - selectionController: SelectedItemsController - linkingController: LinkingController - navigationController: NavigationController + itemListController: ItemListController } -const FileContextMenu: FunctionComponent = observer( - ({ filesController, selectionController, linkingController, navigationController }) => { - const { showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = filesController - const { selectedFiles } = selectionController +const FileContextMenu: FunctionComponent = observer(({ filesController, itemListController }) => { + const { showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = filesController + const { selectedFiles } = itemListController - return ( - setShowFileContextMenu(!showFileContextMenu)} - align="start" - className="py-2" - > - - setShowFileContextMenu(false)} - shouldShowRenameOption={false} - shouldShowAttachOption={false} - /> - - - ) - }, -) + return ( + setShowFileContextMenu(!showFileContextMenu)} + align="start" + className="py-2" + > + + setShowFileContextMenu(false)} + shouldShowRenameOption={false} + shouldShowAttachOption={false} + /> + + + ) +}) FileContextMenu.displayName = 'FileContextMenu' -const FileContextMenuWrapper: FunctionComponent = ({ - filesController, - linkingController, - navigationController, - selectionController, -}) => { +const FileContextMenuWrapper: FunctionComponent = ({ filesController, itemListController }) => { const { showFileContextMenu } = filesController - const { selectedFiles } = selectionController + const { selectedFiles } = itemListController const selectedFile = selectedFiles[0] @@ -62,14 +48,7 @@ const FileContextMenuWrapper: FunctionComponent = ({ return null } - return ( - - ) + return } export default observer(FileContextMenuWrapper) diff --git a/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx b/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx index 85ae9fbf8..5cfc99053 100644 --- a/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx +++ b/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx @@ -2,7 +2,6 @@ import { FunctionComponent, useCallback, useMemo } from 'react' import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction' import Icon from '@/Components/Icon/Icon' import { observer } from 'mobx-react-lite' -import { FilesController } from '@/Controllers/FilesController' import HorizontalSeparator from '../Shared/HorizontalSeparator' import { formatSizeToReadableString } from '@standardnotes/filepicker' import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider' @@ -13,17 +12,13 @@ import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem' import { FileItem } from '@standardnotes/snjs' import AddTagOption from '../NotesOptions/AddTagOption' import { MenuItemIconSize } from '@/Constants/TailwindClassNames' -import { LinkingController } from '@/Controllers/LinkingController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption' import { iconClass } from '../NotesOptions/ClassNames' import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' +import { useApplication } from '../ApplicationProvider' type Props = { closeMenu: () => void - filesController: FilesController - linkingController: LinkingController - navigationController: NavigationController isFileAttachedToNote?: boolean renameToggleCallback?: (isRenamingFile: boolean) => void shouldShowRenameOption: boolean @@ -33,16 +28,15 @@ type Props = { const FileMenuOptions: FunctionComponent = ({ closeMenu, - filesController, - linkingController, - navigationController, isFileAttachedToNote, renameToggleCallback, shouldShowRenameOption, shouldShowAttachOption, selectedFiles, }) => { - const { handleFileAction } = filesController + const application = useApplication() + + const { handleFileAction } = application.filesController const { toggleAppPane } = useResponsiveAppPane() const hasProtectedFiles = useMemo(() => selectedFiles.some((file) => file.protected), [selectedFiles]) @@ -95,15 +89,15 @@ const FileMenuOptions: FunctionComponent = ({ )} {featureTrunkVaultsEnabled() && } { - void filesController.setProtectionForFiles(hasProtectedFiles, selectedFiles) + void application.filesController.setProtectionForFiles(hasProtectedFiles, selectedFiles) }} > @@ -112,7 +106,7 @@ const FileMenuOptions: FunctionComponent = ({ { - void filesController.downloadFiles(selectedFiles) + void application.filesController.downloadFiles(selectedFiles) closeMenu() }} > @@ -132,7 +126,7 @@ const FileMenuOptions: FunctionComponent = ({ { closeMenuAndToggleFilesList() - void filesController.deleteFilesPermanently(selectedFiles) + void application.filesController.deleteFilesPermanently(selectedFiles) }} > diff --git a/packages/web/src/javascripts/Components/FileContextMenu/FileOptionsPanel.tsx b/packages/web/src/javascripts/Components/FileContextMenu/FileOptionsPanel.tsx index 71ce62ed6..6982effe0 100644 --- a/packages/web/src/javascripts/Components/FileContextMenu/FileOptionsPanel.tsx +++ b/packages/web/src/javascripts/Components/FileContextMenu/FileOptionsPanel.tsx @@ -1,27 +1,16 @@ import { useCallback, useRef, useState } from 'react' import { observer } from 'mobx-react-lite' import FileMenuOptions from './FileMenuOptions' -import { FilesController } from '@/Controllers/FilesController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import Popover from '../Popover/Popover' import RoundIconButton from '../Button/RoundIconButton' import Menu from '../Menu/Menu' -import { LinkingController } from '@/Controllers/LinkingController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' +import { ItemListController } from '@/Controllers/ItemList/ItemListController' type Props = { - filesController: FilesController - selectionController: SelectedItemsController - linkingController: LinkingController - navigationController: NavigationController + itemListController: ItemListController } -const FilesOptionsPanel = ({ - filesController, - linkingController, - navigationController, - selectionController, -}: Props) => { +const FilesOptionsPanel = ({ itemListController }: Props) => { const [isOpen, setIsOpen] = useState(false) const buttonRef = useRef(null) @@ -39,10 +28,7 @@ const FilesOptionsPanel = ({ > { setIsOpen(false) }} diff --git a/packages/web/src/javascripts/Components/FileDragNDropProvider.tsx b/packages/web/src/javascripts/Components/FileDragNDropProvider.tsx index dacab493c..de2f63fe3 100644 --- a/packages/web/src/javascripts/Components/FileDragNDropProvider.tsx +++ b/packages/web/src/javascripts/Components/FileDragNDropProvider.tsx @@ -1,6 +1,4 @@ import { WebApplication } from '@/Application/WebApplication' -import { FeaturesController } from '@/Controllers/FeaturesController' -import { FilesController } from '@/Controllers/FilesController' import { usePremiumModal } from '@/Hooks/usePremiumModal' import { classNames } from '@standardnotes/utils' import { isHandlingFileDrag } from '@/Utils/DragTypeCheck' @@ -35,8 +33,6 @@ export const useFileDragNDrop = () => { type Props = { application: WebApplication - featuresController: FeaturesController - filesController: FilesController children: ReactNode } @@ -47,7 +43,7 @@ const MemoizedChildren = memo(({ children }: { children: ReactNode }) => { return <>{children} }) -const FileDragNDropProvider = ({ application, children, featuresController, filesController }: Props) => { +const FileDragNDropProvider = ({ application, children }: Props) => { const premiumModal = usePremiumModal() const [isDraggingFiles, setIsDraggingFiles] = useState(false) const [tooltipText, setTooltipText] = useState('') @@ -189,7 +185,7 @@ const FileDragNDropProvider = ({ application, children, featuresController, file resetState() - if (!featuresController.entitledToFiles) { + if (!application.featuresController.entitledToFiles) { premiumModal.activate('Files') return } @@ -204,7 +200,7 @@ const FileDragNDropProvider = ({ application, children, featuresController, file return } - const uploadedFile = await filesController.uploadNewFile(fileOrHandle) + const uploadedFile = await application.filesController.uploadNewFile(fileOrHandle) if (!uploadedFile) { return @@ -218,7 +214,7 @@ const FileDragNDropProvider = ({ application, children, featuresController, file dragCounter.current = 0 } }, - [application, featuresController.entitledToFiles, filesController, premiumModal, resetState], + [application, premiumModal, resetState], ) useEffect(() => { diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx index ff271bae7..26cab51bd 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx @@ -141,7 +141,7 @@ const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLe ) : ( { setDownloadedBytes(undefined) }} diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreviewModal.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreviewModal.tsx index 8bc8be491..e3e6b2dbf 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreviewModal.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreviewModal.tsx @@ -5,7 +5,6 @@ import Icon from '@/Components/Icon/Icon' import FilePreviewInfoPanel from './FilePreviewInfoPanel' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { KeyboardKey } from '@standardnotes/ui-services' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import FilePreview from './FilePreview' import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType' @@ -23,11 +22,10 @@ import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/u type Props = { application: WebApplication - viewControllerManager: ViewControllerManager } -const FilePreviewModal = observer(({ application, viewControllerManager }: Props) => { - const { currentFile, setCurrentFile, otherFiles, dismiss } = viewControllerManager.filePreviewModalController +const FilePreviewModal = observer(({ application }: Props) => { + const { currentFile, setCurrentFile, otherFiles, dismiss } = application.filePreviewModalController const [isRenaming, setIsRenaming] = useState(false) const renameInputRef = useRef(null) @@ -229,9 +227,6 @@ const FilePreviewModal = observer(({ application, viewControllerManager }: Props > {showLinkedBubblesContainer && (
- +
)}
@@ -284,14 +276,14 @@ const FilePreviewModal = observer(({ application, viewControllerManager }: Props FilePreviewModal.displayName = 'FilePreviewModal' -const FilePreviewModalWrapper: FunctionComponent = ({ application, viewControllerManager }) => { +const FilePreviewModalWrapper: FunctionComponent = ({ application }) => { return ( - + ) } diff --git a/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx b/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx index dd3a99577..ebe302fb2 100644 --- a/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx @@ -61,7 +61,7 @@ const PreviewComponent: FunctionComponent = ({ const sanitizedName = sanitizeFileName(name) const filename = `${sanitizedName}.${ext}` - void application.mobileDevice().previewFile(fileBase64, filename) + void application.mobileDevice.previewFile(fileBase64, filename) }, [application, bytes, file.mimeType, file.name, isNativeMobileWeb]) if (isNativeMobileWeb && requiresNativePreview) { diff --git a/packages/web/src/javascripts/Components/FileView/FileView.tsx b/packages/web/src/javascripts/Components/FileView/FileView.tsx index b1d9ae9ca..a8a552fb6 100644 --- a/packages/web/src/javascripts/Components/FileView/FileView.tsx +++ b/packages/web/src/javascripts/Components/FileView/FileView.tsx @@ -5,16 +5,16 @@ import FileViewWithoutProtection from './FileViewWithoutProtection' import { FileViewProps } from './FileViewProps' import { ApplicationEvent } from '@standardnotes/snjs' -const FileView = ({ application, viewControllerManager, file }: FileViewProps) => { +const FileView = ({ application, file }: FileViewProps) => { const [shouldShowProtectedOverlay, setShouldShowProtectedOverlay] = useState(false) useEffect(() => { - viewControllerManager.filesController.setShowProtectedOverlay(!application.isAuthorizedToRenderItem(file)) - }, [application, file, viewControllerManager.filesController]) + application.filesController.setShowProtectedOverlay(!application.isAuthorizedToRenderItem(file)) + }, [application, file, application.filesController]) useEffect(() => { - setShouldShowProtectedOverlay(viewControllerManager.filesController.showProtectedOverlay) - }, [viewControllerManager.filesController.showProtectedOverlay]) + setShouldShowProtectedOverlay(application.filesController.showProtectedOverlay) + }, [application.filesController.showProtectedOverlay]) const dismissProtectedOverlay = useCallback(async () => { let showFileContents = true @@ -48,7 +48,7 @@ const FileView = ({ application, viewControllerManager, file }: FileViewProps) = itemType={'file'} /> ) : ( - + ) } diff --git a/packages/web/src/javascripts/Components/FileView/FileViewProps.tsx b/packages/web/src/javascripts/Components/FileView/FileViewProps.tsx index 90566bee3..d88bad083 100644 --- a/packages/web/src/javascripts/Components/FileView/FileViewProps.tsx +++ b/packages/web/src/javascripts/Components/FileView/FileViewProps.tsx @@ -1,9 +1,7 @@ import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { FileItem } from '@standardnotes/snjs' export type FileViewProps = { application: WebApplication - viewControllerManager: ViewControllerManager file: FileItem } diff --git a/packages/web/src/javascripts/Components/FileView/FileViewWithoutProtection.tsx b/packages/web/src/javascripts/Components/FileView/FileViewWithoutProtection.tsx index 2ea020fa2..d44cf8f27 100644 --- a/packages/web/src/javascripts/Components/FileView/FileViewWithoutProtection.tsx +++ b/packages/web/src/javascripts/Components/FileView/FileViewWithoutProtection.tsx @@ -15,7 +15,7 @@ import RoundIconButton from '../Button/RoundIconButton' const SyncTimeoutNoDebounceMs = 100 const SyncTimeoutDebounceMs = 350 -const FileViewWithoutProtection = ({ application, viewControllerManager, file }: FileViewProps) => { +const FileViewWithoutProtection = ({ application, file }: FileViewProps) => { const syncTimeoutRef = useRef() const fileInfoButtonRef = useRef(null) @@ -30,7 +30,7 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }: clearTimeout(syncTimeoutRef.current) } - const shouldNotDebounce = application.noAccount() + const shouldNotDebounce = application.sessions.isSignedOut() const syncDebounceMs = shouldNotDebounce ? SyncTimeoutNoDebounceMs : SyncTimeoutDebounceMs syncTimeoutRef.current = window.setTimeout(async () => { @@ -52,7 +52,7 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }: addDragTarget(target, { tooltipText: 'Drop your files to upload and link them to the current file', async callback(uploadedFile) { - await viewControllerManager.linkingController.linkItems(uploadedFile, file) + await application.linkingController.linkItems(uploadedFile, file) }, }) } @@ -62,7 +62,7 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }: removeDragTarget(target) } } - }, [addDragTarget, file, removeDragTarget, viewControllerManager.linkingController]) + }, [addDragTarget, file, removeDragTarget, application.linkingController]) return (
@@ -89,11 +89,7 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }:
- + - +
- +
diff --git a/packages/web/src/javascripts/Components/Footer/AccountMenuButton.tsx b/packages/web/src/javascripts/Components/Footer/AccountMenuButton.tsx index 09c71a204..eb918525d 100644 --- a/packages/web/src/javascripts/Components/Footer/AccountMenuButton.tsx +++ b/packages/web/src/javascripts/Components/Footer/AccountMenuButton.tsx @@ -12,16 +12,7 @@ type Props = AccountMenuProps & { user: unknown } -const AccountMenuButton = ({ - application, - hasError, - isOpen, - mainApplicationGroup, - onClickOutside, - toggleMenu, - user, - viewControllerManager, -}: Props) => { +const AccountMenuButton = ({ hasError, isOpen, mainApplicationGroup, onClickOutside, toggleMenu, user }: Props) => { const buttonRef = useRef(null) return ( @@ -49,12 +40,7 @@ const AccountMenuButton = ({ align="start" className="py-2" > - + ) diff --git a/packages/web/src/javascripts/Components/Footer/Footer.tsx b/packages/web/src/javascripts/Components/Footer/Footer.tsx index a482f7756..7c8e07814 100644 --- a/packages/web/src/javascripts/Components/Footer/Footer.tsx +++ b/packages/web/src/javascripts/Components/Footer/Footer.tsx @@ -125,12 +125,10 @@ class Footer extends AbstractComponent { }) this.autorun(() => { - const showBetaWarning = this.viewControllerManager.showBetaWarning this.setState({ - showBetaWarning: showBetaWarning, - showAccountMenu: this.viewControllerManager.accountMenuController.show, - showQuickSettingsMenu: this.viewControllerManager.quickSettingsMenuController.open, - showVaultSelectionMenu: this.viewControllerManager.vaultSelectionController.open, + showAccountMenu: this.application.accountMenuController.show, + showQuickSettingsMenu: this.application.quickSettingsMenuController.open, + showVaultSelectionMenu: this.application.vaultSelectionController.open, }) }) } @@ -156,7 +154,7 @@ class Footer extends AbstractComponent { } reloadUser() { - this.user = this.application.getUser() + this.user = this.application.sessions.getUser() } async reloadPasscodeStatus() { @@ -194,7 +192,7 @@ class Footer extends AbstractComponent { if (!this.didCheckForOffline) { this.didCheckForOffline = true if (this.state.offline && this.application.items.getNoteCount() === 0) { - this.viewControllerManager.accountMenuController.setShow(true) + this.application.accountMenuController.setShow(true) } } this.findErrors() @@ -270,7 +268,7 @@ class Footer extends AbstractComponent { updateOfflineStatus() { this.setState({ - offline: this.application.noAccount(), + offline: this.application.sessions.isSignedOut(), }) } @@ -295,15 +293,15 @@ class Footer extends AbstractComponent { } accountMenuClickHandler = () => { - this.viewControllerManager.accountMenuController.toggleShow() + this.application.accountMenuController.toggleShow() } quickSettingsClickHandler = () => { - this.viewControllerManager.quickSettingsMenuController.toggle() + this.application.quickSettingsMenuController.toggle() } vaultSelectionClickHandler = () => { - this.viewControllerManager.vaultSelectionController.toggle() + this.application.vaultSelectionController.toggle() } syncResolutionClickHandler = () => { @@ -313,8 +311,8 @@ class Footer extends AbstractComponent { } closeAccountMenu = () => { - this.viewControllerManager.accountMenuController.setShow(false) - this.viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu) + this.application.accountMenuController.setShow(false) + this.application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu) } lockClickHandler = () => { @@ -342,19 +340,19 @@ class Footer extends AbstractComponent { } clickOutsideAccountMenu = () => { - this.viewControllerManager.accountMenuController.closeAccountMenu() + this.application.accountMenuController.closeAccountMenu() } clickOutsideQuickSettingsMenu = () => { - this.viewControllerManager.quickSettingsMenuController.closeQuickSettingsMenu() + this.application.quickSettingsMenuController.closeQuickSettingsMenu() } openPreferences = (openWhatsNew: boolean) => { this.clickOutsideQuickSettingsMenu() if (openWhatsNew) { - this.viewControllerManager.preferencesController.setCurrentPane('whats-new') + this.application.preferencesController.setCurrentPane('whats-new') } - this.viewControllerManager.preferencesController.openPreferences() + this.application.preferencesController.openPreferences() } override render() { @@ -367,14 +365,12 @@ class Footer extends AbstractComponent {
@@ -387,7 +383,7 @@ class Footer extends AbstractComponent { isOpen={this.state.showQuickSettingsMenu} toggleMenu={this.quickSettingsClickHandler} application={this.application} - quickSettingsMenuController={this.viewControllerManager.quickSettingsMenuController} + quickSettingsMenuController={this.application.quickSettingsMenuController} />
@@ -395,13 +391,13 @@ class Footer extends AbstractComponent { {this.state.showBetaWarning && ( diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx index 879d6f379..7dee40c27 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx @@ -5,9 +5,12 @@ import ImportModalInitialPage from './InitialPage' import Modal, { ModalAction } from '../Modal/Modal' import ModalOverlay from '../Modal/ModalOverlay' import { ImportModalController } from '@/Controllers/ImportModalController' +import { useApplication } from '../ApplicationProvider' const ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => { - const { files, setFiles, updateFile, removeFile, importer, parseAndImport, isVisible, close } = importModalController + const application = useApplication() + + const { files, setFiles, updateFile, removeFile, parseAndImport, isVisible, close } = importModalController const isReadyToImport = files.length > 0 && files.every((file) => file.status === 'ready') const importSuccessOrError = @@ -45,7 +48,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM key={file.id} updateFile={updateFile} removeFile={removeFile} - importer={importer} + importer={application.importer} /> ))} diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsButton.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsButton.tsx index bae558c97..4c3fa571d 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsButton.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsButton.tsx @@ -1,5 +1,3 @@ -import { FeaturesController } from '@/Controllers/FeaturesController' -import { FilesController } from '@/Controllers/FilesController' import { LinkingController } from '@/Controllers/LinkingController' import { observer } from 'mobx-react-lite' import { useRef, useCallback } from 'react' @@ -10,11 +8,9 @@ import LinkedItemsPanel from './LinkedItemsPanel' type Props = { linkingController: LinkingController onClickPreprocessing?: () => Promise - filesController: FilesController - featuresController: FeaturesController } -const LinkedItemsButton = ({ linkingController, filesController, onClickPreprocessing, featuresController }: Props) => { +const LinkedItemsButton = ({ linkingController, onClickPreprocessing }: Props) => { const { activeItem, isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController const buttonRef = useRef(null) @@ -40,13 +36,7 @@ const LinkedItemsButton = ({ linkingController, filesController, onClickPreproce open={isLinkingPanelOpen} className="pb-2" > - + ) diff --git a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx index 4b4857bbf..46bc8f447 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/LinkedItemsPanel.tsx @@ -1,7 +1,4 @@ import { FeatureName } from '@/Controllers/FeatureName' -import { FeaturesController } from '@/Controllers/FeaturesController' -import { FilesController } from '@/Controllers/FilesController' -import { LinkingController } from '@/Controllers/LinkingController' import { classNames } from '@standardnotes/utils' import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults' import { observer } from 'mobx-react-lite' @@ -15,26 +12,16 @@ import { LinkedItemsSectionItem } from './LinkedItemsSectionItem' import { DecryptedItem } from '@standardnotes/snjs' import { useItemLinks } from '@/Hooks/useItemLinks' -const LinkedItemsPanel = ({ - linkingController, - filesController, - featuresController, - isOpen, - item, -}: { - linkingController: LinkingController - filesController: FilesController - featuresController: FeaturesController - isOpen: boolean - item: DecryptedItem -}) => { - const { linkItems, unlinkItems, activateItem, createAndAddNewTag, isEntitledToNoteLinking } = linkingController +const LinkedItemsPanel = ({ isOpen, item }: { isOpen: boolean; item: DecryptedItem }) => { + const application = useApplication() + + const { linkItems, unlinkItems, activateItem, createAndAddNewTag, isEntitledToNoteLinking } = + application.linkingController const { notesLinkedToItem, notesLinkingToItem, filesLinkedToItem, filesLinkingToItem, tagsLinkedToItem } = useItemLinks(item) - const { entitledToFiles } = featuresController - const application = useApplication() + const { entitledToFiles } = application.featuresController const searchInputRef = useRef(null) const [searchQuery, setSearchQuery] = useState('') @@ -49,11 +36,11 @@ const LinkedItemsPanel = ({ const selectAndUploadFiles = async () => { if (!entitledToFiles) { - void featuresController.showPremiumAlert(FeatureName.Files) + void application.featuresController.showPremiumAlert(FeatureName.Files) return } - void filesController.selectAndUploadNewFiles((file) => { + void application.filesController.selectAndUploadNewFiles((file) => { void linkItems(item, file) }) } @@ -122,7 +109,7 @@ const LinkedItemsPanel = ({ searchQuery={searchQuery} unlinkItem={() => unlinkItems(item, link.item)} activateItem={activateItem} - handleFileAction={filesController.handleFileAction} + handleFileAction={application.filesController.handleFileAction} /> ))} @@ -142,7 +129,7 @@ const LinkedItemsPanel = ({ searchQuery={searchQuery} unlinkItem={() => unlinkItems(item, link.item)} activateItem={activateItem} - handleFileAction={filesController.handleFileAction} + handleFileAction={application.filesController.handleFileAction} /> ))} @@ -166,7 +153,7 @@ const LinkedItemsPanel = ({ searchQuery={searchQuery} unlinkItem={() => unlinkItems(item, link.item)} activateItem={activateItem} - handleFileAction={filesController.handleFileAction} + handleFileAction={application.filesController.handleFileAction} /> ))} @@ -185,7 +172,7 @@ const LinkedItemsPanel = ({ searchQuery={searchQuery} unlinkItem={() => unlinkItems(item, link.item)} activateItem={activateItem} - handleFileAction={filesController.handleFileAction} + handleFileAction={application.filesController.handleFileAction} /> ))} @@ -202,7 +189,7 @@ const LinkedItemsPanel = ({ searchQuery={searchQuery} unlinkItem={() => unlinkItems(item, link.item)} activateItem={activateItem} - handleFileAction={filesController.handleFileAction} + handleFileAction={application.filesController.handleFileAction} /> ))} @@ -221,7 +208,7 @@ const LinkedItemsPanel = ({ searchQuery={searchQuery} unlinkItem={() => unlinkItems(item, link.item)} activateItem={activateItem} - handleFileAction={filesController.handleFileAction} + handleFileAction={application.filesController.handleFileAction} /> ))} diff --git a/packages/web/src/javascripts/Components/MultipleSelectedFiles/MultipleSelectedFiles.tsx b/packages/web/src/javascripts/Components/MultipleSelectedFiles/MultipleSelectedFiles.tsx index 2e8ccc393..de7afe0a6 100644 --- a/packages/web/src/javascripts/Components/MultipleSelectedFiles/MultipleSelectedFiles.tsx +++ b/packages/web/src/javascripts/Components/MultipleSelectedFiles/MultipleSelectedFiles.tsx @@ -3,41 +3,25 @@ import { observer } from 'mobx-react-lite' import Button from '../Button/Button' import { useCallback } from 'react' import FileOptionsPanel from '../FileContextMenu/FileOptionsPanel' -import { FilesController } from '@/Controllers/FilesController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { LinkingController } from '@/Controllers/LinkingController' +import { ItemListController } from '@/Controllers/ItemList/ItemListController' type Props = { - filesController: FilesController - selectionController: SelectedItemsController - linkingController: LinkingController - navigationController: NavigationController + itemListController: ItemListController } -const MultipleSelectedFiles = ({ - filesController, - selectionController, - linkingController, - navigationController, -}: Props) => { - const count = selectionController.selectedFilesCount +const MultipleSelectedFiles = ({ itemListController }: Props) => { + const count = itemListController.selectedFilesCount const cancelMultipleSelection = useCallback(() => { - selectionController.cancelMultipleSelection() - }, [selectionController]) + itemListController.cancelMultipleSelection() + }, [itemListController]) return (

{count} selected files

- +
diff --git a/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx b/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx index 1d54910a2..66723e09a 100644 --- a/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx +++ b/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx @@ -5,35 +5,19 @@ import { WebApplication } from '@/Application/WebApplication' import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton' import Button from '../Button/Button' import { useCallback } from 'react' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { NotesController } from '@/Controllers/NotesController/NotesController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' -import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' -import { LinkingController } from '@/Controllers/LinkingController' import ChangeMultipleButton from '../ChangeEditor/ChangeMultipleButton' type Props = { application: WebApplication - navigationController: NavigationController - notesController: NotesController - selectionController: SelectedItemsController - historyModalController: HistoryModalController - linkingController: LinkingController } -const MultipleSelectedNotes = ({ - application, - navigationController, - notesController, - linkingController, - selectionController, - historyModalController, -}: Props) => { +const MultipleSelectedNotes = ({ application }: Props) => { + const { notesController, itemListController } = application const count = notesController.selectedNotesCount const cancelMultipleSelection = useCallback(() => { - selectionController.cancelMultipleSelection() - }, [selectionController]) + itemListController.cancelMultipleSelection() + }, [itemListController]) return (
@@ -46,13 +30,7 @@ const MultipleSelectedNotes = ({
- +
diff --git a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx index 75822680a..9778b436a 100644 --- a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx +++ b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx @@ -1,6 +1,5 @@ import { FileItem } from '@standardnotes/snjs' import { AbstractComponent } from '@/Components/Abstract/PureComponent' -import { WebApplication } from '@/Application/WebApplication' import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes' import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles' import { AppPaneId } from '../Panes/AppPaneMetadata' @@ -8,6 +7,7 @@ import FileView from '../FileView/FileView' import NoteView from '../NoteView/NoteView' import { NoteViewController } from '../NoteView/Controller/NoteViewController' import { FileViewController } from '../NoteView/Controller/FileViewController' +import { WebApplication } from '@/Application/WebApplication' type State = { showMultipleSelectedNotes: boolean @@ -48,36 +48,32 @@ class NoteGroupView extends AbstractComponent { }) this.autorun(() => { - if (!this.viewControllerManager) { - return - } - - if (this.viewControllerManager && this.viewControllerManager.notesController) { + if (this.application.notesController) { this.setState({ - showMultipleSelectedNotes: this.viewControllerManager.notesController.selectedNotesCount > 1, + showMultipleSelectedNotes: this.application.notesController.selectedNotesCount > 1, }) } - if (this.viewControllerManager.selectionController) { + if (this.application.itemListController) { this.setState({ - showMultipleSelectedFiles: this.viewControllerManager.selectionController.selectedFilesCount > 1, + showMultipleSelectedFiles: this.application.itemListController.selectedFilesCount > 1, }) } }) this.autorun(() => { - if (this.viewControllerManager && this.viewControllerManager.selectionController) { + if (this.application.itemListController) { this.setState({ - selectedFile: this.viewControllerManager.selectionController.selectedFiles[0], + selectedFile: this.application.itemListController.selectedFiles[0], }) } }) this.autorun(() => { - if (this.viewControllerManager && this.viewControllerManager.paneController) { + if (this.application.paneController) { this.setState({ - selectedPane: this.viewControllerManager.paneController.currentPane, - isInMobileView: this.viewControllerManager.paneController.isInMobileView, + selectedPane: this.application.paneController.currentPane, + isInMobileView: this.application.paneController.isInMobileView, }) } }) @@ -98,23 +94,9 @@ class NoteGroupView extends AbstractComponent { return ( <> - {this.state.showMultipleSelectedNotes && ( - - )} + {this.state.showMultipleSelectedNotes && } {this.state.showMultipleSelectedFiles && ( - + )} {shouldNotShowMultipleSelectedItems && hasControllers && ( <> @@ -122,12 +104,7 @@ class NoteGroupView extends AbstractComponent { return controller instanceof NoteViewController ? ( ) : ( - + ) })} diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/FileViewController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/FileViewController.ts index 3a8ea99c4..979b7fa9b 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/FileViewController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/FileViewController.ts @@ -1,7 +1,7 @@ import { FileItem } from '@standardnotes/models' import { ContentType } from '@standardnotes/domain-core' -import { SNApplication } from '@standardnotes/snjs' import { ItemViewControllerInterface } from './ItemViewControllerInterface' +import { ItemManagerInterface } from '@standardnotes/snjs' export class FileViewController implements ItemViewControllerInterface { public dealloced = false @@ -9,15 +9,14 @@ export class FileViewController implements ItemViewControllerInterface { public runtimeId = `${Math.random()}` constructor( - private application: SNApplication, public item: FileItem, + private items: ItemManagerInterface, ) {} deinit() { this.dealloced = true this.removeStreamObserver?.() ;(this.removeStreamObserver as unknown) = undefined - ;(this.application as unknown) = undefined ;(this.item as unknown) = undefined } @@ -26,23 +25,20 @@ export class FileViewController implements ItemViewControllerInterface { } private streamItems() { - this.removeStreamObserver = this.application.streamItems( - ContentType.TYPES.File, - ({ changed, inserted }) => { - if (this.dealloced) { - return - } + this.removeStreamObserver = this.items.streamItems(ContentType.TYPES.File, ({ changed, inserted }) => { + if (this.dealloced) { + return + } - const files = changed.concat(inserted) + const files = changed.concat(inserted) - const matchingFile = files.find((item) => { - return item.uuid === this.item.uuid - }) + const matchingFile = files.find((item) => { + return item.uuid === this.item.uuid + }) - if (matchingFile) { - this.item = matchingFile - } - }, - ) + if (matchingFile) { + this.item = matchingFile + } + }) } } diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts index 8d8324f6e..35faa2dbe 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts @@ -1,9 +1,19 @@ -import { WebApplication } from '@/Application/WebApplication' import { removeFromArray } from '@standardnotes/utils' -import { FileItem, SNNote } from '@standardnotes/snjs' +import { + AlertService, + ComponentManagerInterface, + FileItem, + ItemManagerInterface, + MutatorClientInterface, + PreferenceServiceInterface, + SNNote, + SessionsClientInterface, + SyncServiceInterface, +} from '@standardnotes/snjs' import { NoteViewController } from './NoteViewController' import { FileViewController } from './FileViewController' import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions' +import { IsNativeMobileWeb } from '@standardnotes/ui-services' type ItemControllerGroupChangeCallback = (activeController: NoteViewController | FileViewController | undefined) => void @@ -12,10 +22,19 @@ export class ItemGroupController { changeObservers: ItemControllerGroupChangeCallback[] = [] eventObservers: (() => void)[] = [] - constructor(private application: WebApplication) {} + constructor( + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private sessions: SessionsClientInterface, + private preferences: PreferenceServiceInterface, + private components: ComponentManagerInterface, + private alerts: AlertService, + private _isNativeMobileWeb: IsNativeMobileWeb, + ) {} public deinit(): void { - ;(this.application as unknown) = undefined + ;(this.items as unknown) = undefined this.eventObservers.forEach((removeObserver) => { removeObserver() @@ -42,11 +61,32 @@ export class ItemGroupController { let controller!: NoteViewController | FileViewController if (context.file) { - controller = new FileViewController(this.application, context.file) + controller = new FileViewController(context.file, this.items) } else if (context.note) { - controller = new NoteViewController(this.application, context.note) + controller = new NoteViewController( + context.note, + this.items, + this.mutator, + this.sync, + this.sessions, + this.preferences, + this.components, + this.alerts, + this._isNativeMobileWeb, + ) } else if (context.templateOptions) { - controller = new NoteViewController(this.application, undefined, context.templateOptions) + controller = new NoteViewController( + undefined, + this.items, + this.mutator, + this.sync, + this.sessions, + this.preferences, + this.components, + this.alerts, + this._isNativeMobileWeb, + context.templateOptions, + ) } else { throw Error('Invalid input to createItemController') } diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts index b32928d55..1980e5538 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts @@ -9,6 +9,7 @@ import { SyncServiceInterface, ItemManagerInterface, MutatorClientInterface, + PreferenceServiceInterface, } from '@standardnotes/snjs' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { NoteViewController } from './NoteViewController' @@ -18,24 +19,24 @@ describe('note view controller', () => { let componentManager: SNComponentManager beforeEach(() => { - application = {} as jest.Mocked - application.streamItems = jest.fn().mockReturnValue(() => {}) - application.getPreference = jest.fn().mockReturnValue(true) - application.noAccount = jest.fn().mockReturnValue(false) - application.isNativeMobileWeb = jest.fn().mockReturnValue(false) + application = { + preferences: { + getValue: jest.fn().mockReturnValue(true), + } as unknown as jest.Mocked, + items: { + streamItems: jest.fn().mockReturnValue(() => {}), + createTemplateItem: jest.fn().mockReturnValue({} as SNNote), + } as unknown as jest.Mocked, + mutator: {} as jest.Mocked, + } as unknown as jest.Mocked - const items = {} as jest.Mocked - items.createTemplateItem = jest.fn().mockReturnValue({} as SNNote) - Object.defineProperty(application, 'items', { value: items }) + application.isNativeMobileWeb = jest.fn().mockReturnValue(false) Object.defineProperty(application, 'sync', { value: {} as jest.Mocked }) application.sync.sync = jest.fn().mockReturnValue(Promise.resolve()) componentManager = {} as jest.Mocked Object.defineProperty(application, 'componentManager', { value: componentManager }) - - const mutator = {} as jest.Mocked - Object.defineProperty(application, 'mutator', { value: mutator }) }) it('should create notes with plaintext note type', async () => { @@ -43,7 +44,17 @@ describe('note view controller', () => { .fn() .mockReturnValue(NativeFeatureIdentifier.TYPES.PlainEditor) - const controller = new NoteViewController(application) + const controller = new NoteViewController( + undefined, + application.items, + application.mutator, + application.sync, + application.sessions, + application.preferences, + application.componentManager, + application.alerts, + application.isNativeMobileWebUseCase, + ) await controller.initialize() expect(application.items.createTemplateItem).toHaveBeenCalledWith( @@ -64,7 +75,17 @@ describe('note view controller', () => { .fn() .mockReturnValue(NativeFeatureIdentifier.TYPES.MarkdownProEditor) - const controller = new NoteViewController(application) + const controller = new NoteViewController( + undefined, + application.items, + application.mutator, + application.sync, + application.sessions, + application.preferences, + application.componentManager, + application.alerts, + application.isNativeMobileWebUseCase, + ) await controller.initialize() expect(application.items.createTemplateItem).toHaveBeenCalledWith( @@ -86,7 +107,18 @@ describe('note view controller', () => { application.items.findItem = jest.fn().mockReturnValue(tag) application.mutator.addTagToNote = jest.fn() - const controller = new NoteViewController(application, undefined, { tag: tag.uuid }) + const controller = new NoteViewController( + undefined, + application.items, + application.mutator, + application.sync, + application.sessions, + application.preferences, + application.componentManager, + application.alerts, + application.isNativeMobileWebUseCase, + { tag: tag.uuid }, + ) await controller.initialize() expect(controller['defaultTag']).toEqual(tag) @@ -100,7 +132,17 @@ describe('note view controller', () => { application.items.findItem = jest.fn().mockReturnValue(note) - const controller = new NoteViewController(application, note) + const controller = new NoteViewController( + note, + application.items, + application.mutator, + application.sync, + application.sessions, + application.preferences, + application.componentManager, + application.alerts, + application.isNativeMobileWebUseCase, + ) await controller.initialize() const changePromise = Deferred() diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts index 673bb0fcb..9cbf27808 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts @@ -1,4 +1,3 @@ -import { WebApplication } from '@/Application/WebApplication' import { noteTypeForEditorIdentifier } from '@standardnotes/features' import { SNNote, @@ -9,13 +8,23 @@ import { PrefKey, PayloadVaultOverrides, } from '@standardnotes/models' -import { UuidString } from '@standardnotes/snjs' +import { + AlertService, + ComponentManagerInterface, + ItemManagerInterface, + MutatorClientInterface, + PreferenceServiceInterface, + SessionsClientInterface, + SyncServiceInterface, + UuidString, +} from '@standardnotes/snjs' import { removeFromArray } from '@standardnotes/utils' import { ContentType } from '@standardnotes/domain-core' import { ItemViewControllerInterface } from './ItemViewControllerInterface' import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions' import { log, LoggingDomain } from '@/Logging' import { NoteSaveFunctionParams, NoteSyncController } from '../../../Controllers/NoteSyncController' +import { IsNativeMobileWeb } from '@standardnotes/ui-services' export type EditorValues = { title: string @@ -37,8 +46,15 @@ export class NoteViewController implements ItemViewControllerInterface { private syncController!: NoteSyncController constructor( - private application: WebApplication, - item?: SNNote, + item: SNNote | undefined, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private sessions: SessionsClientInterface, + private preferences: PreferenceServiceInterface, + private components: ComponentManagerInterface, + private alerts: AlertService, + private _isNativeMobileWeb: IsNativeMobileWeb, public templateNoteOptions?: TemplateNoteViewControllerOptions, ) { if (item) { @@ -50,10 +66,18 @@ export class NoteViewController implements ItemViewControllerInterface { } if (this.defaultTagUuid) { - this.defaultTag = this.application.items.findItem(this.defaultTagUuid) as SNTag + this.defaultTag = this.items.findItem(this.defaultTagUuid) as SNTag } - this.syncController = new NoteSyncController(this.application, this.item) + this.syncController = new NoteSyncController( + this.item, + this.items, + this.mutator, + this.sessions, + this.sync, + this.alerts, + this._isNativeMobileWeb, + ) } deinit(): void { @@ -74,9 +98,6 @@ export class NoteViewController implements ItemViewControllerInterface { disposer() } this.disposers.length = 0 - ;(this.application as unknown) = undefined - ;(this.item as unknown) = undefined - this.innerValueChangeObservers.length = 0 } @@ -89,16 +110,16 @@ export class NoteViewController implements ItemViewControllerInterface { this.needsInit = false - const addTagHierarchy = this.application.getPreference(PrefKey.NoteAddToParentFolders, true) + const addTagHierarchy = this.preferences.getValue(PrefKey.NoteAddToParentFolders, true) if (!this.item) { log(LoggingDomain.NoteView, 'Initializing as template note') - const editorIdentifier = this.application.componentManager.getDefaultEditorIdentifier(this.defaultTag) + const editorIdentifier = this.components.getDefaultEditorIdentifier(this.defaultTag) const noteType = noteTypeForEditorIdentifier(editorIdentifier) - const note = this.application.items.createTemplateItem( + const note = this.items.createTemplateItem( ContentType.TYPES.Note, { text: '', @@ -118,8 +139,8 @@ export class NoteViewController implements ItemViewControllerInterface { this.syncController.setItem(this.item) if (this.defaultTagUuid) { - const tag = this.application.items.findItem(this.defaultTagUuid) as SNTag - await this.application.mutator.addTagToNote(note, tag, addTagHierarchy) + const tag = this.items.findItem(this.defaultTagUuid) as SNTag + await this.mutator.addTagToNote(note, tag, addTagHierarchy) } this.notifyObservers(this.item, PayloadEmitSource.InitialObserverRegistrationPush) @@ -140,7 +161,7 @@ export class NoteViewController implements ItemViewControllerInterface { } this.disposers.push( - this.application.streamItems(ContentType.TYPES.Note, ({ changed, inserted, source }) => { + this.items.streamItems(ContentType.TYPES.Note, ({ changed, inserted, source }) => { if (this.dealloced) { return } @@ -163,7 +184,7 @@ export class NoteViewController implements ItemViewControllerInterface { public insertTemplatedNote(): Promise { log(LoggingDomain.NoteView, 'Inserting template note') this.isTemplateNote = false - return this.application.mutator.insertItem(this.item) + return this.mutator.insertItem(this.item) } /** diff --git a/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal/NoteConflictResolutionModal.tsx b/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal/NoteConflictResolutionModal.tsx index c0707770e..094b17b90 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal/NoteConflictResolutionModal.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal/NoteConflictResolutionModal.tsx @@ -105,7 +105,7 @@ const NoteConflictResolutionModal = ({ mutator.conflictOf = undefined }) setIsPerformingAction(false) - void application.controllers.selectionController.selectItem(selectedNotes[0].uuid, true) + void application.itemListController.selectItem(selectedNotes[0].uuid, true) void application.sync.sync() close() } diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts b/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts index ae688d350..cae8ebb9f 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts @@ -3,7 +3,6 @@ */ import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { NotesController } from '@/Controllers/NotesController/NotesController' import { ApplicationEvent, @@ -18,7 +17,7 @@ import { NoteViewController } from './Controller/NoteViewController' describe('NoteView', () => { let noteViewController: NoteViewController let application: WebApplication - let viewControllerManager: ViewControllerManager + let notesController: NotesController const createNoteView = () => @@ -37,13 +36,11 @@ describe('NoteView', () => { notesController.getSpellcheckStateForNote = jest.fn() notesController.getEditorWidthForNote = jest.fn() - viewControllerManager = { - notesController: notesController, - } as jest.Mocked - application = { - controllers: viewControllerManager, - } as jest.Mocked + notesController, + noteViewController, + } as unknown as jest.Mocked + application.hasProtectionSources = jest.fn().mockReturnValue(true) application.authorizeNoteAccess = jest.fn() application.addWebEventObserver = jest.fn() diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 0553252f9..a839adcae 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -111,7 +111,7 @@ class NoteView extends AbstractComponent { if (!this.controller || this.controller.dealloced) { return } - this.application.getDesktopService()?.redoSearch() + this.application.desktopManager?.redoSearch() } this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25) @@ -215,7 +215,7 @@ class NoteView extends AbstractComponent { this.autorun(() => { this.setState({ - showProtectedWarning: this.viewControllerManager.notesController.showProtectedWarning, + showProtectedWarning: this.application.notesController.showProtectedWarning, }) }) @@ -224,7 +224,7 @@ class NoteView extends AbstractComponent { const showProtectedWarning = this.note.protected && - (!this.application.hasProtectionSources() || !this.application.hasUnprotectedAccessSession()) + (!this.application.hasProtectionSources() || !this.application.protections.hasUnprotectedAccessSession()) this.setShowProtectedOverlay(showProtectedWarning) this.reloadPreferences().catch(console.error) @@ -429,7 +429,7 @@ class NoteView extends AbstractComponent { } streamItems() { - this.removeNoteStreamObserver = this.application.streamItems(ContentType.TYPES.Note, async () => { + this.removeNoteStreamObserver = this.application.items.streamItems(ContentType.TYPES.Note, async () => { if (!this.note) { return } @@ -541,7 +541,7 @@ class NoteView extends AbstractComponent { }) this.setStatus({ type: 'saved', - message: 'All changes saved' + (this.application.noAccount() ? ' offline' : ''), + message: 'All changes saved' + (this.application.sessions.isSignedOut() ? ' offline' : ''), }) } @@ -615,7 +615,7 @@ class NoteView extends AbstractComponent { } setShowProtectedOverlay(show: boolean) { - this.viewControllerManager.notesController.setShowProtectedWarning(show) + this.application.notesController.setShowProtectedWarning(show) } async deleteNote(permanently: boolean) { @@ -677,7 +677,7 @@ class NoteView extends AbstractComponent { } async reloadSpellcheck() { - const spellcheck = this.viewControllerManager.notesController.getSpellcheckStateForNote(this.note) + const spellcheck = this.application.notesController.getSpellcheckStateForNote(this.note) if (spellcheck !== this.state.spellcheck) { reloadFont(this.state.monospaceFont) this.setState({ spellcheck }) @@ -685,7 +685,7 @@ class NoteView extends AbstractComponent { } reloadLineWidth() { - const editorLineWidth = this.viewControllerManager.notesController.getEditorWidthForNote(this.note) + const editorLineWidth = this.application.notesController.getEditorWidthForNote(this.note) this.setState({ editorLineWidth, @@ -849,15 +849,15 @@ class NoteView extends AbstractComponent { {this.note && ( )} {this.state.noteLocked && ( this.viewControllerManager.notesController.setLockSelectedNotes(!this.state.noteLocked)} + onClick={() => this.application.notesController.setLockSelectedNotes(!this.state.noteLocked)} noteLocked={this.state.noteLocked} /> )} @@ -913,36 +913,26 @@ class NoteView extends AbstractComponent {
)}
- +
)} @@ -990,8 +980,8 @@ class NoteView extends AbstractComponent { diff --git a/packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx b/packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx index 0ec1ded3a..bf70ec7c9 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx @@ -24,7 +24,7 @@ const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement, file tooltipText: 'Drop your files to upload and link them to the current note', callback: async (uploadedFile) => { await linkingController.linkItems(note, uploadedFile) - void application.changeAndSaveItem(uploadedFile, (mutator) => { + void application.changeAndSaveItem.execute(uploadedFile, (mutator) => { mutator.protected = note.protected }) filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid) diff --git a/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx b/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx index 34448dff2..9391906cc 100644 --- a/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx +++ b/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx @@ -46,6 +46,7 @@ export const PlainEditor = forwardRef( const note = useRef(controller.item) const tabObserverDisposer = useRef() + const mutationObserver = useRef(null) useImperativeHandle(ref, () => ({ focus() { @@ -53,6 +54,15 @@ export const PlainEditor = forwardRef( }, })) + useEffect(() => { + return () => { + mutationObserver.current?.disconnect() + tabObserverDisposer.current?.() + tabObserverDisposer.current = undefined + mutationObserver.current = null + } + }, []) + useEffect(() => { const disposer = controller.addNoteInnerValueChangeObserver((updatedNote, source) => { if (updatedNote.uuid !== note.current.uuid) { @@ -219,15 +229,18 @@ export const PlainEditor = forwardRef( const observer = new MutationObserver((records) => { for (const record of records) { record.removedNodes.forEach((node) => { - if (node === editor) { + if (node.isEqualNode(editor)) { tabObserverDisposer.current?.() tabObserverDisposer.current = undefined + observer.disconnect() } }) } }) observer.observe(editor.parentElement as HTMLElement, { childList: true }) + + mutationObserver.current = observer } if (textareaUnloading) { diff --git a/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx b/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx index b96618bbe..385088392 100644 --- a/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx +++ b/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx @@ -1,30 +1,14 @@ import { observer } from 'mobx-react-lite' import NotesOptions from '@/Components/NotesOptions/NotesOptions' import { useCallback, useState } from 'react' -import { NotesController } from '@/Controllers/NotesController/NotesController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' import Popover from '../Popover/Popover' -import { LinkingController } from '@/Controllers/LinkingController' import Menu from '../Menu/Menu' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' +import { useApplication } from '../ApplicationProvider' -type Props = { - navigationController: NavigationController - notesController: NotesController - linkingController: LinkingController - historyModalController: HistoryModalController - selectionController: SelectedItemsController -} +const NotesContextMenu = () => { + const application = useApplication() -const NotesContextMenu = ({ - navigationController, - notesController, - linkingController, - historyModalController, - selectionController, -}: Props) => { - const { contextMenuOpen, contextMenuClickLocation, setContextMenuOpen } = notesController + const { contextMenuOpen, contextMenuClickLocation, setContextMenuOpen } = application.notesController const closeMenu = () => setContextMenuOpen(!contextMenuOpen) @@ -48,12 +32,7 @@ const NotesContextMenu = ({ >
diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx index 420580266..f3dddd96b 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -50,15 +50,7 @@ const iconClassDanger = `text-danger mr-2 ${iconSize}` const iconClassWarning = `text-warning mr-2 ${iconSize}` const iconClassSuccess = `text-success mr-2 ${iconSize}` -const NotesOptions = ({ - notes, - navigationController, - notesController, - linkingController, - selectionController, - historyModalController, - closeMenu, -}: NotesOptionsProps) => { +const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => { const application = useApplication() const [altKeyDown, setAltKeyDown] = useState(false) @@ -117,7 +109,7 @@ const NotesOptions = ({ if (notes.length === 1) { const note = notes[0] const blob = getNoteBlob(application, note) - application.getArchiveService().downloadData(blob, getNoteFileName(application, note)) + application.archiveService.downloadData(blob, getNoteFileName(application, note)) return } @@ -126,7 +118,7 @@ const NotesOptions = ({ type: ToastType.Loading, message: `Exporting ${notes.length} notes...`, }) - await application.getArchiveService().downloadDataAsZip( + await application.archiveService.downloadDataAsZip( notes.map((note) => { return { name: getNoteFileName(application, note), @@ -163,7 +155,7 @@ const NotesOptions = ({ { label: 'Open', handler: (toastId) => { - selectionController.selectItem(duplicated.uuid, true).catch(console.error) + application.itemListController.selectItem(duplicated.uuid, true).catch(console.error) dismissToast(toastId) }, }, @@ -176,11 +168,11 @@ const NotesOptions = ({ ) void application.sync.sync() closeMenuAndToggleNotesList() - }, [application.mutator, application.sync, closeMenuAndToggleNotesList, notes, selectionController]) + }, [application.mutator, application.itemListController, application.sync, closeMenuAndToggleNotesList, notes]) const openRevisionHistoryModal = useCallback(() => { - historyModalController.openModal(notesController.firstSelectedNote) - }, [historyModalController, notesController.firstSelectedNote]) + application.historyModalController.openModal(application.notesController.firstSelectedNote) + }, [application.historyModalController, application.notesController.firstSelectedNote]) const historyShortcut = useMemo( () => application.keyboardService.keyboardShortcutForCommand(OPEN_NOTE_HISTORY_COMMAND), @@ -243,7 +235,7 @@ const NotesOptions = ({ { - notesController.setLockSelectedNotes(locked) + application.notesController.setLockSelectedNotes(locked) }} > @@ -252,7 +244,7 @@ const NotesOptions = ({ { - notesController.setHideSelectedNotePreviews(!hidePreviews) + application.notesController.setHideSelectedNotePreviews(!hidePreviews) }} > @@ -261,7 +253,7 @@ const NotesOptions = ({ { - notesController.setProtectSelectedNotes(protect).catch(console.error) + application.notesController.setProtectSelectedNotes(protect).catch(console.error) }} > @@ -277,17 +269,17 @@ const NotesOptions = ({ {featureTrunkVaultsEnabled() && } - {navigationController.tagsCount > 0 && ( + {application.navigationController.tagsCount > 0 && ( )} { - notesController.setStarSelectedNotes(!starred) + application.notesController.setStarSelectedNotes(!starred) }} > @@ -298,7 +290,7 @@ const NotesOptions = ({ {unpinned && ( { - notesController.setPinSelectedNotes(true) + application.notesController.setPinSelectedNotes(true) }} > @@ -309,7 +301,7 @@ const NotesOptions = ({ {pinned && ( { - notesController.setPinSelectedNotes(false) + application.notesController.setPinSelectedNotes(false) }} > @@ -394,7 +386,7 @@ const NotesOptions = ({ {unarchived && ( { - await notesController.setArchiveSelectedNotes(true).catch(console.error) + await application.notesController.setArchiveSelectedNotes(true).catch(console.error) closeMenuAndToggleNotesList() }} > @@ -405,7 +397,7 @@ const NotesOptions = ({ {archived && ( { - await notesController.setArchiveSelectedNotes(false).catch(console.error) + await application.notesController.setArchiveSelectedNotes(false).catch(console.error) closeMenuAndToggleNotesList() }} > @@ -417,14 +409,14 @@ const NotesOptions = ({ (altKeyDown ? ( { - await notesController.deleteNotesPermanently() + await application.notesController.deleteNotesPermanently() closeMenuAndToggleNotesList() }} /> ) : ( { - await notesController.setTrashSelectedNotes(true) + await application.notesController.setTrashSelectedNotes(true) closeMenuAndToggleNotesList() }} > @@ -436,7 +428,7 @@ const NotesOptions = ({ <> { - await notesController.setTrashSelectedNotes(false) + await application.notesController.setTrashSelectedNotes(false) closeMenuAndToggleNotesList() }} > @@ -445,13 +437,13 @@ const NotesOptions = ({ { - await notesController.deleteNotesPermanently() + await application.notesController.deleteNotesPermanently() closeMenuAndToggleNotesList() }} /> { - await notesController.emptyTrash() + await application.notesController.emptyTrash() closeMenuAndToggleNotesList() }} > @@ -459,7 +451,7 @@ const NotesOptions = ({
Empty Trash
-
{notesController.trashedNotesCount} notes in Trash
+
{application.notesController.trashedNotesCount} notes in Trash
@@ -482,7 +474,11 @@ const NotesOptions = ({ {editorForNote && ( - + )} diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsPanel.tsx b/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsPanel.tsx index a397ddff6..66a971edf 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsPanel.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsPanel.tsx @@ -2,31 +2,16 @@ import { useCallback, useRef, useState } from 'react' import { observer } from 'mobx-react-lite' import NotesOptions from './NotesOptions' import { NotesController } from '@/Controllers/NotesController/NotesController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' import Popover from '../Popover/Popover' -import { LinkingController } from '@/Controllers/LinkingController' import RoundIconButton from '../Button/RoundIconButton' import Menu from '../Menu/Menu' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' type Props = { - navigationController: NavigationController notesController: NotesController - linkingController: LinkingController - historyModalController: HistoryModalController - selectionController: SelectedItemsController onClickPreprocessing?: () => Promise } -const NotesOptionsPanel = ({ - navigationController, - notesController, - linkingController, - historyModalController, - selectionController, - onClickPreprocessing, -}: Props) => { +const NotesOptionsPanel = ({ notesController, onClickPreprocessing }: Props) => { const [isOpen, setIsOpen] = useState(false) const buttonRef = useRef(null) @@ -57,11 +42,6 @@ const NotesOptionsPanel = ({
diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsProps.ts b/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsProps.ts index 97ea133cb..92d29146f 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsProps.ts +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptionsProps.ts @@ -1,17 +1,7 @@ -import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { NotesController } from '@/Controllers/NotesController/NotesController' -import { LinkingController } from '@/Controllers/LinkingController' import { SNNote } from '@standardnotes/snjs' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' export type NotesOptionsProps = { notes: SNNote[] - navigationController: NavigationController - notesController: NotesController - linkingController: LinkingController - historyModalController: HistoryModalController - selectionController: SelectedItemsController requestDisableClickOutside?: (disabled: boolean) => void closeMenu: () => void } diff --git a/packages/web/src/javascripts/Components/OtherSessionsSignOut/OtherSessionsSignOut.tsx b/packages/web/src/javascripts/Components/OtherSessionsSignOut/OtherSessionsSignOut.tsx index a400283c7..7db301ba6 100644 --- a/packages/web/src/javascripts/Components/OtherSessionsSignOut/OtherSessionsSignOut.tsx +++ b/packages/web/src/javascripts/Components/OtherSessionsSignOut/OtherSessionsSignOut.tsx @@ -1,6 +1,5 @@ import { useCallback, useRef } from 'react' import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import Button from '@/Components/Button/Button' import Icon from '../Icon/Icon' @@ -8,15 +7,14 @@ import AlertDialog from '../AlertDialog/AlertDialog' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager } -const ConfirmOtherSessionsSignOut = observer(({ application, viewControllerManager }: Props) => { +const ConfirmOtherSessionsSignOut = observer(({ application }: Props) => { const cancelRef = useRef(null) const closeDialog = useCallback(() => { - viewControllerManager.accountMenuController.setOtherSessionsSignOut(false) - }, [viewControllerManager]) + application.accountMenuController.setOtherSessionsSignOut(false) + }, [application]) const confirm = useCallback(() => { application.revokeAllOtherSessions().catch(console.error) @@ -55,7 +53,7 @@ const ConfirmOtherSessionsSignOut = observer(({ application, viewControllerManag ConfirmOtherSessionsSignOut.displayName = 'ConfirmOtherSessionsSignOut' const OtherSessionsSignOutContainer = (props: Props) => { - if (!props.viewControllerManager.accountMenuController.otherSessionsSignOut) { + if (!props.application.accountMenuController.otherSessionsSignOut) { return null } return diff --git a/packages/web/src/javascripts/Components/Panes/PanesSystemComponent.tsx b/packages/web/src/javascripts/Components/Panes/PanesSystemComponent.tsx index 639e4268c..8113c2e5b 100644 --- a/packages/web/src/javascripts/Components/Panes/PanesSystemComponent.tsx +++ b/packages/web/src/javascripts/Components/Panes/PanesSystemComponent.tsx @@ -38,8 +38,6 @@ const PanesSystemComponent = () => { const [panesPendingEntrance, setPanesPendingEntrance] = useState([]) const [panesPendingExit, setPanesPendingExit] = useState([]) - const viewControllerManager = application.controllers - const [navigationPanelWidth, setNavigationPanelWidth] = useState( application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH), ) @@ -296,18 +294,6 @@ const PanesSystemComponent = () => { key={'content-list-view'} application={application} onPanelWidthLoad={handleInitialItemsListPanelWidthLoad} - accountMenuController={viewControllerManager.accountMenuController} - filesController={viewControllerManager.filesController} - itemListController={viewControllerManager.itemListController} - navigationController={viewControllerManager.navigationController} - noAccountWarningController={viewControllerManager.noAccountWarningController} - notesController={viewControllerManager.notesController} - selectionController={viewControllerManager.selectionController} - searchOptionsController={viewControllerManager.searchOptionsController} - linkingController={viewControllerManager.linkingController} - featuresController={viewControllerManager.featuresController} - historyModalController={viewControllerManager.historyModalController} - paneController={viewControllerManager.paneController} > {showPanelResizers && listRef && ( { return false } - if (!this.application.getUser()?.email) { + if (!this.application.sessions.getUser()?.email) { this.application.alerts .alert("We don't have your email stored. Please sign out then log back in to fix this issue.") .catch(console.error) diff --git a/packages/web/src/javascripts/Components/Preferences/PaneSelector.tsx b/packages/web/src/javascripts/Components/Preferences/PaneSelector.tsx index 7cf196c1a..e72b7f4c3 100644 --- a/packages/web/src/javascripts/Components/Preferences/PaneSelector.tsx +++ b/packages/web/src/javascripts/Components/Preferences/PaneSelector.tsx @@ -15,39 +15,23 @@ import Vaults from './Panes/Vaults/Vaults' const PaneSelector: FunctionComponent = ({ menu, - viewControllerManager, application, - mfaProvider, - userProvider, }) => { switch (menu.selectedPaneId) { case 'general': - return ( - - ) + return case 'account': - return + return case 'appearance': return case 'home-server': return case 'security': - return ( - - ) + return case 'vaults': return case 'backups': - return + return case 'listed': return case 'shortcuts': @@ -61,13 +45,7 @@ const PaneSelector: FunctionComponent default: - return ( - - ) + return } } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx index 8b3e4cf80..4a9e20519 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx @@ -1,7 +1,6 @@ import { observer } from 'mobx-react-lite' import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import Authentication from './Authentication' import Credentials from './Credentials' import Sync from './Sync' @@ -15,30 +14,29 @@ import DeleteAccount from '@/Components/Preferences/Panes/Account/DeleteAccount' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager } -const AccountPreferences = ({ application, viewControllerManager }: Props) => { +const AccountPreferences = ({ application }: Props) => { const isUsingThirdPartyServer = application.isThirdPartyHostUsed() return ( {!application.hasAccount() ? ( - + ) : ( <> - + )} - - {application.hasAccount() && viewControllerManager.featuresController.entitledToFiles && ( + + {application.hasAccount() && application.featuresController.entitledToFiles && ( )} {application.hasAccount() && !isUsingThirdPartyServer && } - - + + ) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Authentication.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Authentication.tsx index d82a7c7d2..e66dae01c 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Authentication.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Authentication.tsx @@ -1,7 +1,6 @@ import Button from '@/Components/Button/Button' import { Title } from '@/Components/Preferences/PreferencesComponents/Content' import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'react' import { AccountIllustration } from '@standardnotes/icons' @@ -11,20 +10,19 @@ import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager } -const Authentication: FunctionComponent = ({ viewControllerManager }) => { +const Authentication: FunctionComponent = ({ application }) => { const clickSignIn = () => { - viewControllerManager.preferencesController.closePreferences() - viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.SignIn) - viewControllerManager.accountMenuController.setShow(true) + application.preferencesController.closePreferences() + application.accountMenuController.setCurrentPane(AccountMenuPane.SignIn) + application.accountMenuController.setShow(true) } const clickRegister = () => { - viewControllerManager.preferencesController.closePreferences() - viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.Register) - viewControllerManager.accountMenuController.setShow(true) + application.preferencesController.closePreferences() + application.accountMenuController.setCurrentPane(AccountMenuPane.Register) + application.accountMenuController.setShow(true) } return ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx index 0de8e3167..25c7b0128 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx @@ -1,14 +1,14 @@ import Button from '@/Components/Button/Button' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'react' import { Title, Text } from '../../PreferencesComponents/Content' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' +import { useApplication } from '@/Components/ApplicationProvider' + +const ClearSessionDataView: FunctionComponent = () => { + const application = useApplication() -const ClearSessionDataView: FunctionComponent<{ - viewControllerManager: ViewControllerManager -}> = ({ viewControllerManager }) => { return ( @@ -18,7 +18,7 @@ const ClearSessionDataView: FunctionComponent<{ colorStyle="danger" label="Clear workspace" onClick={() => { - viewControllerManager.accountMenuController.setSigningOut(true) + application.accountMenuController.setSigningOut(true) }} /> diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Credentials.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Credentials.tsx index b71eb6c19..2543d1508 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Credentials.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Credentials.tsx @@ -6,7 +6,6 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import { dateToLocalizedString } from '@standardnotes/snjs' import { useCallback, useState, FunctionComponent } from 'react' import ChangeEmail from '@/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import PasswordWizard from '@/Components/PasswordWizard/PasswordWizard' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' @@ -14,14 +13,13 @@ import ModalOverlay from '@/Components/Modal/ModalOverlay' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager } const Credentials: FunctionComponent = ({ application }: Props) => { const [isChangeEmailDialogOpen, setIsChangeEmailDialogOpen] = useState(false) const [shouldShowPasswordWizard, setShouldShowPasswordWizard] = useState(false) - const user = application.getUser() + const user = application.sessions.getUser() const passwordCreatedAtTimestamp = application.getUserPasswordCreationDate() as Date const passwordCreatedOn = dateToLocalizedString(passwordCreatedAtTimestamp) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/DeleteAccount.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/DeleteAccount.tsx index c3c6bc6ac..9d398852e 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/DeleteAccount.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/DeleteAccount.tsx @@ -3,15 +3,13 @@ import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/P import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' import Button from '@/Components/Button/Button' import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup' -import { ViewControllerManager } from '@Controllers/ViewControllerManager' import { WebApplication } from '@/Application/WebApplication' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager } -const DeleteAccount = ({ application, viewControllerManager }: Props) => { +const DeleteAccount = ({ application }: Props) => { if (!application.hasAccount()) { return null } @@ -25,7 +23,7 @@ const DeleteAccount = ({ application, viewControllerManager }: Props) => { colorStyle="danger" label="Delete my account" onClick={() => { - viewControllerManager.accountMenuController.setDeletingAccount(true) + application.accountMenuController.setDeletingAccount(true) }} /> diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email/Email.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email/Email.tsx index f51d80509..540aac9b9 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email/Email.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email/Email.tsx @@ -43,7 +43,7 @@ const Email: FunctionComponent = ({ application }: Props) => { } const loadSettings = useCallback(async () => { - if (!application.getUser()) { + if (!application.sessions.getUser()) { return } setIsLoading(true) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SignOutView.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SignOutView.tsx index 68172c59f..53796f663 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SignOutView.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SignOutView.tsx @@ -2,7 +2,6 @@ import Button from '@/Components/Button/Button' import OtherSessionsSignOutContainer from '@/Components/OtherSessionsSignOut/OtherSessionsSignOut' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'react' import { Subtitle, Title, Text } from '../../PreferencesComponents/Content' @@ -12,10 +11,9 @@ import ClearSessionDataView from './ClearSessionDataView' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager } -const SignOutView: FunctionComponent = observer(({ application, viewControllerManager }) => { +const SignOutView: FunctionComponent = observer(({ application }) => { return ( <> @@ -27,10 +25,10 @@ const SignOutView: FunctionComponent = observer(({ application, viewContr - +
= ({ viewControllerManager, application }) => { - if (!viewControllerManager.purchaseFlowController.isOpen) { +const PurchaseFlowWrapper: FunctionComponent = ({ application }) => { + if (!application.purchaseFlowController.isOpen) { return null } - return + return } export default observer(PurchaseFlowWrapper) diff --git a/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowWrapperProps.tsx b/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowWrapperProps.tsx index 4f50c7f08..53c6875b8 100644 --- a/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowWrapperProps.tsx +++ b/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowWrapperProps.tsx @@ -1,7 +1,5 @@ import { WebApplication } from '@/Application/WebApplication' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' export type PurchaseFlowWrapperProps = { - viewControllerManager: ViewControllerManager application: WebApplication } diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx index fb21a64d5..1c419bb34 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -67,7 +67,7 @@ const QuickSettingsMenu: FunctionComponent = ({ quickSettingsMenuCont }, [reloadThemes, themes.length]) useEffect(() => { - const cleanupItemStream = application.streamItems(ContentType.TYPES.Theme, () => { + const cleanupItemStream = application.items.streamItems(ContentType.TYPES.Theme, () => { reloadThemes() }) @@ -85,7 +85,7 @@ const QuickSettingsMenu: FunctionComponent = ({ quickSettingsMenuCont }, [application, reloadThemes]) useEffect(() => { - const cleanupItemStream = application.streamItems(ContentType.TYPES.Component, () => { + const cleanupItemStream = application.items.streamItems(ContentType.TYPES.Component, () => { reloadEditorStackComponents() }) diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalContentPane.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalContentPane.tsx index 076a41ba5..0ac2f895f 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalContentPane.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalContentPane.tsx @@ -1,6 +1,7 @@ import RevisionContentLocked from './RevisionContentLocked' import { observer } from 'mobx-react-lite' -import { NoteHistoryController, RevisionContentState } from '@/Controllers/NoteHistory/NoteHistoryController' +import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController' +import { RevisionContentState } from '@/Controllers/NoteHistory/Types' import Spinner from '@/Components/Spinner/Spinner' import { ReadonlyNoteContent } from '../NoteView/ReadonlyNoteContent' import { SNNote } from '@standardnotes/snjs' diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalDialogContent.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalDialogContent.tsx index 169e9b741..bdf8dce3c 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalDialogContent.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalDialogContent.tsx @@ -11,14 +11,29 @@ import { HistoryModalMobileTab } from './utils' import MobileModalAction from '../Modal/MobileModalAction' import Popover from '../Popover/Popover' import MobileModalHeader from '../Modal/MobileModalHeader' +import { useApplication } from '../ApplicationProvider' -const HistoryModalDialogContent = ({ - application, - dismissModal, - note, - selectionController, -}: RevisionHistoryModalContentProps) => { - const [noteHistoryController] = useState(() => new NoteHistoryController(application, note, selectionController)) +const HistoryModalDialogContent = ({ dismissModal, note }: RevisionHistoryModalContentProps) => { + const application = useApplication() + + const [noteHistoryController] = useState( + () => + new NoteHistoryController( + note, + application.itemListController, + application.features, + application.items, + application.mutator, + application.sync, + application.actions, + application.history, + application.alerts, + application.getRevision, + application.listRevisions, + application.deleteRevision, + application.changeAndSaveItem, + ), + ) const [selectedMobileTab, setSelectedMobileTab] = useState('List') const tabOptionRef = useRef(null) diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModal.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModal.tsx index d3b3f630a..aacc24474 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModal.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModal.tsx @@ -6,15 +6,12 @@ import { RevisionHistoryModalProps } from './RevisionHistoryModalProps' import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler' import { useModalAnimation } from '../Modal/useModalAnimation' -const RevisionHistoryModal: FunctionComponent = ({ - application, - historyModalController, - selectionController, -}) => { +const RevisionHistoryModal: FunctionComponent = ({ application }) => { const addAndroidBackHandler = useAndroidBackHandler() const isOpen = Boolean( - historyModalController.note && application.isAuthorizedToRenderItem(historyModalController.note), + application.historyModalController.note && + application.isAuthorizedToRenderItem(application.historyModalController.note), ) useEffect(() => { @@ -22,7 +19,7 @@ const RevisionHistoryModal: FunctionComponent = ({ if (isOpen) { removeListener = addAndroidBackHandler(() => { - historyModalController.dismissModal() + application.historyModalController.dismissModal() return true }) } @@ -32,7 +29,7 @@ const RevisionHistoryModal: FunctionComponent = ({ removeListener() } } - }, [addAndroidBackHandler, historyModalController, isOpen]) + }, [addAndroidBackHandler, application, isOpen]) const [isMounted, setElement] = useModalAnimation(isOpen) @@ -41,13 +38,12 @@ const RevisionHistoryModal: FunctionComponent = ({ } return ( - - {!!historyModalController.note && ( + + {!!application.historyModalController.note && ( )} diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalProps.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalProps.tsx index 3c9c9a1bf..7ff86d38d 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalProps.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalProps.tsx @@ -1,16 +1,11 @@ import { WebApplication } from '@/Application/WebApplication' -import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { SNNote } from '@standardnotes/snjs' type CommonProps = { application: WebApplication - selectionController: SelectedItemsController } -export type RevisionHistoryModalProps = CommonProps & { - historyModalController: HistoryModalController -} +export type RevisionHistoryModalProps = CommonProps export type RevisionHistoryModalContentProps = CommonProps & { note: SNNote diff --git a/packages/web/src/javascripts/Components/SessionsModal/SessionsModal.tsx b/packages/web/src/javascripts/Components/SessionsModal/SessionsModal.tsx index f587ddbf0..fbe7c6184 100644 --- a/packages/web/src/javascripts/Components/SessionsModal/SessionsModal.tsx +++ b/packages/web/src/javascripts/Components/SessionsModal/SessionsModal.tsx @@ -1,4 +1,3 @@ -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { SNApplication, SessionStrings, @@ -85,9 +84,8 @@ function useSessions( } const SessionsModalContent: FunctionComponent<{ - viewControllerManager: ViewControllerManager - application: SNApplication -}> = ({ viewControllerManager, application }) => { + application: WebApplication +}> = ({ application }) => { const [sessions, refresh, refreshing, revokeSession, errorMessage] = useSessions(application) const [confirmRevokingSessionUuid, setRevokingSessionUuid] = useState('') @@ -115,7 +113,7 @@ const SessionsModalContent: FunctionComponent<{ (): ModalAction[] => [ { label: 'Close', - onClick: viewControllerManager.closeSessionsModal, + onClick: application.closeSessionsModal, type: 'cancel', mobileSlot: 'left', }, @@ -126,14 +124,14 @@ const SessionsModalContent: FunctionComponent<{ mobileSlot: 'right', }, ], - [refresh, viewControllerManager.closeSessionsModal], + [refresh, application.closeSessionsModal], ) return ( <> = ({ viewControllerManager, application }) => { +}> = ({ application }) => { return ( - - + + ) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts b/packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts index f663f0565..0d85b280c 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts @@ -37,7 +37,7 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS if (uploadedFile) { editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid) void linkingController.linkItemToSelectedItem(uploadedFile) - void application.changeAndSaveItem(uploadedFile, (mutator) => { + void application.changeAndSaveItem.execute(uploadedFile, (mutator) => { mutator.protected = currentNote.protected }) } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts index 23d54763e..5003b4ba6 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts @@ -23,14 +23,16 @@ export const ExportPlugin = () => { const downloadData = useCallback( (data: Blob, fileName: string) => { if (!application.isNativeMobileWeb()) { - application.getArchiveService().downloadData(data, fileName) + application.archiveService.downloadData(data, fileName) return } if (application.platform === Platform.Android) { - downloadBlobOnAndroid(application, data, fileName).catch(console.error) + downloadBlobOnAndroid(application.mobileDevice, data, fileName).catch(console.error) } else { - shareBlobOnMobile(application, data, fileName).catch(console.error) + shareBlobOnMobile(application.mobileDevice, application.isNativeMobileWeb(), data, fileName).catch( + console.error, + ) } }, [application], diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ReadonlyPlugin/ReadonlyPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ReadonlyPlugin/ReadonlyPlugin.tsx index 9f5f35443..b99660f25 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ReadonlyPlugin/ReadonlyPlugin.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ReadonlyPlugin/ReadonlyPlugin.tsx @@ -9,7 +9,7 @@ const ReadonlyPlugin = ({ note }: { note: SNNote }) => { const [readOnly, setReadOnly] = useState(note.locked) useEffect(() => { - return application.streamItems(ContentType.TYPES.Note, ({ changed }) => { + return application.items.streamItems(ContentType.TYPES.Note, ({ changed }) => { const changedNoteItem = changed.find((changedItem) => changedItem.uuid === note.uuid) if (changedNoteItem) { diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/RemoteImagePlugin/RemoteImageComponent.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/RemoteImagePlugin/RemoteImageComponent.tsx index 8580972a5..adcd6b35c 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/RemoteImagePlugin/RemoteImageComponent.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/RemoteImagePlugin/RemoteImageComponent.tsx @@ -1,7 +1,7 @@ import { useApplication } from '@/Components/ApplicationProvider' import Icon from '@/Components/Icon/Icon' import Spinner from '@/Components/Spinner/Spinner' -import { isDesktopApplication, isIOS } from '@/Utils' +import { isDesktopApplication } from '@/Utils' import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { classNames, Platform } from '@standardnotes/snjs' @@ -9,6 +9,7 @@ import { ElementFormatType, NodeKey } from 'lexical' import { useCallback, useState } from 'react' import { $createFileNode } from '../EncryptedFilePlugin/Nodes/FileUtils' import { RemoteImageNode } from './RemoteImageNode' +import { isIOS } from '@standardnotes/ui-services' type Props = { src: string @@ -41,7 +42,7 @@ const RemoteImageComponent = ({ className, src, alt, node, format, nodeKey }: Pr const blob = await response.blob() const file = new File([blob], src, { type: blob.type }) - const { filesController, linkingController } = application.controllers + const { filesController, linkingController } = application const uploadedFile = await filesController.uploadNewFile(file, false) diff --git a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx index 8fcfcf046..2444e8107 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx @@ -94,7 +94,17 @@ const SuperNoteConverter = ({ const performConvert = useCallback( async (text: string) => { - const controller = new NoteViewController(application, note) + const controller = new NoteViewController( + note, + application.items, + application.mutator, + application.sync, + application.sessions, + application.preferences, + application.componentManager, + application.alerts, + application.isNativeMobileWebUseCase, + ) await controller.initialize() await controller.saveAndAwaitLocalPropagation({ text, diff --git a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteImporter.tsx b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteImporter.tsx index 9b56c7a46..27eff3696 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteImporter.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteImporter.tsx @@ -43,7 +43,17 @@ export const SuperNoteImporter: FunctionComponent = ({ note, application, const performConvert = useCallback( async (text: string, previewPlain: string) => { - const controller = new NoteViewController(application, note) + const controller = new NoteViewController( + note, + application.items, + application.mutator, + application.sync, + application.sessions, + application.preferences, + application.componentManager, + application.alerts, + application.isNativeMobileWebUseCase, + ) await controller.initialize() await controller.saveAndAwaitLocalPropagation({ text: text, diff --git a/packages/web/src/javascripts/Components/Tags/Navigation.tsx b/packages/web/src/javascripts/Components/Tags/Navigation.tsx index c983bba2c..4d69582b7 100644 --- a/packages/web/src/javascripts/Components/Tags/Navigation.tsx +++ b/packages/web/src/javascripts/Components/Tags/Navigation.tsx @@ -3,7 +3,7 @@ import TagsSection from '@/Components/Tags/TagsSection' import { WebApplication } from '@/Application/WebApplication' import { ApplicationEvent, PrefKey, WebAppEvent } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { forwardRef, useEffect, useMemo, useState } from 'react' +import { forwardRef, useEffect, useState } from 'react' import { classNames } from '@standardnotes/utils' import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider' import UpgradeNow from '../Footer/UpgradeNow' @@ -24,7 +24,6 @@ type Props = { } const Navigation = forwardRef(({ application, className, children, id }, ref) => { - const viewControllerManager = useMemo(() => application.controllers, [application]) const { setPaneLayout } = useResponsiveAppPane() const [hasPasscode, setHasPasscode] = useState(() => application.hasPasscode()) @@ -79,10 +78,10 @@ const Navigation = forwardRef(({ application, className, > - +
(({ application, className, /> { - viewControllerManager.accountMenuController.toggleShow() + application.accountMenuController.toggleShow() }} label="Go to account menu" icon="account-circle" @@ -124,7 +123,7 @@ const Navigation = forwardRef(({ application, className, { - viewControllerManager.preferencesController.openPreferences() + application.preferencesController.openPreferences() }} label="Go to preferences" icon="tune" @@ -132,7 +131,7 @@ const Navigation = forwardRef(({ application, className, { - viewControllerManager.quickSettingsMenuController.toggle() + application.quickSettingsMenuController.toggle() }} label="Go to quick settings menu" icon="themes" @@ -141,7 +140,7 @@ const Navigation = forwardRef(({ application, className, { - viewControllerManager.vaultSelectionController.toggle() + application.vaultSelectionController.toggle() }} label="Go to vaults menu" icon="safe-square" diff --git a/packages/web/src/javascripts/Components/Tags/SmartViewsListItem.tsx b/packages/web/src/javascripts/Components/Tags/SmartViewsListItem.tsx index c00590ad9..9f6441bed 100644 --- a/packages/web/src/javascripts/Components/Tags/SmartViewsListItem.tsx +++ b/packages/web/src/javascripts/Components/Tags/SmartViewsListItem.tsx @@ -101,7 +101,7 @@ const SmartViewsListItem: FunctionComponent = ({ view, tagsState, setEdit return } - return application.streamItems(ContentType.TYPES.Note, () => { + return application.items.streamItems(ContentType.TYPES.Note, () => { setConflictsCount(application.items.numberOfNotesWithConflicts()) }) }, [application, view]) diff --git a/packages/web/src/javascripts/Components/Tags/TagContextMenu.tsx b/packages/web/src/javascripts/Components/Tags/TagContextMenu.tsx index 5a3834713..9d1179360 100644 --- a/packages/web/src/javascripts/Components/Tags/TagContextMenu.tsx +++ b/packages/web/src/javascripts/Components/Tags/TagContextMenu.tsx @@ -14,6 +14,7 @@ import Popover from '../Popover/Popover' import IconPicker from '../Icon/IconPicker' import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption' import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' +import { useApplication } from '../ApplicationProvider' type ContextMenuProps = { navigationController: NavigationController @@ -22,9 +23,11 @@ type ContextMenuProps = { } const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag }: ContextMenuProps) => { + const application = useApplication() + const premiumModal = usePremiumModal() - const { contextMenuOpen, contextMenuClickLocation, application } = navigationController + const { contextMenuOpen, contextMenuClickLocation } = navigationController const contextMenuRef = useRef(null) diff --git a/packages/web/src/javascripts/Components/Tags/TagsList.tsx b/packages/web/src/javascripts/Components/Tags/TagsList.tsx index 983c8a733..7b4062199 100644 --- a/packages/web/src/javascripts/Components/Tags/TagsList.tsx +++ b/packages/web/src/javascripts/Components/Tags/TagsList.tsx @@ -1,38 +1,39 @@ -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { SNTag } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback } from 'react' import RootTagDropZone from './RootTagDropZone' import { TagListSectionType } from './TagListSection' import { TagsListItem } from './TagsListItem' +import { useApplication } from '../ApplicationProvider' type Props = { - viewControllerManager: ViewControllerManager type: TagListSectionType } -const TagsList: FunctionComponent = ({ viewControllerManager, type }: Props) => { - const navigationController = viewControllerManager.navigationController - const allTags = type === 'all' ? navigationController.allLocalRootTags : navigationController.starredTags +const TagsList: FunctionComponent = ({ type }: Props) => { + const application = useApplication() + + const allTags = + type === 'all' ? application.navigationController.allLocalRootTags : application.navigationController.starredTags const openTagContextMenu = useCallback( (posX: number, posY: number) => { - viewControllerManager.navigationController.setContextMenuClickLocation({ + application.navigationController.setContextMenuClickLocation({ x: posX, y: posY, }) - viewControllerManager.navigationController.reloadContextMenuLayout() - viewControllerManager.navigationController.setContextMenuOpen(true) + application.navigationController.reloadContextMenuLayout() + application.navigationController.setContextMenuOpen(true) }, - [viewControllerManager], + [application], ) const onContextMenu = useCallback( (tag: SNTag, posX: number, posY: number) => { - void viewControllerManager.navigationController.setSelectedTag(tag, type) + void application.navigationController.setSelectedTag(tag, type) openTagContextMenu(posX, posY) }, - [viewControllerManager, openTagContextMenu, type], + [application, openTagContextMenu, type], ) return ( @@ -50,14 +51,14 @@ const TagsList: FunctionComponent = ({ viewControllerManager, type }: Pro key={tag.uuid} tag={tag} type={type} - navigationController={navigationController} - features={viewControllerManager.featuresController} - linkingController={viewControllerManager.linkingController} + navigationController={application.navigationController} + features={application.featuresController} + linkingController={application.linkingController} onContextMenu={onContextMenu} /> ) })} - {type === 'all' && } + {type === 'all' && } )} diff --git a/packages/web/src/javascripts/Components/Tags/TagsSection.tsx b/packages/web/src/javascripts/Components/Tags/TagsSection.tsx index 766c229bc..5ee9dcf58 100644 --- a/packages/web/src/javascripts/Components/Tags/TagsSection.tsx +++ b/packages/web/src/javascripts/Components/Tags/TagsSection.tsx @@ -1,24 +1,22 @@ import TagsList from '@/Components/Tags/TagsList' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { ApplicationEvent } from '@/__mocks__/@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useEffect, useState } from 'react' import TagsSectionAddButton from './TagsSectionAddButton' import TagsSectionTitle from './TagsSectionTitle' +import { useApplication } from '../ApplicationProvider' -type Props = { - viewControllerManager: ViewControllerManager -} +const TagsSection: FunctionComponent = () => { + const application = useApplication() -const TagsSection: FunctionComponent = ({ viewControllerManager }) => { const [hasMigration, setHasMigration] = useState(false) const checkIfMigrationNeeded = useCallback(() => { - setHasMigration(viewControllerManager.application.items.hasTagsNeedingFoldersMigration()) - }, [viewControllerManager]) + setHasMigration(application.items.hasTagsNeedingFoldersMigration()) + }, [application]) useEffect(() => { - const removeObserver = viewControllerManager.application.addEventObserver(async (event) => { + const removeObserver = application.addEventObserver(async (event) => { const events = [ApplicationEvent.CompletedInitialSync, ApplicationEvent.SignedIn] if (events.includes(event)) { checkIfMigrationNeeded() @@ -28,11 +26,11 @@ const TagsSection: FunctionComponent = ({ viewControllerManager }) => { return () => { removeObserver() } - }, [viewControllerManager, checkIfMigrationNeeded]) + }, [application, checkIfMigrationNeeded]) const runMigration = useCallback(async () => { if ( - await viewControllerManager.application.alerts.confirm( + await application.alerts.confirm( 'Introducing native, built-in nested tags without requiring the legacy Folders extension.

' + " To get started, we'll need to migrate any tags containing a dot character to the new system.

" + ' This migration will convert any tags with dots appearing in their name into a natural' + @@ -42,19 +40,19 @@ const TagsSection: FunctionComponent = ({ viewControllerManager }) => { 'Run Migration', ) ) { - viewControllerManager.application.mutator + application.mutator .migrateTagsToFolders() .then(() => { - void viewControllerManager.application.sync.sync() + void application.sync.sync() checkIfMigrationNeeded() }) .catch(console.error) } - }, [viewControllerManager, checkIfMigrationNeeded]) + }, [application, checkIfMigrationNeeded]) return ( <> - {viewControllerManager.navigationController.starredTags.length > 0 && ( + {application.navigationController.starredTags.length > 0 && (
@@ -63,7 +61,7 @@ const TagsSection: FunctionComponent = ({ viewControllerManager }) => {
- + )} @@ -71,17 +69,14 @@ const TagsSection: FunctionComponent = ({ viewControllerManager }) => {
- +
- + ) diff --git a/packages/web/src/javascripts/Controllers/Abstract/AbstractViewController.ts b/packages/web/src/javascripts/Controllers/Abstract/AbstractViewController.ts index 3eb0e631d..c564bd2a0 100644 --- a/packages/web/src/javascripts/Controllers/Abstract/AbstractViewController.ts +++ b/packages/web/src/javascripts/Controllers/Abstract/AbstractViewController.ts @@ -1,6 +1,5 @@ import { CrossControllerEvent } from '../CrossControllerEvent' import { InternalEventBusInterface, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs' -import { WebApplication } from '../../Application/WebApplication' import { Disposer } from '@/Types/Disposer' type ControllerEventObserver = (event: Event, data: EventData) => void @@ -10,10 +9,7 @@ export abstract class AbstractViewController { protected disposers: Disposer[] = [] private eventObservers: ControllerEventObserver[] = [] - constructor( - public application: WebApplication, - protected eventBus: InternalEventBusInterface, - ) {} + constructor(protected eventBus: InternalEventBusInterface) {} protected async publishCrossControllerEventSync(name: CrossControllerEvent, data?: unknown): Promise { await this.eventBus.publishSync({ type: name, payload: data }, InternalEventPublishStrategy.SEQUENCE) @@ -21,7 +17,6 @@ export abstract class AbstractViewController { deinit(): void { this.dealloced = true - ;(this.application as unknown) = undefined ;(this.eventBus as unknown) = undefined for (const disposer of this.disposers) { diff --git a/packages/web/src/javascripts/Controllers/Abstract/PersistenceService.ts b/packages/web/src/javascripts/Controllers/Abstract/PersistenceService.ts index 1dd5e63b8..753ef985f 100644 --- a/packages/web/src/javascripts/Controllers/Abstract/PersistenceService.ts +++ b/packages/web/src/javascripts/Controllers/Abstract/PersistenceService.ts @@ -1,44 +1,72 @@ -import { WebApplication } from '@/Application/WebApplication' import { ShouldPersistNoteStateKey } from '@/Components/Preferences/Panes/General/Persistence' -import { ApplicationEvent, ContentType, InternalEventBusInterface } from '@standardnotes/snjs' -import { PersistedStateValue, StorageKey } from '@standardnotes/ui-services' +import { + ApplicationEvent, + ContentType, + InternalEventBusInterface, + InternalEventHandlerInterface, + InternalEventInterface, + ItemManagerInterface, + StorageServiceInterface, + SyncServiceInterface, +} from '@standardnotes/snjs' +import { PersistedStateValue, PersistenceKey, StorageKey } from '@standardnotes/ui-services' import { CrossControllerEvent } from '../CrossControllerEvent' +import { NavigationController } from '../Navigation/NavigationController' +import { ItemListController } from '../ItemList/ItemListController' -export class PersistenceService { - private unsubAppEventObserver: () => void +export class PersistenceService implements InternalEventHandlerInterface { private didHydrateOnce = false constructor( - private application: WebApplication, + private itemListController: ItemListController, + private navigationController: NavigationController, + private storage: StorageServiceInterface, + private items: ItemManagerInterface, + private sync: SyncServiceInterface, private eventBus: InternalEventBusInterface, ) { - this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => { - if (!this.application) { - return - } - - void this.onAppEvent(eventName) - }) + eventBus.addEventHandler(this, ApplicationEvent.LocalDataLoaded) + eventBus.addEventHandler(this, ApplicationEvent.LocalDataIncrementalLoad) + eventBus.addEventHandler(this, CrossControllerEvent.HydrateFromPersistedValues) + eventBus.addEventHandler(this, CrossControllerEvent.RequestValuePersistence) } - async onAppEvent(eventName: ApplicationEvent) { - if (eventName === ApplicationEvent.LocalDataLoaded && !this.didHydrateOnce) { - this.hydratePersistedValues() - this.didHydrateOnce = true - } else if (eventName === ApplicationEvent.LocalDataIncrementalLoad) { - const canHydrate = this.application.items.getItems([ContentType.TYPES.Note, ContentType.TYPES.Tag]).length > 0 - - if (!canHydrate) { - return + async handleEvent(event: InternalEventInterface): Promise { + switch (event.type) { + case ApplicationEvent.LocalDataLoaded: { + if (!this.didHydrateOnce) { + this.hydratePersistedValues() + this.didHydrateOnce = true + } + break } - this.hydratePersistedValues() - this.didHydrateOnce = true + case ApplicationEvent.LocalDataIncrementalLoad: { + const canHydrate = this.items.getItems([ContentType.TYPES.Note, ContentType.TYPES.Tag]).length > 0 + + if (!canHydrate) { + return + } + + this.hydratePersistedValues() + this.didHydrateOnce = true + break + } + + case CrossControllerEvent.HydrateFromPersistedValues: { + this.hydrateFromPersistedValues(event.payload as PersistedStateValue | undefined) + break + } + + case CrossControllerEvent.RequestValuePersistence: { + this.persistCurrentState() + break + } } } get persistenceEnabled() { - return this.application.getValue(ShouldPersistNoteStateKey) ?? true + return this.storage.getValue(ShouldPersistNoteStateKey) ?? true } hydratePersistedValues = () => { @@ -48,8 +76,37 @@ export class PersistenceService { }) } + persistCurrentState(): void { + const values: PersistedStateValue = { + [PersistenceKey.ItemListController]: this.itemListController.getPersistableValue(), + [PersistenceKey.NavigationController]: this.navigationController.getPersistableValue(), + } + + this.persistValues(values) + + const selectedItemsState = values['selected-items-controller'] + const navigationSelectionState = values['navigation-controller'] + const launchPriorityUuids: string[] = [] + if (selectedItemsState.selectedUuids.length) { + launchPriorityUuids.push(...selectedItemsState.selectedUuids) + } + if (navigationSelectionState.selectedTagUuid) { + launchPriorityUuids.push(navigationSelectionState.selectedTagUuid) + } + + this.sync.setLaunchPriorityUuids(launchPriorityUuids) + } + + hydrateFromPersistedValues(values: PersistedStateValue | undefined): void { + const navigationState = values?.[PersistenceKey.NavigationController] + this.navigationController.hydrateFromPersistedValue(navigationState) + + const selectedItemsState = values?.[PersistenceKey.ItemListController] + this.itemListController.hydrateFromPersistedValue(selectedItemsState) + } + persistValues(values: PersistedStateValue): void { - if (!this.application.isDatabaseLoaded()) { + if (!this.sync.isDatabaseLoaded()) { return } @@ -57,22 +114,18 @@ export class PersistenceService { return } - this.application.setValue(StorageKey.MasterStatePersistenceKey, values) + this.storage.setValue(StorageKey.MasterStatePersistenceKey, values) } clearPersistedValues(): void { - if (!this.application.isDatabaseLoaded()) { + if (!this.sync.isDatabaseLoaded()) { return } - void this.application.removeValue(StorageKey.MasterStatePersistenceKey) + void this.storage.removeValue(StorageKey.MasterStatePersistenceKey) } getPersistedValues(): PersistedStateValue { - return this.application.getValue(StorageKey.MasterStatePersistenceKey) as PersistedStateValue - } - - deinit() { - this.unsubAppEventObserver() + return this.storage.getValue(StorageKey.MasterStatePersistenceKey) as PersistedStateValue } } diff --git a/packages/web/src/javascripts/Controllers/AccountMenu/AccountMenuController.ts b/packages/web/src/javascripts/Controllers/AccountMenu/AccountMenuController.ts index 23a7f45c9..cb06cbb4e 100644 --- a/packages/web/src/javascripts/Controllers/AccountMenu/AccountMenuController.ts +++ b/packages/web/src/javascripts/Controllers/AccountMenu/AccountMenuController.ts @@ -1,11 +1,20 @@ -import { destroyAllObjectProperties, isDev } from '@/Utils' +import { destroyAllObjectProperties } from '@/Utils' import { action, computed, makeObservable, observable, runInAction } from 'mobx' -import { ApplicationEvent, ContentType, InternalEventBusInterface, SNNote, SNTag } from '@standardnotes/snjs' -import { WebApplication } from '@/Application/WebApplication' +import { + ApplicationEvent, + ContentType, + GetHost, + InternalEventBusInterface, + InternalEventHandlerInterface, + InternalEventInterface, + ItemManagerInterface, + SNNote, + SNTag, +} from '@standardnotes/snjs' import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane' import { AbstractViewController } from '../Abstract/AbstractViewController' -export class AccountMenuController extends AbstractViewController { +export class AccountMenuController extends AbstractViewController implements InternalEventHandlerInterface { show = false signingOut = false otherSessionsSignOut = false @@ -28,8 +37,12 @@ export class AccountMenuController extends AbstractViewController { destroyAllObjectProperties(this) } - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor( + private items: ItemManagerInterface, + private _getHost: GetHost, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) makeObservable(this, { show: observable, @@ -63,27 +76,26 @@ export class AccountMenuController extends AbstractViewController { notesAndTagsCount: computed, }) - this.disposers.push( - this.application.addEventObserver(async () => { - runInAction(() => { - if (isDev && window.devAccountServer) { - this.setServer(window.devAccountServer) - this.application.setCustomHost(window.devAccountServer).catch(console.error) - } else { - this.setServer(this.application.getHost()) - } - }) - }, ApplicationEvent.Launched), - ) + eventBus.addEventHandler(this, ApplicationEvent.Launched) this.disposers.push( - this.application.streamItems([ContentType.TYPES.Note, ContentType.TYPES.Tag], () => { + this.items.streamItems([ContentType.TYPES.Note, ContentType.TYPES.Tag], () => { runInAction(() => { - this.notesAndTags = this.application.items.getItems([ContentType.TYPES.Note, ContentType.TYPES.Tag]) + this.notesAndTags = this.items.getItems([ContentType.TYPES.Note, ContentType.TYPES.Tag]) }) }), ) } + async handleEvent(event: InternalEventInterface): Promise { + switch (event.type) { + case ApplicationEvent.Launched: { + runInAction(() => { + this.setServer(this._getHost.execute().getValue()) + }) + break + } + } + } setShow = (show: boolean): void => { this.show = show diff --git a/packages/web/src/javascripts/Controllers/CrossControllerEvent.ts b/packages/web/src/javascripts/Controllers/CrossControllerEvent.ts index 7baad84fc..9cc4fbeea 100644 --- a/packages/web/src/javascripts/Controllers/CrossControllerEvent.ts +++ b/packages/web/src/javascripts/Controllers/CrossControllerEvent.ts @@ -1,7 +1,8 @@ export enum CrossControllerEvent { - TagChanged = 'TagChanged', - ActiveEditorChanged = 'ActiveEditorChanged', - HydrateFromPersistedValues = 'HydrateFromPersistedValues', - RequestValuePersistence = 'RequestValuePersistence', - DisplayPremiumModal = 'DisplayPremiumModal', + TagChanged = 'CrossControllerEvent:TagChanged', + ActiveEditorChanged = 'CrossControllerEvent:ActiveEditorChanged', + HydrateFromPersistedValues = 'CrossControllerEvent:HydrateFromPersistedValues', + RequestValuePersistence = 'CrossControllerEvent:RequestValuePersistence', + DisplayPremiumModal = 'CrossControllerEvent:DisplayPremiumModal', + UnselectAllNotes = 'CrossControllerEvent:UnselectAllNotes', } diff --git a/packages/web/src/javascripts/Controllers/FeaturesController.ts b/packages/web/src/javascripts/Controllers/FeaturesController.ts index 2bc59a56c..6675bb040 100644 --- a/packages/web/src/javascripts/Controllers/FeaturesController.ts +++ b/packages/web/src/javascripts/Controllers/FeaturesController.ts @@ -1,5 +1,5 @@ +import { FeaturesClientInterface, InternalEventHandlerInterface } from '@standardnotes/services' import { FeatureName } from './FeatureName' -import { WebApplication } from '@/Application/WebApplication' import { PremiumFeatureModalType } from '@/Components/PremiumFeaturesModal/PremiumFeatureModalType' import { destroyAllObjectProperties } from '@/Utils' import { @@ -13,7 +13,7 @@ import { action, makeObservable, observable, runInAction, when } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' import { CrossControllerEvent } from './CrossControllerEvent' -export class FeaturesController extends AbstractViewController { +export class FeaturesController extends AbstractViewController implements InternalEventHandlerInterface { hasFolders: boolean hasSmartViews: boolean entitledToFiles: boolean @@ -33,16 +33,17 @@ export class FeaturesController extends AbstractViewController { destroyAllObjectProperties(this) } - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor( + private features: FeaturesClientInterface, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) this.hasFolders = this.isEntitledToFolders() this.hasSmartViews = this.isEntitledToSmartViews() this.entitledToFiles = this.isEntitledToFiles() this.premiumAlertFeatureName = undefined - eventBus.addEventHandler(this, CrossControllerEvent.DisplayPremiumModal) - makeObservable(this, { hasFolders: observable, hasSmartViews: observable, @@ -54,33 +55,38 @@ export class FeaturesController extends AbstractViewController { showPurchaseSuccessAlert: action, }) + eventBus.addEventHandler(this, CrossControllerEvent.DisplayPremiumModal) + eventBus.addEventHandler(this, ApplicationEvent.DidPurchaseSubscription) + eventBus.addEventHandler(this, ApplicationEvent.FeaturesAvailabilityChanged) + eventBus.addEventHandler(this, ApplicationEvent.Launched) + eventBus.addEventHandler(this, ApplicationEvent.LocalDataLoaded) + eventBus.addEventHandler(this, ApplicationEvent.UserRolesChanged) + this.showPremiumAlert = this.showPremiumAlert.bind(this) this.closePremiumAlert = this.closePremiumAlert.bind(this) - - this.disposers.push( - application.addEventObserver(async (event) => { - switch (event) { - case ApplicationEvent.DidPurchaseSubscription: - this.showPurchaseSuccessAlert() - break - case ApplicationEvent.FeaturesAvailabilityChanged: - case ApplicationEvent.Launched: - case ApplicationEvent.LocalDataLoaded: - case ApplicationEvent.UserRolesChanged: - runInAction(() => { - this.hasFolders = this.isEntitledToFolders() - this.hasSmartViews = this.isEntitledToSmartViews() - this.entitledToFiles = this.isEntitledToFiles() - }) - } - }), - ) } async handleEvent(event: InternalEventInterface): Promise { - if (event.type === CrossControllerEvent.DisplayPremiumModal) { - const payload = event.payload as { featureName: string } - void this.showPremiumAlert(payload.featureName) + switch (event.type) { + case ApplicationEvent.DidPurchaseSubscription: + this.showPurchaseSuccessAlert() + break + case ApplicationEvent.FeaturesAvailabilityChanged: + case ApplicationEvent.Launched: + case ApplicationEvent.LocalDataLoaded: + case ApplicationEvent.UserRolesChanged: + runInAction(() => { + this.hasFolders = this.isEntitledToFolders() + this.hasSmartViews = this.isEntitledToSmartViews() + this.entitledToFiles = this.isEntitledToFiles() + }) + break + case CrossControllerEvent.DisplayPremiumModal: + { + const payload = event.payload as { featureName: string } + void this.showPremiumAlert(payload.featureName) + } + break } } @@ -100,7 +106,7 @@ export class FeaturesController extends AbstractViewController { } private isEntitledToFiles(): boolean { - const status = this.application.features.getFeatureStatus( + const status = this.features.getFeatureStatus( NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Files).getValue(), ) @@ -108,7 +114,7 @@ export class FeaturesController extends AbstractViewController { } private isEntitledToFolders(): boolean { - const status = this.application.features.getFeatureStatus( + const status = this.features.getFeatureStatus( NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TagNesting).getValue(), ) @@ -116,7 +122,7 @@ export class FeaturesController extends AbstractViewController { } private isEntitledToSmartViews(): boolean { - const status = this.application.features.getFeatureStatus( + const status = this.features.getFeatureStatus( NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SmartFilters).getValue(), ) diff --git a/packages/web/src/javascripts/Controllers/FilePreviewModalController.ts b/packages/web/src/javascripts/Controllers/FilePreviewModalController.ts index 3e3cfa47c..f0e7e670d 100644 --- a/packages/web/src/javascripts/Controllers/FilePreviewModalController.ts +++ b/packages/web/src/javascripts/Controllers/FilePreviewModalController.ts @@ -1,5 +1,4 @@ -import { WebApplication } from '@/Application/WebApplication' -import { ContentType, FileItem } from '@standardnotes/snjs' +import { ContentType, FileItem, ItemManagerInterface } from '@standardnotes/snjs' import { action, makeObservable, observable } from 'mobx' export class FilePreviewModalController { @@ -9,7 +8,7 @@ export class FilePreviewModalController { eventObservers: (() => void)[] = [] - constructor(application: WebApplication) { + constructor(items: ItemManagerInterface) { makeObservable(this, { isOpen: observable, currentFile: observable, @@ -21,7 +20,7 @@ export class FilePreviewModalController { }) this.eventObservers.push( - application.streamItems(ContentType.TYPES.File, ({ changed, removed }) => { + items.streamItems(ContentType.TYPES.File, ({ changed, removed }) => { if (!this.currentFile) { return } diff --git a/packages/web/src/javascripts/Controllers/FilesController.ts b/packages/web/src/javascripts/Controllers/FilesController.ts index 7e71345e9..24ecec228 100644 --- a/packages/web/src/javascripts/Controllers/FilesController.ts +++ b/packages/web/src/javascripts/Controllers/FilesController.ts @@ -1,12 +1,18 @@ import { FileDownloadProgress, fileProgressToHumanReadableString, + FilesClientInterface, OnChunkCallbackNoProgress, } from '@standardnotes/files' import { FilePreviewModalController } from './FilePreviewModalController' import { FileItemAction, FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction' import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants' -import { confirmDialog } from '@standardnotes/ui-services' +import { + ArchiveManager, + confirmDialog, + IsNativeMobileWeb, + VaultDisplayServiceInterface, +} from '@standardnotes/ui-services' import { Strings, StringUtils } from '@/Constants/Strings' import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays' import { @@ -17,17 +23,22 @@ import { parseFileName, } from '@standardnotes/filepicker' import { + AlertService, ChallengeReason, ClientDisplayableError, ContentType, FileItem, InternalEventBusInterface, isFile, + ItemManagerInterface, + MobileDeviceInterface, + MutatorClientInterface, Platform, + ProtectionsClientInterface, + SyncServiceInterface, } from '@standardnotes/snjs' import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/toast' import { action, makeObservable, observable, reaction } from 'mobx' -import { WebApplication } from '../Application/WebApplication' import { AbstractViewController } from './Abstract/AbstractViewController' import { NotesController } from './NotesController/NotesController' import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform' @@ -65,12 +76,22 @@ export class FilesController extends AbstractViewController { + items.streamItems(ContentType.TYPES.File, () => { this.reloadAllFiles() this.reloadAttachedFiles() }), @@ -117,13 +138,13 @@ export class FilesController extends AbstractViewController { - this.allFiles = this.application.items.getDisplayableFiles() + this.allFiles = this.items.getDisplayableFiles() } reloadAttachedFiles = () => { const note = this.notesController.firstSelectedNote if (note) { - this.attachedFiles = this.application.items.itemsReferencingItem(note).filter(isFile) + this.attachedFiles = this.items.itemsReferencingItem(note).filter(isFile) } } @@ -137,7 +158,7 @@ export class FilesController extends AbstractViewController { @@ -169,31 +190,31 @@ export class FilesController extends AbstractViewController { let result: FileItem | undefined if (file.protected) { - result = await this.application.protections.unprotectFile(file) + result = await this.protections.unprotectFile(file) } else { - result = await this.application.protections.protectFile(file) + result = await this.protections.protectFile(file) } - void this.application.sync.sync() + void this.sync.sync() const isProtected = result ? result.protected : file.protected return isProtected } authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => { - const authorizedFiles = await this.application.protections.authorizeProtectedActionForItems([file], challengeReason) + const authorizedFiles = await this.protections.authorizeProtectedActionForItems([file], challengeReason) const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) return isAuthorized } renameFile = async (file: FileItem, fileName: string) => { - await this.application.mutator.renameFile(file, fileName) - void this.application.sync.sync() + await this.mutator.renameFile(file, fileName) + void this.sync.sync() } handleFileAction = async ( @@ -243,7 +264,7 @@ export class FilesController extends AbstractViewController { + const result = await this.files.downloadFile(file, async (decryptedBytes, progress) => { if (isUsingStreamingSaver) { await saver.pushBytes(decryptedBytes) } else { @@ -301,7 +322,16 @@ export class FilesController extends AbstractViewController { if (!this.shouldUseStreamingReader && this.maxFileSize && file.size >= this.maxFileSize) { - this.application.alerts + this.alerts .alert( `This file exceeds the limits supported in this browser. To upload files greater than ${ this.maxFileSize / BYTES_IN_ONE_MEGABYTE @@ -360,7 +390,7 @@ export class FilesController extends AbstractViewController { - await this.application.files.pushBytesForUpload(operation, data, index, isLast) + await this.files.pushBytesForUpload(operation, data, index, isLast) const percentComplete = Math.round(operation.getProgress().percentComplete) if (toastId) { @@ -416,10 +446,10 @@ export class FilesController extends AbstractViewController this.application.files.deleteFile(file))) - void this.application.sync.sync() + await Promise.all(files.map((file) => this.files.deleteFile(file))) + void this.sync.sync() } } setProtectionForFiles = async (protect: boolean, files: FileItem[]) => { if (protect) { - const protectedItems = await this.application.protections.protectItems(files) + const protectedItems = await this.protections.protectItems(files) if (protectedItems) { this.setShowProtectedOverlay(true) } } else { - const unprotectedItems = await this.application.protections.unprotectItems(files, ChallengeReason.UnprotectFile) + const unprotectedItems = await this.protections.unprotectItems(files, ChallengeReason.UnprotectFile) if (unprotectedItems) { this.setShowProtectedOverlay(false) } } - void this.application.sync.sync() + void this.sync.sync() } downloadFiles = async (files: FileItem[]) => { - if (this.application.platform === Platform.MacDesktop) { + if (this.platform === Platform.MacDesktop) { for (const file of files) { await this.handleFileAction({ type: FileItemActionType.DownloadFile, diff --git a/packages/web/src/javascripts/Controllers/ImportModalController.ts b/packages/web/src/javascripts/Controllers/ImportModalController.ts index cbb8c530a..8c92acd64 100644 --- a/packages/web/src/javascripts/Controllers/ImportModalController.ts +++ b/packages/web/src/javascripts/Controllers/ImportModalController.ts @@ -1,6 +1,11 @@ -import { WebApplication } from '@/Application/WebApplication' import { DecryptedTransferPayload, SNTag, TagContent } from '@standardnotes/models' -import { ContentType, pluralize, UuidGenerator } from '@standardnotes/snjs' +import { + ContentType, + ItemManagerInterface, + MutatorClientInterface, + pluralize, + UuidGenerator, +} from '@standardnotes/snjs' import { Importer, NoteImportType } from '@standardnotes/ui-services' import { action, makeObservable, observable } from 'mobx' import { NavigationController } from './Navigation/NavigationController' @@ -23,13 +28,14 @@ export type ImportModalFile = ( export class ImportModalController { isVisible = false - importer: Importer files: ImportModalFile[] = [] importTag: SNTag | undefined = undefined constructor( - private application: WebApplication, + private importer: Importer, private navigationController: NavigationController, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, ) { makeObservable(this, { isVisible: observable, @@ -43,8 +49,6 @@ export class ImportModalController { importTag: observable, setImportTag: action, }) - - this.importer = new Importer(application) } setIsVisible = (isVisible: boolean) => { @@ -152,7 +156,7 @@ export class ImportModalController { } } const currentDate = new Date() - const importTagItem = this.application.items.createTemplateItem(ContentType.TYPES.Tag, { + const importTagItem = this.items.createTemplateItem(ContentType.TYPES.Tag, { title: `Imported on ${currentDate.toLocaleString()}`, expanded: false, iconString: '', @@ -163,7 +167,7 @@ export class ImportModalController { uuid: payload.uuid, })), }) - const importTag = await this.application.mutator.insertItem(importTagItem) + const importTag = await this.mutator.insertItem(importTagItem) if (importTag) { this.setImportTag(importTag as SNTag) } diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.spec.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.spec.ts index c72fe391b..15c93efae 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.spec.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.spec.ts @@ -1,39 +1,51 @@ -import { ContentType, SNTag } from '@standardnotes/snjs' -import { InternalEventBus } from '@standardnotes/services' +import { ContentType, Result, SNTag } from '@standardnotes/snjs' +import { InternalEventBus, ItemManagerInterface } from '@standardnotes/services' import { WebApplication } from '@/Application/WebApplication' import { NavigationController } from '../Navigation/NavigationController' import { NotesController } from '../NotesController/NotesController' import { SearchOptionsController } from '../SearchOptionsController' -import { SelectedItemsController } from '../SelectedItemsController' import { ItemListController } from './ItemListController' import { ItemsReloadSource } from './ItemsReloadSource' +import { IsNativeMobileWeb } from '@standardnotes/ui-services' +import { runInAction } from 'mobx' describe('item list controller', () => { let application: WebApplication let controller: ItemListController - let navigationController: NavigationController - let selectionController: SelectedItemsController beforeEach(() => { - application = {} as jest.Mocked - application.streamItems = jest.fn() + application = { + navigationController: {} as jest.Mocked, + searchOptionsController: {} as jest.Mocked, + notesController: {} as jest.Mocked, + isNativeMobileWebUseCase: { + execute: jest.fn().mockReturnValue(Result.ok(false)), + } as unknown as jest.Mocked, + items: { + streamItems: jest.fn(), + } as unknown as jest.Mocked, + } as unknown as jest.Mocked + application.addEventObserver = jest.fn() application.addWebEventObserver = jest.fn() application.isNativeMobileWeb = jest.fn().mockReturnValue(false) - navigationController = {} as jest.Mocked - selectionController = {} as jest.Mocked - - const searchOptionsController = {} as jest.Mocked - const notesController = {} as jest.Mocked const eventBus = new InternalEventBus() controller = new ItemListController( - application, - navigationController, - searchOptionsController, - selectionController, - notesController, + application.keyboardService, + application.paneController, + application.navigationController, + application.searchOptionsController, + application.items, + application.preferences, + application.itemControllerGroup, + application.vaultDisplayService, + application.desktopManager, + application.protections, + application.options, + application.isNativeMobileWebUseCase, + application.changeAndSaveItem, eventBus, ) }) @@ -42,14 +54,13 @@ describe('item list controller', () => { beforeEach(() => { controller.getFirstNonProtectedItem = jest.fn() - Object.defineProperty(selectionController, 'selectedUuids', { - get: () => new Set(), - configurable: true, + runInAction(() => { + controller.selectedUuids = new Set() }) }) - it('should return false is platform is native mobile web', () => { - application.isNativeMobileWeb = jest.fn().mockReturnValue(true) + it('should return false if platform is native mobile web', () => { + application.isNativeMobileWebUseCase.execute = jest.fn().mockReturnValue(Result.ok(true)) expect(controller.shouldSelectFirstItem(ItemsReloadSource.TagChange)).toBe(false) }) @@ -68,7 +79,7 @@ describe('item list controller', () => { content_type: ContentType.TYPES.Tag, } as jest.Mocked - Object.defineProperty(navigationController, 'selected', { + Object.defineProperty(application.navigationController, 'selected', { get: () => tag, }) @@ -80,7 +91,7 @@ describe('item list controller', () => { content_type: ContentType.TYPES.Tag, } as jest.Mocked - Object.defineProperty(navigationController, 'selected', { + Object.defineProperty(application.navigationController, 'selected', { get: () => tag, }) @@ -92,11 +103,11 @@ describe('item list controller', () => { content_type: ContentType.TYPES.Tag, } as jest.Mocked - Object.defineProperty(selectionController, 'selectedUuids', { - get: () => new Set(['123']), + runInAction(() => { + controller.selectedUuids = new Set(['123']) }) - Object.defineProperty(navigationController, 'selected', { + Object.defineProperty(application.navigationController, 'selected', { get: () => tag, }) diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts index db47d0c31..41ccc204c 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts @@ -23,15 +23,23 @@ import { NotesAndFilesDisplayControllerOptions, InternalEventBusInterface, PrefDefaults, + ItemManagerInterface, + PreferenceServiceInterface, + ChangeAndSaveItem, + DesktopManagerInterface, + UuidString, + ProtectionsClientInterface, + FullyResolvedApplicationOptions, + Uuids, + isNote, + ChallengeReason, + KeyboardModifier, } from '@standardnotes/snjs' import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx' -import { WebApplication } from '../../Application/WebApplication' import { WebDisplayOptions } from './WebDisplayOptions' import { NavigationController } from '../Navigation/NavigationController' import { CrossControllerEvent } from '../CrossControllerEvent' import { SearchOptionsController } from '../SearchOptionsController' -import { SelectedItemsController } from '../SelectedItemsController' -import { NotesController } from '../NotesController/NotesController' import { formatDateAndTimeForNote } from '@/Utils/DateUtils' import { AbstractViewController } from '../Abstract/AbstractViewController' @@ -40,14 +48,28 @@ import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewCon import { FileViewController } from '@/Components/NoteView/Controller/FileViewController' import { TemplateNoteViewAutofocusBehavior } from '@/Components/NoteView/Controller/TemplateNoteViewControllerOptions' import { ItemsReloadSource } from './ItemsReloadSource' -import { VaultDisplayServiceEvent } from '@standardnotes/ui-services' +import { + IsNativeMobileWeb, + KeyboardService, + SelectionControllerPersistableValue, + VaultDisplayServiceEvent, + VaultDisplayServiceInterface, +} from '@standardnotes/ui-services' import { getDayjsFormattedString } from '@/Utils/GetDayjsFormattedString' +import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' +import { Persistable } from '../Abstract/Persistable' +import { PaneController } from '../PaneController/PaneController' +import { requestCloseAllOpenModalsAndPopovers } from '@/Utils/CloseOpenModalsAndPopovers' +import { PaneLayout } from '../PaneController/PaneLayout' const MinNoteCellHeight = 51.0 const DefaultListNumNotes = 20 const ElementIdScrollContainer = 'notes-scrollable' -export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface { +export class ItemListController + extends AbstractViewController + implements InternalEventHandlerInterface, Persistable +{ completedFullSync = false noteFilterText = '' notes: SNNote[] = [] @@ -75,6 +97,10 @@ export class ItemListController extends AbstractViewController implements Intern isTableViewEnabled = false private reloadItemsPromise?: Promise + lastSelectedItem: ListableContentItem | undefined + selectedUuids: Set = observable(new Set()) + selectedItems: Record = {} + override deinit() { super.deinit() ;(this.noteFilterText as unknown) = undefined @@ -82,113 +108,28 @@ export class ItemListController extends AbstractViewController implements Intern ;(this.renderedItems as unknown) = undefined ;(this.navigationController as unknown) = undefined ;(this.searchOptionsController as unknown) = undefined - ;(this.selectionController as unknown) = undefined - ;(this.notesController as unknown) = undefined ;(window.onresize as unknown) = undefined destroyAllObjectProperties(this) } constructor( - application: WebApplication, + private keyboardService: KeyboardService, + private paneController: PaneController, private navigationController: NavigationController, private searchOptionsController: SearchOptionsController, - private selectionController: SelectedItemsController, - private notesController: NotesController, + private itemManager: ItemManagerInterface, + private preferences: PreferenceServiceInterface, + private itemControllerGroup: ItemGroupController, + private vaultDisplayService: VaultDisplayServiceInterface, + private desktopManager: DesktopManagerInterface | undefined, + private protections: ProtectionsClientInterface, + private options: FullyResolvedApplicationOptions, + private _isNativeMobileWeb: IsNativeMobileWeb, + private _changeAndSaveItem: ChangeAndSaveItem, eventBus: InternalEventBusInterface, ) { - super(application, eventBus) - - eventBus.addEventHandler(this, CrossControllerEvent.TagChanged) - eventBus.addEventHandler(this, CrossControllerEvent.ActiveEditorChanged) - eventBus.addEventHandler(this, VaultDisplayServiceEvent.VaultDisplayOptionsChanged) - - this.resetPagination() - - this.disposers.push( - application.streamItems([ContentType.TYPES.Note, ContentType.TYPES.File], () => { - void this.reloadItems(ItemsReloadSource.ItemStream) - }), - ) - - this.disposers.push( - application.streamItems( - [ContentType.TYPES.Tag, ContentType.TYPES.SmartView], - async ({ changed, inserted }) => { - const tags = [...changed, ...inserted] - - const { didReloadItems } = await this.reloadDisplayPreferences({ userTriggered: false }) - if (!didReloadItems) { - /** A tag could have changed its relationships, so we need to reload the filter */ - this.reloadNotesDisplayOptions() - void this.reloadItems(ItemsReloadSource.ItemStream) - } - - if ( - this.navigationController.selected && - findInArray(tags, 'uuid', this.navigationController.selected.uuid) - ) { - /** Tag title could have changed */ - this.reloadPanelTitle() - } - }, - ), - ) - - this.disposers.push( - application.addEventObserver(async () => { - void this.reloadDisplayPreferences({ userTriggered: false }) - }, ApplicationEvent.PreferencesChanged), - ) - - this.disposers.push( - application.addEventObserver(async () => { - this.application.itemControllerGroup.closeAllItemControllers() - void this.selectFirstItem() - this.setCompletedFullSync(false) - }, ApplicationEvent.SignedIn), - ) - - this.disposers.push( - application.addEventObserver(async () => { - if (!this.completedFullSync) { - void this.reloadItems(ItemsReloadSource.SyncEvent).then(() => { - if ( - this.notes.length === 0 && - this.navigationController.selected instanceof SmartView && - this.navigationController.selected.uuid === SystemViewId.AllNotes && - this.noteFilterText === '' && - !this.getActiveItemController() - ) { - this.createPlaceholderNote()?.catch(console.error) - } - }) - this.setCompletedFullSync(true) - } - }, ApplicationEvent.CompletedFullSync), - ) - - this.disposers.push( - application.addWebEventObserver((webEvent) => { - if (webEvent === WebAppEvent.EditorFocused) { - this.setShowDisplayOptionsMenu(false) - } - }), - ) - - this.disposers.push( - reaction( - () => [ - this.searchOptionsController.includeProtectedContents, - this.searchOptionsController.includeArchived, - this.searchOptionsController.includeTrashed, - ], - () => { - this.reloadNotesDisplayOptions() - void this.reloadItems(ItemsReloadSource.DisplayOptionsChange) - }, - ), - ) + super(eventBus) makeObservable(this, { completedFullSync: observable, @@ -214,21 +155,186 @@ export class ItemListController extends AbstractViewController implements Intern optionsSubtitle: computed, activeControllerItem: computed, + + selectedUuids: observable, + selectedItems: observable, + + selectedItemsCount: computed, + selectedFiles: computed, + selectedFilesCount: computed, + firstSelectedItem: computed, + + selectItem: action, + setSelectedUuids: action, + setSelectedItems: action, + + hydrateFromPersistedValue: action, }) + eventBus.addEventHandler(this, CrossControllerEvent.TagChanged) + eventBus.addEventHandler(this, CrossControllerEvent.ActiveEditorChanged) + eventBus.addEventHandler(this, VaultDisplayServiceEvent.VaultDisplayOptionsChanged) + + this.resetPagination() + + this.disposers.push( + itemManager.streamItems([ContentType.TYPES.Note, ContentType.TYPES.File], () => { + void this.reloadItems(ItemsReloadSource.ItemStream) + }), + ) + + this.disposers.push( + itemManager.streamItems( + [ContentType.TYPES.Tag, ContentType.TYPES.SmartView], + async ({ changed, inserted }) => { + const tags = [...changed, ...inserted] + + const { didReloadItems } = await this.reloadDisplayPreferences({ userTriggered: false }) + if (!didReloadItems) { + /** A tag could have changed its relationships, so we need to reload the filter */ + this.reloadNotesDisplayOptions() + void this.reloadItems(ItemsReloadSource.ItemStream) + } + + if ( + this.navigationController.selected && + findInArray(tags, 'uuid', this.navigationController.selected.uuid) + ) { + /** Tag title could have changed */ + this.reloadPanelTitle() + } + }, + ), + ) + + eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged) + eventBus.addEventHandler(this, ApplicationEvent.SignedIn) + eventBus.addEventHandler(this, ApplicationEvent.CompletedFullSync) + eventBus.addEventHandler(this, WebAppEvent.EditorFocused) + + this.disposers.push( + reaction( + () => [ + this.searchOptionsController.includeProtectedContents, + this.searchOptionsController.includeArchived, + this.searchOptionsController.includeTrashed, + ], + () => { + this.reloadNotesDisplayOptions() + void this.reloadItems(ItemsReloadSource.DisplayOptionsChange) + }, + ), + ) + + this.disposers.push( + reaction( + () => this.selectedUuids, + () => { + eventBus.publish({ + type: CrossControllerEvent.RequestValuePersistence, + payload: undefined, + }) + }, + ), + ) + + this.disposers.push( + this.itemManager.streamItems( + [ContentType.TYPES.Note, ContentType.TYPES.File], + ({ changed, inserted, removed }) => { + runInAction(() => { + for (const removedItem of removed) { + this.removeSelectedItem(removedItem.uuid) + } + + for (const item of [...changed, ...inserted]) { + if (this.selectedItems[item.uuid]) { + this.selectedItems[item.uuid] = item + } + } + }) + }, + ), + ) + window.onresize = () => { this.resetPagination(true) } } + getPersistableValue = (): SelectionControllerPersistableValue => { + return { + selectedUuids: Array.from(this.selectedUuids), + } + } + + hydrateFromPersistedValue = (state: SelectionControllerPersistableValue | undefined): void => { + if (!state) { + return + } + + if (!this.selectedUuids.size && state.selectedUuids.length > 0) { + if (!this.options.allowNoteSelectionStatePersistence) { + const items = this.itemManager.findItems(state.selectedUuids).filter((item) => !isNote(item)) + void this.selectUuids(Uuids(items)) + } else { + void this.selectUuids(state.selectedUuids) + } + } + } + async handleEvent(event: InternalEventInterface): Promise { - if (event.type === CrossControllerEvent.TagChanged) { - const payload = event.payload as { userTriggered: boolean } - await this.handleTagChange(payload.userTriggered) - } else if (event.type === CrossControllerEvent.ActiveEditorChanged) { - this.handleEditorChange().catch(console.error) - } else if (event.type === VaultDisplayServiceEvent.VaultDisplayOptionsChanged) { - void this.reloadItems(ItemsReloadSource.DisplayOptionsChange) + switch (event.type) { + case CrossControllerEvent.TagChanged: { + const payload = event.payload as { userTriggered: boolean } + await this.handleTagChange(payload.userTriggered) + break + } + + case CrossControllerEvent.ActiveEditorChanged: { + await this.handleEditorChange() + break + } + + case VaultDisplayServiceEvent.VaultDisplayOptionsChanged: { + void this.reloadItems(ItemsReloadSource.DisplayOptionsChange) + break + } + + case ApplicationEvent.PreferencesChanged: { + void this.reloadDisplayPreferences({ userTriggered: false }) + break + } + + case WebAppEvent.EditorFocused: { + this.setShowDisplayOptionsMenu(false) + break + } + + case ApplicationEvent.SignedIn: { + this.itemControllerGroup.closeAllItemControllers() + void this.selectFirstItem() + this.setCompletedFullSync(false) + break + } + + case ApplicationEvent.CompletedFullSync: { + if (!this.completedFullSync) { + void this.reloadItems(ItemsReloadSource.SyncEvent).then(() => { + if ( + this.notes.length === 0 && + this.navigationController.selected instanceof SmartView && + this.navigationController.selected.uuid === SystemViewId.AllNotes && + this.noteFilterText === '' && + !this.getActiveItemController() + ) { + this.createPlaceholderNote()?.catch(console.error) + } + }) + this.setCompletedFullSync(true) + break + } + } } } @@ -237,7 +343,7 @@ export class ItemListController extends AbstractViewController implements Intern } public getActiveItemController(): NoteViewController | FileViewController | undefined { - return this.application.itemControllerGroup.activeItemViewController + return this.itemControllerGroup.activeItemViewController } public get activeControllerItem() { @@ -249,13 +355,13 @@ export class ItemListController extends AbstractViewController implements Intern return } - const note = this.application.items.findItem(uuid) + const note = this.itemManager.findItem(uuid) if (!note) { console.warn('Tried accessing a non-existant note of UUID ' + uuid) return } - await this.application.itemControllerGroup.createItemController({ note }) + await this.itemControllerGroup.createItemController({ note }) await this.publishCrossControllerEventSync(CrossControllerEvent.ActiveEditorChanged) } @@ -265,13 +371,13 @@ export class ItemListController extends AbstractViewController implements Intern return } - const file = this.application.items.findItem(fileUuid) + const file = this.itemManager.findItem(fileUuid) if (!file) { console.warn('Tried accessing a non-existant file of UUID ' + fileUuid) return } - await this.application.itemControllerGroup.createItemController({ file }) + await this.itemControllerGroup.createItemController({ file }) } setCompletedFullSync = (completed: boolean) => { @@ -315,9 +421,9 @@ export class ItemListController extends AbstractViewController implements Intern return } - const notes = this.application.items.getDisplayableNotes() + const notes = this.itemManager.getDisplayableNotes() - const items = this.application.items.getDisplayableNotesAndFiles() + const items = this.itemManager.getDisplayableNotesAndFiles() const renderedItems = items.slice(0, this.notesToDisplay) @@ -333,7 +439,7 @@ export class ItemListController extends AbstractViewController implements Intern } private shouldLeaveSelectionUnchanged = (activeController: NoteViewController | FileViewController | undefined) => { - const hasMultipleItemsSelected = this.selectionController.selectedItemsCount >= 2 + const hasMultipleItemsSelected = this.selectedItemsCount >= 2 return ( hasMultipleItemsSelected || (activeController instanceof NoteViewController && activeController.isTemplateNote) @@ -420,11 +526,11 @@ export class ItemListController extends AbstractViewController implements Intern } private shouldSelectActiveItem = (activeItem: SNNote | FileItem) => { - return !this.selectionController.isItemSelected(activeItem) + return !this.isItemSelected(activeItem) } shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => { - if (this.application.isNativeMobileWeb()) { + if (this._isNativeMobileWeb.execute().getValue()) { return false } @@ -440,7 +546,7 @@ export class ItemListController extends AbstractViewController implements Intern } const userChangedTag = itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange - const hasNoSelectedItem = !this.selectionController.selectedUuids.size + const hasNoSelectedItem = !this.selectedUuids.size return userChangedTag || hasNoSelectedItem } @@ -458,7 +564,7 @@ export class ItemListController extends AbstractViewController implements Intern if (activeController && activeItem && this.shouldCloseActiveItem(activeItem, itemsReloadSource)) { this.closeItemController(activeController) - this.selectionController.deselectItem(activeItem) + this.deselectItem(activeItem) if (this.shouldSelectFirstItem(itemsReloadSource)) { if (this.isTableViewEnabled && !isMobileScreen()) { @@ -466,11 +572,11 @@ export class ItemListController extends AbstractViewController implements Intern } log(LoggingDomain.Selection, 'Selecting next item after closing active one') - this.selectionController.selectNextItem({ userTriggered: false }) + this.selectNextItem({ userTriggered: false }) } } else if (activeItem && this.shouldSelectActiveItem(activeItem)) { log(LoggingDomain.Selection, 'Selecting active item') - await this.selectionController.selectItem(activeItem.uuid).catch(console.error) + await this.selectItem(activeItem.uuid).catch(console.error) } else if (this.shouldSelectFirstItem(itemsReloadSource)) { await this.selectFirstItem() } else if (this.shouldSelectNextItemOrCreateNewNote(activeItem)) { @@ -511,7 +617,7 @@ export class ItemListController extends AbstractViewController implements Intern }, } - this.application.items.setPrimaryItemDisplayOptions(criteria) + this.itemManager.setPrimaryItemDisplayOptions(criteria) } reloadDisplayPreferences = async ({ @@ -525,7 +631,7 @@ export class ItemListController extends AbstractViewController implements Intern const selectedTag = this.navigationController.selected const isSystemTag = selectedTag && isSmartView(selectedTag) && isSystemView(selectedTag) const selectedTagPreferences = isSystemTag - ? this.application.getPreference(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId] + ? this.preferences.getValue(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId] : selectedTag?.preferences this.isTableViewEnabled = Boolean(selectedTagPreferences?.useTableView) @@ -533,7 +639,7 @@ export class ItemListController extends AbstractViewController implements Intern const currentSortBy = this.displayOptions.sortBy let sortBy = selectedTagPreferences?.sortBy || - this.application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy]) + this.preferences.getValue(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy]) if (sortBy === CollectionSort.UpdatedAt || (sortBy as string) === 'client_updated_at') { sortBy = CollectionSort.UpdatedAt } @@ -543,49 +649,49 @@ export class ItemListController extends AbstractViewController implements Intern newDisplayOptions.sortDirection = useBoolean( selectedTagPreferences?.sortReverse, - this.application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]), + this.preferences.getValue(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]), ) === false ? 'dsc' : 'asc' newDisplayOptions.includeArchived = useBoolean( selectedTagPreferences?.showArchived, - this.application.getPreference(PrefKey.NotesShowArchived, PrefDefaults[PrefKey.NotesShowArchived]), + this.preferences.getValue(PrefKey.NotesShowArchived, PrefDefaults[PrefKey.NotesShowArchived]), ) newDisplayOptions.includeTrashed = useBoolean( selectedTagPreferences?.showTrashed, - this.application.getPreference(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]), + this.preferences.getValue(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]), ) newDisplayOptions.includePinned = !useBoolean( selectedTagPreferences?.hidePinned, - this.application.getPreference(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]), + this.preferences.getValue(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]), ) newDisplayOptions.includeProtected = !useBoolean( selectedTagPreferences?.hideProtected, - this.application.getPreference(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]), + this.preferences.getValue(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]), ) newWebDisplayOptions.hideNotePreview = useBoolean( selectedTagPreferences?.hideNotePreview, - this.application.getPreference(PrefKey.NotesHideNotePreview, PrefDefaults[PrefKey.NotesHideNotePreview]), + this.preferences.getValue(PrefKey.NotesHideNotePreview, PrefDefaults[PrefKey.NotesHideNotePreview]), ) newWebDisplayOptions.hideDate = useBoolean( selectedTagPreferences?.hideDate, - this.application.getPreference(PrefKey.NotesHideDate, PrefDefaults[PrefKey.NotesHideDate]), + this.preferences.getValue(PrefKey.NotesHideDate, PrefDefaults[PrefKey.NotesHideDate]), ) newWebDisplayOptions.hideTags = useBoolean( selectedTagPreferences?.hideTags, - this.application.getPreference(PrefKey.NotesHideTags, PrefDefaults[PrefKey.NotesHideTags]), + this.preferences.getValue(PrefKey.NotesHideTags, PrefDefaults[PrefKey.NotesHideTags]), ) newWebDisplayOptions.hideEditorIcon = useBoolean( selectedTagPreferences?.hideEditorIcon, - this.application.getPreference(PrefKey.NotesHideEditorIcon, PrefDefaults[PrefKey.NotesHideEditorIcon]), + this.preferences.getValue(PrefKey.NotesHideEditorIcon, PrefDefaults[PrefKey.NotesHideEditorIcon]), ) const displayOptionsChanged = @@ -633,13 +739,13 @@ export class ItemListController extends AbstractViewController implements Intern const activeRegularTagUuid = selectedTag instanceof SNTag ? selectedTag.uuid : undefined - return this.application.itemControllerGroup.createItemController({ + return this.itemControllerGroup.createItemController({ templateOptions: { title, tag: activeRegularTagUuid, createdAt, autofocusBehavior, - vault: this.application.vaultDisplayService.exclusivelyShownVault, + vault: this.vaultDisplayService.exclusivelyShownVault, }, }) } @@ -652,12 +758,12 @@ export class ItemListController extends AbstractViewController implements Intern const selectedTag = this.navigationController.selected const isSystemTag = selectedTag && isSmartView(selectedTag) && isSystemView(selectedTag) const selectedTagPreferences = isSystemTag - ? this.application.getPreference(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId] + ? this.preferences.getValue(PrefKey.SystemViewPreferences)?.[selectedTag.uuid as SystemViewId] : selectedTag?.preferences const titleFormat = selectedTagPreferences?.newNoteTitleFormat || - this.application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat]) + this.preferences.getValue(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat]) if (titleFormat === NewNoteTitleFormat.CurrentNoteCount) { return `Note ${this.notes.length + 1}` @@ -666,7 +772,7 @@ export class ItemListController extends AbstractViewController implements Intern if (titleFormat === NewNoteTitleFormat.CustomFormat) { const customFormat = this.navigationController.selected?.preferences?.customNoteTitleFormat || - this.application.getPreference(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat]) + this.preferences.getValue(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat]) try { return getDayjsFormattedString(createdAt, customFormat) @@ -684,7 +790,7 @@ export class ItemListController extends AbstractViewController implements Intern } createNewNote = async (title?: string, createdAt?: Date, autofocusBehavior?: TemplateNoteViewAutofocusBehavior) => { - this.notesController.unselectNotes() + void this.publishCrossControllerEventSync(CrossControllerEvent.UnselectAllNotes) if (this.navigationController.isInSmartView() && !this.navigationController.isInHomeView()) { await this.navigationController.selectHomeNavigationView() @@ -694,7 +800,7 @@ export class ItemListController extends AbstractViewController implements Intern const controller = await this.createNewNoteController(useTitle, createdAt, autofocusBehavior) - this.selectionController.scrollToItem(controller.item) + this.scrollToItem(controller.item) } createPlaceholderNote = () => { @@ -725,7 +831,7 @@ export class ItemListController extends AbstractViewController implements Intern void this.reloadItems(ItemsReloadSource.Pagination) if (this.searchSubmitted) { - this.application.getDesktopService()?.searchText(this.noteFilterText) + this.desktopManager?.searchText(this.noteFilterText) } } @@ -759,7 +865,7 @@ export class ItemListController extends AbstractViewController implements Intern if (item) { log(LoggingDomain.Selection, 'Selecting first item', item.uuid) - await this.selectionController.selectItemWithScrollHandling(item, { + await this.selectItemWithScrollHandling(item, { userTriggered: false, scrollIntoView: false, }) @@ -773,12 +879,10 @@ export class ItemListController extends AbstractViewController implements Intern if (item) { log(LoggingDomain.Selection, 'selectNextItemOrCreateNewNote') - await this.selectionController - .selectItemWithScrollHandling(item, { - userTriggered: false, - scrollIntoView: false, - }) - .catch(console.error) + await this.selectItemWithScrollHandling(item, { + userTriggered: false, + scrollIntoView: false, + }).catch(console.error) } else { await this.createNewNote() } @@ -794,18 +898,16 @@ export class ItemListController extends AbstractViewController implements Intern } handleEditorChange = async () => { - const activeNote = this.application.itemControllerGroup.activeItemViewController?.item + const activeNote = this.itemControllerGroup.activeItemViewController?.item if (activeNote && activeNote.conflictOf) { - this.application - .changeAndSaveItem(activeNote, (mutator) => { - mutator.conflictOf = undefined - }) - .catch(console.error) + void this._changeAndSaveItem.execute(activeNote, (mutator) => { + mutator.conflictOf = undefined + }) } if (this.isFiltering) { - this.application.getDesktopService()?.searchText(this.noteFilterText) + this.desktopManager?.searchText(this.noteFilterText) } } @@ -818,7 +920,7 @@ export class ItemListController extends AbstractViewController implements Intern private closeItemController(controller: NoteViewController | FileViewController): void { log(LoggingDomain.Selection, 'Closing item controller', controller.runtimeId) - this.application.itemControllerGroup.closeItemController(controller) + this.itemControllerGroup.closeItemController(controller) } handleTagChange = async (userTriggered: boolean) => { @@ -833,7 +935,7 @@ export class ItemListController extends AbstractViewController implements Intern this.setNoteFilterText('') - this.application.getDesktopService()?.searchText() + this.desktopManager?.searchText() this.resetPagination() @@ -853,7 +955,7 @@ export class ItemListController extends AbstractViewController implements Intern */ this.searchSubmitted = true - this.application.getDesktopService()?.searchText(this.noteFilterText) + this.desktopManager?.searchText(this.noteFilterText) } get isCurrentNoteTemplate(): boolean { @@ -894,4 +996,292 @@ export class ItemListController extends AbstractViewController implements Intern this.handleFilterTextChanged() this.resetPagination() } + + get selectedItemsCount(): number { + return Object.keys(this.selectedItems).length + } + + get selectedFiles(): FileItem[] { + return this.getFilteredSelectedItems(ContentType.TYPES.File) + } + + get selectedFilesCount(): number { + return this.selectedFiles.length + } + + get firstSelectedItem() { + return Object.values(this.selectedItems)[0] + } + + getSelectedItems = () => { + const uuids = Array.from(this.selectedUuids) + return uuids.map((uuid) => this.itemManager.findSureItem(uuid)).filter((item) => !!item) + } + + getFilteredSelectedItems = (contentType?: string): T[] => { + return Object.values(this.selectedItems).filter((item) => { + return !contentType ? true : item.content_type === contentType + }) as T[] + } + + setSelectedItems = () => { + this.selectedItems = Object.fromEntries(this.getSelectedItems().map((item) => [item.uuid, item])) + } + + setSelectedUuids = (selectedUuids: Set) => { + log(LoggingDomain.Selection, 'Setting selected uuids', selectedUuids) + this.selectedUuids = new Set(selectedUuids) + this.setSelectedItems() + } + + private removeSelectedItem = (uuid: UuidString) => { + this.selectedUuids.delete(uuid) + this.setSelectedUuids(this.selectedUuids) + delete this.selectedItems[uuid] + } + + public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => { + log(LoggingDomain.Selection, 'Deselecting item', item.uuid) + this.removeSelectedItem(item.uuid) + + if (item.uuid === this.lastSelectedItem?.uuid) { + this.lastSelectedItem = undefined + } + } + + public isItemSelected = (item: ListableContentItem): boolean => { + return this.selectedUuids.has(item.uuid) + } + + private selectItemsRange = async ({ + selectedItem, + startingIndex, + endingIndex, + }: { + selectedItem?: ListableContentItem + startingIndex?: number + endingIndex?: number + }): Promise => { + const items = this.renderedItems + + const lastSelectedItemIndex = startingIndex ?? items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid) + const selectedItemIndex = endingIndex ?? items.findIndex((item) => item.uuid == selectedItem?.uuid) + + let itemsToSelect = [] + if (selectedItemIndex > lastSelectedItemIndex) { + itemsToSelect = items.slice(lastSelectedItemIndex, selectedItemIndex + 1) + } else { + itemsToSelect = items.slice(selectedItemIndex, lastSelectedItemIndex + 1) + } + + const authorizedItems = await this.protections.authorizeProtectedActionForItems( + itemsToSelect, + ChallengeReason.SelectProtectedNote, + ) + + for (const item of authorizedItems) { + runInAction(() => { + this.setSelectedUuids(this.selectedUuids.add(item.uuid)) + this.lastSelectedItem = item + }) + } + } + + cancelMultipleSelection = () => { + this.keyboardService.cancelAllKeyboardModifiers() + + const firstSelectedItem = this.firstSelectedItem + + if (firstSelectedItem) { + this.replaceSelection(firstSelectedItem) + } else { + this.deselectAll() + } + } + + private replaceSelection = (item: ListableContentItem): void => { + this.deselectAll() + runInAction(() => this.setSelectedUuids(this.selectedUuids.add(item.uuid))) + + this.lastSelectedItem = item + } + + selectAll = () => { + void this.selectItemsRange({ + startingIndex: 0, + endingIndex: this.listLength - 1, + }) + } + + deselectAll = (): void => { + this.selectedUuids.clear() + this.setSelectedUuids(this.selectedUuids) + + this.lastSelectedItem = undefined + } + + openSingleSelectedItem = async ({ userTriggered } = { userTriggered: true }) => { + if (this.selectedItemsCount === 1) { + const item = this.firstSelectedItem + + if (item.content_type === ContentType.TYPES.Note) { + await this.openNote(item.uuid) + } else if (item.content_type === ContentType.TYPES.File) { + await this.openFile(item.uuid) + } + + if (!this.paneController.isInMobileView || userTriggered) { + void this.paneController.setPaneLayout(PaneLayout.Editing) + } + + if (this.paneController.isInMobileView && userTriggered) { + requestCloseAllOpenModalsAndPopovers() + } + } + } + + selectItem = async ( + uuid: UuidString, + userTriggered?: boolean, + ): Promise<{ + didSelect: boolean + }> => { + const item = this.itemManager.findItem(uuid) + + if (!item) { + return { + didSelect: false, + } + } + + log(LoggingDomain.Selection, 'Select item', item.uuid) + + const supportsMultipleSelection = this.options.allowMultipleSelection + const hasMeta = this.keyboardService.activeModifiers.has(KeyboardModifier.Meta) + const hasCtrl = this.keyboardService.activeModifiers.has(KeyboardModifier.Ctrl) + const hasShift = this.keyboardService.activeModifiers.has(KeyboardModifier.Shift) + const hasMoreThanOneSelected = this.selectedItemsCount > 1 + const isAuthorizedForAccess = await this.protections.authorizeItemAccess(item) + + if (supportsMultipleSelection && userTriggered && (hasMeta || hasCtrl)) { + if (this.selectedUuids.has(uuid) && hasMoreThanOneSelected) { + this.removeSelectedItem(uuid) + } else if (isAuthorizedForAccess) { + this.selectedUuids.add(uuid) + this.setSelectedUuids(this.selectedUuids) + this.lastSelectedItem = item + } + } else if (supportsMultipleSelection && userTriggered && hasShift) { + await this.selectItemsRange({ selectedItem: item }) + } else { + const shouldSelectNote = hasMoreThanOneSelected || !this.selectedUuids.has(uuid) + if (shouldSelectNote && isAuthorizedForAccess) { + this.replaceSelection(item) + } + } + + await this.openSingleSelectedItem({ userTriggered: userTriggered ?? false }) + + return { + didSelect: this.selectedUuids.has(uuid), + } + } + + selectItemWithScrollHandling = async ( + item: { + uuid: ListableContentItem['uuid'] + }, + { userTriggered = false, scrollIntoView = true, animated = true }, + ): Promise => { + const { didSelect } = await this.selectItem(item.uuid, userTriggered) + + const avoidMobileScrollingDueToIncompatibilityWithPaneAnimations = isMobileScreen() + + if (didSelect && scrollIntoView && !avoidMobileScrollingDueToIncompatibilityWithPaneAnimations) { + this.scrollToItem(item, animated) + } + } + + scrollToItem = (item: { uuid: ListableContentItem['uuid'] }, animated = true): void => { + const itemElement = document.getElementById(item.uuid) + itemElement?.scrollIntoView({ + behavior: animated ? 'smooth' : 'auto', + }) + } + + selectUuids = async (uuids: UuidString[], userTriggered = false) => { + const itemsForUuids = this.itemManager.findItems(uuids).filter((item) => !isFile(item)) + + if (itemsForUuids.length < 1) { + return + } + + if (!userTriggered && itemsForUuids.some((item) => item.protected && isFile(item))) { + return + } + + this.setSelectedUuids(new Set(Uuids(itemsForUuids))) + + if (itemsForUuids.length === 1) { + void this.openSingleSelectedItem({ userTriggered }) + } + } + + selectNextItem = ({ userTriggered } = { userTriggered: true }) => { + const displayableItems = this.items + + const currentIndex = displayableItems.findIndex((candidate) => { + return candidate.uuid === this.lastSelectedItem?.uuid + }) + + let nextIndex = currentIndex + 1 + + while (nextIndex < displayableItems.length) { + const nextItem = displayableItems[nextIndex] + + nextIndex++ + + if (nextItem.protected) { + continue + } + + this.selectItemWithScrollHandling(nextItem, { userTriggered }).catch(console.error) + + const nextNoteElement = document.getElementById(nextItem.uuid) + + nextNoteElement?.focus() + + return + } + } + + selectPreviousItem = () => { + const displayableItems = this.items + + if (!this.lastSelectedItem) { + return + } + + const currentIndex = displayableItems.indexOf(this.lastSelectedItem) + + let previousIndex = currentIndex - 1 + + while (previousIndex >= 0) { + const previousItem = displayableItems[previousIndex] + + previousIndex-- + + if (previousItem.protected) { + continue + } + + this.selectItemWithScrollHandling(previousItem, { userTriggered: true }).catch(console.error) + + const previousNoteElement = document.getElementById(previousItem.uuid) + + previousNoteElement?.focus() + + return + } + } } diff --git a/packages/web/src/javascripts/Controllers/LinkingController.spec.ts b/packages/web/src/javascripts/Controllers/LinkingController.spec.ts index 8ce8e6ff3..a4b339761 100644 --- a/packages/web/src/javascripts/Controllers/LinkingController.spec.ts +++ b/packages/web/src/javascripts/Controllers/LinkingController.spec.ts @@ -14,12 +14,12 @@ import { ItemInterface, InternalFeatureService, InternalFeature, + PreferenceServiceInterface, } from '@standardnotes/snjs' import { FilesController } from './FilesController' import { ItemListController } from './ItemList/ItemListController' import { LinkingController } from './LinkingController' import { NavigationController } from './Navigation/NavigationController' -import { SelectedItemsController } from './SelectedItemsController' import { SubscriptionController } from './Subscription/SubscriptionController' import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults' @@ -46,44 +46,52 @@ const createFile = (name: string, options?: Partial) => { } describe('LinkingController', () => { - let linkingController: LinkingController let application: WebApplication - let navigationController: NavigationController - let selectionController: SelectedItemsController let eventBus: InternalEventBus - let itemListController: ItemListController - let filesController: FilesController - let subscriptionController: SubscriptionController - beforeEach(() => { application = { vaults: {} as jest.Mocked, alerts: {} as jest.Mocked, sync: {} as jest.Mocked, mutator: {} as jest.Mocked, + preferences: { + getValue: jest.fn().mockReturnValue(true), + } as unknown as jest.Mocked, itemControllerGroup: {} as jest.Mocked, + navigationController: {} as jest.Mocked, + itemListController: {} as jest.Mocked, + filesController: {} as jest.Mocked, + subscriptionController: {} as jest.Mocked, } as unknown as jest.Mocked application.getPreference = jest.fn() application.addSingleEventObserver = jest.fn() - application.streamItems = jest.fn() application.sync.sync = jest.fn() Object.defineProperty(application, 'items', { value: {} as jest.Mocked }) - navigationController = {} as jest.Mocked - - selectionController = {} as jest.Mocked - eventBus = {} as jest.Mocked + eventBus.addEventHandler = jest.fn() - itemListController = {} as jest.Mocked - filesController = {} as jest.Mocked - subscriptionController = {} as jest.Mocked - - linkingController = new LinkingController(application, navigationController, selectionController, eventBus) - linkingController.setServicesPostConstruction(itemListController, filesController, subscriptionController) + Object.defineProperty(application, 'linkingController', { + get: () => + new LinkingController( + application.itemListController, + application.filesController, + application.subscriptionController, + application.navigationController, + application.itemControllerGroup, + application.vaultDisplayService, + application.preferences, + application.items, + application.mutator, + application.sync, + application.vaults, + eventBus, + ), + configurable: true, + }) }) describe('isValidSearchResult', () => { @@ -257,7 +265,7 @@ describe('LinkingController', () => { return undefined }) - await linkingController.linkItems(note, file) + await application.linkingController.linkItems(note, file) expect(moveToVaultSpy).toHaveBeenCalled() }) diff --git a/packages/web/src/javascripts/Controllers/LinkingController.tsx b/packages/web/src/javascripts/Controllers/LinkingController.tsx index 9c021a97b..a9619a61a 100644 --- a/packages/web/src/javascripts/Controllers/LinkingController.tsx +++ b/packages/web/src/javascripts/Controllers/LinkingController.tsx @@ -17,6 +17,13 @@ import { InternalEventBusInterface, isTag, PrefDefaults, + PreferenceServiceInterface, + InternalEventHandlerInterface, + InternalEventInterface, + ItemManagerInterface, + MutatorClientInterface, + SyncServiceInterface, + VaultServiceInterface, } from '@standardnotes/snjs' import { action, computed, makeObservable, observable } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' @@ -24,25 +31,30 @@ import { CrossControllerEvent } from './CrossControllerEvent' import { FilesController } from './FilesController' import { ItemListController } from './ItemList/ItemListController' import { NavigationController } from './Navigation/NavigationController' -import { SelectedItemsController } from './SelectedItemsController' import { SubscriptionController } from './Subscription/SubscriptionController' -import { WebApplication } from '@/Application/WebApplication' import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' +import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' +import { VaultDisplayServiceInterface } from '@standardnotes/ui-services' -export class LinkingController extends AbstractViewController { +export class LinkingController extends AbstractViewController implements InternalEventHandlerInterface { shouldLinkToParentFolders: boolean isLinkingPanelOpen = false - private itemListController!: ItemListController - private filesController!: FilesController - private subscriptionController!: SubscriptionController constructor( - application: WebApplication, + private itemListController: ItemListController, + private filesController: FilesController, + private subscriptionController: SubscriptionController, private navigationController: NavigationController, - private selectionController: SelectedItemsController, + private itemControllerGroup: ItemGroupController, + private vaultDisplayService: VaultDisplayServiceInterface, + private preferences: PreferenceServiceInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private vaults: VaultServiceInterface, eventBus: InternalEventBusInterface, ) { - super(application, eventBus) + super(eventBus) makeObservable(this, { isLinkingPanelOpen: observable, @@ -52,29 +64,26 @@ export class LinkingController extends AbstractViewController { setIsLinkingPanelOpen: action, }) - this.shouldLinkToParentFolders = application.getPreference( + this.shouldLinkToParentFolders = preferences.getValue( PrefKey.NoteAddToParentFolders, PrefDefaults[PrefKey.NoteAddToParentFolders], ) - this.disposers.push( - this.application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { - this.shouldLinkToParentFolders = this.application.getPreference( + eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged) + } + + handleEvent(event: InternalEventInterface): Promise { + switch (event.type) { + case ApplicationEvent.PreferencesChanged: { + this.shouldLinkToParentFolders = this.preferences.getValue( PrefKey.NoteAddToParentFolders, PrefDefaults[PrefKey.NoteAddToParentFolders], ) - }), - ) - } + break + } + } - public setServicesPostConstruction( - itemListController: ItemListController, - filesController: FilesController, - subscriptionController: SubscriptionController, - ) { - this.itemListController = itemListController - this.filesController = filesController - this.subscriptionController = subscriptionController + return Promise.resolve() } get isEntitledToNoteLinking() { @@ -86,20 +95,20 @@ export class LinkingController extends AbstractViewController { } get activeItem() { - return this.application.itemControllerGroup.activeItemViewController?.item + return this.itemControllerGroup.activeItemViewController?.item } getFilesLinksForItem = (item: LinkableItem | undefined) => { - if (!item || this.application.items.isTemplateItem(item)) { + if (!item || this.items.isTemplateItem(item)) { return { filesLinkedToItem: [], filesLinkingToItem: [], } } - const referencesOfItem = naturalSort(this.application.items.referencesForItem(item).filter(isFile), 'title') + const referencesOfItem = naturalSort(this.items.referencesForItem(item).filter(isFile), 'title') - const referencingItem = naturalSort(this.application.items.itemsReferencingItem(item).filter(isFile), 'title') + const referencingItem = naturalSort(this.items.itemsReferencingItem(item).filter(isFile), 'title') if (item.content_type === ContentType.TYPES.File) { return { @@ -119,15 +128,15 @@ export class LinkingController extends AbstractViewController { return } - return this.application.items.getSortedTagsForItem(item).map((tag) => createLinkFromItem(tag, 'linked')) + return this.items.getSortedTagsForItem(item).map((tag) => createLinkFromItem(tag, 'linked')) } getLinkedNotesForItem = (item: LinkableItem | undefined) => { - if (!item || this.application.items.isTemplateItem(item)) { + if (!item || this.items.isTemplateItem(item)) { return [] } - return naturalSort(this.application.items.referencesForItem(item).filter(isNote), 'title').map((item) => + return naturalSort(this.items.referencesForItem(item).filter(isNote), 'title').map((item) => createLinkFromItem(item, 'linked'), ) } @@ -137,7 +146,7 @@ export class LinkingController extends AbstractViewController { return [] } - return naturalSort(this.application.items.itemsReferencingItem(item).filter(isNote), 'title').map((item) => + return naturalSort(this.items.itemsReferencingItem(item).filter(isNote), 'title').map((item) => createLinkFromItem(item, 'linked-by'), ) } @@ -150,7 +159,7 @@ export class LinkingController extends AbstractViewController { return AppPaneId.Items } else if (item instanceof SNNote) { await this.navigationController.selectHomeNavigationView() - const { didSelect } = await this.selectionController.selectItem(item.uuid, true) + const { didSelect } = await this.itemListController.selectItem(item.uuid, true) if (didSelect) { return AppPaneId.Editor } @@ -169,16 +178,16 @@ export class LinkingController extends AbstractViewController { unlinkItems = async (item: LinkableItem, itemToUnlink: LinkableItem) => { try { - await this.application.mutator.unlinkItems(item, itemToUnlink) + await this.mutator.unlinkItems(item, itemToUnlink) } catch (error) { console.error(error) } - void this.application.sync.sync() + void this.sync.sync() } unlinkItemFromSelectedItem = async (itemToUnlink: LinkableItem) => { - const selectedItem = this.selectionController.firstSelectedItem + const selectedItem = this.itemListController.firstSelectedItem if (!selectedItem) { return @@ -196,25 +205,25 @@ export class LinkingController extends AbstractViewController { linkItems = async (item: LinkableItem, itemToLink: LinkableItem) => { const linkNoteAndFile = async (note: SNNote, file: FileItem) => { - const updatedFile = await this.application.mutator.associateFileWithNote(file, note) + const updatedFile = await this.mutator.associateFileWithNote(file, note) if (featureTrunkVaultsEnabled()) { if (updatedFile) { - const noteVault = this.application.vaults.getItemVault(note) - const fileVault = this.application.vaults.getItemVault(updatedFile) + const noteVault = this.vaults.getItemVault(note) + const fileVault = this.vaults.getItemVault(updatedFile) if (noteVault && !fileVault) { - await this.application.vaults.moveItemToVault(noteVault, file) + await this.vaults.moveItemToVault(noteVault, file) } } } } const linkFileAndFile = async (file1: FileItem, file2: FileItem) => { - await this.application.mutator.linkFileToFile(file1, file2) + await this.mutator.linkFileToFile(file1, file2) } const linkNoteToNote = async (note1: SNNote, note2: SNNote) => { - await this.application.mutator.linkNoteToNote(note1, note2) + await this.mutator.linkNoteToNote(note1, note2) } const linkTagToNote = async (tag: SNTag, note: SNNote) => { @@ -260,7 +269,7 @@ export class LinkingController extends AbstractViewController { throw new Error('First item must be a note or file') } - void this.application.sync.sync() + void this.sync.sync() } linkItemToSelectedItem = async (itemToLink: LinkableItem): Promise => { @@ -286,9 +295,9 @@ export class LinkingController extends AbstractViewController { createAndAddNewTag = async (title: string): Promise => { await this.ensureActiveItemIsInserted() - const vault = this.application.vaultDisplayService.exclusivelyShownVault + const vault = this.vaultDisplayService.exclusivelyShownVault - const newTag = await this.application.mutator.findOrCreateTag(title, vault) + const newTag = await this.mutator.findOrCreateTag(title, vault) const activeItem = this.activeItem if (activeItem) { @@ -300,11 +309,11 @@ export class LinkingController extends AbstractViewController { addTagToItem = async (tag: SNTag, item: FileItem | SNNote) => { if (item instanceof SNNote) { - await this.application.mutator.addTagToNote(item, tag, this.shouldLinkToParentFolders) + await this.mutator.addTagToNote(item, tag, this.shouldLinkToParentFolders) } else if (item instanceof FileItem) { - await this.application.mutator.addTagToFile(item, tag, this.shouldLinkToParentFolders) + await this.mutator.addTagToFile(item, tag, this.shouldLinkToParentFolders) } - this.application.sync.sync().catch(console.error) + this.sync.sync().catch(console.error) } } diff --git a/packages/web/src/javascripts/Controllers/Moments/MomentsService.ts b/packages/web/src/javascripts/Controllers/Moments/MomentsService.ts index 13b900a79..06bf9a0f4 100644 --- a/packages/web/src/javascripts/Controllers/Moments/MomentsService.ts +++ b/packages/web/src/javascripts/Controllers/Moments/MomentsService.ts @@ -1,13 +1,26 @@ import { addToast, dismissToast, ToastType } from '@standardnotes/toast' -import { ApplicationEvent, InternalEventBusInterface, StorageKey } from '@standardnotes/services' +import { + ApplicationEvent, + DesktopDeviceInterface, + InternalEventBusInterface, + InternalEventHandlerInterface, + InternalEventInterface, + ItemManagerInterface, + PreferenceServiceInterface, + ProtectionEvent, + ProtectionsClientInterface, + StorageKey, + StorageServiceInterface, +} from '@standardnotes/services' import { isDev } from '@/Utils' import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs' import { FilesController } from '../FilesController' import { action, makeObservable, observable } from 'mobx' import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewController' -import { WebApplication } from '@/Application/WebApplication' import { dateToStringStyle1 } from '@/Utils/DateUtils' import { PhotoRecorder } from './PhotoRecorder' +import { LinkingController } from '../LinkingController' +import { IsMobileDevice } from '@standardnotes/ui-services' const EVERY_HOUR = 1000 * 60 * 60 const EVERY_TEN_SECONDS = 1000 * 10 @@ -15,33 +28,22 @@ const DEBUG_MODE = isDev && false const DELAY_AFTER_STARTING_CAMERA_TO_ALLOW_MOBILE_AUTOFOCUS = 2000 -export class MomentsService extends AbstractViewController { +export class MomentsService extends AbstractViewController implements InternalEventHandlerInterface { isEnabled = false private intervalReference: ReturnType | undefined constructor( - application: WebApplication, private filesController: FilesController, + private linkingController: LinkingController, + private storage: StorageServiceInterface, + private preferences: PreferenceServiceInterface, + private items: ItemManagerInterface, + private protections: ProtectionsClientInterface, + private desktopDevice: DesktopDeviceInterface | undefined, + private _isMobileDevice: IsMobileDevice, eventBus: InternalEventBusInterface, ) { - super(application, eventBus) - - this.disposers.push( - application.addEventObserver(async () => { - this.isEnabled = (this.application.getValue(StorageKey.MomentsEnabled) as boolean) ?? false - if (this.isEnabled) { - void this.beginTakingPhotos() - } - }, ApplicationEvent.LocalDataLoaded), - - application.addEventObserver(async () => { - this.pauseMoments() - }, ApplicationEvent.BiometricsSoftLockEngaged), - - application.addEventObserver(async () => { - this.resumeMoments() - }, ApplicationEvent.BiometricsSoftLockDisengaged), - ) + super(eventBus) makeObservable(this, { isEnabled: observable, @@ -49,16 +51,41 @@ export class MomentsService extends AbstractViewController { enableMoments: action, disableMoments: action, }) + + eventBus.addEventHandler(this, ApplicationEvent.LocalDataLoaded) + eventBus.addEventHandler(this, ProtectionEvent.BiometricsSoftLockEngaged) + eventBus.addEventHandler(this, ProtectionEvent.BiometricsSoftLockDisengaged) + } + + async handleEvent(event: InternalEventInterface): Promise { + switch (event.type) { + case ApplicationEvent.LocalDataLoaded: { + this.isEnabled = (this.storage.getValue(StorageKey.MomentsEnabled) as boolean) ?? false + if (this.isEnabled) { + void this.beginTakingPhotos() + } + break + } + + case ProtectionEvent.BiometricsSoftLockEngaged: { + this.pauseMoments() + break + } + + case ProtectionEvent.BiometricsSoftLockDisengaged: { + this.resumeMoments() + break + } + } } override deinit() { super.deinit() - ;(this.application as unknown) = undefined ;(this.filesController as unknown) = undefined } public enableMoments = (): void => { - this.application.setValue(StorageKey.MomentsEnabled, true) + this.storage.setValue(StorageKey.MomentsEnabled, true) this.isEnabled = true @@ -66,7 +93,7 @@ export class MomentsService extends AbstractViewController { } public disableMoments = (): void => { - this.application.setValue(StorageKey.MomentsEnabled, false) + this.storage.setValue(StorageKey.MomentsEnabled, false) this.isEnabled = false @@ -101,15 +128,15 @@ export class MomentsService extends AbstractViewController { } private getDefaultTag(): SNTag | undefined { - const defaultTagId = this.application.getPreference(PrefKey.MomentsDefaultTagUuid) + const defaultTagId = this.preferences.getValue(PrefKey.MomentsDefaultTagUuid) if (defaultTagId) { - return this.application.items.findItem(defaultTagId) + return this.items.findItem(defaultTagId) } } public takePhoto = async (): Promise => { - const isAppLocked = await this.application.isLocked() + const isAppLocked = await this.protections.isLocked() if (isAppLocked) { return @@ -127,8 +154,8 @@ export class MomentsService extends AbstractViewController { }) } - if (this.application.desktopDevice) { - const granted = await this.application.desktopDevice.askForMediaAccess('camera') + if (this.desktopDevice) { + const granted = await this.desktopDevice.askForMediaAccess('camera') if (!granted) { if (toastId) { dismissToast(toastId) @@ -147,7 +174,7 @@ export class MomentsService extends AbstractViewController { const camera = new PhotoRecorder() await camera.initialize() - if (this.application.isMobileDevice) { + if (this._isMobileDevice.execute().getValue()) { await sleep(DELAY_AFTER_STARTING_CAMERA_TO_ALLOW_MOBILE_AUTOFOCUS) } @@ -168,12 +195,12 @@ export class MomentsService extends AbstractViewController { if (uploadedFile) { if (isAppInForeground) { - void this.application.linkingController.linkItemToSelectedItem(uploadedFile) + void this.linkingController.linkItemToSelectedItem(uploadedFile) } const defaultTag = this.getDefaultTag() if (defaultTag) { - void this.application.linkingController.linkItems(uploadedFile, defaultTag) + void this.linkingController.linkItems(uploadedFile, defaultTag) } } diff --git a/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts b/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts index fdd66a0ca..4d6a70ce1 100644 --- a/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts +++ b/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts @@ -1,7 +1,9 @@ import { confirmDialog, CREATE_NEW_TAG_COMMAND, + KeyboardService, NavigationControllerPersistableValue, + VaultDisplayService, VaultDisplayServiceEvent, } from '@standardnotes/ui-services' import { STRING_DELETE_TAG } from '@/Constants/Strings' @@ -22,9 +24,14 @@ import { InternalEventBusInterface, InternalEventHandlerInterface, InternalEventInterface, + ItemManagerInterface, + SyncServiceInterface, + MutatorClientInterface, + AlertService, + PreferenceServiceInterface, + ChangeAndSaveItem, } from '@standardnotes/snjs' import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx' -import { WebApplication } from '../../Application/WebApplication' import { FeaturesController } from '../FeaturesController' import { destroyAllObjectProperties } from '@/Utils' import { isValidFutureSiblings, rootTags, tagSiblings } from './Utils' @@ -35,6 +42,7 @@ import { Persistable } from '../Abstract/Persistable' import { TagListSectionType } from '@/Components/Tags/TagListSection' import { PaneLayout } from '../PaneController/PaneLayout' import { TagsCountsState } from './TagsCountsState' +import { PaneController } from '../PaneController/PaneController' export class NavigationController extends AbstractViewController @@ -63,16 +71,24 @@ export class NavigationController private readonly tagsCountsState: TagsCountsState constructor( - application: WebApplication, private featuresController: FeaturesController, + private vaultDisplayService: VaultDisplayService, + private keyboardService: KeyboardService, + private paneController: PaneController, + private sync: SyncServiceInterface, + private mutator: MutatorClientInterface, + private items: ItemManagerInterface, + private preferences: PreferenceServiceInterface, + private alerts: AlertService, + private _changeAndSaveItem: ChangeAndSaveItem, eventBus: InternalEventBusInterface, ) { - super(application, eventBus) + super(eventBus) eventBus.addEventHandler(this, VaultDisplayServiceEvent.VaultDisplayOptionsChanged) - this.tagsCountsState = new TagsCountsState(this.application) - this.smartViews = this.application.items.getSmartViews() + this.tagsCountsState = new TagsCountsState(items) + this.smartViews = items.getSmartViews() makeObservable(this, { tags: observable, @@ -122,7 +138,7 @@ export class NavigationController }) this.disposers.push( - this.application.streamItems([ContentType.TYPES.Tag, ContentType.TYPES.SmartView], ({ changed, removed }) => { + this.items.streamItems([ContentType.TYPES.Tag, ContentType.TYPES.SmartView], ({ changed, removed }) => { this.reloadTags() runInAction(() => { @@ -150,12 +166,12 @@ export class NavigationController ) this.disposers.push( - this.application.items.addNoteCountChangeObserver((tagUuid) => { + this.items.addNoteCountChangeObserver((tagUuid) => { if (!tagUuid) { - this.setAllNotesCount(this.application.items.allCountableNotesCount()) - this.setAllFilesCount(this.application.items.allCountableFilesCount()) + this.setAllNotesCount(this.items.allCountableNotesCount()) + this.setAllFilesCount(this.items.allCountableFilesCount()) } else { - const tag = this.application.items.findItem(tagUuid) + const tag = this.items.findItem(tagUuid) if (tag) { this.tagsCountsState.update([tag]) } @@ -176,7 +192,7 @@ export class NavigationController ) this.disposers.push( - this.application.keyboardService.addCommandHandler({ + this.keyboardService.addCommandHandler({ command: CREATE_NEW_TAG_COMMAND, onKeyDown: () => { this.createNewTemplate() @@ -187,9 +203,9 @@ export class NavigationController private reloadTags(): void { runInAction(() => { - this.tags = this.application.items.getDisplayableTags() + this.tags = this.items.getDisplayableTags() this.starredTags = this.tags.filter((tag) => tag.starred) - this.smartViews = this.application.items.getSmartViews() + this.smartViews = this.items.getSmartViews() }) } @@ -258,14 +274,14 @@ export class NavigationController return } - const createdTag = await this.application.mutator.createTagOrSmartView( + const createdTag = await this.mutator.createTagOrSmartView( title, - this.application.vaultDisplayService.exclusivelyShownVault, + this.vaultDisplayService.exclusivelyShownVault, ) - const futureSiblings = this.application.items.getTagChildren(parent) + const futureSiblings = this.items.getTagChildren(parent) - if (!isValidFutureSiblings(this.application, futureSiblings, createdTag)) { + if (!isValidFutureSiblings(this.alerts, futureSiblings, createdTag)) { this.setAddingSubtagTo(undefined) this.remove(createdTag, false).catch(console.error) return @@ -273,7 +289,7 @@ export class NavigationController this.assignParent(createdTag.uuid, parent.uuid).catch(console.error) - this.application.sync.sync().catch(console.error) + this.sync.sync().catch(console.error) runInAction(() => { void this.setSelectedTag(createdTag as SNTag, 'all') @@ -301,7 +317,7 @@ export class NavigationController tagUsesTableView(tag: AnyTag): boolean { const isSystemView = tag instanceof SmartView && Object.values(SystemViewId).includes(tag.uuid as SystemViewId) const useTableView = isSystemView - ? this.application.getPreference(PrefKey.SystemViewPreferences)?.[tag.uuid as SystemViewId] + ? this.preferences.getValue(PrefKey.SystemViewPreferences)?.[tag.uuid as SystemViewId] : tag?.preferences return Boolean(useTableView) } @@ -390,7 +406,7 @@ export class NavigationController } public get allLocalRootTags(): SNTag[] { - if (this.editing_ instanceof SNTag && this.application.items.isTemplateItem(this.editing_)) { + if (this.editing_ instanceof SNTag && this.items.isTemplateItem(this.editing_)) { return [this.editing_, ...this.rootTags] } return this.rootTags @@ -401,11 +417,11 @@ export class NavigationController } getChildren(tag: SNTag): SNTag[] { - if (this.application.items.isTemplateItem(tag)) { + if (this.items.isTemplateItem(tag)) { return [] } - const children = this.application.items.getTagChildren(tag) + const children = this.items.getTagChildren(tag) const childrenUuids = children.map((childTag) => childTag.uuid) const childrenTags = this.tags.filter((tag) => childrenUuids.includes(tag.uuid)) @@ -413,45 +429,45 @@ export class NavigationController } isValidTagParent(parent: SNTag, tag: SNTag): boolean { - return this.application.items.isValidTagParent(parent, tag) + return this.items.isValidTagParent(parent, tag) } public hasParent(tagUuid: UuidString): boolean { - const item = this.application.items.findItem(tagUuid) + const item = this.items.findItem(tagUuid) return !!item && !!(item as SNTag).parentId } public async assignParent(tagUuid: string, futureParentUuid: string | undefined): Promise { - const tag = this.application.items.findItem(tagUuid) as SNTag + const tag = this.items.findItem(tagUuid) as SNTag - const currentParent = this.application.items.getTagParent(tag) + const currentParent = this.items.getTagParent(tag) const currentParentUuid = currentParent?.uuid if (currentParentUuid === futureParentUuid) { return } - const futureParent = futureParentUuid && (this.application.items.findItem(futureParentUuid) as SNTag) + const futureParent = futureParentUuid && (this.items.findItem(futureParentUuid) as SNTag) if (!futureParent) { - const futureSiblings = rootTags(this.application) - if (!isValidFutureSiblings(this.application, futureSiblings, tag)) { + const futureSiblings = rootTags(this.items) + if (!isValidFutureSiblings(this.alerts, futureSiblings, tag)) { return } - await this.application.mutator.unsetTagParent(tag) + await this.mutator.unsetTagParent(tag) } else { - const futureSiblings = this.application.items.getTagChildren(futureParent) - if (!isValidFutureSiblings(this.application, futureSiblings, tag)) { + const futureSiblings = this.items.getTagChildren(futureParent) + if (!isValidFutureSiblings(this.alerts, futureSiblings, tag)) { return } - await this.application.mutator.setTagParent(futureParent, tag) + await this.mutator.setTagParent(futureParent, tag) } - await this.application.sync.sync() + await this.sync.sync() } get rootTags(): SNTag[] { - return this.tags.filter((tag) => !this.application.items.getTagParent(tag)) + return this.tags.filter((tag) => !this.items.getTagParent(tag)) } get tagsCount(): number { @@ -483,7 +499,7 @@ export class NavigationController } public async setPanelWidthForTag(tag: SNTag, width: number): Promise { - await this.application.changeAndSaveItem(tag, (mutator) => { + await this._changeAndSaveItem.execute(tag, (mutator) => { mutator.preferences = { ...mutator.preferences, panelWidth: width, @@ -497,17 +513,17 @@ export class NavigationController { userTriggered } = { userTriggered: false }, ) { if (tag && tag.conflictOf) { - this.application - .changeAndSaveItem(tag, (mutator) => { + this._changeAndSaveItem + .execute(tag, (mutator) => { mutator.conflictOf = undefined }) .catch(console.error) } if (tag && (this.isTagFilesView(tag) || this.tagUsesTableView(tag))) { - this.application.paneController.setPaneLayout(PaneLayout.TableView) + this.paneController.setPaneLayout(PaneLayout.TableView) } else if (userTriggered) { - this.application.paneController.setPaneLayout(PaneLayout.ItemSelection) + this.paneController.setPaneLayout(PaneLayout.ItemSelection) } this.previouslySelected_ = this.selected_ @@ -516,7 +532,7 @@ export class NavigationController this.setSelectedTagInstance(tag) this.selectedLocation = location - if (tag && this.application.items.isTemplateItem(tag)) { + if (tag && this.items.isTemplateItem(tag)) { return } @@ -558,24 +574,24 @@ export class NavigationController return } - this.application - .changeAndSaveItem(tag, (mutator) => { + this._changeAndSaveItem + .execute(tag, (mutator) => { mutator.expanded = expanded }) .catch(console.error) } public async setFavorite(tag: SNTag, favorite: boolean) { - return this.application - .changeAndSaveItem(tag, (mutator) => { + return this._changeAndSaveItem + .execute(tag, (mutator) => { mutator.starred = favorite }) .catch(console.error) } public setIcon(tag: SNTag, icon: VectorIconNameOrEmoji) { - this.application - .changeAndSaveItem(tag, (mutator) => { + this._changeAndSaveItem + .execute(tag, (mutator) => { mutator.iconString = icon as string }) .catch(console.error) @@ -593,13 +609,13 @@ export class NavigationController } public createNewTemplate() { - const isAlreadyEditingATemplate = this.editing_ && this.application.items.isTemplateItem(this.editing_) + const isAlreadyEditingATemplate = this.editing_ && this.items.isTemplateItem(this.editing_) if (isAlreadyEditingATemplate) { return } - const newTag = this.application.items.createTemplateItem(ContentType.TYPES.Tag) as SNTag + const newTag = this.items.createTemplateItem(ContentType.TYPES.Tag) as SNTag runInAction(() => { this.selectedLocation = 'all' @@ -622,9 +638,9 @@ export class NavigationController }) } if (shouldDelete) { - this.application.mutator + this.mutator .deleteItem(tag) - .then(() => this.application.sync.sync()) + .then(() => this.sync.sync()) .catch(console.error) await this.setSelectedTag(this.smartViews[0], 'views') } @@ -633,9 +649,9 @@ export class NavigationController public async save(tag: SNTag | SmartView, newTitle: string) { const hasEmptyTitle = newTitle.length === 0 const hasNotChangedTitle = newTitle === tag.title - const isTemplateChange = this.application.items.isTemplateItem(tag) + const isTemplateChange = this.items.isTemplateItem(tag) - const siblings = tag instanceof SNTag ? tagSiblings(this.application, tag) : [] + const siblings = tag instanceof SNTag ? tagSiblings(this.items, tag) : [] const hasDuplicatedTitle = siblings.some((other) => other.title.toLowerCase() === newTitle.toLowerCase()) runInAction(() => { @@ -653,12 +669,12 @@ export class NavigationController if (isTemplateChange) { this.undoCreateNewTag() } - this.application.alerts?.alert('A tag with this name already exists.').catch(console.error) + this.alerts.alert('A tag with this name already exists.').catch(console.error) return } if (isTemplateChange) { - const isSmartViewTitle = this.application.items.isSmartViewTitle(newTitle) + const isSmartViewTitle = this.items.isSmartViewTitle(newTitle) if (isSmartViewTitle) { if (!this.featuresController.hasSmartViews) { @@ -667,16 +683,16 @@ export class NavigationController } } - const insertedTag = await this.application.mutator.createTagOrSmartView( + const insertedTag = await this.mutator.createTagOrSmartView( newTitle, - this.application.vaultDisplayService.exclusivelyShownVault, + this.vaultDisplayService.exclusivelyShownVault, ) - this.application.sync.sync().catch(console.error) + this.sync.sync().catch(console.error) runInAction(() => { void this.setSelectedTag(insertedTag, this.selectedLocation || 'views') }) } else { - await this.application.changeAndSaveItem(tag, (mutator) => { + await this._changeAndSaveItem.execute(tag, (mutator) => { mutator.title = newTitle }) } diff --git a/packages/web/src/javascripts/Controllers/Navigation/TagsCountsState.ts b/packages/web/src/javascripts/Controllers/Navigation/TagsCountsState.ts index f37cec41c..7c4b6229e 100644 --- a/packages/web/src/javascripts/Controllers/Navigation/TagsCountsState.ts +++ b/packages/web/src/javascripts/Controllers/Navigation/TagsCountsState.ts @@ -1,11 +1,10 @@ -import { SNTag } from '@standardnotes/snjs' +import { ItemManagerInterface, SNTag } from '@standardnotes/snjs' import { action, makeAutoObservable, observable } from 'mobx' -import { WebApplication } from '../../Application/WebApplication' export class TagsCountsState { public counts: { [uuid: string]: number } = {} - public constructor(private application: WebApplication) { + public constructor(private items: ItemManagerInterface) { makeAutoObservable(this, { counts: observable.ref, update: action, @@ -16,7 +15,7 @@ export class TagsCountsState { const newCounts: { [uuid: string]: number } = Object.assign({}, this.counts) tags.forEach((tag) => { - newCounts[tag.uuid] = this.application.items.countableNotesForTag(tag) + newCounts[tag.uuid] = this.items.countableNotesForTag(tag) }) this.counts = newCounts diff --git a/packages/web/src/javascripts/Controllers/Navigation/Utils.ts b/packages/web/src/javascripts/Controllers/Navigation/Utils.ts index 81fda0e83..80bb65574 100644 --- a/packages/web/src/javascripts/Controllers/Navigation/Utils.ts +++ b/packages/web/src/javascripts/Controllers/Navigation/Utils.ts @@ -1,33 +1,33 @@ -import { SNApplication, SNTag } from '@standardnotes/snjs' +import { AlertService, ItemManagerInterface, SNTag } from '@standardnotes/snjs' -export const rootTags = (application: SNApplication): SNTag[] => { - const hasNoParent = (tag: SNTag) => !application.items.getTagParent(tag) +export const rootTags = (items: ItemManagerInterface): SNTag[] => { + const hasNoParent = (tag: SNTag) => !items.getTagParent(tag) - const allTags = application.items.getDisplayableTags() + const allTags = items.getDisplayableTags() const rootTags = allTags.filter(hasNoParent) return rootTags } -export const tagSiblings = (application: SNApplication, tag: SNTag): SNTag[] => { +export const tagSiblings = (items: ItemManagerInterface, tag: SNTag): SNTag[] => { const withoutCurrentTag = (tags: SNTag[]) => tags.filter((other) => other.uuid !== tag.uuid) - const isTemplateTag = application.items.isTemplateItem(tag) - const parentTag = !isTemplateTag && application.items.getTagParent(tag) + const isTemplateTag = items.isTemplateItem(tag) + const parentTag = !isTemplateTag && items.getTagParent(tag) if (parentTag) { - const siblingsAndTag = application.items.getTagChildren(parentTag) + const siblingsAndTag = items.getTagChildren(parentTag) return withoutCurrentTag(siblingsAndTag) } - return withoutCurrentTag(rootTags(application)) + return withoutCurrentTag(rootTags(items)) } -export const isValidFutureSiblings = (application: SNApplication, futureSiblings: SNTag[], tag: SNTag): boolean => { +export const isValidFutureSiblings = (alerts: AlertService, futureSiblings: SNTag[], tag: SNTag): boolean => { const siblingWithSameName = futureSiblings.find((otherTag) => otherTag.title === tag.title) if (siblingWithSameName) { - application.alerts + alerts ?.alert( `A tag with the name ${tag.title} already exists at this destination. Please rename this tag before moving and try again.`, ) diff --git a/packages/web/src/javascripts/Controllers/NoAccountWarningController.ts b/packages/web/src/javascripts/Controllers/NoAccountWarningController.ts index b052e2d88..0d90f45c2 100644 --- a/packages/web/src/javascripts/Controllers/NoAccountWarningController.ts +++ b/packages/web/src/javascripts/Controllers/NoAccountWarningController.ts @@ -1,34 +1,27 @@ import { storage, StorageKey } from '@standardnotes/ui-services' -import { ApplicationEvent, InternalEventBusInterface } from '@standardnotes/snjs' +import { + ApplicationEvent, + InternalEventBusInterface, + InternalEventHandlerInterface, + InternalEventInterface, + SessionsClientInterface, +} from '@standardnotes/snjs' import { runInAction, makeObservable, observable, action } from 'mobx' -import { WebApplication } from '../Application/WebApplication' import { AbstractViewController } from './Abstract/AbstractViewController' -export class NoAccountWarningController extends AbstractViewController { +export class NoAccountWarningController extends AbstractViewController implements InternalEventHandlerInterface { show: boolean - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor( + private sessions: SessionsClientInterface, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) - this.show = application.hasAccount() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true + this.show = sessions.isSignedIn() ? false : storage.get(StorageKey.ShowNoAccountWarning) ?? true - this.disposers.push( - application.addEventObserver(async () => { - runInAction(() => { - this.show = false - }) - }, ApplicationEvent.SignedIn), - ) - - this.disposers.push( - application.addEventObserver(async () => { - if (application.hasAccount()) { - runInAction(() => { - this.show = false - }) - } - }, ApplicationEvent.Started), - ) + eventBus.addEventHandler(this, ApplicationEvent.SignedIn) + eventBus.addEventHandler(this, ApplicationEvent.Started) makeObservable(this, { show: observable, @@ -36,6 +29,23 @@ export class NoAccountWarningController extends AbstractViewController { }) } + async handleEvent(event: InternalEventInterface): Promise { + switch (event.type) { + case ApplicationEvent.SignedIn: + runInAction(() => { + this.show = false + }) + break + case ApplicationEvent.Started: + if (this.sessions.isSignedIn()) { + runInAction(() => { + this.show = false + }) + } + break + } + } + hide = (): void => { this.show = false storage.set(StorageKey.ShowNoAccountWarning, false) diff --git a/packages/web/src/javascripts/Controllers/NoteHistory/HistoryModalController.ts b/packages/web/src/javascripts/Controllers/NoteHistory/HistoryModalController.ts index 2f0655f7d..1e37e0e6f 100644 --- a/packages/web/src/javascripts/Controllers/NoteHistory/HistoryModalController.ts +++ b/packages/web/src/javascripts/Controllers/NoteHistory/HistoryModalController.ts @@ -1,6 +1,5 @@ -import { WebApplication } from '@/Application/WebApplication' import { InternalEventBusInterface, SNNote } from '@standardnotes/snjs' -import { OPEN_NOTE_HISTORY_COMMAND } from '@standardnotes/ui-services' +import { KeyboardService, OPEN_NOTE_HISTORY_COMMAND } from '@standardnotes/ui-services' import { action, makeObservable, observable } from 'mobx' import { AbstractViewController } from '../Abstract/AbstractViewController' import { NotesControllerInterface } from '../NotesController/NotesControllerInterface' @@ -14,11 +13,11 @@ export class HistoryModalController extends AbstractViewController { } constructor( - application: WebApplication, - eventBus: InternalEventBusInterface, notesController: NotesControllerInterface, + keyboardService: KeyboardService, + eventBus: InternalEventBusInterface, ) { - super(application, eventBus) + super(eventBus) makeObservable(this, { note: observable, @@ -26,7 +25,7 @@ export class HistoryModalController extends AbstractViewController { }) this.disposers.push( - application.keyboardService.addCommandHandler({ + keyboardService.addCommandHandler({ command: OPEN_NOTE_HISTORY_COMMAND, onKeyDown: () => { this.openModal(notesController.firstSelectedNote) diff --git a/packages/web/src/javascripts/Controllers/NoteHistory/NoteHistoryController.ts b/packages/web/src/javascripts/Controllers/NoteHistory/NoteHistoryController.ts index 51bd65505..48de3ca46 100644 --- a/packages/web/src/javascripts/Controllers/NoteHistory/NoteHistoryController.ts +++ b/packages/web/src/javascripts/Controllers/NoteHistory/NoteHistoryController.ts @@ -1,42 +1,38 @@ -import { WebApplication } from '@/Application/WebApplication' import { RevisionType } from '@/Components/RevisionHistoryModal/RevisionType' -import { - LegacyHistoryEntry, - ListGroup, - RemoteRevisionListGroup, - sortRevisionListIntoGroups, -} from '@/Components/RevisionHistoryModal/utils' +import { sortRevisionListIntoGroups } from '@/Components/RevisionHistoryModal/utils' import { STRING_RESTORE_LOCKED_ATTEMPT } from '@/Constants/Strings' import { confirmDialog } from '@standardnotes/ui-services' import { Action, ActionVerb, + ActionsService, + AlertService, ButtonType, + ChangeAndSaveItem, + DeleteRevision, + FeaturesClientInterface, + GetRevision, HistoryEntry, + HistoryServiceInterface, + ItemManagerInterface, + ListRevisions, + MutatorClientInterface, NoteHistoryEntry, PayloadEmitSource, RevisionMetadata, SNNote, + SyncServiceInterface, } from '@standardnotes/snjs' import { makeObservable, observable, action } from 'mobx' -import { SelectedItemsController } from '../SelectedItemsController' - -type RemoteHistory = RemoteRevisionListGroup[] - -type SessionHistory = ListGroup[] - -type LegacyHistory = Action[] - -type SelectedRevision = HistoryEntry | LegacyHistoryEntry | undefined - -type SelectedEntry = RevisionMetadata | NoteHistoryEntry | Action | undefined - -export enum RevisionContentState { - Idle, - Loading, - Loaded, - NotEntitled, -} +import { + RemoteHistory, + SessionHistory, + LegacyHistory, + SelectedRevision, + SelectedEntry, + RevisionContentState, +} from './Types' +import { ItemListController } from '../ItemList/ItemListController' export class NoteHistoryController { remoteHistory: RemoteHistory = [] @@ -52,12 +48,20 @@ export class NoteHistoryController { currentTab = RevisionType.Remote constructor( - private application: WebApplication, private note: SNNote, - private selectionController: SelectedItemsController, + private itemListController: ItemListController, + private features: FeaturesClientInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private actions: ActionsService, + private history: HistoryServiceInterface, + private alerts: AlertService, + private _getRevision: GetRevision, + private _listRevisions: ListRevisions, + private _deleteRevision: DeleteRevision, + private _changeAndSaveItem: ChangeAndSaveItem, ) { - void this.fetchAllHistory() - makeObservable(this, { selectedRevision: observable, setSelectedRevision: action, @@ -84,6 +88,8 @@ export class NoteHistoryController { contentState: observable, setContentState: action, }) + + void this.fetchAllHistory() } setSelectedRevision = (revision: SelectedRevision) => { @@ -119,7 +125,7 @@ export class NoteHistoryController { return } - if (!this.application.features.hasMinimumRole(entry.required_role)) { + if (!this.features.hasMinimumRole(entry.required_role)) { this.setContentState(RevisionContentState.NotEntitled) this.setSelectedRevision(undefined) return @@ -130,7 +136,7 @@ export class NoteHistoryController { try { this.setSelectedEntry(entry) - const remoteRevisionOrError = await this.application.getRevision.execute({ + const remoteRevisionOrError = await this._getRevision.execute({ itemUuid: this.note.uuid, revisionUuid: entry.uuid, }) @@ -162,7 +168,7 @@ export class NoteHistoryController { this.setSelectedEntry(entry) - const response = await this.application.actions.runAction(entry.subactions[0], this.note) + const response = await this.actions.runAction(entry.subactions[0], this.note) if (!response) { throw new Error('Could not fetch revision') @@ -241,7 +247,7 @@ export class NoteHistoryController { if (this.note) { this.setIsFetchingRemoteHistory(true) try { - const revisionsListOrError = await this.application.listRevisions.execute({ itemUuid: this.note.uuid }) + const revisionsListOrError = await this._listRevisions.execute({ itemUuid: this.note.uuid }) if (revisionsListOrError.isFailed()) { throw new Error(revisionsListOrError.getError()) } @@ -261,14 +267,14 @@ export class NoteHistoryController { } fetchLegacyHistory = async () => { - const actionExtensions = this.application.actions.getExtensions() + const actionExtensions = this.actions.getExtensions() actionExtensions.forEach(async (ext) => { if (!this.note) { return } - const actionExtension = await this.application.actions.loadExtensionInContextOfItem(ext, this.note) + const actionExtension = await this.actions.loadExtensionInContextOfItem(ext, this.note) if (!actionExtension) { return @@ -296,9 +302,7 @@ export class NoteHistoryController { } this.setSessionHistory( - sortRevisionListIntoGroups( - this.application.history.sessionHistoryForItem(this.note) as NoteHistoryEntry[], - ), + sortRevisionListIntoGroups(this.history.sessionHistoryForItem(this.note) as NoteHistoryEntry[]), ) await this.fetchRemoteHistory() await this.fetchLegacyHistory() @@ -313,10 +317,10 @@ export class NoteHistoryController { } restoreRevision = async (revision: NonNullable) => { - const originalNote = this.application.items.findItem(revision.payload.uuid) + const originalNote = this.items.findItem(revision.payload.uuid) if (originalNote?.locked) { - this.application.alerts.alert(STRING_RESTORE_LOCKED_ATTEMPT).catch(console.error) + this.alerts.alert(STRING_RESTORE_LOCKED_ATTEMPT).catch(console.error) return } @@ -330,7 +334,7 @@ export class NoteHistoryController { } if (didConfirm) { - void this.application.changeAndSaveItem( + void this._changeAndSaveItem.execute( originalNote, (mutator) => { mutator.setCustomContent(revision.payload.content) @@ -342,20 +346,20 @@ export class NoteHistoryController { } restoreRevisionAsCopy = async (revision: NonNullable) => { - const originalNote = this.application.items.findSureItem(revision.payload.uuid) + const originalNote = this.items.findSureItem(revision.payload.uuid) - const duplicatedItem = await this.application.mutator.duplicateItem(originalNote, false, { + const duplicatedItem = await this.mutator.duplicateItem(originalNote, false, { ...revision.payload.content, title: revision.payload.content.title ? revision.payload.content.title + ' (copy)' : undefined, }) - void this.application.sync.sync() + void this.sync.sync() - this.selectionController.selectItem(duplicatedItem.uuid).catch(console.error) + this.itemListController.selectItem(duplicatedItem.uuid).catch(console.error) } deleteRemoteRevision = async (revisionEntry: RevisionMetadata) => { - const shouldDelete = await this.application.alerts.confirm( + const shouldDelete = await this.alerts.confirm( 'Are you sure you want to delete this revision?', 'Delete revision?', 'Delete revision', @@ -367,7 +371,7 @@ export class NoteHistoryController { return } - const deleteRevisionOrError = await this.application.deleteRevision.execute({ + const deleteRevisionOrError = await this._deleteRevision.execute({ itemUuid: this.note.uuid, revisionUuid: revisionEntry.uuid, }) diff --git a/packages/web/src/javascripts/Controllers/NoteHistory/Types.ts b/packages/web/src/javascripts/Controllers/NoteHistory/Types.ts new file mode 100644 index 000000000..240827f8d --- /dev/null +++ b/packages/web/src/javascripts/Controllers/NoteHistory/Types.ts @@ -0,0 +1,20 @@ +import { LegacyHistoryEntry, ListGroup, RemoteRevisionListGroup } from '@/Components/RevisionHistoryModal/utils' + +import { Action, HistoryEntry, NoteHistoryEntry, RevisionMetadata } from '@standardnotes/snjs' + +export type RemoteHistory = RemoteRevisionListGroup[] + +export type SessionHistory = ListGroup[] + +export type LegacyHistory = Action[] + +export type SelectedRevision = HistoryEntry | LegacyHistoryEntry | undefined + +export type SelectedEntry = RevisionMetadata | NoteHistoryEntry | Action | undefined + +export enum RevisionContentState { + Idle, + Loading, + Loaded, + NotEntitled, +} diff --git a/packages/web/src/javascripts/Controllers/NoteSyncController.ts b/packages/web/src/javascripts/Controllers/NoteSyncController.ts index dc18a1fb4..78c04e4cf 100644 --- a/packages/web/src/javascripts/Controllers/NoteSyncController.ts +++ b/packages/web/src/javascripts/Controllers/NoteSyncController.ts @@ -1,8 +1,15 @@ -import { WebApplication } from '@/Application/WebApplication' import { MutationType, NoteMutator, SNNote } from '@standardnotes/models' -import { InfoStrings } from '@standardnotes/snjs' +import { + AlertService, + InfoStrings, + ItemManagerInterface, + MutatorClientInterface, + SessionsClientInterface, + SyncServiceInterface, +} from '@standardnotes/snjs' import { Deferred } from '@standardnotes/utils' import { EditorSaveTimeoutDebounce } from '../Components/NoteView/Controller/EditorSaveTimeoutDebounce' +import { IsNativeMobileWeb } from '@standardnotes/ui-services' const NotePreviewCharLimit = 160 @@ -24,8 +31,13 @@ export class NoteSyncController { private saveTimeout?: ReturnType constructor( - private application: WebApplication, private item: SNNote, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sessions: SessionsClientInterface, + private sync: SyncServiceInterface, + private alerts: AlertService, + private _isNativeMobileWeb: IsNativeMobileWeb, ) {} setItem(item: SNNote) { @@ -41,7 +53,6 @@ export class NoteSyncController { } this.savingLocallyPromise = null this.saveTimeout = undefined - ;(this.application as unknown) = undefined ;(this.item as unknown) = undefined } @@ -52,11 +63,11 @@ export class NoteSyncController { clearTimeout(this.saveTimeout) } - const noDebounce = params.bypassDebouncer || this.application.noAccount() + const noDebounce = params.bypassDebouncer || this.sessions.isSignedOut() const syncDebouceMs = noDebounce ? EditorSaveTimeoutDebounce.ImmediateChange - : this.application.isNativeMobileWeb() + : this._isNativeMobileWeb.execute().getValue() ? EditorSaveTimeoutDebounce.NativeMobileWeb : EditorSaveTimeoutDebounce.Desktop @@ -76,12 +87,12 @@ export class NoteSyncController { } private async undebouncedSave(params: NoteSaveFunctionParams): Promise { - if (!this.application.items.findItem(this.item.uuid)) { - void this.application.alerts.alert(InfoStrings.InvalidNote) + if (!this.items.findItem(this.item.uuid)) { + void this.alerts.alert(InfoStrings.InvalidNote) return } - await this.application.mutator.changeItem( + await this.mutator.changeItem( this.item, (mutator) => { const noteMutator = mutator as NoteMutator @@ -112,7 +123,7 @@ export class NoteSyncController { params.isUserModified ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, ) - void this.application.sync.sync().then(() => { + void this.sync.sync().then(() => { params.onRemoteSyncComplete?.() }) diff --git a/packages/web/src/javascripts/Controllers/NotesController/NotesController.ts b/packages/web/src/javascripts/Controllers/NotesController/NotesController.ts index b70749854..81945213f 100644 --- a/packages/web/src/javascripts/Controllers/NotesController/NotesController.ts +++ b/packages/web/src/javascripts/Controllers/NotesController/NotesController.ts @@ -1,5 +1,12 @@ import { destroyAllObjectProperties } from '@/Utils' -import { confirmDialog, PIN_NOTE_COMMAND, STAR_NOTE_COMMAND } from '@standardnotes/ui-services' +import { + confirmDialog, + GetItemTags, + IsGlobalSpellcheckEnabled, + KeyboardService, + PIN_NOTE_COMMAND, + STAR_NOTE_COMMAND, +} from '@standardnotes/ui-services' import { StringEmptyTrash, Strings, StringUtils } from '@/Constants/Strings' import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants' import { @@ -13,16 +20,27 @@ import { InternalEventBusInterface, MutationType, PrefDefaults, + PreferenceServiceInterface, + InternalEventHandlerInterface, + InternalEventInterface, + ItemManagerInterface, + MutatorClientInterface, + SyncServiceInterface, + AlertService, + ProtectionsClientInterface, } from '@standardnotes/snjs' import { makeObservable, observable, action, computed, runInAction } from 'mobx' -import { WebApplication } from '../../Application/WebApplication' import { AbstractViewController } from '../Abstract/AbstractViewController' -import { SelectedItemsController } from '../SelectedItemsController' -import { ItemListController } from '../ItemList/ItemListController' import { NavigationController } from '../Navigation/NavigationController' import { NotesControllerInterface } from './NotesControllerInterface' +import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' +import { CrossControllerEvent } from '../CrossControllerEvent' +import { ItemListController } from '../ItemList/ItemListController' -export class NotesController extends AbstractViewController implements NotesControllerInterface { +export class NotesController + extends AbstractViewController + implements NotesControllerInterface, InternalEventHandlerInterface +{ shouldLinkToParentFolders: boolean lastSelectedNote: SNNote | undefined contextMenuOpen = false @@ -33,25 +51,23 @@ export class NotesController extends AbstractViewController implements NotesCont contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 } contextMenuMaxHeight: number | 'auto' = 'auto' showProtectedWarning = false - private itemListController!: ItemListController - - override deinit() { - super.deinit() - ;(this.lastSelectedNote as unknown) = undefined - ;(this.selectionController as unknown) = undefined - ;(this.navigationController as unknown) = undefined - ;(this.itemListController as unknown) = undefined - - destroyAllObjectProperties(this) - } constructor( - application: WebApplication, - private selectionController: SelectedItemsController, + private itemListController: ItemListController, private navigationController: NavigationController, + private itemControllerGroup: ItemGroupController, + private keyboardService: KeyboardService, + private preferences: PreferenceServiceInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private protections: ProtectionsClientInterface, + private alerts: AlertService, + private _isGlobalSpellcheckEnabled: IsGlobalSpellcheckEnabled, + private _getItemTags: GetItemTags, eventBus: InternalEventBusInterface, ) { - super(application, eventBus) + super(eventBus) makeObservable(this, { contextMenuOpen: observable, @@ -71,39 +87,32 @@ export class NotesController extends AbstractViewController implements NotesCont unselectNotes: action, }) - this.shouldLinkToParentFolders = application.getPreference( + this.shouldLinkToParentFolders = preferences.getValue( PrefKey.NoteAddToParentFolders, PrefDefaults[PrefKey.NoteAddToParentFolders], ) + eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged) + eventBus.addEventHandler(this, CrossControllerEvent.UnselectAllNotes) + this.disposers.push( - this.application.keyboardService.addCommandHandler({ + this.keyboardService.addCommandHandler({ command: PIN_NOTE_COMMAND, onKeyDown: () => { this.togglePinSelectedNotes() }, }), - this.application.keyboardService.addCommandHandler({ + this.keyboardService.addCommandHandler({ command: STAR_NOTE_COMMAND, onKeyDown: () => { this.toggleStarSelectedNotes() }, }), - this.application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { - this.shouldLinkToParentFolders = this.application.getPreference( - PrefKey.NoteAddToParentFolders, - PrefDefaults[PrefKey.NoteAddToParentFolders], - ) - }), ) - } - - public setServicesPostConstruction(itemListController: ItemListController) { - this.itemListController = itemListController this.disposers.push( - this.application.itemControllerGroup.addActiveControllerChangeObserver(() => { - const controllers = this.application.itemControllerGroup.itemControllers + this.itemControllerGroup.addActiveControllerChangeObserver(() => { + const controllers = this.itemControllerGroup.itemControllers const activeNoteUuids = controllers.map((controller) => controller.item.uuid) @@ -111,15 +120,35 @@ export class NotesController extends AbstractViewController implements NotesCont for (const selectedId of selectedUuids) { if (!activeNoteUuids.includes(selectedId)) { - this.selectionController.deselectItem({ uuid: selectedId }) + this.itemListController.deselectItem({ uuid: selectedId }) } } }), ) } + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === ApplicationEvent.PreferencesChanged) { + this.shouldLinkToParentFolders = this.preferences.getValue( + PrefKey.NoteAddToParentFolders, + PrefDefaults[PrefKey.NoteAddToParentFolders], + ) + } else if (event.type === CrossControllerEvent.UnselectAllNotes) { + this.unselectNotes() + } + } + + override deinit() { + super.deinit() + ;(this.lastSelectedNote as unknown) = undefined + ;(this.itemListController as unknown) = undefined + ;(this.navigationController as unknown) = undefined + + destroyAllObjectProperties(this) + } + public get selectedNotes(): SNNote[] { - return this.selectionController.getFilteredSelectedItems(ContentType.TYPES.Note) + return this.itemListController.getFilteredSelectedItems(ContentType.TYPES.Note) } get firstSelectedNote(): SNNote | undefined { @@ -135,7 +164,7 @@ export class NotesController extends AbstractViewController implements NotesCont } get trashedNotesCount(): number { - return this.application.items.trashedItems.length + return this.items.trashedItems.length } setContextMenuOpen = (open: boolean) => { @@ -202,8 +231,8 @@ export class NotesController extends AbstractViewController implements NotesCont } async changeSelectedNotes(mutate: (mutator: NoteMutator) => void): Promise { - await this.application.mutator.changeItems(this.getSelectedNotesList(), mutate, MutationType.NoUpdateUserTimestamps) - this.application.sync.sync().catch(console.error) + await this.mutator.changeItems(this.getSelectedNotesList(), mutate, MutationType.NoUpdateUserTimestamps) + this.sync.sync().catch(console.error) } setHideSelectedNotePreviews(hide: boolean): void { @@ -243,7 +272,7 @@ export class NotesController extends AbstractViewController implements NotesCont async deleteNotes(permanently: boolean): Promise { if (this.getSelectedNotesList().some((note) => note.locked)) { const text = StringUtils.deleteLockedNotesAttempt(this.selectedNotesCount) - this.application.alerts.alert(text).catch(console.error) + this.alerts.alert(text).catch(console.error) return false } @@ -262,10 +291,10 @@ export class NotesController extends AbstractViewController implements NotesCont confirmButtonStyle: 'danger', }) ) { - this.selectionController.selectNextItem() + this.itemListController.selectNextItem() if (permanently) { - await this.application.mutator.deleteItems(this.getSelectedNotesList()) - void this.application.sync.sync() + await this.mutator.deleteItems(this.getSelectedNotesList()) + void this.sync.sync() } else { await this.changeSelectedNotes((mutator) => { mutator.trashed = true @@ -313,9 +342,7 @@ export class NotesController extends AbstractViewController implements NotesCont async setArchiveSelectedNotes(archived: boolean): Promise { if (this.getSelectedNotesList().some((note) => note.locked)) { - this.application.alerts - .alert(StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount)) - .catch(console.error) + this.alerts.alert(StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount)).catch(console.error) return } @@ -324,7 +351,7 @@ export class NotesController extends AbstractViewController implements NotesCont }) runInAction(() => { - this.selectionController.deselectAll() + this.itemListController.deselectAll() this.contextMenuOpen = false }) } @@ -332,76 +359,77 @@ export class NotesController extends AbstractViewController implements NotesCont async setProtectSelectedNotes(protect: boolean): Promise { const selectedNotes = this.getSelectedNotesList() if (protect) { - await this.application.protections.protectNotes(selectedNotes) + await this.protections.protectNotes(selectedNotes) this.setShowProtectedWarning(true) } else { - await this.application.protections.unprotectNotes(selectedNotes) + await this.protections.unprotectNotes(selectedNotes) this.setShowProtectedWarning(false) } - void this.application.sync.sync() + void this.sync.sync() } unselectNotes(): void { - this.selectionController.deselectAll() + this.itemListController.deselectAll() } getSpellcheckStateForNote(note: SNNote) { - return note.spellcheck != undefined ? note.spellcheck : this.application.isGlobalSpellcheckEnabled() + return note.spellcheck != undefined ? note.spellcheck : this._isGlobalSpellcheckEnabled.execute().getValue() } async toggleGlobalSpellcheckForNote(note: SNNote) { - await this.application.mutator.changeItem( + await this.mutator.changeItem( note, (mutator) => { mutator.toggleSpellcheck() }, MutationType.NoUpdateUserTimestamps, ) - this.application.sync.sync().catch(console.error) + this.sync.sync().catch(console.error) } getEditorWidthForNote(note: SNNote) { - return ( - note.editorWidth ?? this.application.getPreference(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth]) - ) + return note.editorWidth ?? this.preferences.getValue(PrefKey.EditorLineWidth, PrefDefaults[PrefKey.EditorLineWidth]) } async setNoteEditorWidth(note: SNNote, editorWidth: EditorLineWidth) { - await this.application.mutator.changeItem( + await this.mutator.changeItem( note, (mutator) => { mutator.editorWidth = editorWidth }, MutationType.NoUpdateUserTimestamps, ) - this.application.sync.sync().catch(console.error) + this.sync.sync().catch(console.error) } async addTagToSelectedNotes(tag: SNTag): Promise { const selectedNotes = this.getSelectedNotesList() await Promise.all( selectedNotes.map(async (note) => { - await this.application.mutator.addTagToNote(note, tag, this.shouldLinkToParentFolders) + await this.mutator.addTagToNote(note, tag, this.shouldLinkToParentFolders) }), ) - this.application.sync.sync().catch(console.error) + this.sync.sync().catch(console.error) } async removeTagFromSelectedNotes(tag: SNTag): Promise { const selectedNotes = this.getSelectedNotesList() - await this.application.mutator.changeItem(tag, (mutator) => { + await this.mutator.changeItem(tag, (mutator) => { for (const note of selectedNotes) { mutator.removeItemAsRelationship(note) } }) - this.application.sync.sync().catch(console.error) + this.sync.sync().catch(console.error) } isTagInSelectedNotes(tag: SNTag): boolean { const selectedNotes = this.getSelectedNotesList() return selectedNotes.every((note) => - this.application.getItemTags(note).find((noteTag) => noteTag.uuid === tag.uuid), + this._getItemTags + .execute(note) + .getValue() + .find((noteTag) => noteTag.uuid === tag.uuid), ) } @@ -416,8 +444,8 @@ export class NotesController extends AbstractViewController implements NotesCont confirmButtonStyle: 'danger', }) ) { - await this.application.mutator.emptyTrash() - this.application.sync.sync().catch(console.error) + await this.mutator.emptyTrash() + this.sync.sync().catch(console.error) } } diff --git a/packages/web/src/javascripts/Controllers/PaneController/PaneController.ts b/packages/web/src/javascripts/Controllers/PaneController/PaneController.ts index 3c14352a3..281048d36 100644 --- a/packages/web/src/javascripts/Controllers/PaneController/PaneController.ts +++ b/packages/web/src/javascripts/Controllers/PaneController/PaneController.ts @@ -1,4 +1,11 @@ +import { PanesForLayout } from './../../Application/UseCase/PanesForLayout' import { + InternalEventHandlerInterface, + InternalEventInterface, + PreferenceServiceInterface, +} from '@standardnotes/services' +import { + KeyboardService, TOGGLE_FOCUS_MODE_COMMAND, TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND, @@ -15,12 +22,10 @@ import { isMobileScreen } from '@/Utils' import { makeObservable, observable, action, computed } from 'mobx' import { Disposer } from '@/Types/Disposer' import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery' -import { WebApplication } from '@/Application/WebApplication' import { AbstractViewController } from '../Abstract/AbstractViewController' import { log, LoggingDomain } from '@/Logging' import { PaneLayout } from './PaneLayout' -import { panesForLayout } from './panesForLayout' -import { getIsTabletOrMobileScreen } from '@/Hooks/useIsTabletOrMobileScreen' +import { IsTabletOrMobileScreen } from '@/Application/UseCase/IsTabletOrMobileScreen' const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth] const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth] @@ -28,7 +33,7 @@ const FOCUS_MODE_CLASS_NAME = 'focus-mode' const DISABLING_FOCUS_MODE_CLASS_NAME = 'disable-focus-mode' const FOCUS_MODE_ANIMATION_DURATION = 1255 -export class PaneController extends AbstractViewController { +export class PaneController extends AbstractViewController implements InternalEventHandlerInterface { isInMobileView = isMobileScreen() protected disposers: Disposer[] = [] panes: AppPaneId[] = [] @@ -40,8 +45,14 @@ export class PaneController extends AbstractViewController { listPaneExplicitelyCollapsed = false navigationPaneExplicitelyCollapsed = false - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor( + private preferences: PreferenceServiceInterface, + private keyboardService: KeyboardService, + private _isTabletOrMobileScreen: IsTabletOrMobileScreen, + private _panesForLayout: PanesForLayout, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) makeObservable(this, { panes: observable, @@ -70,10 +81,10 @@ export class PaneController extends AbstractViewController { setFocusModeEnabled: action, }) - this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)) - this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)) + this.setCurrentNavPanelWidth(preferences.getValue(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)) + this.setCurrentItemsPanelWidth(preferences.getValue(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)) - const screen = getIsTabletOrMobileScreen(application) + const screen = this._isTabletOrMobileScreen.execute().getValue() this.panes = screen.isTabletOrMobile ? [AppPaneId.Navigation, AppPaneId.Items] @@ -86,13 +97,10 @@ export class PaneController extends AbstractViewController { mediaQuery.addListener(this.mediumScreenMQHandler) } - this.disposers.push( - application.addEventObserver(async () => { - this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)) - this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)) - }, ApplicationEvent.PreferencesChanged), + eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged) - application.keyboardService.addCommandHandler({ + this.disposers.push( + keyboardService.addCommandHandler({ command: TOGGLE_FOCUS_MODE_COMMAND, onKeyDown: (event) => { event.preventDefault() @@ -100,14 +108,14 @@ export class PaneController extends AbstractViewController { return true }, }), - application.keyboardService.addCommandHandler({ + keyboardService.addCommandHandler({ command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND, onKeyDown: (event) => { event.preventDefault() this.toggleListPane() }, }), - application.keyboardService.addCommandHandler({ + keyboardService.addCommandHandler({ command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND, onKeyDown: (event) => { event.preventDefault() @@ -117,6 +125,13 @@ export class PaneController extends AbstractViewController { ) } + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === ApplicationEvent.PreferencesChanged) { + this.setCurrentNavPanelWidth(this.preferences.getValue(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)) + this.setCurrentItemsPanelWidth(this.preferences.getValue(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)) + } + } + setCurrentNavPanelWidth(width: number) { this.currentNavPanelWidth = width } @@ -158,7 +173,7 @@ export class PaneController extends AbstractViewController { setPaneLayout = (layout: PaneLayout) => { log(LoggingDomain.Panes, 'Set pane layout', layout) - const panes = panesForLayout(layout, this.application) + const panes = this._panesForLayout.execute(layout).getValue() if (panes.includes(AppPaneId.Items) && this.listPaneExplicitelyCollapsed) { removeFromArray(panes, AppPaneId.Items) diff --git a/packages/web/src/javascripts/Controllers/PaneController/panesForLayout.ts b/packages/web/src/javascripts/Controllers/PaneController/panesForLayout.ts index 6213450ab..3e6333810 100644 --- a/packages/web/src/javascripts/Controllers/PaneController/panesForLayout.ts +++ b/packages/web/src/javascripts/Controllers/PaneController/panesForLayout.ts @@ -1,34 +1,4 @@ import { AppPaneId } from '../../Components/Panes/AppPaneMetadata' -import { PaneLayout } from './PaneLayout' -import { WebApplication } from '@/Application/WebApplication' -import { getIsTabletOrMobileScreen } from '@/Hooks/useIsTabletOrMobileScreen' - -export function panesForLayout(layout: PaneLayout, application: WebApplication): AppPaneId[] { - const screen = getIsTabletOrMobileScreen(application) - if (screen.isTablet) { - if (layout === PaneLayout.TagSelection || layout === PaneLayout.TableView) { - return [AppPaneId.Navigation, AppPaneId.Items] - } else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.Editing) { - return [AppPaneId.Items, AppPaneId.Editor] - } - } else if (screen.isMobile) { - if (layout === PaneLayout.TagSelection) { - return [AppPaneId.Navigation] - } else if (layout === PaneLayout.ItemSelection || layout === PaneLayout.TableView) { - return [AppPaneId.Navigation, AppPaneId.Items] - } else if (layout === PaneLayout.Editing) { - return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor] - } - } else { - if (layout === PaneLayout.TableView) { - return [AppPaneId.Navigation, AppPaneId.Items] - } else { - return [AppPaneId.Navigation, AppPaneId.Items, AppPaneId.Editor] - } - } - - throw Error('Unhandled pane layout') -} export function isPanesChangeLeafDismiss(from: AppPaneId[], to: AppPaneId[]): boolean { const fromWithoutLast = from.slice(0, from.length - 1) diff --git a/packages/web/src/javascripts/Controllers/PreferencesController.ts b/packages/web/src/javascripts/Controllers/PreferencesController.ts index cb1013115..c589b8c36 100644 --- a/packages/web/src/javascripts/Controllers/PreferencesController.ts +++ b/packages/web/src/javascripts/Controllers/PreferencesController.ts @@ -1,8 +1,7 @@ import { InternalEventBusInterface } from '@standardnotes/snjs' import { action, computed, makeObservable, observable } from 'mobx' -import { PreferenceId, RootQueryParam } from '@standardnotes/ui-services' +import { PreferenceId, RootQueryParam, RouteServiceInterface } from '@standardnotes/ui-services' import { AbstractViewController } from './Abstract/AbstractViewController' -import { WebApplication } from '@/Application/WebApplication' const DEFAULT_PANE: PreferenceId = 'account' @@ -10,8 +9,11 @@ export class PreferencesController extends AbstractViewController { private _open = false currentPane: PreferenceId = DEFAULT_PANE - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor( + private routeService: RouteServiceInterface, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) makeObservable(this, { _open: observable, @@ -34,7 +36,7 @@ export class PreferencesController extends AbstractViewController { closePreferences = (): void => { this._open = false this.currentPane = DEFAULT_PANE - this.application.routeService.removeQueryParameterFromURL(RootQueryParam.Settings) + this.routeService.removeQueryParameterFromURL(RootQueryParam.Settings) } get isOpen(): boolean { diff --git a/packages/web/src/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts b/packages/web/src/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts index a270a3374..532b047b5 100644 --- a/packages/web/src/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts +++ b/packages/web/src/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts @@ -1,17 +1,33 @@ +import { + AlertService, + LegacyApiServiceInterface, + MobileDeviceInterface, + SessionsClientInterface, + SubscriptionManagerInterface, +} from '@standardnotes/services' import { LoggingDomain, log } from '@/Logging' -import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowFunctions' import { AppleIAPProductId, InternalEventBusInterface } from '@standardnotes/snjs' import { action, makeObservable, observable } from 'mobx' -import { WebApplication } from '../../Application/WebApplication' import { AbstractViewController } from '../Abstract/AbstractViewController' import { PurchaseFlowPane } from './PurchaseFlowPane' +import { LoadPurchaseFlowUrl } from '@/Application/UseCase/LoadPurchaseFlowUrl' +import { IsNativeIOS } from '@standardnotes/ui-services' export class PurchaseFlowController extends AbstractViewController { isOpen = false currentPane = PurchaseFlowPane.CreateAccount - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor( + private sessions: SessionsClientInterface, + private subscriptions: SubscriptionManagerInterface, + private legacyApi: LegacyApiServiceInterface, + private alerts: AlertService, + private mobileDevice: MobileDeviceInterface | undefined, + private _loadPurchaseFlowUrl: LoadPurchaseFlowUrl, + private _isNativeIOS: IsNativeIOS, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) makeObservable(this, { isOpen: observable, @@ -28,45 +44,46 @@ export class PurchaseFlowController extends AbstractViewController { } openPurchaseFlow = async (plan = AppleIAPProductId.ProPlanYearly) => { - const user = this.application.getUser() + const user = this.sessions.getUser() if (!user) { this.isOpen = true return } - if (this.application.isNativeIOS()) { + if (this._isNativeIOS.execute().getValue()) { await this.beginIosIapPurchaseFlow(plan) } else { - await loadPurchaseFlowUrl(this.application) + await this._loadPurchaseFlowUrl.execute() } } - openPurchaseWebpage = () => { - loadPurchaseFlowUrl(this.application).catch((err) => { - console.error(err) - this.application.alerts.alert(err).catch(console.error) - }) + openPurchaseWebpage = async () => { + const result = await this._loadPurchaseFlowUrl.execute() + if (result.isFailed()) { + console.error(result.getError()) + void this.alerts.alert(result.getError()) + } } beginIosIapPurchaseFlow = async (plan: AppleIAPProductId): Promise => { - const result = await this.application.mobileDevice().purchaseSubscriptionIAP(plan) + const result = await this.mobileDevice?.purchaseSubscriptionIAP(plan) log(LoggingDomain.Purchasing, 'BeginIosIapPurchaseFlow result', result) if (!result) { - void this.application.alerts.alert('Your purchase was canceled or failed. Please try again.') + void this.alerts.alert('Your purchase was canceled or failed. Please try again.') return } const showGenericError = () => { - void this.application.alerts.alert( + void this.alerts.alert( 'There was an error confirming your purchase. Please contact support at help@standardnotes.com.', ) } log(LoggingDomain.Purchasing, 'Confirming result with our server') - const token = await this.application.getNewSubscriptionToken() + const token = await this.legacyApi.getNewSubscriptionToken() if (!token) { log(LoggingDomain.Purchasing, 'Unable to generate subscription token') @@ -74,12 +91,12 @@ export class PurchaseFlowController extends AbstractViewController { return } - const confirmResult = await this.application.subscriptions.confirmAppleIAP(result, token) + const confirmResult = await this.subscriptions.confirmAppleIAP(result, token) log(LoggingDomain.Purchasing, 'Server confirm result', confirmResult) if (confirmResult) { - void this.application.alerts.alert( + void this.alerts.alert( 'Please allow a few minutes for your subscription benefits to activate. You will see a confirmation alert in the app when your subscription is ready.', 'Your purchase was successful!', ) diff --git a/packages/web/src/javascripts/Controllers/QuickSettingsController.ts b/packages/web/src/javascripts/Controllers/QuickSettingsController.ts index 53cadc4e3..1bbfaa604 100644 --- a/packages/web/src/javascripts/Controllers/QuickSettingsController.ts +++ b/packages/web/src/javascripts/Controllers/QuickSettingsController.ts @@ -1,5 +1,4 @@ import { InternalEventBusInterface } from '@standardnotes/snjs' -import { WebApplication } from '@/Application/WebApplication' import { action, makeObservable, observable } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' @@ -7,8 +6,8 @@ export class QuickSettingsController extends AbstractViewController { open = false shouldAnimateCloseMenu = false - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor(eventBus: InternalEventBusInterface) { + super(eventBus) makeObservable(this, { open: observable, diff --git a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts index e0d56dc6c..0ae8ccf8c 100644 --- a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts +++ b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts @@ -1,15 +1,23 @@ -import { ApplicationEvent, InternalEventBusInterface } from '@standardnotes/snjs' +import { ProtectionsClientInterface } from '@standardnotes/services' +import { + ApplicationEvent, + InternalEventBusInterface, + InternalEventHandlerInterface, + InternalEventInterface, +} from '@standardnotes/snjs' import { makeObservable, observable, action, runInAction } from 'mobx' -import { WebApplication } from '../Application/WebApplication' import { AbstractViewController } from './Abstract/AbstractViewController' -export class SearchOptionsController extends AbstractViewController { +export class SearchOptionsController extends AbstractViewController implements InternalEventHandlerInterface { includeProtectedContents = false includeArchived = false includeTrashed = false - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor( + private protections: ProtectionsClientInterface, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) makeObservable(this, { includeProtectedContents: observable, @@ -22,14 +30,16 @@ export class SearchOptionsController extends AbstractViewController { refreshIncludeProtectedContents: action, }) - this.disposers.push( - this.application.addEventObserver(async () => { - this.refreshIncludeProtectedContents() - }, ApplicationEvent.UnprotectedSessionBegan), - this.application.addEventObserver(async () => { - this.refreshIncludeProtectedContents() - }, ApplicationEvent.UnprotectedSessionExpired), - ) + eventBus.addEventHandler(this, ApplicationEvent.UnprotectedSessionBegan) + eventBus.addEventHandler(this, ApplicationEvent.UnprotectedSessionExpired) + } + + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === ApplicationEvent.UnprotectedSessionBegan) { + this.refreshIncludeProtectedContents() + } else if (event.type === ApplicationEvent.UnprotectedSessionExpired) { + this.refreshIncludeProtectedContents() + } } toggleIncludeArchived = (): void => { @@ -41,14 +51,14 @@ export class SearchOptionsController extends AbstractViewController { } refreshIncludeProtectedContents = (): void => { - this.includeProtectedContents = this.application.hasUnprotectedAccessSession() + this.includeProtectedContents = this.protections.hasUnprotectedAccessSession() } toggleIncludeProtectedContents = async (): Promise => { if (this.includeProtectedContents) { this.includeProtectedContents = false } else { - await this.application.authorizeSearchingProtectedNotesText() + await this.protections.authorizeSearchingProtectedNotesText() runInAction(() => { this.refreshIncludeProtectedContents() }) diff --git a/packages/web/src/javascripts/Controllers/SelectedItemsController.ts b/packages/web/src/javascripts/Controllers/SelectedItemsController.ts deleted file mode 100644 index 8307506b8..000000000 --- a/packages/web/src/javascripts/Controllers/SelectedItemsController.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { isMobileScreen } from '@/Utils' -import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem' -import { log, LoggingDomain } from '@/Logging' -import { - ChallengeReason, - ContentType, - KeyboardModifier, - FileItem, - SNNote, - UuidString, - isFile, - Uuids, - isNote, - InternalEventBusInterface, -} from '@standardnotes/snjs' -import { SelectionControllerPersistableValue } from '@standardnotes/ui-services' -import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx' -import { WebApplication } from '../Application/WebApplication' -import { AbstractViewController } from './Abstract/AbstractViewController' -import { Persistable } from './Abstract/Persistable' -import { CrossControllerEvent } from './CrossControllerEvent' -import { ItemListController } from './ItemList/ItemListController' -import { PaneLayout } from './PaneController/PaneLayout' -import { requestCloseAllOpenModalsAndPopovers } from '@/Utils/CloseOpenModalsAndPopovers' - -export class SelectedItemsController - extends AbstractViewController - implements Persistable -{ - lastSelectedItem: ListableContentItem | undefined - selectedUuids: Set = observable(new Set()) - selectedItems: Record = {} - private itemListController!: ItemListController - - override deinit(): void { - super.deinit() - ;(this.itemListController as unknown) = undefined - } - - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) - - makeObservable(this, { - selectedUuids: observable, - selectedItems: observable, - - selectedItemsCount: computed, - selectedFiles: computed, - selectedFilesCount: computed, - firstSelectedItem: computed, - - selectItem: action, - setSelectedUuids: action, - setSelectedItems: action, - - hydrateFromPersistedValue: action, - }) - - this.disposers.push( - reaction( - () => this.selectedUuids, - () => { - eventBus.publish({ - type: CrossControllerEvent.RequestValuePersistence, - payload: undefined, - }) - }, - ), - ) - } - - getPersistableValue = (): SelectionControllerPersistableValue => { - return { - selectedUuids: Array.from(this.selectedUuids), - } - } - - hydrateFromPersistedValue = (state: SelectionControllerPersistableValue | undefined): void => { - if (!state) { - return - } - - if (!this.selectedUuids.size && state.selectedUuids.length > 0) { - if (!this.application.options.allowNoteSelectionStatePersistence) { - const items = this.application.items.findItems(state.selectedUuids).filter((item) => !isNote(item)) - void this.selectUuids(Uuids(items)) - } else { - void this.selectUuids(state.selectedUuids) - } - } - } - - public setServicesPostConstruction(itemListController: ItemListController) { - this.itemListController = itemListController - - this.disposers.push( - this.application.streamItems( - [ContentType.TYPES.Note, ContentType.TYPES.File], - ({ changed, inserted, removed }) => { - runInAction(() => { - for (const removedItem of removed) { - this.removeSelectedItem(removedItem.uuid) - } - - for (const item of [...changed, ...inserted]) { - if (this.selectedItems[item.uuid]) { - this.selectedItems[item.uuid] = item - } - } - }) - }, - ), - ) - } - - private get keyboardService() { - return this.application.keyboardService - } - - get selectedItemsCount(): number { - return Object.keys(this.selectedItems).length - } - - get selectedFiles(): FileItem[] { - return this.getFilteredSelectedItems(ContentType.TYPES.File) - } - - get selectedFilesCount(): number { - return this.selectedFiles.length - } - - get firstSelectedItem() { - return Object.values(this.selectedItems)[0] - } - - getSelectedItems = () => { - const uuids = Array.from(this.selectedUuids) - return uuids.map((uuid) => this.application.items.findSureItem(uuid)).filter((item) => !!item) - } - - getFilteredSelectedItems = (contentType?: string): T[] => { - return Object.values(this.selectedItems).filter((item) => { - return !contentType ? true : item.content_type === contentType - }) as T[] - } - - setSelectedItems = () => { - this.selectedItems = Object.fromEntries(this.getSelectedItems().map((item) => [item.uuid, item])) - } - - setSelectedUuids = (selectedUuids: Set) => { - log(LoggingDomain.Selection, 'Setting selected uuids', selectedUuids) - this.selectedUuids = new Set(selectedUuids) - this.setSelectedItems() - } - - private removeSelectedItem = (uuid: UuidString) => { - this.selectedUuids.delete(uuid) - this.setSelectedUuids(this.selectedUuids) - delete this.selectedItems[uuid] - } - - public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => { - log(LoggingDomain.Selection, 'Deselecting item', item.uuid) - this.removeSelectedItem(item.uuid) - - if (item.uuid === this.lastSelectedItem?.uuid) { - this.lastSelectedItem = undefined - } - } - - public isItemSelected = (item: ListableContentItem): boolean => { - return this.selectedUuids.has(item.uuid) - } - - private selectItemsRange = async ({ - selectedItem, - startingIndex, - endingIndex, - }: { - selectedItem?: ListableContentItem - startingIndex?: number - endingIndex?: number - }): Promise => { - const items = this.itemListController.renderedItems - - const lastSelectedItemIndex = startingIndex ?? items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid) - const selectedItemIndex = endingIndex ?? items.findIndex((item) => item.uuid == selectedItem?.uuid) - - let itemsToSelect = [] - if (selectedItemIndex > lastSelectedItemIndex) { - itemsToSelect = items.slice(lastSelectedItemIndex, selectedItemIndex + 1) - } else { - itemsToSelect = items.slice(selectedItemIndex, lastSelectedItemIndex + 1) - } - - const authorizedItems = await this.application.protections.authorizeProtectedActionForItems( - itemsToSelect, - ChallengeReason.SelectProtectedNote, - ) - - for (const item of authorizedItems) { - runInAction(() => { - this.setSelectedUuids(this.selectedUuids.add(item.uuid)) - this.lastSelectedItem = item - }) - } - } - - cancelMultipleSelection = () => { - this.keyboardService.cancelAllKeyboardModifiers() - - const firstSelectedItem = this.firstSelectedItem - - if (firstSelectedItem) { - this.replaceSelection(firstSelectedItem) - } else { - this.deselectAll() - } - } - - private replaceSelection = (item: ListableContentItem): void => { - this.deselectAll() - runInAction(() => this.setSelectedUuids(this.selectedUuids.add(item.uuid))) - - this.lastSelectedItem = item - } - - selectAll = () => { - void this.selectItemsRange({ - startingIndex: 0, - endingIndex: this.itemListController.listLength - 1, - }) - } - - deselectAll = (): void => { - this.selectedUuids.clear() - this.setSelectedUuids(this.selectedUuids) - - this.lastSelectedItem = undefined - } - - openSingleSelectedItem = async ({ userTriggered } = { userTriggered: true }) => { - if (this.selectedItemsCount === 1) { - const item = this.firstSelectedItem - - if (item.content_type === ContentType.TYPES.Note) { - await this.itemListController.openNote(item.uuid) - } else if (item.content_type === ContentType.TYPES.File) { - await this.itemListController.openFile(item.uuid) - } - - if (!this.application.paneController.isInMobileView || userTriggered) { - void this.application.paneController.setPaneLayout(PaneLayout.Editing) - } - - if (this.application.paneController.isInMobileView && userTriggered) { - requestCloseAllOpenModalsAndPopovers() - } - } - } - - selectItem = async ( - uuid: UuidString, - userTriggered?: boolean, - ): Promise<{ - didSelect: boolean - }> => { - const item = this.application.items.findItem(uuid) - - if (!item) { - return { - didSelect: false, - } - } - - log(LoggingDomain.Selection, 'Select item', item.uuid) - - const supportsMultipleSelection = this.application.options.allowMultipleSelection - const hasMeta = this.keyboardService.activeModifiers.has(KeyboardModifier.Meta) - const hasCtrl = this.keyboardService.activeModifiers.has(KeyboardModifier.Ctrl) - const hasShift = this.keyboardService.activeModifiers.has(KeyboardModifier.Shift) - const hasMoreThanOneSelected = this.selectedItemsCount > 1 - const isAuthorizedForAccess = await this.application.protections.authorizeItemAccess(item) - - if (supportsMultipleSelection && userTriggered && (hasMeta || hasCtrl)) { - if (this.selectedUuids.has(uuid) && hasMoreThanOneSelected) { - this.removeSelectedItem(uuid) - } else if (isAuthorizedForAccess) { - this.selectedUuids.add(uuid) - this.setSelectedUuids(this.selectedUuids) - this.lastSelectedItem = item - } - } else if (supportsMultipleSelection && userTriggered && hasShift) { - await this.selectItemsRange({ selectedItem: item }) - } else { - const shouldSelectNote = hasMoreThanOneSelected || !this.selectedUuids.has(uuid) - if (shouldSelectNote && isAuthorizedForAccess) { - this.replaceSelection(item) - } - } - - await this.openSingleSelectedItem({ userTriggered: userTriggered ?? false }) - - return { - didSelect: this.selectedUuids.has(uuid), - } - } - - selectItemWithScrollHandling = async ( - item: { - uuid: ListableContentItem['uuid'] - }, - { userTriggered = false, scrollIntoView = true, animated = true }, - ): Promise => { - const { didSelect } = await this.selectItem(item.uuid, userTriggered) - - const avoidMobileScrollingDueToIncompatibilityWithPaneAnimations = isMobileScreen() - - if (didSelect && scrollIntoView && !avoidMobileScrollingDueToIncompatibilityWithPaneAnimations) { - this.scrollToItem(item, animated) - } - } - - scrollToItem = (item: { uuid: ListableContentItem['uuid'] }, animated = true): void => { - const itemElement = document.getElementById(item.uuid) - itemElement?.scrollIntoView({ - behavior: animated ? 'smooth' : 'auto', - }) - } - - selectUuids = async (uuids: UuidString[], userTriggered = false) => { - const itemsForUuids = this.application.items.findItems(uuids).filter((item) => !isFile(item)) - - if (itemsForUuids.length < 1) { - return - } - - if (!userTriggered && itemsForUuids.some((item) => item.protected && isFile(item))) { - return - } - - this.setSelectedUuids(new Set(Uuids(itemsForUuids))) - - if (itemsForUuids.length === 1) { - void this.openSingleSelectedItem({ userTriggered }) - } - } - - selectNextItem = ({ userTriggered } = { userTriggered: true }) => { - const displayableItems = this.itemListController.items - - const currentIndex = displayableItems.findIndex((candidate) => { - return candidate.uuid === this.lastSelectedItem?.uuid - }) - - let nextIndex = currentIndex + 1 - - while (nextIndex < displayableItems.length) { - const nextItem = displayableItems[nextIndex] - - nextIndex++ - - if (nextItem.protected) { - continue - } - - this.selectItemWithScrollHandling(nextItem, { userTriggered }).catch(console.error) - - const nextNoteElement = document.getElementById(nextItem.uuid) - - nextNoteElement?.focus() - - return - } - } - - selectPreviousItem = () => { - const displayableItems = this.itemListController.items - - if (!this.lastSelectedItem) { - return - } - - const currentIndex = displayableItems.indexOf(this.lastSelectedItem) - - let previousIndex = currentIndex - 1 - - while (previousIndex >= 0) { - const previousItem = displayableItems[previousIndex] - - previousIndex-- - - if (previousItem.protected) { - continue - } - - this.selectItemWithScrollHandling(previousItem, { userTriggered: true }).catch(console.error) - - const previousNoteElement = document.getElementById(previousItem.uuid) - - previousNoteElement?.focus() - - return - } - } -} diff --git a/packages/web/src/javascripts/Controllers/Subscription/SubscriptionController.ts b/packages/web/src/javascripts/Controllers/Subscription/SubscriptionController.ts index f173482cd..092119a78 100644 --- a/packages/web/src/javascripts/Controllers/Subscription/SubscriptionController.ts +++ b/packages/web/src/javascripts/Controllers/Subscription/SubscriptionController.ts @@ -2,37 +2,34 @@ import { Subscription } from '@standardnotes/responses' import { destroyAllObjectProperties } from '@/Utils' import { ApplicationEvent, + FeaturesClientInterface, InternalEventBusInterface, + InternalEventHandlerInterface, + InternalEventInterface, Invitation, InvitationStatus, + SessionsClientInterface, SubscriptionManagerEvent, SubscriptionManagerInterface, } from '@standardnotes/snjs' import { computed, makeObservable, observable, runInAction } from 'mobx' -import { WebApplication } from '../../Application/WebApplication' import { AbstractViewController } from '../Abstract/AbstractViewController' -export class SubscriptionController extends AbstractViewController { +export class SubscriptionController extends AbstractViewController implements InternalEventHandlerInterface { private readonly ALLOWED_SUBSCRIPTION_INVITATIONS = 5 subscriptionInvitations: Invitation[] | undefined = undefined hasAccount: boolean onlineSubscription: Subscription | undefined = undefined - override deinit() { - super.deinit() - ;(this.subscriptionInvitations as unknown) = undefined - - destroyAllObjectProperties(this) - } - constructor( - application: WebApplication, + private subscriptions: SubscriptionManagerInterface, + private sessions: SessionsClientInterface, + private features: FeaturesClientInterface, eventBus: InternalEventBusInterface, - private subscriptionManager: SubscriptionManagerInterface, ) { - super(application, eventBus) - this.hasAccount = application.hasAccount() + super(eventBus) + this.hasAccount = sessions.isSignedIn() makeObservable(this, { subscriptionInvitations: observable, @@ -45,52 +42,62 @@ export class SubscriptionController extends AbstractViewController { allInvitationsUsed: computed, }) - this.disposers.push( - application.addEventObserver(async () => { - if (application.hasAccount()) { + eventBus.addEventHandler(this, ApplicationEvent.Launched) + eventBus.addEventHandler(this, ApplicationEvent.SignedIn) + eventBus.addEventHandler(this, ApplicationEvent.UserRolesChanged) + eventBus.addEventHandler(this, SubscriptionManagerEvent.DidFetchSubscription) + } + + override deinit() { + super.deinit() + ;(this.subscriptionInvitations as unknown) = undefined + + destroyAllObjectProperties(this) + } + + async handleEvent(event: InternalEventInterface): Promise { + switch (event.type) { + case ApplicationEvent.Launched: { + if (this.sessions.isSignedIn()) { this.reloadSubscriptionInvitations().catch(console.error) } runInAction(() => { - this.hasAccount = application.hasAccount() + this.hasAccount = this.sessions.isSignedIn() }) - }, ApplicationEvent.Launched), - ) + break + } - this.disposers.push( - application.addEventObserver(async () => { + case ApplicationEvent.SignedIn: { this.reloadSubscriptionInvitations().catch(console.error) runInAction(() => { - this.hasAccount = application.hasAccount() + this.hasAccount = this.sessions.isSignedIn() }) - }, ApplicationEvent.SignedIn), - ) + break + } - this.disposers.push( - application.subscriptions.addEventObserver(async (event) => { - if (event === SubscriptionManagerEvent.DidFetchSubscription) { - runInAction(() => { - this.onlineSubscription = application.subscriptions.getOnlineSubscription() - }) - } - }), - ) + case SubscriptionManagerEvent.DidFetchSubscription: { + runInAction(() => { + this.onlineSubscription = this.subscriptions.getOnlineSubscription() + }) + break + } - this.disposers.push( - application.addEventObserver(async () => { + case ApplicationEvent.UserRolesChanged: { this.reloadSubscriptionInvitations().catch(console.error) - }, ApplicationEvent.UserRolesChanged), - ) + break + } + } } get hasFirstPartyOnlineOrOfflineSubscription(): boolean { - if (this.application.sessions.isSignedIn()) { - if (!this.application.sessions.isSignedIntoFirstPartyServer()) { + if (this.sessions.isSignedIn()) { + if (!this.sessions.isSignedIntoFirstPartyServer()) { return false } - return this.application.subscriptions.getOnlineSubscription() !== undefined + return this.subscriptions.getOnlineSubscription() !== undefined } else { - return this.application.features.hasFirstPartyOfflineSubscription() + return this.features.hasFirstPartyOfflineSubscription() } } @@ -111,7 +118,7 @@ export class SubscriptionController extends AbstractViewController { } async sendSubscriptionInvitation(inviteeEmail: string): Promise { - const success = await this.subscriptionManager.inviteToSubscription(inviteeEmail) + const success = await this.subscriptions.inviteToSubscription(inviteeEmail) if (success) { await this.reloadSubscriptionInvitations() @@ -121,7 +128,7 @@ export class SubscriptionController extends AbstractViewController { } async cancelSubscriptionInvitation(invitationUuid: string): Promise { - const success = await this.subscriptionManager.cancelInvitation(invitationUuid) + const success = await this.subscriptions.cancelInvitation(invitationUuid) if (success) { await this.reloadSubscriptionInvitations() @@ -131,6 +138,6 @@ export class SubscriptionController extends AbstractViewController { } private async reloadSubscriptionInvitations(): Promise { - this.subscriptionInvitations = await this.subscriptionManager.listSubscriptionInvitations() + this.subscriptionInvitations = await this.subscriptions.listSubscriptionInvitations() } } diff --git a/packages/web/src/javascripts/Controllers/VaultSelectionMenuController.ts b/packages/web/src/javascripts/Controllers/VaultSelectionMenuController.ts index ff605901d..d18592e5b 100644 --- a/packages/web/src/javascripts/Controllers/VaultSelectionMenuController.ts +++ b/packages/web/src/javascripts/Controllers/VaultSelectionMenuController.ts @@ -1,5 +1,4 @@ import { InternalEventBusInterface } from '@standardnotes/snjs' -import { WebApplication } from '@/Application/WebApplication' import { action, makeObservable, observable } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' @@ -7,8 +6,8 @@ export class VaultSelectionMenuController extends AbstractViewController { open = false shouldAnimateCloseMenu = false - constructor(application: WebApplication, eventBus: InternalEventBusInterface) { - super(application, eventBus) + constructor(eventBus: InternalEventBusInterface) { + super(eventBus) makeObservable(this, { open: observable, diff --git a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts deleted file mode 100644 index a8a934be0..000000000 --- a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { PaneController } from './PaneController/PaneController' -import { - PersistedStateValue, - PersistenceKey, - storage, - StorageKey, - ToastService, - ToastServiceInterface, -} from '@standardnotes/ui-services' -import { WebApplication } from '@/Application/WebApplication' -import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' -import { destroyAllObjectProperties } from '@/Utils' -import { - DeinitSource, - WebOrDesktopDeviceInterface, - SubscriptionManagerInterface, - InternalEventHandlerInterface, - InternalEventInterface, -} from '@standardnotes/snjs' -import { action, makeObservable, observable } from 'mobx' -import { ActionsMenuController } from './ActionsMenuController' -import { FeaturesController } from './FeaturesController' -import { FilesController } from './FilesController' -import { NotesController } from './NotesController/NotesController' -import { ItemListController } from './ItemList/ItemListController' -import { NoAccountWarningController } from './NoAccountWarningController' -import { PreferencesController } from './PreferencesController' -import { PurchaseFlowController } from './PurchaseFlow/PurchaseFlowController' -import { QuickSettingsController } from './QuickSettingsController' -import { SearchOptionsController } from './SearchOptionsController' -import { SubscriptionController } from './Subscription/SubscriptionController' -import { SyncStatusController } from './SyncStatusController' -import { NavigationController } from './Navigation/NavigationController' -import { FilePreviewModalController } from './FilePreviewModalController' -import { SelectedItemsController } from './SelectedItemsController' -import { HistoryModalController } from './NoteHistory/HistoryModalController' -import { LinkingController } from './LinkingController' -import { PersistenceService } from './Abstract/PersistenceService' -import { CrossControllerEvent } from './CrossControllerEvent' -import { EventObserverInterface } from '@/Event/EventObserverInterface' -import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver' -import { ImportModalController } from './ImportModalController' -import { VaultSelectionMenuController } from './VaultSelectionMenuController' - -export class ViewControllerManager implements InternalEventHandlerInterface { - readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures - - private unsubAppEventObserver!: () => void - showBetaWarning: boolean - public dealloced = false - - readonly accountMenuController: AccountMenuController - readonly actionsMenuController = new ActionsMenuController() - readonly featuresController: FeaturesController - readonly filePreviewModalController: FilePreviewModalController - readonly filesController: FilesController - readonly noAccountWarningController: NoAccountWarningController - readonly notesController: NotesController - readonly itemListController: ItemListController - readonly preferencesController: PreferencesController - readonly purchaseFlowController: PurchaseFlowController - readonly quickSettingsMenuController: QuickSettingsController - readonly vaultSelectionController: VaultSelectionMenuController - readonly searchOptionsController: SearchOptionsController - readonly subscriptionController: SubscriptionController - readonly syncStatusController = new SyncStatusController() - readonly navigationController: NavigationController - readonly selectionController: SelectedItemsController - readonly historyModalController: HistoryModalController - readonly linkingController: LinkingController - readonly paneController: PaneController - readonly importModalController: ImportModalController - - public isSessionsModalVisible = false - - private appEventObserverRemovers: (() => void)[] = [] - - private subscriptionManager: SubscriptionManagerInterface - private persistenceService: PersistenceService - private applicationEventObserver: EventObserverInterface - private toastService: ToastServiceInterface - - constructor( - public application: WebApplication, - private device: WebOrDesktopDeviceInterface, - ) { - const eventBus = application.events - - this.persistenceService = new PersistenceService(application, eventBus) - - eventBus.addEventHandler(this, CrossControllerEvent.HydrateFromPersistedValues) - eventBus.addEventHandler(this, CrossControllerEvent.RequestValuePersistence) - - this.subscriptionManager = application.subscriptions - - this.filePreviewModalController = new FilePreviewModalController(application) - - this.quickSettingsMenuController = new QuickSettingsController(application, eventBus) - - this.vaultSelectionController = new VaultSelectionMenuController(application, eventBus) - - this.paneController = new PaneController(application, eventBus) - - this.preferencesController = new PreferencesController(application, eventBus) - - this.selectionController = new SelectedItemsController(application, eventBus) - - this.featuresController = new FeaturesController(application, eventBus) - - this.navigationController = new NavigationController(application, this.featuresController, eventBus) - - this.notesController = new NotesController( - application, - this.selectionController, - this.navigationController, - eventBus, - ) - - this.searchOptionsController = new SearchOptionsController(application, eventBus) - - this.linkingController = new LinkingController( - application, - this.navigationController, - this.selectionController, - eventBus, - ) - - this.itemListController = new ItemListController( - application, - this.navigationController, - this.searchOptionsController, - this.selectionController, - this.notesController, - eventBus, - ) - - this.notesController.setServicesPostConstruction(this.itemListController) - this.selectionController.setServicesPostConstruction(this.itemListController) - - this.noAccountWarningController = new NoAccountWarningController(application, eventBus) - - this.accountMenuController = new AccountMenuController(application, eventBus) - - this.subscriptionController = new SubscriptionController(application, eventBus, this.subscriptionManager) - - this.purchaseFlowController = new PurchaseFlowController(application, eventBus) - - this.filesController = new FilesController( - application, - this.notesController, - this.filePreviewModalController, - eventBus, - ) - - this.linkingController.setServicesPostConstruction( - this.itemListController, - this.filesController, - this.subscriptionController, - ) - - this.historyModalController = new HistoryModalController(this.application, eventBus, this.notesController) - - this.importModalController = new ImportModalController(this.application, this.navigationController) - - this.toastService = new ToastService() - - this.applicationEventObserver = new ApplicationEventObserver( - application, - application.routeService, - this.purchaseFlowController, - this.accountMenuController, - this.preferencesController, - this.syncStatusController, - application.sync, - application.sessions, - application.subscriptions, - this.toastService, - application.user, - ) - - this.addAppEventObserver() - - if (this.device.appVersion.includes('-beta')) { - this.showBetaWarning = storage.get(StorageKey.ShowBetaWarning) ?? true - } else { - this.showBetaWarning = false - } - - makeObservable(this, { - showBetaWarning: observable, - isSessionsModalVisible: observable, - preferencesController: observable, - - openSessionsModal: action, - closeSessionsModal: action, - }) - } - - deinit(source: DeinitSource): void { - this.dealloced = true - ;(this.application as unknown) = undefined - - if (source === DeinitSource.SignOut) { - storage.remove(StorageKey.ShowBetaWarning) - this.noAccountWarningController.reset() - } - - this.unsubAppEventObserver?.() - ;(this.unsubAppEventObserver as unknown) = undefined - - this.appEventObserverRemovers.forEach((remover) => remover()) - this.appEventObserverRemovers.length = 0 - ;(this.device as unknown) = undefined - this.filePreviewModalController.deinit() - ;(this.filePreviewModalController as unknown) = undefined - ;(this.preferencesController as unknown) = undefined - ;(this.quickSettingsMenuController as unknown) = undefined - ;(this.vaultSelectionController as unknown) = undefined - ;(this.syncStatusController as unknown) = undefined - - this.persistenceService.deinit() - ;(this.persistenceService as unknown) = undefined - - this.actionsMenuController.reset() - ;(this.actionsMenuController as unknown) = undefined - - this.featuresController.deinit() - ;(this.featuresController as unknown) = undefined - - this.accountMenuController.deinit() - ;(this.accountMenuController as unknown) = undefined - - this.filesController.deinit() - ;(this.filesController as unknown) = undefined - - this.noAccountWarningController.deinit() - ;(this.noAccountWarningController as unknown) = undefined - - this.notesController.deinit() - ;(this.notesController as unknown) = undefined - - this.itemListController.deinit() - ;(this.itemListController as unknown) = undefined - - this.linkingController.deinit() - ;(this.linkingController as unknown) = undefined - - this.purchaseFlowController.deinit() - ;(this.purchaseFlowController as unknown) = undefined - - this.searchOptionsController.deinit() - ;(this.searchOptionsController as unknown) = undefined - - this.subscriptionController.deinit() - ;(this.subscriptionController as unknown) = undefined - - this.navigationController.deinit() - ;(this.navigationController as unknown) = undefined - - this.historyModalController.deinit() - ;(this.historyModalController as unknown) = undefined - - this.paneController.deinit() - ;(this.paneController as unknown) = undefined - - destroyAllObjectProperties(this) - } - - openSessionsModal = () => { - this.isSessionsModalVisible = true - } - - closeSessionsModal = () => { - this.isSessionsModalVisible = false - } - - addAppEventObserver() { - this.unsubAppEventObserver = this.application.addEventObserver( - this.applicationEventObserver.handle.bind(this.applicationEventObserver), - ) - } - - persistValues = (): void => { - const values: PersistedStateValue = { - [PersistenceKey.SelectedItemsController]: this.selectionController.getPersistableValue(), - [PersistenceKey.NavigationController]: this.navigationController.getPersistableValue(), - } - - this.persistenceService.persistValues(values) - - const selectedItemsState = values['selected-items-controller'] - const navigationSelectionState = values['navigation-controller'] - const launchPriorityUuids: string[] = [] - if (selectedItemsState.selectedUuids.length) { - launchPriorityUuids.push(...selectedItemsState.selectedUuids) - } - if (navigationSelectionState.selectedTagUuid) { - launchPriorityUuids.push(navigationSelectionState.selectedTagUuid) - } - this.application.sync.setLaunchPriorityUuids(launchPriorityUuids) - } - - clearPersistedValues = (): void => { - this.persistenceService.clearPersistedValues() - } - - hydrateFromPersistedValues = (values: PersistedStateValue | undefined): void => { - const navigationState = values?.[PersistenceKey.NavigationController] - this.navigationController.hydrateFromPersistedValue(navigationState) - - const selectedItemsState = values?.[PersistenceKey.SelectedItemsController] - this.selectionController.hydrateFromPersistedValue(selectedItemsState) - } - - async handleEvent(event: InternalEventInterface): Promise { - if (event.type === CrossControllerEvent.HydrateFromPersistedValues) { - this.hydrateFromPersistedValues(event.payload as PersistedStateValue | undefined) - } else if (event.type === CrossControllerEvent.RequestValuePersistence) { - this.persistValues() - } - } -} diff --git a/packages/web/src/javascripts/Event/ApplicationEventObserver.ts b/packages/web/src/javascripts/Event/ApplicationEventObserver.ts index 68b45c62e..e8e4c24b5 100644 --- a/packages/web/src/javascripts/Event/ApplicationEventObserver.ts +++ b/packages/web/src/javascripts/Event/ApplicationEventObserver.ts @@ -4,6 +4,7 @@ import { RouteServiceInterface, RouteType, ToastServiceInterface, + WebApplicationInterface, } from '@standardnotes/ui-services' import { ApplicationEvent, @@ -21,13 +22,12 @@ import { SyncStatusController } from '@/Controllers/SyncStatusController' import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane' import { EventObserverInterface } from './EventObserverInterface' -import { WebApplication } from '@/Application/WebApplication' export const JoinWorkspaceSuccessString = 'Successfully joined a shared subscription.' export class ApplicationEventObserver implements EventObserverInterface { constructor( - private application: WebApplication, + private application: WebApplicationInterface, private routeService: RouteServiceInterface, private purchaseFlowController: PurchaseFlowController, private accountMenuController: AccountMenuController, diff --git a/packages/web/src/javascripts/Hooks/useContextMenuEvent.tsx b/packages/web/src/javascripts/Hooks/useContextMenuEvent.tsx index fead61e1f..cd57cee63 100644 --- a/packages/web/src/javascripts/Hooks/useContextMenuEvent.tsx +++ b/packages/web/src/javascripts/Hooks/useContextMenuEvent.tsx @@ -1,6 +1,6 @@ -import { isIOS } from '@/Utils' import { RefObject, useCallback, useEffect } from 'react' import { useLongPressEvent } from './useLongPress' +import { isIOS } from '@standardnotes/ui-services' export const useContextMenuEvent = (elementRef: RefObject, listener: (x: number, y: number) => void) => { const { attachEvents, cleanupEvents } = useLongPressEvent(elementRef, listener) diff --git a/packages/web/src/javascripts/Hooks/useDocumentRect.ts b/packages/web/src/javascripts/Hooks/useDocumentRect.ts index b8c8f771e..a9dea4082 100644 --- a/packages/web/src/javascripts/Hooks/useDocumentRect.ts +++ b/packages/web/src/javascripts/Hooks/useDocumentRect.ts @@ -1,4 +1,4 @@ -import { isIOS } from '@/Utils' +import { isIOS } from '@standardnotes/ui-services' import { useEffect, useState } from 'react' const DebounceTimeInMs = 100 diff --git a/packages/web/src/javascripts/Hooks/useIsTabletOrMobileScreen.tsx b/packages/web/src/javascripts/Hooks/useIsTabletOrMobileScreen.tsx index db948ed1c..8442c8921 100644 --- a/packages/web/src/javascripts/Hooks/useIsTabletOrMobileScreen.tsx +++ b/packages/web/src/javascripts/Hooks/useIsTabletOrMobileScreen.tsx @@ -1,28 +1,12 @@ -import { WebApplication } from '@/Application/WebApplication' +import { IsTabletOrMobileScreen } from '@/Application/UseCase/IsTabletOrMobileScreen' import { useApplication } from '@/Components/ApplicationProvider' -import { debounce, isMobileScreen, isTabletOrMobileScreen, isTabletScreen } from '@/Utils' -import { useEffect, useState } from 'react' - -export function getIsTabletOrMobileScreen(application: WebApplication) { - const isNativeMobile = application.isNativeMobileWeb() - const isTabletOrMobile = isTabletOrMobileScreen() || isNativeMobile - const isTablet = isTabletScreen() || (isNativeMobile && !isMobileScreen()) - const isMobile = isMobileScreen() || (isNativeMobile && !isTablet) - - if (isTablet && isMobile) { - throw Error('isTablet and isMobile cannot both be true') - } - - return { - isTabletOrMobile, - isTablet, - isMobile, - } -} +import { debounce } from '@/Utils' +import { useEffect, useMemo, useState } from 'react' export default function useIsTabletOrMobileScreen() { const [_windowSize, setWindowSize] = useState(0) const application = useApplication() + const usecase = useMemo(() => new IsTabletOrMobileScreen(application.environment), [application]) useEffect(() => { const handleResize = debounce(() => { @@ -37,5 +21,6 @@ export default function useIsTabletOrMobileScreen() { } }, []) - return getIsTabletOrMobileScreen(application) + const isTabletOrMobileScreen = usecase.execute().getValue() + return isTabletOrMobileScreen } diff --git a/packages/web/src/javascripts/Hooks/useItem.ts b/packages/web/src/javascripts/Hooks/useItem.ts index 095685f8b..836f4edc0 100644 --- a/packages/web/src/javascripts/Hooks/useItem.ts +++ b/packages/web/src/javascripts/Hooks/useItem.ts @@ -12,7 +12,7 @@ const useItem = (uuid: string | undefined) => return } - const live = new LiveItem(uuid, application, (item) => { + const live = new LiveItem(uuid, application.items, (item) => { setItem(item) }) diff --git a/packages/web/src/javascripts/Hooks/useItemLinks.ts b/packages/web/src/javascripts/Hooks/useItemLinks.ts index 8a53cd225..cd90340ac 100644 --- a/packages/web/src/javascripts/Hooks/useItemLinks.ts +++ b/packages/web/src/javascripts/Hooks/useItemLinks.ts @@ -17,7 +17,7 @@ export const useItemLinks = (item: DecryptedItem | undefined) => { useEffect( () => - application.streamItems([ContentType.TYPES.Note, ContentType.TYPES.File, ContentType.TYPES.Tag], () => { + application.items.streamItems([ContentType.TYPES.Note, ContentType.TYPES.File, ContentType.TYPES.Tag], () => { refresh(Date.now()) }), [application], diff --git a/packages/web/src/javascripts/Hooks/usePremiumModal.tsx b/packages/web/src/javascripts/Hooks/usePremiumModal.tsx index c894af7ee..332d7083a 100644 --- a/packages/web/src/javascripts/Hooks/usePremiumModal.tsx +++ b/packages/web/src/javascripts/Hooks/usePremiumModal.tsx @@ -2,7 +2,6 @@ import { WebApplication } from '@/Application/WebApplication' import { observer } from 'mobx-react-lite' import { FunctionComponent, createContext, useCallback, useContext, ReactNode } from 'react' import PremiumFeaturesModal from '@/Components/PremiumFeaturesModal/PremiumFeaturesModal' -import { FeaturesController } from '@/Controllers/FeaturesController' type PremiumModalContextData = { activate: (featureName: string) => void @@ -24,53 +23,46 @@ export const usePremiumModal = (): PremiumModalContextData => { interface Props { application: WebApplication - featuresController: FeaturesController children: ReactNode } -const PremiumModalProvider: FunctionComponent = observer( - ({ application, featuresController, children }: Props) => { - const featureName = featuresController.premiumAlertFeatureName || '' +const PremiumModalProvider: FunctionComponent = observer(({ application, children }: Props) => { + const featureName = application.featuresController.premiumAlertFeatureName || '' - const hasSubscription = application.hasValidFirstPartySubscription() + const hasSubscription = application.hasValidFirstPartySubscription() - const activate = useCallback( - (feature: string) => { - featuresController.showPremiumAlert(feature).catch(console.error) - }, - [featuresController], - ) + const activate = useCallback( + (feature: string) => { + application.featuresController.showPremiumAlert(feature).catch(console.error) + }, + [application.featuresController], + ) - const close = useCallback(() => { - featuresController.closePremiumAlert() - }, [featuresController]) + const close = useCallback(() => { + application.featuresController.closePremiumAlert() + }, [application.featuresController]) - return ( - <> - {featuresController.premiumAlertType != undefined && ( - - )} - {children} - - ) - }, -) + return ( + <> + {application.featuresController.premiumAlertType != undefined && ( + + )} + {children} + + ) +}) PremiumModalProvider.displayName = 'PremiumModalProvider' -const PremiumModalProviderWithDeallocateHandling: FunctionComponent = ({ - application, - featuresController, - children, -}) => { - return +const PremiumModalProviderWithDeallocateHandling: FunctionComponent = ({ application, children }) => { + return } export default observer(PremiumModalProviderWithDeallocateHandling) diff --git a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx index f76eb80d7..fdd705c17 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx @@ -1,19 +1,15 @@ -import { WebApplication } from '@/Application/WebApplication' import { getBase64FromBlob } from '@/Utils' import { parseFileName } from '@standardnotes/filepicker' -import { Platform } from '@standardnotes/snjs' +import { MobileDeviceInterface } from '@standardnotes/snjs' import { addToast, ToastType, dismissToast } from '@standardnotes/toast' import { sanitizeFileName } from '@standardnotes/ui-services' export const downloadBlobOnAndroid = async ( - application: WebApplication, + mobileDevice: MobileDeviceInterface, blob: Blob, filename: string, showToast = true, ) => { - if (!application.isNativeMobileWeb() || application.platform !== Platform.Android) { - throw new Error('Download function being used on non-android platform') - } let loadingToastId: string | undefined if (showToast) { loadingToastId = addToast({ @@ -25,7 +21,7 @@ export const downloadBlobOnAndroid = async ( const { name, ext } = parseFileName(filename) const sanitizedName = sanitizeFileName(name) filename = `${sanitizedName}.${ext}` - const downloaded = await application.mobileDevice().downloadBase64AsFile(base64, filename) + const downloaded = await mobileDevice.downloadBase64AsFile(base64, filename) if (loadingToastId) { dismissToast(loadingToastId) } diff --git a/packages/web/src/javascripts/NativeMobileWeb/DownloadSelectedNotesOnAndroid.tsx b/packages/web/src/javascripts/NativeMobileWeb/DownloadSelectedNotesOnAndroid.tsx index 3dd623d92..cbddd19b5 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/DownloadSelectedNotesOnAndroid.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/DownloadSelectedNotesOnAndroid.tsx @@ -14,11 +14,11 @@ export const downloadSelectedNotesOnAndroid = async (application: WebApplication const blob = getNoteBlob(application, note) const { name, ext } = parseFileName(getNoteFileName(application, note)) const filename = `${sanitizeFileName(name)}.${ext}` - await downloadBlobOnAndroid(application, blob, filename) + await downloadBlobOnAndroid(application.mobileDevice, blob, filename) return } if (notes.length > 1) { - const zippedDataBlob = await application.getArchiveService().zipData( + const zippedDataBlob = await application.archiveService.zipData( notes.map((note) => { return { name: getNoteFileName(application, note), @@ -26,7 +26,7 @@ export const downloadSelectedNotesOnAndroid = async (application: WebApplication } }), ) - const filename = `Standard Notes Export - ${application.getArchiveService().formattedDateForExports()}.zip` - await downloadBlobOnAndroid(application, zippedDataBlob, filename) + const filename = `Standard Notes Export - ${application.archiveService.formattedDateForExports()}.zip` + await downloadBlobOnAndroid(application.mobileDevice, zippedDataBlob, filename) } } diff --git a/packages/web/src/javascripts/NativeMobileWeb/ShareBlobOnMobile.ts b/packages/web/src/javascripts/NativeMobileWeb/ShareBlobOnMobile.ts index 92bf8bad6..aa361dbbe 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/ShareBlobOnMobile.ts +++ b/packages/web/src/javascripts/NativeMobileWeb/ShareBlobOnMobile.ts @@ -1,10 +1,15 @@ -import { WebApplication } from '@/Application/WebApplication' import { getBase64FromBlob } from '@/Utils' +import { MobileDeviceInterface } from '@standardnotes/snjs' -export const shareBlobOnMobile = async (application: WebApplication, blob: Blob, filename: string) => { - if (!application.isNativeMobileWeb()) { +export const shareBlobOnMobile = async ( + mobileDevice: MobileDeviceInterface, + isNativeMobileWeb: boolean, + blob: Blob, + filename: string, +) => { + if (!isNativeMobileWeb) { throw new Error('Share function being used outside mobile webview') } const base64 = await getBase64FromBlob(blob) - void application.mobileDevice().shareBase64AsFile(base64, filename) + void mobileDevice.shareBase64AsFile(base64, filename) } diff --git a/packages/web/src/javascripts/NativeMobileWeb/ShareSelectedNotes.tsx b/packages/web/src/javascripts/NativeMobileWeb/ShareSelectedNotes.tsx index 671afdda6..0af85acba 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/ShareSelectedNotes.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/ShareSelectedNotes.tsx @@ -14,11 +14,11 @@ export const shareSelectedNotes = async (application: WebApplication, notes: SNN const blob = getNoteBlob(application, note) const { name, ext } = parseFileName(getNoteFileName(application, note)) const filename = `${sanitizeFileName(name)}.${ext}` - void shareBlobOnMobile(application, blob, filename) + void shareBlobOnMobile(application.mobileDevice, application.isNativeMobileWeb(), blob, filename) return } if (notes.length > 1) { - const zippedDataBlob = await application.getArchiveService().zipData( + const zippedDataBlob = await application.archiveService.zipData( notes.map((note) => { return { name: getNoteFileName(application, note), @@ -27,9 +27,10 @@ export const shareSelectedNotes = async (application: WebApplication, notes: SNN }), ) void shareBlobOnMobile( - application, + application.mobileDevice, + application.isNativeMobileWeb(), zippedDataBlob, - `Standard Notes Export - ${application.getArchiveService().formattedDateForExports()}.zip`, + `Standard Notes Export - ${application.archiveService.formattedDateForExports()}.zip`, ) } } diff --git a/packages/web/src/javascripts/NativeMobileWeb/useAndroidBackHandler.tsx b/packages/web/src/javascripts/NativeMobileWeb/useAndroidBackHandler.tsx index b78ac443a..6988a4d39 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/useAndroidBackHandler.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/useAndroidBackHandler.tsx @@ -37,7 +37,7 @@ const AndroidBackHandlerProvider = ({ application, children }: ProviderProps) => application.setAndroidBackHandlerFallbackListener(() => { const shouldConfirm = (application.getValue(AndroidConfirmBeforeExitKey) as boolean) ?? true - application.mobileDevice().exitApp(shouldConfirm) + application.mobileDevice.exitApp(shouldConfirm) return true }) diff --git a/packages/web/src/javascripts/Utils/DownloadOrShareBasedOnPlatform.ts b/packages/web/src/javascripts/Utils/DownloadOrShareBasedOnPlatform.ts index 4ebd00395..a4dd3d0d9 100644 --- a/packages/web/src/javascripts/Utils/DownloadOrShareBasedOnPlatform.ts +++ b/packages/web/src/javascripts/Utils/DownloadOrShareBasedOnPlatform.ts @@ -1,26 +1,29 @@ -import { WebApplication } from '@/Application/WebApplication' import { downloadBlobOnAndroid } from '@/NativeMobileWeb/DownloadBlobOnAndroid' import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile' -import { Platform } from '@standardnotes/snjs' +import { MobileDeviceInterface, Platform } from '@standardnotes/snjs' +import { ArchiveManager } from '@standardnotes/ui-services' -export const downloadOrShareBlobBasedOnPlatform = async ( - application: WebApplication, - blob: Blob, - filename: string, - showToastOnAndroid = true, -) => { - if (!application.isNativeMobileWeb()) { - application.getArchiveService().downloadData(blob, filename) +export const downloadOrShareBlobBasedOnPlatform = async (dto: { + archiveService: ArchiveManager + platform: Platform + mobileDevice: MobileDeviceInterface | undefined + blob: Blob + filename: string + isNativeMobileWeb: boolean + showToastOnAndroid?: boolean +}) => { + if (!dto.isNativeMobileWeb) { + dto.archiveService.downloadData(dto.blob, dto.filename) return } - if (application.platform === Platform.Ios) { - void shareBlobOnMobile(application, blob, filename) + if (dto.mobileDevice && dto.platform === Platform.Ios) { + void shareBlobOnMobile(dto.mobileDevice, dto.isNativeMobileWeb, dto.blob, dto.filename) return } - if (application.platform === Platform.Android) { - void downloadBlobOnAndroid(application, blob, filename, showToastOnAndroid) + if (dto.mobileDevice && dto.platform === Platform.Android) { + void downloadBlobOnAndroid(dto.mobileDevice, dto.blob, dto.filename, dto.showToastOnAndroid ?? true) return } } diff --git a/packages/web/src/javascripts/Utils/ManageSubscription.ts b/packages/web/src/javascripts/Utils/ManageSubscription.ts deleted file mode 100644 index 92f1eaedf..000000000 --- a/packages/web/src/javascripts/Utils/ManageSubscription.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Environment } from '@standardnotes/snjs' -import { WebApplicationInterface } from '@standardnotes/ui-services' - -export async function openSubscriptionDashboard(application: WebApplicationInterface) { - const token = await application.getNewSubscriptionToken() - if (!token) { - return - } - - const url = `${window.dashboardUrl}?subscription_token=${token}` - - if (application.device.environment === Environment.Mobile) { - application.device.openUrl(url) - return - } - - if (application.device.environment === Environment.Desktop) { - window.open(url, '_blank') - return - } - - const windowProxy = window.open('', '_blank') - ;(windowProxy as WindowProxy).location = url -} diff --git a/packages/web/src/javascripts/Utils/Utils.ts b/packages/web/src/javascripts/Utils/Utils.ts index 0ecb9d8d8..c9428cf57 100644 --- a/packages/web/src/javascripts/Utils/Utils.ts +++ b/packages/web/src/javascripts/Utils/Utils.ts @@ -2,6 +2,7 @@ import { DeviceInterface, MobileDeviceInterface, Platform, platformFromString } import { IsDesktopPlatform, IsWebPlatform } from '@/Constants/Version' import { EMAIL_REGEX } from '../Constants/Constants' import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery' +import { isIOS } from '@standardnotes/ui-services' declare const process: { env: { @@ -176,14 +177,6 @@ export const convertStringifiedBooleanToBoolean = (value: string) => { return value !== 'false' } -// https://stackoverflow.com/questions/9038625/detect-if-device-is-ios/9039885#9039885 -export const isIOS = () => - (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) || - (navigator.userAgent.includes('Mac') && 'ontouchend' in document && navigator.maxTouchPoints > 1) || - window.platform === Platform.Ios - -export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android') - // https://stackoverflow.com/a/57527009/2504429 export const disableIosTextFieldZoom = () => { const addMaximumScaleToMetaViewport = () => {