From 60e92c7035a54fd60e88034ca5ee120ae23e7c1d Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 6 Nov 2025 22:00:35 +0530 Subject: [PATCH] fix: Fix native themes not working on external editors (#2957) [skip e2e] --- packages/mobile/src/Lib/MobileDevice.ts | 12 ++++++ .../Domain/Device/MobileDeviceInterface.ts | 1 + .../ComponentManager/ComponentManager.ts | 40 ++++++++++++++++++- packages/utils/src/Domain/Utils/Utils.ts | 14 +++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/mobile/src/Lib/MobileDevice.ts b/packages/mobile/src/Lib/MobileDevice.ts index 9c1580afa..f5bad44a6 100644 --- a/packages/mobile/src/Lib/MobileDevice.ts +++ b/packages/mobile/src/Lib/MobileDevice.ts @@ -38,6 +38,9 @@ import { DocumentDirectoryPath, DownloadDirectoryPath, exists, + MainBundlePath, + readFile, + readFileAssets, unlink, writeFile, } from 'react-native-fs' @@ -508,6 +511,15 @@ export class MobileDevice implements MobileDeviceInterface { } } + async getNativeThemeCSS(identifier: string): Promise { + let path = `Web.bundle/src/web-src/components/assets/${identifier}/index.css` + if (Platform.OS === 'ios') { + path = `${MainBundlePath}/${path}` + } + const content = Platform.OS === 'android' ? readFileAssets(path) : readFile(path) + return content + } + async previewFile(base64: string, filename: string): Promise { const tempLocation = await this.downloadBase64AsFile(base64, filename, true) diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts index 84b0fe8c3..9af067298 100644 --- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -24,6 +24,7 @@ export interface MobileDeviceInterface extends DeviceInterface { handleThemeSchemeChange(isDark: boolean, bgColor: string): void shareBase64AsFile(base64: string, filename: string): Promise downloadBase64AsFile(base64: string, filename: string, saveInTempLocation?: boolean): Promise + getNativeThemeCSS(identifier: string): Promise previewFile(base64: string, filename: string): Promise exitApp(confirm?: boolean): void diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts index e6523e238..2296b15f3 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts @@ -30,7 +30,7 @@ import { NativeFeatureIdentifier, GetDeprecatedEditors, } from '@standardnotes/features' -import { Copy, removeFromArray, sleep, isNotUndefined, LoggerInterface } from '@standardnotes/utils' +import { Copy, removeFromArray, sleep, isNotUndefined, LoggerInterface, blobToBase64 } from '@standardnotes/utils' import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer' import { AbstractService, @@ -90,6 +90,8 @@ export class ComponentManager this.items, ) + private nativeThemesAsBase64: Record = {} + constructor( private items: ItemManagerInterface, private mutator: MutatorClientInterface, @@ -109,6 +111,7 @@ export class ComponentManager this.addSyncedComponentItemObserver() this.registerMobileNativeComponentUrls() this.registerDeprecatedEditorUrlsForAndroid() + void this.fetchNativeThemesOnMobile() this.eventDisposers.push( preferences.addEventObserver((event) => { @@ -295,6 +298,29 @@ export class ComponentManager } } + /** + * Gets all the native themes' CSS and stores them as `data:text/css;base64,...` URLs. + */ + private async fetchNativeThemesOnMobile(): Promise { + if (!isMobileDevice(this.device)) { + return + } + try { + for await (const theme of GetNativeThemes()) { + const css = await this.device.getNativeThemeCSS(theme.identifier) + if (css) { + const blob = new Blob([css], { type: 'text/css' }) + const base64 = await blobToBase64(blob) + this.nativeThemesAsBase64[theme.identifier] = base64 + } + } + } catch (error) { + console.error(error) + } finally { + this.postActiveThemesToAllViewers() + } + } + private registerDeprecatedEditorUrlsForAndroid(): void { if (!isMobileDevice(this.device)) { return @@ -367,7 +393,19 @@ export class ComponentManager public urlsForActiveThemes(): string[] { const themes = this.getActiveThemes() const urls = [] + const isMobile = isMobileDevice(this.device) for (const theme of themes) { + if (isMobile && theme.isNativeFeature) { + /** + * Since native themes on mobile are stored in the app bundle and accessed as `file://` URLs, + * external editors cannot access them. To solve this, we store base64 encoded versions of the themes and send those to the editor instead of a file URL. + */ + const base64 = this.nativeThemesAsBase64[theme.featureIdentifier] + if (base64) { + urls.push(base64) + continue + } + } const url = this.urlForFeature(theme) if (url) { urls.push(url) diff --git a/packages/utils/src/Domain/Utils/Utils.ts b/packages/utils/src/Domain/Utils/Utils.ts index 7325b9c92..367f5257d 100644 --- a/packages/utils/src/Domain/Utils/Utils.ts +++ b/packages/utils/src/Domain/Utils/Utils.ts @@ -699,3 +699,17 @@ export function spaceSeparatedStrings(...strings: string[]): string { export function pluralize(count: number, singular: string, plural: string): string { return count === 1 ? singular : plural } + +export function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => { + if (reader.result && typeof reader.result === 'string') { + resolve(reader.result) + } else { + reject() + } + } + reader.readAsDataURL(blob) + }) +}