internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -0,0 +1,52 @@
import { AbstractService, InternalEventBusInterface, ApplicationEvent } from '@standardnotes/services'
import { AbstractUIServiceInterface } from './AbstractUIServiceInterface'
import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface'
export class AbstractUIServicee<EventName = string, EventData = unknown>
extends AbstractService<EventName, EventData>
implements AbstractUIServiceInterface<EventName, EventData>
{
private unsubApp!: () => void
constructor(
protected application: WebApplicationInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
this.addAppEventObserverAfterSubclassesFinishConstructing()
}
async onAppStart() {
return
}
async onAppEvent(_event: ApplicationEvent) {
return
}
private addAppEventObserverAfterSubclassesFinishConstructing() {
setTimeout(() => {
this.addAppEventObserver()
}, 0)
}
private addAppEventObserver() {
if (this.application.isStarted()) {
void this.onAppStart()
}
this.unsubApp = this.application.addEventObserver(async (event: ApplicationEvent) => {
await this.onAppEvent(event)
if (event === ApplicationEvent.Started) {
void this.onAppStart()
}
})
}
override deinit() {
;(this.application as unknown) = undefined
this.unsubApp()
;(this.unsubApp as unknown) = undefined
super.deinit()
}
}

View File

@@ -0,0 +1,7 @@
import { ApplicationEvent, ServiceInterface } from '@standardnotes/services'
export interface AbstractUIServiceInterface<EventName = string, EventData = unknown>
extends ServiceInterface<EventName, EventData> {
onAppStart(): Promise<void>
onAppEvent(event: ApplicationEvent): Promise<void>
}

View File

