fix: color scheme handling on mobile (#1902)
This commit is contained in:
19
packages/mobile/src/ColorSchemeObserverService.ts
Normal file
19
packages/mobile/src/ColorSchemeObserverService.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { AbstractService, InternalEventBus, ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||||
|
import { Appearance, NativeEventSubscription } from 'react-native'
|
||||||
|
|
||||||
|
export class ColorSchemeObserverService extends AbstractService<ReactNativeToWebEvent> {
|
||||||
|
private removeListener: NativeEventSubscription
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const internalEventBus = new InternalEventBus()
|
||||||
|
super(internalEventBus)
|
||||||
|
|
||||||
|
this.removeListener = Appearance.addChangeListener(() => {
|
||||||
|
void this.notifyEvent(ReactNativeToWebEvent.ColorSchemeChanged)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit() {
|
||||||
|
this.removeListener.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,18 @@ import {
|
|||||||
TransferPayload,
|
TransferPayload,
|
||||||
UuidString,
|
UuidString,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { Alert, AppState, AppStateStatus, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
|
import { ColorSchemeObserverService } from 'ColorSchemeObserverService'
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Appearance,
|
||||||
|
AppState,
|
||||||
|
AppStateStatus,
|
||||||
|
ColorSchemeName,
|
||||||
|
Linking,
|
||||||
|
PermissionsAndroid,
|
||||||
|
Platform,
|
||||||
|
StatusBar,
|
||||||
|
} from 'react-native'
|
||||||
import FileViewer from 'react-native-file-viewer'
|
import FileViewer from 'react-native-file-viewer'
|
||||||
import FingerprintScanner from 'react-native-fingerprint-scanner'
|
import FingerprintScanner from 'react-native-fingerprint-scanner'
|
||||||
import FlagSecure from 'react-native-flag-secure-android'
|
import FlagSecure from 'react-native-flag-secure-android'
|
||||||
@@ -85,6 +96,7 @@ export class MobileDevice implements MobileDeviceInterface {
|
|||||||
constructor(
|
constructor(
|
||||||
private stateObserverService?: AppStateObserverService,
|
private stateObserverService?: AppStateObserverService,
|
||||||
private androidBackHandlerService?: AndroidBackHandlerService,
|
private androidBackHandlerService?: AndroidBackHandlerService,
|
||||||
|
private colorSchemeService?: ColorSchemeObserverService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
deinit() {
|
deinit() {
|
||||||
@@ -92,6 +104,8 @@ export class MobileDevice implements MobileDeviceInterface {
|
|||||||
;(this.stateObserverService as unknown) = undefined
|
;(this.stateObserverService as unknown) = undefined
|
||||||
this.androidBackHandlerService?.deinit()
|
this.androidBackHandlerService?.deinit()
|
||||||
;(this.androidBackHandlerService as unknown) = undefined
|
;(this.androidBackHandlerService as unknown) = undefined
|
||||||
|
this.colorSchemeService?.deinit()
|
||||||
|
;(this.colorSchemeService as unknown) = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
consoleLog(...args: any[]): void {
|
consoleLog(...args: any[]): void {
|
||||||
@@ -616,4 +630,8 @@ export class MobileDevice implements MobileDeviceInterface {
|
|||||||
async getAppState(): Promise<AppStateStatus> {
|
async getAppState(): Promise<AppStateStatus> {
|
||||||
return AppState.currentState
|
return AppState.currentState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getColorScheme(): Promise<ColorSchemeName> {
|
||||||
|
return Appearance.getColorScheme()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||||
|
import { ColorSchemeObserverService } from './ColorSchemeObserverService'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Keyboard, Platform } from 'react-native'
|
import { Keyboard, Platform } from 'react-native'
|
||||||
import VersionInfo from 'react-native-version-info'
|
import VersionInfo from 'react-native-version-info'
|
||||||
@@ -28,9 +29,10 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
|||||||
const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/src/index.html'
|
const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/src/index.html'
|
||||||
const stateService = useMemo(() => new AppStateObserverService(), [])
|
const stateService = useMemo(() => new AppStateObserverService(), [])
|
||||||
const androidBackHandlerService = useMemo(() => new AndroidBackHandlerService(), [])
|
const androidBackHandlerService = useMemo(() => new AndroidBackHandlerService(), [])
|
||||||
|
const colorSchemeService = useMemo(() => new ColorSchemeObserverService(), [])
|
||||||
const device = useMemo(
|
const device = useMemo(
|
||||||
() => new MobileDevice(stateService, androidBackHandlerService),
|
() => new MobileDevice(stateService, androidBackHandlerService, colorSchemeService),
|
||||||
[androidBackHandlerService, stateService],
|
[androidBackHandlerService, colorSchemeService, stateService],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,6 +46,10 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const removeColorSchemeServiceListener = colorSchemeService.addEventObserver((event: ReactNativeToWebEvent) => {
|
||||||
|
webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' }))
|
||||||
|
})
|
||||||
|
|
||||||
const keyboardShowListener = Keyboard.addListener('keyboardWillShow', () => {
|
const keyboardShowListener = Keyboard.addListener('keyboardWillShow', () => {
|
||||||
device.reloadStatusBarStyle(false)
|
device.reloadStatusBarStyle(false)
|
||||||
})
|
})
|
||||||
@@ -55,10 +61,11 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
|||||||
return () => {
|
return () => {
|
||||||
removeStateServiceListener()
|
removeStateServiceListener()
|
||||||
removeBackHandlerServiceListener()
|
removeBackHandlerServiceListener()
|
||||||
|
removeColorSchemeServiceListener()
|
||||||
keyboardShowListener.remove()
|
keyboardShowListener.remove()
|
||||||
keyboardHideListener.remove()
|
keyboardHideListener.remove()
|
||||||
}
|
}
|
||||||
}, [webViewRef, stateService, device, androidBackHandlerService])
|
}, [webViewRef, stateService, device, androidBackHandlerService, colorSchemeService])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = device.addMobileWebEventReceiver((event) => {
|
const observer = device.addMobileWebEventReceiver((event) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface WebApplicationInterface extends ApplicationInterface {
|
|||||||
handleMobileGainingFocusEvent(): Promise<void>
|
handleMobileGainingFocusEvent(): Promise<void>
|
||||||
handleMobileLosingFocusEvent(): Promise<void>
|
handleMobileLosingFocusEvent(): Promise<void>
|
||||||
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
||||||
|
handleMobileColorSchemeChangeEvent(): void
|
||||||
isNativeMobileWeb(): boolean
|
isNativeMobileWeb(): boolean
|
||||||
mobileDevice(): MobileDeviceInterface
|
mobileDevice(): MobileDeviceInterface
|
||||||
handleAndroidBackButtonPressed(): void
|
handleAndroidBackButtonPressed(): void
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
|||||||
removeComponentUrl(componentUuid: string): void
|
removeComponentUrl(componentUuid: string): void
|
||||||
isUrlComponentUrl(url: string): boolean
|
isUrlComponentUrl(url: string): boolean
|
||||||
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
|
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
|
||||||
|
getColorScheme(): Promise<'light' | 'dark' | null | undefined>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export enum ReactNativeToWebEvent {
|
|||||||
GainingFocus = 'GainingFocus',
|
GainingFocus = 'GainingFocus',
|
||||||
LosingFocus = 'LosingFocus',
|
LosingFocus = 'LosingFocus',
|
||||||
AndroidBackButtonPressed = 'AndroidBackButtonPressed',
|
AndroidBackButtonPressed = 'AndroidBackButtonPressed',
|
||||||
|
ColorSchemeChanged = 'ColorSchemeChanged',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,11 +59,13 @@ export class ThemeManager extends AbstractService {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ApplicationEvent.Launched: {
|
case ApplicationEvent.Launched: {
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.colorSchemeEventHandler)
|
if (!this.application.isNativeMobileWeb()) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', this.colorSchemeEventHandler)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ApplicationEvent.PreferencesChanged: {
|
case ApplicationEvent.PreferencesChanged: {
|
||||||
this.handlePreferencesChangeEvent()
|
void this.handlePreferencesChangeEvent()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +90,16 @@ export class ThemeManager extends AbstractService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private handlePreferencesChangeEvent(): void {
|
async handleMobileColorSchemeChangeEvent() {
|
||||||
|
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
|
||||||
|
|
||||||
|
if (useDeviceThemeSettings) {
|
||||||
|
const prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark'
|
||||||
|
this.setThemeAsPerColorScheme(prefersDarkColorScheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handlePreferencesChangeEvent() {
|
||||||
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
|
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false)
|
||||||
|
|
||||||
const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings
|
const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings
|
||||||
@@ -98,9 +109,13 @@ export class ThemeManager extends AbstractService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasPreferenceChanged && useDeviceThemeSettings) {
|
if (hasPreferenceChanged && useDeviceThemeSettings) {
|
||||||
const prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
let prefersDarkColorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
|
||||||
this.setThemeAsPerColorScheme(prefersDarkColorScheme.matches)
|
if (this.application.isNativeMobileWeb()) {
|
||||||
|
prefersDarkColorScheme = (await this.application.mobileDevice().getColorScheme()) === 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setThemeAsPerColorScheme(prefersDarkColorScheme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
setViewportHeightWithFallback()
|
setViewportHeightWithFallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMobileColorSchemeChangeEvent() {
|
||||||
|
void this.getThemeService().handleMobileColorSchemeChangeEvent()
|
||||||
|
}
|
||||||
|
|
||||||
private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> {
|
private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> {
|
||||||
const isLocked = await this.isLocked()
|
const isLocked = await this.isLocked()
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export class MobileWebReceiver {
|
|||||||
case ReactNativeToWebEvent.AndroidBackButtonPressed:
|
case ReactNativeToWebEvent.AndroidBackButtonPressed:
|
||||||
void this.application.handleAndroidBackButtonPressed()
|
void this.application.handleAndroidBackButtonPressed()
|
||||||
break
|
break
|
||||||
|
case ReactNativeToWebEvent.ColorSchemeChanged:
|
||||||
|
void this.application.handleMobileColorSchemeChangeEvent()
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
|||||||
Reference in New Issue
Block a user