Add Staff Tags plugin: rebuilt for current Revenge/Discord, with per-tag colors, gradients, and visibility
This commit is contained in:
commit
17395fbbf5
17 changed files with 5185 additions and 0 deletions
39
.github/workflows/deploy.yml
vendored
Normal file
39
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
name: Build and deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install deps
|
||||
run: npm install
|
||||
|
||||
- name: Build plugin(s)
|
||||
run: node ./build.mjs
|
||||
|
||||
- name: Copy additional files
|
||||
run: |
|
||||
cp README.md dist/README.md
|
||||
printf -- "---\npermalink: /404.html\n---\n" > dist/404.md
|
||||
printf -- "> **Note:** You accessed a link that returned a 404, probably by clicking one of the plugin links. You're supposed to copy the link address and add it into Revenge.\n\n" >> dist/404.md
|
||||
cat README.md >> dist/404.md
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./dist
|
||||
enable_jekyll: true
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
||||
47
README.md
Normal file
47
README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# revenge-plugins
|
||||
|
||||
A small collection of plugins for [Revenge](https://github.com/revenge-mod/revenge-bundle), the Discord Android client mod.
|
||||
|
||||
## Installing a plugin
|
||||
|
||||
1. Open Discord with Revenge installed, go to **Settings → Plugins**.
|
||||
2. Tap the `+` button and paste the plugin's install link (see below).
|
||||
3. Enable it.
|
||||
|
||||
| Plugin | Description | Install link |
|
||||
| --- | --- | --- |
|
||||
| Staff Tags | Adds OWNER/ADMIN/STAFF/MOD-style tags next to members, based on their actual server permissions. Fully customizable: per-tag text, colors, gradients, and visibility. | `https://everestmcarthur.github.io/revenge-plugins/staff-tags/` |
|
||||
|
||||
## About Staff Tags
|
||||
|
||||
This is a rebuilt version of the old `staff-tags` plugin, which stopped working after Discord renamed some of the
|
||||
internal components it depended on (`DisplayName`, `HeaderName`, `getTagProperties`) - when any one of those lookups
|
||||
came back empty, the entire plugin used to fail to load, taking every tag down with it, everywhere.
|
||||
|
||||
This version isolates each patch (chat tags, member list, profile, channel header) so that if Discord changes one of
|
||||
them again in the future, only that specific surface stops showing tags instead of the whole plugin breaking. It also
|
||||
adds:
|
||||
|
||||
- Per-tag show/hide toggles
|
||||
- Custom tag text
|
||||
- Custom solid colors
|
||||
- Gradient tags (member list & profile only - chat message tags are rendered from data rather than a
|
||||
patchable element, so they always use a solid color)
|
||||
- "Use top role color" as a fallback when no custom color is set
|
||||
|
||||
Tags are computed from the built-in `computePermissions` API against real permission bits (Administrator, Manage
|
||||
Server, Manage Messages, Kick/Ban, etc.), the same as the original plugin, not from role names.
|
||||
|
||||
## Building locally
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Output is written to `dist/<plugin>/`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues and PRs are welcome, especially if a Discord update breaks a lookup a plugin relies on - `findByName` /
|
||||
`findByProps` targets can and do change without notice.
|
||||
93
build.mjs
Normal file
93
build.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { readFile, writeFile, readdir } from "fs/promises";
|
||||
import { extname } from "path";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
import { rollup } from "rollup";
|
||||
import url from "@rollup/plugin-url";
|
||||
import esbuild from "rollup-plugin-esbuild";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import nodeResolve from "@rollup/plugin-node-resolve";
|
||||
import swc from "@swc/core";
|
||||
|
||||
const extensions = [".js", ".jsx", ".mjs", ".ts", ".tsx", ".cts", ".mts"];
|
||||
|
||||
/** @type import("rollup").InputPluginOption */
|
||||
const plugins = [
|
||||
nodeResolve(),
|
||||
commonjs(),
|
||||
url({
|
||||
include: ["**/*.svg", "**/*.png", "**/*.jpg", "**/*.gif"],
|
||||
limit: 0,
|
||||
}),
|
||||
{
|
||||
name: "swc",
|
||||
async transform(code, id) {
|
||||
const ext = extname(id);
|
||||
if (!extensions.includes(ext)) return null;
|
||||
|
||||
const ts = ext.includes("ts");
|
||||
const tsx = ts ? ext.endsWith("x") : undefined;
|
||||
const jsx = !ts ? ext.endsWith("x") : undefined;
|
||||
|
||||
const result = await swc.transform(code, {
|
||||
filename: id,
|
||||
jsc: {
|
||||
externalHelpers: true,
|
||||
parser: {
|
||||
syntax: ts ? "typescript" : "ecmascript",
|
||||
tsx,
|
||||
jsx,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
targets: "defaults",
|
||||
include: [
|
||||
"transform-classes",
|
||||
"transform-arrow-functions",
|
||||
],
|
||||
},
|
||||
});
|
||||
return result.code;
|
||||
},
|
||||
},
|
||||
esbuild({ minify: true }),
|
||||
];
|
||||
|
||||
for (const plug of await readdir("./plugins")) {
|
||||
const manifest = JSON.parse(await readFile(`./plugins/${plug}/manifest.json`));
|
||||
const outPath = `./dist/${plug}/index.js`;
|
||||
|
||||
try {
|
||||
const bundle = await rollup({
|
||||
input: `./plugins/${plug}/${manifest.main}`,
|
||||
onwarn: () => {},
|
||||
plugins,
|
||||
});
|
||||
|
||||
await bundle.write({
|
||||
file: outPath,
|
||||
globals(id) {
|
||||
if (id.startsWith("@vendetta")) return id.substring(1).replace(/\//g, ".");
|
||||
const map = {
|
||||
react: "window.React",
|
||||
};
|
||||
|
||||
return map[id] || null;
|
||||
},
|
||||
format: "iife",
|
||||
compact: true,
|
||||
exports: "named",
|
||||
});
|
||||
await bundle.close();
|
||||
|
||||
const toHash = await readFile(outPath);
|
||||
manifest.hash = createHash("sha256").update(toHash).digest("hex");
|
||||
manifest.main = "index.js";
|
||||
await writeFile(`./dist/${plug}/manifest.json`, JSON.stringify(manifest));
|
||||
|
||||
console.log(`Successfully built ${manifest.name}!`);
|
||||
} catch (e) {
|
||||
console.error(`Failed to build ${plug}...`, e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
4432
package-lock.json
generated
Normal file
4432
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "revenge-plugins",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node build.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^24.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-url": "latest",
|
||||
"@swc/core": "^1.3.35",
|
||||
"esbuild": "^0.16.14",
|
||||
"rollup": "^3.9.1",
|
||||
"rollup-plugin-esbuild": "^5.0.0",
|
||||
"vendetta-types": "latest"
|
||||
}
|
||||
}
|
||||
11
plugins/staff-tags/manifest.json
Normal file
11
plugins/staff-tags/manifest.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"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.",
|
||||
"authors": [
|
||||
{ "name": "everestmcarthur" }
|
||||
],
|
||||
"main": "src/index.ts",
|
||||
"vendetta": {
|
||||
"icon": "ic_badge_staff"
|
||||
}
|
||||
}
|
||||
37
plugins/staff-tags/src/index.ts
Normal file
37
plugins/staff-tags/src/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import patchChat from "./patches/chat";
|
||||
import patchDetails from "./patches/details";
|
||||
import patchName from "./patches/name";
|
||||
import patchTag from "./patches/tag";
|
||||
import Settings from "./ui/pages/Settings";
|
||||
|
||||
let patches: (() => void)[] = [];
|
||||
|
||||
// Discord occasionally renames/removes the internals a single surface (chat, member list, etc.)
|
||||
// depends on. Isolating each patch means one broken lookup only disables that surface instead of
|
||||
// taking the whole plugin down, which is what happened to the old build of this plugin.
|
||||
function safePatch(name: string, apply: () => () => void): () => void {
|
||||
try {
|
||||
return apply();
|
||||
} catch (e) {
|
||||
logger.error(`[Staff Tags] Failed to apply the "${name}" patch, that surface will be skipped: ${e}`);
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
storage.useRoleColor ??= false;
|
||||
storage.tags ??= {};
|
||||
|
||||
patches = [
|
||||
safePatch("tag", patchTag),
|
||||
safePatch("chat", patchChat),
|
||||
safePatch("name", patchName),
|
||||
safePatch("details", patchDetails)
|
||||
];
|
||||
},
|
||||
onUnload: () => patches.forEach((unpatch) => unpatch()),
|
||||
settings: Settings
|
||||
};
|
||||
119
plugins/staff-tags/src/lib/getTag.ts
Normal file
119
plugins/staff-tags/src/lib/getTag.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { chroma, constants } from "@vendetta/metro/common";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { rawColors } from "@vendetta/ui";
|
||||
|
||||
const { Permissions } = constants;
|
||||
const permissionsModule = findByProps("computePermissions", "canEveryoneRole");
|
||||
const GuildMemberStore = findByStoreName("GuildMemberStore");
|
||||
|
||||
export interface TagDefinition {
|
||||
id: string;
|
||||
defaultText: string;
|
||||
defaultColor: string;
|
||||
condition?: (guild: any, channel: any, user: any) => boolean;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export const TAG_DEFINITIONS: TagDefinition[] = [
|
||||
{ id: "webhook", defaultText: "WEBHOOK", defaultColor: "#99AAB5", condition: (_g, _c, user) => !!user?.isNonUserBot?.() },
|
||||
{ id: "owner", defaultText: "OWNER", defaultColor: "#F0B232", condition: (guild, _c, user) => !!guild && guild.ownerId === user?.id },
|
||||
{ id: "admin", defaultText: "ADMIN", defaultColor: "#F23F42", permissions: ["ADMINISTRATOR"] },
|
||||
{ id: "staff", defaultText: "STAFF", defaultColor: "#23A55A", permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES", "MANAGE_WEBHOOKS"] },
|
||||
{ id: "mod", defaultText: "MOD", defaultColor: "#5865F2", permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"] },
|
||||
{ id: "vc_mod", defaultText: "VC Mod", defaultColor: "#1ABC9C", permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"] },
|
||||
{ id: "chat_mod", defaultText: "Chat Mod", defaultColor: "#9B59B6", permissions: ["MODERATE_MEMBERS"] }
|
||||
];
|
||||
|
||||
export interface TagOverride {
|
||||
enabled?: boolean;
|
||||
text?: string;
|
||||
useCustomColor?: boolean;
|
||||
color?: string;
|
||||
useGradient?: boolean;
|
||||
gradientColor?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedTag {
|
||||
id: string;
|
||||
text: string;
|
||||
textColor: any;
|
||||
backgroundColor: string;
|
||||
gradientColor?: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
|
||||
/** Lazily initializes and returns the per-tag settings object, backed directly by plugin storage. */
|
||||
export function tagSettings(id: string): TagOverride {
|
||||
storage.tags ??= {};
|
||||
storage.tags[id] ??= {};
|
||||
return storage.tags[id];
|
||||
}
|
||||
|
||||
function resolveBackgroundColor(def: TagDefinition, settings: TagOverride, guild: any, user: any): string {
|
||||
if (settings.useCustomColor && settings.color && HEX_REGEX.test(settings.color)) {
|
||||
return settings.color;
|
||||
}
|
||||
|
||||
if (storage.useRoleColor) {
|
||||
try {
|
||||
const roleColor = GuildMemberStore?.getMember?.(guild?.id, user?.id)?.colorString;
|
||||
if (roleColor) return roleColor;
|
||||
} catch { /* fall through to default */ }
|
||||
}
|
||||
|
||||
return def.defaultColor;
|
||||
}
|
||||
|
||||
export default function getTag(guild: any, channel: any, user: any): ResolvedTag | undefined {
|
||||
if (!user) return undefined;
|
||||
|
||||
let permissions: string[] = [];
|
||||
if (guild) {
|
||||
try {
|
||||
const permissionsInt = permissionsModule?.computePermissions?.({
|
||||
user,
|
||||
context: guild,
|
||||
overwrites: channel?.permissionOverwrites
|
||||
});
|
||||
|
||||
if (permissionsInt != null) {
|
||||
permissions = Object.entries(Permissions)
|
||||
.filter(([, bit]) => permissionsInt & (bit as bigint))
|
||||
.map(([name]) => name);
|
||||
}
|
||||
} catch { /* no guild permission context available, treat as none */ }
|
||||
}
|
||||
|
||||
for (const def of TAG_DEFINITIONS) {
|
||||
const settings = tagSettings(def.id);
|
||||
if (settings.enabled === false) continue;
|
||||
|
||||
const matchesCondition = !!def.condition?.(guild, channel, user);
|
||||
const matchesPermission = !user.bot && !!def.permissions?.some(p => permissions.includes(p));
|
||||
if (!matchesCondition && !matchesPermission) continue;
|
||||
|
||||
const backgroundColor = resolveBackgroundColor(def, settings, guild, user);
|
||||
const textColor = chroma(backgroundColor).get("lab.l") < 70 ? rawColors.WHITE_500 : rawColors.BLACK_500;
|
||||
|
||||
let gradientColor: string | undefined;
|
||||
if (settings.useGradient) {
|
||||
gradientColor = settings.gradientColor && HEX_REGEX.test(settings.gradientColor)
|
||||
? settings.gradientColor
|
||||
: chroma(backgroundColor).brighten(1.4).hex();
|
||||
}
|
||||
|
||||
return {
|
||||
id: def.id,
|
||||
text: settings.text?.trim() || def.defaultText,
|
||||
textColor,
|
||||
backgroundColor,
|
||||
gradientColor,
|
||||
verified: false
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
25
plugins/staff-tags/src/lib/gradient.ts
Normal file
25
plugins/staff-tags/src/lib/gradient.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
|
||||
let cached: any;
|
||||
let attempted = false;
|
||||
|
||||
/**
|
||||
* Discord ships a native gradient view (used for boost/nitro badges) but it isn't part of any
|
||||
* stable public API, so this is a best-effort lookup that quietly returns null if it can't be found -
|
||||
* gradient tags then fall back to a solid color instead of crashing anything.
|
||||
*/
|
||||
export function getGradientComponent(): any {
|
||||
if (attempted) return cached;
|
||||
attempted = true;
|
||||
|
||||
try {
|
||||
cached =
|
||||
findByProps("colors", "start", "end")?.default ??
|
||||
findByProps("colors", "locations")?.default ??
|
||||
null;
|
||||
} catch {
|
||||
cached = null;
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
32
plugins/staff-tags/src/patches/chat.ts
Normal file
32
plugins/staff-tags/src/patches/chat.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { findByName, findByStoreName } from "@vendetta/metro";
|
||||
import { ReactNative, chroma } from "@vendetta/metro/common";
|
||||
import { after } from "@vendetta/patcher";
|
||||
import getTag from "../lib/getTag";
|
||||
|
||||
const getTagProperties = findByName("getTagProperties", false);
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const ChannelStore = findByStoreName("ChannelStore");
|
||||
|
||||
// Chat message tags are rendered from plain data (not a patchable element tree), so gradients
|
||||
// aren't possible here - only in the member list and profile, where we control the JSX directly.
|
||||
export default () => {
|
||||
if (!getTagProperties) return () => {};
|
||||
|
||||
return after("default", getTagProperties, ([{ message }], ret) => {
|
||||
if (ret?.tagType) return;
|
||||
|
||||
const channel = ChannelStore?.getChannel(message?.channel_id);
|
||||
const guild = GuildStore?.getGuild(channel?.guild_id);
|
||||
const tag = getTag(guild, channel, message?.author);
|
||||
if (!tag) return;
|
||||
|
||||
return {
|
||||
...ret,
|
||||
tagText: tag.text,
|
||||
tagTextColor: tag.textColor ? ReactNative.processColor(chroma(tag.textColor).hex()) : undefined,
|
||||
tagBackgroundColor: ReactNative.processColor(chroma(tag.backgroundColor).hex()),
|
||||
tagVerified: tag.verified,
|
||||
tagType: undefined
|
||||
};
|
||||
});
|
||||
};
|
||||
78
plugins/staff-tags/src/patches/details.tsx
Normal file
78
plugins/staff-tags/src/patches/details.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { findByProps, findByStoreName, findByTypeNameAll } from "@vendetta/metro";
|
||||
import { after } from "@vendetta/patcher";
|
||||
import { findInReactTree } from "@vendetta/utils";
|
||||
import getTag from "../lib/getTag";
|
||||
import GradientTag from "../ui/GradientTag";
|
||||
|
||||
// Covers member list rows and the profile popout, where the name and tag sit on the same line.
|
||||
const TagModule = findByProps("getBotLabel");
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
|
||||
const rowPatch = ([{ guildId, user }]: any[], res: any) => {
|
||||
try {
|
||||
const label = res?.props?.label;
|
||||
const nameContainer = findInReactTree(
|
||||
label,
|
||||
(c) =>
|
||||
Array.isArray(c?.props?.children) &&
|
||||
c.props.children.some(
|
||||
(ch: any) => typeof ch === "string" || typeof ch?.props?.children === "string"
|
||||
)
|
||||
);
|
||||
if (!nameContainer) return;
|
||||
|
||||
const existingTag = findInReactTree(nameContainer, (c) => c?.type?.Types);
|
||||
if (existingTag && existingTag.props?.type !== 0) return;
|
||||
|
||||
const guild = GuildStore?.getGuild(guildId);
|
||||
const tag = getTag(guild, undefined, user);
|
||||
if (!tag) return;
|
||||
|
||||
if (existingTag) {
|
||||
Object.assign(existingTag.props, {
|
||||
type: 0,
|
||||
text: tag.text,
|
||||
textColor: tag.textColor,
|
||||
backgroundColor: tag.backgroundColor,
|
||||
verified: tag.verified
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(nameContainer.props.children)) {
|
||||
nameContainer.props.children = [nameContainer.props.children];
|
||||
}
|
||||
|
||||
if (tag.gradientColor) {
|
||||
nameContainer.props.children.push(
|
||||
<GradientTag
|
||||
text={tag.text}
|
||||
textColor={tag.textColor}
|
||||
backgroundColor={tag.backgroundColor}
|
||||
gradientColor={tag.gradientColor}
|
||||
/>
|
||||
);
|
||||
} else if (TagModule) {
|
||||
nameContainer.props.children.push(
|
||||
<TagModule.default
|
||||
type={0}
|
||||
text={tag.text}
|
||||
textColor={tag.textColor}
|
||||
backgroundColor={tag.backgroundColor}
|
||||
verified={tag.verified}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Never let one bad lookup take down the whole member list.
|
||||
}
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const patches: (() => void)[] = [];
|
||||
const rows = findByTypeNameAll("UserRow") ?? [];
|
||||
|
||||
rows.forEach((UserRow: any) => patches.push(after("type", UserRow, rowPatch)));
|
||||
|
||||
return () => patches.forEach((unpatch) => unpatch());
|
||||
};
|
||||
80
plugins/staff-tags/src/patches/name.tsx
Normal file
80
plugins/staff-tags/src/patches/name.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { findByName, findByProps, findByStoreName } from "@vendetta/metro";
|
||||
import { after } from "@vendetta/patcher";
|
||||
import { findInReactTree } from "@vendetta/utils";
|
||||
import getTag from "../lib/getTag";
|
||||
import GradientTag from "../ui/GradientTag";
|
||||
|
||||
// Covers the name row shown in chat messages (next to the display name + timestamp) and channel headers.
|
||||
const DisplayName = findByName("DisplayName", false);
|
||||
const HeaderName = findByName("HeaderName", false);
|
||||
const TagModule = findByProps("getBotLabel");
|
||||
|
||||
const GuildStore = findByStoreName("GuildStore");
|
||||
const ChannelStore = findByStoreName("ChannelStore");
|
||||
|
||||
export default () => {
|
||||
const patches: (() => void)[] = [];
|
||||
|
||||
if (HeaderName) {
|
||||
patches.push(after("default", HeaderName, ([{ channelId }], ret) => {
|
||||
if (ret?.props) ret.props.channelId = channelId;
|
||||
}));
|
||||
}
|
||||
|
||||
if (DisplayName) {
|
||||
patches.push(after("default", DisplayName, ([{ guildId, channelId, user }], ret) => {
|
||||
try {
|
||||
const tagComponent = findInReactTree(ret, (c) => c?.type?.Types);
|
||||
|
||||
// A real built-in tag (bot/system/etc.) is already present - don't touch it.
|
||||
if (tagComponent && tagComponent.props?.type !== 0) return;
|
||||
|
||||
const guild = GuildStore?.getGuild(guildId);
|
||||
const channel = ChannelStore?.getChannel(channelId);
|
||||
const tag = getTag(guild, channel, user);
|
||||
if (!tag) return;
|
||||
|
||||
if (tagComponent) {
|
||||
tagComponent.props = {
|
||||
type: 0,
|
||||
text: tag.text,
|
||||
textColor: tag.textColor,
|
||||
backgroundColor: tag.backgroundColor,
|
||||
verified: tag.verified
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const row = findInReactTree(ret, (c) => c?.props?.style?.flexDirection === "row");
|
||||
if (!Array.isArray(row?.props?.children)) return;
|
||||
|
||||
if (tag.gradientColor) {
|
||||
row.props.children.push(
|
||||
<GradientTag
|
||||
style={{ marginLeft: 0 }}
|
||||
text={tag.text}
|
||||
textColor={tag.textColor}
|
||||
backgroundColor={tag.backgroundColor}
|
||||
gradientColor={tag.gradientColor}
|
||||
/>
|
||||
);
|
||||
} else if (TagModule) {
|
||||
row.props.children.push(
|
||||
<TagModule.default
|
||||
style={{ marginLeft: 0 }}
|
||||
type={0}
|
||||
text={tag.text}
|
||||
textColor={tag.textColor}
|
||||
backgroundColor={tag.backgroundColor}
|
||||
verified={tag.verified}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Never let one bad lookup take down every name row on screen.
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return () => patches.forEach((unpatch) => unpatch());
|
||||
};
|
||||
22
plugins/staff-tags/src/patches/tag.tsx
Normal file
22
plugins/staff-tags/src/patches/tag.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
import { after } from "@vendetta/patcher";
|
||||
import { findInReactTree } from "@vendetta/utils";
|
||||
|
||||
const Tag = findByProps("getBotLabel");
|
||||
|
||||
/**
|
||||
* Discord's own Tag component ignores custom text/color props unless we push them into the
|
||||
* already-rendered tree ourselves - this is what actually makes our overrides visible.
|
||||
*/
|
||||
export default () => {
|
||||
if (!Tag) return () => {};
|
||||
|
||||
return after("default", Tag, ([{ text, textColor, backgroundColor }], ret) => {
|
||||
const label = findInReactTree(ret, (c) => typeof c?.props?.children === "string");
|
||||
if (!label) return;
|
||||
|
||||
if (text) label.props.children = text;
|
||||
if (textColor) label.props.style?.push?.({ color: textColor });
|
||||
if (backgroundColor) ret?.props?.style?.push?.({ backgroundColor });
|
||||
});
|
||||
};
|
||||
45
plugins/staff-tags/src/ui/GradientTag.tsx
Normal file
45
plugins/staff-tags/src/ui/GradientTag.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { React, ReactNative } from "@vendetta/metro/common";
|
||||
import { getGradientComponent } from "../lib/gradient";
|
||||
|
||||
const { View, Text } = ReactNative;
|
||||
|
||||
const baseStyle = {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 4,
|
||||
marginLeft: 4,
|
||||
overflow: "hidden"
|
||||
};
|
||||
|
||||
interface GradientTagProps {
|
||||
text: string;
|
||||
textColor: any;
|
||||
backgroundColor: string;
|
||||
gradientColor?: string;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export default function GradientTag({ text, textColor, backgroundColor, gradientColor, style }: GradientTagProps) {
|
||||
const textStyle = { color: textColor, fontSize: 11, fontWeight: "700" };
|
||||
const Gradient = gradientColor ? getGradientComponent() : null;
|
||||
|
||||
if (Gradient) {
|
||||
return (
|
||||
<Gradient
|
||||
colors={[backgroundColor, gradientColor]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[baseStyle, style]}
|
||||
>
|
||||
<Text style={textStyle}>{text}</Text>
|
||||
</Gradient>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[baseStyle, { backgroundColor }, style]}>
|
||||
<Text style={textStyle}>{text}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
91
plugins/staff-tags/src/ui/pages/Settings.tsx
Normal file
91
plugins/staff-tags/src/ui/pages/Settings.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { React, ReactNative } from "@vendetta/metro/common";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { Forms } from "@vendetta/ui/components";
|
||||
import { TAG_DEFINITIONS, tagSettings } from "../../lib/getTag";
|
||||
|
||||
const { ScrollView, View } = ReactNative;
|
||||
const { FormSection, FormSwitchRow, FormInput } = Forms;
|
||||
|
||||
const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
||||
|
||||
function TagSettingsSection({ id, defaultText, defaultColor }: { id: string; defaultText: string; defaultColor: string }) {
|
||||
const settings = tagSettings(id);
|
||||
useProxy(settings);
|
||||
|
||||
const enabled = settings.enabled !== false;
|
||||
const invalidColor = !!settings.color && !HEX_REGEX.test(settings.color);
|
||||
const invalidGradient = !!settings.gradientColor && !HEX_REGEX.test(settings.gradientColor);
|
||||
|
||||
return (
|
||||
<FormSection title={defaultText}>
|
||||
<FormSwitchRow
|
||||
label="Show this tag"
|
||||
value={enabled}
|
||||
onValueChange={(v: boolean) => { settings.enabled = v; }}
|
||||
/>
|
||||
<FormInput
|
||||
title="Tag text"
|
||||
placeholder={defaultText}
|
||||
value={settings.text ?? ""}
|
||||
editable={enabled}
|
||||
onChange={(v: string) => { settings.text = v; }}
|
||||
/>
|
||||
<FormSwitchRow
|
||||
label="Custom color"
|
||||
subLabel={`Default: ${defaultColor}`}
|
||||
value={!!settings.useCustomColor}
|
||||
disabled={!enabled}
|
||||
onValueChange={(v: boolean) => { settings.useCustomColor = v; }}
|
||||
/>
|
||||
{settings.useCustomColor && (
|
||||
<FormInput
|
||||
title="Color (hex)"
|
||||
placeholder={defaultColor}
|
||||
value={settings.color ?? ""}
|
||||
editable={enabled}
|
||||
onChange={(v: string) => { settings.color = v; }}
|
||||
helpText={invalidColor ? "Invalid hex color, e.g. #5865F2 - falling back to default for now" : undefined}
|
||||
/>
|
||||
)}
|
||||
<FormSwitchRow
|
||||
label="Gradient"
|
||||
subLabel="Member list & profile only, chat tags stay solid"
|
||||
value={!!settings.useGradient}
|
||||
disabled={!enabled}
|
||||
onValueChange={(v: boolean) => { settings.useGradient = v; }}
|
||||
/>
|
||||
{settings.useGradient && (
|
||||
<FormInput
|
||||
title="Gradient color (hex)"
|
||||
placeholder="Leave blank to auto-generate"
|
||||
value={settings.gradientColor ?? ""}
|
||||
editable={enabled}
|
||||
onChange={(v: string) => { settings.gradientColor = v; }}
|
||||
helpText={invalidGradient ? "Invalid hex color, leave blank to auto-generate" : undefined}
|
||||
/>
|
||||
)}
|
||||
</FormSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
useProxy(storage);
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<FormSection title="General">
|
||||
<FormSwitchRow
|
||||
label="Use top role color"
|
||||
subLabel="Used when a tag has no custom color set"
|
||||
value={!!storage.useRoleColor}
|
||||
onValueChange={(v: boolean) => { storage.useRoleColor = v; }}
|
||||
/>
|
||||
</FormSection>
|
||||
{TAG_DEFINITIONS.map((def) => (
|
||||
<TagSettingsSection key={def.id} id={def.id} defaultText={def.defaultText} defaultColor={def.defaultColor} />
|
||||
))}
|
||||
<View style={{ height: 24 }} />
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-native",
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["plugins", "node_modules/vendetta-types"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue