diff --git a/plugins/radial-status/LICENSE b/plugins/radial-status/LICENSE deleted file mode 100644 index 3744c08..0000000 --- a/plugins/radial-status/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2024, Angel - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/plugins/radial-status/js/index.ts b/plugins/radial-status/js/index.ts deleted file mode 100644 index d0217ac..0000000 --- a/plugins/radial-status/js/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import patchRing from './patches/ring' -import Settings from './ui/pages/Settings' -import type { RadialStatusStorage } from './lib/types' - -export default plugin<{ jsonStorage: RadialStatusStorage }>({ - jsonStorage: { - load: true, - default: { - enabled: true, - colors: { - online: '#23A55A', - idle: '#F0B232', - dnd: '#F23F42', - }, - ringThickness: 2, - }, - }, - - start(api) { - let unpatch = () => {} - try { - unpatch = patchRing(api.jsonStorage) - } catch (e) { - api.logger.error(`[Radial Status] Failed to apply the ring patch: ${e}`) - } - api.cleanup(unpatch) - }, - - SettingsComponent: Settings, -}) diff --git a/plugins/radial-status/js/lib/color.ts b/plugins/radial-status/js/lib/color.ts deleted file mode 100644 index 9bdd798..0000000 --- a/plugins/radial-status/js/lib/color.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i - -export function isValidHex(value: string | undefined | null): value is string { - return !!value && HEX_REGEX.test(value) -} diff --git a/plugins/radial-status/js/lib/safe.ts b/plugins/radial-status/js/lib/safe.ts deleted file mode 100644 index 92d11f4..0000000 --- a/plugins/radial-status/js/lib/safe.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function guard(fn: () => T, fallback: T): T { - try { - return fn() - } catch { - return fallback - } -} diff --git a/plugins/radial-status/js/lib/types.ts b/plugins/radial-status/js/lib/types.ts deleted file mode 100644 index b72c88e..0000000 --- a/plugins/radial-status/js/lib/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RadialStatusStorage { - enabled: boolean - colors: Record - ringThickness: number -} diff --git a/plugins/radial-status/js/patches/ring.tsx b/plugins/radial-status/js/patches/ring.tsx deleted file mode 100644 index 82f8b74..0000000 --- a/plugins/radial-status/js/patches/ring.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { getModules } from '@revenge-mod/modules/finders' -import { withProps } from '@revenge-mod/modules/finders/filters' -import { beforeJSX } from '@revenge-mod/react/jsx-runtime' -import { guard } from '../lib/safe' -import type { JsonStorage } from '@revenge-mod/json-storage' -import type { ElementType } from 'react' -import type { RadialStatusStorage } from '../lib/types' - -const CONFIRMED_SIZES = new Set([24, 32, 40, 50, 60, 80]) - -function tryApplyRing( - props: unknown, - storage: JsonStorage, -) { - if (!storage.cache?.enabled) return - if (!props || !Array.isArray((props as any).style)) return - - const wrapper = props as { style: any[]; children?: any[] } - if (!Array.isArray(wrapper.children) || wrapper.children.length !== 5) return - - const circleIdx = wrapper.style.findIndex( - s => - s && - typeof s.width === 'number' && - s.width === s.height && - s.borderRadius === s.width / 2 && - CONFIRMED_SIZES.has(s.width), - ) - if (circleIdx === -1) return - - const userProps = wrapper.children?.[1]?.props - const presenceProps = wrapper.children?.[3]?.props - if (!userProps || typeof userProps.user?.id !== 'string') return - if (!presenceProps || typeof presenceProps.status !== 'string') return - - const colors = storage.cache?.colors ?? {} - const color = colors[presenceProps.status as string] - if (!color) return - - const baseSize = wrapper.style[circleIdx].width - const thickness = storage.cache?.ringThickness ?? 2 - const newSize = baseSize + thickness * 2 - - presenceProps.size = 0 - presenceProps.isMobileOnline = false - if (presenceProps.style) presenceProps.style.display = 'none' - if (userProps.cutout?.nativeCutouts?.[0]) - userProps.cutout.nativeCutouts[0].size = 0 - - wrapper.style[circleIdx] = { - width: newSize, - height: newSize, - borderRadius: newSize / 2, - overflow: 'hidden', - } - wrapper.style.push({ - borderWidth: thickness, - borderColor: color, - borderStyle: 'solid', - }) -} - -export default function patchRing(storage: JsonStorage) { - const cleanups: (() => void)[] = [] - - const unsubGeneral = getModules( - withProps('Button', 'Text', 'View'), - mod => { - guard(() => { - const View = (mod as { View?: ElementType }).View - if (!View) return - - cleanups.push( - beforeJSX(View, args => { - guard(() => tryApplyRing(args[1], storage), undefined) - return args - }), - ) - }, undefined) - }, - { skipDefault: true }, - ) - cleanups.push(unsubGeneral) - - return () => cleanups.forEach(fn => fn()) -} diff --git a/plugins/radial-status/js/ui/ColorInput.tsx b/plugins/radial-status/js/ui/ColorInput.tsx deleted file mode 100644 index d5460d8..0000000 --- a/plugins/radial-status/js/ui/ColorInput.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Text, TextInput, TouchableOpacity, View } from 'react-native' -import { isValidHex } from '../lib/color' - -const PRESETS = [ - '#F23F42', - '#F0B232', - '#FEE75C', - '#23A55A', - '#1ABC9C', - '#5865F2', - '#9B59B6', - '#EB459E', - '#99AAB5', - '#FFFFFF', -] - -interface ColorInputProps { - title: string - value?: string - placeholder?: string - onChange: (value: string) => void -} - -export default function ColorInput({ - title, - value, - placeholder, - onChange, -}: ColorInputProps) { - const valid = !value || isValidHex(value) - const swatchColor = isValidHex(value) - ? value - : isValidHex(placeholder) - ? placeholder - : '#5865F2' - - return ( - - - - {title} - - - - {PRESETS.map(preset => { - const active = value?.toLowerCase() === preset.toLowerCase() - return ( - onChange(preset)} - style={{ - width: 24, - height: 24, - borderRadius: 6, - backgroundColor: preset, - marginRight: 8, - marginBottom: 8, - borderWidth: active ? 2 : 1, - borderColor: active ? '#ffffff' : 'rgba(128,128,128,0.35)', - }} - /> - ) - })} - - {!valid && ( - - Invalid hex color, e.g. #5865F2 - - )} - - ) -} diff --git a/plugins/radial-status/js/ui/pages/Settings.tsx b/plugins/radial-status/js/ui/pages/Settings.tsx deleted file mode 100644 index 1b17d77..0000000 --- a/plugins/radial-status/js/ui/pages/Settings.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Design } from '@revenge-mod/discord/design' -import { ScrollView, View } from 'react-native' -import ColorInput from '../ColorInput' -import type { PluginApi } from '@revenge-mod/plugins/types' -import type { RadialStatusStorage } from '../../lib/types' - -const STATUSES: { key: string; label: string; defaultColor?: string }[] = [ - { key: 'online', label: 'Online', defaultColor: '#23A55A' }, - { key: 'idle', label: 'Idle', defaultColor: '#F0B232' }, - { key: 'dnd', label: 'Do Not Disturb', defaultColor: '#F23F42' }, - { key: 'offline', label: 'Offline', defaultColor: '#80848E' }, -] - -export default function Settings({ - api, -}: { - api: PluginApi<{ jsonStorage: RadialStatusStorage }> -}) { - const { TableRowGroup, TableSwitchRow, TextInput, Text } = Design - const storage = api.jsonStorage.use() - const colors = storage?.colors ?? {} - - return ( - - - - Replaces the small presence dot on an avatar with a colored ring - instead, per status below. Leave a status blank to keep showing the - normal dot for it. - - - - api.jsonStorage.set({ enabled: v })} - /> - - - {STATUSES.map(({ key, label, defaultColor }) => ( - - api.jsonStorage.set({ colors: { [key]: v } }) - } - /> - ))} - - - { - const n = Number.parseFloat(v) - if (!Number.isNaN(n) && n > 0) - api.jsonStorage.set({ ringThickness: n }) - }} - /> - - - - ) -} diff --git a/plugins/radial-status/manifest.json b/plugins/radial-status/manifest.json deleted file mode 100644 index 9526df0..0000000 --- a/plugins/radial-status/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "format": 1, - "id": "dev.everestmcarthur.radial-status", - "name": "Radial Status", - "description": "Replaces the small presence dot on avatars with a colored ring instead - one color per status (online/idle/dnd/offline), fully configurable, plus thickness control.", - "author": "Rosie", - "version": "1.0.0", - "dependencies": { - "revenge.api": { - "version": ">=1 <2" - }, - "discord": { - "version": "*" - } - }, - "dist": { - "script": "index.js" - } -} diff --git a/plugins/readme.md b/plugins/readme.md new file mode 100644 index 0000000..4c19f71 --- /dev/null +++ b/plugins/readme.md @@ -0,0 +1 @@ +More Plugins Coming Soon diff --git a/plugins/staff-tags/js/index.ts b/plugins/staff-tags/js/index.ts deleted file mode 100644 index 66bf4ad..0000000 --- a/plugins/staff-tags/js/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { applyPatches } from './lib/applyPatches' -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' -import type { StaffTagsStorage } from './lib/types' - -export default plugin<{ jsonStorage: StaffTagsStorage }>({ - jsonStorage: { - load: true, - default: { useRoleColor: false, tags: {} }, - }, - - start(api) { - const unpatchAll = applyPatches('Staff Tags', api.logger, { - tag: patchTag, - chat: () => patchChat(api.jsonStorage), - name: () => patchName(api.jsonStorage), - details: () => patchDetails(api.jsonStorage), - }) - - api.cleanup(unpatchAll) - }, - - SettingsComponent: Settings, -}) diff --git a/plugins/staff-tags/js/lib/afterRendered.ts b/plugins/staff-tags/js/lib/afterRendered.ts deleted file mode 100644 index 4b4d0a0..0000000 --- a/plugins/staff-tags/js/lib/afterRendered.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { after } from '@revenge-mod/patcher' -import type { FC, ReactElement, ReactNode } from 'react' - -/** - * Hooks the render output of a *specific element instance* by patching its own `type` property. - * - * This is safe (unlike patching a component's module export) because `element.type` is a fresh - * reference on an object we just created ourselves - nothing else has captured it yet, so there's - * no same-chunk-destructuring staleness to worry about. See @revenge-mod/patcher's `after` docs - * and the pattern used by Revenge's own `user-badges` plugin (`afterRendered` there). - * - * For a `React.memo()`-wrapped type, `element.type` is a non-callable `{$$typeof, type, compare}` - * object, not a function. Patching `element.type` directly (confirmed live - crashed the member - * list on `UserRow`) replaces it with a proxy whose own stub target *is* callable, which fools - * React's reconciler into taking the plain-function-component fast path (it checks - * `typeof Component === 'function'` before checking `$$typeof`) instead of the memo path - React - * then calls the proxy directly, whose `apply` trap forwards to `Reflect.apply(realMemoWrapper, - * ...)` and throws `TypeError: target is not callable`, since the real memo wrapper was never - * callable. Patching the wrapper's *inner* `.type` instead - the actual render function memo - * hides - avoids this: it's genuinely callable, it's what React actually invokes for a memo - * component (`updateSimpleMemoComponent` calls the inner function, not the wrapper), and leaving - * the wrapper's own shape (`$$typeof`/`compare`) untouched keeps React's own type detection - * correct. - * - * CAUTION: only call this once per distinct memo-wrapped type, ever. The memo wrapper's inner - * `.type` is shared by every instance of that component (memo only produces one render function - * total), not fresh per instance the way a plain element's `.type` is - so calling this from - * something that re-registers on every render (e.g. an `afterJSX` callback firing once per - * `createElement`) stacks a new self-unpatching hook onto that same shared function each time, - * and each stacked hook fires on *any* instance's render, not just the one that registered it. - * That mismatch broke tag placement in the member list for a long time (see git history on - * `patches/details.tsx`) - `patches/details.tsx` now patches the memo case directly with a single - * persistent `@revenge-mod/patcher` `after()` instead of going through this helper. - */ -export const afterRendered = ( - element: ReactElement>, - hook: (el: ReactNode) => ReactNode, -) => { - const type = element.type as unknown as { - type?: unknown - $$typeof?: unknown - } - const isMemoWrapped = - typeof element.type !== 'function' && typeof type?.type === 'function' - const target = (isMemoWrapped ? type : element) as Record<'type', FC> - - return after(target, 'type', el => - el instanceof Promise ? el.then(hook) : hook(el), - ) -} diff --git a/plugins/staff-tags/js/lib/applyPatches.ts b/plugins/staff-tags/js/lib/applyPatches.ts deleted file mode 100644 index 5cf2a1d..0000000 --- a/plugins/staff-tags/js/lib/applyPatches.ts +++ /dev/null @@ -1,35 +0,0 @@ -export interface PluginLogger { - error(message: string): void -} - -/** - * Wraps a patch-application function so a single broken lookup only disables that one surface - * instead of crashing the whole plugin's `start()` and taking every other patch down with it. - */ -function safePatch( - pluginName: string, - patchName: string, - apply: () => () => unknown, - logger: PluginLogger, -): () => unknown { - try { - return apply() - } catch (e) { - logger.error( - `[${pluginName}] Failed to apply the "${patchName}" patch, that surface will be skipped: ${e}`, - ) - return () => {} - } -} - -/** Applies a named set of patches with safePatch and returns one combined unpatch function. */ -export function applyPatches( - pluginName: string, - logger: PluginLogger, - patches: Record () => unknown>, -): () => void { - const unpatches = Object.entries(patches).map(([name, apply]) => - safePatch(pluginName, name, apply, logger), - ) - return () => unpatches.forEach(unpatch => unpatch()) -} diff --git a/plugins/staff-tags/js/lib/color.ts b/plugins/staff-tags/js/lib/color.ts deleted file mode 100644 index 9fee18c..0000000 --- a/plugins/staff-tags/js/lib/color.ts +++ /dev/null @@ -1,64 +0,0 @@ -export const HEX_REGEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i - -export function isValidHex(value: string | undefined | null): value is string { - return !!value && HEX_REGEX.test(value) -} - -/** Expands short form #abc to #aabbcc; leaves 6-digit hex untouched. */ -export function normalizeHex(hex: string): string { - if (hex.length === 4) { - return `#${hex - .slice(1) - .split('') - .map(c => c + c) - .join('')}` - } - return hex -} - -function hexToRgb(hex: string): number[] { - return (normalizeHex(hex).match(/\w\w/g) ?? ['00', '00', '00']).map(x => - Number.parseInt(x, 16), - ) -} - -function rgbToHex(rgb: number[]): string { - return `#${rgb.map(x => Math.max(0, Math.min(255, x)).toString(16).padStart(2, '0')).join('')}` -} - -export function interpolateColor( - color1: string, - color2: string, - percentage: number, -): string { - const rgb1 = hexToRgb(color1) - const rgb2 = hexToRgb(color2) - const interpolated = rgb1.map((c1, i) => - Math.round(c1 + (rgb2[i] - c1) * percentage), - ) - - return rgbToHex(interpolated) -} - -/** Mixes a color toward white by `amount` (0-1). Stands in for chroma's `.brighten()` without a chroma-js module lookup. */ -export function brighten(hex: string, amount: number): string { - return interpolateColor(hex, '#FFFFFF', Math.max(0, Math.min(1, amount))) -} - -/** - * WCAG relative luminance (0 = black, 1 = white). Used to pick readable text color against a - * background - stands in for chroma's `.get('lab.l')` threshold without a chroma-js module lookup. - */ -export function relativeLuminance(hex: string): number { - const [r, g, b] = hexToRgb(hex).map(c => { - const s = c / 255 - return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4 - }) - return 0.2126 * r + 0.7152 * g + 0.0722 * b -} - -/** Hardcoded Discord raw palette values (not exposed anywhere in @revenge-mod/types yet). */ -export const rawColors = { - WHITE_500: '#FFFFFF', - BLACK_500: '#1E1F22', -} diff --git a/plugins/staff-tags/js/lib/fiber.ts b/plugins/staff-tags/js/lib/fiber.ts deleted file mode 100644 index 33a8b5e..0000000 --- a/plugins/staff-tags/js/lib/fiber.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * `findInReactFiber` only narrows its result when given a type-predicate function - * (`(node) => node is X`). These walks are over untyped, Discord-internal fiber/element shapes, - * so there's no real `X` to narrow to - this just satisfies that signature with `any`. - */ -export function fiberFilter( - predicate: (node: any) => boolean, -): (node: any) => node is any { - return predicate as (node: any) => node is any -} diff --git a/plugins/staff-tags/js/lib/filters.ts b/plugins/staff-tags/js/lib/filters.ts deleted file mode 100644 index b4fea85..0000000 --- a/plugins/staff-tags/js/lib/filters.ts +++ /dev/null @@ -1,28 +0,0 @@ -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: , ...}`, 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, -) diff --git a/plugins/staff-tags/js/lib/getTag.ts b/plugins/staff-tags/js/lib/getTag.ts deleted file mode 100644 index 77a9c97..0000000 --- a/plugins/staff-tags/js/lib/getTag.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { Stores } from '@revenge-mod/discord/flux' -import { getModules } from '@revenge-mod/modules/finders' -import { withProps, withSingleProp } from '@revenge-mod/modules/finders/filters' -import { brighten, isValidHex, rawColors, relativeLuminance } from './color' -import type { DiscordModules } from '@revenge-mod/discord/types' -import type { JsonStorage } from '@revenge-mod/json-storage' -import type { StaffTagsStorage, TagOverride } from './types' - -interface PermissionsModule { - computePermissions(args: { - user: unknown - context: unknown - overwrites?: unknown - }): bigint | number | undefined - canEveryoneRole?: unknown -} - -interface GuildMember { - colorString?: string -} - -type GuildMemberStoreType = DiscordModules.Flux.Store<{ - getMember( - guildId: string | undefined, - userId: string | undefined, - ): GuildMember | null -}> - -/** - * Reads `Stores.GuildMemberStore` fresh on each call rather than caching it at module scope. - * - * External plugin bundles run via a bare `new Function('revenge', 'plugin', script)` call with - * no Metro-style dependency ordering, at preInit, regardless of which lifecycle the plugin - * actually declares - so touching *any* `revenge.*` API at module top level runs before - * Revenge's own API-providing plugins are guaranteed to have set anything up. This function is - * only ever called from inside a render patch, well after boot, where that's no longer a risk. - */ -function getGuildMemberStore(): GuildMemberStoreType { - return Stores.GuildMemberStore as GuildMemberStoreType -} - -/** - * Subscribed once, lazily, from `start()` rather than at plugin-module import time. Uses - * `getModules` (reacts once Discord itself initializes the module) instead of `lookupModule` - * (which force-initializes uninitialized modules right away) - `getTag()` only runs from inside - * a render patch, well after boot, so this is lower-risk than the boot-time lookups elsewhere in - * this plugin, but there's no reason to force it either. If the module isn't warmed up yet on the - * very first call, permission-based tags just don't resolve for that call and pick up on the next - * render once it's available. - * - * TODO(live-verify): computePermissions/Permissions module shapes, see eval-for-revenge. - */ -let permissionsModule: PermissionsModule | undefined -let PermissionsMap: { Permissions: Record } | undefined -let permissionsSubscribed = false - -function resolvePermissionsModules(): void { - if (permissionsSubscribed) return - permissionsSubscribed = true - getModules( - withProps('computePermissions', 'canEveryoneRole'), - mod => { - permissionsModule = mod - }, - ) - getModules( - withSingleProp<{ Permissions: Record }>( - 'Permissions', - ), - mod => { - PermissionsMap = mod - }, - ) -} - -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 ResolvedTag { - id: string - text: string - textColor: string - backgroundColor: string - gradientColor?: string - verified: boolean -} - -/** Reads the per-tag settings override from the (already-loaded) storage cache. Never mutates it - use `storage.set()` for writes. */ -export function tagSettings( - storage: JsonStorage, - id: string, -): TagOverride { - return storage.cache?.tags?.[id] ?? {} -} - -function resolveBackgroundColor( - def: TagDefinition, - settings: TagOverride, - storage: JsonStorage, - guild: any, - user: any, -): string { - if (settings.useCustomColor && isValidHex(settings.color)) { - return settings.color - } - - if (storage.cache?.useRoleColor) { - try { - const roleColor = getGuildMemberStore()?.getMember?.( - guild?.id, - user?.id, - )?.colorString - if (roleColor) return roleColor - } catch { - // fall through to default - } - } - - return def.defaultColor -} - -export default function getTag( - storage: JsonStorage, - guild: any, - channel: any, - user: any, -): ResolvedTag | undefined { - if (!user) return undefined - - resolvePermissionsModules() - - let permissions: string[] = [] - if (guild) { - try { - const permissionsInt = permissionsModule?.computePermissions?.({ - user, - context: guild, - overwrites: channel?.permissionOverwrites, - }) - - if (permissionsInt != null && PermissionsMap?.Permissions) { - permissions = Object.entries(PermissionsMap.Permissions) - .filter(([, bit]) => (permissionsInt as bigint) & (bit as bigint)) - .map(([name]) => name) - } - } catch { - // no guild permission context available, treat as none - } - } - - for (const def of TAG_DEFINITIONS) { - const settings = tagSettings(storage, 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, - storage, - guild, - user, - ) - const textColor = - relativeLuminance(backgroundColor) > 0.35 - ? rawColors.BLACK_500 - : rawColors.WHITE_500 - - let gradientColor: string | undefined - if (settings.useGradient) { - gradientColor = isValidHex(settings.gradientColor) - ? settings.gradientColor - : brighten(backgroundColor, 0.4) - } - - return { - id: def.id, - text: settings.text?.trim() || def.defaultText, - textColor, - backgroundColor, - gradientColor, - verified: false, - } - } - - return undefined -} diff --git a/plugins/staff-tags/js/lib/gradient.ts b/plugins/staff-tags/js/lib/gradient.ts deleted file mode 100644 index c777c1e..0000000 --- a/plugins/staff-tags/js/lib/gradient.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { lookupModule } from '@revenge-mod/modules/finders' -import { withProps } from '@revenge-mod/modules/finders/filters' -import type { FC, ReactNode } from 'react' -import type { StyleProp, ViewStyle } from 'react-native' - -interface GradientProps { - colors: string[] - start?: { x: number; y: number } - end?: { x: number; y: number } - style?: StyleProp - children?: ReactNode -} - -let cached: FC | null | undefined -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. - * - * `withProps` checks the default export automatically and unwraps to it, mirroring the old - * `findByProps(...).default` lookup from the Classic build. - * - * Deliberately still uses `lookupModule` (which force-initializes uninitialized modules) rather - * than `getModules` here, unlike the other lookups in this plugin: this only ever runs from - * inside `GradientTag`'s render, which only happens post-boot when a gradient tag is actually - * about to be drawn on screen, and the call site needs a component reference synchronously to - * decide what JSX to return - `getModules`' callback-based "maybe later" shape doesn't fit here. - * - * TODO(live-verify): unconfirmed against the current Next/Discord build, see eval-for-revenge. - */ -export function getGradientComponent(): FC | null { - if (attempted) return cached ?? null - attempted = true - - try { - const [startEndMatch] = lookupModule( - withProps>('colors', 'start', 'end'), - ) - const [locationsMatch] = lookupModule( - withProps>('colors', 'locations'), - ) - cached = - (startEndMatch as FC | undefined) ?? - (locationsMatch as FC | undefined) ?? - null - } catch { - cached = null - } - - return cached -} diff --git a/plugins/staff-tags/js/lib/safe.ts b/plugins/staff-tags/js/lib/safe.ts deleted file mode 100644 index 3c91a53..0000000 --- a/plugins/staff-tags/js/lib/safe.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Runs `fn` and swallows any throw, returning `fallback` instead. - * - * `applyPatches`/`safePatch` only guard the synchronous call that *registers* a patch (e.g. - * `getModules(filter, cb)` itself). The `cb` runs later - whenever Discord happens to initialize - * a matching module, which can be deep inside Discord's own boot sequence - completely outside - * that try/catch. An uncaught throw there crashes app startup instead of just skipping a tag. - * Every render-triggered callback (getModules/afterJSX/beforeJSX/afterRendered) needs its own - * guard for this reason. - */ -export function guard(fn: () => T, fallback: T): T { - try { - return fn() - } catch { - return fallback - } -} - -/** - * Adds a style object to an existing `style` prop without assuming its shape. RN style props are - * commonly a single object (not an array) - spreading a non-array/non-iterable value with `[...x]` - * throws, which is what crashed app boot the first time this shipped. - */ -export function mergeStyle( - style: unknown, - patch: Record, -): unknown[] { - if (Array.isArray(style)) return [...style, patch] - if (style) return [style, patch] - return [patch] -} diff --git a/plugins/staff-tags/js/lib/types.ts b/plugins/staff-tags/js/lib/types.ts deleted file mode 100644 index 4af4dd9..0000000 --- a/plugins/staff-tags/js/lib/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface TagOverride { - enabled?: boolean - text?: string - useCustomColor?: boolean - color?: string - useGradient?: boolean - gradientColor?: string -} - -export interface StaffTagsStorage { - useRoleColor: boolean - tags: Record -} diff --git a/plugins/staff-tags/js/patches/chat.ts b/plugins/staff-tags/js/patches/chat.ts deleted file mode 100644 index 6bba9c3..0000000 --- a/plugins/staff-tags/js/patches/chat.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Stores } from '@revenge-mod/discord/flux' -import { getModules } from '@revenge-mod/modules/finders' -import { withName } from '@revenge-mod/modules/finders/filters' -import { instead } from '@revenge-mod/patcher' -import { processColor } from 'react-native' -import getTag from '../lib/getTag' -import { guard } from '../lib/safe' -import type { JsonStorage } from '@revenge-mod/json-storage' -import type { StaffTagsStorage } from '../lib/types' - -interface GetTagPropertiesArgs { - message: { author?: any } - channel?: { guild_id?: string } -} - -interface TagProperties { - tagType?: unknown - tagText?: string - tagTextColor?: unknown - tagBackgroundColor?: unknown - tagVerified?: boolean -} - -/** - * 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. - * - * `getTagProperties` is a plain data function, not a rendered component, so `afterJSX` doesn't - * apply - this stays a module-property patch via `patcher.instead`, same mechanism the Classic - * build used successfully for this exact function. - * - * Uses `getModules` (reacts once Discord itself initializes the module) instead of - * `lookupModule` (which force-initializes uninitialized modules right away) - forcing this - * module to init during `start()`, before Discord is done booting, is what crashed app startup - * the first time this shipped. - * - * `{ returnNamespace: true }` asks for the whole module object (not just the unwrapped default - * export) so `instead` has a `parent`/`key` pair to patch in place. - * - * The `instead` hook is guarded: unlike the JSX patches, it *replaces* the original function for - * every future call, so an uncaught throw here wouldn't just skip one tag - it would break - * message rendering entirely from that point on. - * - * Confirmed live (see /root/evals-for-rn): args are `{message, isSystemDM, channel, colors}` - - * `channel` is handed to us directly, so no `ChannelStore.getChannel()` lookup is needed. - */ -export default function patchChat(storage: JsonStorage) { - let unpatch = () => {} - - const GuildStore = Stores.GuildStore as unknown as { - getGuild(id: string | undefined): any - } - - const unsub = getModules( - withName<(args: GetTagPropertiesArgs) => TagProperties>('getTagProperties'), - TagPropertiesModule => { - guard(() => { - const mod = TagPropertiesModule as unknown as { - default?: (args: GetTagPropertiesArgs) => TagProperties - } - if (!mod.default) return - - unpatch = instead( - mod as { default: (args: GetTagPropertiesArgs) => TagProperties }, - 'default', - ([args], original) => { - // Call through unguarded - if Discord's own function throws, that's not - // something we caused or can meaningfully recover from here. - const ret = original(args) - - // Only our own augmentation is guarded: fall back to the real, unmodified - // result rather than risk breaking every message render from here on. - return guard(() => { - if (ret?.tagType) return ret - - const { message, channel } = args - const guild = GuildStore?.getGuild(channel?.guild_id) - const tag = getTag(storage, guild, channel, message?.author) - if (!tag) return ret - - return { - ...ret, - tagText: tag.text, - tagTextColor: tag.textColor - ? processColor(tag.textColor) - : undefined, - tagBackgroundColor: processColor(tag.backgroundColor), - tagVerified: tag.verified, - tagType: undefined, - } - }, ret) - }, - ) - }, undefined) - }, - { returnNamespace: true }, - ) - - return () => { - unsub() - unpatch() - } -} diff --git a/plugins/staff-tags/js/patches/details.tsx b/plugins/staff-tags/js/patches/details.tsx deleted file mode 100644 index ad7c6e0..0000000 --- a/plugins/staff-tags/js/patches/details.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Stores } from '@revenge-mod/discord/flux' -import { getModules } from '@revenge-mod/modules/finders' -import { afterJSX } from '@revenge-mod/react/jsx-runtime' -import { withMemoDefaultName } from '../lib/filters' -import getTag from '../lib/getTag' -import { guard } from '../lib/safe' -import GradientTag from '../ui/GradientTag' -import type { JsonStorage } from '@revenge-mod/json-storage' -import type { FC, ReactElement } from 'react' -import type { StaffTagsStorage } from '../lib/types' - -interface UserRowProps { - guildId?: string - user: any -} - -/** - * Covers member list rows and the profile popout, where the name and tag sit on the same line. - * - * Multiple distinct `UserRow` implementations can exist in the same build (member list vs. - * 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: }` - `mod.default` is the exact reference - * Discord's own `createElement(UserRow, ...)` calls use, which is what `afterJSX` needs. - * - * Does NOT patch the memo wrapper's shared inner `.type` via `@revenge-mod/patcher`'s generic - * `after()` (tried and confirmed live to be unsound - see git history on this file). That's true - * even for a single, non-stacking, persistent `after()` call: wrapping the shared render function - * in `after()`'s Proxy broke member list rendering outright, every time, even on a fully fresh app - * process with nothing else patched. Whatever `after()` does to make a target "patchable" doesn't - * survive contact with this particular shared function. - * - * Instead, `afterJSX(wrapper, el => ...)` intercepts each `createElement(UserRow, props)` call - * (the per-instance element, never the shared object) and does a **plain, unwrapped property - * assignment** - `el.type = patchedRender` - to a single stable function reused for every row - * (defined once, outside the `afterJSX` callback, not a fresh closure per row). `patchedRender` - * manually calls the real inner render function itself (`innerRender(props)`, safe to call - * directly - React only ever needs the dispatcher active, and it is, since `patchedRender` itself - * is invoked *by* React as `el`'s render function), then augments the returned tree. This avoids - * `after()`'s Proxy machinery entirely while still landing a plain, always-callable function - * (never the non-callable memo wrapper) on `element.type`, so it doesn't hit the earlier - * "TypeError: target is not callable" crash either. Reusing the same function reference across - * every row (rather than a fresh closure per element) keeps `element.type` identity stable across - * re-renders, letting React reconcile these rows normally instead of remounting each one. - * - * `UserRow` renders a single `TableRow` whose name+tag live at `ret.props.label` (a `View`) as a - * **prop**, not somewhere reachable by a generic "find array children containing a string" - * search - that mismatch (not the render-output shape itself) is why tags never appeared even - * before the crash: `findInReactFiber` was searching for a shape that doesn't exist in this tree. - * `label.props.children` is `[nameElement, fragment, existingTagElement]` - confirmed live via - * `/root/evals-for-rn` - where `existingTagElement` is Discord's own `BotTag` component with a - * static `.Types` enum and a numeric `type` prop; ordinary members render it already, with - * `type: 0` (an inactive placeholder, not the "BOT" badge that enum value name suggests). Kept - * `existingTag.props?.type !== 0` as a guard against stacking our own tag next to a real built-in - * badge (bot/official/system), but dropped the old "mutate `existingTag`'s props in place" path - * entirely - confirmed live that `BotTag` derives its displayed label purely from the `type` - * enum (via the same module's `getBotLabel`), ignoring any custom `text`/color props entirely, so - * that branch silently did nothing. Always injecting our own plugin-authored `GradientTag` - * instead (confirmed live to actually render custom text/color) is the only path that works; - * `GradientTag` already branches on its own `gradientColor` prop for solid-vs-gradient, so there's - * no need for the old `TagModule` (Discord's `BotTag` module again, same dead end) fallback. - * - * Uses `getModules` (reacts once Discord itself initializes the module) instead of - * `lookupModule` (which force-initializes uninitialized modules right away) - forcing UserRow 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/ - * initializes the matched module, not inside `applyPatches`' try/catch, which only covers the - * synchronous setup calls - an uncaught throw here would otherwise skip a row's tag at best, or - * (per the shared-target lesson above) risk destabilizing every row's rendering at worst. - */ -export default function patchDetails(storage: JsonStorage) { - const patches: (() => void)[] = [] - - const GuildStore = Stores.GuildStore as unknown as { - getGuild(id: string | undefined): any - } - - const unsubUserRow = getModules( - withMemoDefaultName('UserRow'), - mod => { - guard(() => { - const wrapper = (mod as { default: { type: FC } }).default - const innerRender = wrapper.type - if (typeof innerRender !== 'function') return - - // Call through unguarded - if Discord's own render function throws, that's not - // something we caused or can meaningfully recover from here (same rationale as - // `chat.ts`'s `getTagProperties` patch). Only our own augmentation is guarded. - const patchedRender: FC = props => { - const ret = innerRender(props) - - return guard(() => { - const { guildId, user } = props - if (!user) return ret - - const label = (ret as ReactElement)?.props?.label as - | ReactElement - | undefined - if (!label || !Array.isArray(label.props?.children)) return ret - - // A plain array `.find()`, not `findInReactFiber` - confirmed live that - // `findInReactFiber` doesn't match here. It's built for real React fiber - // nodes (`.child`/`.sibling`/`.return`), not the plain element tree `ret` - // actually is at this point (`innerRender`'s return value, pre-reconciliation - // - createElement() output, not yet a fiber). `label.props.children` is a - // flat, already-known array, so a direct search needs no fiber-walking helper. - const existingTag = ( - label.props.children as { - type?: { Types?: unknown } - props?: any - }[] - ).find(c => c?.type?.Types) - if (existingTag && existingTag.props?.type !== 0) return ret - - const guild = GuildStore?.getGuild(guildId) - const tag = getTag(storage, guild, undefined, user) - if (!tag) return ret - - const container = label.props as { children: unknown[] } - container.children.push( - , - ) - - return ret - }, ret) - } - - patches.push( - afterJSX(wrapper as unknown as FC, el => - guard(() => { - ;(el as { type: unknown }).type = patchedRender - return el - }, el), - ), - ) - }, undefined) - }, - { max: 4 }, - ) - patches.push(unsubUserRow) - - return () => patches.forEach(unpatch => unpatch()) -} diff --git a/plugins/staff-tags/js/patches/name.tsx b/plugins/staff-tags/js/patches/name.tsx deleted file mode 100644 index 3aa70c4..0000000 --- a/plugins/staff-tags/js/patches/name.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { Stores } from '@revenge-mod/discord/flux' -import { getModules } from '@revenge-mod/modules/finders' -import { withName, 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 getTag from '../lib/getTag' -import { guard } from '../lib/safe' -import GradientTag from '../ui/GradientTag' -import type { JsonStorage } from '@revenge-mod/json-storage' -import type { FC } from 'react' -import type { StaffTagsStorage } from '../lib/types' - -interface DisplayNameProps { - guildId?: string - channelId?: string - user: any -} - -interface HeaderNameProps { - channelId?: string -} - -interface TagComponentProps { - type: number - text: string - textColor: string - backgroundColor: string - verified: boolean - style?: any -} - -/** - * 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/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/ - * initializes the matched module, not inside `applyPatches`' try/catch, which only covers the - * synchronous setup calls - an uncaught throw here crashes app startup instead of just skipping - * a name row. - * - * 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) { - const cleanups: (() => void)[] = [] - - let TagModule: { default?: FC } | undefined - const unsubTagModule = getModules( - withProps('default', 'getBotLabel'), - mod => { - TagModule = mod as { default?: FC } - }, - { skipDefault: true }, - ) - cleanups.push(unsubTagModule) - - const GuildStore = Stores.GuildStore as unknown as { - getGuild(id: string | undefined): any - } - const ChannelStore = Stores.ChannelStore as unknown as { - getChannel(id: string | undefined): any - } - - const unsubHeaderName = getModules( - withName>('ChannelHeader'), - ChannelHeader => { - guard(() => { - cleanups.push( - afterJSX(ChannelHeader, el => - guard(() => { - const { channelId } = el.props - const unpatch = afterRendered(el, ret => - guard(() => { - unpatch() - const node = ret as any - if (node?.props) node.props.channelId = channelId - return ret - }, ret), - ) - return el - }, el), - ), - ) - }, undefined) - }, - ) - cleanups.push(unsubHeaderName) - - const unsubDisplayName = getModules( - withName>('DisplayName'), - DisplayName => { - guard(() => { - cleanups.push( - afterJSX(DisplayName, el => - guard(() => { - const { guildId, channelId, user } = el.props - const unpatch = afterRendered(el, ret => - guard(() => { - unpatch() - - const tagComponent = findInReactFiber( - ret as any, - fiberFilter(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 ret - - const guild = GuildStore?.getGuild(guildId) - const channel = ChannelStore?.getChannel(channelId) - const tag = getTag(storage, guild, channel, user) - if (!tag) return ret - - if (tagComponent) { - tagComponent.props = { - type: 0, - text: tag.text, - textColor: tag.textColor, - backgroundColor: tag.backgroundColor, - verified: tag.verified, - } - return ret - } - - const row = findInReactFiber( - ret as any, - fiberFilter(c => c?.props?.style?.flexDirection === 'row'), - ) - if (!Array.isArray(row?.props?.children)) return ret - - if (tag.gradientColor) { - row.props.children.push( - , - ) - } else if (TagModule?.default) { - const Component = TagModule.default - row.props.children.push( - , - ) - } - - return ret - }, ret), - ) - return el - }, el), - ), - ) - }, undefined) - }, - ) - cleanups.push(unsubDisplayName) - - return () => cleanups.forEach(fn => fn()) -} diff --git a/plugins/staff-tags/js/patches/tag.tsx b/plugins/staff-tags/js/patches/tag.tsx deleted file mode 100644 index 22c44d4..0000000 --- a/plugins/staff-tags/js/patches/tag.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { getModules } from '@revenge-mod/modules/finders' -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 { guard, mergeStyle } from '../lib/safe' -import type { FC } from 'react' - -interface TagProps { - text?: string - textColor?: string - backgroundColor?: string -} - -/** - * Discord's own Tag component ignores custom text/color props unless we reach into its - * already-rendered output and push them in ourselves - this is what actually makes the - * text/textColor/backgroundColor props (that `name.tsx`/`details.tsx` inject) visible. - * - * `afterJSX` intercepts every `createElement(Tag, ...)` call globally, regardless of who calls - * it (Discord's own code or ours) and regardless of which chunk captured the reference - - * patching the component's module export directly wouldn't reliably reach Discord's own - * same-chunk render calls. - * - * Uses `getModules` (reacts once Discord itself initializes the module) instead of - * `lookupModule` (which force-initializes uninitialized modules right away to check their - * exports) - forcing this 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/ - * initializes the matched module, not inside `applyPatches`' try/catch, which only covers the - * synchronous setup call above - an uncaught throw here crashes app startup instead of just - * skipping this one tag. - * - * TODO(live-verify): Tag module shape (`default` + `getBotLabel`), see eval-for-revenge. - */ -export default function patchTag() { - let unpatchTag = () => {} - - const unsub = getModules( - withProps<{ default: FC; getBotLabel: unknown }>( - 'default', - 'getBotLabel', - ), - TagModule => { - guard(() => { - const Tag = TagModule.default - if (!Tag) return - - unpatchTag = afterJSX(Tag, el => - guard(() => { - const { text, textColor, backgroundColor } = el.props - if (!text && !textColor && !backgroundColor) return el - - const unpatch = afterRendered(el, ret => - guard(() => { - unpatch() - - const label = findInReactFiber( - ret as any, - fiberFilter(c => typeof c?.props?.children === 'string'), - ) - if (!label) return ret - - if (text) label.props.children = text - if (textColor) - label.props.style = mergeStyle(label.props.style, { - color: textColor, - }) - if (backgroundColor && (ret as any)?.props) { - ;(ret as any).props.style = mergeStyle( - (ret as any).props.style, - { backgroundColor }, - ) - } - - return ret - }, ret), - ) - - return el - }, el), - ) - }, undefined) - }, - { skipDefault: true }, - ) - - return () => { - unsub() - unpatchTag() - } -} diff --git a/plugins/staff-tags/js/ui/ColorInput.tsx b/plugins/staff-tags/js/ui/ColorInput.tsx deleted file mode 100644 index bd03c16..0000000 --- a/plugins/staff-tags/js/ui/ColorInput.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Text, TextInput, TouchableOpacity, View } from 'react-native' -import { isValidHex } from '../lib/color' - -const PRESETS = [ - '#F23F42', - '#F0B232', - '#FEE75C', - '#23A55A', - '#1ABC9C', - '#5865F2', - '#9B59B6', - '#EB459E', - '#99AAB5', - '#FFFFFF', -] - -interface ColorInputProps { - title: string - value?: string - placeholder?: string - onChange: (value: string) => void -} - -/** - * Hex color field with a live swatch preview and tap-to-pick presets. Built entirely from RN - * primitives (no Discord-internal component reuse), so it doesn't carry the same "might silently - * stop rendering after a Discord update" risk as the plugin's UI patches do. - */ -export default function ColorInput({ - title, - value, - placeholder, - onChange, -}: ColorInputProps) { - const valid = !value || isValidHex(value) - const swatchColor = isValidHex(value) - ? value - : isValidHex(placeholder) - ? placeholder - : '#5865F2' - - return ( - - - - {title} - - - - {PRESETS.map(preset => { - const active = value?.toLowerCase() === preset.toLowerCase() - return ( - onChange(preset)} - style={{ - width: 24, - height: 24, - borderRadius: 6, - backgroundColor: preset, - marginRight: 8, - marginBottom: 8, - borderWidth: active ? 2 : 1, - borderColor: active ? '#ffffff' : 'rgba(128,128,128,0.35)', - }} - /> - ) - })} - - {!valid && ( - - Invalid hex color, e.g. #5865F2 - - )} - - ) -} diff --git a/plugins/staff-tags/js/ui/GradientTag.tsx b/plugins/staff-tags/js/ui/GradientTag.tsx deleted file mode 100644 index 43d4e10..0000000 --- a/plugins/staff-tags/js/ui/GradientTag.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Text, View } from 'react-native' -import { getGradientComponent } from '../lib/gradient' -import type { StyleProp, ViewStyle } from 'react-native' - -const baseStyle: ViewStyle = { - flexDirection: 'row', - alignItems: 'center', - borderRadius: 4, - paddingHorizontal: 4, - marginLeft: 4, - overflow: 'hidden', -} - -interface GradientTagProps { - text: string - textColor: string - backgroundColor: string - gradientColor?: string - style?: StyleProp -} - -export default function GradientTag({ - text, - textColor, - backgroundColor, - gradientColor, - style, -}: GradientTagProps) { - const textStyle = { - color: textColor, - fontSize: 11, - fontWeight: '700' as const, - } - const Gradient = gradientColor ? getGradientComponent() : null - - if (Gradient) { - return ( - - {text} - - ) - } - - return ( - - {text} - - ) -} diff --git a/plugins/staff-tags/js/ui/pages/Settings.tsx b/plugins/staff-tags/js/ui/pages/Settings.tsx deleted file mode 100644 index 073741b..0000000 --- a/plugins/staff-tags/js/ui/pages/Settings.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Design } from '@revenge-mod/discord/design' -import { ScrollView, View } from 'react-native' -import { TAG_DEFINITIONS } from '../../lib/getTag' -import ColorInput from '../ColorInput' -import type { PluginApi } from '@revenge-mod/plugins/types' -import type { StaffTagsStorage, TagOverride } from '../../lib/types' - -interface TagSettingsSectionProps { - api: PluginApi<{ jsonStorage: StaffTagsStorage }> - id: string - defaultText: string - defaultColor: string -} - -/** - * `Design` is read inside each component (not destructured at module top level): external - * plugin bundles run via a bare `new Function('revenge', 'plugin', script)` call at preInit with - * no Metro-style dependency ordering, so `revenge.discord.design.Design`'s fields aren't - * guaranteed populated yet at that point. By the time these components actually render (the user - * opened the plugin's settings page), Discord's design system is long since ready. - */ -function TagSettingsSection({ - api, - id, - defaultText, - defaultColor, -}: TagSettingsSectionProps) { - const { TableRowGroup, TableSwitchRow, TextInput } = Design - const storage = api.jsonStorage.use() - const settings: TagOverride = storage?.tags?.[id] ?? {} - const enabled = settings.enabled !== false - - const update = (patch: TagOverride) => { - api.jsonStorage.set({ tags: { [id]: patch } }) - } - - return ( - - update({ enabled: v })} - /> - update({ text: v })} - /> - update({ useCustomColor: v })} - /> - {settings.useCustomColor && ( - update({ color: v })} - /> - )} - update({ useGradient: v })} - /> - {settings.useGradient && ( - update({ gradientColor: v })} - /> - )} - - ) -} - -export default function Settings({ - api, -}: { - api: PluginApi<{ jsonStorage: StaffTagsStorage }> -}) { - const { TableRowGroup, TableSwitchRow } = Design - const storage = api.jsonStorage.use() - - return ( - - - - api.jsonStorage.set({ useRoleColor: v }) - } - /> - - {TAG_DEFINITIONS.map(def => ( - - ))} - - - ) -} diff --git a/plugins/staff-tags/manifest.json b/plugins/staff-tags/manifest.json deleted file mode 100644 index 0822320..0000000 --- a/plugins/staff-tags/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "format": 1, - "id": "dev.everestmcarthur.staff-tags", - "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": "Rosie", - "version": "1.0.10", - "dependencies": { - "revenge.api": { - "version": ">=1 <2" - }, - "discord": { - "version": "*" - } - }, - "dist": { - "script": "index.js" - } -}