From ad7d405b7c9651b4e9177336ccc7b44e4c169e84 Mon Sep 17 00:00:00 2001 From: Rosie Val Date: Sat, 19 Sep 2026 19:01:47 -0400 Subject: [PATCH] fix: modern Discord themes compatibility, storage async resilience, and startup safety --- src/core/vendetta/storage.ts | 46 +++++-- src/entry.ts | 46 ++++--- src/lib/addons/themes/colors/parser.ts | 15 ++- .../addons/themes/colors/patches/resolver.ts | 115 ++++++++++-------- src/lib/addons/themes/colors/updater.ts | 94 ++++++++------ src/metro/internals/caches.ts | 6 +- 6 files changed, 199 insertions(+), 123 deletions(-) diff --git a/src/core/vendetta/storage.ts b/src/core/vendetta/storage.ts index 08579c6..4559ea8 100644 --- a/src/core/vendetta/storage.ts +++ b/src/core/vendetta/storage.ts @@ -13,8 +13,9 @@ export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } function createProxy(target: any, path: string[]): any { return new Proxy(target, { - get(target, prop: string) { + get(target, prop: string | symbol) { if ((prop as unknown) === emitterSymbol) return emitter; + if (typeof prop === "symbol") return target[prop]; const newPath = [...path, prop]; const value: any = target[prop]; @@ -82,25 +83,45 @@ export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } } export function useProxy(storage: T): T { - const emitter = (storage as any)?.[emitterSymbol] as Emitter; - if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined"); - const [, forceUpdate] = React.useReducer(n => ~n, 0); React.useEffect(() => { - const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => { - if (event === "DEL" && data.value === storage) return; - forceUpdate(); + if (!storage) return; + + let unsubscribe: (() => void) | undefined; + + const attach = (s: any) => { + const emitter = s?.[emitterSymbol] as Emitter | undefined; + if (!emitter) return; + + const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => { + if (event === "DEL" && data.value === s) return; + forceUpdate(); + }; + + emitter.on("SET", listener); + emitter.on("DEL", listener); + + unsubscribe = () => { + emitter.off("SET", listener); + emitter.off("DEL", listener); + }; }; - emitter.on("SET", listener); - emitter.on("DEL", listener); + const currentEmitter = (storage as any)?.[emitterSymbol]; + if (currentEmitter) { + attach(storage); + } else if (typeof (storage as any)?.[syncAwaitSymbol] === "function") { + (storage as any)[syncAwaitSymbol](() => { + attach(storage); + forceUpdate(); + }); + } return () => { - emitter.off("SET", listener); - emitter.off("DEL", listener); + unsubscribe?.(); }; - }, []); + }, [storage]); return storage; } @@ -135,6 +156,7 @@ export function wrapSync>(store: T): Awaited { ), get(target, prop, recv) { if (prop === syncAwaitSymbol) return awaitInit; + if (prop === emitterSymbol) return (awaited as any)?.[emitterSymbol]; return Reflect.get(awaited ?? target, prop, recv); }, }); diff --git a/src/entry.ts b/src/entry.ts index 6893bda..3739b12 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -10,21 +10,15 @@ async function initializeShiggyCord() { // Make 'freeze' and 'seal' do nothing Object.freeze = Object.seal = Object; - await require("@metro/internals/caches").initMetroCache(); + try { + await require("@metro/internals/caches").initMetroCache(); + } catch (e) { + console.warn("Failed to initialize metro cache, proceeding anyway:", e); + } require(".").default(); } catch (e) { - const { ClientInfoManager } = require("@lib/api/native/modules"); const stack = e instanceof Error ? e.stack : undefined; - - console.log(stack ?? e?.toString?.() ?? e); - alert( - [ - "Failed to load ShiggyCord!\n", - `Build Number: ${ClientInfoManager.getConstants().Build}`, - `ShiggyCord: ${version}`, - stack || e?.toString?.(), - ].join("\n"), - ); + console.error("Failed to load ShiggyCord:", stack ?? e?.toString?.() ?? e); } } @@ -109,13 +103,29 @@ if (typeof window.__r === "undefined") { } const startDiscord = async () => { - await initializeShiggyCord(); + try { + await initializeShiggyCord(); + } catch (err) { + console.error("Critical error during initializeShiggyCord:", err); + } finally { + for (const unpatch of unpatches) { + try { + unpatch(); + } catch {} + } + unpatches.length = 0; - for (const unpatch of unpatches) unpatch(); - unpatches.length = 0; - - originalRequire(0); - resumeDeferred(); + try { + originalRequire(0); + } catch (e) { + console.error("originalRequire(0) error:", e); + } + try { + resumeDeferred(); + } catch (e) { + console.error("resumeDeferred error:", e); + } + } }; startDiscord(); diff --git a/src/lib/addons/themes/colors/parser.ts b/src/lib/addons/themes/colors/parser.ts index 0b7d342..c4cdb21 100644 --- a/src/lib/addons/themes/colors/parser.ts +++ b/src/lib/addons/themes/colors/parser.ts @@ -10,7 +10,20 @@ import { ColorManifest, InternalColorDefinition } from "./types"; const tokenRef = findByProps("SemanticColor"); export function parseColorManifest(manifest: ColorManifest): InternalColorDefinition { - const resolveType = (type = "dark") => (colorsPref.type ?? type) === "dark" ? "darker" : "light"; + const resolveType = (type = "dark") => { + const pref = colorsPref.type ?? type; + if (pref === "light") return "light"; + if (tokenRef?.SemanticColor) { + const sample = Object.values(tokenRef.SemanticColor)[0] as Record | undefined; + if (sample) { + if ("darker" in sample) return "darker"; + if ("midnight" in sample) return "midnight"; + if ("dark" in sample) return "dark"; + if ("onyx" in sample) return "onyx"; + } + } + return "dark"; + }; if (manifest.spec === 3) { const semanticColorDefinitions: InternalColorDefinition["semantic"] = {}; diff --git a/src/lib/addons/themes/colors/patches/resolver.ts b/src/lib/addons/themes/colors/patches/resolver.ts index e577f9c..6c22e97 100644 --- a/src/lib/addons/themes/colors/patches/resolver.ts +++ b/src/lib/addons/themes/colors/patches/resolver.ts @@ -2,14 +2,11 @@ import { _colorRef } from "@lib/addons/themes/colors/updater"; import { NativeThemeModule } from "@lib/api/native/modules"; import { before, instead } from "@lib/api/patcher"; import { findByProps } from "@metro"; +import { byMutableProp } from "@metro/filters"; +import { createLazyModule } from "@metro/lazy"; import chroma from "chroma-js"; -const tokenReference = findByProps("SemanticColor"); -const themeTypes = findByProps("ThemeTypes")?.ThemeTypes; - -const origRawColor = { ...tokenReference.RawColor }; -const origDarker = themeTypes.DARKER as string; -const origLight = themeTypes.LIGHT as string; +const isThemeModule = createLazyModule(byMutableProp("isThemeDark")); const SEMANTIC_FALLBACK_MAP: Record = { "BG_BACKDROP": "BACKGROUND_FLOATING", @@ -24,41 +21,57 @@ const SEMANTIC_FALLBACK_MAP: Record = { "BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY" }; +const alphaCache = new Map(); +let alphaCacheKey: string | null = null; + +function cachedAlphaHex(hex: string, opacity: number): string { + if (alphaCacheKey !== _colorRef.key) { + alphaCache.clear(); + alphaCacheKey = _colorRef.key; + } + const cacheKey = `${hex}|${opacity}`; + let result = alphaCache.get(cacheKey); + if (result === undefined) { + result = chroma(hex).alpha(opacity).hex(); + alphaCache.set(cacheKey, result); + } + return result; +} + export default function patchDefinitionAndResolver() { - const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0; + const tokenReference = findByProps("SemanticColor"); + if (!tokenReference) return () => {}; - Object.defineProperty(themeTypes, "DARKER", { - configurable: true, - enumerable: true, - get: () => _colorRef.current?.reference === "darker" ? _colorRef.key : origDarker, - }); - Object.defineProperty(themeTypes, "LIGHT", { - configurable: true, - enumerable: true, - get: () => _colorRef.current?.reference === "light" ? _colorRef.key : origLight, - }); + const origRawColor = { ...tokenReference.RawColor }; - Object.keys(tokenReference.RawColor).forEach(key => { - Object.defineProperty(tokenReference.RawColor, key, { - configurable: true, - enumerable: true, - get: () => { - const ret = _colorRef.current?.raw[key]; - if (ret) return ret; - return origRawColor[key]; - } + const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current?.reference ?? "dark"] : void 0; + + if (tokenReference.RawColor) { + Object.keys(tokenReference.RawColor).forEach(key => { + Object.defineProperty(tokenReference.RawColor, key, { + configurable: true, + enumerable: true, + get: () => { + const ret = _colorRef.current?.raw[key]; + return ret || origRawColor[key]; + } + }); }); - }); + } + + const resolverTarget = tokenReference.default?.meta ?? tokenReference.default?.internal ?? tokenReference.default; const unpatches = [ + before("isThemeDark", isThemeModule, callback), + before("isThemeLight", isThemeModule, callback), before("updateTheme", NativeThemeModule, callback), - instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => { + resolverTarget ? instead("resolveSemanticColor", resolverTarget, (args: any[], orig: any) => { if (!_colorRef.current) return orig(...args); if (args[0] !== _colorRef.key) return orig(...args); args[0] = _colorRef.current.reference; - const [name, colorDef] = extractInfo(_colorRef.current!.reference, args[1]); + const [name, colorDef] = extractInfo(_colorRef.current.reference, args[1], tokenReference); let semanticDef = _colorRef.current.semantic[name]; if (!semanticDef && _colorRef.current.spec === 2 && name in SEMANTIC_FALLBACK_MAP) { @@ -66,42 +79,40 @@ export default function patchDefinitionAndResolver() { } if (semanticDef?.value) { - return semanticDef.opacity === 1 - ? semanticDef.value - : chroma(semanticDef.value).alpha(semanticDef.opacity).hex(); + if (semanticDef.opacity === 1) return semanticDef.value; + return cachedAlphaHex(semanticDef.value, semanticDef.opacity); } - const rawValue = _colorRef.current.raw[colorDef.raw]; - if (rawValue) { - // Set opacity if needed - return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex(); + if (colorDef?.raw) { + const rawValue = _colorRef.current.raw[colorDef.raw]; + if (rawValue) { + return colorDef.opacity === 1 ? rawValue : cachedAlphaHex(rawValue, colorDef.opacity); + } } - // Fallback to default return orig(...args); - }), + }) : () => {}, () => { - Object.defineProperty(themeTypes, "DARKER", { - configurable: true, writable: true, value: origDarker - }); - Object.defineProperty(themeTypes, "LIGHT", { - configurable: true, writable: true, value: origLight - }); - Object.defineProperty(tokenReference, "RawColor", { - configurable: true, - writable: true, - value: origRawColor - }); + if (tokenReference.RawColor) { + Object.defineProperty(tokenReference, "RawColor", { + configurable: true, + writable: true, + value: origRawColor + }); + } } ]; return () => unpatches.forEach(p => p()); } -function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: any] { +function extractInfo(themeName: string, colorObj: any, tokenReference: any): [name: string, colorDef: any] { + if (!colorObj || typeof colorObj !== "object") return ["", null]; // @ts-ignore - assigning to extractInfo._sym const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]]; - const colorDef = tokenReference.SemanticColor[propName]; + const colorDef = tokenReference?.SemanticColor?.[propName]; + if (!colorDef) return [propName, null]; - return [propName, colorDef[themeName]]; + const targetDef = colorDef[themeName] ?? colorDef.darker ?? colorDef.dark ?? colorDef.midnight ?? colorDef.onyx ?? Object.values(colorDef)[0]; + return [propName, targetDef]; } diff --git a/src/lib/addons/themes/colors/updater.ts b/src/lib/addons/themes/colors/updater.ts index daeadd8..1743610 100644 --- a/src/lib/addons/themes/colors/updater.ts +++ b/src/lib/addons/themes/colors/updater.ts @@ -4,8 +4,6 @@ import { findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro"; import { parseColorManifest } from "./parser"; import { ColorManifest, InternalColorDefinition } from "./types"; -const tokenRef = findByProps("SemanticColor"); -const origRawColor = { ...tokenRef.RawColor }; const AppearanceManager = findByPropsLazy("updateTheme"); const ThemeStore = findByStoreNameLazy("ThemeStore"); const FormDivider = findByPropsLazy("DIVIDER_COLORS"); @@ -23,63 +21,85 @@ interface InternalColorRef { export const _colorRef: InternalColorRef = { current: null, key: `bn-theme-${_inc}`, - origRaw: origRawColor, - lastSetDiscordTheme: "darker" + origRaw: {}, + lastSetDiscordTheme: "dark" }; export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) { if (settings.safeMode?.enabled) return; + const tokenRef = findByProps("SemanticColor"); + if (tokenRef?.RawColor && Object.keys(_colorRef.origRaw).length === 0) { + Object.assign(_colorRef.origRaw, tokenRef.RawColor); + } + const internalDef = colorManifest ? parseColorManifest(colorManifest) : null; + const currentThemeName = ThemeStore?.theme ?? "dark"; const ref = Object.assign(_colorRef, { current: internalDef, - key: `bn-theme-${++_inc}`, - lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-") - ? ThemeStore.theme + key: `bn-theme-${++_inc}` as const, + lastSetDiscordTheme: !currentThemeName.startsWith("bn-theme-") + ? currentThemeName : _colorRef.lastSetDiscordTheme }); - if (internalDef != null) { - // Register the synthetic key BEFORE calling native, and verify it actually - // landed in every table native reads from. If any registration step fails, - // bail out to a known-good theme instead of handing native an unknown key. + if (internalDef != null && tokenRef) { try { - tokenRef.Theme[ref.key.toUpperCase()] = ref.key; - - if (!FormDivider.DIVIDER_COLORS[ref.current!.reference]) { - throw new Error(`Missing divider color reference: ${ref.current!.reference}`); + if (tokenRef.Theme) { + tokenRef.Theme[ref.key.toUpperCase()] = ref.key; } - FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference]; - Object.keys(tokenRef.Shadow).forEach(k => { - if (!(ref.current!.reference in tokenRef.Shadow[k])) { - throw new Error(`Missing shadow reference: ${ref.current!.reference}`); - } - tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference]; - }); + const targetRef = ref.current?.reference ?? "dark"; - Object.keys(tokenRef.SemanticColor).forEach(k => { - if (!(ref.current!.reference in tokenRef.SemanticColor[k])) { - throw new Error(`Missing semantic color reference: ${ref.current!.reference}`); - } - tokenRef.SemanticColor[k][ref.key] = { - ...tokenRef.SemanticColor[k][ref.current!.reference] - }; - }); + if (FormDivider?.DIVIDER_COLORS) { + const dividerFallback = FormDivider.DIVIDER_COLORS[targetRef] ?? + FormDivider.DIVIDER_COLORS.dark ?? + FormDivider.DIVIDER_COLORS.darker ?? + Object.values(FormDivider.DIVIDER_COLORS)[0]; + FormDivider.DIVIDER_COLORS[ref.key] = dividerFallback; + } + + if (tokenRef.Shadow) { + Object.keys(tokenRef.Shadow).forEach(k => { + const shadowGroup = tokenRef.Shadow[k]; + if (shadowGroup) { + const shadowFallback = shadowGroup[targetRef] ?? + shadowGroup.dark ?? + shadowGroup.darker ?? + shadowGroup.midnight ?? + shadowGroup.onyx ?? + Object.values(shadowGroup)[0]; + shadowGroup[ref.key] = shadowFallback; + } + }); + } + + if (tokenRef.SemanticColor) { + Object.keys(tokenRef.SemanticColor).forEach(k => { + const colorGroup = tokenRef.SemanticColor[k]; + if (colorGroup) { + const colorFallback = colorGroup[targetRef] ?? + colorGroup.dark ?? + colorGroup.darker ?? + colorGroup.midnight ?? + colorGroup.onyx ?? + Object.values(colorGroup)[0]; + colorGroup[ref.key] = { ...colorFallback }; + } + }); + } } catch (e) { - console.error("Failed to register custom theme key, falling back", e); - ref.current = null; // force fallback path below + console.error("Failed to register custom theme keys:", e); } } - if (update) { - AppearanceManager.setShouldSyncAppearanceSettings(false); + if (update && AppearanceManager?.updateTheme) { + try { + AppearanceManager.setShouldSyncAppearanceSettings?.(false); + } catch {} const targetTheme = ref.current != null ? ref.key : ref.lastSetDiscordTheme; - // This is the actual crash site: a raw call into native code with no - // guard. Wrap it so a native-side rejection can't take the whole - // bridge/thread down — fall back to the last known-good Discord theme. try { AppearanceManager.updateTheme(targetTheme); } catch (e) { diff --git a/src/metro/internals/caches.ts b/src/metro/internals/caches.ts index 360bbf3..0062440 100644 --- a/src/metro/internals/caches.ts +++ b/src/metro/internals/caches.ts @@ -33,10 +33,10 @@ function buildInitCache() { /** @internal */ export async function initMetroCache() { - if (!await fileExists(BUNNY_METRO_CACHE_PATH)) return void buildInitCache(); - const rawCache = await readFile(BUNNY_METRO_CACHE_PATH); - try { + if (!await fileExists(BUNNY_METRO_CACHE_PATH)) return void buildInitCache(); + const rawCache = await readFile(BUNNY_METRO_CACHE_PATH); + _metroCache = JSON.parse(rawCache); if (_metroCache._v !== CACHE_VERSION) { _metroCache = null!;