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:
@@ -72,6 +72,7 @@ export type ThemeFeatureDescription = ComponentFeatureDescription & {
|
||||
/** Some themes can be layered on top of other themes */
|
||||
layerable?: boolean
|
||||
dock_icon?: ThemeDockIcon
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
export type FeatureDescription = BaseFeatureDescription &
|
||||
|
||||
@@ -12,6 +12,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
permission_name: PermissionName.MidnightTheme,
|
||||
description: 'Elegant utilitarianism.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/midnight-with-mobile.jpg',
|
||||
isDark: true,
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
background_color: '#086DD6',
|
||||
@@ -27,6 +28,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
permission_name: PermissionName.FuturaTheme,
|
||||
description: 'Calm and relaxed. Take some time off.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/futura-with-mobile.jpg',
|
||||
isDark: true,
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
background_color: '#fca429',
|
||||
@@ -42,6 +44,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
permission_name: PermissionName.SolarizedDarkTheme,
|
||||
description: 'The perfect theme for any time.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/solarized-dark.jpg',
|
||||
isDark: true,
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
background_color: '#2AA198',
|
||||
@@ -72,6 +75,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
permission_name: PermissionName.FocusedTheme,
|
||||
description: 'For when you need to go in.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/themes/focus-with-mobile.jpg',
|
||||
isDark: true,
|
||||
dock_icon: {
|
||||
type: 'circle',
|
||||
background_color: '#a464c2',
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
removeFromArray,
|
||||
TransferPayload,
|
||||
} 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 FlagSecure from 'react-native-flag-secure-android'
|
||||
import { hide, show } from 'react-native-privacy-snapshot'
|
||||
@@ -65,6 +65,7 @@ const showLoadFailForItemIds = (failedItemIds: string[]) => {
|
||||
export class MobileDevice implements MobileDeviceInterface {
|
||||
environment: Environment.Mobile = Environment.Mobile
|
||||
private eventObservers: MobileDeviceEventHandler[] = []
|
||||
public isDarkMode = false
|
||||
|
||||
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 {
|
||||
for (const handler of this.eventObservers) {
|
||||
handler(event)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MobileDevice, MobileDeviceEvent } from '@Lib/Interface'
|
||||
import { IsDev } from '@Lib/Utils'
|
||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
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 { AppStateObserverService } from './AppStateObserverService'
|
||||
|
||||
@@ -29,8 +29,18 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
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 () => {
|
||||
removeListener()
|
||||
keyboardShowListener.remove()
|
||||
keyboardHideListener.remove()
|
||||
}
|
||||
}, [webViewRef, stateService])
|
||||
|
||||
@@ -183,6 +193,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
||||
allowFileAccess={true}
|
||||
allowUniversalAccessFromFileURLs={true}
|
||||
injectedJavaScriptBeforeContentLoaded={injectedJS}
|
||||
bounces={false}
|
||||
/>
|
||||
)
|
||||
/* eslint-enable @typescript-eslint/no-empty-function */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeatureDescription } from '@standardnotes/features'
|
||||
import { FeatureDescription, ThemeFeatureDescription } from '@standardnotes/features'
|
||||
|
||||
type ThirdPartyPackageInfo = {
|
||||
version: string
|
||||
@@ -6,3 +6,4 @@ type ThirdPartyPackageInfo = {
|
||||
}
|
||||
|
||||
export type ComponentPackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo>
|
||||
export type ThemePackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo> & ThemeFeatureDescription
|
||||
|
||||
@@ -6,11 +6,13 @@ import { HistoryEntryInterface } from '../../Runtime/History'
|
||||
import { DecryptedItemInterface, ItemInterface } from '../../Abstract/Item'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { useBoolean } from '@standardnotes/utils'
|
||||
import { ThemePackageInfo } from '../Component/PackageInfo'
|
||||
|
||||
export const isTheme = (x: ItemInterface): x is SNTheme => x.content_type === ContentType.Theme
|
||||
|
||||
export class SNTheme extends SNComponent {
|
||||
public override area: ComponentArea = ComponentArea.Themes
|
||||
public override readonly package_info!: ThemePackageInfo
|
||||
|
||||
isLayerable(): boolean {
|
||||
return useBoolean(this.package_info && this.package_info.layerable, false)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MobileDeviceInterface } from './../Device/MobileDeviceInterface'
|
||||
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
|
||||
import { WebAppEvent } from '../Event/WebAppEvent'
|
||||
import { ApplicationInterface } from './ApplicationInterface'
|
||||
@@ -9,4 +10,6 @@ export interface WebApplicationInterface extends ApplicationInterface {
|
||||
handleMobileGainingFocusEvent(): Promise<void>
|
||||
handleMobileLosingFocusEvent(): Promise<void>
|
||||
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
||||
isNativeMobileWeb(): boolean
|
||||
get mobileDevice(): MobileDeviceInterface
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
||||
hideMobileInterfaceFromScreenshots(): void
|
||||
stopHidingMobileInterfaceFromScreenshots(): void
|
||||
consoleLog(...args: any[]): void
|
||||
handleThemeSchemeChange(isDark: boolean): void
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@typescript-eslint/parser": "^5.12.1",
|
||||
"eslint-plugin-prettier": "*",
|
||||
"jest": "^28.1.2",
|
||||
"ts-jest": "^28.0.5"
|
||||
"ts-jest": "^28.0.5",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,13 +248,14 @@ export class ThemeManager extends AbstractService {
|
||||
this.deactivateTheme(theme.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
if (source !== PayloadEmitSource.LocalRetrieved) {
|
||||
this.cacheThemeState().catch(console.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public deactivateAllThemes() {
|
||||
private deactivateAllThemes() {
|
||||
const activeThemes = this.activeThemes.slice()
|
||||
|
||||
for (const uuid of activeThemes) {
|
||||
@@ -289,6 +290,10 @@ export class ThemeManager extends AbstractService {
|
||||
link.id = theme.uuid
|
||||
link.onload = this.syncThemeColorMetadata
|
||||
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) {
|
||||
if (!this.activeThemes.includes(uuid)) {
|
||||
return
|
||||
}
|
||||
|
||||
const element = document.getElementById(uuid) as HTMLLinkElement
|
||||
if (element) {
|
||||
element.disabled = true
|
||||
@@ -313,6 +322,10 @@ export class ThemeManager extends AbstractService {
|
||||
}
|
||||
|
||||
removeFromArray(this.activeThemes, uuid)
|
||||
|
||||
if (this.activeThemes.length === 0) {
|
||||
this.application.mobileDevice.handleThemeSchemeChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheThemeState() {
|
||||
|
||||
Reference in New Issue
Block a user