From 66d1777e44601f5c1b09d059f8502dccd8b37326 Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Sun, 2 Aug 2026 19:41:10 -0400 Subject: [PATCH] Staff Tags/Custom User Tags: show tags on the full profile screen too --- plugins/custom-user-tags/src/index.ts | 4 +- .../custom-user-tags/src/patches/profile.tsx | 70 ++++++++++++++++ plugins/staff-tags/src/index.ts | 4 +- plugins/staff-tags/src/patches/profile.tsx | 82 +++++++++++++++++++ 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 plugins/custom-user-tags/src/patches/profile.tsx create mode 100644 plugins/staff-tags/src/patches/profile.tsx diff --git a/plugins/custom-user-tags/src/index.ts b/plugins/custom-user-tags/src/index.ts index 12bd60e..ddbd7c6 100644 --- a/plugins/custom-user-tags/src/index.ts +++ b/plugins/custom-user-tags/src/index.ts @@ -4,6 +4,7 @@ import { applyPatches } from "@shared/lib/patcher"; import patchChat from "./patches/chat"; import patchName from "./patches/name"; import patchDetails from "./patches/details"; +import patchProfile from "./patches/profile"; import patchTag from "./patches/tag"; import Settings from "./ui/Settings"; @@ -17,7 +18,8 @@ export default { tag: patchTag, chat: patchChat, name: patchName, - details: patchDetails + details: patchDetails, + profile: patchProfile }); }, onUnload: () => unpatchAll(), diff --git a/plugins/custom-user-tags/src/patches/profile.tsx b/plugins/custom-user-tags/src/patches/profile.tsx new file mode 100644 index 0000000..e0d3b99 --- /dev/null +++ b/plugins/custom-user-tags/src/patches/profile.tsx @@ -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( + + ); + } catch { + // Skip - a broken profile tag beats a crashed profile screen. + } + }); +} diff --git a/plugins/staff-tags/src/index.ts b/plugins/staff-tags/src/index.ts index 432b328..1473ccb 100644 --- a/plugins/staff-tags/src/index.ts +++ b/plugins/staff-tags/src/index.ts @@ -4,6 +4,7 @@ import { applyPatches } from "@shared/lib/patcher"; import patchChat from "./patches/chat"; import patchDetails from "./patches/details"; import patchName from "./patches/name"; +import patchProfile from "./patches/profile"; import patchTag from "./patches/tag"; import Settings from "./ui/pages/Settings"; @@ -18,7 +19,8 @@ export default { tag: patchTag, chat: patchChat, name: patchName, - details: patchDetails + details: patchDetails, + profile: patchProfile }); }, onUnload: () => unpatchAll(), diff --git a/plugins/staff-tags/src/patches/profile.tsx b/plugins/staff-tags/src/patches/profile.tsx new file mode 100644 index 0000000..b54c87e --- /dev/null +++ b/plugins/staff-tags/src/patches/profile.tsx @@ -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( + + ); + } catch { + // Skip - a broken profile tag beats a crashed profile screen. + } + }); +}