diff --git a/packages/features/src/Domain/Feature/FeatureDescription.ts b/packages/features/src/Domain/Feature/FeatureDescription.ts index 4f5c81b60..b6fd23259 100644 --- a/packages/features/src/Domain/Feature/FeatureDescription.ts +++ b/packages/features/src/Domain/Feature/FeatureDescription.ts @@ -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 & diff --git a/packages/features/src/Domain/Lists/Themes.ts b/packages/features/src/Domain/Lists/Themes.ts index e6d835b21..df586b802 100644 --- a/packages/features/src/Domain/Lists/Themes.ts +++ b/packages/features/src/Domain/Lists/Themes.ts @@ -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', diff --git a/packages/mobile/src/Lib/Interface.ts b/packages/mobile/src/Lib/Interface.ts index 403eaffd7..aa5b840de 100644 --- a/packages/mobile/src/Lib/Interface.ts +++ b/packages/mobile/src/Lib/Interface.ts @@ -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) diff --git a/packages/mobile/src/MobileWebAppContainer.tsx b/packages/mobile/src/MobileWebAppContainer.tsx index 85d5c5d07..aed6ccbcf 100644 --- a/packages/mobile/src/MobileWebAppContainer.tsx +++ b/packages/mobile/src/MobileWebAppContainer.tsx @@ -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 */ diff --git a/packages/models/src/Domain/Syncable/Component/PackageInfo.ts b/packages/models/src/Domain/Syncable/Component/PackageInfo.ts index 6f9b1820a..79f7aa0de 100644 --- a/packages/models/src/Domain/Syncable/Component/PackageInfo.ts +++ b/packages/models/src/Domain/Syncable/Component/PackageInfo.ts @@ -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 +export type ThemePackageInfo = FeatureDescription & Partial & ThemeFeatureDescription diff --git a/packages/models/src/Domain/Syncable/Theme/Theme.ts b/packages/models/src/Domain/Syncable/Theme/Theme.ts index b288d4440..c5ea79e1a 100644 --- a/packages/models/src/Domain/Syncable/Theme/Theme.ts +++ b/packages/models/src/Domain/Syncable/Theme/Theme.ts @@ -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) diff --git a/packages/services/src/Domain/Application/WebApplicationInterface.ts b/packages/services/src/Domain/Application/WebApplicationInterface.ts index 6cc84394b..1d888c3a5 100644 --- a/packages/services/src/Domain/Application/WebApplicationInterface.ts +++ b/packages/services/src/Domain/Application/WebApplicationInterface.ts @@ -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 handleMobileLosingFocusEvent(): Promise handleMobileResumingFromBackgroundEvent(): Promise + isNativeMobileWeb(): boolean + get mobileDevice(): MobileDeviceInterface } diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts index 65bfa7279..61f25205b 100644 --- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -11,4 +11,5 @@ export interface MobileDeviceInterface extends DeviceInterface { hideMobileInterfaceFromScreenshots(): void stopHidingMobileInterfaceFromScreenshots(): void consoleLog(...args: any[]): void + handleThemeSchemeChange(isDark: boolean): void } diff --git a/packages/ui-services/package.json b/packages/ui-services/package.json index ea6e6c185..6fee39237 100644 --- a/packages/ui-services/package.json +++ b/packages/ui-services/package.json @@ -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": "*" } } diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index d0a67a55f..9a6e0adcf 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -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() { diff --git a/yarn.lock b/yarn.lock index 48687897d..c1a940acf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7650,6 +7650,7 @@ __metadata: eslint-plugin-prettier: "*" jest: ^28.1.2 ts-jest: ^28.0.5 + typescript: "*" languageName: unknown linkType: soft