fix(mobile): adjust status bar color to match current theme (#1624)

* feat: sync page theme color metadata with active theme bg

* fix: lint

* refactor: extract to method

* feat: recieve theme scheme change on mobile

* fix: handle issue where status bar color changes when keyboard appears on iOS

* fix: disable bouncing on web view
This commit is contained in:
Mo
2022-09-23 13:48:51 -05:00
committed by GitHub
parent da6622dc95
commit 4d5429cc89
11 changed files with 54 additions and 5 deletions

View File

@@ -72,6 +72,7 @@ export type ThemeFeatureDescription = ComponentFeatureDescription & {
/** Some themes can be layered on top of other themes */ /** Some themes can be layered on top of other themes */
layerable?: boolean layerable?: boolean
dock_icon?: ThemeDockIcon dock_icon?: ThemeDockIcon
isDark?: boolean
} }
export type FeatureDescription = BaseFeatureDescription & export type FeatureDescription = BaseFeatureDescription &

View File

@@ -12,6 +12,7 @@ export function themes(): ThemeFeatureDescription[] {
permission_name: PermissionName.MidnightTheme, permission_name: PermissionName.MidnightTheme,
description: 'Elegant utilitarianism.', description: 'Elegant utilitarianism.',
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/midnight-with-mobile.jpg', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/midnight-with-mobile.jpg',
isDark: true,
dock_icon: { dock_icon: {
type: 'circle', type: 'circle',
background_color: '#086DD6', background_color: '#086DD6',
@@ -27,6 +28,7 @@ export function themes(): ThemeFeatureDescription[] {
permission_name: PermissionName.FuturaTheme, permission_name: PermissionName.FuturaTheme,
description: 'Calm and relaxed. Take some time off.', description: 'Calm and relaxed. Take some time off.',
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/futura-with-mobile.jpg', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/futura-with-mobile.jpg',
isDark: true,
dock_icon: { dock_icon: {
type: 'circle', type: 'circle',
background_color: '#fca429', background_color: '#fca429',
@@ -42,6 +44,7 @@ export function themes(): ThemeFeatureDescription[] {
permission_name: PermissionName.SolarizedDarkTheme, permission_name: PermissionName.SolarizedDarkTheme,
description: 'The perfect theme for any time.', description: 'The perfect theme for any time.',
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/solarized-dark.jpg', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/solarized-dark.jpg',
isDark: true,
dock_icon: { dock_icon: {
type: 'circle', type: 'circle',
background_color: '#2AA198', background_color: '#2AA198',
@@ -72,6 +75,7 @@ export function themes(): ThemeFeatureDescription[] {
permission_name: PermissionName.FocusedTheme, permission_name: PermissionName.FocusedTheme,
description: 'For when you need to go in.', description: 'For when you need to go in.',
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/focus-with-mobile.jpg', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/focus-with-mobile.jpg',
isDark: true,
dock_icon: { dock_icon: {
type: 'circle', type: 'circle',
background_color: '#a464c2', background_color: '#a464c2',

View File

@@ -11,7 +11,7 @@ import {
removeFromArray, removeFromArray,
TransferPayload, TransferPayload,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { Alert, Linking, Platform } from 'react-native' import { Alert, Linking, Platform, StatusBar } from 'react-native'
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'
import { hide, show } from 'react-native-privacy-snapshot' import { hide, show } from 'react-native-privacy-snapshot'
@@ -65,6 +65,7 @@ const showLoadFailForItemIds = (failedItemIds: string[]) => {
export class MobileDevice implements MobileDeviceInterface { export class MobileDevice implements MobileDeviceInterface {
environment: Environment.Mobile = Environment.Mobile environment: Environment.Mobile = Environment.Mobile
private eventObservers: MobileDeviceEventHandler[] = [] private eventObservers: MobileDeviceEventHandler[] = []
public isDarkMode = false
constructor(private stateObserverService?: AppStateObserverService) {} constructor(private stateObserverService?: AppStateObserverService) {}
@@ -432,6 +433,16 @@ export class MobileDevice implements MobileDeviceInterface {
} }
} }
handleThemeSchemeChange(isDark: boolean): void {
this.isDarkMode = isDark
this.reloadStatusBarStyle()
}
reloadStatusBarStyle(animated = true) {
StatusBar.setBarStyle(this.isDarkMode ? 'light-content' : 'dark-content', animated)
}
private notifyEvent(event: MobileDeviceEvent): void { private notifyEvent(event: MobileDeviceEvent): void {
for (const handler of this.eventObservers) { for (const handler of this.eventObservers) {
handler(event) handler(event)

View File

@@ -2,7 +2,7 @@ import { MobileDevice, MobileDeviceEvent } from '@Lib/Interface'
import { IsDev } from '@Lib/Utils' import { IsDev } from '@Lib/Utils'
import { ReactNativeToWebEvent } from '@standardnotes/snjs' import { ReactNativeToWebEvent } from '@standardnotes/snjs'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Platform } from 'react-native' import { Keyboard, Platform } from 'react-native'
import { WebView, WebViewMessageEvent } from 'react-native-webview' import { WebView, WebViewMessageEvent } from 'react-native-webview'
import { AppStateObserverService } from './AppStateObserverService' import { AppStateObserverService } from './AppStateObserverService'
@@ -29,8 +29,18 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' })) webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' }))
}) })
const keyboardShowListener = Keyboard.addListener('keyboardWillShow', () => {
device.reloadStatusBarStyle(false)
})
const keyboardHideListener = Keyboard.addListener('keyboardDidHide', () => {
device.reloadStatusBarStyle(false)
})
return () => { return () => {
removeListener() removeListener()
keyboardShowListener.remove()
keyboardHideListener.remove()
} }
}, [webViewRef, stateService]) }, [webViewRef, stateService])
@@ -183,6 +193,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
allowFileAccess={true} allowFileAccess={true}
allowUniversalAccessFromFileURLs={true} allowUniversalAccessFromFileURLs={true}
injectedJavaScriptBeforeContentLoaded={injectedJS} injectedJavaScriptBeforeContentLoaded={injectedJS}
bounces={false}
/> />
) )
/* eslint-enable @typescript-eslint/no-empty-function */ /* eslint-enable @typescript-eslint/no-empty-function */

View File

@@ -1,4 +1,4 @@
import { FeatureDescription } from '@standardnotes/features' import { FeatureDescription, ThemeFeatureDescription } from '@standardnotes/features'
type ThirdPartyPackageInfo = { type ThirdPartyPackageInfo = {
version: string version: string
@@ -6,3 +6,4 @@ type ThirdPartyPackageInfo = {
} }
export type ComponentPackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo> export type ComponentPackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo>
export type ThemePackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo> & ThemeFeatureDescription

View File

@@ -6,11 +6,13 @@ import { HistoryEntryInterface } from '../../Runtime/History'
import { DecryptedItemInterface, ItemInterface } from '../../Abstract/Item' import { DecryptedItemInterface, ItemInterface } from '../../Abstract/Item'
import { ContentType } from '@standardnotes/common' import { ContentType } from '@standardnotes/common'
import { useBoolean } from '@standardnotes/utils' import { useBoolean } from '@standardnotes/utils'
import { ThemePackageInfo } from '../Component/PackageInfo'
export const isTheme = (x: ItemInterface): x is SNTheme => x.content_type === ContentType.Theme export const isTheme = (x: ItemInterface): x is SNTheme => x.content_type === ContentType.Theme
export class SNTheme extends SNComponent { export class SNTheme extends SNComponent {
public override area: ComponentArea = ComponentArea.Themes public override area: ComponentArea = ComponentArea.Themes
public override readonly package_info!: ThemePackageInfo
isLayerable(): boolean { isLayerable(): boolean {
return useBoolean(this.package_info && this.package_info.layerable, false) return useBoolean(this.package_info && this.package_info.layerable, false)

View File

@@ -1,3 +1,4 @@
import { MobileDeviceInterface } from './../Device/MobileDeviceInterface'
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface' import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
import { WebAppEvent } from '../Event/WebAppEvent' import { WebAppEvent } from '../Event/WebAppEvent'
import { ApplicationInterface } from './ApplicationInterface' import { ApplicationInterface } from './ApplicationInterface'
@@ -9,4 +10,6 @@ export interface WebApplicationInterface extends ApplicationInterface {
handleMobileGainingFocusEvent(): Promise<void> handleMobileGainingFocusEvent(): Promise<void>
handleMobileLosingFocusEvent(): Promise<void> handleMobileLosingFocusEvent(): Promise<void>
handleMobileResumingFromBackgroundEvent(): Promise<void> handleMobileResumingFromBackgroundEvent(): Promise<void>
isNativeMobileWeb(): boolean
get mobileDevice(): MobileDeviceInterface
} }

View File

@@ -11,4 +11,5 @@ export interface MobileDeviceInterface extends DeviceInterface {
hideMobileInterfaceFromScreenshots(): void hideMobileInterfaceFromScreenshots(): void
stopHidingMobileInterfaceFromScreenshots(): void stopHidingMobileInterfaceFromScreenshots(): void
consoleLog(...args: any[]): void consoleLog(...args: any[]): void
handleThemeSchemeChange(isDark: boolean): void
} }

View File

@@ -36,6 +36,7 @@
"@typescript-eslint/parser": "^5.12.1", "@typescript-eslint/parser": "^5.12.1",
"eslint-plugin-prettier": "*", "eslint-plugin-prettier": "*",
"jest": "^28.1.2", "jest": "^28.1.2",
"ts-jest": "^28.0.5" "ts-jest": "^28.0.5",
"typescript": "*"
} }
} }

View File

@@ -248,13 +248,14 @@ export class ThemeManager extends AbstractService {
this.deactivateTheme(theme.uuid) this.deactivateTheme(theme.uuid)
} }
} }
if (source !== PayloadEmitSource.LocalRetrieved) { if (source !== PayloadEmitSource.LocalRetrieved) {
this.cacheThemeState().catch(console.error) this.cacheThemeState().catch(console.error)
} }
}) })
} }
public deactivateAllThemes() { private deactivateAllThemes() {
const activeThemes = this.activeThemes.slice() const activeThemes = this.activeThemes.slice()
for (const uuid of activeThemes) { for (const uuid of activeThemes) {
@@ -289,6 +290,10 @@ export class ThemeManager extends AbstractService {
link.id = theme.uuid link.id = theme.uuid
link.onload = this.syncThemeColorMetadata link.onload = this.syncThemeColorMetadata
document.getElementsByTagName('head')[0].appendChild(link) document.getElementsByTagName('head')[0].appendChild(link)
if (this.application.isNativeMobileWeb()) {
this.application.mobileDevice.handleThemeSchemeChange(theme.package_info.isDark ?? false)
}
} }
/** /**
@@ -306,6 +311,10 @@ export class ThemeManager extends AbstractService {
} }
private deactivateTheme(uuid: string) { private deactivateTheme(uuid: string) {
if (!this.activeThemes.includes(uuid)) {
return
}
const element = document.getElementById(uuid) as HTMLLinkElement const element = document.getElementById(uuid) as HTMLLinkElement
if (element) { if (element) {
element.disabled = true element.disabled = true
@@ -313,6 +322,10 @@ export class ThemeManager extends AbstractService {
} }
removeFromArray(this.activeThemes, uuid) removeFromArray(this.activeThemes, uuid)
if (this.activeThemes.length === 0) {
this.application.mobileDevice.handleThemeSchemeChange(false)
}
} }
private async cacheThemeState() { private async cacheThemeState() {

View File

@@ -7650,6 +7650,7 @@ __metadata:
eslint-plugin-prettier: "*" eslint-plugin-prettier: "*"
jest: ^28.1.2 jest: ^28.1.2
ts-jest: ^28.0.5 ts-jest: ^28.0.5
typescript: "*"
languageName: unknown languageName: unknown
linkType: soft linkType: soft