Staff Tags/Custom User Tags: show tags on the full profile screen too

This commit is contained in:
Ally Founder 2026-08-02 19:41:10 -04:00
parent 2b111e7d8c
commit 66d1777e44
4 changed files with 158 additions and 2 deletions

View file

@ -4,6 +4,7 @@ import { applyPatches } from "@shared/lib/patcher";
import patchChat from "./patches/chat"; import patchChat from "./patches/chat";
import patchName from "./patches/name"; import patchName from "./patches/name";
import patchDetails from "./patches/details"; import patchDetails from "./patches/details";
import patchProfile from "./patches/profile";
import patchTag from "./patches/tag"; import patchTag from "./patches/tag";
import Settings from "./ui/Settings"; import Settings from "./ui/Settings";
@ -17,7 +18,8 @@ export default {
tag: patchTag, tag: patchTag,
chat: patchChat, chat: patchChat,
name: patchName, name: patchName,
details: patchDetails details: patchDetails,
profile: patchProfile
}); });
}, },
onUnload: () => unpatchAll(), onUnload: () => unpatchAll(),

View file

@ -0,0 +1,70 @@
import { find, findByProps } from "@vendetta/metro";
import { after } from "@vendetta/patcher";
import { findInReactTree } from "@vendetta/utils";
import resolveTag from "../lib/resolveTag";
// Same top-level profile component PronounDB already confirmed works, and the same one Staff Tags
// now patches for its own profile-screen tag - see that plugin's patches/profile.tsx for the full
// writeup on why this is a separate component from UserRow (which details.tsx already handles).
const UserProfile =
find((m: any) => m?.type?.name === "UserProfileContent") ??
find((m: any) => m?.type?.name === "UserProfile");
const TagModule = findByProps("getBotLabel");
/**
* Not independently confirmed against a live render yet - the exact prop shape passed to this
* component isn't verified on-device, so this pulls from whichever of several plausible field names
* is actually present. Every step is guarded, so a wrong assumption just means no tag appears here.
*/
export default function patchProfile(): () => void {
if (!UserProfile) return () => {};
return after("type", UserProfile, (args: any[], res: any) => {
try {
const props = args?.[0] ?? {};
const userId: string | undefined = props.userId ?? props.user?.id ?? props.displayProfile?.userId;
if (!userId) return;
const tag = resolveTag(userId);
if (!tag) return;
const primaryInfo = findInReactTree(res, (c) => c?.type?.name === "PrimaryInfo" || c?.type?.displayName === "PrimaryInfo");
if (!primaryInfo) return;
const existingTag = findInReactTree(primaryInfo, (c) => c?.type?.Types);
if (existingTag && existingTag.props?.type !== 0) return;
if (existingTag) {
Object.assign(existingTag.props, {
type: 0,
text: tag.text,
textColor: tag.textColor,
backgroundColor: tag.backgroundColor,
verified: false
});
return;
}
const nameRow = findInReactTree(
primaryInfo,
(c) =>
Array.isArray(c?.props?.children) &&
c.props.children.some((ch: any) => typeof ch === "string" || typeof ch?.props?.children === "string")
);
if (!Array.isArray(nameRow?.props?.children) || !TagModule) return;
nameRow.props.children.push(
<TagModule.default
type={0}
text={tag.text}
textColor={tag.textColor}
backgroundColor={tag.backgroundColor}
verified={false}
/>
);
} catch {
// Skip - a broken profile tag beats a crashed profile screen.
}
});
}

View file

@ -4,6 +4,7 @@ import { applyPatches } from "@shared/lib/patcher";
import patchChat from "./patches/chat"; import patchChat from "./patches/chat";
import patchDetails from "./patches/details"; import patchDetails from "./patches/details";
import patchName from "./patches/name"; import patchName from "./patches/name";
import patchProfile from "./patches/profile";
import patchTag from "./patches/tag"; import patchTag from "./patches/tag";
import Settings from "./ui/pages/Settings"; import Settings from "./ui/pages/Settings";
@ -18,7 +19,8 @@ export default {
tag: patchTag, tag: patchTag,
chat: patchChat, chat: patchChat,
name: patchName, name: patchName,
details: patchDetails details: patchDetails,
profile: patchProfile
}); });
}, },
onUnload: () => unpatchAll(), onUnload: () => unpatchAll(),

View file

@ -0,0 +1,82 @@
import { find, findByProps, findByStoreName } from "@vendetta/metro";
import { after } from "@vendetta/patcher";
import { findInReactTree } from "@vendetta/utils";
import getTag from "../lib/getTag";
// Same top-level profile component PronounDB already confirmed works on current Discord (the
// findByName-based lookup this repo used to use for it stopped matching after a rename, but this
// predicate-based find still succeeds). The full profile screen doesn't render through UserRow the
// way member list rows and the old "profile popout" did - this is a separate component entirely,
// which is why tags weren't showing up here even though details.tsx's own patch is working fine.
const UserProfile =
find((m: any) => m?.type?.name === "UserProfileContent") ??
find((m: any) => m?.type?.name === "UserProfile");
const TagModule = findByProps("getBotLabel");
const GuildStore = findByStoreName("GuildStore");
const UserStore = findByStoreName("UserStore");
/**
* Not independently confirmed against a live render the way most of this repo's fixes are - the
* exact prop shape passed to this component's render (userId vs. a full user object, whether
* guildId is even present) isn't verified on-device yet, so this pulls from whichever of several
* plausible field names is actually present rather than assuming one. Every step is guarded, so a
* wrong assumption just means no tag appears here, not a crash.
*/
export default function patchProfile(): () => void {
if (!UserProfile) return () => {};
return after("type", UserProfile, (args: any[], res: any) => {
try {
const props = args?.[0] ?? {};
const userId: string | undefined = props.userId ?? props.user?.id ?? props.displayProfile?.userId;
if (!userId) return;
const user = props.user ?? UserStore?.getUser?.(userId);
if (!user) return;
const guildId: string | undefined = props.guildId;
const guild = guildId ? GuildStore?.getGuild(guildId) : undefined;
const tag = getTag(guild, undefined, user);
if (!tag) return;
const primaryInfo = findInReactTree(res, (c) => c?.type?.name === "PrimaryInfo" || c?.type?.displayName === "PrimaryInfo");
if (!primaryInfo) return;
const existingTag = findInReactTree(primaryInfo, (c) => c?.type?.Types);
if (existingTag && existingTag.props?.type !== 0) return;
if (existingTag) {
Object.assign(existingTag.props, {
type: 0,
text: tag.text,
textColor: tag.textColor,
backgroundColor: tag.backgroundColor,
verified: false
});
return;
}
const nameRow = findInReactTree(
primaryInfo,
(c) =>
Array.isArray(c?.props?.children) &&
c.props.children.some((ch: any) => typeof ch === "string" || typeof ch?.props?.children === "string")
);
if (!Array.isArray(nameRow?.props?.children) || !TagModule) return;
nameRow.props.children.push(
<TagModule.default
type={0}
text={tag.text}
textColor={tag.textColor}
backgroundColor={tag.backgroundColor}
verified={false}
/>
);
} catch {
// Skip - a broken profile tag beats a crashed profile screen.
}
});
}