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"
SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 4cce221dd782d3ff7c4172167bba09d58af67ccb
@@ -743,7 +743,7 @@ SPEC CHECKSUMS:
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18
RCTTypeSafety: cb09f3e4747b6d18331a15eb05271de7441ca0b3
React: 13109005b5353095c052f26af37413340ccf7a5d

View File

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

View File

@@ -14,8 +14,6 @@ export interface ChallengeServiceInterface extends AbstractService {
submitValuesForChallenge(challenge: ChallengeInterface, values: ChallengeValue[]): Promise<void>
cancelChallenge(challenge: ChallengeInterface): void
isPasscodeLocked(): Promise<boolean>
/**
* Resolves when the challenge has been completed.
* 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
registerUpdateObserver(callback: (component: ComponentInterface) => void): () => void
getExtServerHost(): string
saveDesktopBackup(): Promise<void>
searchText(text?: string): void
redoSearch(): void
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,9 @@ export enum 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] | 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>
/** 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 { ChallengeInterface, ChallengeReason } from '../Challenge'
import { MobileUnlockTiming } from './MobileUnlockTiming'
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
authorizeProtectedActionForItems<T extends DecryptedItem>(files: T[], challengeReason: ChallengeReason): Promise<T[]>
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
isSignedIn(): boolean
isSignedOut(): boolean
get userUuid(): string
getSureUser(): User
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/DeinitMode'
export * from './Application/DeinitSource'
export * from './Application/Options/ApplicationOptions'
export * from './Application/Options/Defaults'
export * from './Application/Options/OptionalOptions'
export * from './Application/Options/RequiredOptions'
export * from './AsymmetricMessage/AsymmetricMessageService'
export * from './AsymmetricMessage/AsymmetricMessageServiceInterface'
export * from './AsymmetricMessage/UseCase/GetInboundMessages'
@@ -117,12 +121,14 @@ export * from './Item/StaticItemCounter'
export * from './ItemsEncryption/ItemsEncryption'
export * from './ItemsEncryption/ItemsEncryption'
export * from './KeySystem/KeySystemKeyManager'
export * from './Mfa/MfaServiceInterface'
export * from './Mutator/ImportDataUseCase'
export * from './Mutator/MutatorClientInterface'
export * from './Payloads/PayloadManagerInterface'
export * from './Preferences/PreferenceServiceInterface'
export * from './Protection/MobileUnlockTiming'
export * from './Protection/ProtectionClientInterface'
export * from './Protection/ProtectionEvent'
export * from './Protection/TimingDisplayOption'
export * from './Revision/RevisionClientInterface'
export * from './Revision/RevisionManager'
@@ -170,20 +176,23 @@ export * from './Sync/SyncOptions'
export * from './Sync/SyncQueueStrategy'
export * from './Sync/SyncServiceInterface'
export * from './Sync/SyncSource'
export * from './UseCase/ChangeAndSaveItem'
export * from './UseCase/DiscardItemsLocally'
export * from './UseCase/GetHost'
export * from './UseCase/SetHost'
export * from './User/AccountEvent'
export * from './User/AccountEventData'
export * from './User/CredentialsChangeFunctionResponse'
export * from './User/SignedInOrRegisteredEventPayload'
export * from './User/SignedOutEventPayload'
export * from './User/UserServiceInterface'
export * from './User/UserServiceInterface'
export * from './User/UserService'
export * from './User/UserServiceInterface'
export * from './User/UserServiceInterface'
export * from './UserEvent/NotificationService'
export * from './UserEvent/NotificationServiceEvent'
export * from './Vault/UseCase/ChangeVaultStorageMode'
export * from './Vault/UseCase/ChangeVaultKeyOptions'
export * from './Vault/UseCase/ChangeVaultKeyOptionsDTO'
export * from './Vault/UseCase/ChangeVaultStorageMode'
export * from './Vault/UseCase/CreateVault'
export * from './Vault/UseCase/DeleteVault'
export * from './Vault/UseCase/GetVault'

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 { WebSocketsService } from './../Services/Api/WebsocketsService'
import { MigrationService } from './../Services/Migration/MigrationService'
@@ -20,7 +20,6 @@ import {
ApplicationStageChangedEventPayload,
StorageValueModes,
ChallengeObserver,
SyncOptions,
ImportDataReturnType,
ImportDataUseCase,
StoragePersistencePolicies,
@@ -57,7 +56,6 @@ import {
ApplicationInterface,
EncryptionService,
EncryptionServiceEvent,
ChallengePrompt,
Challenge,
ErrorAlertStrings,
SessionsClientInterface,
@@ -75,32 +73,33 @@ import {
VaultInviteServiceInterface,
NotificationServiceEvent,
VaultLockServiceInterface,
ApplicationConstructorOptions,
FullyResolvedApplicationOptions,
ApplicationOptionsDefaults,
ChangeAndSaveItem,
ProtectionEvent,
GetHost,
SetHost,
MfaServiceInterface,
} from '@standardnotes/services'
import {
PayloadEmitSource,
SNNote,
PrefKey,
PrefValue,
DecryptedItemMutator,
BackupFile,
DecryptedItemInterface,
EncryptedItemInterface,
Environment,
ItemStream,
Platform,
MutationType,
} from '@standardnotes/models'
import {
HttpResponse,
SessionListResponse,
User,
SignInResponse,
ClientDisplayableError,
SessionListEntry,
} from '@standardnotes/responses'
import {
SyncService,
ProtectionEvent,
SettingsService,
ActionsService,
ChallengeResponse,
@@ -116,14 +115,13 @@ import {
UuidGenerator,
useBoolean,
LoggerInterface,
canBlockDeinit,
} from '@standardnotes/utils'
import { UuidString, ApplicationEventPayload } from '../Types'
import { applicationEventForSyncEvent } from '@Lib/Application/Event'
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
import { ComputePrivateUsername } from '@standardnotes/encryption'
import { SNLog } from '../Log'
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
import { ApplicationOptionsDefaults } from './Options/Defaults'
import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { UseCaseContainerInterface } from '@Lib/Domain/UseCase/UseCaseContainerInterface'
import { GetRecoveryCodes } from '@Lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes'
@@ -137,7 +135,6 @@ import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetA
import { GetAuthenticatorAuthenticationOptions } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions'
import { Dependencies } from './Dependencies/Dependencies'
import { TYPES } from './Dependencies/Types'
import { canBlockDeinit } from './Dependencies/isDeinitable'
/** How often to automatically sync, in milliseconds */
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
@@ -165,7 +162,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private eventHandlers: ApplicationObserver[] = []
private streamRemovers: ObserverRemover[] = []
private serviceObservers: ObserverRemover[] = []
private managedSubscribers: ObserverRemover[] = []
private autoSyncInterval!: ReturnType<typeof setInterval>
@@ -178,7 +174,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private launched = false
/** Whether the application has been destroyed via .deinit() */
public dealloced = false
private isBiometricsSoftLockEngaged = false
private revokingSession = false
private handledFullSyncStage = false
@@ -561,13 +557,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
void this.migrations.handleApplicationEvent(event)
}
/**
* Whether the local database has completed loading local items.
*/
public isDatabaseLoaded(): boolean {
return this.sync.isDatabaseLoaded()
}
public getSessions(): Promise<HttpResponse<SessionListEntry[]>> {
return this.sessions.getSessionsList()
}
@@ -594,65 +583,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return compareVersions(userVersion, ProtocolVersion.V004) >= 0
}
/**
* Begin streaming items to display in the UI. The stream callback will be called
* immediately with the present items that match the constraint, and over time whenever
* items matching the constraint are added, changed, or deleted.
*/
public streamItems<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> {
await this.setHost(host)
await this.setHost.execute(host)
this.sockets.setWebSocketUrl(undefined)
}
public getUser(): User | undefined {
if (!this.launched) {
throw Error('Attempting to access user before application unlocked')
}
return this.sessions.getUser()
}
public getUserPasswordCreationDate(): Date | undefined {
return this.encryption.getPasswordCreatedDate()
}
@@ -699,10 +635,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return result
}
public noAccount(): boolean {
return !this.hasAccount()
}
public hasAccount(): boolean {
return this.encryption.hasAccount()
}
@@ -715,10 +647,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.protections.hasProtectionSources()
}
public hasUnprotectedAccessSession(): boolean {
return this.protections.hasUnprotectedAccessSession()
}
/**
* When a user specifies a non-zero remember duration on a protection
* challenge, a session will be started during which protections are disabled.
@@ -746,10 +674,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.protections.authorizeAutolockIntervalChange()
}
public authorizeSearchingProtectedNotesText(): Promise<boolean> {
return this.protections.authorizeSearchingProtectedNotesText()
}
public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise<BackupFile | undefined> {
return this.encryption.createEncryptedBackupFile()
}
@@ -852,7 +776,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.serviceObservers.length = 0
this.managedSubscribers.length = 0
this.streamRemovers.length = 0
this.started = false
@@ -921,39 +844,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
})
}
public async changeAndSaveItem<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> {
const usecase = this.dependencies.get<ImportDataUseCase>(TYPES.ImportDataUseCase)
return usecase.execute(data, awaitSync)
@@ -991,42 +881,18 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
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> {
/** Because locking is a critical operation, we want to try to do it safely,
* but only up to a certain limit. */
/**
* Because locking is a critical operation, we want to try to do it safely,
* but only up to a certain limit.
*/
const MaximumWaitTime = 500
await this.prepareForDeinit(MaximumWaitTime)
return this.deinit(this.getDeinitMode(), DeinitSource.Lock)
}
public softLockBiometrics(): void {
const challenge = new Challenge(
[new ChallengePrompt(ChallengeValidation.Biometric)],
ChallengeReason.ApplicationUnlock,
false,
)
void this.challenges.promptForChallengeResponse(challenge)
this.isBiometricsSoftLockEngaged = true
void this.notifyEvent(ApplicationEvent.BiometricsSoftLockEngaged)
this.addChallengeObserver(challenge, {
onComplete: () => {
this.isBiometricsSoftLockEngaged = false
void this.notifyEvent(ApplicationEvent.BiometricsSoftLockDisengaged)
},
})
}
isNativeMobileWeb() {
return this.environment === Environment.Mobile
}
@@ -1102,10 +968,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}
}
public getNewSubscriptionToken(): Promise<string | undefined> {
return this.legacyApi.getNewSubscriptionToken()
}
public isThirdPartyHostUsed(): boolean {
return this.legacyApi.isThirdPartyHostUsed()
}
@@ -1117,7 +979,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return false
}
return this.getHost() === (await homeServerService.getHomeServerUrl())
return this.getHost.execute().getValue() === (await homeServerService.getHomeServerUrl())
}
private createBackgroundDependencies() {
@@ -1361,14 +1223,30 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<SharedVaultServiceInterface>(TYPES.SharedVaultService)
}
private get migrations(): MigrationService {
return this.dependencies.get<MigrationService>(TYPES.MigrationService)
public get changeAndSaveItem(): ChangeAndSaveItem {
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)
}
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 {
return this.dependencies.get<HttpServiceInterface>(TYPES.HttpService)
}
@@ -1376,8 +1254,4 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private get sockets(): 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 { ListedService } from '../../Services/Listed/ListedService'
import { MigrationService } from '../../Services/Migration/MigrationService'
import { SNMfaService } from '../../Services/Mfa/MfaService'
import { MfaService } from '../../Services/Mfa/MfaService'
import { SNComponentManager } from '../../Services/ComponentManager/ComponentManager'
import { FeaturesService } from '@Lib/Services/Features/FeaturesService'
import { SettingsService } from '../../Services/Settings/SNSettingsService'
@@ -126,6 +126,10 @@ import {
AlertService,
DesktopDeviceInterface,
ChangeVaultStorageMode,
ChangeAndSaveItem,
FullyResolvedApplicationOptions,
GetHost,
SetHost,
} from '@standardnotes/services'
import { ItemManager } from '../../Services/Items/ItemManager'
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
@@ -151,10 +155,8 @@ import {
WebSocketApiService,
WebSocketServer,
} from '@standardnotes/api'
import { FullyResolvedApplicationOptions } from '../Options/ApplicationOptions'
import { TYPES } from './Types'
import { isDeinitable } from './isDeinitable'
import { Logger, isNotUndefined } from '@standardnotes/utils'
import { Logger, isNotUndefined, isDeinitable } from '@standardnotes/utils'
import { EncryptionOperators } from '@standardnotes/encryption'
import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
@@ -219,6 +221,14 @@ export class Dependencies {
)
})
this.factory.set(TYPES.GetHost, () => {
return new GetHost(this.get<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, () => {
return new GetKeyPairs(this.get<RootKeyManager>(TYPES.RootKeyManager))
})
@@ -307,6 +317,14 @@ export class Dependencies {
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, () => {
return new GetSharedVaults(this.get<GetVaults>(TYPES.GetVaults))
})
@@ -1080,7 +1098,7 @@ export class Dependencies {
})
this.factory.set(TYPES.MfaService, () => {
return new SNMfaService(
return new MfaService(
this.get<SettingsService>(TYPES.SettingsService),
this.get<PureCryptoInterface>(TYPES.Crypto),
this.get<FeaturesService>(TYPES.FeaturesService),

View File

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

View File

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

View File

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

View File

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

View File

@@ -58,7 +58,7 @@ describe('GetFeatureUrl', () => {
})
describe('desktop', () => {
let desktopManager: DesktopManagerInterface | undefined
let desktopManager: jest.Mocked<DesktopManagerInterface | undefined>
beforeEach(() => {
desktopManager = {
@@ -69,7 +69,7 @@ describe('GetFeatureUrl', () => {
getExtServerHost() {
return desktopExtHost
},
}
} as unknown as jest.Mocked<DesktopManagerInterface | undefined>
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 systemSmartViews: Models.SmartView[]
private itemCounter!: Models.ItemCounter
private streamDisposers: (() => void)[] = []
private navigationDisplayController!: Models.ItemDisplayController<
Models.SNNote | Models.FileItem,
@@ -230,6 +231,7 @@ export class ItemManager extends Services.AbstractService implements Services.It
public override deinit(): void {
this.unsubChangeObserver()
this.streamDisposers.length = 0
;(this.unsubChangeObserver as unknown) = undefined
;(this.payloadManager as unknown) = undefined
;(this.collection as unknown) = undefined
@@ -865,4 +867,34 @@ export class ItemManager extends Services.AbstractService implements Services.It
getNoteLinkedFiles(note: Models.SNNote): Models.FileItem[] {
return this.itemsReferencingItem(note).filter(Models.isFile)
}
/**
* Begin streaming items to display in the UI. The stream callback will be called
* immediately with the present items that match the constraint, and over time whenever
* items matching the constraint are added, changed, or deleted.
*/
public streamItems<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 { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { FeaturesService } from '../Features/FeaturesService'
import { AbstractService, InternalEventBusInterface, SignInStrings } from '@standardnotes/services'
import { AbstractService, InternalEventBusInterface, MfaServiceInterface, SignInStrings } from '@standardnotes/services'
export class SNMfaService extends AbstractService {
export class MfaService extends AbstractService implements MfaServiceInterface {
constructor(
private settingsService: SettingsService,
private crypto: PureCryptoInterface,

View File

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

View File

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

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 './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 {
return this.getUser() != undefined
}
public isSignedOut(): boolean {
return !this.isSignedIn()
}
public isSignedIntoFirstPartyServer(): boolean {
return this.isSignedIn() && !this.apiService.isThirdPartyHostUsed()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,8 @@ describe('items', () => {
const item = this.application.items.items[0]
expect(item.pinned).to.not.be.ok
const refreshedItem = await this.application.changeAndSaveItem(
const refreshedItem = (
await this.application.changeAndSaveItem.execute(
item,
(mutator) => {
mutator.pinned = true
@@ -61,6 +62,7 @@ describe('items', () => {
undefined,
syncOptions,
)
).getValue()
expect(refreshedItem.pinned).to.equal(true)
expect(refreshedItem.archived).to.equal(true)
expect(refreshedItem.locked).to.equal(true)
@@ -77,7 +79,8 @@ describe('items', () => {
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
// items should ignore this field when checking for equality
item1 = await this.application.changeAndSaveItem(
item1 = (
await this.application.changeAndSaveItem.execute(
item1,
(mutator) => {
mutator.userModifiedDate = new Date()
@@ -86,7 +89,9 @@ describe('items', () => {
undefined,
syncOptions,
)
item2 = await this.application.changeAndSaveItem(
).getValue()
item2 = (
await this.application.changeAndSaveItem.execute(
item2,
(mutator) => {
mutator.userModifiedDate = undefined
@@ -95,10 +100,12 @@ describe('items', () => {
undefined,
syncOptions,
)
).getValue()
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
item1 = await this.application.changeAndSaveItem(
item1 = (
await this.application.changeAndSaveItem.execute(
item1,
(mutator) => {
mutator.mutableContent.foo = 'bar'
@@ -107,10 +114,12 @@ describe('items', () => {
undefined,
syncOptions,
)
).getValue()
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
item2 = await this.application.changeAndSaveItem(
item2 = (
await this.application.changeAndSaveItem.execute(
item2,
(mutator) => {
mutator.mutableContent.foo = 'bar'
@@ -119,11 +128,13 @@ describe('items', () => {
undefined,
syncOptions,
)
).getValue()
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
expect(item2.isItemContentEqualWith(item1)).to.equal(true)
item1 = await this.application.changeAndSaveItem(
item1 = (
await this.application.changeAndSaveItem.execute(
item1,
(mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
@@ -132,7 +143,9 @@ describe('items', () => {
undefined,
syncOptions,
)
item2 = await this.application.changeAndSaveItem(
).getValue()
item2 = (
await this.application.changeAndSaveItem.execute(
item2,
(mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
@@ -141,13 +154,15 @@ describe('items', () => {
undefined,
syncOptions,
)
).getValue()
expect(item1.content.references.length).to.equal(1)
expect(item2.content.references.length).to.equal(1)
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
item1 = await this.application.changeAndSaveItem(
item1 = (
await this.application.changeAndSaveItem.execute(
item1,
(mutator) => {
mutator.removeItemAsRelationship(item2)
@@ -156,7 +171,9 @@ describe('items', () => {
undefined,
syncOptions,
)
item2 = await this.application.changeAndSaveItem(
).getValue()
item2 = (
await this.application.changeAndSaveItem.execute(
item2,
(mutator) => {
mutator.removeItemAsRelationship(item1)
@@ -165,6 +182,7 @@ describe('items', () => {
undefined,
syncOptions,
)
).getValue()
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
expect(item1.content.references.length).to.equal(0)
@@ -179,7 +197,8 @@ describe('items', () => {
let item1 = this.application.items.getDisplayableNotes()[0]
const item2 = this.application.items.getDisplayableNotes()[1]
item1 = await this.application.changeAndSaveItem(
item1 = (
await this.application.changeAndSaveItem.execute(
item1,
(mutator) => {
mutator.mutableContent.foo = 'bar'
@@ -188,6 +207,7 @@ describe('items', () => {
undefined,
syncOptions,
)
).getValue()
expect(item1.content.foo).to.equal('bar')

View File

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

View File

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

View File

@@ -298,7 +298,7 @@ describe('online conflict handling', function () {
await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
await this.application.changeAndSaveItem(
await this.application.changeAndSaveItem.execute(
note,
(mutator) => {
// client A
@@ -332,7 +332,7 @@ describe('online conflict handling', function () {
await this.application.mutator.setItemDirty(note)
this.expectedItemCount++
await this.application.changeAndSaveItem(
await this.application.changeAndSaveItem.execute(
note,
(mutator) => {
// client A
@@ -602,7 +602,8 @@ describe('online conflict handling', function () {
*/
let tag = await Factory.createMappedTag(this.application)
let note = await Factory.createMappedNote(this.application)
tag = await this.application.changeAndSaveItem(
tag = (
await this.application.changeAndSaveItem.execute(
tag,
(mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
@@ -611,6 +612,7 @@ describe('online conflict handling', function () {
undefined,
syncOptions,
)
).getValue()
await this.application.mutator.setItemDirty(note)
this.expectedItemCount += 2
@@ -732,7 +734,9 @@ describe('online conflict handling', function () {
})
/** This test takes too long on Docker CI */
it.skip('registering for account with bulk offline data belonging to another account should be error-free', async function () {
it.skip(
'registering for account with bulk offline data belonging to another account should be error-free',
async function () {
/**
* When performing a multi-page sync request where we are uploading data imported from a backup,
* if the first page of the sync request returns conflicted items keys, we rotate their UUID.
@@ -764,7 +768,8 @@ describe('online conflict handling', function () {
await newApp.sync.sync(syncOptions)
expect(newApp.payloads.invalidPayloads.length).to.equal(0)
await Factory.safeDeinit(newApp)
}).timeout(80000)
},
).timeout(80000)
it('importing data belonging to another account should not result in duplication', async function () {
/** Create primary account and export data */
@@ -801,7 +806,7 @@ describe('online conflict handling', function () {
await createSyncedNoteWithTag(this.application)
const tag = this.application.items.getDisplayableTags()[0]
const note2 = await Factory.createMappedNote(this.application)
await this.application.changeAndSaveItem(tag, (mutator) => {
await this.application.changeAndSaveItem.execute(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note2)
})
let backupFile = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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: [] })
internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultLocked)
internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultUnlocked)
internalEventBus.addEventHandler(this, ApplicationEvent.ApplicationStageChanged)
makeObservable(this, {
options: observable,
@@ -48,6 +44,10 @@ export class VaultDisplayService
unhideVault: action,
showOnlyVault: action,
})
internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultLocked)
internalEventBus.addEventHandler(this, VaultLockServiceEvent.VaultUnlocked)
internalEventBus.addEventHandler(this, ApplicationEvent.ApplicationStageChanged)
}
async handleEvent(event: InternalEventInterface): Promise<void> {

View File

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

View File

@@ -29,6 +29,12 @@ export * from './Route/RouteServiceEvent'
export * from './Security/AutolockService'
export * from './Storage/LocalStorage'
export * from './UseCase/IsGlobalSpellcheckEnabled'
export * from './UseCase/IsNativeMobileWeb'
export * from './UseCase/IsMobileDevice'
export * from './UseCase/IsNativeIOS'
export * from './UseCase/GetItemTags'
export * from './Theme/ThemeManager'
export * from './Theme/GetAllThemesUseCase'
@@ -42,3 +48,4 @@ export * from './Vaults/VaultDisplayServiceEvent'
export * from './Vaults/VaultDisplayServiceInterface'
export * from './WebApplication/WebApplicationInterface'
export * from './Utils/Utils'

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 './Deferred/Deferred'
export * from './Dependency/DependencyContainer'
export * from './Dependency/isDeinitable'
export * from './Logger/Logger'
export * from './Logger/LoggerInterface'
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 */
async purchaseMockSubscription() {
const subscriptionId = 2002
const email = this.application.getUser()?.email
const email = this.application.sessions.getUser()?.email
const response = await fetch('http://localhost:3124/events', {
method: 'POST',
headers: {

View File

@@ -90,7 +90,7 @@ export class DesktopManager
}
}
async saveDesktopBackup() {
async saveDesktopBackup(): Promise<void> {
this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload)
const data = await this.getBackupFile()
@@ -149,12 +149,12 @@ export class DesktopManager
}
}
searchText(text?: string) {
searchText(text?: string): void {
this.lastSearchedText = text
this.device.onSearch(text)
}
redoSearch() {
redoSearch(): void {
if (this.lastSearchedText) {
this.searchText(this.lastSearchedText)
}
@@ -188,7 +188,8 @@ export class DesktopManager
return
}
const updatedComponent = await this.application.changeAndSaveItem(
const updatedComponent = (
await this.application.changeAndSaveItem.execute(
component,
(m) => {
const mutator = m as ComponentMutator
@@ -200,6 +201,7 @@ export class DesktopManager
},
undefined,
)
).getValue()
for (const observer of this.updateObservers) {
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 { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice'
import {
DeinitSource,
Platform,
SNApplication,
removeFromArray,
DesktopDeviceInterface,
isDesktopDevice,
DeinitMode,
@@ -19,57 +17,82 @@ import {
DecryptedItem,
Environment,
ApplicationOptionsDefaults,
BackupServiceInterface,
InternalFeatureService,
InternalFeatureServiceInterface,
PrefDefaults,
NoteContent,
SNNote,
DesktopManagerInterface,
} from '@standardnotes/snjs'
import { makeObservable, observable } from 'mobx'
import { action, computed, makeObservable, observable } from 'mobx'
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
import { PanelResizedData } from '@/Types/PanelResizedData'
import { getBlobFromBase64, isAndroid, isDesktopApplication, isDev, isIOS } from '@/Utils'
import { DesktopManager } from './Device/DesktopManager'
import { getBlobFromBase64, isDesktopApplication, isDev } from '@/Utils'
import {
ArchiveManager,
AutolockService,
ChangelogService,
Importer,
IsGlobalSpellcheckEnabled,
IsMobileDevice,
IsNativeIOS,
IsNativeMobileWeb,
KeyboardService,
PreferenceId,
RouteService,
RouteServiceInterface,
ThemeManager,
VaultDisplayService,
VaultDisplayServiceInterface,
WebAlertService,
WebApplicationInterface,
} from '@standardnotes/ui-services'
import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver'
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
import { setCustomViewportHeight } from '@/setViewportHeightWithFallback'
import { WebServices } from './WebServices'
import { FeatureName } from '@/Controllers/FeatureName'
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
import { VisibilityObserver } from './VisibilityObserver'
import { MomentsService } from '@/Controllers/Moments/MomentsService'
import { DevMode } from './DevMode'
import { ToastType, addToast, dismissToast } from '@standardnotes/toast'
import { WebDependencies } from './Dependencies/WebDependencies'
import { Web_TYPES } from './Dependencies/Types'
import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver'
import { PaneController } from '@/Controllers/PaneController/PaneController'
import { LinkingController } from '@/Controllers/LinkingController'
import { MomentsService } from '@/Controllers/Moments/MomentsService'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { FilesController } from '@/Controllers/FilesController'
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { PurchaseFlowController } from '@/Controllers/PurchaseFlow/PurchaseFlowController'
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
import { PreferencesController } from '@/Controllers/PreferencesController'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { ImportModalController } from '@/Controllers/ImportModalController'
import { SyncStatusController } from '@/Controllers/SyncStatusController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { FilePreviewModalController } from '@/Controllers/FilePreviewModalController'
import { OpenSubscriptionDashboard } from './UseCase/OpenSubscriptionDashboard'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
import { VaultSelectionMenuController } from '@/Controllers/VaultSelectionMenuController'
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
import { PersistenceService } from '@/Controllers/Abstract/PersistenceService'
import { removeFromArray } from '@standardnotes/utils'
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
export class WebApplication extends SNApplication implements WebApplicationInterface {
public readonly itemControllerGroup: ItemGroupController
public readonly routeService: RouteServiceInterface
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
private readonly webServices!: WebServices
private readonly deps = new WebDependencies(this)
private visibilityObserver?: VisibilityObserver
private readonly webEventObservers: WebEventObserver[] = []
private readonly mobileWebReceiver?: MobileWebReceiver
private readonly androidBackHandler?: AndroidBackHandler
private readonly visibilityObserver?: VisibilityObserver
private readonly mobileAppEventObserver?: () => void
private disposers: (() => void)[] = []
public readonly devMode?: DevMode
public isSessionsModalVisible = false
public devMode?: DevMode
constructor(
deviceInterface: WebOrDesktopDevice,
@@ -102,48 +125,49 @@ export class WebApplication extends SNApplication implements WebApplicationInter
) => Promise<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) {
this.devMode = new DevMode(this)
}
makeObservable(this, {
dealloced: observable,
})
if (!this.isNativeMobileWeb()) {
deviceInterface.setApplication(this)
this.webOrDesktopDevice.setApplication(this)
}
this.itemControllerGroup = new ItemGroupController(this)
this.routeService = new RouteService(this, this.events)
this.webServices = {} as WebServices
this.webServices.keyboardService = new KeyboardService(platform, this.environment)
this.webServices.archiveService = new ArchiveManager(this)
this.webServices.themeService = new ThemeManager(this, this.preferences, this.componentManager, this.events)
this.webServices.autolockService = this.isNativeMobileWeb() ? undefined : new AutolockService(this, this.events)
this.webServices.desktopService = isDesktopDevice(deviceInterface)
? new DesktopManager(this, deviceInterface, this.fileBackups as BackupServiceInterface)
: undefined
this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface)
this.webServices.changelogService = new ChangelogService(this.environment, this.storage)
this.webServices.momentsService = new MomentsService(
this,
this.webServices.viewControllerManager.filesController,
this.events,
)
this.webServices.vaultDisplayService = new VaultDisplayService(this, this.events)
const appEventObserver = this.deps.get<ApplicationEventObserver>(Web_TYPES.ApplicationEventObserver)
this.disposers.push(this.addEventObserver(appEventObserver.handle.bind(appEventObserver)))
if (this.isNativeMobileWeb()) {
this.mobileWebReceiver = new MobileWebReceiver(this)
this.androidBackHandler = new AndroidBackHandler()
this.mobileAppEventObserver = this.addEventObserver(async (event) => {
this.mobileDevice().notifyApplicationEvent(event)
})
this.disposers.push(
this.addEventObserver(async (event) => {
this.mobileDevice.notifyApplicationEvent(event)
}),
)
// eslint-disable-next-line no-console
console.log = (...args) => {
this.mobileDevice().consoleLog(...args)
this.mobileDevice.consoleLog(...args)
}
}
@@ -158,42 +182,23 @@ export class WebApplication extends SNApplication implements WebApplicationInter
super.deinit(mode, source)
if (!this.isNativeMobileWeb()) {
this.webOrDesktopDevice().removeApplication(this)
this.webOrDesktopDevice.removeApplication(this)
}
for (const disposer of this.disposers) {
disposer()
}
this.disposers.length = 0
this.deps.deinit()
try {
for (const service of Object.values(this.webServices)) {
if (!service) {
continue
}
if ('deinit' in service) {
service.deinit?.(source)
}
;(service as { application?: WebApplication }).application = undefined
}
;(this.webServices as unknown) = undefined
this.itemControllerGroup.deinit()
;(this.itemControllerGroup as unknown) = undefined
;(this.mobileWebReceiver as unknown) = undefined
this.routeService.deinit()
;(this.routeService as unknown) = undefined
this.webEventObservers.length = 0
if (this.visibilityObserver) {
this.visibilityObserver.deinit()
;(this.visibilityObserver as unknown) = undefined
}
if (this.mobileAppEventObserver) {
this.mobileAppEventObserver()
;(this.mobileAppEventObserver as unknown) = undefined
}
} catch (error) {
console.error('Error while deiniting application', error)
}
@@ -225,46 +230,6 @@ export class WebApplication extends SNApplication implements WebApplicationInter
this.notifyWebEvent(WebAppEvent.PanelResized, data)
}
public get vaultDisplayService(): VaultDisplayServiceInterface {
return this.webServices.vaultDisplayService
}
public get controllers(): ViewControllerManager {
return this.webServices.viewControllerManager
}
public getDesktopService(): DesktopManager | undefined {
return this.webServices.desktopService
}
public getAutolockService() {
return this.webServices.autolockService
}
public getArchiveService() {
return this.webServices.archiveService
}
public get paneController() {
return this.webServices.viewControllerManager.paneController
}
public get linkingController() {
return this.webServices.viewControllerManager.linkingController
}
public get changelogService() {
return this.webServices.changelogService
}
public get momentsService() {
return this.webServices.momentsService
}
public get featuresController() {
return this.controllers.featuresController
}
public get desktopDevice(): DesktopDeviceInterface | undefined {
if (isDesktopDevice(this.device)) {
return this.device
@@ -277,53 +242,42 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return InternalFeatureService.get()
}
isNativeIOS() {
return this.isNativeMobileWeb() && this.platform === Platform.Ios
isNativeIOS(): boolean {
return this.deps.get<IsNativeIOS>(Web_TYPES.IsNativeIOS).execute().getValue()
}
get isMobileDevice() {
return this.isNativeMobileWeb() || isIOS() || isAndroid()
get isMobileDevice(): boolean {
return this.deps.get<IsMobileDevice>(Web_TYPES.IsMobileDevice).execute().getValue()
}
get hideOutboundSubscriptionLinks() {
return this.isNativeIOS()
}
mobileDevice(): MobileDeviceInterface {
if (!this.isNativeMobileWeb()) {
throw Error('Attempting to access device as mobile device on non mobile platform')
}
get mobileDevice(): MobileDeviceInterface {
return this.device as MobileDeviceInterface
}
webOrDesktopDevice(): WebOrDesktopDevice {
get webOrDesktopDevice(): WebOrDesktopDevice {
return this.device as WebOrDesktopDevice
}
public getThemeService() {
return this.webServices.themeService
}
public get keyboardService() {
return this.webServices.keyboardService
}
async checkForSecurityUpdate() {
async checkForSecurityUpdate(): Promise<boolean> {
return this.protocolUpgradeAvailable()
}
performDesktopTextBackup(): void | Promise<void> {
return this.getDesktopService()?.saveDesktopBackup()
return this.desktopManager?.saveDesktopBackup()
}
isGlobalSpellcheckEnabled(): boolean {
return this.getPreference(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck])
return this.deps.get<IsGlobalSpellcheckEnabled>(Web_TYPES.IsGlobalSpellcheckEnabled).execute().getValue()
}
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
}) as SNTag[]
})
}
public get version(): string {
@@ -349,15 +303,15 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
if (this.protections.getMobileScreenshotPrivacyEnabled()) {
this.mobileDevice().setAndroidScreenshotPrivacy(true)
this.mobileDevice.setAndroidScreenshotPrivacy(true)
} else {
this.mobileDevice().setAndroidScreenshotPrivacy(false)
this.mobileDevice.setAndroidScreenshotPrivacy(false)
}
}
async handleMobileLosingFocusEvent(): Promise<void> {
if (this.protections.getMobileScreenshotPrivacyEnabled()) {
this.mobileDevice().stopHidingMobileInterfaceFromScreenshots()
this.mobileDevice.stopHidingMobileInterfaceFromScreenshots()
}
await this.lockApplicationAfterMobileEventIfApplicable()
@@ -365,12 +319,20 @@ export class WebApplication extends SNApplication implements WebApplicationInter
async handleMobileResumingFromBackgroundEvent(): Promise<void> {
if (this.protections.getMobileScreenshotPrivacyEnabled()) {
this.mobileDevice().hideMobileInterfaceFromScreenshots()
this.mobileDevice.hideMobileInterfaceFromScreenshots()
}
}
handleMobileColorSchemeChangeEvent() {
void this.getThemeService().handleMobileColorSchemeChangeEvent()
void this.themeManager.handleMobileColorSchemeChangeEvent()
}
openSessionsModal = () => {
this.isSessionsModalVisible = true
}
closeSessionsModal = () => {
this.isSessionsModalVisible = false
}
handleMobileKeyboardWillChangeFrameEvent(frame: {
@@ -392,14 +354,14 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
handleReceivedFileEvent(file: { name: string; mimeType: string; data: string }): void {
const filesController = this.controllers.filesController
const filesController = this.filesController
const blob = getBlobFromBase64(file.data, file.mimeType)
const mappedFile = new File([blob], file.name, { type: file.mimeType })
filesController.uploadNewFile(mappedFile, true).catch(console.error)
}
async handleReceivedTextEvent({ text, title }: { text: string; title?: string | undefined }) {
const titleForNote = title || this.controllers.itemListController.titleForNewNote()
const titleForNote = title || this.itemListController.titleForNewNote()
const note = this.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, {
title: titleForNote,
@@ -409,7 +371,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
const insertedNote = await this.mutator.insertItem(note)
this.controllers.selectionController.selectItem(insertedNote.uuid, true).catch(console.error)
this.itemListController.selectItem(insertedNote.uuid, true).catch(console.error)
addToast({
type: ToastType.Success,
@@ -437,7 +399,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
const file = new File([imgBlob], finalPath, {
type: imgBlob.type,
})
this.controllers.filesController.uploadNewFile(file, true).catch(console.error)
this.filesController.uploadNewFile(file, true).catch(console.error)
} catch (error) {
console.error(error)
} finally {
@@ -453,7 +415,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> {
const isLocked = await this.isLocked()
const isLocked = await this.protections.isLocked()
if (isLocked) {
return
}
@@ -469,7 +431,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
if (passcodeLockImmediately) {
await this.lock()
} else if (biometricsLockImmediately) {
this.softLockBiometrics()
this.protections.softLockBiometrics()
}
}
@@ -494,7 +456,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
isAuthorizedToRenderItem(item: DecryptedItem): boolean {
if (item.protected && this.hasProtectionSources()) {
return this.hasUnprotectedAccessSession()
return this.protections.hasUnprotectedAccessSession()
}
return true
@@ -505,19 +467,19 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
get entitledToFiles(): boolean {
return this.controllers.featuresController.entitledToFiles
return this.featuresController.entitledToFiles
}
showPremiumModal(featureName?: FeatureName): void {
void this.controllers.featuresController.showPremiumAlert(featureName)
void this.featuresController.showPremiumAlert(featureName)
}
hasValidFirstPartySubscription(): boolean {
return this.controllers.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription
return this.subscriptionController.hasFirstPartyOnlineOrOfflineSubscription
}
async openPurchaseFlow() {
await this.controllers.purchaseFlowController.openPurchaseFlow()
await this.purchaseFlowController.openPurchaseFlow()
}
addNativeMobileEventListener = (listener: NativeMobileEventListener) => {
@@ -529,11 +491,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
showAccountMenu(): void {
this.controllers.accountMenuController.setShow(true)
this.accountMenuController.setShow(true)
}
hideAccountMenu(): void {
this.controllers.accountMenuController.setShow(false)
this.accountMenuController.setShow(false)
}
/**
@@ -545,13 +507,158 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
openPreferences(pane?: PreferenceId): void {
this.controllers.preferencesController.openPreferences()
this.preferencesController.openPreferences()
if (pane) {
this.controllers.preferencesController.setCurrentPane(pane)
this.preferencesController.setCurrentPane(pane)
}
}
generateUUID(): string {
return this.options.crypto.generateUUID()
}
/**
* Dependency
* Accessors
*/
get routeService(): RouteServiceInterface {
return this.deps.get<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()) {
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 { WebApplication } from '@/Application/WebApplication'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { autorun, IReactionDisposer, IReactionPublic } from 'mobx'
import { Component } from 'react'
import { WebApplication } from '@/Application/WebApplication'
export type PureComponentState = Partial<Record<string, unknown>>
export type PureComponentProps = Partial<Record<string, unknown>>
@@ -13,7 +12,7 @@ export abstract class AbstractComponent<P = PureComponentProps, S = PureComponen
constructor(
props: P,
protected application: WebApplication,
public readonly application: WebApplication,
) {
super(props)
}
@@ -40,10 +39,6 @@ export abstract class AbstractComponent<P = PureComponentProps, S = PureComponen
this.deinit()
}
public get viewControllerManager(): ViewControllerManager {
return this.application.controllers
}
autorun(view: (r: IReactionPublic) => void): void {
this.reactionDisposers.push(autorun(view))
}

View File

@@ -1,35 +1,30 @@
import { observer } from 'mobx-react-lite'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { WebApplication } from '@/Application/WebApplication'
import { useCallback, FunctionComponent, KeyboardEventHandler } from 'react'
import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { AccountMenuPane } from './AccountMenuPane'
import MenuPaneSelector from './MenuPaneSelector'
import { KeyboardKey } from '@standardnotes/ui-services'
import { useApplication } from '../ApplicationProvider'
export type AccountMenuProps = {
viewControllerManager: ViewControllerManager
application: WebApplication
onClickOutside: () => void
mainApplicationGroup: WebApplicationGroup
}
const AccountMenu: FunctionComponent<AccountMenuProps> = ({
application,
viewControllerManager,
mainApplicationGroup,
}) => {
const { currentPane } = viewControllerManager.accountMenuController
const AccountMenu: FunctionComponent<AccountMenuProps> = ({ mainApplicationGroup }) => {
const application = useApplication()
const { currentPane } = application.accountMenuController
const closeAccountMenu = useCallback(() => {
viewControllerManager.accountMenuController.closeAccountMenu()
}, [viewControllerManager])
application.accountMenuController.closeAccountMenu()
}, [application])
const setCurrentPane = useCallback(
(pane: AccountMenuPane) => {
viewControllerManager.accountMenuController.setCurrentPane(pane)
application.accountMenuController.setCurrentPane(pane)
},
[viewControllerManager],
[application],
)
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback(
@@ -50,8 +45,6 @@ const AccountMenu: FunctionComponent<AccountMenuProps> = ({
return (
<div id="account-menu" className="sn-component" onKeyDown={handleKeyDown}>
<MenuPaneSelector
viewControllerManager={viewControllerManager}
application={application}
mainApplicationGroup={mainApplicationGroup}
menuPane={currentPane}
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 { ChangeEventHandler, FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react'
import Checkbox from '@/Components/Checkbox/Checkbox'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import Icon from '@/Components/Icon/Icon'
import { useApplication } from '../ApplicationProvider'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
disabled?: boolean
onPrivateUsernameModeChange?: (isPrivate: boolean, identifier?: string) => void
onStrictSignInChange?: (isStrictSignIn: boolean) => void
@@ -17,15 +14,15 @@ type Props = {
}
const AdvancedOptions: FunctionComponent<Props> = ({
viewControllerManager,
application,
disabled = false,
onPrivateUsernameModeChange,
onStrictSignInChange,
onRecoveryCodesChange,
children,
}) => {
const { server, setServer, enableServerOption, setEnableServerOption } = viewControllerManager.accountMenuController
const application = useApplication()
const { server, setServer, enableServerOption, setEnableServerOption } = application.accountMenuController
const [showAdvanced, setShowAdvanced] = useState(false)
const [isPrivateUsername, setIsPrivateUsername] = useState(false)

View File

@@ -1,6 +1,4 @@
import { STRING_NON_MATCHING_PASSWORDS } from '@/Constants/Strings'
import { WebApplication } from '@/Application/WebApplication'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import {
FormEventHandler,
@@ -17,23 +15,18 @@ import Checkbox from '@/Components/Checkbox/Checkbox'
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
import Icon from '@/Components/Icon/Icon'
import IconButton from '@/Components/Button/IconButton'
import { useApplication } from '../ApplicationProvider'
type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
setMenuPane: (pane: AccountMenuPane) => void
email: string
password: string
}
const ConfirmPassword: FunctionComponent<Props> = ({
application,
viewControllerManager,
setMenuPane,
email,
password,
}) => {
const { notesAndTagsCount } = viewControllerManager.accountMenuController
const ConfirmPassword: FunctionComponent<Props> = ({ setMenuPane, email, password }) => {
const application = useApplication()
const { notesAndTagsCount } = application.accountMenuController
const [confirmPassword, setConfirmPassword] = useState('')
const [isRegistering, setIsRegistering] = useState(false)
const [isEphemeral, setIsEphemeral] = useState(false)
@@ -72,8 +65,8 @@ const ConfirmPassword: FunctionComponent<Props> = ({
application
.register(email, password, isEphemeral, shouldMergeLocal)
.then(() => {
viewControllerManager.accountMenuController.closeAccountMenu()
viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
application.accountMenuController.closeAccountMenu()
application.accountMenuController.setCurrentPane(AccountMenuPane.GeneralMenu)
})
.catch((err) => {
console.error(err)
@@ -88,7 +81,7 @@ const ConfirmPassword: FunctionComponent<Props> = ({
passwordInputRef.current?.focus()
}
},
[viewControllerManager, application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
[application, confirmPassword, email, isEphemeral, password, shouldMergeLocal],
)
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 {
FormEventHandler,
@@ -20,8 +18,6 @@ import AdvancedOptions from './AdvancedOptions'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
setMenuPane: (pane: AccountMenuPane) => void
email: string
setEmail: React.Dispatch<React.SetStateAction<string>>
@@ -29,15 +25,7 @@ type Props = {
setPassword: React.Dispatch<React.SetStateAction<string>>
}
const CreateAccount: FunctionComponent<Props> = ({
viewControllerManager,
application,
setMenuPane,
email,
setEmail,
password,
setPassword,
}) => {
const CreateAccount: FunctionComponent<Props> = ({ setMenuPane, email, setEmail, password, setPassword }) => {
const emailInputRef = useRef<HTMLInputElement>(null)
const passwordInputRef = useRef<HTMLInputElement>(null)
const [isPrivateUsername, setIsPrivateUsername] = useState(false)
@@ -145,11 +133,7 @@ const CreateAccount: FunctionComponent<Props> = ({
<Button className="mt-1" label="Next" primary onClick={handleRegisterFormSubmit} fullWidth={true} />
</form>
<HorizontalSeparator classes="my-2" />
<AdvancedOptions
application={application}
viewControllerManager={viewControllerManager}
onPrivateUsernameModeChange={onPrivateUsernameChange}
/>
<AdvancedOptions 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 Icon from '@/Components/Icon/Icon'
import { SyncQueueStrategy } from '@standardnotes/snjs'
@@ -14,10 +12,9 @@ import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { formatLastSyncDate } from '@/Utils/DateUtils'
import Spinner from '@/Components/Spinner/Spinner'
import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
import { useApplication } from '../ApplicationProvider'
type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
mainApplicationGroup: WebApplicationGroup
setMenuPane: (pane: AccountMenuPane) => void
closeMenu: () => void
@@ -25,13 +22,9 @@ type Props = {
const iconClassName = `text-neutral mr-2 ${MenuItemIconSize}`
const GeneralAccountMenu: FunctionComponent<Props> = ({
application,
viewControllerManager,
setMenuPane,
closeMenu,
mainApplicationGroup,
}) => {
const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu, mainApplicationGroup }) => {
const application = useApplication()
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
@@ -58,23 +51,23 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
})
}, [application])
const user = useMemo(() => application.getUser(), [application])
const user = useMemo(() => application.sessions.getUser(), [application])
const openPreferences = useCallback(() => {
viewControllerManager.accountMenuController.closeAccountMenu()
viewControllerManager.preferencesController.setCurrentPane('account')
viewControllerManager.preferencesController.openPreferences()
}, [viewControllerManager])
application.accountMenuController.closeAccountMenu()
application.preferencesController.setCurrentPane('account')
application.preferencesController.openPreferences()
}, [application])
const openHelp = useCallback(() => {
viewControllerManager.accountMenuController.closeAccountMenu()
viewControllerManager.preferencesController.setCurrentPane('help-feedback')
viewControllerManager.preferencesController.openPreferences()
}, [viewControllerManager])
application.accountMenuController.closeAccountMenu()
application.preferencesController.setCurrentPane('help-feedback')
application.preferencesController.openPreferences()
}, [application])
const signOut = useCallback(() => {
viewControllerManager.accountMenuController.setSigningOut(true)
}, [viewControllerManager])
application.accountMenuController.setSigningOut(true)
}, [application])
const activateRegisterPane = useCallback(() => {
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>You're signed in as:</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 className="mb-2 flex items-start justify-between px-3 text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item">
{isSyncingInProgress ? (
@@ -137,16 +130,13 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
</>
)}
<Menu
isOpen={viewControllerManager.accountMenuController.show}
isOpen={application.accountMenuController.show}
a11yLabel="General account menu"
closeMenu={closeMenu}
initialFocus={!application.hasAccount() ? CREATE_ACCOUNT_INDEX : SWITCHER_INDEX}
>
<MenuItemSeparator />
<WorkspaceSwitcherOption
mainApplicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
/>
<WorkspaceSwitcherOption mainApplicationGroup={mainApplicationGroup} />
<MenuItemSeparator />
{user ? (
<MenuItem onClick={openPreferences}>
@@ -167,8 +157,8 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
)}
<MenuItem
onClick={() => {
viewControllerManager.importModalController.setIsVisible(true)
viewControllerManager.accountMenuController.closeAccountMenu()
application.importModalController.setIsVisible(true)
application.accountMenuController.closeAccountMenu()
}}
>
<Icon type="archive" className={iconClassName} />

View File

@@ -1,6 +1,4 @@
import { WebApplication } from '@/Application/WebApplication'
import { WebApplicationGroup } from '@/Application/WebApplicationGroup'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useState } from 'react'
import { AccountMenuPane } from './AccountMenuPane'
@@ -10,22 +8,13 @@ import GeneralAccountMenu from './GeneralAccountMenu'
import SignInPane from './SignIn'
type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
mainApplicationGroup: WebApplicationGroup
menuPane: AccountMenuPane
setMenuPane: (pane: AccountMenuPane) => void
closeMenu: () => void
}
const MenuPaneSelector: FunctionComponent<Props> = ({
application,
viewControllerManager,
menuPane,
setMenuPane,
closeMenu,
mainApplicationGroup,
}) => {
const MenuPaneSelector: FunctionComponent<Props> = ({ menuPane, setMenuPane, closeMenu, mainApplicationGroup }) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
@@ -33,22 +22,16 @@ const MenuPaneSelector: FunctionComponent<Props> = ({
case AccountMenuPane.GeneralMenu:
return (
<GeneralAccountMenu
viewControllerManager={viewControllerManager}
application={application}
mainApplicationGroup={mainApplicationGroup}
setMenuPane={setMenuPane}
closeMenu={closeMenu}
/>
)
case AccountMenuPane.SignIn:
return (
<SignInPane viewControllerManager={viewControllerManager} application={application} setMenuPane={setMenuPane} />
)
return <SignInPane setMenuPane={setMenuPane} />
case AccountMenuPane.Register:
return (
<CreateAccount
viewControllerManager={viewControllerManager}
application={application}
setMenuPane={setMenuPane}
email={email}
setEmail={setEmail}
@@ -57,15 +40,7 @@ const MenuPaneSelector: FunctionComponent<Props> = ({
/>
)
case AccountMenuPane.ConfirmPassword:
return (
<ConfirmPassword
viewControllerManager={viewControllerManager}
application={application}
setMenuPane={setMenuPane}
email={email}
password={password}
/>
)
return <ConfirmPassword 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 { observer } from 'mobx-react-lite'
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 HorizontalSeparator from '../Shared/HorizontalSeparator'
import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/snjs'
import { useApplication } from '../ApplicationProvider'
type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication
setMenuPane: (pane: AccountMenuPane) => void
}
const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManager, setMenuPane }) => {
const { notesAndTagsCount } = viewControllerManager.accountMenuController
const SignInPane: FunctionComponent<Props> = ({ setMenuPane }) => {
const application = useApplication()
const { notesAndTagsCount } = application.accountMenuController
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [recoveryCodes, setRecoveryCodes] = useState('')
@@ -101,7 +100,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
if (isErrorResponse(response)) {
throw new Error(getErrorFromErrorResponse(response).message)
}
viewControllerManager.accountMenuController.closeAccountMenu()
application.accountMenuController.closeAccountMenu()
})
.catch((err) => {
console.error(err)
@@ -112,7 +111,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
.finally(() => {
setIsSigningIn(false)
})
}, [viewControllerManager, application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
}, [application, email, isEphemeral, isStrictSignin, password, shouldMergeLocal])
const recoverySignIn = useCallback(() => {
setIsSigningIn(true)
@@ -129,7 +128,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
if (result.isFailed()) {
throw new Error(result.getError())
}
viewControllerManager.accountMenuController.closeAccountMenu()
application.accountMenuController.closeAccountMenu()
})
.catch((err) => {
console.error(err)
@@ -140,7 +139,7 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
.finally(() => {
setIsSigningIn(false)
})
}, [viewControllerManager, application, email, password, recoveryCodes])
}, [application, email, password, recoveryCodes])
const onPrivateUsernameChange = useCallback(
(newisPrivateUsername: boolean, privateUsernameIdentifier?: string) => {
@@ -251,8 +250,6 @@ const SignInPane: FunctionComponent<Props> = ({ application, viewControllerManag
</div>
<HorizontalSeparator classes="my-2" />
<AdvancedOptions
viewControllerManager={viewControllerManager}
application={application}
disabled={isSigningIn}
onPrivateUsernameModeChange={onPrivateUsernameChange}
onStrictSignInChange={handleStrictSigninChange}

View File

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

View File

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

View File

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

View File

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

View File

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

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