@@ -4,6 +4,30 @@ import { SKAlert } from '@standardnotes/styles'
import { alertDialog, confirmDialog } from './Functions'
export class WebAlertService extends AlertService {
override confirmV2(dto: {
text: string
title?: string | undefined
confirmButtonText?: string | undefined
confirmButtonType?: ButtonType | undefined
cancelButtonText?: string | undefined
}): Promise<boolean> {
return confirmDialog({
text: dto.text,
title: dto.title,
confirmButtonText: dto.confirmButtonText,
cancelButtonText: dto.cancelButtonText,
confirmButtonStyle: dto.confirmButtonType === ButtonType.Danger ? 'danger' : 'info',
})
}
override alertV2(dto: {
text: string
title?: string | undefined
closeButtonText?: string | undefined
}): Promise<void> {
return alertDialog({ text: dto.text, title: dto.title, closeButtonText: dto.closeButtonText })
}
alert(text: string, title?: string, closeButtonText?: string) {
return alertDialog({ text, title, closeButtonText })
}

View File

@@ -1,5 +1,5 @@
import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface'
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
import { WebApplicationInterface } from '@standardnotes/services'
import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter'
import data from './testData'

View File

@@ -2,7 +2,7 @@ import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { ContentType } from '@standardnotes/common'
import { readFileAsText } from '../Utils'
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
import { WebApplicationInterface } from '@standardnotes/services'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
type AegisData = {
db: {

View File

@@ -3,10 +3,10 @@
*/
import { ContentType } from '@standardnotes/common'
import { WebApplicationInterface } from '@standardnotes/services'
import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models'
import { EvernoteConverter } from './EvernoteConverter'
import data from './testData'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
// Mock dayjs so dayjs.extend() doesn't throw an error in EvernoteConverter.ts
jest.mock('dayjs', () => {

View File

@@ -1,10 +1,10 @@
import { ContentType } from '@standardnotes/common'
import { WebApplicationInterface } from '@standardnotes/services'
import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models'
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'
dayjs.extend(customParseFormat)
dayjs.extend(utc)

View File

@@ -2,9 +2,9 @@
* @jest-environment jsdom
*/
import { WebApplicationInterface } from '@standardnotes/snjs'
import { jsonTestData, htmlTestData } from './testData'
import { GoogleKeepConverter } from './GoogleKeepConverter'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
describe('GoogleKeepConverter', () => {
let application: WebApplicationInterface

View File

@@ -1,7 +1,7 @@
import { WebApplicationInterface } from '@standardnotes/services'
import { ContentType } from '@standardnotes/common'
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { readFileAsText } from '../Utils'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
type GoogleKeepJsonNote = {
color: string

View File

@@ -1,5 +1,5 @@
import { parseFileName } from '@standardnotes/filepicker'
import { FeatureStatus, WebApplicationInterface } from '@standardnotes/services'
import { FeatureStatus } from '@standardnotes/services'
import { FeatureIdentifier } from '@standardnotes/features'
import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter'
import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter'
@@ -8,6 +8,7 @@ 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'
@@ -82,7 +83,7 @@ export class Importer {
const insertedItems = await Promise.all(
payloads.map(async (payload) => {
const content = payload.content as NoteContent
const note = this.application.mutator.createTemplateItem(
const note = this.application.items.createTemplateItem(
payload.content_type,
{
text: content.text,

View File

@@ -1,8 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { parseFileName } from '@standardnotes/filepicker'
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { WebApplicationInterface } from '@standardnotes/services'
import { readFileAsText } from '../Utils'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
export class PlaintextConverter {
constructor(protected application: WebApplicationInterface) {}

View File

@@ -1,4 +1,4 @@
import { WebApplicationInterface } from '@standardnotes/services'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
import { SimplenoteConverter } from './SimplenoteConverter'
import data from './testData'

View File

@@ -1,7 +1,7 @@
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { ContentType } from '@standardnotes/common'
import { readFileAsText } from '../Utils'
import { WebApplicationInterface } from '@standardnotes/services'
import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface'
type SimplenoteItem = {
creationDate: string

View File

@@ -1,4 +1,4 @@
import { Environment, Platform } from '@standardnotes/snjs'
import { Environment, Platform } from '@standardnotes/models'
import { isMacPlatform } from './platformCheck'
import {
CREATE_NEW_NOTE_KEYBOARD_COMMAND,

View File

@@ -2,6 +2,7 @@ const PREFERENCE_IDS = [
'general',
'account',
'security',
'vaults',
'appearance',
'backups',
'listed',

View File

@@ -8,41 +8,31 @@ import {
SNTheme,
} from '@standardnotes/models'
import { removeFromArray } from '@standardnotes/utils'
import {
AbstractService,
WebApplicationInterface,
InternalEventBusInterface,
ApplicationEvent,
StorageValueModes,
FeatureStatus,
} from '@standardnotes/services'
import { InternalEventBusInterface, ApplicationEvent, StorageValueModes, FeatureStatus } from '@standardnotes/services'
import { FeatureIdentifier } from '@standardnotes/features'
import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface'
import { AbstractUIServicee } from '../Abstract/AbstractUIService'
const CachedThemesKey = 'cachedThemes'
const TimeBeforeApplyingColorScheme = 5
const DefaultThemeIdentifier = 'Default'
export class ThemeManager extends AbstractService {
export class ThemeManager extends AbstractUIServicee {
private activeThemes: string[] = []
private unregisterDesktop?: () => void
private unregisterStream!: () => void
private lastUseDeviceThemeSettings = false
private unsubApp!: () => void
constructor(
protected application: WebApplicationInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
this.addAppEventObserverAfterSubclassesFinishConstructing()
constructor(application: WebApplicationInterface, internalEventBus: InternalEventBusInterface) {
super(application, internalEventBus)
this.colorSchemeEventHandler = this.colorSchemeEventHandler.bind(this)
}
async onAppStart() {
override async onAppStart() {
this.registerObservers()
}
async onAppEvent(event: ApplicationEvent) {
override async onAppEvent(event: ApplicationEvent) {
switch (event) {
case ApplicationEvent.SignedOut: {
this.deactivateAllThemes()
@@ -76,25 +66,6 @@ export class ThemeManager extends AbstractService {
}
}
addAppEventObserverAfterSubclassesFinishConstructing() {
setTimeout(() => {
this.addAppEventObserver()
}, 0)
}
addAppEventObserver() {
if (this.application.isStarted()) {
void this.onAppStart()
}
this.unsubApp = this.application.addEventObserver(async (event: ApplicationEvent) => {
await this.onAppEvent(event)
if (event === ApplicationEvent.Started) {
void this.onAppStart()
}
})
}
async handleMobileColorSchemeChangeEvent() {
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
@@ -124,10 +95,6 @@ export class ThemeManager extends AbstractService {
}
}
get webApplication() {
return this.application as WebApplicationInterface
}
override deinit() {
this.activeThemes.length = 0
@@ -143,11 +110,6 @@ export class ThemeManager extends AbstractService {
mq.removeListener(this.colorSchemeEventHandler)
}
;(this.application as unknown) = undefined
this.unsubApp()
;(this.unsubApp as unknown) = undefined
super.deinit()
}
@@ -166,7 +128,7 @@ export class ThemeManager extends AbstractService {
const status = this.application.features.getFeatureStatus(theme.identifier)
if (status !== FeatureStatus.Entitled) {
if (theme.active) {
this.application.mutator.toggleTheme(theme).catch(console.error)
this.application.componentManager.toggleTheme(theme.uuid).catch(console.error)
} else {
this.deactivateTheme(theme.uuid)
}
@@ -242,7 +204,7 @@ export class ThemeManager extends AbstractService {
const toggleActiveTheme = () => {
if (activeTheme) {
void this.application.mutator.toggleTheme(activeTheme)
void this.application.componentManager.toggleTheme(activeTheme.uuid)
}
}
@@ -252,7 +214,7 @@ export class ThemeManager extends AbstractService {
} else {
const theme = themes.find((theme) => theme.package_info.identifier === themeIdentifier)
if (theme && !theme.active) {
this.application.mutator.toggleTheme(theme).catch(console.error)
this.application.componentManager.toggleTheme(theme.uuid).catch(console.error)
}
}
}
@@ -272,7 +234,7 @@ export class ThemeManager extends AbstractService {
}
private registerObservers() {
this.unregisterDesktop = this.webApplication.getDesktopService()?.registerUpdateObserver((component) => {
this.unregisterDesktop = this.application.getDesktopService()?.registerUpdateObserver((component) => {
if (component.active && component.isTheme()) {
this.deactivateTheme(component.uuid)
setTimeout(() => {

View File

@@ -0,0 +1,216 @@
import {
ApplicationEvent,
ApplicationStage,
ApplicationStageChangedEventPayload,
Challenge,
ChallengePrompt,
ChallengeReason,
ChallengeStrings,
ChallengeValidation,
InternalEventBusInterface,
InternalEventHandlerInterface,
InternalEventInterface,
StorageKey,
VaultServiceEvent,
} from '@standardnotes/services'
import { VaultDisplayOptions, VaultDisplayOptionsPersistable, VaultListingInterface } from '@standardnotes/models'
import { VaultDisplayServiceEvent } from './VaultDisplayServiceEvent'
import { AbstractUIServicee } from '../Abstract/AbstractUIService'
import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface'
import { VaultDisplayServiceInterface } from './VaultDisplayServiceInterface'
import { action, makeObservable, observable } from 'mobx'
export class VaultDisplayService
extends AbstractUIServicee<VaultDisplayServiceEvent>
implements VaultDisplayServiceInterface, InternalEventHandlerInterface
{
options: VaultDisplayOptions
public exclusivelyShownVault: VaultListingInterface | undefined = undefined
constructor(application: WebApplicationInterface, internalEventBus: InternalEventBusInterface) {
super(application, internalEventBus)
this.options = new VaultDisplayOptions({ exclude: [], locked: [] })
internalEventBus.addEventHandler(this, VaultServiceEvent.VaultLocked)
internalEventBus.addEventHandler(this, VaultServiceEvent.VaultUnlocked)
internalEventBus.addEventHandler(this, ApplicationEvent.ApplicationStageChanged)
makeObservable(this, {
options: observable,
isVaultExplicitelyExcluded: observable,
isVaultExclusivelyShown: observable,
exclusivelyShownVault: observable,
hideVault: action,
unhideVault: action,
showOnlyVault: action,
})
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === VaultServiceEvent.VaultLocked || event.type === VaultServiceEvent.VaultUnlocked) {
this.handleVaultLockingStatusChanged()
} else if (event.type === ApplicationEvent.ApplicationStageChanged) {
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
if (stage === ApplicationStage.StorageDecrypted_09) {
void this.loadVaultSelectionOptionsFromDisk()
}
}
}
private handleVaultLockingStatusChanged(): void {
const lockedVaults = this.application.vaults.getLockedvaults()
const options = this.options.newOptionsByIntakingLockedVaults(lockedVaults)
this.setVaultSelectionOptions(options)
}
public getOptions(): VaultDisplayOptions {
return this.options
}
isVaultExplicitelyExcluded = (vault: VaultListingInterface): boolean => {
return this.options.isVaultExplicitelyExcluded(vault) ?? false
}
isVaultDisabledOrLocked(vault: VaultListingInterface): boolean {
return this.options.isVaultDisabledOrLocked(vault)
}
isVaultExclusivelyShown = (vault: VaultListingInterface): boolean => {
return this.options.isVaultExclusivelyShown(vault)
}
isInExclusiveDisplayMode(): boolean {
return this.options.isInExclusiveDisplayMode()
}
changeToMultipleVaultDisplayMode(): void {
const vaults = this.application.vaults.getVaults()
const lockedVaults = this.application.vaults.getLockedvaults()
const newOptions = new VaultDisplayOptions({
exclude: vaults
.map((vault) => vault.systemIdentifier)
.filter((identifier) => identifier !== this.exclusivelyShownVault?.systemIdentifier),
locked: lockedVaults.map((vault) => vault.systemIdentifier),
})
this.setVaultSelectionOptions(newOptions)
}
hideVault = (vault: VaultListingInterface) => {
const lockedVaults = this.application.vaults.getLockedvaults()
const newOptions = this.options.newOptionsByExcludingVault(vault, lockedVaults)
this.setVaultSelectionOptions(newOptions)
}
unhideVault = async (vault: VaultListingInterface) => {
if (this.application.vaults.isVaultLocked(vault)) {
const unlocked = await this.unlockVault(vault)
if (!unlocked) {
return
}
}
const lockedVaults = this.application.vaults.getLockedvaults()
const newOptions = this.options.newOptionsByUnexcludingVault(vault, lockedVaults)
this.setVaultSelectionOptions(newOptions)
}
showOnlyVault = async (vault: VaultListingInterface) => {
if (this.application.vaults.isVaultLocked(vault)) {
const unlocked = await this.unlockVault(vault)
if (!unlocked) {
return
}
}
const newOptions = new VaultDisplayOptions({ exclusive: vault.systemIdentifier })
this.setVaultSelectionOptions(newOptions)
}
async unlockVault(vault: VaultListingInterface): Promise<boolean> {
if (!this.application.vaults.isVaultLocked(vault)) {
throw new Error('Attempting to unlock a vault that is not locked.')
}
const challenge = new Challenge(
[new ChallengePrompt(ChallengeValidation.None, undefined, 'Password')],
ChallengeReason.Custom,
true,
ChallengeStrings.UnlockVault(vault.name),
ChallengeStrings.EnterVaultPassword,
)
return new Promise((resolve) => {
this.application.challenges.addChallengeObserver(challenge, {
onCancel() {
resolve(false)
},
onNonvalidatedSubmit: async (challengeResponse) => {
const value = challengeResponse.getDefaultValue()
if (!value) {
this.application.challenges.completeChallenge(challenge)
resolve(false)
return
}
const password = value.value as string
const unlocked = await this.application.vaults.unlockNonPersistentVault(vault, password)
if (!unlocked) {
this.application.challenges.setValidationStatusForChallenge(challenge, value, false)
resolve(false)
return
}
this.application.challenges.completeChallenge(challenge)
resolve(true)
},
})
void this.application.challenges.promptForChallengeResponse(challenge)
})
}
private setVaultSelectionOptions = (options: VaultDisplayOptions) => {
this.options = options
if (this.isInExclusiveDisplayMode()) {
this.exclusivelyShownVault = this.application.vaults.getVault({
keySystemIdentifier: this.options.getExclusivelyShownVault(),
})
} else {
this.exclusivelyShownVault = undefined
}
this.application.items.setVaultDisplayOptions(options)
void this.notifyEvent(VaultDisplayServiceEvent.VaultDisplayOptionsChanged, options)
if (this.application.isLaunched()) {
this.application.setValue(StorageKey.VaultSelectionOptions, options.getPersistableValue())
}
}
private loadVaultSelectionOptionsFromDisk = (): void => {
const raw = this.application.getValue<VaultDisplayOptionsPersistable>(StorageKey.VaultSelectionOptions)
if (!raw) {
return
}
const options = VaultDisplayOptions.FromPersistableValue(raw)
this.options = options
void this.notifyEvent(VaultDisplayServiceEvent.VaultDisplayOptionsChanged, options)
}
override deinit(): void {
;(this.options as unknown) = undefined
super.deinit()
}
}

View File

@@ -0,0 +1,3 @@
export enum VaultDisplayServiceEvent {
VaultDisplayOptionsChanged = 'VaultDisplayOptionsChanged',
}

View File

@@ -0,0 +1,20 @@
import { VaultDisplayOptions, VaultListingInterface } from '@standardnotes/models'
import { AbstractUIServiceInterface } from '../Abstract/AbstractUIServiceInterface'
export interface VaultDisplayServiceInterface extends AbstractUIServiceInterface {
exclusivelyShownVault?: VaultListingInterface
getOptions(): VaultDisplayOptions
isVaultDisabledOrLocked(vault: VaultListingInterface): boolean
isVaultExplicitelyExcluded: (vault: VaultListingInterface) => boolean
isVaultExclusivelyShown: (vault: VaultListingInterface) => boolean
isInExclusiveDisplayMode(): boolean
changeToMultipleVaultDisplayMode(): void
hideVault: (vault: VaultListingInterface) => void
unhideVault: (vault: VaultListingInterface) => void
showOnlyVault: (vault: VaultListingInterface) => void
unlockVault(vault: VaultListingInterface): Promise<boolean>
}

View File

@@ -0,0 +1,24 @@
import {
ApplicationInterface,
DesktopManagerInterface,
MobileDeviceInterface,
WebAppEvent,
} from '@standardnotes/services'
export interface WebApplicationInterface extends ApplicationInterface {
notifyWebEvent(event: WebAppEvent, data?: unknown): void
getDesktopService(): DesktopManagerInterface | undefined
handleMobileEnteringBackgroundEvent(): Promise<void>
handleMobileGainingFocusEvent(): Promise<void>
handleMobileLosingFocusEvent(): Promise<void>
handleMobileResumingFromBackgroundEvent(): Promise<void>
handleMobileColorSchemeChangeEvent(): void
handleMobileKeyboardWillChangeFrameEvent(frame: { height: number; contentHeight: number }): void
handleMobileKeyboardDidChangeFrameEvent(frame: { height: number; contentHeight: number }): void
isNativeMobileWeb(): boolean
mobileDevice(): MobileDeviceInterface
handleAndroidBackButtonPressed(): void
addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined
setAndroidBackHandlerFallbackListener(listener: () => boolean): void
generateUUID(): string
}

View File

@@ -33,3 +33,9 @@ export * from './Toast/ToastService'
export * from './Toast/ToastServiceInterface'
export * from './StatePersistence/StatePersistence'
export * from './Import'
export * from './Vaults/VaultDisplayService'
export * from './Vaults/VaultDisplayServiceEvent'
export * from './Vaults/VaultDisplayServiceInterface'
export * from './WebApplication/WebApplicationInterface'