feat(labs): super editor (#2001)

This commit is contained in:
Mo
2022-11-16 05:54:32 -06:00
committed by GitHub
parent f0c9f899e9
commit 59f8547a8d
89 changed files with 1021 additions and 615 deletions

View File

@@ -0,0 +1,95 @@
/**
* @jest-environment jsdom
*/
import {
Environment,
FeatureIdentifier,
namespacedKey,
Platform,
RawStorageKey,
SNComponent,
SNComponentManager,
SNLog,
SNTag,
} from '@standardnotes/snjs'
import { WebApplication } from '@/Application/Application'
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
describe('web application', () => {
let application: WebApplication
let componentManager: SNComponentManager
// eslint-disable-next-line no-console
SNLog.onLog = console.log
SNLog.onError = console.error
beforeEach(() => {
const identifier = '123'
window.matchMedia = jest.fn().mockReturnValue({ matches: true, addListener: jest.fn() })
const device = {
environment: Environment.Desktop,
appVersion: '1.2.3',
setApplication: jest.fn(),
openDatabase: jest.fn().mockReturnValue(Promise.resolve()),
getRawStorageValue: jest.fn().mockImplementation((key) => {
if (key === namespacedKey(identifier, RawStorageKey.SnjsVersion)) {
return '10.0.0'
}
return undefined
}),
setRawStorageValue: jest.fn(),
} as unknown as jest.Mocked<WebOrDesktopDevice>
application = new WebApplication(device, Platform.MacWeb, identifier, 'https://sync', 'https://socket')
componentManager = {} as jest.Mocked<SNComponentManager>
componentManager.legacyGetDefaultEditor = jest.fn()
Object.defineProperty(application, 'componentManager', { value: componentManager })
application.prepareForLaunch({ receiveChallenge: jest.fn() })
})
describe('geDefaultEditorIdentifier', () => {
it('should return plain editor if no default tag editor or component editor', () => {
const editorIdentifier = application.geDefaultEditorIdentifier()
expect(editorIdentifier).toEqual(FeatureIdentifier.PlainEditor)
})
it('should return pref key based value if available', () => {
application.getPreference = jest.fn().mockReturnValue(FeatureIdentifier.SuperEditor)
const editorIdentifier = application.geDefaultEditorIdentifier()
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
})
it('should return default tag identifier if tag supplied', () => {
const tag = {
preferences: {
editorIdentifier: FeatureIdentifier.SuperEditor,
},
} as jest.Mocked<SNTag>
const editorIdentifier = application.geDefaultEditorIdentifier(tag)
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
})
it('should return legacy editor identifier', () => {
const editor = {
legacyIsDefaultEditor: jest.fn().mockReturnValue(true),
identifier: FeatureIdentifier.MarkdownProEditor,
} as unknown as jest.Mocked<SNComponent>
componentManager.legacyGetDefaultEditor = jest.fn().mockReturnValue(editor)
const editorIdentifier = application.geDefaultEditorIdentifier()
expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
})
})
})

View File

@@ -5,7 +5,6 @@ import {
DeinitSource,
Platform,
SNApplication,
ItemGroupController,
removeFromArray,
DesktopDeviceInterface,
isDesktopDevice,
@@ -20,6 +19,8 @@ import {
MobileUnlockTiming,
InternalEventBus,
DecryptedItem,
EditorIdentifier,
FeatureIdentifier,
} from '@standardnotes/snjs'
import { makeObservable, observable } from 'mobx'
import { PanelResizedData } from '@/Types/PanelResizedData'
@@ -40,6 +41,8 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
import { setCustomViewportHeight } from '@/setViewportHeightWithFallback'
import { WebServices } from './WebServices'
import { FeatureName } from '@/Controllers/FeatureName'
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
import { VisibilityObserver } from './VisibilityObserver'
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
@@ -47,10 +50,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
private webServices!: WebServices
private webEventObservers: WebEventObserver[] = []
public itemControllerGroup: ItemGroupController
private onVisibilityChange: () => void
private mobileWebReceiver?: MobileWebReceiver
private androidBackHandler?: AndroidBackHandler
public readonly routeService: RouteServiceInterface
private visibilityObserver?: VisibilityObserver
constructor(
deviceInterface: WebOrDesktopDevice,
@@ -106,14 +109,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
}
}
this.onVisibilityChange = () => {
const visible = document.visibilityState === 'visible'
const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur
this.notifyWebEvent(event)
}
if (!isDesktopApplication()) {
document.addEventListener('visibilitychange', this.onVisibilityChange)
this.visibilityObserver = new VisibilityObserver((event) => {
this.notifyWebEvent(event)
})
}
}
@@ -144,8 +143,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
this.webEventObservers.length = 0
document.removeEventListener('visibilitychange', this.onVisibilityChange)
;(this.onVisibilityChange as unknown) = undefined
if (this.visibilityObserver) {
this.visibilityObserver.deinit()
this.visibilityObserver = undefined
}
} catch (error) {
console.error('Error while deiniting application', error)
}
@@ -379,4 +380,13 @@ export class WebApplication extends SNApplication implements WebApplicationInter
showAccountMenu(): void {
this.getViewControllerManager().accountMenuController.setShow(true)
}
geDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier {
return (
currentTag?.preferences?.editorIdentifier ||
this.getPreference(PrefKey.DefaultEditorIdentifier) ||
this.componentManager.legacyGetDefaultEditor()?.identifier ||
FeatureIdentifier.PlainEditor
)
}
}

View File

@@ -0,0 +1,40 @@
import { WebAppEvent } from '@standardnotes/snjs'
export class VisibilityObserver {
private raceTimeout?: ReturnType<typeof setTimeout>
constructor(private onEvent: (event: WebAppEvent) => void) {
/**
* Browsers may handle focus and visibilitychange events differently.
* Focus better handles window focus events but may not handle tab switching.
* We will listen for both and debouce notifying so that the most recent event wins.
*/
document.addEventListener('visibilitychange', this.onVisibilityChange)
window.addEventListener('focus', this.onFocusEvent, false)
}
onVisibilityChange = () => {
const visible = document.visibilityState === 'visible'
const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur
this.notifyEvent(event)
}
onFocusEvent = () => {
this.notifyEvent(WebAppEvent.WindowDidFocus)
}
private notifyEvent(event: WebAppEvent): void {
if (this.raceTimeout) {
clearTimeout(this.raceTimeout)
}
this.raceTimeout = setTimeout(() => {
this.onEvent(event)
}, 250)
}
deinit(): void {
document.removeEventListener('visibilitychange', this.onVisibilityChange)
window.removeEventListener('focus', this.onFocusEvent)
;(this.onEvent as unknown) = undefined
}
}