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");