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 */
|
/** 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 &
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user