diff --git a/plugins/better-timestamps/src/patches/rowManager.ts b/plugins/better-timestamps/src/patches/rowManager.ts
index 3ea5a33..8b4f3a4 100644
--- a/plugins/better-timestamps/src/patches/rowManager.ts
+++ b/plugins/better-timestamps/src/patches/rowManager.ts
@@ -24,12 +24,8 @@ function parseTimestamp(value: any): any {
return moment(value);
}
-// RowManager used to be looked up eagerly at module-import time with the cached findByName - a
-// plugin's top-level code runs as soon as Discord requires its bundle, which can be before Discord's
-// own code has required RowManager itself, and Revenge's findByName permanently caches a negative
-// result. Confirmed live via Key Inspector's Eval console: a raw, uncached scan found
-// RowManager.prototype.generate present and correct once the module had actually initialized - the
-// class itself was never the problem, just the timing of a one-shot lookup racing it.
+// findByName caches a negative result permanently if it runs before RowManager registers -
+// waitFor + a raw lookup retries until it's actually there.
export default function patchRowManager(): () => void {
const patches: (() => void)[] = [];
diff --git a/plugins/copy-role-color/src/index.ts b/plugins/copy-role-color/src/index.ts
index cb30ceb..1ea1a72 100644
--- a/plugins/copy-role-color/src/index.ts
+++ b/plugins/copy-role-color/src/index.ts
@@ -48,9 +48,7 @@ function copyRoleColor(colorStrings: any, fallbackColor?: string | null) {
}
function patchRolePill(): () => void {
- // Confirmed against decompiled current-build Discord source: the component is now just
- // "RolePill" (at app/components_native/common/RolePill.tsx) - "ThemedRolePill" doesn't exist
- // anywhere in it. Checking both names covers older builds that might still use the old one.
+ // "RolePill" on current builds, "ThemedRolePill" on older ones.
const RolePillComponent = findByName("RolePill", false) ?? findByName("ThemedRolePill", false);
if (!RolePillComponent) return () => {};
@@ -81,11 +79,8 @@ function patchRolePill(): () => void {
});
}
-// Covers the role pills in a full profile's Roles section - a different render path from
-// patchRolePill above (that one's for role mentions in message text). This section renders
-// UserProfileRolesCard -> RolesList -> RoleItem, none of which are top-level exports, so we need
-// createElementIntercept to catch RoleItem's reference. It hands us the role object directly, with
-// both colorStrings and colorString on it already, so no need to guess at it from rendered styles.
+// Covers role pills in a full profile's Roles section (UserProfileRolesCard -> RolesList ->
+// RoleItem, none of which are top-level exports, hence createElementIntercept).
function patchProfileRoleItem(): () => void {
const cleanups: (() => void)[] = [];
patchCreateElement(cleanups);
diff --git a/plugins/custom-user-tags/src/patches/details.tsx b/plugins/custom-user-tags/src/patches/details.tsx
index 8afb9c1..f5a3b59 100644
--- a/plugins/custom-user-tags/src/patches/details.tsx
+++ b/plugins/custom-user-tags/src/patches/details.tsx
@@ -6,9 +6,8 @@ import openTagEditor from "../ui/TagEditorAlert";
const TagModule = findByProps("getBotLabel");
-// Covers member list rows and the profile popout, same target Staff Tags already confirmed works
-// (findByTypeNameAll("UserRow")) - and the long-press hook for assigning a tag, since this repo has
-// no confirmed way to hook Discord's own user context menu.
+// Covers member list rows and the profile popout, plus the long-press hook for assigning a tag
+// (no confirmed way to hook Discord's own user context menu).
const rowPatch = ([{ user }]: any[], res: any) => {
try {
if (user?.id && res?.props) {
diff --git a/plugins/custom-user-tags/src/patches/name.tsx b/plugins/custom-user-tags/src/patches/name.tsx
index 245adbe..6a1079b 100644
--- a/plugins/custom-user-tags/src/patches/name.tsx
+++ b/plugins/custom-user-tags/src/patches/name.tsx
@@ -5,10 +5,8 @@ import resolveTag from "../lib/resolveTag";
const TagModule = findByProps("getBotLabel");
-// findByName("DisplayName", false) doesn't match - confirmed live via Key Inspector's Eval console
-// (a full module-key scan, while fixing Staff Tags' identical lookup) that the export survives as a
-// plain named property, not the component's own runtime .name/.displayName, which doesn't survive
-// production minification. findByProps looks at property keys instead.
+// findByName("DisplayName") doesn't survive minification - findByProps looks at the property key
+// instead.
const DisplayNameModule = findByProps("DisplayName") as any;
export default () => {
diff --git a/plugins/custom-user-tags/src/patches/tag.tsx b/plugins/custom-user-tags/src/patches/tag.tsx
index 13f14b0..3dc1b84 100644
--- a/plugins/custom-user-tags/src/patches/tag.tsx
+++ b/plugins/custom-user-tags/src/patches/tag.tsx
@@ -7,12 +7,8 @@ import Icon from "../ui/Icon";
const Tag = findByProps("getBotLabel");
const { View, Text } = ReactNative;
-/**
- * Discord's own Tag component ignores custom text/color props unless we push them into the
- * already-rendered tree ourselves - this is what actually makes our overrides visible. Now also
- * supports an `icon` prop (an IconDef from our icon library) and `iconColor` so tags can render
- * with or without text.
- */
+// Discord's Tag component ignores custom text/color props unless pushed into the already-rendered
+// tree directly.
export default () => {
if (!Tag) return () => {};
diff --git a/plugins/fake-profile-themes-and-effects/src/index.ts b/plugins/fake-profile-themes-and-effects/src/index.ts
index 4af5efb..4e6498f 100644
--- a/plugins/fake-profile-themes-and-effects/src/index.ts
+++ b/plugins/fake-profile-themes-and-effects/src/index.ts
@@ -31,14 +31,9 @@ let unpatchAll: () => void = () => {};
export default {
onLoad() {
- // The "real" picker (EffectPickerActionSheet.tsx) works by manually calling Discord's own
- // EditProfileEffectActionSheet as a plain function from inside this plugin's own render,
- // outside React's normal reconciliation - Discord's component's hooks end up chained onto
- // this fiber's hook list. If that component's own hook count/order ever varies, that's the
- // "Rendered more/fewer hooks" crash class, and it's the one this plugin's own settings
- // screen already had a manual override toggle for. Defaulting to the fallback picker (a
- // self-contained implementation using only its own hooks) instead of opting into that risk
- // by default - it's a real crash a user hit live opening the Effects picker.
+ // The "real" picker chains Discord's own hooks onto this fiber outside React's normal
+ // reconciliation, risking a "Rendered more/fewer hooks" crash - default to the safer,
+ // self-contained fallback picker instead.
storage.forceFallbackEffectPicker ??= true;
unpatchAll = applyPatches("FakeProfileThemesAndEffects", logger, {
diff --git a/plugins/fake-profile-themes-and-effects/src/lib/fpte.ts b/plugins/fake-profile-themes-and-effects/src/lib/fpte.ts
index 43ad996..a933dda 100644
--- a/plugins/fake-profile-themes-and-effects/src/lib/fpte.ts
+++ b/plugins/fake-profile-themes-and-effects/src/lib/fpte.ts
@@ -1,44 +1,24 @@
/* eslint-disable @stylistic/brace-style */
-/** The FPTE delimiter codepoint (codepoint of zero-width space). */
-const DELIMITER_CODEPOINT = 0x200B;
-/** The FPTE delimiter (zero-width space). */
+const DELIMITER_CODEPOINT = 0x200B; // zero-width space
const DELIMITER = String.fromCodePoint(DELIMITER_CODEPOINT);
-/** The FPTE radix (number of default-ignorable codepoints in the SSP plane). */
-const RADIX = 0x1000;
-/** The FPTE starting codepoint (first codepoint in the SSP plane). */
-const STARTING_CODEPOINT = 0xE0000;
-/** The FPTE ending codepoint (last default-ignorable codepoint in the SSP plane). */
+const RADIX = 0x1000; // count of default-ignorable codepoints in the SSP plane
+const STARTING_CODEPOINT = 0xE0000; // first codepoint in the SSP plane
const ENDING_CODEPOINT = STARTING_CODEPOINT + RADIX - 1;
-/**
- * Builds a theme color string in the legacy format: `[#primary,#accent]`, where primary and accent are
- * 24-bit colors as base-16 strings, with each codepoint of the string offset by +{@link STARTING_CODEPOINT}.
- * @param primary The 24-bit primary color.
- * @param accent The 24-bit accent color.
- * @returns The built legacy-format theme color string.
- */
+/** Legacy format: `[#primary,#accent]`, each codepoint offset by +STARTING_CODEPOINT. */
export function encodeColorsLegacy(primary: number, accent: number) {
return String.fromCodePoint(...[...`[#${primary.toString(16)},#${accent.toString(16)}]`]
.map(c => c.codePointAt(0)! + STARTING_CODEPOINT));
}
-/**
- * Extracts the theme colors from a legacy-format string.
- * @param str The legacy-format string to extract the theme colors from.
- * @returns The profile theme colors. Colors will be -1 if not found.
- * @see {@link encodeColorsLegacy}
- */
+/** @returns [primary, accent] colors, or -1 for either if not found. */
export function decodeColorsLegacy(str: string): [primaryColor: number, accentColor: number] {
const [primary, accent] = str.matchAll(/(?<=#)[\dA-Fa-f]{1,6}/g);
return [primary ? parseInt(primary[0], 16) : -1, accent ? parseInt(accent[0], 16) : -1];
}
-/**
- * Converts a 24-bit color to a base-{@link RADIX} string with each codepoint offset by +{@link STARTING_CODEPOINT}.
- * @param color The 24-bit color to be converted.
- * @returns The converted base-{@link RADIX} string with +{@link STARTING_CODEPOINT} offset.
- */
+/** Base-RADIX encoding of a 24-bit color, each codepoint offset by +STARTING_CODEPOINT. */
export function encodeColor(color: number) {
if (color === 0) return String.fromCodePoint(STARTING_CODEPOINT);
let str = "";
@@ -47,12 +27,7 @@ export function encodeColor(color: number) {
return str;
}
-/**
- * Converts a no-offset base-{@link RADIX} string to a 24-bit color.
- * @param str The no-offset base-{@link RADIX} string to be converted.
- * @returns The converted 24-bit color.
- * Will be -1 if `str` is empty and -2 if the color is greater than the maximum 24-bit color, 0xFFFFFF.
- */
+/** @returns the decoded 24-bit color; -1 if str is empty, -2 if over 0xFFFFFF. */
export function decodeColor(str: string) {
if (str === "") return -1;
let color = 0;
@@ -63,11 +38,7 @@ export function decodeColor(str: string) {
return color;
}
-/**
- * Converts an effect ID to a base-{@link RADIX} string with each code point offset by +{@link STARTING_CODEPOINT}.
- * @param id The effect ID to be converted.
- * @returns The converted base-{@link RADIX} string with +{@link STARTING_CODEPOINT} offset.
- */
+/** Base-RADIX encoding of an effect ID, each codepoint offset by +STARTING_CODEPOINT. */
export function encodeEffect(id: bigint) {
if (id === 0n) return String.fromCodePoint(STARTING_CODEPOINT);
let str = "";
@@ -76,12 +47,7 @@ export function encodeEffect(id: bigint) {
return str;
}
-/**
- * Converts a no-offset base-{@link RADIX} string to an effect ID.
- * @param str The no-offset base-{@link RADIX} string to be converted.
- * @returns The converted effect ID.
- * Will be -1n if `str` is empty and -2n if the color is greater than the maximum effect ID.
- */
+/** @returns the decoded effect ID; -1n if str is empty, -2n if over the max effect ID. */
export function decodeEffect(str: string) {
if (str === "") return -1n;
let id = 0n;
@@ -93,122 +59,68 @@ export function decodeEffect(str: string) {
}
/**
- * Builds a FPTE string containing the given primary/accent colors and effect ID. If the FPTE Builder is NOT set to backwards
- * compatibility mode, the primary and accent colors will be converted to base-{@link RADIX} before they are encoded.
- * @param primary The primary profile theme color. Must be negative if unset.
- * @param accent The accent profile theme color. Must be negative if unset.
- * @param effect The profile effect ID. Must be empty if unset.
- * @param legacy Whether the primary and accent colors should be legacy encoded.
- * @returns The built FPTE string. Will be empty if the given colors and effect are all unset.
+ * Builds an FPTE string from primary/accent colors (negative = unset) and an effect ID (empty =
+ * unset). In legacy mode, colors are legacy-encoded and both are always included (accent falls
+ * back to primary, or vice versa); otherwise a shared/unset color is omitted to save space.
*/
export function buildFPTE(primary: number, accent: number, effect: string, legacy: boolean) {
- /** The FPTE string to be returned. */
let fpte = "";
- // If the FPTE Builder is set to backwards compatibility mode,
- // the primary and accent colors, if set, will be legacy encoded.
if (legacy) {
- // Legacy FPTE strings must include both the primary and accent colors, even if they are the same.
-
if (primary >= 0) {
- // If both the primary and accent colors are set, they will be legacy encoded and added to the
- // string; otherwise, if the accent color is unset, the primary color will be used in its place.
- if (accent >= 0)
- fpte = encodeColorsLegacy(primary, accent);
- else
- fpte = encodeColorsLegacy(primary, primary);
-
- // If the effect ID is set, it will be encoded and added to the string prefixed by one delimiter.
- if (effect)
- fpte += DELIMITER + encodeEffect(BigInt(effect));
-
+ fpte = accent >= 0 ? encodeColorsLegacy(primary, accent) : encodeColorsLegacy(primary, primary);
+ if (effect) fpte += DELIMITER + encodeEffect(BigInt(effect));
return fpte;
}
-
- // Since the primary color is unset, the accent color, if set, will be used in its place.
if (accent >= 0) {
fpte = encodeColorsLegacy(accent, accent);
-
- // If the effect ID is set, it will be encoded and added to the string prefixed by one delimiter.
- if (effect)
- fpte += DELIMITER + encodeEffect(BigInt(effect));
-
+ if (effect) fpte += DELIMITER + encodeEffect(BigInt(effect));
return fpte;
}
}
- // If the primary color is set, it will be encoded and added to the string.
else if (primary >= 0) {
fpte = encodeColor(primary);
-
- // If the accent color is set and different from the primary color, it
- // will be encoded and added to the string prefixed by one delimiter.
if (accent >= 0 && primary !== accent) {
fpte += DELIMITER + encodeColor(accent);
-
- // If the effect ID is set, it will be encoded and added to the string prefixed by one delimiter.
- if (effect)
- fpte += DELIMITER + encodeEffect(BigInt(effect));
-
+ if (effect) fpte += DELIMITER + encodeEffect(BigInt(effect));
return fpte;
}
}
- // If only the accent color is set, it will be encoded and added to the string.
- else if (accent >= 0)
- fpte = encodeColor(accent);
+ else if (accent >= 0) fpte = encodeColor(accent);
- // Since either the primary/accent colors are the same, both are unset, or just one is set, only one color will be added
- // to the string; therefore, the effect ID, if set, will be encoded and added to the string prefixed by two delimiters.
+ // Only one (or no) color was written above, so the effect needs a double delimiter to mark
+ // the skipped slot.
if (effect)
fpte += DELIMITER + DELIMITER + encodeEffect(BigInt(effect));
return fpte;
}
-/**
- * Extracts the delimiter-separated values of the first FPTE substring in a string.
- * @param str The string to be searched for a FPTE substring.
- * @returns An array of the found FPTE substring's extracted values. Values will be empty if not found.
- */
+/** Extracts the delimiter-separated values of the first FPTE substring in a string. */
export function extractFPTE(str: string) {
- /** The array of extracted FPTE values to be returned. */
const fpte: [maybePrimaryOrLegacy: string, maybeAccentOrEffect: string, maybeEffect: string] = ["", "", ""];
- /** The current index of {@link fpte} getting extracted. */
let i = 0;
for (const char of str) {
- /** The current character's codepoint. */
const cp = char.codePointAt(0)!;
- // If the current character is a delimiter, then the current index of fpte has been completed.
if (cp === DELIMITER_CODEPOINT) {
- // If the current index of fpte is the last, then the extraction is done.
if (i >= 2) break;
- i++; // Start extracting the next index of fpte.
+ i++;
}
- // If the current character is not a delimiter but a valid FPTE
- // character, it will be added to the current index of fpte.
else if (cp >= STARTING_CODEPOINT && cp <= ENDING_CODEPOINT)
fpte[i]! += String.fromCodePoint(cp - STARTING_CODEPOINT);
- // If an FPTE string has been found and its end has been reached, then the extraction is done.
else if (i > 0 || fpte[0]) break;
}
return fpte;
}
-/**
- * Detects if a string contains an FPTE substring based on the presence of
- * FPTE codepoints or the FPTE delimiter.
- * @param str The string to check.
- * @returns True if the string contains FPTE codepoints or delimiters, false otherwise.
- */
+/** Detects an FPTE substring by the presence of FPTE codepoints or the delimiter. */
export function hasFPTE(str: string): boolean {
for (const char of str) {
const cp = char.codePointAt(0)!;
- if (
- (cp >= STARTING_CODEPOINT && cp <= ENDING_CODEPOINT) || // FPTE codepoint range
- cp === DELIMITER_CODEPOINT // delimiter zero-width space
- ) {
+ if ((cp >= STARTING_CODEPOINT && cp <= ENDING_CODEPOINT) || cp === DELIMITER_CODEPOINT) {
return true;
}
}
@@ -216,11 +128,11 @@ export function hasFPTE(str: string): boolean {
}
export function stripFPTE(str: string) {
- return [...str]
- .filter(ch => {
- const cp = ch.codePointAt(0)!;
- return (cp < STARTING_CODEPOINT || cp > ENDING_CODEPOINT) && cp !== DELIMITER_CODEPOINT;
- })
- .join("")
- .trim();
- }
\ No newline at end of file
+ return [...str]
+ .filter(ch => {
+ const cp = ch.codePointAt(0)!;
+ return (cp < STARTING_CODEPOINT || cp > ENDING_CODEPOINT) && cp !== DELIMITER_CODEPOINT;
+ })
+ .join("")
+ .trim();
+}
diff --git a/plugins/fake-profile-themes-and-effects/src/lib/stores/ProfileEffectStore.ts b/plugins/fake-profile-themes-and-effects/src/lib/stores/ProfileEffectStore.ts
index 4a079e6..ba236f3 100644
--- a/plugins/fake-profile-themes-and-effects/src/lib/stores/ProfileEffectStore.ts
+++ b/plugins/fake-profile-themes-and-effects/src/lib/stores/ProfileEffectStore.ts
@@ -4,15 +4,10 @@ import { lazy } from "@shared/lib/lazy";
import type { CollectiblesItemType } from "@fpte/lib/records";
-// There is no dedicated "ProfileEffectStore" anymore - confirmed live via devtools that it's
-// absent from Metro's module registry even after actually using Discord's real, native Nitro
-// effect picker on a Nitro account (so this isn't a lazy-loading timing issue, the store itself
-// doesn't exist in the current build). Profile effects are now served from the same shop/
-// collectibles catalog used for avatar decorations and nameplates: getProfileEffectsFromCategories
-// (confirmed live to return real data - 340 real effects) reads CollectiblesCategoryStore's
-// categories and returns ProfileEffectConfig objects directly, not wrapped in the old
-// {id, skuId, config} shape this file's ProfileEffect type expects - the adapter below rebuilds
-// that shape so every other file in this plugin didn't need to change.
+// There's no dedicated ProfileEffectStore anymore - effects are served from the same shop/
+// collectibles catalog used for avatar decorations and nameplates. getProfileEffectsFromCategories
+// returns ProfileEffectConfig objects directly, not the old {id, skuId, config} shape this file's
+// ProfileEffect type expects, so the adapter below rebuilds that shape.
const getShopEffectsUtil = lazy(() => findByProps("getProfileEffectsFromCategories") as
{ getProfileEffectsFromCategories(categories: unknown): ProfileEffectConfig[] } | undefined);
const getCategoryStore = lazy(() => findByStoreName("CollectiblesCategoryStore") as
diff --git a/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/EffectPickerActionSheet.tsx b/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/EffectPickerActionSheet.tsx
index 2d5e115..f03b325 100644
--- a/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/EffectPickerActionSheet.tsx
+++ b/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/EffectPickerActionSheet.tsx
@@ -24,14 +24,9 @@ let lastGoodTree: RN.Node;
function PatchedEffectPickerActionSheet(props: EffectPickerActionSheetProps) {
const { currentEffectId, effects, onSelect, user } = props;
- // This has to be the very first thing checked, before EffectPicker is ever called below -
- // that call runs Discord's own EditProfileEffectActionSheet as a plain function from inside
- // this component's own render, which chains its internal hooks onto this fiber. Checking the
- // setting only *after* that call (as this used to) still paid that cost on every render
- // regardless of the setting - the crash it exists to avoid had already happened by the time
- // the fallback kicked in. forceFallbackEffectPicker doesn't change while this component is
- // mounted, so returning early here before any hooks run is safe across this component's own
- // re-renders.
+ // Must be checked before EffectPicker is called below - that call chains Discord's own hooks
+ // onto this fiber, so checking the setting after the call still pays the crash it's meant to
+ // avoid. forceFallbackEffectPicker doesn't change while mounted, so this early return is safe.
if (storage.forceFallbackEffectPicker)
return ;
@@ -39,10 +34,8 @@ function PatchedEffectPickerActionSheet(props: EffectPickerActionSheetProps) {
const themeContext = useThemeContext();
- // ProfileEffectRecord's fields mirror the raw shop config (skuId, title, thumbnailPreviewSrc,
- // effects, ...) - confirmed live that constructing it from our {id, skuId, config} wrapper
- // instead of effect.config leaves everything but skuId/type undefined, which is why the picker
- // used to open with a fully empty effect list.
+ // Must be built from effect.config, not our {id, skuId, config} wrapper - the wrapper leaves
+ // everything but skuId/type undefined.
const effectRecords = useMemo(() => effects.map(effect => ({ items: new ProfileEffectRecord(effect.config) })), [effects]);
let isLegacyEffectPicker = false;
@@ -105,8 +98,7 @@ function PatchedEffectPickerActionSheet(props: EffectPickerActionSheetProps) {
setPreviewUserId(user.id);
applyButton.props.onPress = () => {
setPreviewUserId(undefined);
- // selectedProfileEffect is a ProfileEffectRecord - confirmed live it has no .id field, only
- // .skuId, so comparing against effect.id here always missed and onSelect got null every time.
+ // selectedProfileEffect has no .id field, only .skuId.
onSelect(effects.find(effect => effect.skuId === effectPickerInner.props.selectedProfileEffect?.skuId)?.config ?? null);
};
diff --git a/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/FallbackEffectPickerActionSheet.tsx b/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/FallbackEffectPickerActionSheet.tsx
index ceead60..a5df51c 100644
--- a/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/FallbackEffectPickerActionSheet.tsx
+++ b/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/FallbackEffectPickerActionSheet.tsx
@@ -11,12 +11,8 @@ import { Radius, SafeAreaContext, Spacing, useWindowDimensions } from "@fpte/ui/
const ROW_SIZE = 3;
-// lodash isn't a dependency of this repo (no node_modules/lodash, nothing in package.json) - the
-// build's Rollup config only marks react/react-native external, so an unresolved "lodash" import
-// got emitted as a bare, undefined global reference in the compiled bundle. That threw
-// "ReferenceError: lodash is not defined" the instant Revenge evaluated this plugin's bundle,
-// before onLoad ever ran - which is why enabling the plugin silently reverted with no visible
-// error anywhere. Only one function was ever used from it, so it's inlined here instead.
+// lodash isn't a dependency here - an unresolved import threw "lodash is not defined" before
+// onLoad ever ran. Only one function was used from it, so it's inlined here instead.
function chunk(arr: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
diff --git a/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/showEffectPicker.tsx b/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/showEffectPicker.tsx
index c2109f3..4e03115 100644
--- a/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/showEffectPicker.tsx
+++ b/plugins/fake-profile-themes-and-effects/src/ui/actionSheets/showEffectPicker.tsx
@@ -11,10 +11,8 @@ export function showEffectPicker(
onSelect: (effect: ProfileEffectConfig | null) => void,
currentEffectId?: string | undefined
) {
- // Called directly from a native onPress with nothing else between it and the touch dispatch -
- // an uncaught throw here is fatal to the whole app (confirmed live via adb logcat: a plain
- // TypeError here surfaced as AndroidRuntime FATAL EXCEPTION, not a recoverable React error
- // screen), so this needs its own try/catch rather than relying on React's own error handling.
+ // Called directly from a native onPress - an uncaught throw here is fatal to the whole app
+ // (surfaces as AndroidRuntime FATAL EXCEPTION, not a recoverable React error screen).
try {
const user = UserStore.getCurrentUser();
const profileEffectStore = getProfileEffectStore();
diff --git a/plugins/fake-profile-themes-and-effects/src/ui/color.ts b/plugins/fake-profile-themes-and-effects/src/ui/color.ts
index 67805d8..8788693 100644
--- a/plugins/fake-profile-themes-and-effects/src/ui/color.ts
+++ b/plugins/fake-profile-themes-and-effects/src/ui/color.ts
@@ -11,12 +11,8 @@ export const resolveSemanticColor: (theme: Theme, semanticColor: EmptyObject) =>
?? find(m => m.meta?.resolveSemanticColor)?.meta.resolveSemanticColor
?? (() => undefined);
-/**
- * Discord renames semantic color tokens across versions - confirmed via live on-device diagnostics
- * that HEADER_SECONDARY, BACKGROUND_PRIMARY, BACKGROUND_FLOATING, and TEXT_NORMAL have all been
- * removed on at least one current build, which is exactly what crashed this plugin's profile editor
- * every time it rendered. This tries each candidate token name in order and never throws.
- */
+// Discord renames semantic color tokens across versions - tries each candidate name in order and
+// never throws.
export function resolveSemanticColorSafe(theme: Theme, tokenNames: string[], fallbackHex: string): string {
for (const name of tokenNames) {
const token = semanticColors?.[name];
@@ -41,10 +37,8 @@ export const useAvatarColors: (
export type Theme = "dark" | "light" | "midnight" | "darker";
-// The unguarded version of this line (`findByProps("getProfileTheme").getProfileTheme`) is what was
-// crashing the whole plugin bundle at load time on at least one Discord build - accessing a property
-// on the result of a failed findByProps throws immediately, outside any of this plugin's own
-// try/catch, since it runs while the module is being evaluated, before onLoad is ever reached.
+// Unguarded, this crashed the whole plugin bundle at load time - a property access on a failed
+// findByProps throws while the module is being evaluated, before onLoad is ever reached.
export const getProfileTheme: (primaryColor: T) => T extends number ? Theme : null
= (findByProps("getProfileTheme") as Record | undefined)?.getProfileTheme
?? ((primaryColor: any) => (primaryColor == null ? null : "dark")) as any;
diff --git a/plugins/fake-profile-themes-and-effects/src/ui/components/Icon.ts b/plugins/fake-profile-themes-and-effects/src/ui/components/Icon.ts
index 2c2828d..862f578 100644
--- a/plugins/fake-profile-themes-and-effects/src/ui/components/Icon.ts
+++ b/plugins/fake-profile-themes-and-effects/src/ui/components/Icon.ts
@@ -10,11 +10,8 @@ export interface IconProps extends Pick null` fallback, which has no .Sizes property at all for callers
-// like `Icon.Sizes.LARGE` to read. Falling back to a real, static SizeKey map instead of an
-// empty function means Icon always has a valid .Sizes even before the real module resolves.
+// A bare `() => null` fallback has no .Sizes for callers like Icon.Sizes.LARGE to read before
+// the real module resolves - this static map keeps Icon.Sizes always valid.
const FALLBACK_SIZES: Record = {
EXTRA_SMALL_10: "extra_small_10", EXTRA_SMALL: "extra_small", SMALL: "small", SMALL_20: "small_20",
MEDIUM: "medium", LARGE: "large", CUSTOM: "custom", REFRESH_SMALL_16: "refresh_small_16", SMALL_14: "small_14",
diff --git a/plugins/fake-profile-themes-and-effects/src/ui/components/index.ts b/plugins/fake-profile-themes-and-effects/src/ui/components/index.ts
index 228fa53..8df135a 100644
--- a/plugins/fake-profile-themes-and-effects/src/ui/components/index.ts
+++ b/plugins/fake-profile-themes-and-effects/src/ui/components/index.ts
@@ -12,10 +12,7 @@ export * from "./Icon";
export * from "./StaticEffect";
export * from "./Text";
-// findByName("FlashList") never matches - confirmed live (Key Inspector's Eval console, a full
-// module-key scan) that the exported property is still literally named FlashList, but findByName
-// matches by the component's own runtime .name/.displayName, which doesn't survive production
-// minification the way the export's property key does. findByProps looks at property keys instead.
+// findByName("FlashList") doesn't survive minification - findByProps looks at the property key.
const flashListModule = findByProps("FlashList") as Record | undefined;
export const FlashList: typeof $FlashList = flashListModule?.FlashList ?? (() => null);
diff --git a/plugins/key-inspector/src/lib/apiTree.ts b/plugins/key-inspector/src/lib/apiTree.ts
index f9545b3..6390bb4 100644
--- a/plugins/key-inspector/src/lib/apiTree.ts
+++ b/plugins/key-inspector/src/lib/apiTree.ts
@@ -11,11 +11,8 @@ function kindOf(value: any): string {
return typeof value;
}
-/**
- * Recursively walks the stable `window.vendetta` API surface exposed to every plugin and lists
- * every key path found, bounded so it stays useful instead of dumping megabytes of Discord internals.
- * This is the actual compat API plugins are built against - not Discord's full module registry.
- */
+// Walks the stable window.vendetta API surface and lists every key path found, bounded so it
+// stays useful instead of dumping Discord's full internals.
export function dumpVendettaApiTree(maxDepth = 3): string {
const seen = new WeakSet();
const lines: string[] = [];
diff --git a/plugins/key-inspector/src/lib/checks.ts b/plugins/key-inspector/src/lib/checks.ts
index 0417f2a..e5cf7f0 100644
--- a/plugins/key-inspector/src/lib/checks.ts
+++ b/plugins/key-inspector/src/lib/checks.ts
@@ -12,9 +12,8 @@ interface Check {
run: () => any;
}
-// Every internal lookup this repo's plugins (plus in-progress ports like FPTE) actually depend on.
-// Add to this list whenever a new plugin introduces a new lookup - that's what makes "run a full
-// scan" useful instead of checking one thing at a time.
+// Every internal lookup this repo's plugins depend on - add to this list whenever a new plugin
+// introduces one.
const CHECKS: Check[] = [
// Staff Tags
{ label: 'findByProps("computePermissions", "canEveryoneRole")', run: () => findByProps("computePermissions", "canEveryoneRole") },
diff --git a/plugins/key-inspector/src/lib/evalTool.ts b/plugins/key-inspector/src/lib/evalTool.ts
index 009b97b..22989ad 100644
--- a/plugins/key-inspector/src/lib/evalTool.ts
+++ b/plugins/key-inspector/src/lib/evalTool.ts
@@ -40,10 +40,8 @@ function safeStringify(value: any): string {
}
}
-// Android/iOS keyboards commonly have "smart punctuation" that silently swaps straight quotes for
-// curly ones as you type - that turns "foo" into “foo”, a different character to the JS parser.
-// Harmless to keep normalizing even though it turned out not to be the actual cause of the errors
-// seen while building this (see below) - cheap, and still a real footgun on its own.
+// Mobile keyboards commonly swap straight quotes for curly ones while typing, which is a
+// different character to the JS parser.
function normalizeSmartPunctuation(code: string): string {
return code
.replace(/[‘’]/g, "'")
@@ -51,22 +49,10 @@ function normalizeSmartPunctuation(code: string): string {
.replace(/[–—]/g, "-");
}
-/**
- * Runs arbitrary code typed into the Eval box, with this repo's own lookup helpers already in
- * scope - built so an on-device assumption behind a fix (does this property exist, does this call
- * behave the way decompiled source suggested) can be checked in seconds instead of a full
- * build/push/update/test cycle.
- *
- * The dynamically-compiled function body is deliberately NOT `async`. Hermes (RN's JS engine)
- * precompiles the whole app bundle to bytecode ahead of time, and its runtime `eval`/`new Function`
- * path only supports a reduced subset of the language - confirmed on-device that it rejects `async`
- * functions outright ("async functions are unsupported"), even though normal precompiled code
- * (including this very function) uses async/await freely. So the user's code runs as a plain
- * synchronous function; if it needs to do something timed/async, it returns a Promise itself
- * (`return new Promise(resolve => setTimeout(() => resolve(x), 1000))` works fine - only the
- * `async`/`await` *keywords* are the problem, not Promises as a runtime object) and this function
- * awaits that result out here, in precompiled code where await is fully supported.
- */
+// Runs arbitrary code typed into the Eval box, with this repo's own lookup helpers in scope. The
+// compiled function body is deliberately NOT async - Hermes's runtime eval/new Function path
+// rejects the async keyword outright, so the code runs as a plain sync function and returns a
+// Promise itself if it needs to do something timed, which this function awaits out here instead.
export async function runEval(rawCode: string): Promise {
const code = normalizeSmartPunctuation(rawCode);
try {
diff --git a/plugins/key-inspector/src/lib/fluxLogger.ts b/plugins/key-inspector/src/lib/fluxLogger.ts
index 6887f17..50f0e36 100644
--- a/plugins/key-inspector/src/lib/fluxLogger.ts
+++ b/plugins/key-inspector/src/lib/fluxLogger.ts
@@ -3,14 +3,9 @@ import { before } from "@vendetta/patcher";
const MAX_DISTINCT_TYPES = 500;
-/**
- * Captures every FluxDispatcher action type dispatched over `durationMs`, with an optional
- * substring filter, and returns a count-sorted report. Built for the everyday plugin-dev question
- * "what actually fires when I do X" - patches FluxDispatcher.dispatch itself (the same `before`
- * technique every other patch in this repo uses) rather than any internal interceptor API, so it
- * observes without ever altering or swallowing a single dispatch. Auto-unpatches when the window
- * ends or the distinct-type cap is hit, so a runaway capture can't leak a patch or grow unbounded.
- */
+// Captures every FluxDispatcher action type dispatched over durationMs and returns a count-sorted
+// report. Patches dispatch itself, observing without altering it, and auto-unpatches when the
+// window ends or the distinct-type cap is hit.
export function captureFluxEvents(durationMs: number, filter?: string): Promise {
return new Promise((resolve) => {
const counts = new Map();
diff --git a/plugins/key-inspector/src/lib/renderLogger.ts b/plugins/key-inspector/src/lib/renderLogger.ts
index 6ea12eb..56baf29 100644
--- a/plugins/key-inspector/src/lib/renderLogger.ts
+++ b/plugins/key-inspector/src/lib/renderLogger.ts
@@ -13,16 +13,9 @@ function safeStringify(value: any): string {
}
}
-/**
- * Captures the props a component actually receives, for up to `maxCalls` renders or `durationMs`
- * (whichever comes first) - built for the everyday "what shape are these props really" question
- * that normally means digging through decompiled source. Only works for components exported as a
- * React.memo/forwardRef wrapper (i.e. they have a `.type` property React calls through) - that
- * covers most of what findByTypeName/findByName actually return, but a bare unwrapped function
- * component can't be patched this way (there's no property to intercept a plain function's own
- * call through; you'd need the module object it's exported from instead - grab that with the Eval
- * tool and `before("default", thatModule, ...)` the same way this file does).
- */
+// Captures the props a component actually receives, for up to maxCalls renders or durationMs.
+// Only works for React.memo/forwardRef-wrapped components (they have a .type property to patch) -
+// a bare function component has no property to intercept its own calls through.
export function captureComponentRenders(componentName: string, maxCalls: number, durationMs: number): Promise {
return new Promise((resolve) => {
const name = componentName.trim();
diff --git a/plugins/message-logger/src/lib/capture.ts b/plugins/message-logger/src/lib/capture.ts
index c13d64d..59fa931 100644
--- a/plugins/message-logger/src/lib/capture.ts
+++ b/plugins/message-logger/src/lib/capture.ts
@@ -1,8 +1,7 @@
import type { LoggedAttachment, LoggedMessage } from "./types";
-// Every field read below is part of Discord's own public message object schema - checked in both
-// camelCase and snake_case since MessageStore's cached objects don't consistently normalize every
-// field the same way across message sources (gateway vs. REST-backfilled history).
+// Checked in both camelCase and snake_case since MessageStore's cached objects don't consistently
+// normalize every field across message sources.
function attachmentsOf(message: any): LoggedAttachment[] {
const attachments = message?.attachments;
if (!Array.isArray(attachments)) return [];
diff --git a/plugins/message-logger/src/patches/fluxIntercept.ts b/plugins/message-logger/src/patches/fluxIntercept.ts
index 50eb461..9154976 100644
--- a/plugins/message-logger/src/patches/fluxIntercept.ts
+++ b/plugins/message-logger/src/patches/fluxIntercept.ts
@@ -14,15 +14,11 @@ const CLEANUP_FLAG = "__msgLoggerCleanup";
const ChannelMessages = findByProps("_channelMessages");
const UserStore = findByStoreName("UserStore");
-// Ids currently kept "alive" as a faked MESSAGE_UPDATE instead of a real delete, mapped to the
-// channel they belong to - tracked so onUnload can put them back the way it found them (dispatch a
-// real delete for each) rather than leaving the client's own message cache holding fake state
-// forever if the plugin gets disabled.
+// Ids currently kept "alive" as a faked MESSAGE_UPDATE instead of a real delete, mapped to their
+// channel - tracked so onUnload can dispatch a real delete for each on unload.
export const fakedMessages = new Map();
-// Every past content version for a message, oldest first, current content NOT included (rowStyling
-// appends that itself from the live record). Same id-in-external-map pattern as fakedMessages -
-// row.message.id survives into the row JSON, a custom property doesn't (confirmed live).
+// Past content versions per message, oldest first, current content not included.
export const editHistory = new Map();
function currentUserId(): string | undefined {
@@ -130,9 +126,8 @@ function handleBulkDelete(event: any): any {
if (!kept.length) return undefined; // nothing kept inline - let the real bulk delete proceed as-is
- // Dispatching more actions from inside a dispatch-in-progress is a real re-entrancy risk (Flux
- // implementations commonly disallow it) - deferred to a microtask, one dispatch per kept message,
- // running after this bulk-delete's own dispatch has fully finished.
+ // Dispatching from inside a dispatch-in-progress risks Flux's re-entrancy guard - deferred to
+ // a microtask, after this bulk-delete's own dispatch has finished.
queueMicrotask(() => {
for (const id of kept) {
const original = ChannelMessages?.get?.(channelId)?.get?.(id);
diff --git a/plugins/message-logger/src/patches/messageRecord.ts b/plugins/message-logger/src/patches/messageRecord.ts
index 48298e9..4c73bb6 100644
--- a/plugins/message-logger/src/patches/messageRecord.ts
+++ b/plugins/message-logger/src/patches/messageRecord.ts
@@ -4,11 +4,8 @@ import { after, instead } from "@vendetta/patcher";
const TAG = "[MessageLogger]";
const FAKE_DELETE_FLAG = "__msgLoggerDeleted";
-// Discord wraps raw message JSON into a "MessageRecord" class before it ever reaches row
-// generation - a plain custom field set directly on the raw object doesn't reliably survive that
-// wrap. Confirmed against three independent existing plugins doing the same delete-as-update trick
-// (all thread a custom flag through this exact layer) - without this, the flag rowStyling.ts looks
-// for is gone by the time a row actually gets built.
+// Discord wraps raw message JSON into a MessageRecord class before it reaches row generation - a
+// plain custom field on the raw object doesn't reliably survive that wrap without this.
export function patchMessageRecord(cleanups: (() => void)[]): boolean {
const MessageRecordUtils = findByProps("updateMessageRecord", "createMessageRecord");
const MessageRecord = findByName("MessageRecord", false);
diff --git a/plugins/message-logger/src/patches/rowStyling.ts b/plugins/message-logger/src/patches/rowStyling.ts
index ce23c71..ac23676 100644
--- a/plugins/message-logger/src/patches/rowStyling.ts
+++ b/plugins/message-logger/src/patches/rowStyling.ts
@@ -11,14 +11,9 @@ function textNode(text: string) {
return { content: text, type: "text" };
}
-// Confirmed live via the decompiled Discord bundle: Discord's own "(edited)" tag isn't a plain text
-// node - it's colored through a dedicated `editedColor` field on the row that only applies to ONE
-// tag per row, which our per-history-entry tags can't use (there can be several, one per past
-// version). role-color-everywhere already found the workaround for coloring arbitrary text nodes on
-// this bridge: wrap the node in a `{type: "link", target: "usernameOnClick", ...}` shell and set a
-// `linkColor` inside it - text nodes themselves don't support a color field, but this wrapper does.
-// TEXT_MUTED is the same semantic token Discord's own muted/secondary text (timestamps, the real
-// edited tag) uses, confirmed live: resolves to #8a8a9a on this build's theme.
+// Discord's own "(edited)" tag is colored via a dedicated `editedColor` row field that only applies
+// once per row - no good for multiple per-history tags. Text nodes have no color field of their
+// own, so this wraps the tag in the same `link`/`linkColor` trick role-color-everywhere uses.
function editedTagNode() {
const color = resolveSemanticColorSafe(["TEXT_MUTED"], "#949BA4");
return {
@@ -39,10 +34,8 @@ function editedTagNode() {
}
// message.content in the row JSON is an array of {content, type} rich-text nodes, not a plain
-// string (confirmed live). Old versions get their own muted-colored "(edited)" tag stacked above
-// the current content - the current version keeps Discord's own native tag rather than getting a
-// second one from here, since that already renders correctly on a genuinely-edited message with no
-// help needed.
+// string. Old versions get their own muted "(edited)" tag stacked above the current content, which
+// keeps Discord's native tag since that already renders correctly on its own.
function applyEditHistory(message: any) {
const history = editHistory.get(message.id);
if (!history?.length) return;
@@ -58,11 +51,9 @@ function applyEditHistory(message: any) {
message.content = nodes;
}
-// Confirmed live: whatever builds the row JSON sent across the native bridge only carries known
-// schema fields through - a custom property set directly on the MessageRecord (confirmed present
-// there via eval) never survives into row.message here. message.id does survive, though, so
-// checking membership in fluxIntercept's own fakedMessages/editHistory maps (already tracked there
-// for onUnload cleanup / log capture) works where a custom flag on the row itself can't.
+// The row JSON sent across the native bridge only carries known schema fields - a custom property
+// on the message never survives, but message.id does, so we check membership in fluxIntercept's
+// own maps instead of a flag on the row itself.
function handleRow(row: any) {
const message = row?.message;
if (!message?.id) return;
diff --git a/plugins/message-logger/src/ui/LogViewerPage.tsx b/plugins/message-logger/src/ui/LogViewerPage.tsx
index 0a9114e..38b5e7a 100644
--- a/plugins/message-logger/src/ui/LogViewerPage.tsx
+++ b/plugins/message-logger/src/ui/LogViewerPage.tsx
@@ -7,8 +7,7 @@ import { resolveSemanticColorSafe } from "@shared/lib/color";
import { getLog, removeLogEntry } from "../lib/store";
import type { LoggedMessage } from "../lib/types";
-// Same Navigation/Navigator/close-button lookup already proven working elsewhere in this repo
-// (ViewRaw, RoseUtils' own log viewer) - reused verbatim including the fallback chain.
+// Same Navigation/Navigator/close-button lookup as this repo's ViewRaw plugin.
const Navigation = findByProps("push", "pushLazy", "pop");
const modalCloseButton =
findByProps("getRenderCloseButton")?.getRenderCloseButton ??
diff --git a/plugins/pronoun-db/src/index.tsx b/plugins/pronoun-db/src/index.tsx
index 99afe22..14fae36 100644
--- a/plugins/pronoun-db/src/index.tsx
+++ b/plugins/pronoun-db/src/index.tsx
@@ -5,16 +5,9 @@ import { rawFindByTypeName } from "@shared/lib/rawFind";
import PronounSection from "./ui/PronounSection";
import Settings from "./ui/Settings";
-// @vendetta/metro's find() caches a negative result forever, including one that only missed
-// because this plugin's onLoad ran before UserProfileContent's module had registered yet - once
-// cached, no amount of retrying with find() would ever see the real module. rawFindByTypeName
-// scans window.modules directly instead, so retrying it actually works. Confirmed live: this
-// exact search + the findInReactTree logic below both work correctly once given a real reference
-// - the only thing broken was find()'s one-shot, permanently-cached lookup.
-//
-// Discord renamed the top-level profile component from "UserProfile" to "UserProfileContent" at
-// some point (confirmed against decompiled current-build source) - checking both names covers
-// whichever one a given Discord build actually uses.
+// find() caches a negative result forever, so retrying it after onLoad races module registration
+// never works - rawFindByTypeName scans window.modules directly instead. Checks both names since
+// Discord renamed the top-level profile component from "UserProfile" to "UserProfileContent".
function getUserProfile(): any {
return rawFindByTypeName("UserProfileContent") ?? rawFindByTypeName("UserProfile");
}
@@ -25,8 +18,7 @@ function patchProfile(): (() => void) | false {
return after("type", UserProfile, (_: any, res: any) => {
try {
- // Same story here - the bio card component is now named "UserProfileAboutMeCard",
- // was "UserProfileBio" in older builds.
+ // "UserProfileAboutMeCard" now, "UserProfileBio" in older builds.
const bioSection = findInReactTree(res, (r) =>
Array.isArray(r?.props?.children) &&
r.props.children.some((c: any) => c?.type?.name === "UserProfileAboutMeCard" || c?.type?.name === "UserProfileBio")
@@ -35,8 +27,7 @@ function patchProfile(): (() => void) | false {
const children = bioSection?.props?.children;
if (!Array.isArray(children)) return;
- // userId is a direct prop on these cards now, not nested under displayProfile.userId
- // - keeping the old path as a fallback in case another build still shapes it that way.
+ // userId is a direct prop on these cards now, falling back to the older nested path.
const withProfile = children.find(
(c: any) => typeof c?.props?.userId === "string" || typeof c?.props?.displayProfile?.userId === "string"
);
diff --git a/plugins/pronoun-db/src/ui/PronounSection.tsx b/plugins/pronoun-db/src/ui/PronounSection.tsx
index 3e26261..c4aa759 100644
--- a/plugins/pronoun-db/src/ui/PronounSection.tsx
+++ b/plugins/pronoun-db/src/ui/PronounSection.tsx
@@ -13,8 +13,7 @@ export default function PronounSection({ userId }: { userId: string }) {
if (!pronouns || !UserProfileSection) return null;
- // Had no explicit color at all - illegible black-on-black on Discord's dark theme, since RN's
- // Text defaults to black with no theming of its own.
+ // RN's Text defaults to black with no theming - illegible on Discord's dark theme otherwise.
const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
return (
diff --git a/plugins/quest-orbs/src/lib/modules.ts b/plugins/quest-orbs/src/lib/modules.ts
index 1a6bc16..68525a0 100644
--- a/plugins/quest-orbs/src/lib/modules.ts
+++ b/plugins/quest-orbs/src/lib/modules.ts
@@ -21,14 +21,9 @@ export interface QuestActions {
fetchCurrentQuests?: (...args: any[]) => any;
}
-/**
- * rawFindByProps only checks that named properties exist on a module, not that they're real
- * functions - live-verified against this exact codebase that a decoy module matches
- * ("getAPIBaseURL"+"get"+"post") with "get" set to null, throwing "null is not a function" the
- * moment it's actually called. Discord's real internal REST client additionally has
- * getAPIBaseURL as a real function AND a V8APIError/V6OrEarlierAPIError export the decoy lacks -
- * this combo was confirmed live with an actual GET /quests/@me call before being trusted here.
- */
+// rawFindByProps only checks that named properties exist, not that they're real functions - a
+// decoy module can match with e.g. "get" set to null. Also requiring a real getAPIBaseURL function
+// plus a V8APIError/V6OrEarlierAPIError export rules those decoys out.
export const getRestClient = lazy(() => {
const modules = window?.modules;
if (!modules) return undefined;
@@ -57,15 +52,10 @@ export const getRestClient = lazy(() => {
export const getQuestStore = lazy(() => rawFindByStoreName("QuestStore"));
-/**
- * Only used for an optional, best-effort refresh (see questCompleter.ts) - completion itself goes
- * through direct REST calls (verified live end-to-end), not this module. Live-verified: patching
- * this module's exports to OBSERVE Discord's own internal calls does not work (same-chunk closure
- * calls bypass the exports object - see project_hook_mutation_doesnt_reach_internal_calls), but
- * that limitation is specific to patching/observing internal calls. A plugin calling a function on
- * this exports object directly, as an external caller, goes through the property lookup normally
- * and is unaffected by that limitation.
- */
+// Only used for an optional, best-effort refresh - completion itself goes through direct REST
+// calls, not this module. Calling a function on this exports object directly (as an external
+// caller) works fine; PATCHING it wouldn't, since same-chunk internal calls bypass the exports
+// object.
export const getQuestActions = lazy(() => {
const modules = window?.modules;
if (!modules) return undefined;
diff --git a/plugins/quest-orbs/src/lib/questCompleter.ts b/plugins/quest-orbs/src/lib/questCompleter.ts
index 9517529..3290422 100644
--- a/plugins/quest-orbs/src/lib/questCompleter.ts
+++ b/plugins/quest-orbs/src/lib/questCompleter.ts
@@ -6,10 +6,7 @@ import { getRestClient, getQuestStore, getQuestActions } from "./modules";
const TAG = "[QuestOrbs]";
-// Live-verified against a real WATCH_VIDEO_ON_MOBILE completion: Discord's own client sends a
-// video-progress heartbeat roughly every 7-8s while the video is actually playing. Matching that
-// cadence keeps this real-time paced (timestamps reflect genuinely elapsed time, never faked/sped
-// up), per the explicit "real-time paced, safer" design choice.
+// Matches Discord's own ~7-8s video-progress heartbeat cadence - real-time paced, never sped up.
const HEARTBEAT_INTERVAL_MS = 8000;
export interface QuestRunResult {
@@ -35,11 +32,7 @@ function questName(quest: any): string {
return quest?.config?.messages?.questName ?? quest?.id ?? "unknown quest";
}
-/**
- * Request bodies are live-verified exact shapes from a captured real enroll/claim call - Discord's
- * wire format is snake_case (distinct from QuestStore's client-normalized camelCase quest
- * objects). traffic_metadata_sealed is copied verbatim from the quest object, not generated.
- */
+// Discord's wire format is snake_case, distinct from QuestStore's camelCase quest objects.
function questActionBody(quest: any, extra?: Record) {
return {
location: 12,
@@ -57,12 +50,7 @@ async function enroll(rest: ReturnType, quest: any): Promi
});
}
-/**
- * Sends real-time-paced heartbeats until the response reports completion. Response bodies here are
- * raw server wire format (snake_case) since this calls the REST client directly, not Discord's own
- * action creators - live-verified: completed_at (top-level) flips non-null exactly when the target
- * duration is reached.
- */
+// Sends real-time-paced heartbeats until completed_at flips non-null.
async function watchVideo(rest: ReturnType, quest: any): Promise {
const target: number = quest.config.taskConfigV2.tasks.WATCH_VIDEO_ON_MOBILE.target;
let elapsed = 0;
@@ -84,13 +72,9 @@ async function watchVideo(rest: ReturnType, quest: any): P
type ClaimOutcome = "claimed" | "captcha" | "error";
-/**
- * A first real claim attempt during discovery came back HTTP 400 with a captcha_key/captcha_sitekey
- * body (hCaptcha) - this function detects that shape and stops, it never attempts to solve or
- * bypass it. Whether Discord's REST client rejects the promise or resolves with ok:false on a
- * non-2xx wasn't pinned down live (both code paths are handled defensively here), so this checks
- * both a thrown error and a resolved-but-not-ok response for the captcha shape.
- */
+// Detects a captcha_key response and stops - never attempts to solve or bypass it. Checks both a
+// thrown error and a resolved-but-not-ok response, since which path the REST client takes on a
+// non-2xx isn't pinned down.
async function claim(rest: ReturnType, quest: any): Promise {
try {
const res = await rest!.post({
@@ -108,11 +92,8 @@ async function claim(rest: ReturnType, quest: any): Promis
}
}
-/** Best-effort refresh so Discord's own UI (Quest Dock, Quests screen) doesn't show stale data
- * after this plugin completes quests via direct REST calls instead of Discord's own action
- * creators/dispatcher. Calling this function directly (not patching it) is a normal external call
- * through the exports object, unaffected by the same-chunk-closure limitation that blocks
- * PATCHING it - see modules.ts. Purely cosmetic: never blocks the real completion flow. */
+// Best-effort refresh so Discord's UI doesn't show stale data after completing quests via direct
+// REST calls instead of Discord's own action creators. Purely cosmetic.
function refreshQuestStore(): void {
try {
getQuestActions()?.fetchCurrentQuests?.();
diff --git a/plugins/quest-orbs/src/ui/Settings.tsx b/plugins/quest-orbs/src/ui/Settings.tsx
index b567c74..abba371 100644
--- a/plugins/quest-orbs/src/ui/Settings.tsx
+++ b/plugins/quest-orbs/src/ui/Settings.tsx
@@ -12,13 +12,8 @@ const { View, Text } = ReactNative;
const textColor = () => resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
const warningColor = () => resolveSemanticColorSafe(["TEXT_DANGER", "TEXT_NORMAL"], "#f0b232");
-/**
- * Deliberately the first thing on this screen, not folded into the smaller NoteBox below it -
- * automating quest completion is a form of client modification, and Discord's Terms of Service
- * prohibit exactly that. This isn't a theoretical/fine-print risk: it can get the account
- * suspended or banned, same as any other unofficial client automation. Nothing about running this
- * plugin is guaranteed safe just because the requests themselves mirror real client traffic.
- */
+// Deliberately the first thing on this screen, not folded into the NoteBox below it - this is a
+// real ban risk, not fine print.
function BanWarning() {
return (
void = () => {};
export default {
onLoad: () => {
- // The previous release's force-disable (storage.forceDisabledV2) was based on a misdiagnosis -
- // what looked like a guild-icon false positive was actually the DM list rendering correctly.
- // The real issue was the size-multiplier growth model being disproportionate on larger avatars
- // (confirmed on-device: "way too big" on YouBar) - fixed in ringPatch.ts by switching to a
- // fixed-px additive growth instead of a percentage multiplier. Force re-enabling once now that
- // the actual defect is fixed, superseding the old force-disable flag.
+ // Force re-enabling once, superseding the old force-disable flag from before the growth
+ // model in ringPatch.ts was fixed to additive px instead of a percentage multiplier.
if (storage.forceReenabledV3 !== true) {
storage.enabled = true;
storage.forceReenabledV3 = true;
diff --git a/plugins/radial-status/src/patches/ringPatch.ts b/plugins/radial-status/src/patches/ringPatch.ts
index 2b770df..cdc791f 100644
--- a/plugins/radial-status/src/patches/ringPatch.ts
+++ b/plugins/radial-status/src/patches/ringPatch.ts
@@ -2,20 +2,9 @@ import { General } from "@vendetta/ui/components";
import { before } from "@vendetta/patcher";
import { storage } from "@vendetta/plugin";
-/**
- * Forked from AngelW0lf's Radial Status (BSD-3-Clause, see LICENSE). This repo's own first attempt
- * "generalized" upstream's exact wrapper-size check (32x32, borderRadius 16 only) to any square,
- * fully-circular wrapper, on the theory that avatar sizes differ across contexts - that broadened
- * match visibly corrupted member list rows and the profile status indicator on-device, and still
- * never matched YouBar's own indicator at all.
- *
- * Confirmed live via Key Inspector's Eval console (a capture across YouBar, the profile screen,
- * member lists, and DM lists) exactly which sizes are real presence-carrying wrappers: 24, 32, 40,
- * 50, 60, and 80 (all square, all borderRadius === width/2, all with a `user.id` string + a
- * `status` string on children[1]/[3], and every single one with exactly 5 children) - so this now
- * matches that confirmed set specifically, plus the same child-count check every real one shared,
- * rather than either the overly narrow single-size check or the overly broad any-circle one.
- */
+// Forked from AngelW0lf's Radial Status (BSD-3-Clause, see LICENSE). Real presence-carrying
+// wrappers are square, borderRadius === width/2, with a user.id + status on children[1]/[3], and
+// exactly 5 children - matched against this confirmed set of sizes rather than any circular shape.
const CONFIRMED_SIZES = new Set([24, 32, 40, 50, 60, 80]);
export default function patchRing(): () => void {
@@ -47,12 +36,8 @@ export default function patchRing(): () => void {
if (!color) return; // no color configured for this status - leave the native dot alone
const baseSize = wrapper.style[circleIdx].width;
- // Growth is additive (a fixed number of px on each side), not a percentage multiplier -
- // a flat multiplier applied the same 30% growth everywhere, which meant a much bigger
- // ABSOLUTE size increase on large avatars (e.g. +24px at YouBar's 80px size) than on small
- // ones (+9.6px at a 32px member-list row) even though the relative growth was identical.
- // That's confirmed on-device as "slightly too big" on member list/profile and "way too big"
- // on YouBar - additive growth keeps the ring the same absolute thickness everywhere instead.
+ // Additive growth (fixed px), not a percentage multiplier - a flat multiplier made the
+ // ring way too thick on large avatars (YouBar) relative to small ones (member list).
const thickness = storage.ringThickness ?? 2;
const newSize = baseSize + thickness * 2;
diff --git a/plugins/radial-status/src/ui/Settings.tsx b/plugins/radial-status/src/ui/Settings.tsx
index 337860a..320efc7 100644
--- a/plugins/radial-status/src/ui/Settings.tsx
+++ b/plugins/radial-status/src/ui/Settings.tsx
@@ -6,10 +6,7 @@ import SettingsScaffold from "@shared/ui/SettingsScaffold";
import ColorInput from "@shared/ui/ColorInput";
import NoteBox from "@shared/ui/NoteBox";
-// The ring patch itself does a generic colors[status] lookup, not one hardcoded to three statuses -
-// offline just needed its own configurable entry here to work the same way the other three already
-// do. Discord doesn't show a colored dot for offline at all normally, so this is opt-in (blank by
-// default) rather than given a default color like the other three.
+// Offline is opt-in (blank by default) since Discord doesn't show a colored dot for it normally.
const STATUSES: { key: string; label: string; defaultColor?: string }[] = [
{ key: "online", label: "Online", defaultColor: "#23A55A" },
{ key: "idle", label: "Idle", defaultColor: "#F0B232" },
diff --git a/plugins/raidens-themes/src/patches/settings.ts b/plugins/raidens-themes/src/patches/settings.ts
index 4b5ad6e..e5a4efb 100644
--- a/plugins/raidens-themes/src/patches/settings.ts
+++ b/plugins/raidens-themes/src/patches/settings.ts
@@ -7,10 +7,8 @@ import { findInReactTree } from "@vendetta/utils";
const { FormSection, FormRow } = Forms;
// Pins a settings entry into Discord's own App Settings screen instead of leaving it buried under
-// Revenge's Plugins list - ported from Commands (kmmiio99o/vd-plugins), which calls through
-// window.bunny. This client only has vendetta, not bunny, so those lookups always came back
-// undefined and it silently fell back to copying the theme URL. Swapped to Vendetta's
-// findByProps/findByName, same as every other plugin in this repo.
+// Revenge's Plugins list - ported from Commands (kmmiio99o/vd-plugins), swapped from bunny to
+// Vendetta's findByProps/findByName.
const tabsNavigationRef = findByProps("getRootNavigationRef");
const settingConstants = findByProps("SETTING_RENDERER_CONFIG");
const createListModule = findByProps("createList");
@@ -73,9 +71,7 @@ function patchPanelUI(tabs: any, patches: any[]) {
}, true)
);
} catch {
- // This surface (the tablet/desktop-style settings panel) may not exist on this build at
- // all - patchTabsUI below covers the mobile settings list, which is what actually matters
- // here. A miss on this one shouldn't block that one.
+ // This surface may not exist on this build - patchTabsUI below covers the mobile list.
}
}
diff --git a/plugins/role-color-everywhere/src/lib/color.ts b/plugins/role-color-everywhere/src/lib/color.ts
index 8f925eb..56c60c2 100644
--- a/plugins/role-color-everywhere/src/lib/color.ts
+++ b/plugins/role-color-everywhere/src/lib/color.ts
@@ -2,9 +2,6 @@ import { resolveSemanticColorSafe } from "@shared/lib/color";
export { interpolateColor } from "@shared/lib/color";
-// TEXT_NORMAL was a bare, un-guarded reference here (confirmed dead via Key Inspector - Discord
-// renamed its whole semanticColors scheme, TEXT_NORMAL -> TEXT_DEFAULT among others), unlike every
-// other token lookup in this repo which already goes through a multi-candidate fallback chain.
export function defaultTextColor(): string | undefined {
return resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
}
diff --git a/plugins/role-color-everywhere/src/patches/memberList.ts b/plugins/role-color-everywhere/src/patches/memberList.ts
index 264c1e8..94714cc 100644
--- a/plugins/role-color-everywhere/src/patches/memberList.ts
+++ b/plugins/role-color-everywhere/src/patches/memberList.ts
@@ -7,11 +7,9 @@ const { Text } = ReactNative;
const SelectedGuildStore = findByStoreName("SelectedGuildStore");
const GuildRoleStore = findByStoreName("GuildRoleStore");
-// Colors the role section headers in the member list ("MODERATORS", "ADMINS", etc). The old
-// version patched View.prototype.render looking for a roleId prop - View isn't a class component
-// anymore and roleId doesn't exist by the time this reaches React. All we get is a pre-formatted
-// title string like "🚨 Moderator — 1" on a memo'd component called UserSectionInner, so we parse
-// the role name back out of that and match it against the guild's roles by name.
+// Colors the role section headers in the member list ("MODERATORS", "ADMINS", etc). Only a
+// pre-formatted title string like "🚨 Moderator — 1" is available on UserSectionInner, so the role
+// name is parsed back out of it and matched against the guild's roles by name.
export default function patchMemberList(): () => void {
const cleanups: (() => void)[] = [];
patchCreateElement(cleanups);
diff --git a/plugins/role-color-everywhere/src/patches/rows.ts b/plugins/role-color-everywhere/src/patches/rows.ts
index 117f22b..3603ffd 100644
--- a/plugins/role-color-everywhere/src/patches/rows.ts
+++ b/plugins/role-color-everywhere/src/patches/rows.ts
@@ -100,9 +100,8 @@ function handleRow(row: any) {
}
}
-// Discord's Android client renders the message list natively - message data is passed across the JS/native
-// bridge as JSON, so this is patched at that boundary rather than in a React tree, with a fallback to the
-// older `RowManager.generate` path for builds where the native bridge doesn't exist.
+// Discord's Android client renders the message list natively - patched at the JS/native JSON
+// bridge rather than in a React tree, with a fallback to RowManager.generate for older builds.
export default function patchRows(): () => void {
const { NativeModules } = ReactNative;
const DCDChatManager = NativeModules?.DCDChatManager;
@@ -119,12 +118,8 @@ export default function patchRows(): () => void {
});
}
- // RowManager used to be looked up eagerly at module-import time with the cached findByName - a
- // plugin's top-level code can run before Discord's own code has required RowManager itself, and
- // Revenge's findByName permanently caches a negative result. Confirmed live via Key Inspector's
- // Eval console: a raw, uncached scan found RowManager.prototype.generate present once the module
- // had actually initialized - waitFor + a raw lookup retries until that happens instead of giving
- // up on the first (possibly premature) miss.
+ // findByName caches a negative result permanently if it runs before RowManager registers -
+ // waitFor + a raw lookup retries until it's actually there.
const patches: (() => void)[] = [];
const handle = waitFor(
() => {
diff --git a/plugins/role-color-everywhere/src/patches/typingWrapper.ts b/plugins/role-color-everywhere/src/patches/typingWrapper.ts
index 730a46d..4f3ac05 100644
--- a/plugins/role-color-everywhere/src/patches/typingWrapper.ts
+++ b/plugins/role-color-everywhere/src/patches/typingWrapper.ts
@@ -6,10 +6,9 @@ import { defaultTextColor } from "../lib/color";
const GuildMemberStore = findByStoreName("GuildMemberStore");
-// Colors usernames in the "X is typing..." indicator. TYPING_WRAPPER_HEIGHT (what this used to
-// look up) doesn't exist anymore - the actual component is TypingIndicatorInner, which isn't a
-// top-level export, and it hands us channel + typingUserIds directly as props. Each typing user
-// gets their own inner element in the same order as typingUserIds, so we just zip them up.
+// Colors usernames in the "X is typing..." indicator. The component is TypingIndicatorInner, not
+// a top-level export, hands us channel + typingUserIds as props - zipped against each user's own
+// inner element in the same order.
export default function patchTypingWrapper(): () => void {
const cleanups: (() => void)[] = [];
patchCreateElement(cleanups);
diff --git a/plugins/rose-plugs/src/patches/settings.ts b/plugins/rose-plugs/src/patches/settings.ts
index 137ad40..dbd779c 100644
--- a/plugins/rose-plugs/src/patches/settings.ts
+++ b/plugins/rose-plugs/src/patches/settings.ts
@@ -6,10 +6,8 @@ import { findInReactTree } from "@vendetta/utils";
const { FormSection, FormRow } = Forms;
-// The mobile settings list renders a section's `label` field directly as its header text - `title`
-// goes unused there (confirmed live: an internal-looking label like "ROSES_PLUGS" rendered verbatim
-// instead of a real title). Raiden's Themes' own working section lookup relies on the same thing
-// (label === title === "Revenge"), so both fields just carry the same human-readable string.
+// The mobile settings list renders a section's `label` as its header text, not `title` - both
+// fields just carry the same string.
const SECTION_LABEL = "Rosie's Plugs";
const SECTION_TITLE = "Rosie's Plugs";
@@ -26,14 +24,10 @@ export interface SectionRow {
page: React.ComponentType;
}
-// A section's own row-key list is a one-time snapshot (see patchTabsUI below), but SETTING_RENDERER_
-// CONFIG used to rebuild fresh from getRows() on every access - confirmed live, those two calls can
-// observe different results (discovery is live, plugin state can shift between them), leaving a key
-// the snapshot still references missing from the config. Discord's own settings code assumes every
-// referenced key always resolves and crashes ("Cannot read property 'parent' of undefined") the
-// moment it doesn't. Accumulating here instead of rebuilding means any key ever referenced by a
-// section stays resolvable for good - populated both eagerly (right when a section snapshot is
-// built) and lazily (on every config read), so there's no window where one is ahead of the other.
+// A section's row-key list is a one-time snapshot, but the live config used to rebuild fresh on
+// every access - the two could disagree and leave a key the snapshot references missing from the
+// config, which crashes Discord's settings code. Accumulating instead of rebuilding keeps any
+// key ever referenced resolvable for good.
const rendererRowCache: Record = {};
function cacheRows(rows: SectionRow[]) {
@@ -117,8 +111,7 @@ function patchPanelUI(getRows: () => SectionRow[], patches: (() => void)[]) {
}, true)
);
} catch {
- // This surface (the tablet/desktop-style settings panel) may not exist on this build at
- // all - patchTabsUI below covers the mobile settings list, which is what actually matters.
+ // This surface may not exist on this build - patchTabsUI below covers the mobile list.
}
}
@@ -128,16 +121,10 @@ function patchTabsUI(getRows: () => SectionRow[], patches: (() => void)[]) {
return;
}
- // Raiden's Themes installs this exact same Object.defineProperty-override pattern on this same
- // property. Confirmed live: whichever plugin's override installs LAST completely replaces the
- // other's getter (defineProperty doesn't chain, it overwrites the whole descriptor) - freezing
- // the first plugin's rows as a one-time snapshot baked into the second plugin's closure. Any
- // plugin RosePlugs discovers afterward (installed mid-session, like a freshly-added plugin)
- // then never reaches SETTING_RENDERER_CONFIG again, even though the section still correctly
- // lists its key - the exact row-key/config mismatch that crashes Settings. Re-asserting this
- // override right before every read RosePlugs itself needs (not just once at setup) guarantees
- // RosePlugs' getter is always the most-recently-installed one when it actually matters, no
- // matter what order plugins load in or what either one does afterward.
+ // Raiden's Themes installs this same Object.defineProperty override on the same property -
+ // whichever installs last wins outright (defineProperty overwrites, it doesn't chain). Re-
+ // asserting ours before every read we need keeps it the most-recently-installed one regardless
+ // of plugin load order.
let baseConfigValue: any = settingConstants.SETTING_RENDERER_CONFIG;
const configGetter = () => {
diff --git a/plugins/rose-plugs/src/ui/PluginsBrowser.tsx b/plugins/rose-plugs/src/ui/PluginsBrowser.tsx
index ba9ed57..394e59e 100644
--- a/plugins/rose-plugs/src/ui/PluginsBrowser.tsx
+++ b/plugins/rose-plugs/src/ui/PluginsBrowser.tsx
@@ -15,9 +15,8 @@ interface BrowserState {
plugins: NexusPlugin[];
}
-// Confirmed live: installing from the detail sheet could get stuck on "Installing..." forever -
-// the promise from installPlugin wasn't reliably settling. This timeout is a safety net so a hung
-// call can't leave a row stuck indefinitely, on top of moving the action to an inline button.
+// installPlugin's promise doesn't always reliably settle - this timeout is a safety net so a hung
+// call can't leave a row stuck on "Installing..." forever.
function withTimeout(promise: Promise, ms: number): Promise {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("Timed out")), ms);
diff --git a/plugins/rose-utils/src/lib/Module.ts b/plugins/rose-utils/src/lib/Module.ts
index 5320a49..ad0d66d 100644
--- a/plugins/rose-utils/src/lib/Module.ts
+++ b/plugins/rose-utils/src/lib/Module.ts
@@ -3,9 +3,8 @@ import { React } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin";
import { showToast } from "@vendetta/ui/toasts";
-// Simplified from nexpid's original Module class (nexxutils) - same shape (id/label/settings/
-// handlers, per-module enable storage, error tracking), with the fancier UI-kit bits (custom
-// error viewer modal, choose-sheets, table rows) traded for this repo's plain @shared components.
+// Simplified from nexpid's original Module class (nexxutils), using this repo's plain @shared
+// components instead of the original's custom UI kit.
export const vstorage = storage as {
modules: Record }>;
};
@@ -70,14 +69,9 @@ export class Module> {
private started: boolean;
private listeners: Set<() => void>;
- // Assigned in the constructor body rather than as class-field initializers - this repo's
- // SWC build config lowers `class` to an ES5-style function for compatibility, and that pass
- // doesn't also handle the separate (newer) class-fields proposal, so field initializers like
- // `errors: Record = {};` were silently getting dropped from the compiled
- // output entirely. Confirmed by reading the built bundle directly: the compiled constructor
- // only set id/label/meta/settings/handlers, nothing else - every Module instance was missing
- // errors/patches/started/listeners, which crashed the settings screen on `Object.entries
- // (module.errors)` and made every module's own patches silently no-op.
+ // Assigned in the constructor body, not as class-field initializers - this repo's SWC build
+ // config lowers class syntax without handling class-fields, so field initializers were
+ // silently dropped from the compiled output.
constructor({ id, label, meta, settings, handlers }: {
id: string;
label: string;
diff --git a/plugins/rose-utils/src/lib/messageActionSheet.ts b/plugins/rose-utils/src/lib/messageActionSheet.ts
index 383c5e1..8fc845c 100644
--- a/plugins/rose-utils/src/lib/messageActionSheet.ts
+++ b/plugins/rose-utils/src/lib/messageActionSheet.ts
@@ -4,12 +4,8 @@ import { React } from "@vendetta/metro/common";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { findInReactTree } from "@vendetta/utils";
-// Injection technique ported from this repo's ViewRaw plugin (view-raw/src/index.tsx) - confirmed
-// working there for adding a row to the message long-press action sheet. Generalized here so more
-// than one RoseUtils module can each contribute their own rows through a single shared patch,
-// instead of every module that wants this hook patching openLazy separately (which would stack
-// duplicate patches, and duplicate "instance.__patched" bookkeeping, the moment more than one such
-// module is enabled at once).
+// Injection technique ported from this repo's ViewRaw plugin. Generalized so multiple RoseUtils
+// modules can share one openLazy patch instead of each patching it separately.
export interface MessageActionRow {
key: string;
label: string;
@@ -64,9 +60,8 @@ function patchOpenLazy(): () => void {
if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
component.then((instance: any) => {
- // Discord appears to reuse the same lazy-loaded instance across every action sheet
- // open - only patch it once, and track the active message on the instance itself so
- // re-opening on a different message doesn't need a fresh patch.
+ // Discord reuses the same lazy-loaded instance across opens - patch once, track the
+ // active message on the instance itself.
instance.__roseUtilsActiveMessage = msg.message;
if (instance.__roseUtilsPatched) return;
instance.__roseUtilsPatched = true;
diff --git a/plugins/rose-utils/src/modules/DeveloperMode/index.ts b/plugins/rose-utils/src/modules/DeveloperMode/index.ts
index 5d66da3..568fe7b 100644
--- a/plugins/rose-utils/src/modules/DeveloperMode/index.ts
+++ b/plugins/rose-utils/src/modules/DeveloperMode/index.ts
@@ -6,32 +6,14 @@ import { fluxSubscribe } from "@shared/lib/flux";
import { Module, ModuleCategory } from "../../lib/Module";
import { registerMessageAction, type MessageActionRow } from "../../lib/messageActionSheet";
-// Confirmed against decompiled current-build Discord source: Developer Mode is a proto-backed
-// setting exposed as DeveloperMode.getSetting()/updateSetting(bool), part of the same
-// modules/user_settings/UserSettings.tsx module that exports every other appearance/advanced
-// setting (DarkSidebar, MessageDisplayCompact, etc - all built the same way via defineProtoSetting).
-// It resets itself more than it should on Revenge - rather than guess at why, this just re-asserts
-// it every time the user's settings proto changes, which is the same event Discord's own settings
-// UI reacts to.
+// Developer Mode resets itself more than it should on Revenge - this re-asserts it every time the
+// user's settings proto changes rather than guessing why.
const UserSettingsModule = findByProps("DeveloperMode", "DarkSidebar");
-// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/activity-payload-capture.txt):
-// caught a real LOCAL_ACTIVITY_UPDATE payload while Watch Together was running - the activity
-// object is exactly {type, name, application_id, platform, party, secrets, flags, metadata}, snake
-// case, matching the primary branch checked below. That capture was for the local user specifically
-// (own activities dispatch through a local-first path, which is why PresenceStore.getActivities on
-// your own ID came back empty even mid-session) - but it's the same public Gateway Activity object
-// schema Discord broadcasts for everyone, which is what PresenceStore caches for *other* users -
-// the actual case this code needs, since it looks up the message author, not the local user. Not
-// yet caught a live PresenceStore.getActivities(otherUserId) call directly, but the shape match
-// leaves nothing left to guess at.
const PresenceStore = findByStoreName("PresenceStore");
-// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/all-checks.txt) against real
-// slash-command messages: message.interaction is populated with {id, name, type, user,
-// displayName} exactly as read below (message.type 20 = CHAT_INPUT_COMMAND on these).
-// interactionMetadata/interaction_metadata are checked as a fallback for whichever build doesn't
-// populate the legacy .interaction field.
+// message.interaction/interactionMetadata/interaction_metadata - checked in that order to cover
+// whichever field a given build populates.
function getCommandInteraction(message: any): { id: string; name?: string } | null {
const interaction = message?.interaction ?? message?.interactionMetadata ?? message?.interaction_metadata;
if (!interaction?.id) return null;
@@ -124,16 +106,14 @@ export default new Module({
try {
if (!DeveloperMode.getSetting()) DeveloperMode.updateSetting(true);
} catch {
- // Best-effort - if this particular check throws, the next settings change retries it.
+ // Next settings change retries it.
}
};
enforce();
this.patches.add(fluxSubscribe("USER_SETTINGS_PROTO_UPDATE", enforce));
- // Belt and suspenders: if whatever resets this doesn't go through
- // USER_SETTINGS_PROTO_UPDATE for some reason, this still catches it within 5s
- // instead of staying off for the rest of the session.
+ // Fallback poll in case something resets it without that event firing.
const interval = setInterval(enforce, 5000);
this.patches.add(() => clearInterval(interval));
}
diff --git a/plugins/rose-utils/src/modules/MessageDevTools/index.ts b/plugins/rose-utils/src/modules/MessageDevTools/index.ts
index 5d11994..390cf88 100644
--- a/plugins/rose-utils/src/modules/MessageDevTools/index.ts
+++ b/plugins/rose-utils/src/modules/MessageDevTools/index.ts
@@ -11,12 +11,6 @@ function copy(value: string, toastLabel: string) {
showToast(toastLabel, getAssetIDByName(COPY_ICON));
}
-// Every field read below (webhookId, nonce, attachments, messageReference, type/flags/timestamps)
-// is part of Discord's own public message object schema - the same shape the client already
-// deserializes the gateway/REST payload into before handing it to the action sheet - not
-// internal-UI guesswork, unlike the activity-ID lookup in the Developer Mode module. webhookId,
-// nonce, and messageReference are confirmed live via Key Inspector Eval
-// (/root/eval-for-revenge/ru/all-checks.txt) against real interaction-response and reply messages.
function webhookIdRow(message: any): MessageActionRow[] {
const webhookId = message?.webhookId ?? message?.webhook_id;
if (!webhookId) return [];
diff --git a/plugins/rose-utils/src/modules/MessageLogger/index.ts b/plugins/rose-utils/src/modules/MessageLogger/index.ts
index 4fd297e..8372f74 100644
--- a/plugins/rose-utils/src/modules/MessageLogger/index.ts
+++ b/plugins/rose-utils/src/modules/MessageLogger/index.ts
@@ -9,15 +9,9 @@ import { shouldIgnore, type FilterOptions } from "./lib/filters";
import { addLogEntry, clearLog, getLog } from "./lib/store";
import openLogViewer from "./ui/LogViewerPage";
-// Same core stores this plugin already relies on elsewhere (GuildStore/UserStore/etc. in
-// server-info-tools.ts) - MessageStore is equally fundamental, the client's own cache of every
-// message it currently has loaded. The whole capture mechanism depends on reading a message from
-// here *before* Discord's own MESSAGE_DELETE/MESSAGE_UPDATE handling removes/overwrites it - which
-// is exactly why this patches FluxDispatcher.dispatch itself (the same technique Key Inspector's own
-// flux event logger already proved out in this repo) instead of subscribing normally: a normal
-// FluxDispatcher.subscribe callback fires in registration order alongside every other listener,
-// including MessageStore's own, with no guarantee this one runs first - patching dispatch runs
-// before any listener at all, while the store's cache still holds the pre-delete/pre-edit message.
+// Patches FluxDispatcher.dispatch itself rather than subscribing normally, since a subscribe
+// callback has no guarantee of running before MessageStore's own - dispatch runs before any
+// listener, while the cache still holds the pre-delete/pre-edit message.
const MessageStore = findByStoreName("MessageStore");
const UserStore = findByStoreName("UserStore");
diff --git a/plugins/rose-utils/src/modules/MessageLogger/lib/capture.ts b/plugins/rose-utils/src/modules/MessageLogger/lib/capture.ts
index 2f9c507..4f9f3a7 100644
--- a/plugins/rose-utils/src/modules/MessageLogger/lib/capture.ts
+++ b/plugins/rose-utils/src/modules/MessageLogger/lib/capture.ts
@@ -1,9 +1,7 @@
import type { LoggedAttachment, LoggedEmbed, LoggedMessage, LoggedReaction } from "./types";
-// Every field read below is part of Discord's own public message object schema, the same shape
-// message-dev-tools.ts and server-info-tools.ts already read elsewhere in this plugin - checked in
-// both camelCase and snake_case since MessageStore's cached objects don't consistently normalize
-// every field the same way across message sources (gateway vs. REST-backfilled history).
+// Checked in both camelCase and snake_case since MessageStore's cached objects don't consistently
+// normalize every field across message sources.
function attachmentsOf(message: any): LoggedAttachment[] {
const attachments = message?.attachments;
if (!Array.isArray(attachments)) return [];
diff --git a/plugins/rose-utils/src/modules/MessageLogger/lib/store.ts b/plugins/rose-utils/src/modules/MessageLogger/lib/store.ts
index 996fc6a..8354607 100644
--- a/plugins/rose-utils/src/modules/MessageLogger/lib/store.ts
+++ b/plugins/rose-utils/src/modules/MessageLogger/lib/store.ts
@@ -1,9 +1,8 @@
import { storage } from "@vendetta/plugin";
import type { LoggedMessage } from "./types";
-// Its own top-level storage key, deliberately separate from Module.ts's vstorage.modules[id].options
-// bag (which the generic settings UI treats as a flat map of small config values) - this can grow to
-// thousands of entries, which doesn't belong mixed into that.
+// Its own top-level storage key, separate from Module.ts's vstorage.modules[id].options bag - this
+// can grow to thousands of entries, which doesn't belong mixed into that.
const logStorage = storage as { messageLoggerLog?: LoggedMessage[] };
export function getLog(): LoggedMessage[] {
diff --git a/plugins/rose-utils/src/modules/MessageLogger/ui/LogViewerPage.tsx b/plugins/rose-utils/src/modules/MessageLogger/ui/LogViewerPage.tsx
index 62257dd..cbbb962 100644
--- a/plugins/rose-utils/src/modules/MessageLogger/ui/LogViewerPage.tsx
+++ b/plugins/rose-utils/src/modules/MessageLogger/ui/LogViewerPage.tsx
@@ -7,9 +7,7 @@ import { resolveSemanticColorSafe } from "@shared/lib/color";
import { getLog, removeLogEntry } from "../lib/store";
import type { LoggedMessage } from "../lib/types";
-// Same Navigation/Navigator/close-button lookup pattern already proven working in this repo's
-// ViewRaw plugin - reused verbatim rather than re-derived, including its fallback chain (findByName
-// misses Navigator on current builds, findByProps("Navigator") is the confirmed-working fallback).
+// Same Navigation/Navigator/close-button lookup pattern as this repo's ViewRaw plugin.
const Navigation = findByProps("push", "pushLazy", "pop");
const modalCloseButton =
findByProps("getRenderCloseButton")?.getRenderCloseButton ??
@@ -20,9 +18,7 @@ const { View, Text, ScrollView, TouchableOpacity } = ReactNative;
const PAGE_SIZE = 100;
-// Every raw Text below used to have no explicit color at all - illegible black-on-black on
-// Discord's dark theme, since RN's Text defaults to black with no theming of its own. Same
-// resolveSemanticColorSafe fallback-chain pattern used everywhere else in this repo.
+// RN's Text defaults to black with no theming - illegible on Discord's dark theme otherwise.
function textColor(): string {
return resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
}
diff --git a/plugins/rose-utils/src/modules/Minimod/index.ts b/plugins/rose-utils/src/modules/Minimod/index.ts
index 838257f..92754cc 100644
--- a/plugins/rose-utils/src/modules/Minimod/index.ts
+++ b/plugins/rose-utils/src/modules/Minimod/index.ts
@@ -25,9 +25,6 @@ export default new Module({
},
handlers: {
onStart() {
- // getCommunicationDisabledUserMap doesn't exist anymore (confirmed against decompiled
- // current-build Discord source) - GuildMemberStore only exposes per-member lookups now,
- // via getMember(guildId, userId).communicationDisabledUntil.
if (!this.storage.options.showTimeouts || !GuildMemberStore?.getMember) return;
this.patches.add(
diff --git a/plugins/rose-utils/src/modules/ServerInfoTools/index.ts b/plugins/rose-utils/src/modules/ServerInfoTools/index.ts
index 3ab3fd3..5ed8460 100644
--- a/plugins/rose-utils/src/modules/ServerInfoTools/index.ts
+++ b/plugins/rose-utils/src/modules/ServerInfoTools/index.ts
@@ -6,11 +6,6 @@ import { Module, ModuleCategory } from "../../lib/Module";
import { registerMessageAction, type MessageActionRow } from "../../lib/messageActionSheet";
import { decodeSnowflakeTimestamp } from "../../lib/snowflake";
-// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/all-checks.txt) against a real
-// server: GuildStore.getGuild() returns name/ownerId/features/premiumTier/premiumSubscriberCount/
-// verificationLevel/vanityURLCode exactly as read below. RelationshipStore.getFriendIDs() and
-// GuildMemberStore.getMember(guildId, userId) are confirmed working too - a real friend showed up
-// as a cached member of the test server, cross-checked against UserStore.getUser().username.
const GuildStore = findByStoreName("GuildStore");
const RelationshipStore = findByStoreName("RelationshipStore");
const GuildMemberStore = findByStoreName("GuildMemberStore");
diff --git a/plugins/rose-utils/src/modules/SpotifyListenAlong/index.ts b/plugins/rose-utils/src/modules/SpotifyListenAlong/index.ts
index ac5732a..a7cdf65 100644
--- a/plugins/rose-utils/src/modules/SpotifyListenAlong/index.ts
+++ b/plugins/rose-utils/src/modules/SpotifyListenAlong/index.ts
@@ -8,12 +8,8 @@ import light from "./light.png";
import { Module, ModuleCategory } from "../../lib/Module";
import ClassicListenButton from "./components/ClassicListenButton";
-// Confirmed against decompiled current-build Discord source (app/modules/now_playing/native/
-// UserActivitySpotify.tsx): SpotifyPlayButton is a plain function component now, not a class -
-// `after("render", SpotifyPlayButton.prototype, ...)` is why this threw "render is not a
-// function" on every activity render. Patching the exported property directly works for both
-// function and class components, and the function's own first argument already carries
-// { activity, style } directly - no need to dig through fiber internals for it anymore.
+// SpotifyPlayButton is a plain function component, not a class - patching the exported property
+// directly works for both, and its first argument already carries {activity, style}.
const SpotifyModule = findByProps("SpotifyPlayButton");
export default new Module({
diff --git a/plugins/rose-utils/src/ui/Settings.tsx b/plugins/rose-utils/src/ui/Settings.tsx
index 594e30d..4b78d8f 100644
--- a/plugins/rose-utils/src/ui/Settings.tsx
+++ b/plugins/rose-utils/src/ui/Settings.tsx
@@ -10,9 +10,7 @@ import { ModuleCategory, vstorage, type AnyModule, type ModuleSetting } from "..
const { View, Text, TouchableOpacity } = ReactNative;
-// Raw Text below this point (the category header and bulk-action links) had no explicit color at
-// all - illegible black-on-black on Discord's dark theme, since RN's Text defaults to black with no
-// theming of its own. Same resolveSemanticColorSafe fallback-chain pattern used everywhere else.
+// RN's Text defaults to black with no theming - illegible on Discord's dark theme otherwise.
const textColor = () => resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
function resolveSubLabel(setting: ModuleSetting, value: any): string | undefined {
@@ -104,12 +102,8 @@ function ModuleSettingRow({ module, settingKey }: { module: AnyModule; settingKe
function ModuleSection({ module }: { module: AnyModule }) {
module.useRefresh();
- // Belt and suspenders alongside the custom refresh() pub-sub above: module.storage.enabled is
- // read for the switch's `value` prop right below, and useProxy is the same storage-reactivity
- // mechanism every other plugin's settings screen in this repo already relies on for a toggle to
- // visually update immediately - this repo's own custom listeners system covers module.errors
- // too (plain in-memory state, not part of storage, so useProxy alone can't see it), which is
- // why that mechanism exists at all rather than being replaceable by useProxy outright.
+ // useProxy covers storage reactivity for the toggle below; the custom refresh() above also
+ // covers module.errors, which is plain in-memory state useProxy can't see.
useProxy(module.storage);
const errorEntries = Object.entries(module.errors);
@@ -183,11 +177,7 @@ function CategoryHeader({
const CATEGORY_ORDER: ModuleCategory[] = [ModuleCategory.Useful, ModuleCategory.Fixes, ModuleCategory.Fun];
export default function Settings() {
- // Every module's own toggle already re-renders its own ModuleSection via module.useRefresh()
- // inside ModuleSection - but the category headers here (enabled counts) live in this parent
- // component, which isn't otherwise subscribed to anything. Subscribing every module here too,
- // in this same fixed order every render, keeps those counts in sync with both individual
- // toggles and the bulk All/None actions below.
+ // Subscribes so the category headers' enabled counts stay in sync with toggles/bulk actions.
for (const module of modules) module.useRefresh();
const [query, setQuery] = React.useState("");
diff --git a/plugins/server-drawer/src/components/DmTile.tsx b/plugins/server-drawer/src/components/DmTile.tsx
index e6a238e..8fd97f4 100644
--- a/plugins/server-drawer/src/components/DmTile.tsx
+++ b/plugins/server-drawer/src/components/DmTile.tsx
@@ -21,9 +21,8 @@ function openDms() {
}
}
-// Was previously only reachable via the native left server rail's own DM shortcut icon - moved
-// into the drawer's own grid instead, as the first tile, so DMs live in the same place as every
-// other server rather than in a spot that only exists on the rail this plugin otherwise hides.
+// Moved into the drawer's own grid as the first tile, since this plugin otherwise hides the
+// native rail DMs used to live on.
export default function DmTile() {
const selected = Flux?.useStateFromStores?.(
[NavContext],
diff --git a/plugins/server-drawer/src/patches/contentPatch.tsx b/plugins/server-drawer/src/patches/contentPatch.tsx
index 146da63..13c0ec8 100644
--- a/plugins/server-drawer/src/patches/contentPatch.tsx
+++ b/plugins/server-drawer/src/patches/contentPatch.tsx
@@ -18,12 +18,9 @@ function isNamed(name: string) {
type?.type?.name === name || type?.type?.displayName === name;
}
-// Confirmed live (Key Inspector): find()'s own module-registry search can land on a different copy
-// of QuestDockContentExpanded than the one actually mounted in the live tree - the eager mutation
-// below silently patches the wrong reference while the real one keeps rendering Discord's own
-// (mostly empty) quest content. registerTypeDetector doesn't search the registry at all - it
-// inspects the exact `type` value passed to createElement/jsx the moment something actually
-// creates an element with it, so there's no "which copy" ambiguity to land on wrong.
+// find()'s module-registry search can land on a different copy of QuestDockContentExpanded than
+// the one actually mounted - registerTypeDetector instead inspects the exact `type` value passed
+// to createElement/jsx as it's used, so there's no "which copy" ambiguity.
export function patchExpanded(
cleanups: (() => void)[]
): boolean {
diff --git a/plugins/server-drawer/src/patches/createElementIntercept.ts b/plugins/server-drawer/src/patches/createElementIntercept.ts
index e6aaa83..6c7b5cd 100644
--- a/plugins/server-drawer/src/patches/createElementIntercept.ts
+++ b/plugins/server-drawer/src/patches/createElementIntercept.ts
@@ -4,11 +4,7 @@ import { after } from "@vendetta/patcher";
interface Intercept {
replacement: React.ComponentType;
extraProps?: Record;
- /**
- * How many ancestor levels above this element are allowed to be collapsed to zero size along
- * with it (0 = none). See `COLLAPSE_STYLE` below for why replacing a component with one that
- * renders nothing is, on its own, not enough to reclaim its space.
- */
+ /** How many collapsed-to-zero-size ancestor levels above this element are also allowed to collapse (0 = none). */
collapseAncestors?: number;
}
@@ -36,21 +32,12 @@ let typeDetectors: TypeDetector[] = [];
const detectorKeys = new Set();
let isPatched = false;
-/**
- * A React element created for something we're hiding, mapped to how many more ancestor levels may
- * still be collapsed with it. Elements are plain objects and are created child-first (a parent's
- * jsx() call can only run once its children exist), so a mark placed on a child is always already
- * present by the time the parent's own call is intercepted - which is what makes walking the
- * collapse *upward* possible at element-creation time at all.
- */
+// Elements are created child-first, so a collapse mark on a child is already present by the time
+// the parent's own call is intercepted - that's what makes walking the collapse upward possible.
const collapseMarks = new WeakMap();
-/**
- * Zeroes an element out in every direction a parent could have sized it. `display: "none"` alone
- * is enough on Yoga, but the explicit width/flex zeroes also cover a container that sets its
- * child's size through a style array further up, and `overflow: hidden` stops anything absolutely
- * positioned inside from still painting.
- */
+// display: none alone doesn't reclaim space on every layout, so this also zeroes width/flex/margin
+// and hides overflow.
const COLLAPSE_STYLE = {
display: "none" as const,
width: 0,
@@ -74,31 +61,20 @@ export function registerIntercept(
intercepts.set(original, { replacement, extraProps, collapseAncestors: options?.collapseAncestors });
}
-// Same idea as registerIntercept, but matches on a predicate over the element's props instead of
-// an exact type reference - useful when the component has no reliable name to find it by, but
-// always passes some distinguishing prop. Pass replacement: null to render nothing instead.
+// Matches on a props predicate instead of an exact type reference, for components with no
+// reliable name. Pass replacement: null to render nothing.
export function registerPropsIntercept(predicate: (props: any) => boolean, replacement: React.ComponentType | null) {
propsIntercepts.push({ predicate, replacement });
}
-// Rewrites an element's props in place, keeping the same type - unlike registerPropsIntercept,
-// this doesn't swap in a different component, it just lets the real one render with different
-// input.
+// Rewrites props in place, keeping the same type.
export function registerPropsTransform(predicate: (props: any) => boolean, transform: (props: any) => any) {
propsTransforms.push({ predicate, transform });
}
-// Purely observational: the first time an element is created whose type matches predicate, calls
-// onDetected(type) with the live reference. Doesn't touch rendering - pair it with
-// registerIntercept/registerPropsIntercept in the callback for that. Exists for components that
-// aren't findable by searching Metro's registry after the fact (races against first mount).
-//
-// key dedupes registrations - retrying registration every 200ms used to pile up duplicate
-// detectors, each re-tested against every element created anywhere in the app.
-//
-// persistent keeps the detector alive for every future match instead of firing once. Some type
-// references change across navigation (switching servers via this plugin's own drawer), so a
-// one-shot detector would only ever catch the first, now-abandoned reference.
+// Fires onDetected(type) the first time a matching element is created. For components with no
+// stable module export to search for after the fact. persistent keeps firing on every match
+// instead of once, for references that change across navigation.
export function registerTypeDetector(
key: string,
predicate: (type: any) => boolean,
@@ -123,14 +99,14 @@ function runTypeDetectors(type: any) {
try {
matched = detector.predicate(type);
} catch {
- // A bad predicate shouldn't block every other detector or every element from rendering.
+ // Ignore
}
if (!matched) continue;
try {
detector.onDetected(type);
} catch {
- // Same - a detector's own handler throwing shouldn't take anything else down.
+ // Ignore
}
if (!detector.persistent) {
detector.justFired = true;
@@ -138,9 +114,6 @@ function runTypeDetectors(type: any) {
}
}
- // Only rebuild the list when a one-shot detector actually fired. The previous version rebuilt
- // it on every single element creation app-wide, forever, because a persistent detector keeps
- // the list from ever being empty - an allocation per element on the hottest path in the app.
if (consumed) {
typeDetectors = typeDetectors.filter((d) => d.persistent || !d.justFired);
}
@@ -149,12 +122,7 @@ function runTypeDetectors(type: any) {
declare module "./createElementIntercept" {}
interface TypeDetector { justFired?: boolean }
-/**
- * Returns the collapse depth a parent element should inherit from its children, or 0 for "leave
- * this element alone."
- *
- * Returns the *maximum* collapse budget among real children.
- */
+/** Returns the collapse depth a parent should inherit from its children (max of any real child, 0 if none). */
function inspectCollapseChild(child: any): number {
if (child == null || child === false || typeof child !== "object") return 0;
return collapseMarks.get(child) ?? 0;
@@ -188,7 +156,7 @@ function resolveReplacement(type: any, props: any, rest: any[]): { type: any; pr
effectiveProps = transform(effectiveProps);
}
} catch {
- // A bad transform shouldn't block every other element from rendering.
+ // Ignore
}
}
}
@@ -200,7 +168,7 @@ function resolveReplacement(type: any, props: any, rest: any[]): { type: any; pr
return replacement ? { type: replacement, props: effectiveProps } : null;
}
} catch {
- // A bad predicate shouldn't block every other element from rendering.
+ // Ignore
}
}
}
@@ -214,9 +182,7 @@ function resolveReplacement(type: any, props: any, rest: any[]): { type: any; pr
};
}
- // Nothing to swap here, but if this element's only child is one we've already collapsed, this
- // element is a wrapper that exists purely to hold it - zero it out too and pass the remaining
- // budget further up.
+ // A wrapper whose only child was already collapsed - zero it out too and pass the remaining budget up.
const inherited = inheritedCollapseDepth(effectiveProps, rest);
if (inherited > 0) {
return {
@@ -233,9 +199,8 @@ function resolveReplacement(type: any, props: any, rest: any[]): { type: any; pr
return undefined;
}
-// Mutates the already-created element in place rather than substituting args before creation -
-// after() only sees the return value, not the call itself. A raw property reassignment on the
-// jsx-runtime object doesn't reliably reach real render calls; after() does.
+// after() only sees the return value, not the call itself, so we mutate the element in place
+// rather than substituting args beforehand.
function applyResolved(res: any, type: any, props: any, rest: any[]) {
if (!res || typeof res !== "object") return res;
const resolved = resolveReplacement(type, props, rest);
@@ -265,11 +230,8 @@ export function patchCreateElement(cleanups: (() => void)[]) {
console.warn("[ServerDrawer] React.createElement is null, skipping patch");
}
- // Discord's bundle compiles JSX through the automatic runtime (jsx/jsxs/jsxDEV) instead of
- // React.createElement, so patching createElement alone misses nearly everything. We scan for
- // the runtime by shape rather than name since names get mangled, patch every copy we find (a
- // bundle can have more than one), and keep scanning on an interval since Metro registers
- // modules lazily - a copy that hasn't been required yet at boot just isn't there to find.
+ // Discord compiles JSX through jsx/jsxs/jsxDEV, not React.createElement - scan for those
+ // runtimes by shape (names are mangled) and keep scanning since Metro registers lazily.
const patchedJsxRuntimes = new WeakSet();
function isJsxRuntime(m: any): boolean {
@@ -297,7 +259,7 @@ export function patchCreateElement(cleanups: (() => void)[]) {
const dflt = exports.default;
if (dflt != null && isJsxRuntime(dflt)) patchJsxObject(dflt);
} catch {
- // Ignore one module's bad shape and continue scanning.
+ // Ignore
}
}
@@ -311,13 +273,9 @@ export function patchCreateElement(cleanups: (() => void)[]) {
}
}
- // Patch any jsx runtimes already loaded before Discord's first render pass starts, synchronously
- // - an async scan would complete too late for the very first render of an early-mounting target.
scanAndPatchJsxRuntimes();
- // Fast while the app is still booting and chunks are being pulled in, then slow, then stop -
- // a full window.modules walk is not free and there's nothing left to catch once the UI has
- // settled.
+ // Fast while the app boots, then slow, then stop.
let ticks = 0;
let timer: ReturnType | undefined = setInterval(() => {
scanAndPatchJsxRuntimes();
diff --git a/plugins/server-drawer/src/patches/transparentBackground.ts b/plugins/server-drawer/src/patches/transparentBackground.ts
index 81ee128..cfda6f9 100644
--- a/plugins/server-drawer/src/patches/transparentBackground.ts
+++ b/plugins/server-drawer/src/patches/transparentBackground.ts
@@ -2,13 +2,10 @@ import { registerPropsTransform, registerPropsIntercept } from "./createElementI
const TAG = "[ServerDrawer]";
-// Confirmed live: the quest dock's outer "island" wrapper - a ReanimatedView ten levels above the
-// drawer's own grid - carries an opaque card background (borderRadius: 24) via useAnimatedStyle.
-// It's not anything our own code renders, so there's no component reference to intercept - matching
-// the static part of its computed style is the only way to reach it. The color itself showed up as
-// two different string formats (rgba(...) vs hex) across two live reads of the same element, so
-// matching on the exact color string is unreliable - borderRadius plus "has some background color
-// at all" is the stable signal.
+// The quest dock's outer wrapper carries an opaque card background (borderRadius: 24) via
+// useAnimatedStyle - no component reference to intercept, so match on the computed style instead.
+// The color itself varies in format (rgba vs hex), so borderRadius + "has a background color" is
+// the stable signal, not the color string.
function flatten(style: any): Record {
if (!style) return {};
if (Array.isArray(style)) return Object.assign({}, ...style.map(flatten));
@@ -20,11 +17,9 @@ function isQuestDockCard(props: any): boolean {
return s.borderRadius === 24 && typeof s.backgroundColor === "string";
}
-// Removing the card's own color only revealed a second, separate layer underneath: the quest's own
-// promotional hero photo, rendered as a real pointed at a discordapp.com/quests/
-// URL - confirmed live (an actual promo image showed through once the color was gone). Matching by
-// URL shape, not by component, since the same image component renders ordinary guild/folder icons
-// everywhere else in the app too.
+// Removing the card color reveals a second layer underneath: the quest's promotional hero photo.
+// Matched by URL shape, not component, since the same image component renders ordinary guild/
+// folder icons everywhere else too.
function getSourceUri(props: any): string | undefined {
const source = props?.source;
if (!source) return undefined;
diff --git a/plugins/staff-tags/src/lib/gradient.ts b/plugins/staff-tags/src/lib/gradient.ts
index a1a71de..e1bd4b2 100644
--- a/plugins/staff-tags/src/lib/gradient.ts
+++ b/plugins/staff-tags/src/lib/gradient.ts
@@ -3,11 +3,7 @@ import { findByProps } from "@vendetta/metro";
let cached: any;
let attempted = false;
-/**
- * Discord ships a native gradient view (used for boost/nitro badges) but it isn't part of any
- * stable public API, so this is a best-effort lookup that quietly returns null if it can't be found -
- * gradient tags then fall back to a solid color instead of crashing anything.
- */
+// Not a stable public API - best-effort lookup, falls back to a solid color if not found.
export function getGradientComponent(): any {
if (attempted) return cached;
attempted = true;
diff --git a/plugins/staff-tags/src/patches/name.tsx b/plugins/staff-tags/src/patches/name.tsx
index 1b59623..1d46494 100644
--- a/plugins/staff-tags/src/patches/name.tsx
+++ b/plugins/staff-tags/src/patches/name.tsx
@@ -4,13 +4,9 @@ import { findInReactTree } from "@vendetta/utils";
import getTag from "../lib/getTag";
import GradientTag from "../ui/GradientTag";
-// Covers the name row shown in chat messages (next to the display name + timestamp) and channel headers.
-// findByName("DisplayName", false) stopped matching - confirmed live (Key Inspector's Eval console,
-// a full module-key scan) that the module still exports a property literally named DisplayName, but
-// findByName matches by the component's own runtime .name/.displayName, which doesn't survive
-// production minification the way the export's property key does. findByProps looks at property
-// keys instead, so the patch target changes from DisplayName.default to DisplayNameModule.DisplayName.
-// HeaderName's current name isn't confirmed yet - still guarded, just not fixed.
+// Covers the name row in chat messages and channel headers. findByName("DisplayName") stopped
+// matching under minification since it matches runtime .name, not the export's property key -
+// findByProps looks at the property key instead, so the target is DisplayNameModule.DisplayName.
const DisplayNameModule = findByProps("DisplayName") as any;
const HeaderName = findByName("HeaderName", false);
const TagModule = findByProps("getBotLabel");
diff --git a/plugins/staff-tags/src/patches/profile.tsx b/plugins/staff-tags/src/patches/profile.tsx
index 71ef966..a347e8f 100644
--- a/plugins/staff-tags/src/patches/profile.tsx
+++ b/plugins/staff-tags/src/patches/profile.tsx
@@ -6,13 +6,9 @@ import GradientTag from "../ui/GradientTag";
const GuildStore = findByStoreName("GuildStore");
-// The full profile doesn't render through UserRow at all - it's PrimaryInfo -> UserProfilePrimaryInfo,
-// and PrimaryInfo only shows up unrendered inside UserProfileContent's own output (no children to
-// search). UserProfilePrimaryInfo is the one that actually builds the name row, and it's not a
-// top-level export, so we grab its reference via createElementIntercept the first time it renders.
-// The row it builds is [DisplayName, [UserTagAndPronouns, GuildTag, ProfileBadgeRows]] - none of
-// those read custom text/color (UserTagAndPronouns is just the @handle, GuildTag is Discord's own
-// server-tag feature), so we push our GradientTag in as a sibling instead of trying to reuse one.
+// The full profile renders via PrimaryInfo -> UserProfilePrimaryInfo, which builds the name row
+// but isn't a top-level export - grabbed via createElementIntercept on first render instead, and
+// our GradientTag pushed in as a sibling of the row's existing children.
export default function patchProfile(): () => void {
const cleanups: (() => void)[] = [];
patchCreateElement(cleanups);
diff --git a/plugins/staff-tags/src/patches/tag.tsx b/plugins/staff-tags/src/patches/tag.tsx
index 13f14b0..3dc1b84 100644
--- a/plugins/staff-tags/src/patches/tag.tsx
+++ b/plugins/staff-tags/src/patches/tag.tsx
@@ -7,12 +7,8 @@ import Icon from "../ui/Icon";
const Tag = findByProps("getBotLabel");
const { View, Text } = ReactNative;
-/**
- * Discord's own Tag component ignores custom text/color props unless we push them into the
- * already-rendered tree ourselves - this is what actually makes our overrides visible. Now also
- * supports an `icon` prop (an IconDef from our icon library) and `iconColor` so tags can render
- * with or without text.
- */
+// Discord's Tag component ignores custom text/color props unless pushed into the already-rendered
+// tree directly.
export default () => {
if (!Tag) return () => {};
diff --git a/plugins/typing-avatars/src/components/AvatarStack.tsx b/plugins/typing-avatars/src/components/AvatarStack.tsx
index 443fe82..0b6d64d 100644
--- a/plugins/typing-avatars/src/components/AvatarStack.tsx
+++ b/plugins/typing-avatars/src/components/AvatarStack.tsx
@@ -5,9 +5,8 @@ import { getTypingAvatarURL } from "../lib/avatarUrl";
const SIZE = 20;
const OVERLAP = 8;
-// No text label and no tap handler by design - purely decorative avatars replacing the
-// "X is typing..." text entirely. If a lot of people are typing, this wraps to a second
-// overlapping row rather than clipping or capping with a "+N" badge.
+// Purely decorative, replacing the "X is typing..." text entirely - wraps to a second row instead
+// of capping with a "+N" badge.
export default function AvatarStack({ typingUserIds, guildId }: { typingUserIds: string[]; guildId: string | undefined }) {
if (!typingUserIds?.length) return null;
diff --git a/plugins/typing-avatars/src/lib/avatarUrl.ts b/plugins/typing-avatars/src/lib/avatarUrl.ts
index c930705..0200a90 100644
--- a/plugins/typing-avatars/src/lib/avatarUrl.ts
+++ b/plugins/typing-avatars/src/lib/avatarUrl.ts
@@ -7,15 +7,9 @@ function avatarExtension(hash: string): string {
return hash.startsWith("a_") ? "gif" : "png";
}
-// member.avatar / user.avatar confirmed live via devtools eval: UserStore.getCurrentUser().avatar
-// returned a real hash, and GuildMemberStore.getMember(guildId, userId).avatar exists as a field
-// and is correctly null for a user with no server-specific avatar set (the fallback-to-global
-// path is real and was exercised, not just theorized).
-//
-// The whole body is wrapped so a single bad input (e.g. a malformed userId reaching BigInt())
-// degrades to "this one avatar doesn't render" instead of throwing during the message list's
-// render pass - that render happens outside typingIndicator.tsx's own try/catch, which only
-// guards element creation, not AvatarStack's actual render.
+// Wrapped so a bad input (e.g. a malformed userId reaching BigInt()) degrades to "this one avatar
+// doesn't render" instead of throwing during the message list's render pass, which happens outside
+// typingIndicator.tsx's own try/catch.
export function getTypingAvatarURL(guildId: string | undefined, userId: string, size = 32): string | null {
try {
if (!userId) return null;
diff --git a/plugins/typing-avatars/src/patches/typingIndicator.tsx b/plugins/typing-avatars/src/patches/typingIndicator.tsx
index 799eb9f..e4646ac 100644
--- a/plugins/typing-avatars/src/patches/typingIndicator.tsx
+++ b/plugins/typing-avatars/src/patches/typingIndicator.tsx
@@ -3,11 +3,8 @@ import { findInReactTree } from "@vendetta/utils";
import { registerTypeDetector, registerIntercept, patchCreateElement } from "@shared/lib/createElementIntercept";
import AvatarStack from "../components/AvatarStack";
-// Mirrors role-color-everywhere/src/patches/typingWrapper.ts for the detection/wrapping part
-// (already proven live in this repo), but replaces the label's content with AvatarStack instead
-// of recoloring the existing per-user text nodes. This only needs props.typingUserIds directly,
-// so unlike the color-matching version it isn't affected by the "several people are typing..."
-// collapsed-string case.
+// Mirrors role-color-everywhere's typingWrapper.ts for detection/wrapping, but replaces the
+// label's content with AvatarStack instead of recoloring the existing text nodes.
export default function patchTypingIndicator(): () => void {
const cleanups: (() => void)[] = [];
patchCreateElement(cleanups);
diff --git a/plugins/view-raw/src/RawPage.tsx b/plugins/view-raw/src/RawPage.tsx
index c3bed8e..0595797 100644
--- a/plugins/view-raw/src/RawPage.tsx
+++ b/plugins/view-raw/src/RawPage.tsx
@@ -11,9 +11,6 @@ const { View, ScrollView, TextInput, Text } = ReactNative;
export default function RawPage({ message }: { message: any }) {
const [query, setQuery] = React.useState("");
const stringMessage = React.useMemo(() => JSON.stringify(cleanMessage(message), null, 4), [message.id]);
- // The search box previously hardcoded white text (illegible on a light theme) and the two
- // section labels below had no color at all (illegible black-on-black on a dark theme, RN's
- // Text default with no theming of its own).
const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
return (
diff --git a/plugins/view-raw/src/index.tsx b/plugins/view-raw/src/index.tsx
index 39503b6..479086b 100644
--- a/plugins/view-raw/src/index.tsx
+++ b/plugins/view-raw/src/index.tsx
@@ -1,18 +1,5 @@
// Forked from bwlok/revenge-plugins (plugins/ViewRaw), GPLv3. Original authors: sapphire, Vendicated, Bwlok.
// https://github.com/bwlok/revenge-plugins/tree/master/plugins/ViewRaw
-//
-// Changes in this fork:
-// - Fixed a crash on every message long-press ("Cannot read property 'type' of undefined") caused
-// by assuming Discord's ActionSheetRow icon shape without checking it first.
-// - RawPage rebuilt with a search box, syntax-highlighted JSON, and clearer copy actions.
-// - Action-sheet insertion rewritten using the technique from fshinz/Revenge-Plugins'
-// MoveForwardButton/FastCopyUserID: build the button from the real ActionSheetRow component
-// (via findByProps("ActionSheetRow"), with its own .Icon/.Group) instead of fabricating a fake
-// icon element by copying $$typeof/type off a sibling - and fall back to creating a brand new
-// group if no existing group can be found, rather than giving up.
-// - Guards against re-patching the same action sheet instance twice (Discord appears to reuse the
-// same lazy-loaded instance across opens), and against a missing Navigator lookup.
-// - Diagnostics (visible in Settings) record which strategy matched, or why none did.
import { before, after } from "@vendetta/patcher";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { findInReactTree } from "@vendetta/utils";
@@ -83,9 +70,8 @@ const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, ms
if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
component.then((instance: any) => {
- // Discord appears to reuse the same lazy-loaded instance across every action sheet open, so
- // this only patches it once and tracks the active message on the instance itself - patching on
- // every open would stack duplicate buttons (and duplicate patches) after enough long-presses.
+ // Discord reuses the same lazy-loaded instance across opens - patch once, track the active
+ // message on the instance itself.
instance.__viewRawActiveMessage = msg.message;
if (instance.__viewRawPatched) return;
instance.__viewRawPatched = true;
@@ -100,7 +86,7 @@ const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, ms
const navigator = buildNavigator(() => instance.__viewRawActiveMessage);
- // Strategy 1: a plain list of buttons (older/simpler action sheet layouts).
+ // Strategy 1: a plain list of buttons (older/simpler layouts).
const buttons = findInReactTree(component, (x) => x?.[0]?.type?.name === "ButtonRow");
if (buttons) {
buttons.push(formRowButton(navigator));
@@ -108,9 +94,7 @@ const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, ms
return;
}
- // Strategy 2: real ActionSheetRow groups. Search every group for one that already has
- // ActionSheetRow-shaped children, insert there - and if none match, create a brand new
- // group at the top rather than giving up (this is the actual difference from before).
+ // Strategy 2: real ActionSheetRow groups, falling back to a new group if none match.
const groups = findInReactTree(
component,
(x) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup",
@@ -143,9 +127,7 @@ const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, ms
}
}
- // Strategy 3: name-independent fallback. Look for any array where every element already
- // looks like an action sheet row (has both a label and an onPress) - a shape signature
- // rather than a name, so it survives renames that break strategies 1 and 2.
+ // Strategy 3: name-independent fallback matching on shape (label + onPress) instead.
const genericRowGroup = findInReactTree(
component,
(x) =>
diff --git a/plugins/you-bar-plus/src/index.ts b/plugins/you-bar-plus/src/index.ts
index b57b7a3..9336b6c 100644
--- a/plugins/you-bar-plus/src/index.ts
+++ b/plugins/you-bar-plus/src/index.ts
@@ -14,9 +14,8 @@ function stopRetrying() {
}
}
-// YouBarNotificationsButton isn't registered in Metro until the YouBar itself has rendered at
-// least once, which hasn't necessarily happened yet by the time a plugin's onLoad runs - a single
-// attempt just misses it some of the time. Retries on an interval until it lands, then stops.
+// YouBarNotificationsButton isn't registered until YouBar itself has rendered at least once -
+// retries on an interval until it lands.
function attempt() {
try {
const unpatch = patchYouBarButtons();
diff --git a/plugins/you-bar-plus/src/patches/youBarButtons.tsx b/plugins/you-bar-plus/src/patches/youBarButtons.tsx
index 76103fc..5800082 100644
--- a/plugins/you-bar-plus/src/patches/youBarButtons.tsx
+++ b/plugins/you-bar-plus/src/patches/youBarButtons.tsx
@@ -4,9 +4,8 @@ import { instead } from "@vendetta/patcher";
import { storage } from "@vendetta/plugin";
import { getAssetIDByName } from "@vendetta/ui/assets";
-// Keep the actual patch logic here dead simple - a previous attempt to fix the load-order race
-// by adding retry/re-render logic directly into this function made buttons stop appearing at all.
-// The retry now lives one level up in index.ts instead, calling this repeatedly until it succeeds.
+// Retry logic lives one level up in index.ts, calling this repeatedly until it succeeds - keep
+// this function itself simple.
export let updateYouBar = () => {};
/** Returns null (not undefined) when YouBarNotificationsButton isn't registered in Metro yet, so a caller can tell "not ready" apart from "patched, here's the unpatch fn". */
diff --git a/shared/lib/color.ts b/shared/lib/color.ts
index b1b6600..3dab823 100644
--- a/shared/lib/color.ts
+++ b/shared/lib/color.ts
@@ -5,9 +5,7 @@ export const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
const ThemeStore = findByStoreName("ThemeStore");
-// @vendetta/ui exposes the semanticColors token map but not a resolver function for it - every
-// plugin that needs one has to find it the same way, so it lives here instead of being
-// reimplemented per plugin.
+// @vendetta/ui exposes the semanticColors token map but not a resolver function for it.
const rawResolveSemanticColor: (theme: any, semanticColor: any) => string | undefined =
find((m: any) => m?.default?.internal?.resolveSemanticColor)?.default?.internal?.resolveSemanticColor ??
find((m: any) => m?.meta?.resolveSemanticColor)?.meta?.resolveSemanticColor ??
@@ -23,11 +21,8 @@ export function resolveSemanticColor(token: any, theme: any = ThemeStore?.theme)
}
}
-/**
- * Same as resolveSemanticColor, but tries a list of token names in order and falls back to a
- * hardcoded hex color if every one of them is missing or fails to resolve - Discord renames these
- * tokens across versions often enough that a single hardcoded name isn't reliable on its own.
- */
+// Tries a list of token names in order, falling back to a hardcoded hex color - Discord renames
+// these tokens across versions often enough that a single hardcoded name isn't reliable.
export function resolveSemanticColorSafe(tokenNames: string[], fallbackHex: string, theme: any = ThemeStore?.theme): string {
for (const name of tokenNames) {
const token = (semanticColors as Record | undefined)?.[name];
diff --git a/shared/lib/createElementIntercept.ts b/shared/lib/createElementIntercept.ts
index 7254ab5..34e96ad 100644
--- a/shared/lib/createElementIntercept.ts
+++ b/shared/lib/createElementIntercept.ts
@@ -39,30 +39,20 @@ export function registerIntercept(
intercepts.set(original, { replacement, extraProps });
}
-// Same idea as registerIntercept, but matches on a predicate over the element's props instead of
-// an exact type reference - useful when the component has no reliable name to find it by, but
-// always passes some distinguishing prop. Pass replacement: null to render nothing instead.
+// Matches on a props predicate instead of an exact type reference, for components with no
+// reliable name. Pass replacement: null to render nothing.
export function registerPropsIntercept(predicate: (props: any) => boolean, replacement: React.ComponentType | null) {
propsIntercepts.push({ predicate, replacement });
}
-// Rewrites an element's props in place, keeping the same type - unlike registerPropsIntercept,
-// this doesn't swap in a different component, it just lets the real one render with different
-// input. Useful where patching a hook's exported property wouldn't work (same-chunk closures
-// often bypass the export entirely); every element creation goes through this one shared path
-// regardless of which chunk it came from, so there's no equivalent staleness problem here.
+// Rewrites props in place, keeping the same type.
export function registerPropsTransform(predicate: (props: any) => boolean, transform: (props: any) => any) {
propsTransforms.push({ predicate, transform });
}
-// Purely observational: the first time an element is created whose type matches predicate, calls
-// onDetected(type) with the live reference. Doesn't touch rendering itself - pair it with
-// registerIntercept/registerPropsIntercept in the callback for that. This is how you find a
-// component that isn't a top-level module export and can't be searched for after the fact.
-//
-// key dedupes registrations (matters if the call site can run more than once, e.g. a retry loop).
-// persistent keeps the detector alive for every future match instead of firing once and dropping
-// - needed for components whose reference isn't stable for the whole session.
+// Fires onDetected(type) the first time a matching element is created. For components with no
+// stable module export to search for after the fact. persistent keeps firing on every match
+// instead of once, for references that change across navigation.
export function registerTypeDetector(
key: string,
predicate: (type: any) => boolean,
@@ -87,14 +77,14 @@ function runTypeDetectors(type: any) {
try {
matched = detector.predicate(type);
} catch {
- // A bad predicate shouldn't block every other detector or every element from rendering.
+ // Ignore
}
if (!matched) continue;
try {
detector.onDetected(type);
} catch {
- // Same - a detector's own handler throwing shouldn't take anything else down.
+ // Ignore
}
if (!detector.persistent) {
detector.justFired = true;
@@ -102,9 +92,6 @@ function runTypeDetectors(type: any) {
}
}
- // Only rebuild the list when a one-shot detector actually fired - rebuilding on every single
- // element creation app-wide would be an allocation per element on the hottest path in the app
- // whenever any persistent detector is registered (the list is then never empty).
if (consumed) {
typeDetectors = typeDetectors.filter((d) => d.persistent || !d.justFired);
}
@@ -122,7 +109,7 @@ function resolveReplacement(type: any, props: any, rest: any[]): { type: any; pr
effectiveProps = transform(effectiveProps);
}
} catch {
- // A bad transform shouldn't block every other element from rendering.
+ // Ignore
}
}
}
@@ -134,7 +121,7 @@ function resolveReplacement(type: any, props: any, rest: any[]): { type: any; pr
return replacement ? { type: replacement, props: effectiveProps } : null;
}
} catch {
- // A bad predicate shouldn't block every other element from rendering.
+ // Ignore
}
}
}
@@ -151,12 +138,8 @@ function resolveReplacement(type: any, props: any, rest: any[]): { type: any; pr
return undefined;
}
-// Mutates the already-created element in place rather than substituting args before creation.
-// This has to go through @vendetta/patcher's after() instead of a raw property reassignment on
-// the jsx-runtime object - a plain reassignment (what this file and server-drawer's copy used to
-// do) just doesn't reach real render calls here, after() does. Since after() only sees the return
-// value, not the original call, we mutate res.type/res.props after the fact instead of swapping
-// the type beforehand - same result, React just reads whatever's on the object it gets handed.
+// after() only sees the return value, not the call itself, so we mutate the element in place
+// rather than substituting args beforehand.
function applyResolved(res: any, type: any, props: any, rest: any[]) {
if (!res || typeof res !== "object") return res;
const resolved = resolveReplacement(type, props, rest);
@@ -183,11 +166,8 @@ export function patchCreateElement(cleanups: (() => void)[]) {
console.warn("[createElementIntercept] React.createElement is null, skipping patch");
}
- // Discord's bundle compiles JSX through the automatic runtime (jsx/jsxs/jsxDEV) instead of
- // React.createElement, so patching createElement alone misses nearly everything. We scan for
- // the runtime by shape rather than name since names get mangled, patch every copy we find (a
- // bundle can have more than one), and keep scanning on an interval since Metro registers
- // modules lazily - a copy that hasn't been required yet at boot just isn't there to find.
+ // Discord compiles JSX through jsx/jsxs/jsxDEV, not React.createElement - scan for those
+ // runtimes by shape (names are mangled) and keep scanning since Metro registers lazily.
const patchedJsxRuntimes = new WeakSet();
function isJsxRuntime(m: any): boolean {
@@ -215,7 +195,7 @@ export function patchCreateElement(cleanups: (() => void)[]) {
const dflt = exports.default;
if (dflt != null && isJsxRuntime(dflt)) patchJsxObject(dflt);
} catch {
- // Ignore one module's bad shape and continue scanning.
+ // Ignore
}
}
@@ -229,12 +209,9 @@ export function patchCreateElement(cleanups: (() => void)[]) {
}
}
- // Patch any jsx runtimes already loaded before Discord's first render pass starts, synchronously
- // - an async scan would complete too late for the very first render of an early-mounting target.
scanAndPatchJsxRuntimes();
- // Fast while the app is still booting and chunks are being pulled in, then slow, then stop - a
- // full window.modules walk is not free and there's nothing left to catch once the UI has settled.
+ // Fast while the app boots, then slow, then stop.
let ticks = 0;
let timer: ReturnType | undefined = setInterval(() => {
scanAndPatchJsxRuntimes();
diff --git a/shared/lib/lazy.ts b/shared/lib/lazy.ts
index e0e4889..e6b903c 100644
--- a/shared/lib/lazy.ts
+++ b/shared/lib/lazy.ts
@@ -1,13 +1,7 @@
-/**
- * Wraps a metro lookup so it isn't run until first actually used, and retries on every call until
- * it succeeds once (after which the result is cached forever). A plugin's top-level module code
- * runs as soon as its bundle is required, which can be before Discord's own code has required an
- * internal module it depends on - a one-shot findByX() at module scope can permanently cache
- * undefined even though the real module becomes available moments later. Only meant for values
- * used inside event handlers or plain (non-hook) render logic - wrapping something used as a
- * React hook (e.g. passed to useContext) in this can violate the Rules of Hooks, since the
- * resolved-or-not status is now allowed to change between renders of the same component instance.
- */
+// Wraps a metro lookup so it isn't run until first used, retrying on every call until it succeeds
+// once (then cached forever). Only for values used in event handlers or plain render logic -
+// wrapping something used as a React hook can violate the Rules of Hooks, since resolved-or-not
+// can change between renders.
export function lazy(resolve: () => T | null | undefined): () => T | undefined {
let cached: T | undefined;
let resolved = false;
diff --git a/shared/lib/patchRows.ts b/shared/lib/patchRows.ts
index d6afd6e..1787606 100644
--- a/shared/lib/patchRows.ts
+++ b/shared/lib/patchRows.ts
@@ -4,12 +4,9 @@ import { after, before } from "@vendetta/patcher";
export type RowHandler = (rows: any[]) => void;
-/**
- * Chat rows are rendered natively - message data crosses the JS/native bridge as JSON via
- * DCDChatManager.updateRows on newer builds, or via RowManager.generate (one row per call) on
- * older ones. This runs `handler` against the row array either way, so callers don't have to
- * reimplement the bridge-vs-RowManager fallback themselves.
- */
+// Chat rows are rendered natively - message data crosses the JS/native bridge as JSON via
+// DCDChatManager.updateRows on newer builds, or RowManager.generate on older ones. Runs `handler`
+// against the row array either way.
export function patchRows(handler: RowHandler): () => void {
const { NativeModules } = ReactNative;
const DCDChatManager = NativeModules?.DCDChatManager;
diff --git a/shared/lib/patcher.ts b/shared/lib/patcher.ts
index c94c54d..35fcf86 100644
--- a/shared/lib/patcher.ts
+++ b/shared/lib/patcher.ts
@@ -2,11 +2,8 @@ export interface PluginLogger {
error(message: string): void;
}
-/**
- * Wraps a patch-application function so a single broken lookup only disables that one surface
- * instead of crashing the whole plugin's onLoad and taking every other patch down with it -
- * this is the root-cause fix for why Staff Tags and RoleColorEverywhere used to break completely.
- */
+// Wraps a patch-application function so a single broken lookup only disables that one surface,
+// instead of crashing the whole plugin's onLoad and taking every other patch down with it.
export function safePatch(pluginName: string, patchName: string, apply: () => () => void, logger: PluginLogger): () => void {
try {
return apply();
diff --git a/shared/lib/rawFind.ts b/shared/lib/rawFind.ts
index c84c9a9..5fcd6e2 100644
--- a/shared/lib/rawFind.ts
+++ b/shared/lib/rawFind.ts
@@ -1,21 +1,6 @@
-// Revenge's own metro finders (findByProps/findByName/findByTypeName/findByStoreName, and even
-// the bare `find`) cache their result per search - including a NEGATIVE result. Once a search
-// comes up empty, it's marked "not found" and every later call with the same search never
-// rescans window.modules again, even after the real module registers (confirmed by reading
-// Revenge's own source: metro/internals/modules.ts's getModules() returns early on a cached
-// NOT_FOUND flag). That makes retry loops pointless for any search whose first attempt happens
-// to run before the target is registered - every retry after that first miss hits the same
-// poisoned cache instead of actually looking again.
-//
-// This walks window.modules directly to bypass that cache - but critically, it never forces a
-// module to initialize (never calls window.__r on something that hasn't run yet). An earlier
-// version of this file did force-require everything on every scan, which is genuinely dangerous:
-// Metro only ever runs a module's factory once and caches whatever it produced forever, so
-// force-evaluating a module before whatever it depends on on is actually ready can permanently
-// wedge it into a broken state for the rest of the session - no amount of retrying fixes that,
-// since the factory never runs again. This only inspects modules Metro's own isInitialized flag
-// says have ALREADY been required by something else, which is entirely passive and safe to poll
-// repeatedly - it just waits for Discord's own code to naturally reach the module we want.
+// Revenge's own finders cache a negative result forever, so retry loops never see a module that
+// registers after the first miss. This walks window.modules directly instead, and only inspects
+// modules already marked isInitialized - never force-requires one, which can wedge Metro.
declare const window: any;
export function rawFind(predicate: (exports: any) => boolean): T | undefined {
diff --git a/shared/lib/waitFor.ts b/shared/lib/waitFor.ts
index 37d78ae..c156537 100644
--- a/shared/lib/waitFor.ts
+++ b/shared/lib/waitFor.ts
@@ -3,19 +3,10 @@ export interface WaitForHandle {
cancel(): void;
}
-/**
- * Polls `find` every `intervalMs` (default 100) until it returns a truthy value, then calls
- * `onFound` with it and stops polling.
- *
- * Needed because a plugin's onLoad can run before every Metro module/component it depends on has
- * actually been required - on a cold app start that's a real race, not an edge case. A one-shot
- * `findByX(...)` lookup at that exact moment can come back undefined and, without a retry, never
- * try again for the rest of the session. `waitFor` checks immediately (so there's no added delay
- * once the target is already registered) and keeps retrying otherwise.
- *
- * Returns a handle whose `cancel()` stops the poll - call it from a plugin's unpatch/onUnload so a
- * pending lookup doesn't keep firing (or apply a patch) after the plugin's been turned off.
- */
+// Polls `find` every `intervalMs` until it returns a truthy value, then calls `onFound` and stops.
+// Checks immediately first, so there's no added delay if the target is already registered. Call
+// the returned handle's cancel() from onUnload so a pending lookup can't fire after the plugin's
+// turned off.
export function waitFor(find: () => T | null | undefined, onFound: (value: T) => void, intervalMs = 100): WaitForHandle {
let cancelled = false;
let timer: ReturnType | undefined;
diff --git a/shared/ui/ColorInput.tsx b/shared/ui/ColorInput.tsx
index b1c7e3e..c85ab5d 100644
--- a/shared/ui/ColorInput.tsx
+++ b/shared/ui/ColorInput.tsx
@@ -16,16 +16,12 @@ interface ColorInputProps {
onChange: (value: string) => void;
}
-/**
- * Hex color field with a live swatch preview and tap-to-pick presets. Built entirely from RN
- * primitives (no Discord-internal component reuse), so it doesn't carry the same "might silently
- * stop rendering after a Discord update" risk as the plugins' UI patches do.
- */
+// Hex color field with a live swatch preview and tap-to-pick presets. Built entirely from RN
+// primitives, so it doesn't carry the same "might silently stop rendering after a Discord update"
+// risk as the plugins' UI patches do.
export default function ColorInput({ title, value, placeholder, onChange }: ColorInputProps) {
const valid = !value || isValidHex(value);
const swatchColor = isValidHex(value) ? value : (isValidHex(placeholder) ? placeholder : "#5865F2");
- // Same missing-color bug as NoteBox - Text with no explicit color defaults to black, illegible
- // on Discord's dark theme.
const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
return (
diff --git a/shared/ui/ListSection.tsx b/shared/ui/ListSection.tsx
index 8c4a11c..330b79f 100644
--- a/shared/ui/ListSection.tsx
+++ b/shared/ui/ListSection.tsx
@@ -11,11 +11,9 @@ export interface ListItem {
onPress?: () => void;
}
-/**
- * A titled section listing tap-able rows, with a friendly note shown in place of an empty list.
- * Always pairs onPress with a visible trailing indicator - rows without one have been unreliable
- * about actually registering taps in testing.
- */
+// A titled section listing tap-able rows, with a friendly note shown in place of an empty list.
+// Always pairs onPress with a visible trailing indicator - rows without one have been unreliable
+// about registering taps.
export default function ListSection({ title, items, emptyText }: { title: string; items: ListItem[]; emptyText: string }) {
return (
diff --git a/shared/ui/NoteBox.tsx b/shared/ui/NoteBox.tsx
index 349c926..cd2b59f 100644
--- a/shared/ui/NoteBox.tsx
+++ b/shared/ui/NoteBox.tsx
@@ -3,12 +3,7 @@ import { resolveSemanticColorSafe } from "../lib/color";
const { View, Text } = ReactNative;
-/**
- * Small bordered callout for explanatory text in a settings screen. Text color used to be
- * unset entirely - React Native's Text defaults to black with no explicit color, which is
- * illegible on Discord's dark theme (the default for most users). Explicit theme-aware color now,
- * same resolveSemanticColorSafe fallback-chain pattern used everywhere else in this repo.
- */
+/** Small bordered callout for explanatory text in a settings screen. */
export default function NoteBox({ children, style }: { children: any; style?: any }) {
const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT", "TEXT_MUTED"], "#96989d");
diff --git a/shared/ui/table.ts b/shared/ui/table.ts
index ea61e75..f5f3305 100644
--- a/shared/ui/table.ts
+++ b/shared/ui/table.ts
@@ -1,17 +1,10 @@
import { findByProps } from "@vendetta/metro";
import { Forms } from "@vendetta/ui/components";
-/**
- * Discord's redesigned "Table" row family (TableRow/TableRowGroup/TableSwitchRow/...) replaced
- * the old Forms.Form* components in the app itself a while ago - Forms.* still works (Vendetta's
- * compat layer maps it to the legacy components, see metro/common/components.ts in the loader),
- * but it's the deprecated skin. These are found the same way ViewRaw finds ActionSheetRow: a
- * direct findByProps lookup for the real component, since the vendetta.ui.components compat
- * object never exposed the Table family at all. Falls back to the matching legacy Form
- * component if a lookup ever comes back empty, same "isolate the failure" approach as the rest
- * of this repo - the fallback's prop names don't perfectly match (e.g. `icon` vs `leading`), so a
- * few things would render without an icon/arrow in that fallback path, but nothing crashes.
- */
+// Discord's redesigned "Table" row family replaced Forms.Form* in the app itself, but the
+// vendetta.ui.components compat object never exposed it - found via direct findByProps instead,
+// falling back to the legacy Form component (whose prop names don't perfectly match) if a lookup
+// ever comes back empty.
const find = (prop: string): any => findByProps(prop)?.[prop];
const RealTableRow: any = find("TableRow");