From 19cfaac5382ddbbb7dccc2838196fd12480abd60 Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Mon, 10 Aug 2026 09:53:55 -0400 Subject: [PATCH] CopyRoleColor: copy gradient roles as JSON, not just the flattened hex --- plugins/copy-role-color/src/index.ts | 35 ++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/plugins/copy-role-color/src/index.ts b/plugins/copy-role-color/src/index.ts index a4bc06b..3f2c27c 100644 --- a/plugins/copy-role-color/src/index.ts +++ b/plugins/copy-role-color/src/index.ts @@ -1,5 +1,5 @@ import { logger } from "@vendetta"; -import { findByName } from "@vendetta/metro"; +import { findByName, findByStoreName } from "@vendetta/metro"; import { after } from "@vendetta/patcher"; import { findInReactTree } from "@vendetta/utils"; import { clipboard } from "@vendetta/metro/common"; @@ -8,6 +8,17 @@ import { showToast } from "@vendetta/ui/toasts"; import { applyPatches } from "@shared/lib/patcher"; import { registerTypeDetector, registerIntercept, patchCreateElement } from "@shared/lib/createElementIntercept"; +// A gradient role's real colors live at role.colorStrings ({primaryColor, secondaryColor, +// tertiaryColor}), not the flattened colorString - copying that exact JSON back into Discord's own +// role color picker recreates the gradient. A solid-color role only has primaryColor set, so this +// falls back to the plain hex for those, keeping the existing single-color behavior unchanged. +function getClipboardValue(colorStrings: any, fallbackColor?: string | null): string | null { + if (colorStrings?.primaryColor && colorStrings?.secondaryColor) { + return JSON.stringify(colorStrings); + } + return colorStrings?.primaryColor ?? fallbackColor ?? 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 @@ -15,6 +26,8 @@ function patchRolePill(): () => void { const RolePillComponent = findByName("RolePill", false) ?? findByName("ThemedRolePill", false); if (!RolePillComponent) return () => {}; + const GuildRoleStore = findByStoreName("GuildRoleStore"); + return after("default", RolePillComponent, (_args: any[], res: any) => { try { if (!res?.props?.onPress) return; @@ -27,8 +40,16 @@ function patchRolePill(): () => void { const color = roleIcon?.props?.style?.[1]?.backgroundColor ?? verifiedIcon?.props?.roleColor; if (!color) return; + // The pill only gives us a flattened color from its rendered style - fetching the real + // role record (if guildId/roleId are present on it) is what actually exposes the gradient. + const guildId = res.props?.guildId; + const roleId = res.props?.roleId ?? res.props?.role?.id; + const role = guildId && roleId ? GuildRoleStore?.getRole?.(guildId, roleId) : null; + const value = getClipboardValue(role?.colorStrings, color); + if (!value) return; + res.props.onLongPress = () => { - clipboard.setString(color); + clipboard.setString(value); showToast("Copied role color to clipboard", getAssetIDByName("ic_message_copy")); }; } catch { @@ -40,8 +61,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 -// role.colorString already a hex string, so no need to guess at it from rendered styles. +// 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. function patchProfileRoleItem(): () => void { const cleanups: (() => void)[] = []; patchCreateElement(cleanups); @@ -54,12 +75,12 @@ function patchProfileRoleItem(): () => void { const ret = RoleItem(props); try { - const color = props?.role?.colorString; - if (color && ret && typeof ret === "object") { + const value = getClipboardValue(props?.role?.colorStrings, props?.role?.colorString); + if (value && ret && typeof ret === "object") { ret.props = { ...ret.props, onLongPress: () => { - clipboard.setString(color); + clipboard.setString(value); showToast("Copied role color to clipboard", getAssetIDByName("ic_message_copy")); } };