refactor: improve device interface types (#996)
This commit is contained in:
@@ -20,15 +20,15 @@ import { IsWebPlatform, WebAppVersion } from '@/Version'
|
|||||||
import { Runtime, SNLog } from '@standardnotes/snjs'
|
import { Runtime, SNLog } from '@standardnotes/snjs'
|
||||||
import { render } from 'preact'
|
import { render } from 'preact'
|
||||||
import { ApplicationGroupView } from './Components/ApplicationGroupView'
|
import { ApplicationGroupView } from './Components/ApplicationGroupView'
|
||||||
import { Bridge } from './Services/Bridge'
|
import { WebDevice } from './Device/WebDevice'
|
||||||
import { BrowserBridge } from './Services/BrowserBridge'
|
import { StartApplication } from './Device/StartApplication'
|
||||||
import { StartApplication } from './StartApplication'
|
|
||||||
import { ApplicationGroup } from './UIModels/ApplicationGroup'
|
import { ApplicationGroup } from './UIModels/ApplicationGroup'
|
||||||
import { isDev } from './Utils'
|
import { isDev } from './Utils'
|
||||||
|
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
|
||||||
|
|
||||||
const startApplication: StartApplication = async function startApplication(
|
const startApplication: StartApplication = async function startApplication(
|
||||||
defaultSyncServerHost: string,
|
defaultSyncServerHost: string,
|
||||||
bridge: Bridge,
|
device: WebOrDesktopDevice,
|
||||||
enableUnfinishedFeatures: boolean,
|
enableUnfinishedFeatures: boolean,
|
||||||
webSocketUrl: string,
|
webSocketUrl: string,
|
||||||
) {
|
) {
|
||||||
@@ -37,7 +37,7 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
|
|
||||||
const mainApplicationGroup = new ApplicationGroup(
|
const mainApplicationGroup = new ApplicationGroup(
|
||||||
defaultSyncServerHost,
|
defaultSyncServerHost,
|
||||||
bridge,
|
device,
|
||||||
enableUnfinishedFeatures ? Runtime.Dev : Runtime.Prod,
|
enableUnfinishedFeatures ? Runtime.Dev : Runtime.Prod,
|
||||||
webSocketUrl,
|
webSocketUrl,
|
||||||
)
|
)
|
||||||
@@ -70,7 +70,7 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
if (IsWebPlatform) {
|
if (IsWebPlatform) {
|
||||||
startApplication(
|
startApplication(
|
||||||
window.defaultSyncServer,
|
window.defaultSyncServer,
|
||||||
new BrowserBridge(WebAppVersion),
|
new WebDevice(WebAppVersion),
|
||||||
window.enabledUnfinishedFeatures,
|
window.enabledUnfinishedFeatures,
|
||||||
window.websocketUrl,
|
window.websocketUrl,
|
||||||
).catch(console.error)
|
).catch(console.error)
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ export class ApplicationView extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadApplication() {
|
async loadApplication() {
|
||||||
this.application.componentManager.setDesktopManager(this.application.getDesktopService())
|
const desktopService = this.application.getDesktopService()
|
||||||
|
if (desktopService) {
|
||||||
|
this.application.componentManager.setDesktopManager(desktopService)
|
||||||
|
}
|
||||||
|
|
||||||
await this.application.prepareForLaunch({
|
await this.application.prepareForLaunch({
|
||||||
receiveChallenge: async (challenge) => {
|
receiveChallenge: async (challenge) => {
|
||||||
const challenges = this.state.challenges.slice()
|
const challenges = this.state.challenges.slice()
|
||||||
@@ -67,6 +71,7 @@ export class ApplicationView extends PureComponent<Props, State> {
|
|||||||
this.setState({ challenges: challenges })
|
this.setState({ challenges: challenges })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.application.launch()
|
await this.application.launch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,14 +168,14 @@ export const ComponentView: FunctionalComponent<IProps> = observer(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unregisterDesktopObserver = application
|
const unregisterDesktopObserver = application
|
||||||
.getDesktopService()
|
.getDesktopService()
|
||||||
.registerUpdateObserver((updatedComponent: SNComponent) => {
|
?.registerUpdateObserver((updatedComponent: SNComponent) => {
|
||||||
if (updatedComponent.uuid === component.uuid && updatedComponent.active) {
|
if (updatedComponent.uuid === component.uuid && updatedComponent.active) {
|
||||||
requestReload?.(componentViewer)
|
requestReload?.(componentViewer)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unregisterDesktopObserver()
|
unregisterDesktopObserver?.()
|
||||||
}
|
}
|
||||||
}, [application, requestReload, componentViewer, component.uuid])
|
}, [application, requestReload, componentViewer, component.uuid])
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export const ConfirmSignoutModal = observer(({ application, appState }: Props) =
|
|||||||
const [localBackupsCount, setLocalBackupsCount] = useState(0)
|
const [localBackupsCount, setLocalBackupsCount] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
application.bridge.localBackupsCount().then(setLocalBackupsCount).catch(console.error)
|
application.desktopDevice?.localBackupsCount().then(setLocalBackupsCount).catch(console.error)
|
||||||
}, [appState.accountMenu.signingOut, application.bridge])
|
}, [appState.accountMenu.signingOut, application.desktopDevice])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}>
|
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}>
|
||||||
@@ -44,6 +44,7 @@ export const ConfirmSignoutModal = observer(({ application, appState }: Props) =
|
|||||||
<AlertDialogDescription className="sk-panel-row">
|
<AlertDialogDescription className="sk-panel-row">
|
||||||
<p className="color-foreground">{STRING_SIGN_OUT_CONFIRMATION}</p>
|
<p className="color-foreground">{STRING_SIGN_OUT_CONFIRMATION}</p>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|
||||||
{localBackupsCount > 0 && (
|
{localBackupsCount > 0 && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="sk-panel-row"></div>
|
<div className="sk-panel-row"></div>
|
||||||
@@ -63,13 +64,14 @@ export const ConfirmSignoutModal = observer(({ application, appState }: Props) =
|
|||||||
<button
|
<button
|
||||||
className="capitalize sk-a ml-1.5 p-0 rounded cursor-pointer"
|
className="capitalize sk-a ml-1.5 p-0 rounded cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
application.bridge.viewlocalBackups()
|
application.desktopDevice?.viewlocalBackups()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
View backup files
|
View backup files
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex my-1 mt-4">
|
<div className="flex my-1 mt-4">
|
||||||
<button className="sn-button small neutral" ref={cancelRef} onClick={closeDialog}>
|
<button className="sn-button small neutral" ref={cancelRef} onClick={closeDialog}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export class NoteView extends PureComponent<Props, State> {
|
|||||||
this.controller = props.controller
|
this.controller = props.controller
|
||||||
|
|
||||||
this.onEditorComponentLoad = () => {
|
this.onEditorComponentLoad = () => {
|
||||||
this.application.getDesktopService().redoSearch()
|
this.application.getDesktopService()?.redoSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
|
||||||
|
|||||||
11
app/assets/javascripts/Device/DesktopDeviceInterface.ts
Normal file
11
app/assets/javascripts/Device/DesktopDeviceInterface.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { DeviceInterface, Environment } from '@standardnotes/snjs'
|
||||||
|
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
|
||||||
|
import { WebCommunicationReceiver } from './DesktopWebCommunication'
|
||||||
|
|
||||||
|
export function isDesktopDevice(x: DeviceInterface): x is DesktopDeviceInterface {
|
||||||
|
return x.environment === Environment.Desktop
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DesktopDeviceInterface extends WebOrDesktopDevice, WebCommunicationReceiver {
|
||||||
|
environment: Environment.Desktop
|
||||||
|
}
|
||||||
1
app/assets/javascripts/Device/DesktopSnjsExports.ts
Normal file
1
app/assets/javascripts/Device/DesktopSnjsExports.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Environment, RawKeychainValue } from '@standardnotes/snjs'
|
||||||
44
app/assets/javascripts/Device/DesktopWebCommunication.ts
Normal file
44
app/assets/javascripts/Device/DesktopWebCommunication.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { DecryptedTransferPayload } from '@standardnotes/snjs'
|
||||||
|
|
||||||
|
/** Receives communications emitted by Web Core. This would be the Desktop client. */
|
||||||
|
export interface WebCommunicationReceiver {
|
||||||
|
localBackupsCount(): Promise<number>
|
||||||
|
|
||||||
|
viewlocalBackups(): void
|
||||||
|
|
||||||
|
deleteLocalBackups(): Promise<void>
|
||||||
|
|
||||||
|
syncComponents(payloads: unknown[]): void
|
||||||
|
|
||||||
|
onMajorDataChange(): void
|
||||||
|
|
||||||
|
onInitialDataLoad(): void
|
||||||
|
|
||||||
|
onSignOut(): void
|
||||||
|
|
||||||
|
onSearch(text?: string): void
|
||||||
|
|
||||||
|
downloadBackup(): void | Promise<void>
|
||||||
|
|
||||||
|
get extensionsServerHost(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Receives communications emitted by the desktop client. This would be Web Core. */
|
||||||
|
export interface DesktopCommunicationReceiver {
|
||||||
|
updateAvailable(): void
|
||||||
|
|
||||||
|
windowGainedFocus(): void
|
||||||
|
|
||||||
|
windowLostFocus(): void
|
||||||
|
|
||||||
|
onComponentInstallationComplete(
|
||||||
|
componentData: DecryptedTransferPayload,
|
||||||
|
error: unknown,
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
|
requestBackupFile(): Promise<string | undefined>
|
||||||
|
|
||||||
|
didBeginBackup(): void
|
||||||
|
|
||||||
|
didFinishBackup(success: boolean): void
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Bridge } from './Services/Bridge'
|
import { WebOrDesktopDevice } from './WebOrDesktopDevice'
|
||||||
|
|
||||||
export type StartApplication = (
|
export type StartApplication = (
|
||||||
defaultSyncServerHost: string,
|
defaultSyncServerHost: string,
|
||||||
bridge: Bridge,
|
device: WebOrDesktopDevice,
|
||||||
enableUnfinishedFeatures: boolean,
|
enableUnfinishedFeatures: boolean,
|
||||||
webSocketUrl: string,
|
webSocketUrl: string,
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
26
app/assets/javascripts/Device/WebDevice.ts
Normal file
26
app/assets/javascripts/Device/WebDevice.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Environment, RawKeychainValue } from '@standardnotes/snjs'
|
||||||
|
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
|
||||||
|
|
||||||
|
const KEYCHAIN_STORAGE_KEY = 'keychain'
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,60 @@
|
|||||||
import {
|
import {
|
||||||
getGlobalScope,
|
|
||||||
SNApplication,
|
SNApplication,
|
||||||
ApplicationIdentifier,
|
ApplicationIdentifier,
|
||||||
AbstractDevice,
|
Environment,
|
||||||
|
LegacyRawKeychainValue,
|
||||||
|
RawKeychainValue,
|
||||||
|
TransferPayload,
|
||||||
|
NamespacedRootKeyInKeychain,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { Database } from '@/Database'
|
import { Database } from '../Database'
|
||||||
import { Bridge } from './Services/Bridge'
|
import { WebOrDesktopDeviceInterface } from './WebOrDesktopDeviceInterface'
|
||||||
|
|
||||||
|
export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface {
|
||||||
|
constructor(public appVersion: string) {}
|
||||||
|
|
||||||
export class WebDeviceInterface extends AbstractDevice {
|
|
||||||
private databases: Database[] = []
|
private databases: Database[] = []
|
||||||
|
|
||||||
constructor(private bridge: Bridge) {
|
abstract environment: Environment
|
||||||
super(setTimeout.bind(getGlobalScope()), setInterval.bind(getGlobalScope()))
|
|
||||||
}
|
|
||||||
|
|
||||||
setApplication(application: SNApplication) {
|
setApplication(application: SNApplication) {
|
||||||
const database = new Database(application.identifier, application.alertService)
|
const database = new Database(application.identifier, application.alertService)
|
||||||
|
|
||||||
this.databases.push(database)
|
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) {
|
private databaseForIdentifier(identifier: ApplicationIdentifier) {
|
||||||
return this.databases.find((database) => database.databaseName === identifier) as Database
|
return this.databases.find((database) => database.databaseName === identifier) as Database
|
||||||
}
|
}
|
||||||
|
|
||||||
override deinit() {
|
deinit() {
|
||||||
super.deinit()
|
|
||||||
for (const database of this.databases) {
|
for (const database of this.databases) {
|
||||||
database.deinit()
|
database.deinit()
|
||||||
}
|
}
|
||||||
this.databases = []
|
this.databases = []
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRawStorageValue(key: string) {
|
async getRawStorageValue(key: string): Promise<string | undefined> {
|
||||||
return localStorage.getItem(key) as any
|
const result = localStorage.getItem(key)
|
||||||
|
|
||||||
|
if (result == undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllRawStorageKeyValues() {
|
async getAllRawStorageKeyValues() {
|
||||||
@@ -46,7 +68,7 @@ export class WebDeviceInterface extends AbstractDevice {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRawStorageValue(key: string, value: any) {
|
async setRawStorageValue(key: string, value: string) {
|
||||||
localStorage.setItem(key, value)
|
localStorage.setItem(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +100,11 @@ export class WebDeviceInterface extends AbstractDevice {
|
|||||||
return this.databaseForIdentifier(identifier).getAllPayloads()
|
return this.databaseForIdentifier(identifier).getAllPayloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveRawDatabasePayload(payload: any, identifier: ApplicationIdentifier) {
|
async saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier) {
|
||||||
return this.databaseForIdentifier(identifier).savePayload(payload)
|
return this.databaseForIdentifier(identifier).savePayload(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveRawDatabasePayloads(payloads: any[], identifier: ApplicationIdentifier) {
|
async saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier) {
|
||||||
return this.databaseForIdentifier(identifier).savePayloads(payloads)
|
return this.databaseForIdentifier(identifier).savePayloads(payloads)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,43 +117,44 @@ export class WebDeviceInterface extends AbstractDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getNamespacedKeychainValue(identifier: ApplicationIdentifier) {
|
async getNamespacedKeychainValue(identifier: ApplicationIdentifier) {
|
||||||
const keychain = await this.getRawKeychainValue()
|
const keychain = await this.getKeychainValue()
|
||||||
|
|
||||||
if (!keychain) {
|
if (!keychain) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return keychain[identifier]
|
return keychain[identifier]
|
||||||
}
|
}
|
||||||
|
|
||||||
async setNamespacedKeychainValue(value: any, identifier: ApplicationIdentifier) {
|
async setNamespacedKeychainValue(
|
||||||
let keychain = await this.getRawKeychainValue()
|
value: NamespacedRootKeyInKeychain,
|
||||||
|
identifier: ApplicationIdentifier,
|
||||||
|
) {
|
||||||
|
let keychain = await this.getKeychainValue()
|
||||||
|
|
||||||
if (!keychain) {
|
if (!keychain) {
|
||||||
keychain = {}
|
keychain = {}
|
||||||
}
|
}
|
||||||
return this.bridge.setKeychainValue({
|
|
||||||
|
return this.setKeychainValue({
|
||||||
...keychain,
|
...keychain,
|
||||||
[identifier]: value,
|
[identifier]: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearNamespacedKeychainValue(identifier: ApplicationIdentifier) {
|
async clearNamespacedKeychainValue(identifier: ApplicationIdentifier) {
|
||||||
const keychain = await this.getRawKeychainValue()
|
const keychain = await this.getKeychainValue()
|
||||||
if (!keychain) {
|
if (!keychain) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
delete keychain[identifier]
|
delete keychain[identifier]
|
||||||
return this.bridge.setKeychainValue(keychain)
|
|
||||||
|
return this.setKeychainValue(keychain)
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawKeychainValue(): Promise<any> {
|
setRawKeychainValue(value: unknown): Promise<void> {
|
||||||
return this.bridge.getKeychainValue()
|
return this.setKeychainValue(value)
|
||||||
}
|
|
||||||
|
|
||||||
legacy_setRawKeychainValue(value: unknown): Promise<any> {
|
|
||||||
return this.bridge.setKeychainValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearRawKeychainValue() {
|
|
||||||
return this.bridge.clearKeychainValue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openUrl(url: string) {
|
openUrl(url: string) {
|
||||||
@@ -140,4 +163,14 @@ export class WebDeviceInterface extends AbstractDevice {
|
|||||||
win.focus()
|
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>
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { DeviceInterface, RawKeychainValue } from '@standardnotes/snjs'
|
||||||
|
|
||||||
|
export interface WebOrDesktopDeviceInterface extends DeviceInterface {
|
||||||
|
readonly appVersion: string
|
||||||
|
|
||||||
|
getKeychainValue(): Promise<RawKeychainValue>
|
||||||
|
|
||||||
|
setKeychainValue(value: RawKeychainValue): Promise<void>
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file will be imported by desktop, so we make sure imports are carrying
|
|
||||||
* as little extra code as possible with them.
|
|
||||||
*/
|
|
||||||
import { Environment } from '@standardnotes/snjs'
|
|
||||||
|
|
||||||
export interface ElectronDesktopCallbacks {
|
|
||||||
desktop_updateAvailable(): void
|
|
||||||
desktop_windowGainedFocus(): void
|
|
||||||
desktop_windowLostFocus(): void
|
|
||||||
desktop_onComponentInstallationComplete(componentData: any, error: any): Promise<void>
|
|
||||||
desktop_requestBackupFile(): Promise<string | undefined>
|
|
||||||
desktop_didBeginBackup(): void
|
|
||||||
desktop_didFinishBackup(success: boolean): void
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Platform-specific (i-e Electron/browser) behavior is handled by a Bridge object. */
|
|
||||||
export interface Bridge {
|
|
||||||
readonly appVersion: string
|
|
||||||
environment: Environment
|
|
||||||
|
|
||||||
getKeychainValue(): Promise<unknown>
|
|
||||||
setKeychainValue(value: unknown): Promise<void>
|
|
||||||
clearKeychainValue(): Promise<void>
|
|
||||||
|
|
||||||
localBackupsCount(): Promise<number>
|
|
||||||
viewlocalBackups(): void
|
|
||||||
deleteLocalBackups(): Promise<void>
|
|
||||||
|
|
||||||
extensionsServerHost?: string
|
|
||||||
syncComponents(payloads: unknown[]): void
|
|
||||||
onMajorDataChange(): void
|
|
||||||
onInitialDataLoad(): void
|
|
||||||
onSignOut(): void
|
|
||||||
onSearch(text?: string): void
|
|
||||||
downloadBackup(): void | Promise<void>
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Bridge } from './Bridge'
|
|
||||||
import { Environment } from '@standardnotes/snjs'
|
|
||||||
|
|
||||||
const KEYCHAIN_STORAGE_KEY = 'keychain'
|
|
||||||
|
|
||||||
export class BrowserBridge implements Bridge {
|
|
||||||
constructor(public appVersion: string) {}
|
|
||||||
environment = Environment.Web
|
|
||||||
|
|
||||||
async getKeychainValue(): Promise<unknown> {
|
|
||||||
const value = localStorage.getItem(KEYCHAIN_STORAGE_KEY)
|
|
||||||
if (value) {
|
|
||||||
return JSON.parse(value)
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
async setKeychainValue(value: unknown): Promise<void> {
|
|
||||||
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearKeychainValue(): Promise<void> {
|
|
||||||
localStorage.removeItem(KEYCHAIN_STORAGE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
async localBackupsCount(): Promise<number> {
|
|
||||||
/** Browsers cannot save backups, only let you download one */
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/** No-ops */
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
||||||
async deleteLocalBackups(): Promise<void> {}
|
|
||||||
viewlocalBackups(): void {}
|
|
||||||
syncComponents(): void {}
|
|
||||||
onMajorDataChange(): void {}
|
|
||||||
onInitialDataLoad(): void {}
|
|
||||||
onSearch(): void {}
|
|
||||||
downloadBackup(): void {}
|
|
||||||
onSignOut(): void {}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable camelcase */
|
|
||||||
import {
|
import {
|
||||||
SNComponent,
|
SNComponent,
|
||||||
ComponentMutator,
|
ComponentMutator,
|
||||||
@@ -8,27 +7,26 @@ import {
|
|||||||
removeFromArray,
|
removeFromArray,
|
||||||
DesktopManagerInterface,
|
DesktopManagerInterface,
|
||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
|
DecryptedTransferPayload,
|
||||||
|
ComponentContent,
|
||||||
|
assert,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { WebAppEvent, WebApplication } from '@/UIModels/Application'
|
import { WebAppEvent, WebApplication } from '@/UIModels/Application'
|
||||||
import { isDesktopApplication } from '@/Utils'
|
import { DesktopDeviceInterface } from '../Device/DesktopDeviceInterface'
|
||||||
import { Bridge, ElectronDesktopCallbacks } from './Bridge'
|
import { DesktopCommunicationReceiver } from '@/Device/DesktopWebCommunication'
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface used by the Desktop application to interact with SN
|
|
||||||
*/
|
|
||||||
export class DesktopManager
|
export class DesktopManager
|
||||||
extends ApplicationService
|
extends ApplicationService
|
||||||
implements DesktopManagerInterface, ElectronDesktopCallbacks
|
implements DesktopManagerInterface, DesktopCommunicationReceiver
|
||||||
{
|
{
|
||||||
updateObservers: {
|
updateObservers: {
|
||||||
callback: (component: SNComponent) => void
|
callback: (component: SNComponent) => void
|
||||||
}[] = []
|
}[] = []
|
||||||
|
|
||||||
isDesktop = isDesktopApplication()
|
|
||||||
dataLoaded = false
|
dataLoaded = false
|
||||||
lastSearchedText?: string
|
lastSearchedText?: string
|
||||||
|
|
||||||
constructor(application: WebApplication, private bridge: Bridge) {
|
constructor(application: WebApplication, private device: DesktopDeviceInterface) {
|
||||||
super(application, new InternalEventBus())
|
super(application, new InternalEventBus())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,19 +43,20 @@ export class DesktopManager
|
|||||||
super.onAppEvent(eventName).catch(console.error)
|
super.onAppEvent(eventName).catch(console.error)
|
||||||
if (eventName === ApplicationEvent.LocalDataLoaded) {
|
if (eventName === ApplicationEvent.LocalDataLoaded) {
|
||||||
this.dataLoaded = true
|
this.dataLoaded = true
|
||||||
this.bridge.onInitialDataLoad()
|
this.device.onInitialDataLoad()
|
||||||
} else if (eventName === ApplicationEvent.MajorDataChange) {
|
} else if (eventName === ApplicationEvent.MajorDataChange) {
|
||||||
this.bridge.onMajorDataChange()
|
this.device.onMajorDataChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveBackup() {
|
saveBackup() {
|
||||||
this.bridge.onMajorDataChange()
|
this.device.onMajorDataChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtServerHost(): string {
|
getExtServerHost(): string {
|
||||||
console.assert(!!this.bridge.extensionsServerHost, 'extServerHost is null')
|
assert(this.device.extensionsServerHost)
|
||||||
return this.bridge.extensionsServerHost as string
|
|
||||||
|
return this.device.extensionsServerHost
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,17 +69,13 @@ export class DesktopManager
|
|||||||
|
|
||||||
// All `components` should be installed
|
// All `components` should be installed
|
||||||
syncComponentsInstallation(components: SNComponent[]) {
|
syncComponentsInstallation(components: SNComponent[]) {
|
||||||
if (!this.isDesktop) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
components.map((component) => {
|
components.map((component) => {
|
||||||
return this.convertComponentForTransmission(component)
|
return this.convertComponentForTransmission(component)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then((payloads) => {
|
.then((payloads) => {
|
||||||
this.bridge.syncComponents(payloads)
|
this.device.syncComponents(payloads)
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
@@ -96,11 +91,8 @@ export class DesktopManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchText(text?: string) {
|
searchText(text?: string) {
|
||||||
if (!this.isDesktop) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.lastSearchedText = text
|
this.lastSearchedText = text
|
||||||
this.bridge.onSearch(text)
|
this.device.onSearch(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
redoSearch() {
|
redoSearch() {
|
||||||
@@ -109,23 +101,27 @@ export class DesktopManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_updateAvailable(): void {
|
updateAvailable(): void {
|
||||||
this.webApplication.notifyWebEvent(WebAppEvent.NewUpdateAvailable)
|
this.webApplication.notifyWebEvent(WebAppEvent.NewUpdateAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_windowGainedFocus(): void {
|
windowGainedFocus(): void {
|
||||||
this.webApplication.notifyWebEvent(WebAppEvent.DesktopWindowGainedFocus)
|
this.webApplication.notifyWebEvent(WebAppEvent.DesktopWindowGainedFocus)
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_windowLostFocus(): void {
|
windowLostFocus(): void {
|
||||||
this.webApplication.notifyWebEvent(WebAppEvent.DesktopWindowLostFocus)
|
this.webApplication.notifyWebEvent(WebAppEvent.DesktopWindowLostFocus)
|
||||||
}
|
}
|
||||||
|
|
||||||
async desktop_onComponentInstallationComplete(componentData: any, error: any) {
|
async onComponentInstallationComplete(
|
||||||
|
componentData: DecryptedTransferPayload<ComponentContent>,
|
||||||
|
error: unknown,
|
||||||
|
) {
|
||||||
const component = this.application.items.findItem(componentData.uuid)
|
const component = this.application.items.findItem(componentData.uuid)
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedComponent = await this.application.mutator.changeAndSaveItem(
|
const updatedComponent = await this.application.mutator.changeAndSaveItem(
|
||||||
component,
|
component,
|
||||||
(m) => {
|
(m) => {
|
||||||
@@ -133,7 +129,9 @@ export class DesktopManager
|
|||||||
if (error) {
|
if (error) {
|
||||||
mutator.setAppDataItem(AppDataField.ComponentInstallError, error)
|
mutator.setAppDataItem(AppDataField.ComponentInstallError, error)
|
||||||
} else {
|
} else {
|
||||||
mutator.local_url = componentData.content.local_url
|
// 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.package_info = componentData.content.package_info
|
||||||
mutator.setAppDataItem(AppDataField.ComponentInstallError, undefined)
|
mutator.setAppDataItem(AppDataField.ComponentInstallError, undefined)
|
||||||
}
|
}
|
||||||
@@ -146,7 +144,7 @@ export class DesktopManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async desktop_requestBackupFile(): Promise<string | undefined> {
|
async requestBackupFile(): Promise<string | undefined> {
|
||||||
const encrypted = this.application.hasProtectionSources()
|
const encrypted = this.application.hasProtectionSources()
|
||||||
const data = encrypted
|
const data = encrypted
|
||||||
? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||||
@@ -159,11 +157,11 @@ export class DesktopManager
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_didBeginBackup() {
|
didBeginBackup() {
|
||||||
this.webApplication.getAppState().beganBackupDownload()
|
this.webApplication.getAppState().beganBackupDownload()
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop_didFinishBackup(success: boolean) {
|
didFinishBackup(success: boolean) {
|
||||||
this.webApplication.getAppState().endedBackupDownload(success)
|
this.webApplication.getAppState().endedBackupDownload(success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const DefaultThemeIdentifier = 'Default'
|
|||||||
|
|
||||||
export class ThemeManager extends ApplicationService {
|
export class ThemeManager extends ApplicationService {
|
||||||
private activeThemes: UuidString[] = []
|
private activeThemes: UuidString[] = []
|
||||||
private unregisterDesktop!: () => void
|
private unregisterDesktop?: () => void
|
||||||
private unregisterStream!: () => void
|
private unregisterStream!: () => void
|
||||||
private lastUseDeviceThemeSettings = false
|
private lastUseDeviceThemeSettings = false
|
||||||
|
|
||||||
@@ -91,10 +91,12 @@ export class ThemeManager extends ApplicationService {
|
|||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
this.activeThemes.length = 0
|
this.activeThemes.length = 0
|
||||||
this.unregisterDesktop()
|
|
||||||
|
this.unregisterDesktop?.()
|
||||||
this.unregisterStream()
|
this.unregisterStream()
|
||||||
;(this.unregisterDesktop as unknown) = undefined
|
;(this.unregisterDesktop as unknown) = undefined
|
||||||
;(this.unregisterStream as unknown) = undefined
|
;(this.unregisterStream as unknown) = undefined
|
||||||
|
|
||||||
window
|
window
|
||||||
.matchMedia('(prefers-color-scheme: dark)')
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
.removeEventListener('change', this.colorSchemeEventHandler)
|
.removeEventListener('change', this.colorSchemeEventHandler)
|
||||||
@@ -212,7 +214,7 @@ export class ThemeManager extends ApplicationService {
|
|||||||
private registerObservers() {
|
private registerObservers() {
|
||||||
this.unregisterDesktop = this.webApplication
|
this.unregisterDesktop = this.webApplication
|
||||||
.getDesktopService()
|
.getDesktopService()
|
||||||
.registerUpdateObserver((component) => {
|
?.registerUpdateObserver((component) => {
|
||||||
if (component.active && component.isTheme()) {
|
if (component.active && component.isTheme()) {
|
||||||
this.deactivateTheme(component.uuid)
|
this.deactivateTheme(component.uuid)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Bridge } from '@/Services/Bridge'
|
|
||||||
import { storage, StorageKey } from '@/Services/LocalStorage'
|
import { storage, StorageKey } from '@/Services/LocalStorage'
|
||||||
import { WebApplication, WebAppEvent } from '@/UIModels/Application'
|
import { WebApplication, WebAppEvent } from '@/UIModels/Application'
|
||||||
import { AccountMenuState } from '@/UIModels/AppState/AccountMenuState'
|
import { AccountMenuState } from '@/UIModels/AppState/AccountMenuState'
|
||||||
@@ -32,6 +31,7 @@ import { SearchOptionsState } from './SearchOptionsState'
|
|||||||
import { SubscriptionState } from './SubscriptionState'
|
import { SubscriptionState } from './SubscriptionState'
|
||||||
import { SyncState } from './SyncState'
|
import { SyncState } from './SyncState'
|
||||||
import { TagsState } from './TagsState'
|
import { TagsState } from './TagsState'
|
||||||
|
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
|
||||||
|
|
||||||
export enum AppStateEvent {
|
export enum AppStateEvent {
|
||||||
TagChanged,
|
TagChanged,
|
||||||
@@ -91,7 +91,7 @@ export class AppState {
|
|||||||
|
|
||||||
private readonly tagChangedDisposer: IReactionDisposer
|
private readonly tagChangedDisposer: IReactionDisposer
|
||||||
|
|
||||||
constructor(application: WebApplication, private bridge: Bridge) {
|
constructor(application: WebApplication, private device: WebOrDesktopDevice) {
|
||||||
this.application = application
|
this.application = application
|
||||||
this.notes = new NotesState(
|
this.notes = new NotesState(
|
||||||
application,
|
application,
|
||||||
@@ -120,7 +120,7 @@ export class AppState {
|
|||||||
}
|
}
|
||||||
this.registerVisibilityObservers()
|
this.registerVisibilityObservers()
|
||||||
|
|
||||||
if (this.bridge.appVersion.includes('-beta')) {
|
if (this.device.appVersion.includes('-beta')) {
|
||||||
this.showBetaWarning = storage.get(StorageKey.ShowBetaWarning) ?? true
|
this.showBetaWarning = storage.get(StorageKey.ShowBetaWarning) ?? true
|
||||||
} else {
|
} else {
|
||||||
this.showBetaWarning = false
|
this.showBetaWarning = false
|
||||||
@@ -198,7 +198,7 @@ export class AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get version(): string {
|
public get version(): string {
|
||||||
return this.bridge.appVersion
|
return this.device.appVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
async openNewNote(title?: string) {
|
async openNewNote(title?: string) {
|
||||||
|
|||||||
@@ -379,8 +379,9 @@ export class NotesViewState {
|
|||||||
paginate = () => {
|
paginate = () => {
|
||||||
this.notesToDisplay += this.pageSize
|
this.notesToDisplay += this.pageSize
|
||||||
this.reloadNotes()
|
this.reloadNotes()
|
||||||
|
|
||||||
if (this.searchSubmitted) {
|
if (this.searchSubmitted) {
|
||||||
this.application.getDesktopService().searchText(this.noteFilterText)
|
this.application.getDesktopService()?.searchText(this.noteFilterText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,8 +483,9 @@ export class NotesViewState {
|
|||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isFiltering) {
|
if (this.isFiltering) {
|
||||||
this.application.getDesktopService().searchText(this.noteFilterText)
|
this.application.getDesktopService()?.searchText(this.noteFilterText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,9 +498,13 @@ export class NotesViewState {
|
|||||||
|
|
||||||
handleTagChange = () => {
|
handleTagChange = () => {
|
||||||
this.resetScrollPosition()
|
this.resetScrollPosition()
|
||||||
|
|
||||||
this.setShowDisplayOptionsMenu(false)
|
this.setShowDisplayOptionsMenu(false)
|
||||||
|
|
||||||
this.setNoteFilterText('')
|
this.setNoteFilterText('')
|
||||||
this.application.getDesktopService().searchText()
|
|
||||||
|
this.application.getDesktopService()?.searchText()
|
||||||
|
|
||||||
this.resetPagination()
|
this.resetPagination()
|
||||||
|
|
||||||
/* Capture db load state before beginning reloadNotes,
|
/* Capture db load state before beginning reloadNotes,
|
||||||
@@ -525,7 +531,8 @@ export class NotesViewState {
|
|||||||
* enter before highlighting desktop search results.
|
* enter before highlighting desktop search results.
|
||||||
*/
|
*/
|
||||||
this.searchSubmitted = true
|
this.searchSubmitted = true
|
||||||
this.application.getDesktopService().searchText(this.noteFilterText)
|
|
||||||
|
this.application.getDesktopService()?.searchText(this.noteFilterText)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFilterTextChanged = () => {
|
handleFilterTextChanged = () => {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { WebCrypto } from '@/Crypto'
|
|||||||
import { AlertService } from '@/Services/AlertService'
|
import { AlertService } from '@/Services/AlertService'
|
||||||
import { ArchiveManager } from '@/Services/ArchiveManager'
|
import { ArchiveManager } from '@/Services/ArchiveManager'
|
||||||
import { AutolockService } from '@/Services/AutolockService'
|
import { AutolockService } from '@/Services/AutolockService'
|
||||||
import { Bridge } from '@/Services/Bridge'
|
import { DesktopDeviceInterface, isDesktopDevice } from '@/Device/DesktopDeviceInterface'
|
||||||
import { DesktopManager } from '@/Services/DesktopManager'
|
import { DesktopManager } from '@/Services/DesktopManager'
|
||||||
import { IOService } from '@/Services/IOService'
|
import { IOService } from '@/Services/IOService'
|
||||||
import { StatusManager } from '@/Services/StatusManager'
|
import { StatusManager } from '@/Services/StatusManager'
|
||||||
import { ThemeManager } from '@/Services/ThemeManager'
|
import { ThemeManager } from '@/Services/ThemeManager'
|
||||||
import { AppState } from '@/UIModels/AppState'
|
import { AppState } from '@/UIModels/AppState'
|
||||||
import { WebDeviceInterface } from '@/WebDeviceInterface'
|
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
|
||||||
import {
|
import {
|
||||||
DeinitSource,
|
DeinitSource,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
|
|
||||||
type WebServices = {
|
type WebServices = {
|
||||||
appState: AppState
|
appState: AppState
|
||||||
desktopService: DesktopManager
|
desktopService?: DesktopManager
|
||||||
autolockService: AutolockService
|
autolockService: AutolockService
|
||||||
archiveService: ArchiveManager
|
archiveService: ArchiveManager
|
||||||
statusManager: StatusManager
|
statusManager: StatusManager
|
||||||
@@ -44,26 +44,26 @@ export class WebApplication extends SNApplication {
|
|||||||
public iconsController: IconsController
|
public iconsController: IconsController
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
deviceInterface: WebDeviceInterface,
|
deviceInterface: WebOrDesktopDevice,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
identifier: string,
|
identifier: string,
|
||||||
defaultSyncServerHost: string,
|
defaultSyncServerHost: string,
|
||||||
public bridge: Bridge,
|
|
||||||
webSocketUrl: string,
|
webSocketUrl: string,
|
||||||
runtime: Runtime,
|
runtime: Runtime,
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
environment: bridge.environment,
|
environment: deviceInterface.environment,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
deviceInterface: deviceInterface,
|
deviceInterface: deviceInterface,
|
||||||
crypto: WebCrypto,
|
crypto: WebCrypto,
|
||||||
alertService: new AlertService(),
|
alertService: new AlertService(),
|
||||||
identifier,
|
identifier,
|
||||||
defaultHost: defaultSyncServerHost,
|
defaultHost: defaultSyncServerHost,
|
||||||
appVersion: bridge.appVersion,
|
appVersion: deviceInterface.appVersion,
|
||||||
webSocketUrl: webSocketUrl,
|
webSocketUrl: webSocketUrl,
|
||||||
runtime,
|
runtime,
|
||||||
})
|
})
|
||||||
|
|
||||||
deviceInterface.setApplication(this)
|
deviceInterface.setApplication(this)
|
||||||
this.noteControllerGroup = new NoteGroupController(this)
|
this.noteControllerGroup = new NoteGroupController(this)
|
||||||
this.iconsController = new IconsController()
|
this.iconsController = new IconsController()
|
||||||
@@ -80,7 +80,7 @@ export class WebApplication extends SNApplication {
|
|||||||
if ('deinit' in service) {
|
if ('deinit' in service) {
|
||||||
service.deinit?.(source)
|
service.deinit?.(source)
|
||||||
}
|
}
|
||||||
;(service as any).application = undefined
|
;(service as { application?: WebApplication }).application = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
this.webServices = {} as WebServices
|
this.webServices = {} as WebServices
|
||||||
@@ -88,7 +88,7 @@ export class WebApplication extends SNApplication {
|
|||||||
this.webEventObservers.length = 0
|
this.webEventObservers.length = 0
|
||||||
|
|
||||||
if (source === DeinitSource.SignOut) {
|
if (source === DeinitSource.SignOut) {
|
||||||
this.bridge.onSignOut()
|
isDesktopDevice(this.deviceInterface) && this.deviceInterface.onSignOut()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error while deiniting application', error)
|
console.error('Error while deiniting application', error)
|
||||||
@@ -116,7 +116,7 @@ export class WebApplication extends SNApplication {
|
|||||||
return this.webServices.appState
|
return this.webServices.appState
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDesktopService(): DesktopManager {
|
public getDesktopService(): DesktopManager | undefined {
|
||||||
return this.webServices.desktopService
|
return this.webServices.desktopService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +128,14 @@ export class WebApplication extends SNApplication {
|
|||||||
return this.webServices.archiveService
|
return this.webServices.archiveService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get desktopDevice(): DesktopDeviceInterface | undefined {
|
||||||
|
if (isDesktopDevice(this.deviceInterface)) {
|
||||||
|
return this.deviceInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
getStatusManager() {
|
getStatusManager() {
|
||||||
return this.webServices.statusManager
|
return this.webServices.statusManager
|
||||||
}
|
}
|
||||||
@@ -145,11 +153,14 @@ export class WebApplication extends SNApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadBackup(): void | Promise<void> {
|
downloadBackup(): void | Promise<void> {
|
||||||
return this.bridge.downloadBackup()
|
if (isDesktopDevice(this.deviceInterface)) {
|
||||||
|
return this.deviceInterface.downloadBackup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signOutAndDeleteLocalBackups(): Promise<void> {
|
async signOutAndDeleteLocalBackups(): Promise<void> {
|
||||||
await this.bridge.deleteLocalBackups()
|
isDesktopDevice(this.deviceInterface) && (await this.deviceInterface.deleteLocalBackups())
|
||||||
|
|
||||||
return this.user.signOut()
|
return this.user.signOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { WebDeviceInterface } from '@/WebDeviceInterface'
|
|
||||||
import { WebApplication } from './Application'
|
import { WebApplication } from './Application'
|
||||||
import {
|
import {
|
||||||
ApplicationDescriptor,
|
ApplicationDescriptor,
|
||||||
SNApplicationGroup,
|
SNApplicationGroup,
|
||||||
DeviceInterface,
|
|
||||||
Platform,
|
Platform,
|
||||||
Runtime,
|
Runtime,
|
||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { AppState } from '@/UIModels/AppState'
|
import { AppState } from '@/UIModels/AppState'
|
||||||
import { Bridge } from '@/Services/Bridge'
|
|
||||||
import { getPlatform, isDesktopApplication } from '@/Utils'
|
import { getPlatform, isDesktopApplication } from '@/Utils'
|
||||||
import { ArchiveManager } from '@/Services/ArchiveManager'
|
import { ArchiveManager } from '@/Services/ArchiveManager'
|
||||||
import { DesktopManager } from '@/Services/DesktopManager'
|
import { DesktopManager } from '@/Services/DesktopManager'
|
||||||
@@ -17,15 +14,17 @@ import { IOService } from '@/Services/IOService'
|
|||||||
import { AutolockService } from '@/Services/AutolockService'
|
import { AutolockService } from '@/Services/AutolockService'
|
||||||
import { StatusManager } from '@/Services/StatusManager'
|
import { StatusManager } from '@/Services/StatusManager'
|
||||||
import { ThemeManager } from '@/Services/ThemeManager'
|
import { ThemeManager } from '@/Services/ThemeManager'
|
||||||
|
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
|
||||||
|
import { isDesktopDevice } from '@/Device/DesktopDeviceInterface'
|
||||||
|
|
||||||
export class ApplicationGroup extends SNApplicationGroup {
|
export class ApplicationGroup extends SNApplicationGroup<WebOrDesktopDevice> {
|
||||||
constructor(
|
constructor(
|
||||||
private defaultSyncServerHost: string,
|
private defaultSyncServerHost: string,
|
||||||
private bridge: Bridge,
|
private device: WebOrDesktopDevice,
|
||||||
private runtime: Runtime,
|
private runtime: Runtime,
|
||||||
private webSocketUrl: string,
|
private webSocketUrl: string,
|
||||||
) {
|
) {
|
||||||
super(new WebDeviceInterface(bridge))
|
super(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
override async initialize(): Promise<void> {
|
override async initialize(): Promise<void> {
|
||||||
@@ -34,7 +33,7 @@ export class ApplicationGroup extends SNApplicationGroup {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (isDesktopApplication()) {
|
if (isDesktopApplication()) {
|
||||||
Object.defineProperty(window, 'desktopManager', {
|
Object.defineProperty(window, 'desktopCommunicationReceiver', {
|
||||||
get: () => (this.primaryApplication as WebApplication).getDesktopService(),
|
get: () => (this.primaryApplication as WebApplication).getDesktopService(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -42,34 +41,36 @@ export class ApplicationGroup extends SNApplicationGroup {
|
|||||||
|
|
||||||
private createApplication = (
|
private createApplication = (
|
||||||
descriptor: ApplicationDescriptor,
|
descriptor: ApplicationDescriptor,
|
||||||
deviceInterface: DeviceInterface,
|
deviceInterface: WebOrDesktopDevice,
|
||||||
) => {
|
) => {
|
||||||
const platform = getPlatform()
|
const platform = getPlatform()
|
||||||
const application = new WebApplication(
|
const application = new WebApplication(
|
||||||
deviceInterface as WebDeviceInterface,
|
deviceInterface,
|
||||||
platform,
|
platform,
|
||||||
descriptor.identifier,
|
descriptor.identifier,
|
||||||
this.defaultSyncServerHost,
|
this.defaultSyncServerHost,
|
||||||
this.bridge,
|
|
||||||
this.webSocketUrl,
|
this.webSocketUrl,
|
||||||
this.runtime,
|
this.runtime,
|
||||||
)
|
)
|
||||||
const appState = new AppState(application, this.bridge)
|
const appState = new AppState(application, this.device)
|
||||||
const archiveService = new ArchiveManager(application)
|
const archiveService = new ArchiveManager(application)
|
||||||
const desktopService = new DesktopManager(application, this.bridge)
|
|
||||||
const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop)
|
const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop)
|
||||||
const autolockService = new AutolockService(application, new InternalEventBus())
|
const autolockService = new AutolockService(application, new InternalEventBus())
|
||||||
const statusManager = new StatusManager()
|
const statusManager = new StatusManager()
|
||||||
const themeService = new ThemeManager(application)
|
const themeService = new ThemeManager(application)
|
||||||
|
|
||||||
application.setWebServices({
|
application.setWebServices({
|
||||||
appState,
|
appState,
|
||||||
archiveService,
|
archiveService,
|
||||||
desktopService,
|
desktopService: isDesktopDevice(this.device)
|
||||||
|
? new DesktopManager(application, this.device)
|
||||||
|
: undefined,
|
||||||
io,
|
io,
|
||||||
autolockService,
|
autolockService,
|
||||||
statusManager,
|
statusManager,
|
||||||
themeService,
|
themeService,
|
||||||
})
|
})
|
||||||
|
|
||||||
return application
|
return application
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@babel/plugin-transform-react-jsx": "^7.17.3",
|
"@babel/plugin-transform-react-jsx": "^7.17.3",
|
||||||
"@babel/preset-env": "^7.16.11",
|
"@babel/preset-env": "^7.16.11",
|
||||||
"@babel/preset-typescript": "^7.16.7",
|
"@babel/preset-typescript": "^7.16.7",
|
||||||
"@standardnotes/config": "^2.4.0",
|
"@standardnotes/config": "^2.4.1",
|
||||||
"@svgr/webpack": "^6.2.1",
|
"@svgr/webpack": "^6.2.1",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^27.4.1",
|
||||||
"@types/react": "^17.0.42",
|
"@types/react": "^17.0.42",
|
||||||
@@ -70,9 +70,9 @@
|
|||||||
"@reach/tooltip": "^0.16.2",
|
"@reach/tooltip": "^0.16.2",
|
||||||
"@reach/visually-hidden": "^0.16.0",
|
"@reach/visually-hidden": "^0.16.0",
|
||||||
"@standardnotes/components": "1.7.15",
|
"@standardnotes/components": "1.7.15",
|
||||||
"@standardnotes/filepicker": "1.12.0",
|
"@standardnotes/filepicker": "1.13.2",
|
||||||
"@standardnotes/sncrypto-web": "1.8.3",
|
"@standardnotes/sncrypto-web": "1.8.4",
|
||||||
"@standardnotes/snjs": "2.99.0",
|
"@standardnotes/snjs": "2.99.1",
|
||||||
"@standardnotes/stylekit": "5.23.0",
|
"@standardnotes/stylekit": "5.23.0",
|
||||||
"@zip.js/zip.js": "^2.4.7",
|
"@zip.js/zip.js": "^2.4.7",
|
||||||
"mobx": "^6.5.0",
|
"mobx": "^6.5.0",
|
||||||
|
|||||||
68
yarn.lock
68
yarn.lock
@@ -2406,10 +2406,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@standardnotes/components/-/components-1.7.15.tgz#3d57dd21cd4bf1f95eea7521fce7dd62e8f99455"
|
resolved "https://registry.yarnpkg.com/@standardnotes/components/-/components-1.7.15.tgz#3d57dd21cd4bf1f95eea7521fce7dd62e8f99455"
|
||||||
integrity sha512-fZOaqi62BDhVkqH6Bd9NQhVPyPR3KARob8xm/JhMLWt1d8G1ZFEQ/IOLTMFpYQCt9IroLG/pl6GD0Xj46ISgkw==
|
integrity sha512-fZOaqi62BDhVkqH6Bd9NQhVPyPR3KARob8xm/JhMLWt1d8G1ZFEQ/IOLTMFpYQCt9IroLG/pl6GD0Xj46ISgkw==
|
||||||
|
|
||||||
"@standardnotes/config@^2.4.0":
|
"@standardnotes/config@^2.4.1":
|
||||||
version "2.4.0"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/config/-/config-2.4.0.tgz#4ca7ccd857f23850a154911d274f0aa6bf6b7b05"
|
resolved "https://registry.yarnpkg.com/@standardnotes/config/-/config-2.4.1.tgz#df92445efbdd7ed99a70e011d7f32de35fc12f05"
|
||||||
integrity sha512-iCdGcC4cRB9+gJAjmA1tJr+dgTUDStG0OSleAOVqvaZmtj94rOzX/v8GmiqnsDajyZZXAmD+Ftse3Z3kOAPhTQ==
|
integrity sha512-NQ9ajPJOUwd0/sDuvNQH+4yVFM00oWRmBCceHFzKur7rtT50XUyelVesdlcUm12M9LRdVOf9jWb9iNSvvQjhvg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/eslint-plugin" "^5.12.1"
|
"@typescript-eslint/eslint-plugin" "^5.12.1"
|
||||||
"@typescript-eslint/parser" "^5.12.1"
|
"@typescript-eslint/parser" "^5.12.1"
|
||||||
@@ -2422,14 +2422,14 @@
|
|||||||
"@standardnotes/auth" "^3.18.11"
|
"@standardnotes/auth" "^3.18.11"
|
||||||
"@standardnotes/features" "^1.37.9"
|
"@standardnotes/features" "^1.37.9"
|
||||||
|
|
||||||
"@standardnotes/encryption@^1.4.8":
|
"@standardnotes/encryption@^1.4.9":
|
||||||
version "1.4.8"
|
version "1.4.9"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.4.8.tgz#9d9f118f80df1a241f505474c7af154190990b38"
|
resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.4.9.tgz#1e5f47dd0624e694eed0a851f703d8a620a02c82"
|
||||||
integrity sha512-AU4eFNzU7sJj+BLpyrUrOn3z6WmkLw1XriF0m8q+a/tzco2ez5j0bM4REMAFfpreOhFN5KSJ0yUTh8pLxwyZxA==
|
integrity sha512-Cdwusso9sPVxNo85HCicbZ46g5wmjMph+3WGEGbQ6/3Jb/FaGrH4WbwnNsHvPO9LvjI41wxBqb2iCbUxmsB+ZQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/models" "^1.4.8"
|
"@standardnotes/models" "^1.4.8"
|
||||||
"@standardnotes/responses" "^1.6.10"
|
"@standardnotes/responses" "^1.6.10"
|
||||||
"@standardnotes/services" "^1.9.9"
|
"@standardnotes/services" "^1.9.10"
|
||||||
|
|
||||||
"@standardnotes/features@^1.37.9":
|
"@standardnotes/features@^1.37.9":
|
||||||
version "1.37.9"
|
version "1.37.9"
|
||||||
@@ -2439,15 +2439,10 @@
|
|||||||
"@standardnotes/auth" "^3.18.11"
|
"@standardnotes/auth" "^3.18.11"
|
||||||
"@standardnotes/common" "^1.19.6"
|
"@standardnotes/common" "^1.19.6"
|
||||||
|
|
||||||
"@standardnotes/filepicker@1.12.0":
|
"@standardnotes/filepicker@1.13.2", "@standardnotes/filepicker@^1.13.2":
|
||||||
version "1.12.0"
|
version "1.13.2"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.12.0.tgz#6ee1224c8a7d039565a27950aa4e879a18fecebf"
|
resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.13.2.tgz#63fc12ae5c08c2704f76b6757a080d8fe385ba8c"
|
||||||
integrity sha512-67hsz3/xnOmvhxFVUurjuLU1jaMKEYXCwwGSls/NJxkXEX1WwWA9kvAwAnrJTRoFJAKVDyBglcLpRrFkOvEc9w==
|
integrity sha512-BlGabKd1uBvnnWTWXXexGhydZTX7mTWO/0O9slP8B+h9yUz0G9DYXDlWEGyMSf3/1YArqJOnAHLU+ID/R1lvSA==
|
||||||
|
|
||||||
"@standardnotes/filepicker@^1.13.1":
|
|
||||||
version "1.13.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/filepicker/-/filepicker-1.13.1.tgz#bad1e3434711e4894b08a40cc16ec4e631bf8476"
|
|
||||||
integrity sha512-oK5Ew9avEH9LO2YGW1fSlUHMx9wOYS0KeXt2UOnl0B+SwAiVQN14F6DfvTyZ+JlEqn8PXq0liUoRTN9Lk7Fbgg==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common" "^1.19.6"
|
"@standardnotes/common" "^1.19.6"
|
||||||
"@standardnotes/utils" "^1.6.2"
|
"@standardnotes/utils" "^1.6.2"
|
||||||
@@ -2470,10 +2465,10 @@
|
|||||||
"@standardnotes/common" "^1.19.6"
|
"@standardnotes/common" "^1.19.6"
|
||||||
"@standardnotes/features" "^1.37.9"
|
"@standardnotes/features" "^1.37.9"
|
||||||
|
|
||||||
"@standardnotes/services@^1.9.9":
|
"@standardnotes/services@^1.9.10":
|
||||||
version "1.9.9"
|
version "1.9.10"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.9.9.tgz#5ab11644dae227435e087dea5b9a37145db96a7d"
|
resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.9.10.tgz#000a5e3d75671a8b127bc7751d66c1a24b6443b2"
|
||||||
integrity sha512-moUI8s00zLJZum2MMvqft85Yk3ZDWQFuhZeC+o0Xt8Pp9RE6sLOhuAmS3/WnKPMww3/ggoy7mdOqZ2aIn+q1eg==
|
integrity sha512-ff/WEyIq2KhJX6pG8+/BpQnI9Ulo1sd+x/6FmWUHn5C6ukK8srb5Ev1SV8z1hXdkRD1ltFaQvQaPrAYNuG32aQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common" "^1.19.6"
|
"@standardnotes/common" "^1.19.6"
|
||||||
"@standardnotes/models" "^1.4.8"
|
"@standardnotes/models" "^1.4.8"
|
||||||
@@ -2485,39 +2480,34 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.14.1.tgz#8b6c84e2eeeab274a557bad25cb16a62d60916ec"
|
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.14.1.tgz#8b6c84e2eeeab274a557bad25cb16a62d60916ec"
|
||||||
integrity sha512-zRRBoflI77Zqa5ejZcXFPnvwVKf/gbLNtutB/pQE5SYlDMUgWtg6Qm8r4667Ue/nBUWZixCH0sznEN8XK8rCBw==
|
integrity sha512-zRRBoflI77Zqa5ejZcXFPnvwVKf/gbLNtutB/pQE5SYlDMUgWtg6Qm8r4667Ue/nBUWZixCH0sznEN8XK8rCBw==
|
||||||
|
|
||||||
"@standardnotes/sncrypto-common@^1.7.6":
|
|
||||||
version "1.7.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.7.6.tgz#8ff57f4eb8803d5631983d2f17f3c9ec2ec22011"
|
|
||||||
integrity sha512-jzS4dIIsIVCEw4e6AdoOm0L71NHbc/RcYktiFbVDrr2teQn8z7777PdzHVXUEX5vcmtaZ0VADnev39wm0K5Rvw==
|
|
||||||
|
|
||||||
"@standardnotes/sncrypto-common@^1.7.7":
|
"@standardnotes/sncrypto-common@^1.7.7":
|
||||||
version "1.7.7"
|
version "1.7.7"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.7.7.tgz#5970bbf09dc541a9894fec1101d5c92be5822ad1"
|
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.7.7.tgz#5970bbf09dc541a9894fec1101d5c92be5822ad1"
|
||||||
integrity sha512-ncpzrurFtgupbQvv6+aDts9Cebx57MMI78Dl2KnJJbFm+G47UJuxfX11q2aomBJdp2XdbWhQxCKOQPl5mmwWPQ==
|
integrity sha512-ncpzrurFtgupbQvv6+aDts9Cebx57MMI78Dl2KnJJbFm+G47UJuxfX11q2aomBJdp2XdbWhQxCKOQPl5mmwWPQ==
|
||||||
|
|
||||||
"@standardnotes/sncrypto-web@1.8.3":
|
"@standardnotes/sncrypto-web@1.8.4":
|
||||||
version "1.8.3"
|
version "1.8.4"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.8.3.tgz#788ddf484d4170d6ab69859067f4404085dda380"
|
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.8.4.tgz#82259f6b0210625a9188427f229b20c5ab9204ac"
|
||||||
integrity sha512-PoFOwqxkEnKp0Rw0qJJzt/Bk0+DPKXJIQl2jBjmqa3quqllVkGdL/5ot8JKXP01kf8RZqdg1hqBxmjIiJOJcbw==
|
integrity sha512-n/9uxI2KxYSohilPwgAJOW0zAaPEs97EB7kQ1An0YByIQue4D/+5HHIksqQXUmXlw8KZFTau6jd+v7l7L3dvOQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/sncrypto-common" "^1.7.6"
|
"@standardnotes/sncrypto-common" "^1.7.7"
|
||||||
buffer "^6.0.3"
|
buffer "^6.0.3"
|
||||||
libsodium-wrappers "^0.7.9"
|
libsodium-wrappers "^0.7.9"
|
||||||
|
|
||||||
"@standardnotes/snjs@2.99.0":
|
"@standardnotes/snjs@2.99.1":
|
||||||
version "2.99.0"
|
version "2.99.1"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.99.0.tgz#b9c51d68abb9d9c43ebe108c39229f0896e37f51"
|
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.99.1.tgz#8274b1d44a15538b3a43bf35794301306238d6cb"
|
||||||
integrity sha512-rn/F3YiHBKfzylHTY4U8YbvShX/yNegaHQpAbmvP6uXCXF8msLcppKnecPw5pweQiv+o6JNORSBFfP25C6sbrw==
|
integrity sha512-/DFF9YFLmyc7z34iRvAn/rXBSH5G3zuDhUeyTVWVvotbgY7zvTSD2/wtgE4nZ1pmG58UlxGTDtzTlzyuxW99Ow==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "^3.18.11"
|
"@standardnotes/auth" "^3.18.11"
|
||||||
"@standardnotes/common" "^1.19.6"
|
"@standardnotes/common" "^1.19.6"
|
||||||
"@standardnotes/domain-events" "^2.27.10"
|
"@standardnotes/domain-events" "^2.27.10"
|
||||||
"@standardnotes/encryption" "^1.4.8"
|
"@standardnotes/encryption" "^1.4.9"
|
||||||
"@standardnotes/features" "^1.37.9"
|
"@standardnotes/features" "^1.37.9"
|
||||||
"@standardnotes/filepicker" "^1.13.1"
|
"@standardnotes/filepicker" "^1.13.2"
|
||||||
"@standardnotes/models" "^1.4.8"
|
"@standardnotes/models" "^1.4.8"
|
||||||
"@standardnotes/responses" "^1.6.10"
|
"@standardnotes/responses" "^1.6.10"
|
||||||
"@standardnotes/services" "^1.9.9"
|
"@standardnotes/services" "^1.9.10"
|
||||||
"@standardnotes/settings" "^1.14.1"
|
"@standardnotes/settings" "^1.14.1"
|
||||||
"@standardnotes/sncrypto-common" "^1.7.7"
|
"@standardnotes/sncrypto-common" "^1.7.7"
|
||||||
"@standardnotes/utils" "^1.6.2"
|
"@standardnotes/utils" "^1.6.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user