fix: modern Discord themes compatibility, storage async resilience, and startup safety

This commit is contained in:
Rosie Val 2026-09-19 19:01:47 -04:00
parent 1547421074
commit ad7d405b7c
6 changed files with 199 additions and 123 deletions

View file

@ -13,8 +13,9 @@ export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; }
function createProxy(target: any, path: string[]): any { function createProxy(target: any, path: string[]): any {
return new Proxy(target, { return new Proxy(target, {
get(target, prop: string) { get(target, prop: string | symbol) {
if ((prop as unknown) === emitterSymbol) return emitter; if ((prop as unknown) === emitterSymbol) return emitter;
if (typeof prop === "symbol") return target[prop];
const newPath = [...path, prop]; const newPath = [...path, prop];
const value: any = target[prop]; const value: any = target[prop];
@ -82,25 +83,45 @@ export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; }
} }
export function useProxy<T>(storage: T): T { export function useProxy<T>(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); const [, forceUpdate] = React.useReducer(n => ~n, 0);
React.useEffect(() => { React.useEffect(() => {
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => { if (!storage) return;
if (event === "DEL" && data.value === storage) return;
forceUpdate(); 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); const currentEmitter = (storage as any)?.[emitterSymbol];
emitter.on("DEL", listener); if (currentEmitter) {
attach(storage);
} else if (typeof (storage as any)?.[syncAwaitSymbol] === "function") {
(storage as any)[syncAwaitSymbol](() => {
attach(storage);
forceUpdate();
});
}
return () => { return () => {
emitter.off("SET", listener); unsubscribe?.();
emitter.off("DEL", listener);
}; };
}, []); }, [storage]);
return storage; return storage;
} }
@ -135,6 +156,7 @@ export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
), ),
get(target, prop, recv) { get(target, prop, recv) {
if (prop === syncAwaitSymbol) return awaitInit; if (prop === syncAwaitSymbol) return awaitInit;
if (prop === emitterSymbol) return (awaited as any)?.[emitterSymbol];
return Reflect.get(awaited ?? target, prop, recv); return Reflect.get(awaited ?? target, prop, recv);
}, },
}); });

View file

@ -10,21 +10,15 @@ async function initializeShiggyCord() {
// Make 'freeze' and 'seal' do nothing // Make 'freeze' and 'seal' do nothing
Object.freeze = Object.seal = Object; 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(); require(".").default();
} catch (e) { } catch (e) {
const { ClientInfoManager } = require("@lib/api/native/modules");
const stack = e instanceof Error ? e.stack : undefined; const stack = e instanceof Error ? e.stack : undefined;
console.error("Failed to load ShiggyCord:", stack ?? e?.toString?.() ?? e);
console.log(stack ?? e?.toString?.() ?? e);
alert(
[
"Failed to load ShiggyCord!\n",
`Build Number: ${ClientInfoManager.getConstants().Build}`,
`ShiggyCord: ${version}`,
stack || e?.toString?.(),
].join("\n"),
);
} }
} }
@ -109,13 +103,29 @@ if (typeof window.__r === "undefined") {
} }
const startDiscord = async () => { 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(); try {
unpatches.length = 0; originalRequire(0);
} catch (e) {
originalRequire(0); console.error("originalRequire(0) error:", e);
resumeDeferred(); }
try {
resumeDeferred();
} catch (e) {
console.error("resumeDeferred error:", e);
}
}
}; };
startDiscord(); startDiscord();

View file

