feat: mobile web bridge (#1597)
This commit is contained in:
@@ -17,12 +17,15 @@ import {
|
||||
DecryptedItemInterface,
|
||||
WebAppEvent,
|
||||
WebApplicationInterface,
|
||||
MobileDeviceInterface,
|
||||
MobileUnlockTiming,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable } from 'mobx'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
import { DesktopManager } from './Device/DesktopManager'
|
||||
import { ArchiveManager, AutolockService, IOService, WebAlertService, ThemeManager } from '@standardnotes/ui-services'
|
||||
import { MobileWebReceiver } from './MobileWebReceiver'
|
||||
|
||||
type WebServices = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
@@ -41,6 +44,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
public itemControllerGroup: ItemGroupController
|
||||
public iconsController: IconsController
|
||||
private onVisibilityChange: () => void
|
||||
private mobileWebReceiver?: MobileWebReceiver
|
||||
|
||||
constructor(
|
||||
deviceInterface: WebOrDesktopDevice,
|
||||
@@ -70,6 +74,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
this.itemControllerGroup = new ItemGroupController(this)
|
||||
this.iconsController = new IconsController()
|
||||
|
||||
if (this.isNativeMobileWeb()) {
|
||||
this.mobileWebReceiver = new MobileWebReceiver(this)
|
||||
}
|
||||
|
||||
this.onVisibilityChange = () => {
|
||||
const visible = document.visibilityState === 'visible'
|
||||
const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur
|
||||
@@ -101,6 +109,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
|
||||
this.itemControllerGroup.deinit()
|
||||
;(this.itemControllerGroup as unknown) = undefined
|
||||
;(this.mobileWebReceiver as unknown) = undefined
|
||||
|
||||
this.webEventObservers.length = 0
|
||||
|
||||
@@ -161,6 +170,13 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
return undefined
|
||||
}
|
||||
|
||||
get mobileDevice(): MobileDeviceInterface {
|
||||
if (!this.isNativeMobileWeb()) {
|
||||
throw Error('Attempting to access device as mobile device on non mobile platform')
|
||||
}
|
||||
return this.deviceInterface as MobileDeviceInterface
|
||||
}
|
||||
|
||||
public getThemeService() {
|
||||
return this.webServices.themeService
|
||||
}
|
||||
@@ -203,4 +219,44 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
const currentValue = this.isGlobalSpellcheckEnabled()
|
||||
return this.setPreference(PrefKey.EditorSpellcheck, !currentValue)
|
||||
}
|
||||
|
||||
async handleMobileEnteringBackgroundEvent(): Promise<void> {
|
||||
await this.lockApplicationAfterMobileEventIfApplicable()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
async handleMobileGainingFocusEvent(): Promise<void> {}
|
||||
|
||||
async handleMobileLosingFocusEvent(): Promise<void> {
|
||||
if (await this.getMobileScreenshotPrivacyEnabled()) {
|
||||
this.mobileDevice.stopHidingMobileInterfaceFromScreenshots()
|
||||
}
|
||||
|
||||
await this.lockApplicationAfterMobileEventIfApplicable()
|
||||
}
|
||||
|
||||
async handleMobileResumingFromBackgroundEvent(): Promise<void> {
|
||||
if (await this.getMobileScreenshotPrivacyEnabled()) {
|
||||
this.mobileDevice.hideMobileInterfaceFromScreenshots()
|
||||
}
|
||||
}
|
||||
|
||||
private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> {
|
||||
const isLocked = await this.isLocked()
|
||||
if (isLocked) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasBiometrics = this.hasBiometrics()
|
||||
const hasPasscode = this.hasPasscode()
|
||||
const passcodeTiming = await this.getMobilePasscodeTiming()
|
||||
const biometricsTiming = await this.getMobileBiometricsTiming()
|
||||
|
||||
const passcodeLockImmediately = hasPasscode && passcodeTiming === MobileUnlockTiming.Immediately
|
||||
const biometricsLockImmediately = hasBiometrics && biometricsTiming === MobileUnlockTiming.Immediately
|
||||
|
||||
if (passcodeLockImmediately || biometricsLockImmediately) {
|
||||
await this.lock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ReactNativeToWebEvent, WebApplicationInterface } from '@standardnotes/snjs'
|
||||
|
||||
export class MobileWebReceiver {
|
||||
constructor(private application: WebApplicationInterface) {
|
||||
this.listenForNativeMobileEvents()
|
||||
}
|
||||
|
||||
deinit() {
|
||||
;(this.application as unknown) = undefined
|
||||
window.removeEventListener('message', this.handleNativeMobileWindowMessage)
|
||||
document.removeEventListener('message', this.handleNativeMobileWindowMessage as never)
|
||||
}
|
||||
|
||||
listenForNativeMobileEvents() {
|
||||
const iOSEventRecipient = window
|
||||
const androidEventRecipient = document
|
||||
iOSEventRecipient.addEventListener('message', this.handleNativeMobileWindowMessage)
|
||||
androidEventRecipient.addEventListener('message', this.handleNativeMobileWindowMessage as never)
|
||||
}
|
||||
|
||||
handleNativeMobileWindowMessage = (event: MessageEvent) => {
|
||||
const nullOrigin = event.origin === '' || event.origin == null
|
||||
if (!nullOrigin) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = (event as MessageEvent).data
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(message)
|
||||
const { messageType, reactNativeEvent } = parsed
|
||||
|
||||
if (messageType === 'event' && reactNativeEvent) {
|
||||
const nativeEvent = reactNativeEvent as ReactNativeToWebEvent
|
||||
this.handleNativeEvent(nativeEvent)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error parsing message from React Native', error)
|
||||
}
|
||||
}
|
||||
|
||||
handleNativeEvent(event: ReactNativeToWebEvent) {
|
||||
switch (event) {
|
||||
case ReactNativeToWebEvent.EnteringBackground:
|
||||
void this.application.handleMobileEnteringBackgroundEvent()
|
||||
break
|
||||
case ReactNativeToWebEvent.GainingFocus:
|
||||
void this.application.handleMobileGainingFocusEvent()
|
||||
break
|
||||
case ReactNativeToWebEvent.LosingFocus:
|
||||
void this.application.handleMobileLosingFocusEvent()
|
||||
break
|
||||
case ReactNativeToWebEvent.ResumingFromBackground:
|
||||
void this.application.handleMobileResumingFromBackgroundEvent()
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user