refactor(web): dependency management (#2386)

This commit is contained in:
Mo
2023-08-05 12:48:39 -05:00
committed by GitHub
parent b07da5b663
commit d8d4052a52
274 changed files with 4065 additions and 3873 deletions

View File

@@ -723,7 +723,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga" :path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS: SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558 boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 4cce221dd782d3ff7c4172167bba09d58af67ccb FBLazyVector: 4cce221dd782d3ff7c4172167bba09d58af67ccb
@@ -743,7 +743,7 @@ SPEC CHECKSUMS:
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18 RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18
RCTTypeSafety: cb09f3e4747b6d18331a15eb05271de7441ca0b3 RCTTypeSafety: cb09f3e4747b6d18331a15eb05271de7441ca0b3
React: 13109005b5353095c052f26af37413340ccf7a5d React: 13109005b5353095c052f26af37413340ccf7a5d

View File

@@ -10,6 +10,8 @@ export interface LegacyApiServiceInterface
extends AbstractService<ApiServiceEvent, ApiServiceEventData>, extends AbstractService<ApiServiceEvent, ApiServiceEventData>,
FilesApiInterface { FilesApiInterface {
isThirdPartyHostUsed(): boolean isThirdPartyHostUsed(): boolean
setHost(host: string): Promise<void>
getHost(): string
downloadOfflineFeaturesFromRepo( downloadOfflineFeaturesFromRepo(
repo: SNFeatureRepo, repo: SNFeatureRepo,
@@ -24,4 +26,6 @@ export interface LegacyApiServiceInterface
limit: number, limit: number,
sharedVaultUuids?: string[], sharedVaultUuids?: string[],
): HttpRequest ): HttpRequest
getNewSubscriptionToken(): Promise<string | undefined>
} }

View File

@@ -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 { VaultLockServiceInterface } from './../VaultLock/VaultLockServiceInterface'
import { HistoryServiceInterface } from './../History/HistoryServiceInterface' import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface' import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface'
import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface' import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface'
import { SyncOptions } from './../Sync/SyncOptions'
import { ImportDataReturnType } from './../Mutator/ImportDataUseCase' import { ImportDataReturnType } from './../Mutator/ImportDataUseCase'
import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface' import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface'
import { VaultServiceInterface } from '../Vault/VaultServiceInterface' import { VaultServiceInterface } from '../Vault/VaultServiceInterface'
import { ApplicationIdentifier } from '@standardnotes/common' import { ApplicationIdentifier } from '@standardnotes/common'
import { import { BackupFile, Environment, Platform, PrefKey, PrefValue } from '@standardnotes/models'
BackupFile,
DecryptedItemInterface,
DecryptedItemMutator,
ItemStream,
PayloadEmitSource,
Platform,
PrefKey,
PrefValue,
} from '@standardnotes/models'
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files' import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
import { AlertService } from '../Alert/AlertService' import { AlertService } from '../Alert/AlertService'
@@ -37,7 +40,6 @@ import { DeinitSource } from './DeinitSource'
import { UserServiceInterface } from '../User/UserServiceInterface' import { UserServiceInterface } from '../User/UserServiceInterface'
import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface' import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface'
import { User } from '@standardnotes/responses'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
export interface ApplicationInterface { export interface ApplicationInterface {
@@ -53,49 +55,24 @@ export interface ApplicationInterface {
createDecryptedBackupFile(): Promise<BackupFile | undefined> createDecryptedBackupFile(): Promise<BackupFile | undefined>
hasPasscode(): boolean hasPasscode(): boolean
lock(): Promise<void> lock(): Promise<void>
softLockBiometrics(): void
setValue(key: string, value: unknown, mode?: StorageValueModes): void setValue(key: string, value: unknown, mode?: StorageValueModes): void
getValue<T>(key: string, mode?: StorageValueModes): T getValue<T>(key: string, mode?: StorageValueModes): T
removeValue(key: string, mode?: StorageValueModes): Promise<void> removeValue(key: string, mode?: StorageValueModes): Promise<void>
isLocked(): Promise<boolean>
getPreference<K extends PrefKey>(key: K): PrefValue[K] | undefined getPreference<K extends PrefKey>(key: K): PrefValue[K] | undefined
getPreference<K extends PrefKey>(key: K, defaultValue: PrefValue[K]): PrefValue[K] getPreference<K extends PrefKey>(key: K, defaultValue: PrefValue[K]): PrefValue[K]
getPreference<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined getPreference<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined
setPreference<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void> setPreference<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>
streamItems<I extends DecryptedItemInterface = DecryptedItemInterface>(
contentType: string | string[],
stream: ItemStream<I>,
): () => void
getUser(): User | undefined
hasAccount(): boolean hasAccount(): boolean
setCustomHost(host: string): Promise<void> setCustomHost(host: string): Promise<void>
isThirdPartyHostUsed(): boolean isThirdPartyHostUsed(): boolean
isUsingHomeServer(): Promise<boolean> isUsingHomeServer(): Promise<boolean>
getNewSubscriptionToken(): Promise<string | undefined>
importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType> importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
/**
* Mutates a pre-existing item, marks it as dirty, and syncs it
*/
changeAndSaveItem<M extends DecryptedItemMutator = DecryptedItemMutator>(
itemToLookupUuidFor: DecryptedItemInterface,
mutate: (mutator: M) => void,
updateTimestamps?: boolean,
emitSource?: PayloadEmitSource,
syncOptions?: SyncOptions,
): Promise<DecryptedItemInterface | undefined>
/** get changeAndSaveItem(): ChangeAndSaveItem
* Mutates pre-existing items, marks them as dirty, and syncs get getHost(): GetHost
*/ get setHost(): SetHost
changeAndSaveItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
itemsToLookupUuidsFor: DecryptedItemInterface[],
mutate: (mutator: M) => void,
updateTimestamps?: boolean,
emitSource?: PayloadEmitSource,
syncOptions?: SyncOptions,
): Promise<void>
get alerts(): AlertService get alerts(): AlertService
get asymmetric(): AsymmetricMessageServiceInterface get asymmetric(): AsymmetricMessageServiceInterface
@@ -109,16 +86,24 @@ export interface ApplicationInterface {
get history(): HistoryServiceInterface get history(): HistoryServiceInterface
get homeServer(): HomeServerServiceInterface | undefined get homeServer(): HomeServerServiceInterface | undefined
get items(): ItemManagerInterface get items(): ItemManagerInterface
get legacyApi(): LegacyApiServiceInterface
get mfa(): MfaServiceInterface
get mutator(): MutatorClientInterface get mutator(): MutatorClientInterface
get preferences(): PreferenceServiceInterface get preferences(): PreferenceServiceInterface
get protections(): ProtectionsClientInterface
get sessions(): SessionsClientInterface get sessions(): SessionsClientInterface
get status(): StatusServiceInterface
get storage(): StorageServiceInterface
get subscriptions(): SubscriptionManagerInterface get subscriptions(): SubscriptionManagerInterface
get sync(): SyncServiceInterface
get user(): UserServiceInterface get user(): UserServiceInterface
get vaults(): VaultServiceInterface
get vaultLocks(): VaultLockServiceInterface
get vaultUsers(): VaultUserServiceInterface
get vaultInvites(): VaultInviteServiceInterface get vaultInvites(): VaultInviteServiceInterface
get vaultLocks(): VaultLockServiceInterface
get vaults(): VaultServiceInterface
get vaultUsers(): VaultUserServiceInterface
readonly options: FullyResolvedApplicationOptions
readonly environment: Environment
readonly identifier: ApplicationIdentifier readonly identifier: ApplicationIdentifier
readonly platform: Platform readonly platform: Platform
device: DeviceInterface device: DeviceInterface

View File

@@ -14,8 +14,6 @@ export interface ChallengeServiceInterface extends AbstractService {
submitValuesForChallenge(challenge: ChallengeInterface, values: ChallengeValue[]): Promise<void> submitValuesForChallenge(challenge: ChallengeInterface, values: ChallengeValue[]): Promise<void>
cancelChallenge(challenge: ChallengeInterface): void cancelChallenge(challenge: ChallengeInterface): void
isPasscodeLocked(): Promise<boolean>
/** /**
* Resolves when the challenge has been completed. * Resolves when the challenge has been completed.
* For non-validated challenges, will resolve when the first value is submitted. * For non-validated challenges, will resolve when the first value is submitted.

View File

@@ -4,4 +4,7 @@ export interface DesktopManagerInterface {
syncComponentsInstallation(components: ComponentInterface[]): void syncComponentsInstallation(components: ComponentInterface[]): void
registerUpdateObserver(callback: (component: ComponentInterface) => void): () => void registerUpdateObserver(callback: (component: ComponentInterface) => void): () => void
getExtServerHost(): string getExtServerHost(): string
saveDesktopBackup(): Promise<void>
searchText(text?: string): void
redoSearch(): void
} }

View File

@@ -24,6 +24,8 @@ import {
export interface EncryptionProviderInterface { export interface EncryptionProviderInterface {
initialize(): Promise<void> initialize(): Promise<void>
isPasscodeLocked(): Promise<boolean>
encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface> encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface>
encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]> encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]>
decryptSplitSingle< decryptSplitSingle<

View File

@@ -191,6 +191,7 @@ export class EncryptionService
return ProtocolVersionLatest return ProtocolVersionLatest
} }
/** Unlike SessionManager.isSignedIn, hasAccount can be read before the application is unlocked and is based on the key state */
public hasAccount() { public hasAccount() {
return this.rootKeyManager.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). * @returns True if the root key has not yet been unwrapped (passcode locked).
*/ */
public async isPasscodeLocked() { public async isPasscodeLocked(): Promise<boolean> {
return (await this.rootKeyManager.hasRootKeyWrapper()) && this.rootKeyManager.getRootKey() == undefined return (await this.rootKeyManager.hasRootKeyWrapper()) && this.rootKeyManager.getRootKey() == undefined
} }

View File

@@ -55,7 +55,5 @@ export enum ApplicationEvent {
UnprotectedSessionExpired = 'Application:UnprotectedSessionExpired', UnprotectedSessionExpired = 'Application:UnprotectedSessionExpired',
/** Called when the app first launches and after first sync request made after sign in */ /** Called when the app first launches and after first sync request made after sign in */
CompletedInitialSync = 'Application:CompletedInitialSync', CompletedInitialSync = 'Application:CompletedInitialSync',
BiometricsSoftLockEngaged = 'Application:BiometricsSoftLockEngaged',
BiometricsSoftLockDisengaged = 'Application:BiometricsSoftLockDisengaged',
DidPurchaseSubscription = 'Application:DidPurchaseSubscription', DidPurchaseSubscription = 'Application:DidPurchaseSubscription',
} }

View File

@@ -22,6 +22,7 @@ import {
NotesAndFilesDisplayControllerOptions, NotesAndFilesDisplayControllerOptions,
ThemeInterface, ThemeInterface,
ComponentInterface, ComponentInterface,
ItemStream,
} from '@standardnotes/models' } from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
@@ -57,6 +58,11 @@ export interface ItemManagerInterface extends AbstractService {
callback: ItemManagerChangeObserverCallback<I>, callback: ItemManagerChangeObserverCallback<I>,
): () => void ): () => void
streamItems<I extends DecryptedItemInterface = DecryptedItemInterface>(
contentType: string | string[],
stream: ItemStream<I>,
): () => void
get items(): DecryptedItemInterface[] get items(): DecryptedItemInterface[]
getItems<T extends DecryptedItemInterface>(contentType: string | string[]): T[] getItems<T extends DecryptedItemInterface>(contentType: string | string[]): T[]

View File

@@ -1,11 +1,7 @@
export interface MfaProvider { export interface MfaServiceInterface {
isMfaActivated(): Promise<boolean> isMfaActivated(): Promise<boolean>
generateMfaSecret(): Promise<string> generateMfaSecret(): Promise<string>
getOtpToken(secret: string): Promise<string> getOtpToken(secret: string): Promise<string>
enableMfa(secret: string, otpToken: string): Promise<void> enableMfa(secret: string, otpToken: string): Promise<void>
disableMfa(): Promise<void> disableMfa(): Promise<void>
} }

View File

@@ -6,9 +6,9 @@ export enum PreferencesServiceEvent {
} }
export interface PreferenceServiceInterface extends AbstractService<PreferencesServiceEvent> { export interface PreferenceServiceInterface extends AbstractService<PreferencesServiceEvent> {
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K]): PrefValue[K] getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K]): PrefValue[K]
getValue<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined getValue<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined
setValue<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void> setValue<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>
/** Set value without triggering sync or event notifications */ /** Set value without triggering sync or event notifications */

View File

@@ -1,9 +1,14 @@
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models' import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models'
import { ChallengeInterface, ChallengeReason } from '../Challenge' import { ChallengeInterface, ChallengeReason } from '../Challenge'
import { MobileUnlockTiming } from './MobileUnlockTiming' import { MobileUnlockTiming } from './MobileUnlockTiming'
import { TimingDisplayOption } from './TimingDisplayOption' import { TimingDisplayOption } from './TimingDisplayOption'
import { ProtectionEvent } from './ProtectionEvent'
export interface ProtectionsClientInterface extends ApplicationServiceInterface<ProtectionEvent, unknown> {
isLocked(): Promise<boolean>
softLockBiometrics(): void
export interface ProtectionsClientInterface {
createLaunchChallenge(): ChallengeInterface | undefined createLaunchChallenge(): ChallengeInterface | undefined
authorizeProtectedActionForItems<T extends DecryptedItem>(files: T[], challengeReason: ChallengeReason): Promise<T[]> authorizeProtectedActionForItems<T extends DecryptedItem>(files: T[], challengeReason: ChallengeReason): Promise<T[]>
authorizeItemAccess(item: DecryptedItem): Promise<boolean> authorizeItemAccess(item: DecryptedItem): Promise<boolean>

View File

@@ -0,0 +1,6 @@
export enum ProtectionEvent {
UnprotectedSessionBegan = 'Protection:UnprotectedSessionBegan',
UnprotectedSessionExpired = 'Protection:UnprotectedSessionExpired',
BiometricsSoftLockEngaged = 'Protection:BiometricsSoftLockEngaged',
BiometricsSoftLockDisengaged = 'Protection:BiometricsSoftLockDisengaged',
}

View File

@@ -20,6 +20,7 @@ export interface SessionsClientInterface {
getUser(): User | undefined getUser(): User | undefined
isSignedIn(): boolean isSignedIn(): boolean
isSignedOut(): boolean
get userUuid(): string get userUuid(): string
getSureUser(): User getSureUser(): User
isSignedIntoFirstPartyServer(): boolean isSignedIntoFirstPartyServer(): boolean

View File

@@ -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<DecryptedItemInterface | undefined> {
constructor(
private readonly items: ItemManagerInterface,
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
) {}
async execute<M extends DecryptedItemMutator = DecryptedItemMutator>(
itemToLookupUuidFor: DecryptedItemInterface,
mutate: (mutator: M) => void,
updateTimestamps = true,
emitSource?: PayloadEmitSource,
syncOptions?: SyncOptions,
): Promise<Result<DecryptedItemInterface | undefined>> {
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))
}
}

View File

@@ -0,0 +1,10 @@
import { LegacyApiServiceInterface } from './../Api/LegacyApiServiceInterface'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class GetHost implements SyncUseCaseInterface<string> {
constructor(private legacyApi: LegacyApiServiceInterface) {}
execute(): Result<string> {
return Result.ok(this.legacyApi.getHost())
}
}

View File

@@ -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<void> {
constructor(
private http: HttpServiceInterface,
private legacyApi: LegacyApiServiceInterface,
) {}
async execute(host: string): Promise<Result<string>> {
this.http.setHost(host)
await this.legacyApi.setHost(host)
return Result.ok()
}
}

View File

@@ -10,6 +10,10 @@ export * from './Application/ApplicationStage'
export * from './Application/DeinitCallback' export * from './Application/DeinitCallback'
export * from './Application/DeinitMode' export * from './Application/DeinitMode'
export * from './Application/DeinitSource' 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/AsymmetricMessageService'
export * from './AsymmetricMessage/AsymmetricMessageServiceInterface' export * from './AsymmetricMessage/AsymmetricMessageServiceInterface'
export * from './AsymmetricMessage/UseCase/GetInboundMessages' export * from './AsymmetricMessage/UseCase/GetInboundMessages'
@@ -117,12 +121,14 @@ export * from './Item/StaticItemCounter'
export * from './ItemsEncryption/ItemsEncryption' export * from './ItemsEncryption/ItemsEncryption'
export * from './ItemsEncryption/ItemsEncryption' export * from './ItemsEncryption/ItemsEncryption'
export * from './KeySystem/KeySystemKeyManager' export * from './KeySystem/KeySystemKeyManager'
export * from './Mfa/MfaServiceInterface'
export * from './Mutator/ImportDataUseCase' export * from './Mutator/ImportDataUseCase'
export * from './Mutator/MutatorClientInterface' export * from './Mutator/MutatorClientInterface'
export * from './Payloads/PayloadManagerInterface' export * from './Payloads/PayloadManagerInterface'
export * from './Preferences/PreferenceServiceInterface' export * from './Preferences/PreferenceServiceInterface'
export * from './Protection/MobileUnlockTiming' export * from './Protection/MobileUnlockTiming'
export * from './Protection/ProtectionClientInterface' export * from './Protection/ProtectionClientInterface'
export * from './Protection/ProtectionEvent'
export * from './Protection/TimingDisplayOption' export * from './Protection/TimingDisplayOption'
export * from './Revision/RevisionClientInterface' export * from './Revision/RevisionClientInterface'
export * from './Revision/RevisionManager' export * from './Revision/RevisionManager'
@@ -170,20 +176,23 @@ export * from './Sync/SyncOptions'
export * from './Sync/SyncQueueStrategy' export * from './Sync/SyncQueueStrategy'
export * from './Sync/SyncServiceInterface' export * from './Sync/SyncServiceInterface'
export * from './Sync/SyncSource' export * from './Sync/SyncSource'
export * from './UseCase/ChangeAndSaveItem'
export * from './UseCase/DiscardItemsLocally' export * from './UseCase/DiscardItemsLocally'
export * from './UseCase/GetHost'
export * from './UseCase/SetHost'
export * from './User/AccountEvent' export * from './User/AccountEvent'
export * from './User/AccountEventData' export * from './User/AccountEventData'
export * from './User/CredentialsChangeFunctionResponse' export * from './User/CredentialsChangeFunctionResponse'
export * from './User/SignedInOrRegisteredEventPayload' export * from './User/SignedInOrRegisteredEventPayload'
export * from './User/SignedOutEventPayload' export * from './User/SignedOutEventPayload'
export * from './User/UserServiceInterface'
export * from './User/UserServiceInterface'
export * from './User/UserService' export * from './User/UserService'
export * from './User/UserServiceInterface'
export * from './User/UserServiceInterface'
export * from './UserEvent/NotificationService' export * from './UserEvent/NotificationService'
export * from './UserEvent/NotificationServiceEvent' export * from './UserEvent/NotificationServiceEvent'
export * from './Vault/UseCase/ChangeVaultStorageMode'
export * from './Vault/UseCase/ChangeVaultKeyOptions' export * from './Vault/UseCase/ChangeVaultKeyOptions'
export * from './Vault/UseCase/ChangeVaultKeyOptionsDTO' export * from './Vault/UseCase/ChangeVaultKeyOptionsDTO'
export * from './Vault/UseCase/ChangeVaultStorageMode'
export * from './Vault/UseCase/CreateVault' export * from './Vault/UseCase/CreateVault'
export * from './Vault/UseCase/DeleteVault' export * from './Vault/UseCase/DeleteVault'
export * from './Vault/UseCase/GetVault' export * from './Vault/UseCase/GetVault'

View File

@@ -1,4 +1,4 @@
import { SNMfaService } from './../Services/Mfa/MfaService' import { MfaService } from './../Services/Mfa/MfaService'
import { KeyRecoveryService } from './../Services/KeyRecovery/KeyRecoveryService' import { KeyRecoveryService } from './../Services/KeyRecovery/KeyRecoveryService'
import { WebSocketsService } from './../Services/Api/WebsocketsService' import { WebSocketsService } from './../Services/Api/WebsocketsService'
import { MigrationService } from './../Services/Migration/MigrationService' import { MigrationService } from './../Services/Migration/MigrationService'
@@ -20,7 +20,6 @@ import {
ApplicationStageChangedEventPayload, ApplicationStageChangedEventPayload,
StorageValueModes, StorageValueModes,
ChallengeObserver, ChallengeObserver,
SyncOptions,
ImportDataReturnType, ImportDataReturnType,
ImportDataUseCase, ImportDataUseCase,
StoragePersistencePolicies, StoragePersistencePolicies,
@@ -57,7 +56,6 @@ import {
ApplicationInterface, ApplicationInterface,
EncryptionService, EncryptionService,
EncryptionServiceEvent, EncryptionServiceEvent,
ChallengePrompt,
Challenge, Challenge,
ErrorAlertStrings, ErrorAlertStrings,
SessionsClientInterface, SessionsClientInterface,
@@ -75,32 +73,33 @@ import {
VaultInviteServiceInterface, VaultInviteServiceInterface,
NotificationServiceEvent, NotificationServiceEvent,
VaultLockServiceInterface, VaultLockServiceInterface,
ApplicationConstructorOptions,
FullyResolvedApplicationOptions,
ApplicationOptionsDefaults,
ChangeAndSaveItem,
ProtectionEvent,
GetHost,
SetHost,
MfaServiceInterface,
} from '@standardnotes/services' } from '@standardnotes/services'
import { import {
PayloadEmitSource,
SNNote, SNNote,
PrefKey, PrefKey,
PrefValue, PrefValue,
DecryptedItemMutator,
BackupFile, BackupFile,
DecryptedItemInterface,
EncryptedItemInterface, EncryptedItemInterface,
Environment, Environment,
ItemStream,
Platform, Platform,
MutationType,
} from '@standardnotes/models' } from '@standardnotes/models'
import { import {
HttpResponse, HttpResponse,
SessionListResponse, SessionListResponse,
User,
SignInResponse, SignInResponse,
ClientDisplayableError, ClientDisplayableError,
SessionListEntry, SessionListEntry,
} from '@standardnotes/responses' } from '@standardnotes/responses'
import { import {
SyncService, SyncService,
ProtectionEvent,
SettingsService, SettingsService,
ActionsService, ActionsService,
ChallengeResponse, ChallengeResponse,
@@ -116,14 +115,13 @@ import {
UuidGenerator, UuidGenerator,
useBoolean, useBoolean,
LoggerInterface, LoggerInterface,
canBlockDeinit,
} from '@standardnotes/utils' } from '@standardnotes/utils'
import { UuidString, ApplicationEventPayload } from '../Types' import { UuidString, ApplicationEventPayload } from '../Types'
import { applicationEventForSyncEvent } from '@Lib/Application/Event' import { applicationEventForSyncEvent } from '@Lib/Application/Event'
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files' import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
import { ComputePrivateUsername } from '@standardnotes/encryption' import { ComputePrivateUsername } from '@standardnotes/encryption'
import { SNLog } from '../Log' import { SNLog } from '../Log'
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
import { ApplicationOptionsDefaults } from './Options/Defaults'
import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes' import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { UseCaseContainerInterface } from '@Lib/Domain/UseCase/UseCaseContainerInterface' import { UseCaseContainerInterface } from '@Lib/Domain/UseCase/UseCaseContainerInterface'
import { GetRecoveryCodes } from '@Lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes' 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 { GetAuthenticatorAuthenticationOptions } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions'
import { Dependencies } from './Dependencies/Dependencies' import { Dependencies } from './Dependencies/Dependencies'
import { TYPES } from './Dependencies/Types' import { TYPES } from './Dependencies/Types'
import { canBlockDeinit } from './Dependencies/isDeinitable'
/** How often to automatically sync, in milliseconds */ /** How often to automatically sync, in milliseconds */
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000 const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
@@ -165,7 +162,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private eventHandlers: ApplicationObserver[] = [] private eventHandlers: ApplicationObserver[] = []
private streamRemovers: ObserverRemover[] = []
private serviceObservers: ObserverRemover[] = [] private serviceObservers: ObserverRemover[] = []
private managedSubscribers: ObserverRemover[] = [] private managedSubscribers: ObserverRemover[] = []
private autoSyncInterval!: ReturnType<typeof setInterval> private autoSyncInterval!: ReturnType<typeof setInterval>
@@ -178,7 +174,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private launched = false private launched = false
/** Whether the application has been destroyed via .deinit() */ /** Whether the application has been destroyed via .deinit() */
public dealloced = false public dealloced = false
private isBiometricsSoftLockEngaged = false
private revokingSession = false private revokingSession = false
private handledFullSyncStage = false private handledFullSyncStage = false
@@ -561,13 +557,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
void this.migrations.handleApplicationEvent(event) void this.migrations.handleApplicationEvent(event)
} }
/**
* Whether the local database has completed loading local items.
*/
public isDatabaseLoaded(): boolean {
return this.sync.isDatabaseLoaded()
}
public getSessions(): Promise<HttpResponse<SessionListEntry[]>> { public getSessions(): Promise<HttpResponse<SessionListEntry[]>> {
return this.sessions.getSessionsList() return this.sessions.getSessionsList()
} }
@@ -594,65 +583,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return compareVersions(userVersion, ProtocolVersion.V004) >= 0 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<I extends DecryptedItemInterface = DecryptedItemInterface>(
contentType: string | string[],
stream: ItemStream<I>,
): () => void {
const removeItemManagerObserver = this.items.addObserver<I>(
contentType,
({ changed, inserted, removed, source }) => {
stream({ changed, inserted, removed, source })
},
)
const matches = this.items.getItems<I>(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<void> {
this.http.setHost(host)
await this.legacyApi.setHost(host)
}
public getHost(): string {
return this.legacyApi.getHost()
}
public async setCustomHost(host: string): Promise<void> { public async setCustomHost(host: string): Promise<void> {
await this.setHost(host) await this.setHost.execute(host)
this.sockets.setWebSocketUrl(undefined) 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 { public getUserPasswordCreationDate(): Date | undefined {
return this.encryption.getPasswordCreatedDate() return this.encryption.getPasswordCreatedDate()
} }
@@ -699,10 +635,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return result return result
} }
public noAccount(): boolean {
return !this.hasAccount()
}
public hasAccount(): boolean { public hasAccount(): boolean {
return this.encryption.hasAccount() return this.encryption.hasAccount()
} }
@@ -715,10 +647,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.protections.hasProtectionSources() return this.protections.hasProtectionSources()
} }
public hasUnprotectedAccessSession(): boolean {
return this.protections.hasUnprotectedAccessSession()
}
/** /**
* When a user specifies a non-zero remember duration on a protection * When a user specifies a non-zero remember duration on a protection
* challenge, a session will be started during which protections are disabled. * 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() return this.protections.authorizeAutolockIntervalChange()
} }
public authorizeSearchingProtectedNotesText(): Promise<boolean> {
return this.protections.authorizeSearchingProtectedNotesText()
}
public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise<BackupFile | undefined> { public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise<BackupFile | undefined> {
return this.encryption.createEncryptedBackupFile() return this.encryption.createEncryptedBackupFile()
} }
@@ -852,7 +776,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.serviceObservers.length = 0 this.serviceObservers.length = 0
this.managedSubscribers.length = 0 this.managedSubscribers.length = 0
this.streamRemovers.length = 0
this.started = false this.started = false
@@ -921,39 +844,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}) })
} }
public async changeAndSaveItem<M extends DecryptedItemMutator = DecryptedItemMutator>(
itemToLookupUuidFor: DecryptedItemInterface,
mutate: (mutator: M) => void,
updateTimestamps = true,
emitSource?: PayloadEmitSource,
syncOptions?: SyncOptions,
): Promise<DecryptedItemInterface | undefined> {
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<M extends DecryptedItemMutator = DecryptedItemMutator>(
itemsToLookupUuidsFor: DecryptedItemInterface[],
mutate: (mutator: M) => void,
updateTimestamps = true,
emitSource?: PayloadEmitSource,
syncOptions?: SyncOptions,
): Promise<void> {
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<ImportDataReturnType> { public async importData(data: BackupFile, awaitSync = false): Promise<ImportDataReturnType> {
const usecase = this.dependencies.get<ImportDataUseCase>(TYPES.ImportDataUseCase) const usecase = this.dependencies.get<ImportDataUseCase>(TYPES.ImportDataUseCase)
return usecase.execute(data, awaitSync) return usecase.execute(data, awaitSync)
@@ -991,42 +881,18 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.encryption.hasPasscode() return this.encryption.hasPasscode()
} }
async isLocked(): Promise<boolean> {
if (!this.started) {
return Promise.resolve(true)
}
const isPasscodeLocked = await this.challenges.isPasscodeLocked()
return isPasscodeLocked || this.isBiometricsSoftLockEngaged
}
public async lock(): Promise<void> { public async lock(): Promise<void> {
/** 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 const MaximumWaitTime = 500
await this.prepareForDeinit(MaximumWaitTime) await this.prepareForDeinit(MaximumWaitTime)
return this.deinit(this.getDeinitMode(), DeinitSource.Lock) 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() { isNativeMobileWeb() {
return this.environment === Environment.Mobile return this.environment === Environment.Mobile
} }
@@ -1102,10 +968,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
} }
} }
public getNewSubscriptionToken(): Promise<string | undefined> {
return this.legacyApi.getNewSubscriptionToken()
}
public isThirdPartyHostUsed(): boolean { public isThirdPartyHostUsed(): boolean {
return this.legacyApi.isThirdPartyHostUsed() return this.legacyApi.isThirdPartyHostUsed()
} }
@@ -1117,7 +979,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return false return false
} }
return this.getHost() === (await homeServerService.getHomeServerUrl()) return this.getHost.execute().getValue() === (await homeServerService.getHomeServerUrl())
} }
private createBackgroundDependencies() { private createBackgroundDependencies() {
@@ -1361,14 +1223,30 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<SharedVaultServiceInterface>(TYPES.SharedVaultService) return this.dependencies.get<SharedVaultServiceInterface>(TYPES.SharedVaultService)
} }
private get migrations(): MigrationService { public get changeAndSaveItem(): ChangeAndSaveItem {
return this.dependencies.get<MigrationService>(TYPES.MigrationService) return this.dependencies.get<ChangeAndSaveItem>(TYPES.ChangeAndSaveItem)
} }
private get legacyApi(): LegacyApiService { public get getHost(): GetHost {
return this.dependencies.get<GetHost>(TYPES.GetHost)
}
public get setHost(): SetHost {
return this.dependencies.get<SetHost>(TYPES.SetHost)
}
public get legacyApi(): LegacyApiService {
return this.dependencies.get<LegacyApiService>(TYPES.LegacyApiService) return this.dependencies.get<LegacyApiService>(TYPES.LegacyApiService)
} }
public get mfa(): MfaServiceInterface {
return this.dependencies.get<MfaService>(TYPES.MfaService)
}
private get migrations(): MigrationService {
return this.dependencies.get<MigrationService>(TYPES.MigrationService)
}
private get http(): HttpServiceInterface { private get http(): HttpServiceInterface {
return this.dependencies.get<HttpServiceInterface>(TYPES.HttpService) return this.dependencies.get<HttpServiceInterface>(TYPES.HttpService)
} }
@@ -1376,8 +1254,4 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private get sockets(): WebSocketsService { private get sockets(): WebSocketsService {
return this.dependencies.get<WebSocketsService>(TYPES.WebSocketsService) return this.dependencies.get<WebSocketsService>(TYPES.WebSocketsService)
} }
private get mfa(): SNMfaService {
return this.dependencies.get<SNMfaService>(TYPES.MfaService)
}
} }

View File

@@ -11,7 +11,7 @@ import { GetRecoveryCodes } from '../../Domain/UseCase/GetRecoveryCodes/GetRecov
import { SignInWithRecoveryCodes } from '../../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes' import { SignInWithRecoveryCodes } from '../../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { ListedService } from '../../Services/Listed/ListedService' import { ListedService } from '../../Services/Listed/ListedService'
import { MigrationService } from '../../Services/Migration/MigrationService' 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 { SNComponentManager } from '../../Services/ComponentManager/ComponentManager'
import { FeaturesService } from '@Lib/Services/Features/FeaturesService' import { FeaturesService } from '@Lib/Services/Features/FeaturesService'
import { SettingsService } from '../../Services/Settings/SNSettingsService' import { SettingsService } from '../../Services/Settings/SNSettingsService'
@@ -126,6 +126,10 @@ import {
AlertService, AlertService,
DesktopDeviceInterface, DesktopDeviceInterface,
ChangeVaultStorageMode, ChangeVaultStorageMode,
ChangeAndSaveItem,
FullyResolvedApplicationOptions,
GetHost,
SetHost,
} from '@standardnotes/services' } from '@standardnotes/services'
import { ItemManager } from '../../Services/Items/ItemManager' import { ItemManager } from '../../Services/Items/ItemManager'
import { PayloadManager } from '../../Services/Payloads/PayloadManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager'
@@ -151,10 +155,8 @@ import {
WebSocketApiService, WebSocketApiService,
WebSocketServer, WebSocketServer,
} from '@standardnotes/api' } from '@standardnotes/api'
import { FullyResolvedApplicationOptions } from '../Options/ApplicationOptions'
import { TYPES } from './Types' import { TYPES } from './Types'
import { isDeinitable } from './isDeinitable' import { Logger, isNotUndefined, isDeinitable } from '@standardnotes/utils'
import { Logger, isNotUndefined } from '@standardnotes/utils'
import { EncryptionOperators } from '@standardnotes/encryption' import { EncryptionOperators } from '@standardnotes/encryption'
import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models' import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
@@ -219,6 +221,14 @@ export class Dependencies {
) )
}) })
this.factory.set(TYPES.GetHost, () => {
return new GetHost(this.get<LegacyApiService>(TYPES.LegacyApiService))
})
this.factory.set(TYPES.SetHost, () => {
return new SetHost(this.get<HttpService>(TYPES.HttpService), this.get<LegacyApiService>(TYPES.LegacyApiService))
})
this.factory.set(TYPES.GetKeyPairs, () => { this.factory.set(TYPES.GetKeyPairs, () => {
return new GetKeyPairs(this.get<RootKeyManager>(TYPES.RootKeyManager)) return new GetKeyPairs(this.get<RootKeyManager>(TYPES.RootKeyManager))
}) })
@@ -307,6 +317,14 @@ export class Dependencies {
return new GetVaults(this.get<ItemManager>(TYPES.ItemManager)) return new GetVaults(this.get<ItemManager>(TYPES.ItemManager))
}) })
this.factory.set(TYPES.ChangeAndSaveItem, () => {
return new ChangeAndSaveItem(
this.get<ItemManager>(TYPES.ItemManager),
this.get<MutatorService>(TYPES.MutatorService),
this.get<SyncService>(TYPES.SyncService),
)
})
this.factory.set(TYPES.GetSharedVaults, () => { this.factory.set(TYPES.GetSharedVaults, () => {
return new GetSharedVaults(this.get<GetVaults>(TYPES.GetVaults)) return new GetSharedVaults(this.get<GetVaults>(TYPES.GetVaults))
}) })
@@ -1080,7 +1098,7 @@ export class Dependencies {
}) })
this.factory.set(TYPES.MfaService, () => { this.factory.set(TYPES.MfaService, () => {
return new SNMfaService( return new MfaService(
this.get<SettingsService>(TYPES.SettingsService), this.get<SettingsService>(TYPES.SettingsService),
this.get<PureCryptoInterface>(TYPES.Crypto), this.get<PureCryptoInterface>(TYPES.Crypto),
this.get<FeaturesService>(TYPES.FeaturesService), this.get<FeaturesService>(TYPES.FeaturesService),

View File

@@ -156,6 +156,9 @@ export const TYPES = {
DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'), DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'),
GetKeyPairs: Symbol.for('GetKeyPairs'), GetKeyPairs: Symbol.for('GetKeyPairs'),
ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'), ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'),
ChangeAndSaveItem: Symbol.for('ChangeAndSaveItem'),
GetHost: Symbol.for('GetHost'),
SetHost: Symbol.for('SetHost'),
// Mappers // Mappers
SessionStorageMapper: Symbol.for('SessionStorageMapper'), SessionStorageMapper: Symbol.for('SessionStorageMapper'),

View File

@@ -1,17 +1,17 @@
import { DecryptedItemInterface } from '@standardnotes/models' import { DecryptedItemInterface } from '@standardnotes/models'
import { ApplicationInterface } from '@standardnotes/services' import { ItemManagerInterface } from '@standardnotes/services'
/** Keeps an item reference up to date with changes */ /** Keeps an item reference up to date with changes */
export class LiveItem<T extends DecryptedItemInterface> { export class LiveItem<T extends DecryptedItemInterface> {
public item: T public item: T
private removeObserver: () => void private removeObserver: () => void
constructor(uuid: string, application: ApplicationInterface, onChange?: (item: T) => void) { constructor(uuid: string, items: ItemManagerInterface, onChange?: (item: T) => void) {
this.item = application.items.findSureItem(uuid) this.item = items.findSureItem(uuid)
onChange && onChange(this.item) 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) => { const matchingItem = [...changed, ...inserted].find((item) => {
return item.uuid === uuid return item.uuid === uuid
}) })

View File

@@ -2,4 +2,3 @@ export * from './Application'
export * from './Event' export * from './Event'
export * from './LiveItem' export * from './LiveItem'
export * from './Platforms' export * from './Platforms'
export * from './Options/Defaults'

View File

@@ -1,7 +1,7 @@
import { RootKeyInterface } from '@standardnotes/models' import { RootKeyInterface } from '@standardnotes/models'
import { DiskStorageService } from '../Storage/DiskStorageService' import { DiskStorageService } from '../Storage/DiskStorageService'
import { removeFromArray } from '@standardnotes/utils' import { removeFromArray } from '@standardnotes/utils'
import { isValidProtectionSessionLength } from '../Protection/ProtectionService' import { isValidProtectionSessionLength } from '../Protection/isValidProtectionSessionLength'
import { import {
AbstractService, AbstractService,
ChallengeServiceInterface, ChallengeServiceInterface,
@@ -158,10 +158,6 @@ export class ChallengeService extends AbstractService implements ChallengeServic
return { wrappingKey } return { wrappingKey }
} }
public isPasscodeLocked(): Promise<boolean> {
return this.encryptionService.isPasscodeLocked()
}
public addChallengeObserver(challenge: Challenge, observer: ChallengeObserver): () => void { public addChallengeObserver(challenge: Challenge, observer: ChallengeObserver): () => void {
const observers = this.challengeObservers[challenge.id] || [] const observers = this.challengeObservers[challenge.id] || []

View File

@@ -58,7 +58,7 @@ describe('GetFeatureUrl', () => {
}) })
describe('desktop', () => { describe('desktop', () => {
let desktopManager: DesktopManagerInterface | undefined let desktopManager: jest.Mocked<DesktopManagerInterface | undefined>
beforeEach(() => { beforeEach(() => {
desktopManager = { desktopManager = {
@@ -69,7 +69,7 @@ describe('GetFeatureUrl', () => {
getExtServerHost() { getExtServerHost() {
return desktopExtHost return desktopExtHost
}, },
} } as unknown as jest.Mocked<DesktopManagerInterface | undefined>
usecase = new GetFeatureUrl(desktopManager, Environment.Desktop, Platform.MacDesktop) usecase = new GetFeatureUrl(desktopManager, Environment.Desktop, Platform.MacDesktop)
}) })

View File

@@ -28,6 +28,7 @@ export class ItemManager extends Services.AbstractService implements Services.It
private collection!: Models.ItemCollection private collection!: Models.ItemCollection
private systemSmartViews: Models.SmartView[] private systemSmartViews: Models.SmartView[]
private itemCounter!: Models.ItemCounter private itemCounter!: Models.ItemCounter
private streamDisposers: (() => void)[] = []
private navigationDisplayController!: Models.ItemDisplayController< private navigationDisplayController!: Models.ItemDisplayController<
Models.SNNote | Models.FileItem, Models.SNNote | Models.FileItem,
@@ -230,6 +231,7 @@ export class ItemManager extends Services.AbstractService implements Services.It
public override deinit(): void { public override deinit(): void {
this.unsubChangeObserver() this.unsubChangeObserver()
this.streamDisposers.length = 0
;(this.unsubChangeObserver as unknown) = undefined ;(this.unsubChangeObserver as unknown) = undefined
;(this.payloadManager as unknown) = undefined ;(this.payloadManager as unknown) = undefined
;(this.collection 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[] { getNoteLinkedFiles(note: Models.SNNote): Models.FileItem[] {
return this.itemsReferencingItem(note).filter(Models.isFile) 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<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface>(
contentType: string | string[],
stream: Models.ItemStream<I>,
): () => void {
const removeItemManagerObserver = this.addObserver<I>(contentType, ({ changed, inserted, removed, source }) => {
stream({ changed, inserted, removed, source })
})
const matches = this.getItems<I>(contentType)
stream({
inserted: matches,
changed: [],
removed: [],
source: Models.PayloadEmitSource.InitialObserverRegistrationPush,
})
this.streamDisposers.push(removeItemManagerObserver)
return () => {
removeItemManagerObserver()
removeFromArray(this.streamDisposers, removeItemManagerObserver)
}
}
} }

View File

@@ -3,9 +3,9 @@ import { SettingName } from '@standardnotes/settings'
import { SettingsService } from '../Settings' import { SettingsService } from '../Settings'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { FeaturesService } from '../Features/FeaturesService' 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( constructor(
private settingsService: SettingsService, private settingsService: SettingsService,
private crypto: PureCryptoInterface, private crypto: PureCryptoInterface,

View File

@@ -0,0 +1 @@
export const ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction = 30

View File

@@ -29,45 +29,11 @@ import {
InternalEventInterface, InternalEventInterface,
ApplicationEvent, ApplicationEvent,
ApplicationStageChangedEventPayload, ApplicationStageChangedEventPayload,
ProtectionEvent,
} from '@standardnotes/services' } from '@standardnotes/services'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { isValidProtectionSessionLength } from './isValidProtectionSessionLength'
export enum ProtectionEvent { import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
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',
},
]
/** /**
* Enforces certain actions to require extra authentication, * Enforces certain actions to require extra authentication,
@@ -82,11 +48,14 @@ export class ProtectionService
private mobilePasscodeTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.OnQuit private mobilePasscodeTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.OnQuit
private mobileBiometricsTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.OnQuit private mobileBiometricsTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.OnQuit
private isBiometricsSoftLockEngaged = false
private applicationStarted = false
constructor( constructor(
private encryptionService: EncryptionService, private encryption: EncryptionService,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private challengeService: ChallengeService, private challenges: ChallengeService,
private storageService: DiskStorageService, private storage: DiskStorageService,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -94,9 +63,9 @@ export class ProtectionService
public override deinit(): void { public override deinit(): void {
clearTimeout(this.sessionExpiryTimeout) clearTimeout(this.sessionExpiryTimeout)
;(this.encryptionService as unknown) = undefined ;(this.encryption as unknown) = undefined
;(this.challengeService as unknown) = undefined ;(this.challenges as unknown) = undefined
;(this.storageService as unknown) = undefined ;(this.storage as unknown) = undefined
super.deinit() super.deinit()
} }
@@ -108,11 +77,42 @@ export class ProtectionService
this.mobilePasscodeTiming = this.getMobilePasscodeTiming() this.mobilePasscodeTiming = this.getMobilePasscodeTiming()
this.mobileBiometricsTiming = this.getMobileBiometricsTiming() this.mobileBiometricsTiming = this.getMobileBiometricsTiming()
} }
} else if (event.type === ApplicationEvent.Started) {
this.applicationStarted = true
} }
} }
async isLocked(): Promise<boolean> {
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 { public hasProtectionSources(): boolean {
return this.encryptionService.hasAccount() || this.encryptionService.hasPasscode() || this.hasBiometricsEnabled() return this.encryption.hasAccount() || this.encryption.hasPasscode() || this.hasBiometricsEnabled()
} }
public hasUnprotectedAccessSession(): boolean { public hasUnprotectedAccessSession(): boolean {
@@ -123,7 +123,7 @@ export class ProtectionService
} }
public hasBiometricsEnabled(): boolean { public hasBiometricsEnabled(): boolean {
const biometricsState = this.storageService.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped) const biometricsState = this.storage.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped)
return Boolean(biometricsState) return Boolean(biometricsState)
} }
@@ -133,7 +133,7 @@ export class ProtectionService
return false return false
} }
this.storageService.setValue(StorageKey.BiometricsState, true, StorageValueModes.Nonwrapped) this.storage.setValue(StorageKey.BiometricsState, true, StorageValueModes.Nonwrapped)
return true return true
} }
@@ -145,7 +145,7 @@ export class ProtectionService
} }
if (await this.validateOrRenewSession(ChallengeReason.DisableBiometrics)) { if (await this.validateOrRenewSession(ChallengeReason.DisableBiometrics)) {
this.storageService.setValue(StorageKey.BiometricsState, false, StorageValueModes.Nonwrapped) this.storage.setValue(StorageKey.BiometricsState, false, StorageValueModes.Nonwrapped)
return true return true
} else { } else {
return false return false
@@ -157,7 +157,7 @@ export class ProtectionService
if (this.hasBiometricsEnabled()) { if (this.hasBiometricsEnabled()) {
prompts.push(new ChallengePrompt(ChallengeValidation.Biometric)) prompts.push(new ChallengePrompt(ChallengeValidation.Biometric))
} }
if (this.encryptionService.hasPasscode()) { if (this.encryption.hasPasscode()) {
prompts.push(new ChallengePrompt(ChallengeValidation.LocalPasscode)) prompts.push(new ChallengePrompt(ChallengeValidation.LocalPasscode))
} }
if (prompts.length > 0) { if (prompts.length > 0) {
@@ -316,7 +316,7 @@ export class ProtectionService
} }
getMobileBiometricsTiming(): MobileUnlockTiming | undefined { getMobileBiometricsTiming(): MobileUnlockTiming | undefined {
return this.storageService.getValue<MobileUnlockTiming | undefined>( return this.storage.getValue<MobileUnlockTiming | undefined>(
StorageKey.MobileBiometricsTiming, StorageKey.MobileBiometricsTiming,
StorageValueModes.Nonwrapped, StorageValueModes.Nonwrapped,
MobileUnlockTiming.OnQuit, MobileUnlockTiming.OnQuit,
@@ -324,7 +324,7 @@ export class ProtectionService
} }
getMobilePasscodeTiming(): MobileUnlockTiming | undefined { getMobilePasscodeTiming(): MobileUnlockTiming | undefined {
return this.storageService.getValue<MobileUnlockTiming | undefined>( return this.storage.getValue<MobileUnlockTiming | undefined>(
StorageKey.MobilePasscodeTiming, StorageKey.MobilePasscodeTiming,
StorageValueModes.Nonwrapped, StorageValueModes.Nonwrapped,
MobileUnlockTiming.OnQuit, MobileUnlockTiming.OnQuit,
@@ -332,21 +332,21 @@ export class ProtectionService
} }
setMobileBiometricsTiming(timing: MobileUnlockTiming): void { setMobileBiometricsTiming(timing: MobileUnlockTiming): void {
this.storageService.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped) this.storage.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped)
this.mobileBiometricsTiming = timing this.mobileBiometricsTiming = timing
} }
setMobilePasscodeTiming(timing: MobileUnlockTiming): void { setMobilePasscodeTiming(timing: MobileUnlockTiming): void {
this.storageService.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped) this.storage.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped)
this.mobilePasscodeTiming = timing this.mobilePasscodeTiming = timing
} }
setMobileScreenshotPrivacyEnabled(isEnabled: boolean) { setMobileScreenshotPrivacyEnabled(isEnabled: boolean) {
return this.storageService.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default) return this.storage.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default)
} }
getMobileScreenshotPrivacyEnabled(): boolean { getMobileScreenshotPrivacyEnabled(): boolean {
return this.storageService.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default, false) return this.storage.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default, false)
} }
private async validateOrRenewSession( private async validateOrRenewSession(
@@ -363,19 +363,19 @@ export class ProtectionService
prompts.push(new ChallengePrompt(ChallengeValidation.Biometric)) prompts.push(new ChallengePrompt(ChallengeValidation.Biometric))
} }
if (this.encryptionService.hasPasscode()) { if (this.encryption.hasPasscode()) {
prompts.push(new ChallengePrompt(ChallengeValidation.LocalPasscode)) prompts.push(new ChallengePrompt(ChallengeValidation.LocalPasscode))
} }
if (requireAccountPassword) { if (requireAccountPassword) {
if (!this.encryptionService.hasAccount()) { if (!this.encryption.hasAccount()) {
throw Error('Requiring account password for challenge with no account') throw Error('Requiring account password for challenge with no account')
} }
prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword)) prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword))
} }
if (prompts.length === 0) { if (prompts.length === 0) {
if (fallBackToAccountPassword && this.encryptionService.hasAccount()) { if (fallBackToAccountPassword && this.encryption.hasAccount()) {
prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword)) prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword))
} else { } else {
return true 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) { if (response) {
const length = response.values.find( const length = response.values.find(
@@ -414,7 +414,7 @@ export class ProtectionService
} }
public getSessionExpiryDate(): Date { public getSessionExpiryDate(): Date {
const expiresAt = this.storageService.getValue<number>(StorageKey.ProtectionExpirey) const expiresAt = this.storage.getValue<number>(StorageKey.ProtectionExpirey)
if (expiresAt) { if (expiresAt) {
return new Date(expiresAt) return new Date(expiresAt)
} else { } else {
@@ -428,15 +428,15 @@ export class ProtectionService
} }
private setSessionExpiryDate(date: Date) { private setSessionExpiryDate(date: Date) {
this.storageService.setValue(StorageKey.ProtectionExpirey, date) this.storage.setValue(StorageKey.ProtectionExpirey, date)
} }
private getLastSessionLength(): UnprotectedAccessSecondsDuration | undefined { private getLastSessionLength(): UnprotectedAccessSecondsDuration | undefined {
return this.storageService.getValue(StorageKey.ProtectionSessionLength) return this.storage.getValue(StorageKey.ProtectionSessionLength)
} }
private setSessionLength(length: UnprotectedAccessSecondsDuration): void { private setSessionLength(length: UnprotectedAccessSecondsDuration): void {
this.storageService.setValue(StorageKey.ProtectionSessionLength, length) this.storage.setValue(StorageKey.ProtectionSessionLength, length)
const expiresAt = new Date() const expiresAt = new Date()
expiresAt.setSeconds(expiresAt.getSeconds() + length) expiresAt.setSeconds(expiresAt.getSeconds() + length)
this.setSessionExpiryDate(expiresAt) this.setSessionExpiryDate(expiresAt)

View File

@@ -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',
},
]

View File

@@ -0,0 +1,6 @@
export enum UnprotectedAccessSecondsDuration {
OneMinute = 60,
FiveMinutes = 300,
OneHour = 3600,
OneWeek = 604800,
}

View File

@@ -1 +1,5 @@
export * from './ProtectionService' export * from './ProtectionService'
export * from './ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction'
export * from './ProtectionSessionDurations'
export * from './UnprotectedAccessSecondsDuration'
export * from './isValidProtectionSessionLength'

View File

@@ -0,0 +1,5 @@
import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
export function isValidProtectionSessionLength(number: unknown): boolean {
return typeof number === 'number' && Object.values(UnprotectedAccessSecondsDuration).includes(number)
}

View File

@@ -264,10 +264,15 @@ export class SessionManager
} }
} }
/** Unlike EncryptionService.hasAccount, isSignedIn can only be read once the application is unlocked */
public isSignedIn(): boolean { public isSignedIn(): boolean {
return this.getUser() != undefined return this.getUser() != undefined
} }
public isSignedOut(): boolean {
return !this.isSignedIn()
}
public isSignedIntoFirstPartyServer(): boolean { public isSignedIntoFirstPartyServer(): boolean {
return this.isSignedIn() && !this.apiService.isThirdPartyHostUsed() return this.isSignedIn() && !this.apiService.isThirdPartyHostUsed()
} }

View File

@@ -83,6 +83,7 @@ import {
SyncEventReceivedNotificationsData, SyncEventReceivedNotificationsData,
SyncEventReceivedAsymmetricMessagesData, SyncEventReceivedAsymmetricMessagesData,
SyncOpStatus, SyncOpStatus,
ApplicationSyncOptions,
} from '@standardnotes/services' } from '@standardnotes/services'
import { OfflineSyncResponse } from './Offline/Response' import { OfflineSyncResponse } from './Offline/Response'
import { import {
@@ -92,7 +93,6 @@ import {
SplitPayloadsByEncryptionType, SplitPayloadsByEncryptionType,
} from '@standardnotes/encryption' } from '@standardnotes/encryption'
import { CreatePayloadFromRawServerItem } from './Account/Utilities' import { CreatePayloadFromRawServerItem } from './Account/Utilities'
import { ApplicationSyncOptions } from '@Lib/Application/Options/OptionalOptions'
import { DecryptedServerConflictMap, TrustedServerConflictMap } from './Account/ServerConflictMap' import { DecryptedServerConflictMap, TrustedServerConflictMap } from './Account/ServerConflictMap'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'

View File

@@ -79,8 +79,8 @@ describe('application instances', () => {
}) })
await recreatedContext.launch() await recreatedContext.launch()
expect(recreatedContext.application.getHost()).to.not.equal('http://nonsense.host') expect(recreatedContext.application.getHost.execute().getValue()).to.not.equal('http://nonsense.host')
expect(recreatedContext.application.getHost()).to.equal(Factory.getDefaultHost()) expect(recreatedContext.application.getHost.execute().getValue()).to.equal(Factory.getDefaultHost())
await recreatedContext.deinit() await recreatedContext.deinit()
}) })

View File

@@ -85,7 +85,7 @@ describe('auth fringe cases', () => {
const serverText = 'server text' const serverText = 'server text'
await context.application.changeAndSaveItem(firstVersionOfNote, (mutator) => { await context.application.changeAndSaveItem.execute(firstVersionOfNote, (mutator) => {
mutator.text = serverText mutator.text = serverText
}) })

View File

@@ -141,7 +141,7 @@ describe('features', () => {
expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false) expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false)
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('') const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
const promise = new Promise((resolve) => { const promise = new Promise((resolve) => {
application.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => { application.items.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => {
for (const item of changed) { for (const item of changed) {
if (item.content.migratedToUserSetting) { if (item.content.migratedToUserSetting) {
resolve() resolve()

View File

@@ -34,8 +34,8 @@ describe('history manager', () => {
await Factory.safeDeinit(this.application) await Factory.safeDeinit(this.application)
}) })
function setTextAndSync(application, item, text) { async function setTextAndSync(application, item, text) {
return application.changeAndSaveItem( const result = await application.changeAndSaveItem.execute(
item, item,
(mutator) => { (mutator) => {
mutator.text = text mutator.text = text
@@ -44,6 +44,8 @@ describe('history manager', () => {
undefined, undefined,
syncOptions, syncOptions,
) )
return result.getValue()
} }
function deleteCharsFromString(string, amount) { function deleteCharsFromString(string, amount) {
@@ -59,7 +61,7 @@ describe('history manager', () => {
expect(this.history.sessionHistoryForItem(item).length).to.equal(0) expect(this.history.sessionHistoryForItem(item).length).to.equal(0)
/** Sync with different contents, should create new entry */ /** Sync with different contents, should create new entry */
await this.application.changeAndSaveItem( await this.application.changeAndSaveItem.execute(
item, item,
(mutator) => { (mutator) => {
mutator.title = Math.random() mutator.title = Math.random()
@@ -79,7 +81,7 @@ describe('history manager', () => {
const context = await Factory.createAppContext({ identifier }) const context = await Factory.createAppContext({ identifier })
await context.launch() await context.launch()
expect(context.history.sessionHistoryForItem(item).length).to.equal(0) expect(context.history.sessionHistoryForItem(item).length).to.equal(0)
await context.application.changeAndSaveItem( await context.application.changeAndSaveItem.execute(
item, item,
(mutator) => { (mutator) => {
mutator.title = Math.random() mutator.title = Math.random()
@@ -103,7 +105,7 @@ describe('history manager', () => {
await context.application.mutator.insertItem(item) await context.application.mutator.insertItem(item)
expect(context.history.sessionHistoryForItem(item).length).to.equal(0) expect(context.history.sessionHistoryForItem(item).length).to.equal(0)
await context.application.changeAndSaveItem( await context.application.changeAndSaveItem.execute(
item, item,
(mutator) => { (mutator) => {
mutator.title = Math.random() mutator.title = Math.random()
@@ -243,7 +245,7 @@ describe('history manager', () => {
const payload = Factory.createNotePayload() const payload = Factory.createNotePayload()
await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const item = this.application.items.findItem(payload.uuid) const item = this.application.items.findItem(payload.uuid)
await this.application.changeAndSaveItem( await this.application.changeAndSaveItem.execute(
item, item,
(mutator) => { (mutator) => {
mutator.title = Math.random() mutator.title = Math.random()
@@ -306,7 +308,7 @@ describe('history manager', () => {
expect(itemHistory.length).to.equal(1) expect(itemHistory.length).to.equal(1)
/** Sync with different contents, should not create a new entry */ /** Sync with different contents, should not create a new entry */
await this.application.changeAndSaveItem( await this.application.changeAndSaveItem.execute(
item, item,
(mutator) => { (mutator) => {
mutator.title = Math.random() mutator.title = Math.random()
@@ -327,7 +329,7 @@ describe('history manager', () => {
await Factory.sleep(Factory.ServerRevisionFrequency) await Factory.sleep(Factory.ServerRevisionFrequency)
/** Sync with different contents, should create new entry */ /** Sync with different contents, should create new entry */
const newTitleAfterFirstChange = `The title should be: ${Math.random()}` const newTitleAfterFirstChange = `The title should be: ${Math.random()}`
await this.application.changeAndSaveItem( await this.application.changeAndSaveItem.execute(
item, item,
(mutator) => { (mutator) => {
mutator.title = newTitleAfterFirstChange mutator.title = newTitleAfterFirstChange
@@ -411,7 +413,7 @@ describe('history manager', () => {
await Factory.sleep(Factory.ServerRevisionFrequency) await Factory.sleep(Factory.ServerRevisionFrequency)
const changedText = `${Math.random()}` const changedText = `${Math.random()}`
await this.application.changeAndSaveItem(note, (mutator) => { await this.application.changeAndSaveItem.execute(note, (mutator) => {
mutator.title = changedText mutator.title = changedText
}) })
await Factory.markDirtyAndSyncItem(this.application, note) await Factory.markDirtyAndSyncItem(this.application, note)

View File

@@ -813,7 +813,7 @@ describe('importing', function () {
}, },
}) })
await application.launch(false) await application.launch(false)
await application.setHost(Factory.getDefaultHost()) await application.setHost.execute(Factory.getDefaultHost())
const backupFile = { const backupFile = {
items: [ items: [

View File

@@ -50,17 +50,19 @@ describe('items', () => {
const item = this.application.items.items[0] const item = this.application.items.items[0]
expect(item.pinned).to.not.be.ok expect(item.pinned).to.not.be.ok
const refreshedItem = await this.application.changeAndSaveItem( const refreshedItem = (
item, await this.application.changeAndSaveItem.execute(
(mutator) => { item,
mutator.pinned = true (mutator) => {
mutator.archived = true mutator.pinned = true
mutator.locked = true mutator.archived = true
}, mutator.locked = true
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
)
).getValue()
expect(refreshedItem.pinned).to.equal(true) expect(refreshedItem.pinned).to.equal(true)
expect(refreshedItem.archived).to.equal(true) expect(refreshedItem.archived).to.equal(true)
expect(refreshedItem.locked).to.equal(true) expect(refreshedItem.locked).to.equal(true)
@@ -77,94 +79,110 @@ describe('items', () => {
expect(item1.isItemContentEqualWith(item2)).to.equal(true) expect(item1.isItemContentEqualWith(item2)).to.equal(true)
// items should ignore this field when checking for equality // items should ignore this field when checking for equality
item1 = await this.application.changeAndSaveItem( item1 = (
item1, await this.application.changeAndSaveItem.execute(
(mutator) => { item1,
mutator.userModifiedDate = new Date() (mutator) => {
}, mutator.userModifiedDate = new Date()
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
item2 = await this.application.changeAndSaveItem( )
item2, ).getValue()
(mutator) => { item2 = (
mutator.userModifiedDate = undefined await this.application.changeAndSaveItem.execute(
}, item2,
undefined, (mutator) => {
undefined, mutator.userModifiedDate = undefined
syncOptions, },
) undefined,
undefined,
syncOptions,
)
).getValue()
expect(item1.isItemContentEqualWith(item2)).to.equal(true) expect(item1.isItemContentEqualWith(item2)).to.equal(true)
item1 = await this.application.changeAndSaveItem( item1 = (
item1, await this.application.changeAndSaveItem.execute(
(mutator) => { item1,
mutator.mutableContent.foo = 'bar' (mutator) => {
}, mutator.mutableContent.foo = 'bar'
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
)
).getValue()
expect(item1.isItemContentEqualWith(item2)).to.equal(false) expect(item1.isItemContentEqualWith(item2)).to.equal(false)
item2 = await this.application.changeAndSaveItem( item2 = (
item2, await this.application.changeAndSaveItem.execute(
(mutator) => { item2,
mutator.mutableContent.foo = 'bar' (mutator) => {
}, mutator.mutableContent.foo = 'bar'
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
)
).getValue()
expect(item1.isItemContentEqualWith(item2)).to.equal(true) expect(item1.isItemContentEqualWith(item2)).to.equal(true)
expect(item2.isItemContentEqualWith(item1)).to.equal(true) expect(item2.isItemContentEqualWith(item1)).to.equal(true)
item1 = await this.application.changeAndSaveItem( item1 = (
item1, await this.application.changeAndSaveItem.execute(
(mutator) => { item1,
mutator.e2ePendingRefactor_addItemAsRelationship(item2) (mutator) => {
}, mutator.e2ePendingRefactor_addItemAsRelationship(item2)
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
item2 = await this.application.changeAndSaveItem( )
item2, ).getValue()
(mutator) => { item2 = (
mutator.e2ePendingRefactor_addItemAsRelationship(item1) await this.application.changeAndSaveItem.execute(
}, item2,
undefined, (mutator) => {
undefined, mutator.e2ePendingRefactor_addItemAsRelationship(item1)
syncOptions, },
) undefined,
undefined,
syncOptions,
)
).getValue()
expect(item1.content.references.length).to.equal(1) expect(item1.content.references.length).to.equal(1)
expect(item2.content.references.length).to.equal(1) expect(item2.content.references.length).to.equal(1)
expect(item1.isItemContentEqualWith(item2)).to.equal(false) expect(item1.isItemContentEqualWith(item2)).to.equal(false)
item1 = await this.application.changeAndSaveItem( item1 = (
item1, await this.application.changeAndSaveItem.execute(
(mutator) => { item1,
mutator.removeItemAsRelationship(item2) (mutator) => {
}, mutator.removeItemAsRelationship(item2)
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
item2 = await this.application.changeAndSaveItem( )
item2, ).getValue()
(mutator) => { item2 = (
mutator.removeItemAsRelationship(item1) await this.application.changeAndSaveItem.execute(
}, item2,
undefined, (mutator) => {
undefined, mutator.removeItemAsRelationship(item1)
syncOptions, },
) undefined,
undefined,
syncOptions,
)
).getValue()
expect(item1.isItemContentEqualWith(item2)).to.equal(true) expect(item1.isItemContentEqualWith(item2)).to.equal(true)
expect(item1.content.references.length).to.equal(0) expect(item1.content.references.length).to.equal(0)
@@ -179,15 +197,17 @@ describe('items', () => {
let item1 = this.application.items.getDisplayableNotes()[0] let item1 = this.application.items.getDisplayableNotes()[0]
const item2 = this.application.items.getDisplayableNotes()[1] const item2 = this.application.items.getDisplayableNotes()[1]
item1 = await this.application.changeAndSaveItem( item1 = (
item1, await this.application.changeAndSaveItem.execute(
(mutator) => { item1,
mutator.mutableContent.foo = 'bar' (mutator) => {
}, mutator.mutableContent.foo = 'bar'
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
)
).getValue()
expect(item1.content.foo).to.equal('bar') expect(item1.content.foo).to.equal('bar')

View File

@@ -184,15 +184,17 @@ describe('notes and tags', () => {
expect(note.content.references.length).to.equal(0) expect(note.content.references.length).to.equal(0)
expect(tag.content.references.length).to.equal(1) expect(tag.content.references.length).to.equal(1)
tag = await this.application.changeAndSaveItem( tag = (
tag, await this.application.changeAndSaveItem.execute(
(mutator) => { tag,
mutator.removeItemAsRelationship(note) (mutator) => {
}, mutator.removeItemAsRelationship(note)
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
)
).getValue()
expect(this.application.items.itemsReferencingItem(note).length).to.equal(0) expect(this.application.items.itemsReferencingItem(note).length).to.equal(0)
expect(tag.noteCount).to.equal(0) expect(tag.noteCount).to.equal(0)
@@ -265,15 +267,17 @@ describe('notes and tags', () => {
const notePayload = Factory.createNotePayload() const notePayload = Factory.createNotePayload()
await this.application.mutator.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged) await this.application.mutator.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged)
let note = this.application.items.getItems([ContentType.TYPES.Note])[0] let note = this.application.items.getItems([ContentType.TYPES.Note])[0]
note = await this.application.changeAndSaveItem( note = (
note, await this.application.changeAndSaveItem.execute(
(mutator) => { note,
mutator.mutableContent.title = Math.random() (mutator) => {
}, mutator.mutableContent.title = Math.random()
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
)
).getValue()
expect(note.content.title).to.not.equal(notePayload.content.title) expect(note.content.title).to.not.equal(notePayload.content.title)
}) })

View File

@@ -382,19 +382,19 @@ describe('protections', function () {
this.foo = 'tar' this.foo = 'tar'
application = await Factory.createInitAppWithFakeCrypto() application = await Factory.createInitAppWithFakeCrypto()
await application.addPasscode('passcode') 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 () { it('should return true when session length has been set', async function () {
application = await Factory.createInitAppWithFakeCrypto() application = await Factory.createInitAppWithFakeCrypto()
await application.addPasscode('passcode') await application.addPasscode('passcode')
await application.protections.setSessionLength(UnprotectedAccessSecondsDuration.OneMinute) 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 () { it('should return true when there are no protection sources', async function () {
application = await Factory.createInitAppWithFakeCrypto() application = await Factory.createInitAppWithFakeCrypto()
expect(application.hasUnprotectedAccessSession()).to.be.true expect(application.protections.hasUnprotectedAccessSession()).to.be.true
}) })
}) })

View File

@@ -298,7 +298,7 @@ describe('online conflict handling', function () {
await this.application.mutator.setItemDirty(note) await this.application.mutator.setItemDirty(note)
this.expectedItemCount++ this.expectedItemCount++
await this.application.changeAndSaveItem( await this.application.changeAndSaveItem.execute(
note, note,
(mutator) => { (mutator) => {
// client A // client A
@@ -332,7 +332,7 @@ describe('online conflict handling', function () {
await this.application.mutator.setItemDirty(note) await this.application.mutator.setItemDirty(note)
this.expectedItemCount++ this.expectedItemCount++
await this.application.changeAndSaveItem( await this.application.changeAndSaveItem.execute(
note, note,
(mutator) => { (mutator) => {
// client A // client A
@@ -602,15 +602,17 @@ describe('online conflict handling', function () {
*/ */
let tag = await Factory.createMappedTag(this.application) let tag = await Factory.createMappedTag(this.application)
let note = await Factory.createMappedNote(this.application) let note = await Factory.createMappedNote(this.application)
tag = await this.application.changeAndSaveItem( tag = (
tag, await this.application.changeAndSaveItem.execute(
(mutator) => { tag,
mutator.e2ePendingRefactor_addItemAsRelationship(note) (mutator) => {
}, mutator.e2ePendingRefactor_addItemAsRelationship(note)
undefined, },
undefined, undefined,
syncOptions, undefined,
) syncOptions,
)
).getValue()
await this.application.mutator.setItemDirty(note) await this.application.mutator.setItemDirty(note)
this.expectedItemCount += 2 this.expectedItemCount += 2
@@ -732,39 +734,42 @@ describe('online conflict handling', function () {
}) })
/** This test takes too long on Docker CI */ /** 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 () { it.skip(
/** 'registering for account with bulk offline data belonging to another account should be error-free',
* When performing a multi-page sync request where we are uploading data imported from a backup, async function () {
* 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. * When performing a multi-page sync request where we are uploading data imported from a backup,
* This causes a problem because when that second page is returned as conflicts, we will be looking * if the first page of the sync request returns conflicted items keys, we rotate their UUID.
* for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire * The second page of sync waiting to be sent up is still encrypted with the old items key UUID.
* sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead * This causes a problem because when that second page is returned as conflicts, we will be looking
* take the approach of making sure the decryption function is liberal with regards to searching * for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire
* for the right items key. It will now consider (as a result of this test) an items key as being * sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead
* the correct key to decrypt an item if the itemskey.uuid == item.items_key_id OR if the itemsKey.duplicateOf * take the approach of making sure the decryption function is liberal with regards to searching
* value is equal to item.items_key_id. * 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 */ /** Create bulk data belonging to another account and sync */
const largeItemCount = SyncUpDownLimit + 10 const largeItemCount = SyncUpDownLimit + 10
await Factory.createManyMappedNotes(this.application, largeItemCount) await Factory.createManyMappedNotes(this.application, largeItemCount)
await this.application.sync.sync(syncOptions) await this.application.sync.sync(syncOptions)
const priorData = this.application.items.items const priorData = this.application.items.items
/** Register new account and import this same data */ /** Register new account and import this same data */
const newApp = await Factory.signOutApplicationAndReturnNew(this.application) const newApp = await Factory.signOutApplicationAndReturnNew(this.application)
await Factory.registerUserToApplication({ await Factory.registerUserToApplication({
application: newApp, application: newApp,
email: Utils.generateUuid(), email: Utils.generateUuid(),
password: Utils.generateUuid(), password: Utils.generateUuid(),
}) })
await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload)) await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload))
await newApp.sync.markAllItemsAsNeedingSyncAndPersist() await newApp.sync.markAllItemsAsNeedingSyncAndPersist()
await newApp.sync.sync(syncOptions) await newApp.sync.sync(syncOptions)
expect(newApp.payloads.invalidPayloads.length).to.equal(0) expect(newApp.payloads.invalidPayloads.length).to.equal(0)
await Factory.safeDeinit(newApp) await Factory.safeDeinit(newApp)
}).timeout(80000) },
).timeout(80000)
it('importing data belonging to another account should not result in duplication', async function () { it('importing data belonging to another account should not result in duplication', async function () {
/** Create primary account and export data */ /** Create primary account and export data */
@@ -801,7 +806,7 @@ describe('online conflict handling', function () {
await createSyncedNoteWithTag(this.application) await createSyncedNoteWithTag(this.application)
const tag = this.application.items.getDisplayableTags()[0] const tag = this.application.items.getDisplayableTags()[0]
const note2 = await Factory.createMappedNote(this.application) const note2 = await Factory.createMappedNote(this.application)
await this.application.changeAndSaveItem(tag, (mutator) => { await this.application.changeAndSaveItem.execute(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note2) mutator.e2ePendingRefactor_addItemAsRelationship(note2)
}) })
let backupFile = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() let backupFile = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()

View File

@@ -98,7 +98,7 @@ describe('offline syncing', () => {
this.expectedItemCount++ this.expectedItemCount++
await this.application.sync.sync(syncOptions) await this.application.sync.sync(syncOptions)
this.application = await Factory.signOutApplicationAndReturnNew(this.application) this.application = await Factory.signOutApplicationAndReturnNew(this.application)
expect(this.application.noAccount()).to.equal(true) expect(this.application.sessions.isSignedIn()).to.equal(false)
expect(this.application.getUser()).to.not.be.ok expect(this.application.sessions.getUser()).to.not.be.ok
}) })
}) })

View File

@@ -600,7 +600,7 @@ describe('online syncing', function () {
it('saving an item after sync should persist it with content property', async function () { it('saving an item after sync should persist it with content property', async function () {
const note = await Factory.createMappedNote(this.application) const note = await Factory.createMappedNote(this.application)
const text = Factory.randomString(10000) const text = Factory.randomString(10000)
await this.application.changeAndSaveItem( await this.application.changeAndSaveItem.execute(
note, note,
(mutator) => { (mutator) => {
mutator.text = text 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 () { it('deleting an item permanently should include it in PayloadEmitSource.PreSyncSave item change observer', async function () {
let conditionMet = false 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) { if (source === PayloadEmitSource.PreSyncSave && removed.length === 1) {
conditionMet = true conditionMet = true
} }

View File

@@ -1,19 +1,13 @@
import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface'
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter' import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter'
import data from './testData' import data from './testData'
import { UuidGenerator } from '@standardnotes/utils'
UuidGenerator.SetGenerator(() => String(Math.random()))
describe('AegisConverter', () => { describe('AegisConverter', () => {
let application: WebApplicationInterface
beforeEach(() => {
application = {
generateUUID: jest.fn().mockReturnValue('test'),
} as unknown as WebApplicationInterface
})
it('should parse entries', () => { it('should parse entries', () => {
const converter = new AegisToAuthenticatorConverter(application) const converter = new AegisToAuthenticatorConverter()
const result = converter.parseEntries(data) const result = converter.parseEntries(data)
@@ -34,7 +28,7 @@ describe('AegisConverter', () => {
}) })
it('should create note from entries with editor info', () => { it('should create note from entries with editor info', () => {
const converter = new AegisToAuthenticatorConverter(application) const converter = new AegisToAuthenticatorConverter()
const parsedEntries = converter.parseEntries(data) const parsedEntries = converter.parseEntries(data)
@@ -61,7 +55,7 @@ describe('AegisConverter', () => {
}) })
it('should create note from entries without editor info', () => { it('should create note from entries without editor info', () => {
const converter = new AegisToAuthenticatorConverter(application) const converter = new AegisToAuthenticatorConverter()
const parsedEntries = converter.parseEntries(data) const parsedEntries = converter.parseEntries(data)

View File

@@ -1,8 +1,8 @@
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { readFileAsText } from '../Utils' import { readFileAsText } from '../Utils'
import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { UuidGenerator } from '@standardnotes/utils'
type AegisData = { type AegisData = {
db: { db: {
@@ -27,9 +27,11 @@ type AuthenticatorEntry = {
} }
export class AegisToAuthenticatorConverter { export class AegisToAuthenticatorConverter {
constructor(protected application: WebApplicationInterface) {} constructor() {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static isValidAegisJson(json: any): boolean { 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)) 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, created_at_timestamp: file.lastModified,
updated_at: new Date(file.lastModified), updated_at: new Date(file.lastModified),
updated_at_timestamp: file.lastModified, updated_at_timestamp: file.lastModified,
uuid: this.application.generateUUID(), uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.TYPES.Note, content_type: ContentType.TYPES.Note,
content: { content: {
title: file.name.split('.')[0], title: file.name.split('.')[0],

View File

@@ -6,7 +6,7 @@ import { ContentType } from '@standardnotes/domain-core'
import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models' import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models'
import { EvernoteConverter } from './EvernoteConverter' import { EvernoteConverter } from './EvernoteConverter'
import data from './testData' 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 // Mock dayjs so dayjs.extend() doesn't throw an error in EvernoteConverter.ts
jest.mock('dayjs', () => { jest.mock('dayjs', () => {
@@ -21,17 +21,11 @@ jest.mock('dayjs', () => {
} }
}) })
UuidGenerator.SetGenerator(() => String(Math.random()))
describe('EvernoteConverter', () => { describe('EvernoteConverter', () => {
let application: WebApplicationInterface
beforeEach(() => {
application = {
generateUUID: jest.fn().mockReturnValue(Math.random()),
} as any as WebApplicationInterface
})
it('should parse and strip html', () => { it('should parse and strip html', () => {
const converter = new EvernoteConverter(application) const converter = new EvernoteConverter()
const result = converter.parseENEXData(data, true) const result = converter.parseENEXData(data, true)
@@ -51,7 +45,7 @@ describe('EvernoteConverter', () => {
}) })
it('should parse and not strip html', () => { it('should parse and not strip html', () => {
const converter = new EvernoteConverter(application) const converter = new EvernoteConverter()
const result = converter.parseENEXData(data, false) const result = converter.parseENEXData(data, false)

View File

@@ -3,15 +3,15 @@ import { readFileAsText } from '../Utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat' import customParseFormat from 'dayjs/plugin/customParseFormat'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { UuidGenerator } from '@standardnotes/utils'
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(utc) dayjs.extend(utc)
const dateFormat = 'YYYYMMDDTHHmmss' const dateFormat = 'YYYYMMDDTHHmmss'
export class EvernoteConverter { export class EvernoteConverter {
constructor(protected application: WebApplicationInterface) {} constructor() {}
async convertENEXFileToNotesAndTags(file: File, stripHTML: boolean): Promise<DecryptedTransferPayload[]> { async convertENEXFileToNotesAndTags(file: File, stripHTML: boolean): Promise<DecryptedTransferPayload[]> {
const content = await readFileAsText(file) const content = await readFileAsText(file)
@@ -35,7 +35,7 @@ export class EvernoteConverter {
created_at_timestamp: now.getTime(), created_at_timestamp: now.getTime(),
updated_at: now, updated_at: now,
updated_at_timestamp: now.getTime(), updated_at_timestamp: now.getTime(),
uuid: this.application.generateUUID(), uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.TYPES.Tag, content_type: ContentType.TYPES.Tag,
content: { content: {
title: defaultTagName, title: defaultTagName,
@@ -88,7 +88,7 @@ export class EvernoteConverter {
created_at_timestamp: createdAtDate.getTime(), created_at_timestamp: createdAtDate.getTime(),
updated_at: updatedAtDate, updated_at: updatedAtDate,
updated_at_timestamp: updatedAtDate.getTime(), updated_at_timestamp: updatedAtDate.getTime(),
uuid: this.application.generateUUID(), uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.TYPES.Note, content_type: ContentType.TYPES.Note,
content: { content: {
title: !title ? `Imported note ${index + 1} from Evernote` : title, title: !title ? `Imported note ${index + 1} from Evernote` : title,
@@ -111,7 +111,7 @@ export class EvernoteConverter {
if (!tag) { if (!tag) {
const now = new Date() const now = new Date()
tag = { tag = {
uuid: this.application.generateUUID(), uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.TYPES.Tag, content_type: ContentType.TYPES.Tag,
created_at: now, created_at: now,
created_at_timestamp: now.getTime(), created_at_timestamp: now.getTime(),

View File

@@ -4,19 +4,13 @@
import { jsonTestData, htmlTestData } from './testData' import { jsonTestData, htmlTestData } from './testData'
import { GoogleKeepConverter } from './GoogleKeepConverter' import { GoogleKeepConverter } from './GoogleKeepConverter'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { UuidGenerator } from '@standardnotes/utils'
UuidGenerator.SetGenerator(() => String(Math.random()))
describe('GoogleKeepConverter', () => { describe('GoogleKeepConverter', () => {
let application: WebApplicationInterface
beforeEach(() => {
application = {
generateUUID: jest.fn().mockReturnValue('uuid'),
} as unknown as WebApplicationInterface
})
it('should parse json data', () => { it('should parse json data', () => {
const converter = new GoogleKeepConverter(application) const converter = new GoogleKeepConverter()
const result = converter.tryParseAsJson(jsonTestData) const result = converter.tryParseAsJson(jsonTestData)
@@ -33,7 +27,7 @@ describe('GoogleKeepConverter', () => {
}) })
it('should parse html data', () => { it('should parse html data', () => {
const converter = new GoogleKeepConverter(application) const converter = new GoogleKeepConverter()
const result = converter.tryParseAsHtml( const result = converter.tryParseAsHtml(
htmlTestData, htmlTestData,

View File

@@ -1,7 +1,7 @@
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { readFileAsText } from '../Utils' import { readFileAsText } from '../Utils'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { UuidGenerator } from '@standardnotes/utils'
type GoogleKeepJsonNote = { type GoogleKeepJsonNote = {
color: string color: string
@@ -14,7 +14,7 @@ type GoogleKeepJsonNote = {
} }
export class GoogleKeepConverter { export class GoogleKeepConverter {
constructor(protected application: WebApplicationInterface) {} constructor() {}
async convertGoogleKeepBackupFileToNote( async convertGoogleKeepBackupFileToNote(
file: File, file: File,
@@ -66,7 +66,7 @@ export class GoogleKeepConverter {
created_at_timestamp: date.getTime(), created_at_timestamp: date.getTime(),
updated_at: date, updated_at: date,
updated_at_timestamp: date.getTime(), updated_at_timestamp: date.getTime(),
uuid: this.application.generateUUID(), uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.TYPES.Note, content_type: ContentType.TYPES.Note,
content: { content: {
title: title, title: title,
@@ -96,6 +96,7 @@ export class GoogleKeepConverter {
return return
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static isValidGoogleKeepJson(json: any): boolean { static isValidGoogleKeepJson(json: any): boolean {
return ( return (
typeof json.title === 'string' && typeof json.title === 'string' &&
@@ -120,7 +121,7 @@ export class GoogleKeepConverter {
created_at_timestamp: date.getTime(), created_at_timestamp: date.getTime(),
updated_at: date, updated_at: date,
updated_at_timestamp: date.getTime(), updated_at_timestamp: date.getTime(),
uuid: this.application.generateUUID(), uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.TYPES.Note, content_type: ContentType.TYPES.Note,
content: { content: {
title: parsed.title, title: parsed.title,

View File

@@ -1,5 +1,10 @@
import { parseFileName } from '@standardnotes/filepicker' import { parseFileName } from '@standardnotes/filepicker'
import { FeatureStatus } from '@standardnotes/services' import {
FeatureStatus,
FeaturesClientInterface,
ItemManagerInterface,
MutatorClientInterface,
} from '@standardnotes/services'
import { NativeFeatureIdentifier } from '@standardnotes/features' import { NativeFeatureIdentifier } from '@standardnotes/features'
import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter' import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter'
import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter' import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter'
@@ -8,7 +13,6 @@ import { PlaintextConverter } from './PlaintextConverter/PlaintextConverter'
import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter' import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter'
import { readFileAsText } from './Utils' import { readFileAsText } from './Utils'
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface'
export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis' export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis'
@@ -19,12 +23,16 @@ export class Importer {
plaintextConverter: PlaintextConverter plaintextConverter: PlaintextConverter
evernoteConverter: EvernoteConverter evernoteConverter: EvernoteConverter
constructor(protected application: WebApplicationInterface) { constructor(
this.aegisConverter = new AegisToAuthenticatorConverter(application) private features: FeaturesClientInterface,
this.googleKeepConverter = new GoogleKeepConverter(application) private mutator: MutatorClientInterface,
this.simplenoteConverter = new SimplenoteConverter(application) private items: ItemManagerInterface,
this.plaintextConverter = new PlaintextConverter(application) ) {
this.evernoteConverter = new EvernoteConverter(application) 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<NoteImportType | null> => { static detectService = async (file: File): Promise<NoteImportType | null> => {
@@ -64,7 +72,7 @@ export class Importer {
async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> { async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> {
if (type === 'aegis') { if (type === 'aegis') {
const isEntitledToAuthenticator = const isEntitledToAuthenticator =
this.application.features.getFeatureStatus( this.features.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(), NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
) === FeatureStatus.Entitled ) === FeatureStatus.Entitled
return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)] return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)]
@@ -85,7 +93,7 @@ export class Importer {
const insertedItems = await Promise.all( const insertedItems = await Promise.all(
payloads.map(async (payload) => { payloads.map(async (payload) => {
const content = payload.content as NoteContent const content = payload.content as NoteContent
const note = this.application.items.createTemplateItem( const note = this.items.createTemplateItem(
payload.content_type, payload.content_type,
{ {
text: content.text, text: content.text,
@@ -100,7 +108,7 @@ export class Importer {
uuid: payload.uuid, uuid: payload.uuid,
}, },
) )
return this.application.mutator.insertItem(note) return this.mutator.insertItem(note)
}), }),
) )
return insertedItems return insertedItems

View File

@@ -2,11 +2,9 @@ import { ContentType } from '@standardnotes/domain-core'
import { parseFileName } from '@standardnotes/filepicker' import { parseFileName } from '@standardnotes/filepicker'
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { readFileAsText } from '../Utils' import { readFileAsText } from '../Utils'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { UuidGenerator } from '@standardnotes/utils'
export class PlaintextConverter { export class PlaintextConverter {
constructor(protected application: WebApplicationInterface) {}
static isValidPlaintextFile(file: File): boolean { static isValidPlaintextFile(file: File): boolean {
return file.type === 'text/plain' || file.type === 'text/markdown' return file.type === 'text/plain' || file.type === 'text/markdown'
} }
@@ -24,7 +22,7 @@ export class PlaintextConverter {
created_at_timestamp: createdAtDate.getTime(), created_at_timestamp: createdAtDate.getTime(),
updated_at: updatedAtDate, updated_at: updatedAtDate,
updated_at_timestamp: updatedAtDate.getTime(), updated_at_timestamp: updatedAtDate.getTime(),
uuid: this.application.generateUUID(), uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.TYPES.Note, content_type: ContentType.TYPES.Note,
content: { content: {
title: name, title: name,

View File

@@ -1,18 +1,12 @@
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { UuidGenerator } from '@standardnotes/utils'
import { SimplenoteConverter } from './SimplenoteConverter' import { SimplenoteConverter } from './SimplenoteConverter'
import data from './testData' import data from './testData'
UuidGenerator.SetGenerator(() => String(Math.random()))
describe('SimplenoteConverter', () => { describe('SimplenoteConverter', () => {
let application: WebApplicationInterface
beforeEach(() => {
application = {
generateUUID: jest.fn().mockReturnValue('uuid'),
} as any
})
it('should parse', () => { it('should parse', () => {
const converter = new SimplenoteConverter(application) const converter = new SimplenoteConverter()
const result = converter.parse(data) const result = converter.parse(data)

View File

@@ -1,7 +1,7 @@
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { readFileAsText } from '../Utils' import { readFileAsText } from '../Utils'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { UuidGenerator } from '@standardnotes/utils'
type SimplenoteItem = { type SimplenoteItem = {
creationDate: string creationDate: string
@@ -14,11 +14,13 @@ type SimplenoteData = {
trashedNotes: SimplenoteItem[] trashedNotes: SimplenoteItem[]
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isSimplenoteEntry = (entry: any): boolean => entry.id && entry.content && entry.creationDate && entry.lastModified const isSimplenoteEntry = (entry: any): boolean => entry.id && entry.content && entry.creationDate && entry.lastModified
export class SimplenoteConverter { export class SimplenoteConverter {
constructor(protected application: WebApplicationInterface) {} constructor() {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static isValidSimplenoteJson(json: any): boolean { static isValidSimplenoteJson(json: any): boolean {
return ( return (
(json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) || (json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) ||
@@ -53,7 +55,7 @@ export class SimplenoteConverter {
created_at_timestamp: createdAtDate.getTime(), created_at_timestamp: createdAtDate.getTime(),
updated_at: updatedAtDate, updated_at: updatedAtDate,
updated_at_timestamp: updatedAtDate.getTime(), updated_at_timestamp: updatedAtDate.getTime(),
uuid: this.application.generateUUID(), uuid: UuidGenerator.GenerateUuid(),
content_type: ContentType.TYPES.Note, content_type: ContentType.TYPES.Note,
content: { content: {
title, title,

View File

@@ -21,7 +21,7 @@ const STORAGE_KEY_AUTOLOCK_INTERVAL = 'AutoLockIntervalKey'
export class AutolockService extends AbstractService { export class AutolockService extends AbstractService {
private unsubApp!: () => void private unsubApp!: () => void
private pollInterval: any private pollInterval: ReturnType<typeof setInterval> | undefined
private lastFocusState?: 'hidden' | 'visible' private lastFocusState?: 'hidden' | 'visible'
private lockAfterDate?: Date private lockAfterDate?: Date
@@ -100,7 +100,7 @@ export class AutolockService extends AbstractService {
*/ */
beginPolling() { beginPolling() {
this.pollInterval = setInterval(async () => { 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) { if (!locked && this.lockAfterDate && new Date() > this.lockAfterDate) {
this.lockApplication() this.lockApplication()
} }

View File

@@ -1,5 +1,5 @@
export enum PersistenceKey { export enum PersistenceKey {
SelectedItemsController = 'selected-items-controller', ItemListController = 'selected-items-controller',
NavigationController = 'navigation-controller', NavigationController = 'navigation-controller',
} }
@@ -12,6 +12,6 @@ export type NavigationControllerPersistableValue = {
} }
export type PersistedStateValue = { export type PersistedStateValue = {
[PersistenceKey.SelectedItemsController]: SelectionControllerPersistableValue [PersistenceKey.ItemListController]: SelectionControllerPersistableValue
[PersistenceKey.NavigationController]: NavigationControllerPersistableValue [PersistenceKey.NavigationController]: NavigationControllerPersistableValue
} }

View File

@@ -60,7 +60,7 @@ export class ThemeManager extends AbstractUIServicee {
} }
override async onAppStart() { override async onAppStart() {
const desktopService = this.application.getDesktopService() const desktopService = this.application.desktopManager
if (desktopService) { if (desktopService) {
this.eventDisposers.push( this.eventDisposers.push(
desktopService.registerUpdateObserver((component) => { desktopService.registerUpdateObserver((component) => {
@@ -167,7 +167,7 @@ export class ThemeManager extends AbstractUIServicee {
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false) const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
if (useDeviceThemeSettings) { if (useDeviceThemeSettings) {
const prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark' const prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark'
this.setThemeAsPerColorScheme(prefersDarkColorScheme) this.setThemeAsPerColorScheme(prefersDarkColorScheme)
} }
} }
@@ -187,7 +187,7 @@ export class ThemeManager extends AbstractUIServicee {
let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches
if (this.application.isNativeMobileWeb()) { if (this.application.isNativeMobileWeb()) {
prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark' prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark'
} }
this.setThemeAsPerColorScheme(prefersDarkColorScheme) this.setThemeAsPerColorScheme(prefersDarkColorScheme)
@@ -340,9 +340,7 @@ export class ThemeManager extends AbstractUIServicee {
if (this.application.isNativeMobileWeb() && !theme.layerable) { if (this.application.isNativeMobileWeb() && !theme.layerable) {
const packageInfo = theme.featureDescription const packageInfo = theme.featureDescription
setTimeout(() => { setTimeout(() => {
this.application this.application.mobileDevice.handleThemeSchemeChange(packageInfo.isDark ?? false, this.getBackgroundColor())
.mobileDevice()
.handleThemeSchemeChange(packageInfo.isDark ?? false, this.getBackgroundColor())
}) })
} }
@@ -366,7 +364,7 @@ export class ThemeManager extends AbstractUIServicee {
if (this.themesActiveInTheUI.isEmpty()) { if (this.themesActiveInTheUI.isEmpty()) {
if (this.application.isNativeMobileWeb()) { if (this.application.isNativeMobileWeb()) {
this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff') this.application.mobileDevice.handleThemeSchemeChange(false, '#ffffff')
} }
this.toggleTranslucentUIColors() this.toggleTranslucentUIColors()
} }

View File

@@ -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<SNTag[]> {
constructor(private items: ItemManagerInterface) {}
execute(item: DecryptedItemInterface): Result<SNTag[]> {
return Result.ok(
this.items.itemsReferencingItem<SNTag>(item).filter((ref) => {
return ref.content_type === ContentType.TYPES.Tag
}),
)
}
}

View File

@@ -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<boolean> {
constructor(private preferences: PreferenceServiceInterface) {}
execute(): Result<boolean> {
return Result.ok(this.preferences.getValue(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck]))
}
}

View File

@@ -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<boolean> {
constructor(private _isNativeMobileWeb: IsNativeMobileWeb) {}
execute(): Result<boolean> {
return Result.ok(this._isNativeMobileWeb.execute().getValue() || isIOS() || isAndroid())
}
}

View File

@@ -0,0 +1,13 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { Environment, Platform } from '@standardnotes/models'
export class IsNativeIOS implements SyncUseCaseInterface<boolean> {
constructor(
private environment: Environment,
private platform: Platform,
) {}
execute(): Result<boolean> {
return Result.ok(this.environment === Environment.Mobile && this.platform === Platform.Ios)
}
}

View File

@@ -0,0 +1,10 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { Environment } from '@standardnotes/models'
export class IsNativeMobileWeb implements SyncUseCaseInterface<boolean> {
constructor(private environment: Environment) {}
execute(): Result<boolean> {
return Result.ok(this.environment === Environment.Mobile)
}
}

View File

@@ -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')

View File

@@ -33,10 +33,6 @@ export class VaultDisplayService
this.options = new VaultDisplayOptions({ exclude: [], locked: [] }) this.options = new VaultDisplayOptions({ exclude: [], locked: [] })
internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultLocked)
internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultUnlocked)
internalEventBus.addEventHandler(this, ApplicationEvent.ApplicationStageChanged)
makeObservable(this, { makeObservable(this, {
options: observable, options: observable,
@@ -48,6 +44,10 @@ export class VaultDisplayService
unhideVault: action, unhideVault: action,
showOnlyVault: action, showOnlyVault: action,
}) })
internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultLocked)
internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultUnlocked)
internalEventBus.addEventHandler(this, ApplicationEvent.ApplicationStageChanged)
} }
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {

View File

@@ -1,14 +1,15 @@
import { import {
ApplicationInterface, ApplicationInterface,
DesktopDeviceInterface,
DesktopManagerInterface, DesktopManagerInterface,
MobileDeviceInterface, MobileDeviceInterface,
WebAppEvent, WebAppEvent,
} from '@standardnotes/services' } from '@standardnotes/services'
import { KeyboardService } from '../Keyboard/KeyboardService' import { KeyboardService } from '../Keyboard/KeyboardService'
import { RouteServiceInterface } from '../Route/RouteServiceInterface'
export interface WebApplicationInterface extends ApplicationInterface { export interface WebApplicationInterface extends ApplicationInterface {
notifyWebEvent(event: WebAppEvent, data?: unknown): void notifyWebEvent(event: WebAppEvent, data?: unknown): void
getDesktopService(): DesktopManagerInterface | undefined
handleMobileEnteringBackgroundEvent(): Promise<void> handleMobileEnteringBackgroundEvent(): Promise<void>
handleMobileGainingFocusEvent(): Promise<void> handleMobileGainingFocusEvent(): Promise<void>
handleMobileLosingFocusEvent(): Promise<void> handleMobileLosingFocusEvent(): Promise<void>
@@ -24,10 +25,17 @@ export interface WebApplicationInterface extends ApplicationInterface {
handleReceivedTextEvent(item: { text: string; title?: string }): Promise<void> handleReceivedTextEvent(item: { text: string; title?: string }): Promise<void>
handleReceivedLinkEvent(item: { link: string; title: string }): Promise<void> handleReceivedLinkEvent(item: { link: string; title: string }): Promise<void>
isNativeMobileWeb(): boolean isNativeMobileWeb(): boolean
mobileDevice(): MobileDeviceInterface
handleAndroidBackButtonPressed(): void handleAndroidBackButtonPressed(): void
addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined
setAndroidBackHandlerFallbackListener(listener: () => boolean): void setAndroidBackHandlerFallbackListener(listener: () => boolean): void
handleInitialMobileScreenshotPrivacy(): void
generateUUID(): string generateUUID(): string
checkForSecurityUpdate(): Promise<boolean>
get desktopManager(): DesktopManagerInterface | undefined
get mobileDevice(): MobileDeviceInterface
get isMobileDevice(): boolean
get desktopDevice(): DesktopDeviceInterface | undefined
get keyboardService(): KeyboardService get keyboardService(): KeyboardService
get routeService(): RouteServiceInterface
} }

View File

@@ -29,6 +29,12 @@ export * from './Route/RouteServiceEvent'
export * from './Security/AutolockService' export * from './Security/AutolockService'
export * from './Storage/LocalStorage' 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/ThemeManager'
export * from './Theme/GetAllThemesUseCase' export * from './Theme/GetAllThemesUseCase'
@@ -42,3 +48,4 @@ export * from './Vaults/VaultDisplayServiceEvent'
export * from './Vaults/VaultDisplayServiceInterface' export * from './Vaults/VaultDisplayServiceInterface'
export * from './WebApplication/WebApplicationInterface' export * from './WebApplication/WebApplicationInterface'
export * from './Utils/Utils'

View File

@@ -0,0 +1,50 @@
import { isNotUndefined } from '../Utils/Utils'
import { isDeinitable } from './isDeinitable'
export class DependencyContainer {
private factory = new Map<symbol, () => unknown>()
private dependencies = new Map<symbol, unknown>()
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<T>(sym: symbol, maker: () => T) {
this.factory.set(sym, maker)
}
public get<T>(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
}
}

View File

@@ -1,5 +1,7 @@
export * from './Date/DateUtils' export * from './Date/DateUtils'
export * from './Deferred/Deferred' export * from './Deferred/Deferred'
export * from './Dependency/DependencyContainer'
export * from './Dependency/isDeinitable'
export * from './Logger/Logger' export * from './Logger/Logger'
export * from './Logger/LoggerInterface' export * from './Logger/LoggerInterface'
export * from './Logger/LogLevel' export * from './Logger/LogLevel'

View File

@@ -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'),
}

View File

@@ -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<IsNativeMobileWeb>(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<WebApplicationInterface>(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<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb))
})
this.bind(Web_TYPES.MomentsService, () => {
return new MomentsService(
this.get<FilesController>(Web_TYPES.FilesController),
this.get<LinkingController>(Web_TYPES.LinkingController),
application.storage,
application.preferences,
application.items,
application.protections,
application.desktopDevice,
this.get<IsMobileDevice>(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<ItemListController>(Web_TYPES.ItemListController),
this.get<NavigationController>(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<KeyboardService>(Web_TYPES.KeyboardService),
this.get<IsTabletOrMobileScreen>(Web_TYPES.IsTabletOrMobileScreen),
this.get<PanesForLayout>(Web_TYPES.PanesForLayout),
application.events,
)
})
this.bind(Web_TYPES.PanesForLayout, () => {
return new PanesForLayout(this.get<IsTabletOrMobileScreen>(Web_TYPES.IsTabletOrMobileScreen))
})
this.bind(Web_TYPES.IsTabletOrMobileScreen, () => {
return new IsTabletOrMobileScreen(application.environment)
})
this.bind(Web_TYPES.PreferencesController, () => {
return new PreferencesController(this.get<RouteService>(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<FeaturesController>(Web_TYPES.FeaturesController),
this.get<VaultDisplayService>(Web_TYPES.VaultDisplayService),
this.get<KeyboardService>(Web_TYPES.KeyboardService),
this.get<PaneController>(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<ItemListController>(Web_TYPES.ItemListController),
this.get<NavigationController>(Web_TYPES.NavigationController),
this.get<ItemGroupController>(Web_TYPES.ItemGroupController),
this.get<KeyboardService>(Web_TYPES.KeyboardService),
application.preferences,
application.items,
application.mutator,
application.sync,
application.protections,
application.alerts,
this.get<IsGlobalSpellcheckEnabled>(Web_TYPES.IsGlobalSpellcheckEnabled),
this.get<GetItemTags>(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<ItemListController>(Web_TYPES.ItemListController),
this.get<FilesController>(Web_TYPES.FilesController),
this.get<SubscriptionController>(Web_TYPES.SubscriptionController),
this.get<NavigationController>(Web_TYPES.NavigationController),
this.get<ItemGroupController>(Web_TYPES.ItemGroupController),
this.get<VaultDisplayService>(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<KeyboardService>(Web_TYPES.KeyboardService),
this.get<PaneController>(Web_TYPES.PaneController),
this.get<NavigationController>(Web_TYPES.NavigationController),
this.get<SearchOptionsController>(Web_TYPES.SearchOptionsController),
application.items,
application.preferences,
this.get<ItemGroupController>(Web_TYPES.ItemGroupController),
this.get<VaultDisplayService>(Web_TYPES.VaultDisplayService),
this.get<DesktopManager>(Web_TYPES.DesktopManager),
application.protections,
application.options,
this.get<IsNativeMobileWeb>(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<LoadPurchaseFlowUrl>(Web_TYPES.LoadPurchaseFlowUrl),
this.get<IsNativeIOS>(Web_TYPES.IsNativeIOS),
application.events,
)
})
this.bind(Web_TYPES.LoadPurchaseFlowUrl, () => {
return new LoadPurchaseFlowUrl(application, this.get<GetPurchaseFlowUrl>(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<NotesController>(Web_TYPES.NotesController),
this.get<FilePreviewModalController>(Web_TYPES.FilePreviewModalController),
this.get<ArchiveManager>(Web_TYPES.ArchiveManager),
this.get<VaultDisplayService>(Web_TYPES.VaultDisplayService),
application.items,
application.files,
application.mutator,
application.sync,
application.protections,
application.alerts,
application.platform,
application.mobileDevice,
this.get<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb),
application.events,
)
})
this.bind(Web_TYPES.HistoryModalController, () => {
return new HistoryModalController(
this.get<NotesController>(Web_TYPES.NotesController),
this.get<KeyboardService>(Web_TYPES.KeyboardService),
application.events,
)
})
this.bind(Web_TYPES.ImportModalController, () => {
return new ImportModalController(
this.get<Importer>(Web_TYPES.Importer),
this.get<NavigationController>(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<PurchaseFlowController>(Web_TYPES.PurchaseFlowController),
this.get<AccountMenuController>(Web_TYPES.AccountMenuController),
this.get<PreferencesController>(Web_TYPES.PreferencesController),
this.get<SyncStatusController>(Web_TYPES.SyncStatusController),
application.sync,
application.sessions,
application.subscriptions,
this.get<ToastService>(Web_TYPES.ToastService),
application.user,
)
})
}
}

View File

@@ -10,7 +10,7 @@ export class DevMode {
/** Valid only when running a mock event publisher on port 3124 */ /** Valid only when running a mock event publisher on port 3124 */
async purchaseMockSubscription() { async purchaseMockSubscription() {
const subscriptionId = 2002 const subscriptionId = 2002
const email = this.application.getUser()?.email const email = this.application.sessions.getUser()?.email
const response = await fetch('http://localhost:3124/events', { const response = await fetch('http://localhost:3124/events', {
method: 'POST', method: 'POST',
headers: { headers: {

View File

@@ -90,7 +90,7 @@ export class DesktopManager
} }
} }
async saveDesktopBackup() { async saveDesktopBackup(): Promise<void> {
this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload) this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload)
const data = await this.getBackupFile() const data = await this.getBackupFile()
@@ -149,12 +149,12 @@ export class DesktopManager
} }
} }
searchText(text?: string) { searchText(text?: string): void {
this.lastSearchedText = text this.lastSearchedText = text
this.device.onSearch(text) this.device.onSearch(text)
} }
redoSearch() { redoSearch(): void {
if (this.lastSearchedText) { if (this.lastSearchedText) {
this.searchText(this.lastSearchedText) this.searchText(this.lastSearchedText)
} }
@@ -188,18 +188,20 @@ export class DesktopManager
return return
} }
const updatedComponent = await this.application.changeAndSaveItem( const updatedComponent = (
component, await this.application.changeAndSaveItem.execute(
(m) => { component,
const mutator = m as ComponentMutator (m) => {
// eslint-disable-next-line camelcase const mutator = m as ComponentMutator
mutator.local_url = componentData.content.local_url as string // eslint-disable-next-line camelcase
// eslint-disable-next-line camelcase mutator.local_url = componentData.content.local_url as string
mutator.package_info = componentData.content.package_info // eslint-disable-next-line camelcase
mutator.setAppDataItem(AppDataField.ComponentInstallError, undefined) mutator.package_info = componentData.content.package_info
}, mutator.setAppDataItem(AppDataField.ComponentInstallError, undefined)
undefined, },
) undefined,
)
).getValue()
for (const observer of this.updateObservers) { for (const observer of this.updateObservers) {
observer.callback(updatedComponent as SNComponent) observer.callback(updatedComponent as SNComponent)

View File

@@ -0,0 +1,25 @@
import { isDesktopApplication } from '@/Utils'
import { ApplicationInterface, LegacyApiServiceInterface, Result, UseCaseInterface } from '@standardnotes/snjs'
export class GetPurchaseFlowUrl implements UseCaseInterface<string> {
constructor(
private application: ApplicationInterface,
private legacyApi: LegacyApiServiceInterface,
) {}
async execute(): Promise<Result<string>> {
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.')
}
}

View File

@@ -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<ReturnType> {
private _isNativeMobileWeb = new IsNativeMobileWeb(this.environment)
constructor(private environment: Environment) {}
execute(): Result<ReturnType> {
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,
})
}
}

View File

@@ -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<void> {
constructor(
private application: WebApplicationInterface,
private _getPurchaseFlowUrl: GetPurchaseFlowUrl,
) {}
async execute(): Promise<Result<void>> {
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.')
}
}

View File

@@ -0,0 +1,33 @@
import { Environment, LegacyApiServiceInterface, Result, UseCaseInterface } from '@standardnotes/snjs'
import { WebApplicationInterface } from '@standardnotes/ui-services'
export class OpenSubscriptionDashboard implements UseCaseInterface<void> {
constructor(
private application: WebApplicationInterface,
private legacyApi: LegacyApiServiceInterface,
) {}
async execute(): Promise<Result<void>> {
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()
}
}

View File

@@ -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<AppPaneId[]> {
constructor(private _isTabletOrMobileScreen: IsTabletOrMobileScreen) {}
execute(layout: PaneLayout): Result<AppPaneId[]> {
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')
}
}

View File

@@ -1,11 +1,9 @@
import { WebCrypto } from '@/Application/Crypto' import { WebCrypto } from '@/Application/Crypto'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice' import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice'
import { import {
DeinitSource, DeinitSource,
Platform, Platform,
SNApplication, SNApplication,
removeFromArray,
DesktopDeviceInterface, DesktopDeviceInterface,
isDesktopDevice, isDesktopDevice,
DeinitMode, DeinitMode,
@@ -19,57 +17,82 @@ import {
DecryptedItem, DecryptedItem,
Environment, Environment,
ApplicationOptionsDefaults, ApplicationOptionsDefaults,
BackupServiceInterface,
InternalFeatureService, InternalFeatureService,
InternalFeatureServiceInterface, InternalFeatureServiceInterface,
PrefDefaults,
NoteContent, NoteContent,
SNNote, SNNote,
DesktopManagerInterface,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { makeObservable, observable } from 'mobx' import { action, computed, makeObservable, observable } from 'mobx'
import { startAuthentication, startRegistration } from '@simplewebauthn/browser' import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
import { PanelResizedData } from '@/Types/PanelResizedData' import { PanelResizedData } from '@/Types/PanelResizedData'
import { getBlobFromBase64, isAndroid, isDesktopApplication, isDev, isIOS } from '@/Utils' import { getBlobFromBase64, isDesktopApplication, isDev } from '@/Utils'
import { DesktopManager } from './Device/DesktopManager'
import { import {
ArchiveManager, ArchiveManager,
AutolockService, AutolockService,
ChangelogService, ChangelogService,
Importer,
IsGlobalSpellcheckEnabled,
IsMobileDevice,
IsNativeIOS,
IsNativeMobileWeb,
KeyboardService, KeyboardService,
PreferenceId, PreferenceId,
RouteService,
RouteServiceInterface, RouteServiceInterface,
ThemeManager, ThemeManager,
VaultDisplayService,
VaultDisplayServiceInterface, VaultDisplayServiceInterface,
WebAlertService, WebAlertService,
WebApplicationInterface, WebApplicationInterface,
} from '@standardnotes/ui-services' } from '@standardnotes/ui-services'
import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver' import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver'
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
import { setCustomViewportHeight } from '@/setViewportHeightWithFallback' import { setCustomViewportHeight } from '@/setViewportHeightWithFallback'
import { WebServices } from './WebServices'
import { FeatureName } from '@/Controllers/FeatureName' import { FeatureName } from '@/Controllers/FeatureName'
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
import { VisibilityObserver } from './VisibilityObserver' import { VisibilityObserver } from './VisibilityObserver'
import { MomentsService } from '@/Controllers/Moments/MomentsService'
import { DevMode } from './DevMode' import { DevMode } from './DevMode'
import { ToastType, addToast, dismissToast } from '@standardnotes/toast' 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 type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
export class WebApplication extends SNApplication implements WebApplicationInterface { export class WebApplication extends SNApplication implements WebApplicationInterface {
public readonly itemControllerGroup: ItemGroupController readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
public readonly routeService: RouteServiceInterface
private readonly webServices!: WebServices private readonly deps = new WebDependencies(this)
private visibilityObserver?: VisibilityObserver
private readonly webEventObservers: WebEventObserver[] = [] private readonly webEventObservers: WebEventObserver[] = []
private readonly mobileWebReceiver?: MobileWebReceiver private disposers: (() => void)[] = []
private readonly androidBackHandler?: AndroidBackHandler
private readonly visibilityObserver?: VisibilityObserver
private readonly mobileAppEventObserver?: () => void
public readonly devMode?: DevMode public isSessionsModalVisible = false
public devMode?: DevMode
constructor( constructor(
deviceInterface: WebOrDesktopDevice, deviceInterface: WebOrDesktopDevice,
@@ -102,48 +125,49 @@ export class WebApplication extends SNApplication implements WebApplicationInter
) => Promise<Record<string, unknown>>, ) => Promise<Record<string, unknown>>,
}) })
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) { if (isDev) {
this.devMode = new DevMode(this) this.devMode = new DevMode(this)
} }
makeObservable(this, {
dealloced: observable,
})
if (!this.isNativeMobileWeb()) { if (!this.isNativeMobileWeb()) {
deviceInterface.setApplication(this) this.webOrDesktopDevice.setApplication(this)
} }
this.itemControllerGroup = new ItemGroupController(this) const appEventObserver = this.deps.get<ApplicationEventObserver>(Web_TYPES.ApplicationEventObserver)
this.routeService = new RouteService(this, this.events) this.disposers.push(this.addEventObserver(appEventObserver.handle.bind(appEventObserver)))
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)
if (this.isNativeMobileWeb()) { if (this.isNativeMobileWeb()) {
this.mobileWebReceiver = new MobileWebReceiver(this) this.disposers.push(
this.androidBackHandler = new AndroidBackHandler() this.addEventObserver(async (event) => {
this.mobileAppEventObserver = this.addEventObserver(async (event) => { this.mobileDevice.notifyApplicationEvent(event)
this.mobileDevice().notifyApplicationEvent(event) }),
}) )
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log = (...args) => { 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) super.deinit(mode, source)
if (!this.isNativeMobileWeb()) { 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 { 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 this.webEventObservers.length = 0
if (this.visibilityObserver) { if (this.visibilityObserver) {
this.visibilityObserver.deinit() this.visibilityObserver.deinit()
;(this.visibilityObserver as unknown) = undefined ;(this.visibilityObserver as unknown) = undefined
} }
if (this.mobileAppEventObserver) {
this.mobileAppEventObserver()
;(this.mobileAppEventObserver as unknown) = undefined
}
} catch (error) { } catch (error) {
console.error('Error while deiniting application', error) console.error('Error while deiniting application', error)
} }
@@ -225,46 +230,6 @@ export class WebApplication extends SNApplication implements WebApplicationInter
this.notifyWebEvent(WebAppEvent.PanelResized, data) 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 { public get desktopDevice(): DesktopDeviceInterface | undefined {
if (isDesktopDevice(this.device)) { if (isDesktopDevice(this.device)) {
return this.device return this.device
@@ -277,53 +242,42 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return InternalFeatureService.get() return InternalFeatureService.get()
} }
isNativeIOS() { isNativeIOS(): boolean {
return this.isNativeMobileWeb() && this.platform === Platform.Ios return this.deps.get<IsNativeIOS>(Web_TYPES.IsNativeIOS).execute().getValue()
} }
get isMobileDevice() { get isMobileDevice(): boolean {
return this.isNativeMobileWeb() || isIOS() || isAndroid() return this.deps.get<IsMobileDevice>(Web_TYPES.IsMobileDevice).execute().getValue()
} }
get hideOutboundSubscriptionLinks() { get hideOutboundSubscriptionLinks() {
return this.isNativeIOS() return this.isNativeIOS()
} }
mobileDevice(): MobileDeviceInterface { get mobileDevice(): MobileDeviceInterface {
if (!this.isNativeMobileWeb()) {
throw Error('Attempting to access device as mobile device on non mobile platform')
}
return this.device as MobileDeviceInterface return this.device as MobileDeviceInterface
} }
webOrDesktopDevice(): WebOrDesktopDevice { get webOrDesktopDevice(): WebOrDesktopDevice {
return this.device as WebOrDesktopDevice return this.device as WebOrDesktopDevice
} }
public getThemeService() { async checkForSecurityUpdate(): Promise<boolean> {
return this.webServices.themeService
}
public get keyboardService() {
return this.webServices.keyboardService
}
async checkForSecurityUpdate() {
return this.protocolUpgradeAvailable() return this.protocolUpgradeAvailable()
} }
performDesktopTextBackup(): void | Promise<void> { performDesktopTextBackup(): void | Promise<void> {
return this.getDesktopService()?.saveDesktopBackup() return this.desktopManager?.saveDesktopBackup()
} }
isGlobalSpellcheckEnabled(): boolean { isGlobalSpellcheckEnabled(): boolean {
return this.getPreference(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck]) return this.deps.get<IsGlobalSpellcheckEnabled>(Web_TYPES.IsGlobalSpellcheckEnabled).execute().getValue()
} }
public getItemTags(item: DecryptedItemInterface) { public getItemTags(item: DecryptedItemInterface) {
return this.items.itemsReferencingItem(item).filter((ref) => { return this.items.itemsReferencingItem<SNTag>(item).filter((ref) => {
return ref.content_type === ContentType.TYPES.Tag return ref.content_type === ContentType.TYPES.Tag
}) as SNTag[] })
} }
public get version(): string { public get version(): string {
@@ -349,15 +303,15 @@ export class WebApplication extends SNApplication implements WebApplicationInter
} }
if (this.protections.getMobileScreenshotPrivacyEnabled()) { if (this.protections.getMobileScreenshotPrivacyEnabled()) {
this.mobileDevice().setAndroidScreenshotPrivacy(true) this.mobileDevice.setAndroidScreenshotPrivacy(true)
} else { } else {
this.mobileDevice().setAndroidScreenshotPrivacy(false) this.mobileDevice.setAndroidScreenshotPrivacy(false)
} }
} }
async handleMobileLosingFocusEvent(): Promise<void> { async handleMobileLosingFocusEvent(): Promise<void> {
if (this.protections.getMobileScreenshotPrivacyEnabled()) { if (this.protections.getMobileScreenshotPrivacyEnabled()) {
this.mobileDevice().stopHidingMobileInterfaceFromScreenshots() this.mobileDevice.stopHidingMobileInterfaceFromScreenshots()
} }
await this.lockApplicationAfterMobileEventIfApplicable() await this.lockApplicationAfterMobileEventIfApplicable()
@@ -365,12 +319,20 @@ export class WebApplication extends SNApplication implements WebApplicationInter
async handleMobileResumingFromBackgroundEvent(): Promise<void> { async handleMobileResumingFromBackgroundEvent(): Promise<void> {
if (this.protections.getMobileScreenshotPrivacyEnabled()) { if (this.protections.getMobileScreenshotPrivacyEnabled()) {
this.mobileDevice().hideMobileInterfaceFromScreenshots() this.mobileDevice.hideMobileInterfaceFromScreenshots()
} }
} }
handleMobileColorSchemeChangeEvent() { handleMobileColorSchemeChangeEvent() {
void this.getThemeService().handleMobileColorSchemeChangeEvent() void this.themeManager.handleMobileColorSchemeChangeEvent()
}
openSessionsModal = () => {
this.isSessionsModalVisible = true
}
closeSessionsModal = () => {
this.isSessionsModalVisible = false
} }
handleMobileKeyboardWillChangeFrameEvent(frame: { handleMobileKeyboardWillChangeFrameEvent(frame: {
@@ -392,14 +354,14 @@ export class WebApplication extends SNApplication implements WebApplicationInter
} }
handleReceivedFileEvent(file: { name: string; mimeType: string; data: string }): void { 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 blob = getBlobFromBase64(file.data, file.mimeType)
const mappedFile = new File([blob], file.name, { type: file.mimeType }) const mappedFile = new File([blob], file.name, { type: file.mimeType })
filesController.uploadNewFile(mappedFile, true).catch(console.error) filesController.uploadNewFile(mappedFile, true).catch(console.error)
} }
async handleReceivedTextEvent({ text, title }: { text: string; title?: string | undefined }) { 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<NoteContent, SNNote>(ContentType.TYPES.Note, { const note = this.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, {
title: titleForNote, title: titleForNote,
@@ -409,7 +371,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
const insertedNote = await this.mutator.insertItem(note) 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({ addToast({
type: ToastType.Success, type: ToastType.Success,
@@ -437,7 +399,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
const file = new File([imgBlob], finalPath, { const file = new File([imgBlob], finalPath, {
type: imgBlob.type, type: imgBlob.type,
}) })
this.controllers.filesController.uploadNewFile(file, true).catch(console.error) this.filesController.uploadNewFile(file, true).catch(console.error)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {
@@ -453,7 +415,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
} }
private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> { private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> {
const isLocked = await this.isLocked() const isLocked = await this.protections.isLocked()
if (isLocked) { if (isLocked) {
return return
} }
@@ -469,7 +431,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
if (passcodeLockImmediately) { if (passcodeLockImmediately) {
await this.lock() await this.lock()
} else if (biometricsLockImmediately) { } else if (biometricsLockImmediately) {
this.softLockBiometrics() this.protections.softLockBiometrics()
} }
} }
@@ -494,7 +456,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
isAuthorizedToRenderItem(item: DecryptedItem): boolean { isAuthorizedToRenderItem(item: DecryptedItem): boolean {
if (item.protected && this.hasProtectionSources()) { if (item.protected && this.hasProtectionSources()) {
return this.hasUnprotectedAccessSession() return this.protections.hasUnprotectedAccessSession()
} }
return true return true
@@ -505,19 +467,19 @@ export class WebApplication extends SNApplication implements WebApplicationInter
} }
get entitledToFiles(): boolean { get entitledToFiles(): boolean {
return this.controllers.featuresController.entitledToFiles return this.featuresController.entitledToFiles
} }
showPremiumModal(featureName?: FeatureName): void { showPremiumModal(featureName?: FeatureName): void {
void this.controllers.featuresController.showPremiumAlert(featureName) void this.featuresController.showPremiumAlert(featureName)
} }
hasValidFirstPartySubscription(): boolean { hasValidFirstPartySubscription(): boolean {
return this.controllers.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription return this.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription
} }
async openPurchaseFlow() { async openPurchaseFlow() {
await this.controllers.purchaseFlowController.openPurchaseFlow() await this.purchaseFlowController.openPurchaseFlow()
} }
addNativeMobileEventListener = (listener: NativeMobileEventListener) => { addNativeMobileEventListener = (listener: NativeMobileEventListener) => {
@@ -529,11 +491,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
} }
showAccountMenu(): void { showAccountMenu(): void {
this.controllers.accountMenuController.setShow(true) this.accountMenuController.setShow(true)
} }
hideAccountMenu(): void { 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 { openPreferences(pane?: PreferenceId): void {
this.controllers.preferencesController.openPreferences() this.preferencesController.openPreferences()
if (pane) { if (pane) {
this.controllers.preferencesController.setCurrentPane(pane) this.preferencesController.setCurrentPane(pane)
} }
} }
generateUUID(): string { generateUUID(): string {
return this.options.crypto.generateUUID() return this.options.crypto.generateUUID()
} }
/**
* Dependency
* Accessors
*/
get routeService(): RouteServiceInterface {
return this.deps.get<RouteServiceInterface>(Web_TYPES.RouteService)
}
get androidBackHandler(): AndroidBackHandler {
return this.deps.get<AndroidBackHandler>(Web_TYPES.AndroidBackHandler)
}
get vaultDisplayService(): VaultDisplayServiceInterface {
return this.deps.get<VaultDisplayServiceInterface>(Web_TYPES.VaultDisplayService)
}
get desktopManager(): DesktopManagerInterface | undefined {
return this.deps.get<DesktopManagerInterface | undefined>(Web_TYPES.DesktopManager)
}
get autolockService(): AutolockService | undefined {
return this.deps.get<AutolockService | undefined>(Web_TYPES.AutolockService)
}
get archiveService(): ArchiveManager {
return this.deps.get<ArchiveManager>(Web_TYPES.ArchiveManager)
}
get paneController(): PaneController {
return this.deps.get<PaneController>(Web_TYPES.PaneController)
}
get linkingController(): LinkingController {
return this.deps.get<LinkingController>(Web_TYPES.LinkingController)
}
get changelogService(): ChangelogService {
return this.deps.get<ChangelogService>(Web_TYPES.ChangelogService)
}
get momentsService(): MomentsService {
return this.deps.get<MomentsService>(Web_TYPES.MomentsService)
}
get themeManager(): ThemeManager {
return this.deps.get<ThemeManager>(Web_TYPES.ThemeManager)
}
get keyboardService(): KeyboardService {
return this.deps.get<KeyboardService>(Web_TYPES.KeyboardService)
}
get featuresController(): FeaturesController {
return this.deps.get<FeaturesController>(Web_TYPES.FeaturesController)
}
get filesController(): FilesController {
return this.deps.get<FilesController>(Web_TYPES.FilesController)
}
get filePreviewModalController(): FilePreviewModalController {
return this.deps.get<FilePreviewModalController>(Web_TYPES.FilePreviewModalController)
}
get notesController(): NotesController {
return this.deps.get<NotesController>(Web_TYPES.NotesController)
}
get importModalController(): ImportModalController {
return this.deps.get<ImportModalController>(Web_TYPES.ImportModalController)
}
get navigationController(): NavigationController {
return this.deps.get<NavigationController>(Web_TYPES.NavigationController)
}
get historyModalController(): HistoryModalController {
return this.deps.get<HistoryModalController>(Web_TYPES.HistoryModalController)
}
get syncStatusController(): SyncStatusController {
return this.deps.get<SyncStatusController>(Web_TYPES.SyncStatusController)
}
get itemListController(): ItemListController {
return this.deps.get<ItemListController>(Web_TYPES.ItemListController)
}
get importer(): Importer {
return this.deps.get<Importer>(Web_TYPES.Importer)
}
get subscriptionController(): SubscriptionController {
return this.deps.get<SubscriptionController>(Web_TYPES.SubscriptionController)
}
get purchaseFlowController(): PurchaseFlowController {
return this.deps.get<PurchaseFlowController>(Web_TYPES.PurchaseFlowController)
}
get quickSettingsMenuController(): QuickSettingsController {
return this.deps.get<QuickSettingsController>(Web_TYPES.QuickSettingsController)
}
get persistence(): PersistenceService {
return this.deps.get<PersistenceService>(Web_TYPES.PersistenceService)
}
get itemControllerGroup(): ItemGroupController {
return this.deps.get<ItemGroupController>(Web_TYPES.ItemGroupController)
}
get noAccountWarningController(): NoAccountWarningController {
return this.deps.get<NoAccountWarningController>(Web_TYPES.NoAccountWarningController)
}
get searchOptionsController(): SearchOptionsController {
return this.deps.get<SearchOptionsController>(Web_TYPES.SearchOptionsController)
}
get vaultSelectionController(): VaultSelectionMenuController {
return this.deps.get<VaultSelectionMenuController>(Web_TYPES.VaultSelectionMenuController)
}
get openSubscriptionDashboard(): OpenSubscriptionDashboard {
return this.deps.get<OpenSubscriptionDashboard>(Web_TYPES.OpenSubscriptionDashboard)
}
get mobileWebReceiver(): MobileWebReceiver | undefined {
return this.deps.get<MobileWebReceiver | undefined>(Web_TYPES.MobileWebReceiver)
}
get accountMenuController(): AccountMenuController {
return this.deps.get<AccountMenuController>(Web_TYPES.AccountMenuController)
}
get preferencesController(): PreferencesController {
return this.deps.get<PreferencesController>(Web_TYPES.PreferencesController)
}
get isNativeMobileWebUseCase(): IsNativeMobileWeb {
return this.deps.get<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb)
}
} }

View File

@@ -43,7 +43,7 @@ export class WebApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice>
}) })
if (isDesktopApplication()) { if (isDesktopApplication()) {
window.webClient = (this.primaryApplication as WebApplication).getDesktopService() window.webClient = (this.primaryApplication as WebApplication).desktopManager
} }
} }

View File

@@ -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
}

View File

@@ -1,8 +1,7 @@
import { ApplicationEvent } from '@standardnotes/snjs' import { ApplicationEvent } from '@standardnotes/snjs'
import { WebApplication } from '@/Application/WebApplication'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { autorun, IReactionDisposer, IReactionPublic } from 'mobx' import { autorun, IReactionDisposer, IReactionPublic } from 'mobx'
import { Component } from 'react' import { Component } from 'react'
import { WebApplication } from '@/Application/WebApplication'
export type PureComponentState = Partial<Record<string, unknown>> export type PureComponentState = Partial<Record<string, unknown>>
export type PureComponentProps = Partial<Record<string, unknown>> export type PureComponentProps = Partial<Record<string, unknown>>
@@ -13,7 +12,7 @@ export abstract class AbstractComponent<P = PureComponentProps, S = PureComponen
constructor( constructor(
props: P, props: P,
protected application: WebApplication, public readonly application: WebApplication,
) { ) {
super(props) super(props)
} }
@@ -40,10 +39,6 @@ export abstract class AbstractComponent<P = PureComponentProps, S = PureComponen
this.deinit() this.deinit()
} }
public get viewControllerManager(): ViewControllerManager {
return this.application.controllers
}
autorun(view: (r: IReactionPublic) => void): void { autorun(view: (r: IReactionPublic) => void): void {
this.reactionDisposers.push(autorun(view)) this.reactionDisposers.push(autorun(view))
} }

View File

@@ -1,35 +1,30 @@
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { WebApplication } from '@/Application/WebApplication'
import { useCallback, FunctionComponent, KeyboardEventHandler } from 'react' import { useCallback, FunctionComponent, KeyboardEventHandler } from 'react'
import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { AccountMenuPane } from './AccountMenuPane' import { AccountMenuPane } from './AccountMenuPane'
import MenuPaneSelector from './MenuPaneSelector' import MenuPaneSelector from './MenuPaneSelector'
import { KeyboardKey } from '@standardnotes/ui-services' import { KeyboardKey } from '@standardnotes/ui-services'
import { useApplication } from '../ApplicationProvider'
export type AccountMenuProps = { export type AccountMenuProps = {
viewControllerManager: ViewControllerManager
application: WebApplication
onClickOutside: () => void onClickOutside: () => void
mainApplicationGroup: WebApplicationGroup mainApplicationGroup: WebApplicationGroup
} }
const AccountMenu: FunctionComponent<AccountMenuProps> = ({ const AccountMenu: FunctionComponent<AccountMenuProps> = ({ mainApplicationGroup }) => {
application, const application = useApplication()
viewControllerManager,
mainApplicationGroup, const { currentPane } = application.accountMenuController
}) => {
const { currentPane } = viewControllerManager.accountMenuController
const closeAccountMenu = useCallback(() => { const closeAccountMenu = useCallback(() => {
viewControllerManager.accountMenuController.closeAccountMenu() application.accountMenuController.closeAccountMenu()
}, [viewControllerManager]) }, [application])
const setCurrentPane = useCallback( const setCurrentPane = useCallback(
(pane: AccountMenuPane) => { (pane: AccountMenuPane) => {
viewControllerManager.accountMenuController.setCurrentPane(pane) application.accountMenuController.setCurrentPane(pane)
}, },
[viewControllerManager], [application],
) )
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback( const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback(
@@ -50,8 +45,6 @@ const AccountMenu: FunctionComponent<AccountMenuProps> = ({
return ( return (
<div id="account-menu" className="sn-component" onKeyDown={handleKeyDown}> <div id="account-menu" className="sn-component" onKeyDown={handleKeyDown}>
<MenuPaneSelector <MenuPaneSelector
viewControllerManager={viewControllerManager}
application={application}
mainApplicationGroup={mainApplicationGroup} mainApplicationGroup={mainApplicationGroup}
menuPane={currentPane} menuPane={currentPane}
setMenuPane={setCurrentPane} setMenuPane={setCurrentPane}

View File

@@ -1,14 +1,11 @@
import { WebApplication } from '@/Application/WebApplication'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react' import { ChangeEventHandler, FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react'
import Checkbox from '@/Components/Checkbox/Checkbox' import Checkbox from '@/Components/Checkbox/Checkbox'
import DecoratedInput from '@/Components/Input/DecoratedInput' import DecoratedInput from '@/Components/Input/DecoratedInput'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { useApplication } from '../ApplicationProvider'
type Props = { type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
disabled?: boolean disabled?: boolean
onPrivateUsernameModeChange?: (isPrivate: boolean, identifier?: string) => void onPrivateUsernameModeChange?: (isPrivate: boolean, identifier?: string) => void
onStrictSignInChange?: (isStrictSignIn: boolean) => void onStrictSignInChange?: (isStrictSignIn: boolean) => void
@@ -17,15 +14,15 @@ type Props = {
} }
const AdvancedOptions: FunctionComponent<Props> = ({ const AdvancedOptions: FunctionComponent<Props> = ({
viewControllerManager,
application,
disabled = false, disabled = false,
onPrivateUsernameModeChange, onPrivateUsernameModeChange,
onStrictSignInChange, onStrictSignInChange,
onRecoveryCodesChange, onRecoveryCodesChange,
children, children,
}) => { }) => {
const { server, setServer, enableServerOption, setEnableServerOption } = viewControllerManager.accountMenuController const application = useApplication()
const { server, setServer, enableServerOption, setEnableServerOption } = application.accountMenuController
const [showAdvanced, setShowAdvanced] = useState(false) const [showAdvanced, setShowAdvanced] = useState(false)
const [isPrivateUsername, setIsPrivateUsername] = useState(false) const [isPrivateUsername, setIsPrivateUsername] = useState(false)

View File

@@ -1,6 +1,4 @@
import { STRING_NON_MATCHING_PASSWORDS } from '@/Constants/Strings' 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 { observer } from 'mobx-react-lite'
import { import {
FormEventHandler, FormEventHandler,
@@ -17,23 +15,18 @@ import Checkbox from '@/Components/Checkbox/Checkbox'
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput' import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import IconButton from '@/Components/Button/IconButton' import IconButton from '@/Components/Button/IconButton'
import { useApplication } from '../ApplicationProvider'
type Props = { type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
setMenuPane: (pane: AccountMenuPane) => void setMenuPane: (pane: AccountMenuPane) => void
email: string email: string
password: string password: string
} }
const ConfirmPassword: FunctionComponent<Props> = ({ const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, password }) => {
application, const application = useApplication()
viewControllerManager,
setMenuPane, const { notesAndTagsCount } = application.accountMenuController
email,
password,
}) => {
const { notesAndTagsCount } = viewControllerManager.accountMenuController
const [confirmPassword, setConfirmPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('')
const [isRegistering, setIsRegistering] = useState(false) const [isRegistering, setIsRegistering] = useState(false)
const [isEphemeral, setIsEphemeral] = useState(false) const [isEphemeral, setIsEphemeral] = useState(false)
@@ -72,8 +65,8 @@ const ConfirmPassword: FunctionComponent<Props> = ({
application application
.register(email, password, isEphemeral, shouldMergeLocal) .register(email, password, isEphemeral, shouldMergeLocal)
.then(() => { .then(() => {
viewControllerManager.accountMenuController.closeAccountMenu() application.accountMenuController.closeAccountMenu()
viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu) application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
}) })
.catch((err) => { .catch((err) => {
console.error(err) console.error(err)
@@ -88,7 +81,7 @@ const ConfirmPassword: FunctionComponent<Props> = ({
passwordInputRef.current?.focus() passwordInputRef.current?.focus()
} }
}, },
[viewControllerManager, application, confirmPassword, email, isEphemeral, password, shouldMergeLocal], [application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
) )
const handleKeyDown: KeyboardEventHandler = useCallback( const handleKeyDown: KeyboardEventHandler = useCallback(

View File

@@ -1,5 +1,3 @@
import { WebApplication } from '@/Application/WebApplication'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { import {
FormEventHandler, FormEventHandler,
@@ -20,8 +18,6 @@ import AdvancedOptions from './AdvancedOptions'
import HorizontalSeparator from '../Shared/HorizontalSeparator' import HorizontalSeparator from '../Shared/HorizontalSeparator'
type Props = { type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
setMenuPane: (pane: AccountMenuPane) => void setMenuPane: (pane: AccountMenuPane) => void
email: string email: string
setEmail: React.Dispatch<React.SetStateAction<string>> setEmail: React.Dispatch<React.SetStateAction<string>>
@@ -29,15 +25,7 @@ type Props = {
setPassword: React.Dispatch<React.SetStateAction<string>> setPassword: React.Dispatch<React.SetStateAction<string>>
} }
const CreateAccount: FunctionComponent<Props> = ({ const CreateAccount: FunctionComponent<Props> = ({ setMenuPane, email, setEmail, password, setPassword }) => {
viewControllerManager,
application,
setMenuPane,
email,
setEmail,
password,
setPassword,
}) => {
const emailInputRef = useRef<HTMLInputElement>(null) const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null) const passwordInputRef = useRef<HTMLInputElement>(null)
const [isPrivateUsername, setIsPrivateUsername] = useState(false) const [isPrivateUsername, setIsPrivateUsername] = useState(false)
@@ -145,11 +133,7 @@ const CreateAccount: FunctionComponent<Props> = ({
<Button className="mt-1" label="Next" primary onClick={handleRegisterFormSubmit} fullWidth={true} /> <Button className="mt-1" label="Next" primary onClick={handleRegisterFormSubmit} fullWidth={true} />
</form> </form>
<HorizontalSeparator classes="my-2" /> <HorizontalSeparator classes="my-2" />
<AdvancedOptions <AdvancedOptions onPrivateUsernameModeChange={onPrivateUsernameChange} />
application={application}
viewControllerManager={viewControllerManager}
onPrivateUsernameModeChange={onPrivateUsernameChange}
/>
</> </>
) )
} }

View File

@@ -1,5 +1,3 @@
import { WebApplication } from '@/Application/WebApplication'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { SyncQueueStrategy } from '@standardnotes/snjs' import { SyncQueueStrategy } from '@standardnotes/snjs'
@@ -14,10 +12,9 @@ import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { formatLastSyncDate } from '@/Utils/DateUtils' import { formatLastSyncDate } from '@/Utils/DateUtils'
import Spinner from '@/Components/Spinner/Spinner' import Spinner from '@/Components/Spinner/Spinner'
import { MenuItemIconSize } from '@/Constants/TailwindClassNames' import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
import { useApplication } from '../ApplicationProvider'
type Props = { type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
mainApplicationGroup: WebApplicationGroup mainApplicationGroup: WebApplicationGroup
setMenuPane: (pane: AccountMenuPane) => void setMenuPane: (pane: AccountMenuPane) => void
closeMenu: () => void closeMenu: () => void
@@ -25,13 +22,9 @@ type Props = {
const iconClassName = `text-neutral mr-2 ${MenuItemIconSize}` const iconClassName = `text-neutral mr-2 ${MenuItemIconSize}`
const GeneralAccountMenu: FunctionComponent<Props> = ({ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu, mainApplicationGroup }) => {
application, const application = useApplication()
viewControllerManager,
setMenuPane,
closeMenu,
mainApplicationGroup,
}) => {
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false) const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date)) const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
@@ -58,23 +51,23 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
}) })
}, [application]) }, [application])
const user = useMemo(() => application.getUser(), [application]) const user = useMemo(() => application.sessions.getUser(), [application])
const openPreferences = useCallback(() => { const openPreferences = useCallback(() => {
viewControllerManager.accountMenuController.closeAccountMenu() application.accountMenuController.closeAccountMenu()
viewControllerManager.preferencesController.setCurrentPane('account') application.preferencesController.setCurrentPane('account')
viewControllerManager.preferencesController.openPreferences() application.preferencesController.openPreferences()
}, [viewControllerManager]) }, [application])
const openHelp = useCallback(() => { const openHelp = useCallback(() => {
viewControllerManager.accountMenuController.closeAccountMenu() application.accountMenuController.closeAccountMenu()
viewControllerManager.preferencesController.setCurrentPane('help-feedback') application.preferencesController.setCurrentPane('help-feedback')
viewControllerManager.preferencesController.openPreferences() application.preferencesController.openPreferences()
}, [viewControllerManager]) }, [application])
const signOut = useCallback(() => { const signOut = useCallback(() => {
viewControllerManager.accountMenuController.setSigningOut(true) application.accountMenuController.setSigningOut(true)
}, [viewControllerManager]) }, [application])
const activateRegisterPane = useCallback(() => { const activateRegisterPane = useCallback(() => {
setMenuPane(AccountMenuPane.Register) setMenuPane(AccountMenuPane.Register)
@@ -100,7 +93,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
<div className="mb-3 px-3 text-lg text-foreground lg:text-sm"> <div className="mb-3 px-3 text-lg text-foreground lg:text-sm">
<div>You're signed in as:</div> <div>You're signed in as:</div>
<div className="wrap my-0.5 font-bold">{user.email}</div> <div className="wrap my-0.5 font-bold">{user.email}</div>
<span className="text-neutral">{application.getHost()}</span> <span className="text-neutral">{application.getHost.execute().getValue()}</span>
</div> </div>
<div className="mb-2 flex items-start justify-between px-3 text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item"> <div className="mb-2 flex items-start justify-between px-3 text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item">
{isSyncingInProgress ? ( {isSyncingInProgress ? (
@@ -137,16 +130,13 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
</> </>
)} )}
<Menu <Menu
isOpen={viewControllerManager.accountMenuController.show} isOpen={application.accountMenuController.show}
a11yLabel="General account menu" a11yLabel="General account menu"
closeMenu={closeMenu} closeMenu={closeMenu}
initialFocus={!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX} initialFocus={!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX}
> >
<MenuItemSeparator /> <MenuItemSeparator />
<WorkspaceSwitcherOption <WorkspaceSwitcherOption mainApplicationGroup={mainApplicationGroup} />
mainApplicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
/>
<MenuItemSeparator /> <MenuItemSeparator />
{user ? ( {user ? (
<MenuItem onClick={openPreferences}> <MenuItem onClick={openPreferences}>
@@ -167,8 +157,8 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
)} )}
<MenuItem <MenuItem
onClick={() => { onClick={() => {
viewControllerManager.importModalController.setIsVisible(true) application.importModalController.setIsVisible(true)
viewControllerManager.accountMenuController.closeAccountMenu() application.accountMenuController.closeAccountMenu()
}} }}
> >
<Icon type="archive" className={iconClassName} /> <Icon type="archive" className={iconClassName} />

View File

@@ -1,6 +1,4 @@
import { WebApplication } from '@/Application/WebApplication'
import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useState } from 'react' import { FunctionComponent, useState } from 'react'
import { AccountMenuPane } from './AccountMenuPane' import { AccountMenuPane } from './AccountMenuPane'
@@ -10,22 +8,13 @@ import GeneralAccountMenu from './GeneralAccountMenu'
import SignInPane from './SignIn' import SignInPane from './SignIn'
type Props = { type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
mainApplicationGroup: WebApplicationGroup mainApplicationGroup: WebApplicationGroup
menuPane: AccountMenuPane menuPane: AccountMenuPane
setMenuPane: (pane: AccountMenuPane) => void setMenuPane: (pane: AccountMenuPane) => void
closeMenu: () => void closeMenu: () => void
} }
const MenuPaneSelector: FunctionComponent<Props> = ({ const MenuPaneSelector: FunctionComponent<Props> = ({ menuPane, setMenuPane, closeMenu, mainApplicationGroup }) => {
application,
viewControllerManager,
menuPane,
setMenuPane,
closeMenu,
mainApplicationGroup,
}) => {
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
@@ -33,22 +22,16 @@ const MenuPaneSelector: FunctionComponent<Props> = ({
case AccountMenuPane.GeneralMenu: case AccountMenuPane.GeneralMenu:
return ( return (
<GeneralAccountMenu <GeneralAccountMenu
viewControllerManager={viewControllerManager}
application={application}
mainApplicationGroup={mainApplicationGroup} mainApplicationGroup={mainApplicationGroup}
setMenuPane={setMenuPane} setMenuPane={setMenuPane}
closeMenu={closeMenu} closeMenu={closeMenu}
/> />
) )
case AccountMenuPane.SignIn: case AccountMenuPane.SignIn:
return ( return <SignInPane setMenuPane={setMenuPane} />
<SignInPane viewControllerManager={viewControllerManager} application={application} setMenuPane={setMenuPane} />
)
case AccountMenuPane.Register: case AccountMenuPane.Register:
return ( return (
<CreateAccount <CreateAccount
viewControllerManager={viewControllerManager}
application={application}
setMenuPane={setMenuPane} setMenuPane={setMenuPane}
email={email} email={email}
setEmail={setEmail} setEmail={setEmail}
@@ -57,15 +40,7 @@ const MenuPaneSelector: FunctionComponent<Props> = ({
/> />
) )
case AccountMenuPane.ConfirmPassword: case AccountMenuPane.ConfirmPassword:
return ( return <ConfirmPassword setMenuPane={setMenuPane} email={email} password={password} />
<ConfirmPassword
viewControllerManager={viewControllerManager}
application={application}
setMenuPane={setMenuPane}
email={email}
password={password}
/>
)
} }
} }

View File

@@ -1,5 +1,3 @@
import { WebApplication } from '@/Application/WebApplication'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { isDev } from '@/Utils' import { isDev } from '@/Utils'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import React, { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react' import React, { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react'
@@ -13,15 +11,16 @@ import IconButton from '@/Components/Button/IconButton'
import AdvancedOptions from './AdvancedOptions' import AdvancedOptions from './AdvancedOptions'
import HorizontalSeparator from '../Shared/HorizontalSeparator' import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/snjs' import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/snjs'
import { useApplication } from '../ApplicationProvider'
type Props = { type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
setMenuPane: (pane: AccountMenuPane) => void setMenuPane: (pane: AccountMenuPane) => void
} }
const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManager, setMenuPane }) => { const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
const { notesAndTagsCount } = viewControllerManager.accountMenuController const application = useApplication()
const { notesAndTagsCount } = application.accountMenuController
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [recoveryCodes, setRecoveryCodes] = useState('') const [recoveryCodes, setRecoveryCodes] = useState('')
@@ -101,7 +100,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
if (isErrorResponse(response)) { if (isErrorResponse(response)) {
throw new Error(getErrorFromErrorResponse(response).message) throw new Error(getErrorFromErrorResponse(response).message)
} }
viewControllerManager.accountMenuController.closeAccountMenu() application.accountMenuController.closeAccountMenu()
}) })
.catch((err) => { .catch((err) => {
console.error(err) console.error(err)
@@ -112,7 +111,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
.finally(() => { .finally(() => {
setIsSigningIn(false) setIsSigningIn(false)
}) })
}, [viewControllerManager, application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal]) }, [application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
const recoverySignIn = useCallback(() => { const recoverySignIn = useCallback(() => {
setIsSigningIn(true) setIsSigningIn(true)
@@ -129,7 +128,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
if (result.isFailed()) { if (result.isFailed()) {
throw new Error(result.getError()) throw new Error(result.getError())
} }
viewControllerManager.accountMenuController.closeAccountMenu() application.accountMenuController.closeAccountMenu()
}) })
.catch((err) => { .catch((err) => {
console.error(err) console.error(err)
@@ -140,7 +139,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
.finally(() => { .finally(() => {
setIsSigningIn(false) setIsSigningIn(false)
}) })
}, [viewControllerManager, application, email, password, recoveryCodes]) }, [application, email, password, recoveryCodes])
const onPrivateUsernameChange = useCallback( const onPrivateUsernameChange = useCallback(
(newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => { (newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
@@ -251,8 +250,6 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
</div> </div>
<HorizontalSeparator classes="my-2" /> <HorizontalSeparator classes="my-2" />
<AdvancedOptions <AdvancedOptions
viewControllerManager={viewControllerManager}
application={application}
disabled={isSigningIn} disabled={isSigningIn}
onPrivateUsernameModeChange={onPrivateUsernameChange} onPrivateUsernameModeChange={onPrivateUsernameChange}
onStrictSignInChange={handleStrictSigninChange} onStrictSignInChange={handleStrictSigninChange}

View File

@@ -1,25 +1,20 @@
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { WebApplication } from '@/Application/WebApplication'
import { User as UserType } from '@standardnotes/snjs' import { User as UserType } from '@standardnotes/snjs'
import { useApplication } from '../ApplicationProvider'
type Props = { const User = () => {
viewControllerManager: ViewControllerManager const application = useApplication()
application: WebApplication
}
const User = ({ viewControllerManager, application }: Props) => { const { server } = application.accountMenuController
const { server } = viewControllerManager.accountMenuController const user = application.sessions.getUser() as UserType
const user = application.getUser() as UserType
return ( return (
<div className="sk-panel-section"> <div className="sk-panel-section">
{viewControllerManager.syncStatusController.errorMessage && ( {application.syncStatusController.errorMessage && (
<div className="sk-notification danger"> <div className="sk-notification danger">
<div className="sk-notification-title">Sync Unreachable</div> <div className="sk-notification-title">Sync Unreachable</div>
<div className="sk-notification-text"> <div className="sk-notification-text">
Hmm...we can't seem to sync your account. The reason:{' '} Hmm...we can't seem to sync your account. The reason: {application.syncStatusController.errorMessage}
{viewControllerManager.syncStatusController.errorMessage}
</div> </div>
<a <a
className="sk-a info-contrast sk-bold sk-panel-row" className="sk-a info-contrast sk-bold sk-panel-row"

View File

@@ -1,5 +1,4 @@
import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { ApplicationDescriptor, ApplicationGroupEvent, ButtonType } from '@standardnotes/snjs' import { ApplicationDescriptor, ApplicationGroupEvent, ButtonType } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useState } from 'react' import { FunctionComponent, useCallback, useEffect, useState } from 'react'
@@ -8,20 +7,21 @@ import Menu from '@/Components/Menu/Menu'
import MenuItem from '@/Components/Menu/MenuItem' import MenuItem from '@/Components/Menu/MenuItem'
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator' import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
import WorkspaceMenuItem from './WorkspaceMenuItem' import WorkspaceMenuItem from './WorkspaceMenuItem'
import { useApplication } from '@/Components/ApplicationProvider'
type Props = { type Props = {
mainApplicationGroup: WebApplicationGroup mainApplicationGroup: WebApplicationGroup
viewControllerManager: ViewControllerManager
isOpen: boolean isOpen: boolean
hideWorkspaceOptions?: boolean hideWorkspaceOptions?: boolean
} }
const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({ const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
mainApplicationGroup, mainApplicationGroup,
viewControllerManager,
isOpen, isOpen,
hideWorkspaceOptions = false, hideWorkspaceOptions = false,
}: Props) => { }: Props) => {
const application = useApplication()
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>( const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>(
mainApplicationGroup.getDescriptors(), mainApplicationGroup.getDescriptors(),
) )
@@ -43,7 +43,7 @@ const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
}, [mainApplicationGroup]) }, [mainApplicationGroup])
const signoutAll = useCallback(async () => { const signoutAll = useCallback(async () => {
const confirmed = await viewControllerManager.application.alerts.confirm( const confirmed = await application.alerts.confirm(
'Are you sure you want to sign out of all workspaces on this device?', 'Are you sure you want to sign out of all workspaces on this device?',
undefined, undefined,
'Sign out all', 'Sign out all',
@@ -53,11 +53,11 @@ const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
return return
} }
mainApplicationGroup.signOutAllWorkspaces().catch(console.error) mainApplicationGroup.signOutAllWorkspaces().catch(console.error)
}, [mainApplicationGroup, viewControllerManager]) }, [mainApplicationGroup, application])
const destroyWorkspace = useCallback(() => { const destroyWorkspace = useCallback(() => {
viewControllerManager.accountMenuController.setSigningOut(true) application.accountMenuController.setSigningOut(true)
}, [viewControllerManager]) }, [application])
const activateWorkspace = useCallback( const activateWorkspace = useCallback(
async (descriptor: ApplicationDescriptor) => { async (descriptor: ApplicationDescriptor) => {

View File

@@ -1,6 +1,5 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useRef, useState } from 'react' import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
@@ -11,10 +10,9 @@ import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
type Props = { type Props = {
mainApplicationGroup: WebApplicationGroup mainApplicationGroup: WebApplicationGroup
viewControllerManager: ViewControllerManager
} }
const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => { const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGroup }) => {
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
@@ -40,11 +38,7 @@ const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGrou
side="right" side="right"
togglePopover={toggleMenu} togglePopover={toggleMenu}
> >
<WorkspaceSwitcherMenu <WorkspaceSwitcherMenu mainApplicationGroup={mainApplicationGroup} isOpen={isOpen} />
mainApplicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
isOpen={isOpen}
/>
</Popover> </Popover>
</> </>
) )

View File

@@ -1,7 +1,7 @@
import { WebApplicationGroup } from '@/Application/WebApplicationGroup' import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { getPlatformString, isIOS } from '@/Utils' import { getPlatformString } from '@/Utils'
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs' import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
import { alertDialog, RouteType } from '@standardnotes/ui-services' import { alertDialog, isIOS, RouteType } from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/WebApplication' import { WebApplication } from '@/Application/WebApplication'
import Footer from '@/Components/Footer/Footer' import Footer from '@/Components/Footer/Footer'
import SessionsModal from '@/Components/SessionsModal/SessionsModal' import SessionsModal from '@/Components/SessionsModal/SessionsModal'
@@ -30,6 +30,7 @@ import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider'
import ImportModal from '../ImportModal/ImportModal' import ImportModal from '../ImportModal/ImportModal'
import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose' import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose'
import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal' import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal'
import { ProtectionEvent } from '@standardnotes/services'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -47,10 +48,8 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
const currentWriteErrorDialog = useRef<Promise<void> | null>(null) const currentWriteErrorDialog = useRef<Promise<void> | null>(null)
const currentLoadErrorDialog = useRef<Promise<void> | null>(null) const currentLoadErrorDialog = useRef<Promise<void> | null>(null)
const viewControllerManager = application.controllers
useEffect(() => { useEffect(() => {
const desktopService = application.getDesktopService() const desktopService = application.desktopManager
if (desktopService) { if (desktopService) {
application.componentManager.setDesktopManager(desktopService) application.componentManager.setDesktopManager(desktopService)
@@ -142,10 +141,6 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
}) })
.catch(console.error) .catch(console.error)
} }
} else if (eventName === ApplicationEvent.BiometricsSoftLockEngaged) {
setNeedsUnlock(true)
} else if (eventName === ApplicationEvent.BiometricsSoftLockDisengaged) {
setNeedsUnlock(false)
} }
}) })
@@ -154,10 +149,22 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
} }
}, [application, onAppLaunch, onAppStart]) }, [application, onAppLaunch, onAppStart])
useEffect(() => {
const disposer = application.protections.addEventObserver(async (eventName) => {
if (eventName === ProtectionEvent.BiometricsSoftLockEngaged) {
setNeedsUnlock(true)
} else if (eventName === ProtectionEvent.BiometricsSoftLockDisengaged) {
setNeedsUnlock(false)
}
})
return disposer
}, [application])
useEffect(() => { useEffect(() => {
const removeObserver = application.addWebEventObserver(async (eventName) => { const removeObserver = application.addWebEventObserver(async (eventName) => {
if (eventName === WebAppEvent.WindowDidFocus) { if (eventName === WebAppEvent.WindowDidFocus) {
if (!(await application.isLocked())) { if (!(await application.protections.isLocked())) {
application.sync.sync().catch(console.error) application.sync.sync().catch(console.error)
} }
} }
@@ -178,14 +185,13 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<ChallengeModal <ChallengeModal
key={`${challenge.id}${application.ephemeralIdentifier}`} key={`${challenge.id}${application.ephemeralIdentifier}`}
application={application} application={application}
viewControllerManager={viewControllerManager}
mainApplicationGroup={mainApplicationGroup} mainApplicationGroup={mainApplicationGroup}
challenge={challenge} challenge={challenge}
onDismiss={removeChallenge} onDismiss={removeChallenge}
/> />
</div> </div>
)) ))
}, [viewControllerManager, challenges, mainApplicationGroup, removeChallenge, application]) }, [challenges, mainApplicationGroup, removeChallenge, application])
if (!renderAppContents) { if (!renderAppContents) {
return <AndroidBackHandlerProvider application={application}>{renderChallenges()}</AndroidBackHandlerProvider> return <AndroidBackHandlerProvider application={application}>{renderChallenges()}</AndroidBackHandlerProvider>
@@ -198,23 +204,13 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<ApplicationProvider application={application}> <ApplicationProvider application={application}>
<CommandProvider service={application.keyboardService}> <CommandProvider service={application.keyboardService}>
<AndroidBackHandlerProvider application={application}> <AndroidBackHandlerProvider application={application}>
<ResponsivePaneProvider paneController={application.controllers.paneController}> <ResponsivePaneProvider paneController={application.paneController}>
<PremiumModalProvider <PremiumModalProvider application={application}>
application={application} <LinkingControllerProvider controller={application.linkingController}>
featuresController={viewControllerManager.featuresController} <FileDragNDropProvider application={application}>
> <LazyLoadedClipperView applicationGroup={mainApplicationGroup} />
<LinkingControllerProvider controller={viewControllerManager.linkingController}>
<FileDragNDropProvider
application={application}
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
>
<LazyLoadedClipperView
viewControllerManager={viewControllerManager}
applicationGroup={mainApplicationGroup}
/>
<ToastContainer /> <ToastContainer />
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} /> <FilePreviewModalWrapper application={application} />
{renderChallenges()} {renderChallenges()}
</FileDragNDropProvider> </FileDragNDropProvider>
</LinkingControllerProvider> </LinkingControllerProvider>
@@ -230,64 +226,38 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<ApplicationProvider application={application}> <ApplicationProvider application={application}>
<CommandProvider service={application.keyboardService}> <CommandProvider service={application.keyboardService}>
<AndroidBackHandlerProvider application={application}> <AndroidBackHandlerProvider application={application}>
<ResponsivePaneProvider paneController={application.controllers.paneController}> <ResponsivePaneProvider paneController={application.paneController}>
<PremiumModalProvider <PremiumModalProvider application={application}>
application={application} <LinkingControllerProvider controller={application.linkingController}>
featuresController={viewControllerManager.featuresController}
>
<LinkingControllerProvider controller={viewControllerManager.linkingController}>
<div className={platformString + ' main-ui-view sn-component h-full'}> <div className={platformString + ' main-ui-view sn-component h-full'}>
<FileDragNDropProvider <FileDragNDropProvider application={application}>
application={application}
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
>
<PanesSystemComponent /> <PanesSystemComponent />
</FileDragNDropProvider> </FileDragNDropProvider>
<> <>
<Footer application={application} applicationGroup={mainApplicationGroup} /> <Footer application={application} applicationGroup={mainApplicationGroup} />
<SessionsModal application={application} viewControllerManager={viewControllerManager} /> <SessionsModal application={application} />
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} /> <PreferencesViewWrapper application={application} />
<RevisionHistoryModal <RevisionHistoryModal application={application} />
application={application}
historyModalController={viewControllerManager.historyModalController}
selectionController={viewControllerManager.selectionController}
/>
</> </>
{renderChallenges()} {renderChallenges()}
<> <>
<NotesContextMenu <NotesContextMenu />
navigationController={viewControllerManager.navigationController}
notesController={viewControllerManager.notesController}
linkingController={viewControllerManager.linkingController}
historyModalController={viewControllerManager.historyModalController}
selectionController={viewControllerManager.selectionController}
/>
<TagContextMenuWrapper <TagContextMenuWrapper
navigationController={viewControllerManager.navigationController} navigationController={application.navigationController}
featuresController={viewControllerManager.featuresController} featuresController={application.featuresController}
/> />
<FileContextMenuWrapper <FileContextMenuWrapper
filesController={viewControllerManager.filesController} filesController={application.filesController}
selectionController={viewControllerManager.selectionController} itemListController={application.itemListController}
navigationController={viewControllerManager.navigationController}
linkingController={viewControllerManager.linkingController}
/>
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
<ConfirmSignoutContainer
applicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
application={application}
/> />
<PurchaseFlowWrapper application={application} />
<ConfirmSignoutContainer applicationGroup={mainApplicationGroup} application={application} />
<ToastContainer /> <ToastContainer />
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} /> <FilePreviewModalWrapper application={application} />
<PermissionsModalWrapper application={application} /> <PermissionsModalWrapper application={application} />
<EditorWidthSelectionModalWrapper /> <EditorWidthSelectionModalWrapper />
<ConfirmDeleteAccountContainer <ConfirmDeleteAccountContainer application={application} />
application={application} <ImportModal importModalController={application.importModalController} />
viewControllerManager={viewControllerManager}
/>
<ImportModal importModalController={viewControllerManager.importModalController} />
</> </>
{application.routeService.isDotOrg && <DotOrgNotice />} {application.routeService.isDotOrg && <DotOrgNotice />}
{isIOS() && <IosKeyboardClose />} {isIOS() && <IosKeyboardClose />}

View File

@@ -22,7 +22,7 @@ const BiometricsPrompt = ({ application, onValueChange, prompt, buttonRef }: Pro
fullWidth fullWidth
colorStyle={authenticated ? 'success' : 'info'} colorStyle={authenticated ? 'success' : 'info'}
onClick={async () => { onClick={async () => {
const authenticated = await application.mobileDevice().authenticateWithBiometrics() const authenticated = await application.mobileDevice.authenticateWithBiometrics()
setAuthenticated(authenticated) setAuthenticated(authenticated)
onValueChange(authenticated, prompt) onValueChange(authenticated, prompt)
}} }}

Some files were not shown because too many files have changed in this diff Show More