Merge branch 'kmmiio99o:main' into main

This commit is contained in:
OTKUSteyler 2026-08-18 10:05:14 +02:00 • committed by GitHub
commit 3f02e8bd37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 260 additions and 4 deletions

View file

@ -77,7 +77,7 @@ const config = {
constModules: {
globals: {
"bunny-build-info": {
version: `"1.4.1.6"`,
version: `"1.4.1.7"`,
},
},
},

View file

@ -0,0 +1,112 @@
import { instead } from "@lib/api/patcher";
import { logger } from "@lib/utils/logger";
import { _lazyContextSymbol } from "@metro/lazy";
import { requireModule } from "@metro/internals/modules";
import { LazyModuleContext } from "@metro/types";
import { findByNameLazy } from "@metro/wrappers";
function log(...messages: any[]) {
try {
logger.warn("[ShiggyCord] patchAnimatedValue:", ...messages);
} catch {}
}
/**
* Discord's bundled React Native throws `AnimatedValue: Attempting to set value
* to undefined` when a `undefined` value reaches an `Animated.Value` — either
* through `setValue()` (which calls `_updateValue`) or through the constructor.
* Some of Discord's own flows (and third-party plugins) hit this during
* animation setup, which bubbles up to the React ErrorBoundary and makes
* ShiggyCord render its custom crash screen.
*
* Fix the root cause instead of masking the symptom:
* - `_updateValue` is a prototype method, so one patch covers every instance no
* matter which module captured the class reference.
* - The constructor is guarded by swapping the public `Animated.Value`
* reference (and the module's `default` export) for a patched proxy so that
* `new Animated.Value(undefined)` can no longer throw.
*/
export default function patchAnimatedValue() {
const unpatchers: Array<() => boolean> = [];
const install = (Class: any, source: string) => {
try {
if (
!Class?.prototype ||
typeof Class.prototype._updateValue !== "function"
) {
log(`${source}: resolved value is not AnimatedValue, skipping`);
return;
}
// setValue(undefined) → _updateValue(undefined). Keep the last value.
unpatchers.push(
instead(
"_updateValue",
Class.prototype,
function (this: any, args: any[], origFunc: Function) {
if (args[0] === undefined) args[0] = this?._value ?? 0;
return origFunc(...args);
},
),
);
// Constructor guard: coerce non-number to 0 so `new Animated.Value(undefined)` can't throw.
const guard = (args: any[], origFunc: Function) => {
if (typeof args[0] !== "number") args[0] = 0;
return origFunc(...args);
};
// 1. Module exports (`require(...).default`).
try {
const ctxt: LazyModuleContext | undefined =
findByNameLazy("AnimatedValue")[_lazyContextSymbol];
if (ctxt?.moduleId != null) {
const moduleExports = requireModule(ctxt.moduleId);
if (moduleExports?.default === Class) {
unpatchers.push(instead("default", moduleExports, guard));
}
}
} catch {}
// 2. The public `Animated.Value` namespace reference.
try {
const ReactNative: any = require("react-native");
const Animated = ReactNative?.Animated;
if (Animated && Animated.Value === Class) {
unpatchers.push(instead("Value", Animated, guard));
}
} catch {}
log(`${source}: patched _updateValue + constructor guard`);
} catch (e) {
log(`${source}: patch failed`, e);
}
};
// Primary: Discord's own RN Animated namespace (most reliable, no full scan).
try {
const ReactNative: any = require("react-native");
install(ReactNative?.Animated?.Value, "react-native.Animated.Value");
if (unpatchers.length > 0) {
return () => unpatchers.forEach((unpatch) => unpatch());
}
} catch (e) {
log("react-native.Animated path failed", e);
}
// Fallback: byName lookup.
try {
const ctxt: LazyModuleContext =
findByNameLazy("AnimatedValue")[_lazyContextSymbol];
if (ctxt) {
ctxt.getExports((Class: any) => install(Class, "findByNameLazy"));
} else {
log("no lazy context found");
}
} catch (e) {
log("findByNameLazy path failed", e);
}
return () => unpatchers.forEach((unpatch) => unpatch());
}

View file

@ -0,0 +1,141 @@
import React from "react";
import { View } from "react-native";
import { byProps } from "@metro/filters";
import { findExports } from "@metro/finders";
import { findByStoreNameLazy } from "@metro/wrappers";
import { instead } from "@lib/api/patcher";
import { logger } from "@lib/utils/logger";
import { isSemanticColor, resolveSemanticColor, semanticColors } from "@ui/color";
const ThemeStore = findByStoreNameLazy("ThemeStore");
function log(...messages: any[]) {
try {
logger.warn("[ShiggyCord] patchSafeCard:", ...messages);
} catch {}
}
let cardHealthy = false;
const FALLBACK_STYLE = {
borderRadius: 8,
padding: 16,
};
function resolveCardBackground(): string | undefined {
try {
const color = semanticColors?.CARD_BACKGROUND_DEFAULT;
if (isSemanticColor(color)) return resolveSemanticColor(color);
if (typeof color === "string") return color;
} catch {}
return undefined;
}
export default function patchSafeCard() {
const unpatchers: Array<() => boolean> = [];
try {
const cardModule = findExports(byProps("Card"));
const origCard = cardModule?.Card;
if (typeof origCard !== "function") {
log("no Card function on the resolved module, skipping");
return () => unpatchers.forEach((unpatch) => unpatch());
}
log("resolved design Card from first-match module");
let cardBackground = resolveCardBackground();
const unsubscribeTheme = ThemeStore?.addChangeListener?.(() => {
const resolved = resolveCardBackground();
if (resolved) cardBackground = resolved;
});
if (typeof unsubscribeTheme === "function") {
unpatchers.push(() => {
unsubscribeTheme();
return true;
});
}
class CardProbeBoundary extends React.Component<
{
fallback: React.ReactNode;
children?: React.ReactNode;
},
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: unknown) {
try {
log("Card render threw, using View fallback", error);
} catch {}
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}
class HealthyProbe extends React.Component<{
onHealthy: () => void;
children?: React.ReactNode;
}> {
componentDidMount() {
this.props.onHealthy();
}
render() {
return this.props.children;
}
}
const SafeCard = function SafeCard(props: any) {
if (cardHealthy) return React.createElement(origCard, props);
return React.createElement(
CardProbeBoundary,
{
fallback: React.createElement(
View,
{
style: [
FALLBACK_STYLE,
...(cardBackground
? [{ backgroundColor: cardBackground }]
: []),
props?.style,
],
},
props?.children,
),
},
React.createElement(
HealthyProbe,
{
onHealthy: () => {
cardHealthy = true;
},
},
React.createElement(origCard, props),
),
);
};
unpatchers.push(
instead("Card", cardModule, (args: any[]) => SafeCard(args[0] ?? {})),
);
log("patched design Card (adaptive probe on first mount)");
} catch (e) {
log("patch failed", e);
}
return () => unpatchers.forEach((unpatch) => unpatch());
}

