From 0439a6a67bf179bcc5c76769401a3ec2c1ad8429 Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Sat, 8 Aug 2026 20:56:46 -0400 Subject: [PATCH] staff-tags: fix UserRow/HeaderName lookups, bump to 1.0.5 Two of the three previously-broken module lookups now have well-evidenced fixes (from live sweeps run during an earlier debugging session that were never actually applied to the code): - details.tsx: UserRow is React.memo()-wrapped, so a plain withName('UserRow') can never match it (memo wrapper objects have no .name). Added withMemoDefaultName (lib/filters.ts), a custom filter checking the wrapper's inner .type.name instead, matching on the full module namespace so the callback gets the wrapper itself - the exact reference afterJSX needs. - name.tsx: HeaderName doesn't exist under that name anymore; ChannelHeader is the confirmed replacement (a plain, non-memo function, found via a name-sweep). DisplayName (the message-author-name row) remains unresolved - never found under any name across two live sweep sessions, likely inlined into a bigger component now. Left as a documented, still-guarded no-op rather than guessed at. --- plugins/staff-tags/js/lib/filters.ts | 28 +++++++++++++++++++++++ plugins/staff-tags/js/patches/details.tsx | 18 +++++++++++---- plugins/staff-tags/js/patches/name.tsx | 25 +++++++++++++++----- plugins/staff-tags/manifest.json | 2 +- 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 plugins/staff-tags/js/lib/filters.ts diff --git a/plugins/staff-tags/js/lib/filters.ts b/plugins/staff-tags/js/lib/filters.ts new file mode 100644 index 0000000..b4fea85 --- /dev/null +++ b/plugins/staff-tags/js/lib/filters.ts @@ -0,0 +1,28 @@ +import { + createFilterGenerator, + FilterFlag, + FilterScopes, +} from '@revenge-mod/modules/finders/filters' + +/** + * `withName` checks `exports.name`/`exports.default.name` - a plain function/class's own + * runtime name. A `React.memo()`-wrapped component is a `{$$typeof, type, compare}` object with + * no `.name` of its own (the real name lives on the hidden inner `.type`), so `withName` can + * structurally never match one - confirmed live for `UserRow` via a memo-patching sweep + * (`next-eval-checks`' `memoSweep`), and confirmed from `withName`'s own source + * (`@revenge-mod/modules/finders/filters`): it only ever checks `exports.name` and + * `exports.default.name`, never `exports.default.type.name`. + * + * This filter checks the memo wrapper's inner name instead, and matches on the full module + * namespace (so the callback receives `{default: , ...}`, not the unwrapped + * default) - the wrapper is what `afterJSX` needs, since that's the exact reference Discord's own + * `createElement(UserRow, ...)` calls use as `type`, not the inner render function memo hides. + */ +export const withMemoDefaultName = createFilterGenerator<[name: string]>( + ([name], _id, exports) => + (exports as { default?: { type?: { name?: string } } })?.default?.type + ?.name === name, + ([name]) => `revenge-next-plugs.staff-tags.memoDefaultName(${name})`, + FilterFlag.RequiresExports, + FilterScopes.Initialized, +) diff --git a/plugins/staff-tags/js/patches/details.tsx b/plugins/staff-tags/js/patches/details.tsx index 4aa2c81..4595e39 100644 --- a/plugins/staff-tags/js/patches/details.tsx +++ b/plugins/staff-tags/js/patches/details.tsx @@ -1,10 +1,11 @@ import { Stores } from '@revenge-mod/discord/flux' import { getModules } from '@revenge-mod/modules/finders' -import { withName, withProps } from '@revenge-mod/modules/finders/filters' +import { withProps } from '@revenge-mod/modules/finders/filters' import { afterJSX } from '@revenge-mod/react/jsx-runtime' import { findInReactFiber } from '@revenge-mod/utils/react' import { afterRendered } from '../lib/afterRendered' import { fiberFilter } from '../lib/fiber' +import { withMemoDefaultName } from '../lib/filters' import getTag from '../lib/getTag' import { guard } from '../lib/safe' import GradientTag from '../ui/GradientTag' @@ -32,6 +33,13 @@ interface TagComponentProps { * profile popout, etc.), so this subscribes rather than doing a single lookup - `max` is a guess * and may need adjusting once verified live, see eval-for-revenge. * + * `UserRow` is real but `React.memo()`-wrapped (confirmed live via a memo-patching sweep) - a + * plain `withName('UserRow')` can never match it, since the wrapper object has no `.name` of its + * own (only the memo-hidden inner function does). `withMemoDefaultName` (`../lib/filters`) checks + * the wrapper's inner `.type.name` instead and matches on the full module namespace, so the + * callback below receives `{default: }` - `mod.default` is the exact reference + * Discord's own `createElement(UserRow, ...)` calls use, which is what `afterJSX` needs. + * * Uses `getModules` (reacts once Discord itself initializes each module) instead of * `lookupModule` (which force-initializes uninitialized modules right away) - forcing UserRow / * the Tag module to init during `start()`, before Discord is done booting, is what crashed app @@ -42,8 +50,7 @@ interface TagComponentProps { * synchronous setup calls - an uncaught throw here crashes app startup instead of just skipping * one row. * - * TODO(live-verify): UserRow component name/count, rendered-tree shape (`c?.type?.Types`), see - * eval-for-revenge. + * TODO(live-verify): rendered-tree shape (`c?.type?.Types`) still unconfirmed, see eval-for-revenge. */ export default function patchDetails(storage: JsonStorage) { const patches: (() => void)[] = [] @@ -63,9 +70,10 @@ export default function patchDetails(storage: JsonStorage) { } const unsubUserRow = getModules( - withName>('UserRow'), - UserRow => { + withMemoDefaultName('UserRow'), + mod => { guard(() => { + const UserRow = (mod as { default: FC }).default patches.push( afterJSX(UserRow, el => guard(() => { diff --git a/plugins/staff-tags/js/patches/name.tsx b/plugins/staff-tags/js/patches/name.tsx index 1998de1..3aa70c4 100644 --- a/plugins/staff-tags/js/patches/name.tsx +++ b/plugins/staff-tags/js/patches/name.tsx @@ -35,9 +35,21 @@ interface TagComponentProps { * Covers the name row shown in chat messages (next to the display name + timestamp) and channel * headers. * + * `HeaderName` doesn't exist under that name in the current build (confirmed live - Discord + * renamed/refactored it at some point). `ChannelHeader` is the confirmed replacement: found via a + * broad name-sweep as a plain, non-memo-wrapped function, high confidence given its name and + * shape - a plain `withName` lookup can find it directly, unlike `UserRow` in `details.tsx` which + * needed a memo-aware filter. + * + * `DisplayName` (the message-author-name row) is still unresolved - not found under any name even + * via a memo/forwardRef sweep and a text-content correlation sweep across two separate live + * sessions. Most likely inlined into a bigger message-row component now rather than factored out + * the way Classic had it. Left as a best-effort lookup below (guarded, silently no-ops) rather + * than guessed at - fixing this needs a fresh live investigation, not a guess. + * * Uses `getModules` (reacts once Discord itself initializes each module) instead of * `lookupModule` (which force-initializes uninitialized modules right away) - forcing - * DisplayName/HeaderName/the Tag module to init during `start()`, before Discord is done + * DisplayName/ChannelHeader/the Tag module to init during `start()`, before Discord is done * booting, is what crashed app startup the first time this shipped. * * Every callback below is wrapped in `guard()`: it runs whenever Discord itself renders/ @@ -45,8 +57,9 @@ interface TagComponentProps { * synchronous setup calls - an uncaught throw here crashes app startup instead of just skipping * a name row. * - * TODO(live-verify): DisplayName/HeaderName component names and rendered-tree shapes - * (`c?.type?.Types`, the flexDirection: "row" container), see eval-for-revenge. + * TODO(live-verify): ChannelHeader's rendered-tree shape (`c?.type?.Types`, the + * flexDirection: "row" container) still unconfirmed, see eval-for-revenge. DisplayName remains + * fully unresolved. */ export default function patchName(storage: JsonStorage) { const cleanups: (() => void)[] = [] @@ -69,11 +82,11 @@ export default function patchName(storage: JsonStorage) { } const unsubHeaderName = getModules( - withName>('HeaderName'), - HeaderName => { + withName>('ChannelHeader'), + ChannelHeader => { guard(() => { cleanups.push( - afterJSX(HeaderName, el => + afterJSX(ChannelHeader, el => guard(() => { const { channelId } = el.props const unpatch = afterRendered(el, ret => diff --git a/plugins/staff-tags/manifest.json b/plugins/staff-tags/manifest.json index 56f4ea9..cc0e3dc 100644 --- a/plugins/staff-tags/manifest.json +++ b/plugins/staff-tags/manifest.json @@ -4,7 +4,7 @@ "name": "Staff Tags", "description": "Adds customizable tags (OWNER, ADMIN, STAFF, MOD, and more) next to members based on their server permissions. Supports per-tag colors, gradients, custom text, and visibility toggles.", "author": "everestmcarthur", - "version": "1.0.4", + "version": "1.0.5", "dependencies": { "revenge.api": { "version": ">=1 <2"