refactor: repo (#1070)
This commit is contained in:
209
packages/web/src/javascripts/Application/Application.ts
Normal file
209
packages/web/src/javascripts/Application/Application.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { WebCrypto } from '@/Application/Crypto'
|
||||
import { WebAlertService } from '@/Services/AlertService'
|
||||
import { ArchiveManager } from '@/Services/ArchiveManager'
|
||||
import { AutolockService } from '@/Services/AutolockService'
|
||||
import { DesktopManager } from '@/Services/DesktopManager'
|
||||
import { IOService } from '@/Services/IOService'
|
||||
import { ThemeManager } from '@/Services/ThemeManager'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice'
|
||||
import {
|
||||
DeinitSource,
|
||||
Platform,
|
||||
SNApplication,
|
||||
NoteGroupController,
|
||||
removeFromArray,
|
||||
IconsController,
|
||||
DesktopDeviceInterface,
|
||||
isDesktopDevice,
|
||||
DeinitMode,
|
||||
PrefKey,
|
||||
SNTag,
|
||||
ContentType,
|
||||
DecryptedItemInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable } from 'mobx'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import { WebAppEvent } from './WebAppEvent'
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
|
||||
type WebServices = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
desktopService?: DesktopManager
|
||||
autolockService: AutolockService
|
||||
archiveService: ArchiveManager
|
||||
themeService: ThemeManager
|
||||
io: IOService
|
||||
}
|
||||
|
||||
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
|
||||
|
||||
export class WebApplication extends SNApplication {
|
||||
private webServices!: WebServices
|
||||
private webEventObservers: WebEventObserver[] = []
|
||||
public noteControllerGroup: NoteGroupController
|
||||
public iconsController: IconsController
|
||||
private onVisibilityChange: () => void
|
||||
|
||||
constructor(
|
||||
deviceInterface: WebOrDesktopDevice,
|
||||
platform: Platform,
|
||||
identifier: string,
|
||||
defaultSyncServerHost: string,
|
||||
webSocketUrl: string,
|
||||
) {
|
||||
super({
|
||||
environment: deviceInterface.environment,
|
||||
platform: platform,
|
||||
deviceInterface: deviceInterface,
|
||||
crypto: WebCrypto,
|
||||
alertService: new WebAlertService(),
|
||||
identifier,
|
||||
defaultHost: defaultSyncServerHost,
|
||||
appVersion: deviceInterface.appVersion,
|
||||
webSocketUrl: webSocketUrl,
|
||||
supportsFileNavigation: window.enabledUnfinishedFeatures || false,
|
||||
})
|
||||
|
||||
makeObservable(this, {
|
||||
dealloced: observable,
|
||||
})
|
||||
|
||||
deviceInterface.setApplication(this)
|
||||
this.noteControllerGroup = new NoteGroupController(this)
|
||||
this.iconsController = new IconsController()
|
||||
|
||||
this.onVisibilityChange = () => {
|
||||
const visible = document.visibilityState === 'visible'
|
||||
const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur
|
||||
this.notifyWebEvent(event)
|
||||
}
|
||||
|
||||
if (!isDesktopApplication()) {
|
||||
document.addEventListener('visibilitychange', this.onVisibilityChange)
|
||||
}
|
||||
}
|
||||
|
||||
override deinit(mode: DeinitMode, source: DeinitSource): void {
|
||||
super.deinit(mode, source)
|
||||
|
||||
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 WebServices
|
||||
|
||||
this.noteControllerGroup.deinit()
|
||||
;(this.noteControllerGroup as unknown) = undefined
|
||||
|
||||
this.webEventObservers.length = 0
|
||||
|
||||
document.removeEventListener('visibilitychange', this.onVisibilityChange)
|
||||
;(this.onVisibilityChange as unknown) = undefined
|
||||
} catch (error) {
|
||||
console.error('Error while deiniting application', error)
|
||||
}
|
||||
}
|
||||
|
||||
setWebServices(services: WebServices): void {
|
||||
this.webServices = services
|
||||
}
|
||||
|
||||
public addWebEventObserver(observer: WebEventObserver): () => void {
|
||||
this.webEventObservers.push(observer)
|
||||
|
||||
return () => {
|
||||
removeFromArray(this.webEventObservers, observer)
|
||||
}
|
||||
}
|
||||
|
||||
public notifyWebEvent(event: WebAppEvent, data?: unknown): void {
|
||||
for (const observer of this.webEventObservers) {
|
||||
observer(event, data)
|
||||
}
|
||||
}
|
||||
|
||||
publishPanelDidResizeEvent(name: string, collapsed: boolean) {
|
||||
const data: PanelResizedData = {
|
||||
panel: name,
|
||||
collapsed: collapsed,
|
||||
}
|
||||
this.notifyWebEvent(WebAppEvent.PanelResized, data)
|
||||
}
|
||||
|
||||
public getViewControllerManager(): 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 desktopDevice(): DesktopDeviceInterface | undefined {
|
||||
if (isDesktopDevice(this.deviceInterface)) {
|
||||
return this.deviceInterface
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
public getThemeService() {
|
||||
return this.webServices.themeService
|
||||
}
|
||||
|
||||
public get io() {
|
||||
return this.webServices.io
|
||||
}
|
||||
|
||||
async checkForSecurityUpdate() {
|
||||
return this.protocolUpgradeAvailable()
|
||||
}
|
||||
|
||||
downloadBackup(): void | Promise<void> {
|
||||
if (isDesktopDevice(this.deviceInterface)) {
|
||||
return this.deviceInterface.downloadBackup()
|
||||
}
|
||||
}
|
||||
|
||||
async signOutAndDeleteLocalBackups(): Promise<void> {
|
||||
isDesktopDevice(this.deviceInterface) && (await this.deviceInterface.deleteLocalBackups())
|
||||
|
||||
return this.user.signOut()
|
||||
}
|
||||
|
||||
isGlobalSpellcheckEnabled(): boolean {
|
||||
return this.getPreference(PrefKey.EditorSpellcheck, true)
|
||||
}
|
||||
|
||||
public getItemTags(item: DecryptedItemInterface) {
|
||||
return this.items.itemsReferencingItem(item).filter((ref) => {
|
||||
return ref.content_type === ContentType.Tag
|
||||
}) as SNTag[]
|
||||
}
|
||||
|
||||
public get version(): string {
|
||||
return (this.deviceInterface as WebOrDesktopDevice).appVersion
|
||||
}
|
||||
|
||||
async toggleGlobalSpellcheck() {
|
||||
const currentValue = this.isGlobalSpellcheckEnabled()
|
||||
return this.setPreference(PrefKey.EditorSpellcheck, !currentValue)
|
||||
}
|
||||
}
|
||||
80
packages/web/src/javascripts/Application/ApplicationGroup.ts
Normal file
80
packages/web/src/javascripts/Application/ApplicationGroup.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { WebApplication } from './Application'
|
||||
import {
|
||||
ApplicationDescriptor,
|
||||
SNApplicationGroup,
|
||||
Platform,
|
||||
InternalEventBus,
|
||||
isDesktopDevice,
|
||||
} from '@standardnotes/snjs'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { getPlatform, isDesktopApplication } from '@/Utils'
|
||||
import { ArchiveManager } from '@/Services/ArchiveManager'
|
||||
import { DesktopManager } from '@/Services/DesktopManager'
|
||||
import { IOService } from '@/Services/IOService'
|
||||
import { AutolockService } from '@/Services/AutolockService'
|
||||
import { ThemeManager } from '@/Services/ThemeManager'
|
||||
import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice'
|
||||
|
||||
const createApplication = (
|
||||
descriptor: ApplicationDescriptor,
|
||||
deviceInterface: WebOrDesktopDevice,
|
||||
defaultSyncServerHost: string,
|
||||
device: WebOrDesktopDevice,
|
||||
webSocketUrl: string,
|
||||
) => {
|
||||
const platform = getPlatform()
|
||||
|
||||
const application = new WebApplication(
|
||||
deviceInterface,
|
||||
platform,
|
||||
descriptor.identifier,
|
||||
defaultSyncServerHost,
|
||||
webSocketUrl,
|
||||
)
|
||||
|
||||
const viewControllerManager = new ViewControllerManager(application, device)
|
||||
const archiveService = new ArchiveManager(application)
|
||||
const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop)
|
||||
const autolockService = new AutolockService(application, new InternalEventBus())
|
||||
const themeService = new ThemeManager(application)
|
||||
|
||||
application.setWebServices({
|
||||
viewControllerManager,
|
||||
archiveService,
|
||||
desktopService: isDesktopDevice(device) ? new DesktopManager(application, device) : undefined,
|
||||
io,
|
||||
autolockService,
|
||||
themeService,
|
||||
})
|
||||
|
||||
return application
|
||||
}
|
||||
|
||||
export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
|
||||
constructor(private defaultSyncServerHost: string, device: WebOrDesktopDevice, private webSocketUrl: string) {
|
||||
super(device)
|
||||
}
|
||||
|
||||
override async initialize(): Promise<void> {
|
||||
const defaultSyncServerHost = this.defaultSyncServerHost
|
||||
const webSocketUrl = this.webSocketUrl
|
||||
|
||||
await super.initialize({
|
||||
applicationCreator: async (descriptor, device) => {
|
||||
return createApplication(descriptor, device, defaultSyncServerHost, device, webSocketUrl)
|
||||
},
|
||||
})
|
||||
|
||||
if (isDesktopApplication()) {
|
||||
window.webClient = (this.primaryApplication as WebApplication).getDesktopService()
|
||||
}
|
||||
}
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
|
||||
if (isDesktopApplication()) {
|
||||
delete window.webClient
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/web/src/javascripts/Application/Crypto.ts
Normal file
3
packages/web/src/javascripts/Application/Crypto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { SNWebCrypto } from '@standardnotes/sncrypto-web'
|
||||
|
||||
export const WebCrypto = new SNWebCrypto()
|
||||
263
packages/web/src/javascripts/Application/Database.ts
Normal file
263
packages/web/src/javascripts/Application/Database.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { isString, AlertService, uniqueArray } from '@standardnotes/snjs'
|
||||
|
||||
const STORE_NAME = 'items'
|
||||
const READ_WRITE = 'readwrite'
|
||||
|
||||
const OUT_OF_SPACE =
|
||||
'Unable to save changes locally because your device is out of space. ' +
|
||||
'Please free up some disk space and try again, otherwise, your data may end ' +
|
||||
'up in an inconsistent state.'
|
||||
|
||||
const DB_DELETION_BLOCKED =
|
||||
'Your browser is blocking Standard Notes from deleting the local database. ' +
|
||||
'Make sure there are no other open windows of this app and try again. ' +
|
||||
'If the issue persists, please manually delete app data to sign out.'
|
||||
|
||||
const QUOTE_EXCEEDED_ERROR = 'QuotaExceededError'
|
||||
|
||||
export class Database {
|
||||
private locked = true
|
||||
private db?: IDBDatabase
|
||||
|
||||
constructor(public databaseName: string, private alertService?: AlertService) {}
|
||||
|
||||
public deinit(): void {
|
||||
;(this.alertService as unknown) = undefined
|
||||
this.db = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Relinquishes the lock and allows db operations to proceed
|
||||
*/
|
||||
public unlock(): void {
|
||||
this.locked = false
|
||||
}
|
||||
|
||||
static async getAllDatabaseNames(): Promise<string[] | undefined> {
|
||||
if (!window.indexedDB.databases) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const rawDatabases = await window.indexedDB.databases()
|
||||
return rawDatabases.map((db) => db.name).filter((name) => name && name.length > 0) as string[]
|
||||
}
|
||||
|
||||
static async deleteAll(databaseNames: string[]): Promise<void> {
|
||||
if (window.indexedDB.databases != undefined) {
|
||||
const idbNames = await this.getAllDatabaseNames()
|
||||
|
||||
if (idbNames) {
|
||||
databaseNames = uniqueArray([...idbNames, ...databaseNames])
|
||||
}
|
||||
}
|
||||
|
||||
for (const name of databaseNames) {
|
||||
const db = new Database(name)
|
||||
|
||||
await db.clearAllPayloads()
|
||||
|
||||
db.deinit()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the database natively, or returns the existing database object if already opened.
|
||||
* @param onNewDatabase - Callback to invoke when a database has been created
|
||||
* as part of the open process. This can happen on new application sessions, or if the
|
||||
* browser deleted the database without the user being aware.
|
||||
*/
|
||||
public async openDatabase(onNewDatabase?: () => void): Promise<IDBDatabase | undefined> {
|
||||
if (this.locked) {
|
||||
throw Error('Attempting to open locked database')
|
||||
}
|
||||
if (this.db) {
|
||||
return this.db
|
||||
}
|
||||
const request = window.indexedDB.open(this.databaseName, 1)
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = (event) => {
|
||||
const target = event.target as any
|
||||
if (target.errorCode) {
|
||||
this.showAlert('Offline database issue: ' + target.errorCode)
|
||||
} else {
|
||||
this.displayOfflineAlert()
|
||||
}
|
||||
reject(new Error('Unable to open db'))
|
||||
}
|
||||
request.onblocked = (_event) => {
|
||||
reject(Error('IndexedDB open request blocked'))
|
||||
}
|
||||
request.onsuccess = (event) => {
|
||||
const target = event.target as IDBOpenDBRequest
|
||||
const db = target.result
|
||||
db.onversionchange = () => {
|
||||
db.close()
|
||||
}
|
||||
db.onerror = (errorEvent) => {
|
||||
const target = errorEvent?.target as any
|
||||
throw Error('Database error: ' + target.errorCode)
|
||||
}
|
||||
this.db = db
|
||||
resolve(db)
|
||||
}
|
||||
request.onupgradeneeded = (event) => {
|
||||
const target = event.target as IDBOpenDBRequest
|
||||
const db = target.result
|
||||
db.onversionchange = () => {
|
||||
db.close()
|
||||
}
|
||||
/* Create an objectStore for this database */
|
||||
const objectStore = db.createObjectStore(STORE_NAME, {
|
||||
keyPath: 'uuid',
|
||||
})
|
||||
objectStore.createIndex('uuid', 'uuid', { unique: true })
|
||||
objectStore.transaction.oncomplete = () => {
|
||||
/* Ready to store values in the newly created objectStore. */
|
||||
if (db.version === 1 && onNewDatabase) {
|
||||
onNewDatabase && onNewDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public async getAllPayloads(): Promise<any[]> {
|
||||
const db = (await this.openDatabase()) as IDBDatabase
|
||||
return new Promise((resolve) => {
|
||||
const objectStore = db.transaction(STORE_NAME).objectStore(STORE_NAME)
|
||||
const payloads: any = []
|
||||
const cursorRequest = objectStore.openCursor()
|
||||
cursorRequest.onsuccess = (event) => {
|
||||
const target = event.target as any
|
||||
const cursor = target.result
|
||||
if (cursor) {
|
||||
payloads.push(cursor.value)
|
||||
cursor.continue()
|
||||
} else {
|
||||
resolve(payloads)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public async getAllKeys(): Promise<string[]> {
|
||||
const db = (await this.openDatabase()) as IDBDatabase
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const objectStore = db.transaction(STORE_NAME).objectStore(STORE_NAME)
|
||||
const getAllKeysRequest = objectStore.getAllKeys()
|
||||
getAllKeysRequest.onsuccess = function () {
|
||||
const result = getAllKeysRequest.result
|
||||
|
||||
const strings = result.map((key) => {
|
||||
if (isString(key)) {
|
||||
return key
|
||||
} else {
|
||||
return JSON.stringify(key)
|
||||
}
|
||||
})
|
||||
|
||||
resolve(strings)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public async savePayload(payload: any): Promise<void> {
|
||||
return this.savePayloads([payload])
|
||||
}
|
||||
|
||||
public async savePayloads(payloads: any[]): Promise<void> {
|
||||
if (payloads.length === 0) {
|
||||
return
|
||||
}
|
||||
const db = (await this.openDatabase()) as IDBDatabase
|
||||
const transaction = db.transaction(STORE_NAME, READ_WRITE)
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
transaction.oncomplete = () => {}
|
||||
transaction.onerror = (event) => {
|
||||
const target = event.target as any
|
||||
this.showGenericError(target.error)
|
||||
}
|
||||
transaction.onabort = (event) => {
|
||||
const target = event.target as any
|
||||
const error = target.error
|
||||
if (error.name === QUOTE_EXCEEDED_ERROR) {
|
||||
this.showAlert(OUT_OF_SPACE)
|
||||
} else {
|
||||
this.showGenericError(error)
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
const objectStore = transaction.objectStore(STORE_NAME)
|
||||
this.putItems(objectStore, payloads).then(resolve).catch(console.error)
|
||||
})
|
||||
}
|
||||
|
||||
private async putItems(objectStore: IDBObjectStore, items: any[]): Promise<void> {
|
||||
await Promise.all(
|
||||
items.map((item) => {
|
||||
return new Promise((resolve) => {
|
||||
const request = objectStore.put(item)
|
||||
request.onerror = resolve
|
||||
request.onsuccess = resolve
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
public async deletePayload(uuid: string): Promise<void> {
|
||||
const db = (await this.openDatabase()) as IDBDatabase
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = db.transaction(STORE_NAME, READ_WRITE).objectStore(STORE_NAME).delete(uuid)
|
||||
request.onsuccess = () => {
|
||||
resolve()
|
||||
}
|
||||
request.onerror = reject
|
||||
})
|
||||
}
|
||||
|
||||
public async clearAllPayloads(): Promise<void> {
|
||||
const deleteRequest = window.indexedDB.deleteDatabase(this.databaseName)
|
||||
return new Promise((resolve, reject) => {
|
||||
deleteRequest.onerror = () => {
|
||||
reject(Error('Error deleting database.'))
|
||||
}
|
||||
deleteRequest.onsuccess = () => {
|
||||
this.db = undefined
|
||||
resolve()
|
||||
}
|
||||
deleteRequest.onblocked = (_event) => {
|
||||
this.showAlert(DB_DELETION_BLOCKED)
|
||||
reject(Error('Delete request blocked'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private showAlert(message: string) {
|
||||
if (this.alertService) {
|
||||
this.alertService.alert(message).catch(console.error)
|
||||
} else {
|
||||
window.alert(message)
|
||||
}
|
||||
}
|
||||
|
||||
private showGenericError(error: { code: number; name: string }) {
|
||||
const message =
|
||||
'Unable to save changes locally due to an unknown system issue. ' +
|
||||
`Issue Code: ${error.code} Issue Name: ${error.name}.`
|
||||
|
||||
this.showAlert(message)
|
||||
}
|
||||
|
||||
private displayOfflineAlert() {
|
||||
const message =
|
||||
'There was an issue loading your offline database. This could happen for two reasons:' +
|
||||
"\n\n1. You're in a private window in your browser. We can't save your data without " +
|
||||
'access to the local database. Please use a non-private window.' +
|
||||
'\n\n2. You have two windows of the app open at the same time. ' +
|
||||
'Please close any other app instances and reload the page.'
|
||||
|
||||
this.showAlert(message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
Environment,
|
||||
RawKeychainValue,
|
||||
DesktopDeviceInterface,
|
||||
WebOrDesktopDeviceInterface,
|
||||
DesktopClientRequiresWebMethods,
|
||||
FileBackupsMapping,
|
||||
FileBackupsDevice,
|
||||
} from '@standardnotes/snjs'
|
||||
@@ -0,0 +1,8 @@
|
||||
import { WebOrDesktopDevice } from './WebOrDesktopDevice'
|
||||
|
||||
export type StartApplication = (
|
||||
defaultSyncServerHost: string,
|
||||
device: WebOrDesktopDevice,
|
||||
enableUnfinishedFeatures: boolean,
|
||||
webSocketUrl: string,
|
||||
) => Promise<void>
|
||||
41
packages/web/src/javascripts/Application/Device/WebDevice.ts
Normal file
41
packages/web/src/javascripts/Application/Device/WebDevice.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Environment, RawKeychainValue } from '@standardnotes/snjs'
|
||||
import { WebOrDesktopDevice } from './WebOrDesktopDevice'
|
||||
|
||||
const KEYCHAIN_STORAGE_KEY = 'keychain'
|
||||
const DESTROYED_DEVICE_URL_PARAM = 'destroyed'
|
||||
const DESTROYED_DEVICE_URL_VALUE = 'true'
|
||||
|
||||
export class WebDevice extends WebOrDesktopDevice {
|
||||
environment = Environment.Web
|
||||
|
||||
async getKeychainValue(): Promise<RawKeychainValue> {
|
||||
const value = localStorage.getItem(KEYCHAIN_STORAGE_KEY)
|
||||
|
||||
if (value) {
|
||||
return JSON.parse(value)
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
async setKeychainValue(value: RawKeychainValue): Promise<void> {
|
||||
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value))
|
||||
}
|
||||
|
||||
async clearRawKeychainValue(): Promise<void> {
|
||||
localStorage.removeItem(KEYCHAIN_STORAGE_KEY)
|
||||
}
|
||||
|
||||
async performHardReset(): Promise<void> {
|
||||
const url = new URL(window.location.href)
|
||||
const params = url.searchParams
|
||||
params.append(DESTROYED_DEVICE_URL_PARAM, DESTROYED_DEVICE_URL_VALUE)
|
||||
window.location.replace(url.href)
|
||||
}
|
||||
|
||||
public isDeviceDestroyed(): boolean {
|
||||
const url = new URL(window.location.href)
|
||||
const params = url.searchParams
|
||||
return params.get(DESTROYED_DEVICE_URL_PARAM) === DESTROYED_DEVICE_URL_VALUE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
SNApplication,
|
||||
ApplicationIdentifier,
|
||||
Environment,
|
||||
LegacyRawKeychainValue,
|
||||
RawKeychainValue,
|
||||
TransferPayload,
|
||||
NamespacedRootKeyInKeychain,
|
||||
extendArray,
|
||||
WebOrDesktopDeviceInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Database } from '../Database'
|
||||
|
||||
export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface {
|
||||
constructor(public appVersion: string) {}
|
||||
|
||||
private databases: Database[] = []
|
||||
|
||||
abstract environment: Environment
|
||||
|
||||
setApplication(application: SNApplication): void {
|
||||
const database = new Database(application.identifier, application.alertService)
|
||||
|
||||
this.databases.push(database)
|
||||
}
|
||||
|
||||
public async getJsonParsedRawStorageValue(key: string): Promise<unknown | undefined> {
|
||||
const value = await this.getRawStorageValue(key)
|
||||
if (value == undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
private databaseForIdentifier(identifier: ApplicationIdentifier) {
|
||||
return this.databases.find((database) => database.databaseName === identifier) as Database
|
||||
}
|
||||
|
||||
async clearAllDataFromDevice(workspaceIdentifiers: ApplicationIdentifier[]): Promise<{ killsApplication: boolean }> {
|
||||
await this.clearRawKeychainValue()
|
||||
|
||||
await this.removeAllRawStorageValues()
|
||||
|
||||
await Database.deleteAll(workspaceIdentifiers)
|
||||
|
||||
return { killsApplication: false }
|
||||
}
|
||||
|
||||
deinit() {
|
||||
for (const database of this.databases) {
|
||||
database.deinit()
|
||||
}
|
||||
|
||||
this.databases = []
|
||||
}
|
||||
|
||||
async getRawStorageValue(key: string): Promise<string | undefined> {
|
||||
const result = localStorage.getItem(key)
|
||||
|
||||
if (result == undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async getAllRawStorageKeyValues() {
|
||||
const results = []
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
results.push({
|
||||
key: key,
|
||||
value: localStorage[key],
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
async setRawStorageValue(key: string, value: string) {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
|
||||
async removeRawStorageValue(key: string) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
async removeAllRawStorageValues() {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
async openDatabase(identifier: ApplicationIdentifier) {
|
||||
this.databaseForIdentifier(identifier).unlock()
|
||||
return new Promise((resolve, reject) => {
|
||||
this.databaseForIdentifier(identifier)
|
||||
.openDatabase(() => {
|
||||
resolve({ isNewDatabase: true })
|
||||
})
|
||||
.then(() => {
|
||||
resolve({ isNewDatabase: false })
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
}) as Promise<{ isNewDatabase?: boolean } | undefined>
|
||||
}
|
||||
|
||||
async getAllRawDatabasePayloads(identifier: ApplicationIdentifier) {
|
||||
return this.databaseForIdentifier(identifier).getAllPayloads()
|
||||
}
|
||||
|
||||
async saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier) {
|
||||
return this.databaseForIdentifier(identifier).savePayload(payload)
|
||||
}
|
||||
|
||||
async saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier) {
|
||||
return this.databaseForIdentifier(identifier).savePayloads(payloads)
|
||||
}
|
||||
|
||||
async removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier) {
|
||||
return this.databaseForIdentifier(identifier).deletePayload(id)
|
||||
}
|
||||
|
||||
async removeAllRawDatabasePayloads(identifier: ApplicationIdentifier) {
|
||||
return this.databaseForIdentifier(identifier).clearAllPayloads()
|
||||
}
|
||||
|
||||
async getNamespacedKeychainValue(identifier: ApplicationIdentifier) {
|
||||
const keychain = await this.getKeychainValue()
|
||||
|
||||
if (!keychain) {
|
||||
return
|
||||
}
|
||||
|
||||
return keychain[identifier]
|
||||
}
|
||||
|
||||
async getDatabaseKeys(): Promise<string[]> {
|
||||
const keys: string[] = []
|
||||
|
||||
for (const database of this.databases) {
|
||||
extendArray(keys, await database.getAllKeys())
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
async setNamespacedKeychainValue(value: NamespacedRootKeyInKeychain, identifier: ApplicationIdentifier) {
|
||||
let keychain = await this.getKeychainValue()
|
||||
|
||||
if (!keychain) {
|
||||
keychain = {}
|
||||
}
|
||||
|
||||
return this.setKeychainValue({
|
||||
...keychain,
|
||||
[identifier]: value,
|
||||
})
|
||||
}
|
||||
|
||||
async clearNamespacedKeychainValue(identifier: ApplicationIdentifier) {
|
||||
const keychain = await this.getKeychainValue()
|
||||
if (!keychain) {
|
||||
return
|
||||
}
|
||||
|
||||
delete keychain[identifier]
|
||||
|
||||
return this.setKeychainValue(keychain)
|
||||
}
|
||||
|
||||
setRawKeychainValue(value: unknown): Promise<void> {
|
||||
return this.setKeychainValue(value)
|
||||
}
|
||||
|
||||
openUrl(url: string) {
|
||||
const win = window.open(url, '_blank')
|
||||
if (win) {
|
||||
win.focus()
|
||||
}
|
||||
}
|
||||
|
||||
setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise<void> {
|
||||
return this.setKeychainValue(value)
|
||||
}
|
||||
|
||||
abstract getKeychainValue(): Promise<RawKeychainValue>
|
||||
|
||||
abstract setKeychainValue(value: unknown): Promise<void>
|
||||
|
||||
abstract clearRawKeychainValue(): Promise<void>
|
||||
|
||||
abstract isDeviceDestroyed(): boolean
|
||||
|
||||
abstract performHardReset(): Promise<void>
|
||||
|
||||
async performSoftReset(): Promise<void> {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
9
packages/web/src/javascripts/Application/WebAppEvent.ts
Normal file
9
packages/web/src/javascripts/Application/WebAppEvent.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum WebAppEvent {
|
||||
NewUpdateAvailable = 'NewUpdateAvailable',
|
||||
EditorFocused = 'EditorFocused',
|
||||
BeganBackupDownload = 'BeganBackupDownload',
|
||||
EndedBackupDownload = 'EndedBackupDownload',
|
||||
PanelResized = 'PanelResized',
|
||||
WindowDidFocus = 'WindowDidFocus',
|
||||
WindowDidBlur = 'WindowDidBlur',
|
||||
}
|
||||
Reference in New Issue
Block a user