@ -10,7 +10,20 @@ import { ColorManifest, InternalColorDefinition } from "./types";
const tokenRef = findByProps("SemanticColor"); const tokenRef = findByProps("SemanticColor");
export function parseColorManifest(manifest: ColorManifest): InternalColorDefinition { 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<string, any> | 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) { if (manifest.spec === 3) {
const semanticColorDefinitions: InternalColorDefinition["semantic"] = {}; const semanticColorDefinitions: InternalColorDefinition["semantic"] = {};

View file

@ -2,14 +2,11 @@ import { _colorRef } from "@lib/addons/themes/colors/updater";
import { NativeThemeModule } from "@lib/api/native/modules"; import { NativeThemeModule } from "@lib/api/native/modules";
import { before, instead } from "@lib/api/patcher"; import { before, instead } from "@lib/api/patcher";
import { findByProps } from "@metro"; import { findByProps } from "@metro";
import { byMutableProp } from "@metro/filters";
import { createLazyModule } from "@metro/lazy";
import chroma from "chroma-js"; import chroma from "chroma-js";
const tokenReference = findByProps("SemanticColor"); const isThemeModule = createLazyModule(byMutableProp("isThemeDark"));
const themeTypes = findByProps("ThemeTypes")?.ThemeTypes;
const origRawColor = { ...tokenReference.RawColor };
const origDarker = themeTypes.DARKER as string;
const origLight = themeTypes.LIGHT as string;
const SEMANTIC_FALLBACK_MAP: Record<string, string> = { const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
"BG_BACKDROP": "BACKGROUND_FLOATING", "BG_BACKDROP": "BACKGROUND_FLOATING",
@ -24,41 +21,57 @@ const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
"BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY" "BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY"
}; };
const alphaCache = new Map<string, string>();
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() { 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", { const origRawColor = { ...tokenReference.RawColor };
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,
});
Object.keys(tokenReference.RawColor).forEach(key => { const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current?.reference ?? "dark"] : void 0;
Object.defineProperty(tokenReference.RawColor, key, {
configurable: true, if (tokenReference.RawColor) {
enumerable: true, Object.keys(tokenReference.RawColor).forEach(key => {
get: () => { Object.defineProperty(tokenReference.RawColor, key, {
const ret = _colorRef.current?.raw[key]; configurable: true,
if (ret) return ret; enumerable: true,
return origRawColor[key]; get: () => {
} const ret = _colorRef.current?.raw[key];
return ret || origRawColor[key];
}
});
}); });
}); }
const resolverTarget = tokenReference.default?.meta ?? tokenReference.default?.internal ?? tokenReference.default;
const unpatches = [ const unpatches = [
before("isThemeDark", isThemeModule, callback),
before("isThemeLight", isThemeModule, callback),
before("updateTheme", NativeThemeModule, 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 (!_colorRef.current) return orig(...args);
if (args[0] !== _colorRef.key) return orig(...args); if (args[0] !== _colorRef.key) return orig(...args);
args[0] = _colorRef.current.reference; 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]; let semanticDef = _colorRef.current.semantic[name];
if (!semanticDef && _colorRef.current.spec === 2 && name in SEMANTIC_FALLBACK_MAP) { if (!semanticDef && _colorRef.current.spec === 2 && name in SEMANTIC_FALLBACK_MAP) {
@ -66,42 +79,40 @@ export default function patchDefinitionAndResolver() {
} }
if (semanticDef?.value) { if (semanticDef?.value) {
return semanticDef.opacity === 1 if (semanticDef.opacity === 1) return semanticDef.value;
? semanticDef.value return cachedAlphaHex(semanticDef.value, semanticDef.opacity);
: chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
} }
const rawValue = _colorRef.current.raw[colorDef.raw]; if (colorDef?.raw) {
if (rawValue) { const rawValue = _colorRef.current.raw[colorDef.raw];
// Set opacity if needed if (rawValue) {
return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex(); return colorDef.opacity === 1 ? rawValue : cachedAlphaHex(rawValue, colorDef.opacity);
}
} }
// Fallback to default
return orig(...args); return orig(...args);
}), }) : () => {},
() => { () => {
Object.defineProperty(themeTypes, "DARKER", { if (tokenReference.RawColor) {
configurable: true, writable: true, value: origDarker Object.defineProperty(tokenReference, "RawColor", {
}); configurable: true,
Object.defineProperty(themeTypes, "LIGHT", { writable: true,
configurable: true, writable: true, value: origLight value: origRawColor
}); });
Object.defineProperty(tokenReference, "RawColor", { }
configurable: true,
writable: true,
value: origRawColor
});
} }
]; ];
return () => unpatches.forEach(p => p()); 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 // @ts-ignore - assigning to extractInfo._sym
const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]]; 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];
} }

View file

