From 6071ebffeb55c78eac79f39aecca07f6ebdbed93 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sat, 29 Oct 2022 00:12:03 +0530 Subject: [PATCH] fix: color scheme handling on mobile (#1902) --- .../mobile/src/ColorSchemeObserverService.ts | 19 ++++++++++++++ packages/mobile/src/Lib/Interface.ts | 20 ++++++++++++++- packages/mobile/src/MobileWebAppContainer.tsx | 13 +++++++--- .../Application/WebApplicationInterface.ts | 1 + .../Domain/Device/MobileDeviceInterface.ts | 1 + .../snjs/lib/Client/ReactNativeToWebEvent.ts | 1 + .../ui-services/src/Theme/ThemeManager.ts | 25 +++++++++++++++---- .../javascripts/Application/Application.ts | 4 +++ .../NativeMobileWeb/MobileWebReceiver.ts | 3 +++ 9 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 packages/mobile/src/ColorSchemeObserverService.ts diff --git a/packages/mobile/src/ColorSchemeObserverService.ts b/packages/mobile/src/ColorSchemeObserverService.ts new file mode 100644 index 000000000..8023b2888 --- /dev/null +++ b/packages/mobile/src/ColorSchemeObserverService.ts @@ -0,0 +1,19 @@ +import { AbstractService, InternalEventBus, ReactNativeToWebEvent } from '@standardnotes/snjs' +import { Appearance, NativeEventSubscription } from 'react-native' + +export class ColorSchemeObserverService extends AbstractService { + private removeListener: NativeEventSubscription + + constructor() { + const internalEventBus = new InternalEventBus() + super(internalEventBus) + + this.removeListener = Appearance.addChangeListener(() => { + void this.notifyEvent(ReactNativeToWebEvent.ColorSchemeChanged) + }) + } + + deinit() { + this.removeListener.remove() + } +} diff --git a/packages/mobile/src/Lib/Interface.ts b/packages/mobile/src/Lib/Interface.ts index a4e4b6c2f..6e098653b 100644 --- a/packages/mobile/src/Lib/Interface.ts +++ b/packages/mobile/src/Lib/Interface.ts @@ -13,7 +13,18 @@ import { TransferPayload, UuidString, } 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 FingerprintScanner from 'react-native-fingerprint-scanner' import FlagSecure from 'react-native-flag-secure-android' @@ -85,6 +96,7 @@ export class MobileDevice implements MobileDeviceInterface { constructor( private stateObserverService?: AppStateObserverService, private androidBackHandlerService?: AndroidBackHandlerService, + private colorSchemeService?: ColorSchemeObserverService, ) {} deinit() { @@ -92,6 +104,8 @@ export class MobileDevice implements MobileDeviceInterface { ;(this.stateObserverService as unknown) = undefined this.androidBackHandlerService?.deinit() ;(this.androidBackHandlerService as unknown) = undefined + this.colorSchemeService?.deinit() + ;(this.colorSchemeService as unknown) = undefined } consoleLog(...args: any[]): void { @@ -616,4 +630,8 @@ export class MobileDevice implements MobileDeviceInterface { async getAppState(): Promise { return AppState.currentState } + + async getColorScheme(): Promise { + return Appearance.getColorScheme() + } } diff --git a/packages/mobile/src/MobileWebAppContainer.tsx b/packages/mobile/src/MobileWebAppContainer.tsx index 160fc76bb..bd7b91e44 100644 --- a/packages/mobile/src/MobileWebAppContainer.tsx +++ b/packages/mobile/src/MobileWebAppContainer.tsx @@ -1,4 +1,5 @@ import { ReactNativeToWebEvent } from '@standardnotes/snjs' +import { ColorSchemeObserverService } from './ColorSchemeObserverService' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Keyboard, Platform } from 'react-native' 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 stateService = useMemo(() => new AppStateObserverService(), []) const androidBackHandlerService = useMemo(() => new AndroidBackHandlerService(), []) + const colorSchemeService = useMemo(() => new ColorSchemeObserverService(), []) const device = useMemo( - () => new MobileDevice(stateService, androidBackHandlerService), - [androidBackHandlerService, stateService], + () => new MobileDevice(stateService, androidBackHandlerService, colorSchemeService), + [androidBackHandlerService, colorSchemeService, stateService], ) 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', () => { device.reloadStatusBarStyle(false) }) @@ -55,10 +61,11 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo return () => { removeStateServiceListener() removeBackHandlerServiceListener() + removeColorSchemeServiceListener() keyboardShowListener.remove() keyboardHideListener.remove() } - }, [webViewRef, stateService, device, androidBackHandlerService]) + }, [webViewRef, stateService, device, androidBackHandlerService, colorSchemeService]) useEffect(() => { const observer = device.addMobileWebEventReceiver((event) => { diff --git a/packages/services/src/Domain/Application/WebApplicationInterface.ts b/packages/services/src/Domain/Application/WebApplicationInterface.ts index 36d3d8eac..9e05045dc 100644 --- a/packages/services/src/Domain/Application/WebApplicationInterface.ts +++ b/packages/services/src/Domain/Application/WebApplicationInterface.ts @@ -10,6 +10,7 @@ export interface WebApplicationInterface extends ApplicationInterface { handleMobileGainingFocusEvent(): Promise handleMobileLosingFocusEvent(): Promise handleMobileResumingFromBackgroundEvent(): Promise + handleMobileColorSchemeChangeEvent(): void isNativeMobileWeb(): boolean mobileDevice(): MobileDeviceInterface handleAndroidBackButtonPressed(): void diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts index 21da72e7e..36af893fc 100644 --- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -21,4 +21,5 @@ export interface MobileDeviceInterface extends DeviceInterface { removeComponentUrl(componentUuid: string): void isUrlComponentUrl(url: string): boolean getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'> + getColorScheme(): Promise<'light' | 'dark' | null | undefined> } diff --git a/packages/snjs/lib/Client/ReactNativeToWebEvent.ts b/packages/snjs/lib/Client/ReactNativeToWebEvent.ts index 8cb327d28..edce6db5a 100644 --- a/packages/snjs/lib/Client/ReactNativeToWebEvent.ts +++ b/packages/snjs/lib/Client/ReactNativeToWebEvent.ts @@ -4,4 +4,5 @@ export enum ReactNativeToWebEvent { GainingFocus = 'GainingFocus', LosingFocus = 'LosingFocus', AndroidBackButtonPressed = 'AndroidBackButtonPressed', + ColorSchemeChanged = 'ColorSchemeChanged', } diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index 20d243e20..32a466ae8 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -59,11 +59,13 @@ export class ThemeManager extends AbstractService { break } 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 } case ApplicationEvent.PreferencesChanged: { - this.handlePreferencesChangeEvent() + void this.handlePreferencesChangeEvent() 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 hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings @@ -98,9 +109,13 @@ export class ThemeManager extends AbstractService { } 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) } } diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index a15823788..bed5350a8 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -289,6 +289,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter setViewportHeightWithFallback() } + handleMobileColorSchemeChangeEvent() { + void this.getThemeService().handleMobileColorSchemeChangeEvent() + } + private async lockApplicationAfterMobileEventIfApplicable(): Promise { const isLocked = await this.isLocked() if (isLocked) { diff --git a/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts b/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts index 434e61569..98c7c36fe 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts +++ b/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts @@ -68,6 +68,9 @@ export class MobileWebReceiver { case ReactNativeToWebEvent.AndroidBackButtonPressed: void this.application.handleAndroidBackButtonPressed() break + case ReactNativeToWebEvent.ColorSchemeChanged: + void this.application.handleMobileColorSchemeChangeEvent() + break default: break