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.
This commit is contained in:
Ally Founder 2026-08-08 20:56:46 -04:00
parent e999719b21
commit 0439a6a67b
4 changed files with 61 additions and 12 deletions

View file

@ -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: <memo wrapper>, ...}`, 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,
)

View file

@ -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: <memo wrapper>}` - `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<StaffTagsStorage>) {
const patches: (() => void)[] = []
@ -63,9 +70,10 @@ export default function patchDetails(storage: JsonStorage<StaffTagsStorage>) {
}
const unsubUserRow = getModules(
withName<FC<UserRowProps>>('UserRow'),
UserRow => {
withMemoDefaultName('UserRow'),
mod => {
guard(() => {
const UserRow = (mod as { default: FC<UserRowProps> }).default
patches.push(
afterJSX(UserRow, el =>
guard(() => {

View file

@ -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<StaffTagsStorage>) {
const cleanups: (() => void)[] = []
@ -69,11 +82,11 @@ export default function patchName(storage: JsonStorage<StaffTagsStorage>) {
}
const unsubHeaderName = getModules(
withName<FC<HeaderNameProps>>('HeaderName'),
HeaderName => {
withName<FC<HeaderNameProps>>('ChannelHeader'),
ChannelHeader => {
guard(() => {
cleanups.push(
afterJSX(HeaderName, el =>
afterJSX(ChannelHeader, el =>
guard(() => {
const { channelId } = el.props
const unpatch = afterRendered(el, ret =>

View file

@ -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"