View file

@ -19,7 +19,6 @@ import {
TableSwitchRow,
Stack,
} from "@metro/common/components";
import { semanticColors } from "@ui/color";
import { ScrollView, View } from "react-native";
import ErrorComponentStackCard from "./ErrorComponentStackCard";
@ -74,8 +73,8 @@ export default function ErrorBoundaryScreen(props: {
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>
<Text
variant="heading-md/extrabold"
color="text-default"
style={{
color: semanticColors.HEADER_PRIMARY,
marginBottom: 8,
textAlign: "center",
}}

View file

@ -144,7 +144,7 @@ export default function General() {
subLabel={`${debugInfo.bunny.loader.name} loader`}
icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />}
trailing={
<TableRow.TrailingText text={debugInfo.bunny.loader.version} />
<TableRow.TrailingText text={debugInfo.bunny.loader.version ?? ""} />
}
/>
</TableRowGroup>

View file

@ -1,4 +1,6 @@
import patchAnimatedValue from "@core/debug/patches/patchAnimatedValue";
import patchErrorBoundary from "@core/debug/patches/patchErrorBoundary";
import patchSafeCard from "@core/debug/patches/patchSafeCard";
import initFixes from "@core/fixes";
import { initFetchI18nStrings } from "@core/i18n";
import initSettings from "@core/ui/settings";
@ -30,6 +32,8 @@ export default async () => {
["patchCommands", () => patchCommands()],
["patchJsx", () => patchJsx()],
["patchErrorBoundary", () => patchErrorBoundary()],
["patchAnimatedValue", () => patchAnimatedValue()],
["patchSafeCard", () => patchSafeCard()],
["initVendettaObject", () => initVendettaObject()],
["initFetchI18nStrings", () => initFetchI18nStrings()],
["initSettings", () => initSettings()],