From 9ad026b8ce6e9c0a8a0632007f2359f6a7540d72 Mon Sep 17 00:00:00 2001 From: Hampus Date: Sat, 29 Aug 2026 03:37:41 +0200 Subject: [PATCH] fix(app): repair theme CSS sync persistence and data loss (#2067) --- fluxer_api/src/api/models/UserSettings.ts | 2 +- .../accessibility/state/Accessibility.ts | 24 ++++++++++++++++++- .../src/features/theme/state/ThemeLibrary.ts | 4 +++- .../features/theme/utils/ThemeLibraryDb.ts | 10 ++++++-- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/fluxer_api/src/api/models/UserSettings.ts b/fluxer_api/src/api/models/UserSettings.ts index da0246cee..b5aaf13f3 100644 --- a/fluxer_api/src/api/models/UserSettings.ts +++ b/fluxer_api/src/api/models/UserSettings.ts @@ -254,7 +254,7 @@ function normalizeStoredSyncedPreferences(raw: string | null | undefined): strin try { decodeSyncedPreferences(raw); } catch { - return ''; + return raw; } return raw; } diff --git a/fluxer_app/src/features/accessibility/state/Accessibility.ts b/fluxer_app/src/features/accessibility/state/Accessibility.ts index 4c460724a..efa903137 100644 --- a/fluxer_app/src/features/accessibility/state/Accessibility.ts +++ b/fluxer_app/src/features/accessibility/state/Accessibility.ts @@ -33,6 +33,7 @@ export const ZOOM_LEVEL_MARKERS = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] as cons export const ACCESSIBILITY_STORE_STORAGE_KEY = 'Accessibility'; export const ACCESSIBILITY_ZOOM_STORAGE_KEY = 'Accessibility:zoomLevel'; export const ACCESSIBILITY_CUSTOM_THEME_STORAGE_KEY = 'Accessibility:customThemeCss'; +const ACCESSIBILITY_CUSTOM_THEME_SYNC_STORAGE_KEY = 'Accessibility:customThemeCssSyncAcrossDevices'; export const ACCESSIBILITY_MOTION_STORAGE_KEY = 'Accessibility:motion'; export const ACCESSIBILITY_SHOW_NEKO_STORAGE_KEY = 'Accessibility:showNeko'; export const ACCESSIBILITY_KEEP_NEKO_STILL_STORAGE_KEY = 'Accessibility:keepNekoStill'; @@ -492,6 +493,21 @@ function persistLocalCustomThemeCss(css: string | null): void { } catch {} } +function persistLocalCustomThemeCssSyncAcrossDevices(value: boolean): void { + try { + AppStorage.setItem(ACCESSIBILITY_CUSTOM_THEME_SYNC_STORAGE_KEY, JSON.stringify(value)); + } catch {} +} + +function readLocalCustomThemeCssSyncAcrossDevices(): boolean { + try { + const raw = AppStorage.getItem(ACCESSIBILITY_CUSTOM_THEME_SYNC_STORAGE_KEY); + return raw === null ? false : JSON.parse(raw) === true; + } catch { + return false; + } +} + function normalizeCustomThemeCss(css: string | null | undefined): string | null { if (typeof css !== 'string') { return null; @@ -728,6 +744,7 @@ class Accessibility { if (this.customThemeCss !== null) { persistLocalCustomThemeCss(this.customThemeCss); } + this.customThemeCssSyncAcrossDevices = readLocalCustomThemeCssSyncAcrossDevices(); this.showNeko = readAndMigrateLocalShowNeko(); this.keepNekoStill = readAndMigrateLocalKeepNekoStill(); this.showVideoSeekPreviewThumbnails = readLocalVideoSeekPreviewThumbnails(); @@ -851,7 +868,11 @@ class Accessibility { showStickersInAutocomplete: s.showStickersInExpressionAutocomplete, showMemesInAutocomplete: s.showMemesInExpressionAutocomplete, voiceChannelJoinRequiresDoubleClick: s.voiceChannelJoinRequiresDoubleClick, - customThemeCss: s.customThemeCssSyncAcrossDevices ? (s.customThemeCss ?? '') : (s.serverCustomThemeCss ?? ''), + customThemeCss: ((): string | undefined => { + const local = s.customThemeCss; + const server = s.serverCustomThemeCss; + return (s.customThemeCssSyncAcrossDevices ? local : server) ?? undefined; + })(), showFavorites: s.showFavorites, dmMessagePreviewMode: DM_PREVIEW_TO_PROTO[s.dmMessagePreviewMode], enableTtsCommand: s.enableTTSCommand, @@ -1396,6 +1417,7 @@ class Accessibility { persistLocalCustomThemeCss(this.customThemeCss); } this.customThemeCssSyncAcrossDevices = syncAcrossDevices; + persistLocalCustomThemeCssSyncAcrossDevices(syncAcrossDevices); } subscribe(callback: () => void): () => void { diff --git a/fluxer_app/src/features/theme/state/ThemeLibrary.ts b/fluxer_app/src/features/theme/state/ThemeLibrary.ts index cc1860611..2ebe49403 100644 --- a/fluxer_app/src/features/theme/state/ThemeLibrary.ts +++ b/fluxer_app/src/features/theme/state/ThemeLibrary.ts @@ -125,6 +125,7 @@ class ThemeLibrary { localFiles: Array = []; enabledThemeIds: Array = []; isHydrated = false; + loadFailed = false; isBusy = false; revision = 0; private initPromise: Promise | null = null; @@ -173,12 +174,13 @@ class ThemeLibrary { this.localFiles = localFiles.sort((a, b) => a.name.localeCompare(b.name)); this.enabledThemeIds = enabledThemeIds.filter((id) => themes.some((theme) => theme.id === id)); this.isHydrated = true; + this.loadFailed = false; this.revision += 1; }); } catch (error) { logger.error('Failed to hydrate theme library', error); runInAction(() => { - this.isHydrated = true; + this.loadFailed = true; }); } } diff --git a/fluxer_app/src/features/theme/utils/ThemeLibraryDb.ts b/fluxer_app/src/features/theme/utils/ThemeLibraryDb.ts index deae92e96..8176cce19 100644 --- a/fluxer_app/src/features/theme/utils/ThemeLibraryDb.ts +++ b/fluxer_app/src/features/theme/utils/ThemeLibraryDb.ts @@ -94,7 +94,7 @@ function getDatabase(): Promise { if (openPromise) { return openPromise; } - openPromise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { if (!browserIndexedDB) { reject(new Error('IndexedDB unavailable')); return; @@ -126,7 +126,13 @@ function getDatabase(): Promise { request.onerror = () => reject(request.error ?? new Error('Failed to open theme library database')); request.onblocked = () => reject(new Error('Theme library database upgrade is blocked by another window')); }); - return openPromise; + promise.catch(() => { + if (openPromise === promise) { + openPromise = null; + } + }); + openPromise = promise; + return promise; } async function withReadonlyDb(