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

@@ -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,18 +188,20 @@ export class DesktopManager
return
}
const updatedComponent = await this.application.changeAndSaveItem(
component,
(m) => {
const mutator = m as ComponentMutator
// eslint-disable-next-line camelcase
mutator.local_url = componentData.content.local_url as string
// eslint-disable-next-line camelcase
mutator.package_info = componentData.content.package_info
mutator.setAppDataItem(AppDataField.ComponentInstallError, undefined)
},
undefined,
)
const updatedComponent = (
await this.application.changeAndSaveItem.execute(
component,
(m) => {
const mutator = m as ComponentMutator
// eslint-disable-next-line camelcase
mutator.local_url = componentData.content.local_url as string
// eslint-disable-next-line camelcase
mutator.package_info = componentData.content.package_info
mutator.setAppDataItem(AppDataField.ComponentInstallError, undefined)
},
undefined,
)
).getValue()
for (const observer of this.updateObservers) {
observer.callback(updatedComponent as SNComponent)

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
}