@ -4,8 +4,6 @@ import { findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro";
import { parseColorManifest } from "./parser"; import { parseColorManifest } from "./parser";
import { ColorManifest, InternalColorDefinition } from "./types"; import { ColorManifest, InternalColorDefinition } from "./types";
const tokenRef = findByProps("SemanticColor");
const origRawColor = { ...tokenRef.RawColor };
const AppearanceManager = findByPropsLazy("updateTheme"); const AppearanceManager = findByPropsLazy("updateTheme");
const ThemeStore = findByStoreNameLazy("ThemeStore"); const ThemeStore = findByStoreNameLazy("ThemeStore");
const FormDivider = findByPropsLazy("DIVIDER_COLORS"); const FormDivider = findByPropsLazy("DIVIDER_COLORS");
@ -23,63 +21,85 @@ interface InternalColorRef {
export const _colorRef: InternalColorRef = { export const _colorRef: InternalColorRef = {
current: null, current: null,
key: `bn-theme-${_inc}`, key: `bn-theme-${_inc}`,
origRaw: origRawColor, origRaw: {},
lastSetDiscordTheme: "darker" lastSetDiscordTheme: "dark"
}; };
export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) { export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) {
if (settings.safeMode?.enabled) return; 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 internalDef = colorManifest ? parseColorManifest(colorManifest) : null;
const currentThemeName = ThemeStore?.theme ?? "dark";
const ref = Object.assign(_colorRef, { const ref = Object.assign(_colorRef, {
current: internalDef, current: internalDef,
key: `bn-theme-${++_inc}`, key: `bn-theme-${++_inc}` as const,
lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-") lastSetDiscordTheme: !currentThemeName.startsWith("bn-theme-")
? ThemeStore.theme ? currentThemeName
: _colorRef.lastSetDiscordTheme : _colorRef.lastSetDiscordTheme
}); });
if (internalDef != null) { if (internalDef != null && tokenRef) {
// 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.
try { try {
tokenRef.Theme[ref.key.toUpperCase()] = ref.key; if (tokenRef.Theme) {
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
if (!FormDivider.DIVIDER_COLORS[ref.current!.reference]) {
throw new Error(`Missing divider color reference: ${ref.current!.reference}`);
} }
FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference];
Object.keys(tokenRef.Shadow).forEach(k => { const targetRef = ref.current?.reference ?? "dark";
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];
});
Object.keys(tokenRef.SemanticColor).forEach(k => { if (FormDivider?.DIVIDER_COLORS) {
if (!(ref.current!.reference in tokenRef.SemanticColor[k])) { const dividerFallback = FormDivider.DIVIDER_COLORS[targetRef] ??
throw new Error(`Missing semantic color reference: ${ref.current!.reference}`); FormDivider.DIVIDER_COLORS.dark ??
} FormDivider.DIVIDER_COLORS.darker ??
tokenRef.SemanticColor[k][ref.key] = { Object.values(FormDivider.DIVIDER_COLORS)[0];
...tokenRef.SemanticColor[k][ref.current!.reference] 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) { } catch (e) {
console.error("Failed to register custom theme key, falling back", e); console.error("Failed to register custom theme keys:", e);
ref.current = null; // force fallback path below
} }
} }
if (update) { if (update && AppearanceManager?.updateTheme) {
AppearanceManager.setShouldSyncAppearanceSettings(false); try {
AppearanceManager.setShouldSyncAppearanceSettings?.(false);
} catch {}
const targetTheme = ref.current != null ? ref.key : ref.lastSetDiscordTheme; 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 { try {
AppearanceManager.updateTheme(targetTheme); AppearanceManager.updateTheme(targetTheme);
} catch (e) { } catch (e) {

View file

@ -33,10 +33,10 @@ function buildInitCache() {
/** @internal */ /** @internal */
export async function initMetroCache() { export async function initMetroCache() {
if (!await fileExists(BUNNY_METRO_CACHE_PATH)) return void buildInitCache();
const rawCache = await readFile(BUNNY_METRO_CACHE_PATH);
try { try {
if (!await fileExists(BUNNY_METRO_CACHE_PATH)) return void buildInitCache();
const rawCache = await readFile(BUNNY_METRO_CACHE_PATH);
_metroCache = JSON.parse(rawCache); _metroCache = JSON.parse(rawCache);
if (_metroCache._v !== CACHE_VERSION) { if (_metroCache._v !== CACHE_VERSION) {
_metroCache = null!; _metroCache = null!;