From 8d6bff6c45b3ee3f3c4bdc335603c66764a9cafc Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Wed, 10 Sep 2025 07:36:02 +0100 Subject: [PATCH] add tone indicators plugin (#378) * add tone indicators plugin * im tired of this man * oh my fucking god its EquicordDevs not Devs * dont use json * Update index.tsx --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> Co-authored-by: justjxke --- .../toneIndicators/ToneIndicator.tsx | 34 +++++ src/equicordplugins/toneIndicators/index.tsx | 142 ++++++++++++++++++ .../toneIndicators/indicators.ts | 62 ++++++++ src/utils/constants.ts | 4 + 4 files changed, 242 insertions(+) create mode 100644 src/equicordplugins/toneIndicators/ToneIndicator.tsx create mode 100644 src/equicordplugins/toneIndicators/index.tsx create mode 100644 src/equicordplugins/toneIndicators/indicators.ts diff --git a/src/equicordplugins/toneIndicators/ToneIndicator.tsx b/src/equicordplugins/toneIndicators/ToneIndicator.tsx new file mode 100644 index 000000000..99d5a4ed0 --- /dev/null +++ b/src/equicordplugins/toneIndicators/ToneIndicator.tsx @@ -0,0 +1,34 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Tooltip } from "@webpack/common"; +import type { ReactElement } from "react"; + +export interface ToneIndicatorProps { + indicator: string; + desc: string; +} + +export default function ToneIndicator({ + indicator, + desc, +}: ToneIndicatorProps): ReactElement { + return ( + + {tooltipProps => ( + + /{indicator} + + )} + + ); +} diff --git a/src/equicordplugins/toneIndicators/index.tsx b/src/equicordplugins/toneIndicators/index.tsx new file mode 100644 index 000000000..b70bc0d4a --- /dev/null +++ b/src/equicordplugins/toneIndicators/index.tsx @@ -0,0 +1,142 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { React } from "@webpack/common"; +import { type ReactNode } from "react"; + +import indicatorsDefault from "./indicators"; +import ToneIndicator from "./ToneIndicator"; + +const settings = definePluginSettings({ + customIndicators: { + type: OptionType.STRING, + description: "Custom tone indicators (format: jk=Joking; srs=Serious)", + default: "", + }, + maxIndicators: { + type: OptionType.NUMBER, + description: "Maximum number of tone indicators to show per message", + default: 5, + }, +}); + +function getCustomIndicators(): Record { + const raw = settings.store.customIndicators || ""; + const result: Record = {}; + + raw.split("; ").forEach(entry => { + const [key, ...rest] = entry.split("="); + if (key && rest.length > 0) { + result[key.trim().toLowerCase()] = rest.join("=").trim(); + } + }); + + return result; +} + +function getIndicator(text: string): string | null { + text = text.toLowerCase(); + const customIndicators = getCustomIndicators(); + + return ( + customIndicators[text] || + customIndicators[`_${text}`] || + indicatorsDefault.get(text) || + indicatorsDefault.get(`_${text}`) || + null + ); +} + +function splitTextWithIndicators(text: string): ReactNode[] { + const nodes: ReactNode[] = []; + let lastIndex = 0; + let count = 0; + const regex = /\/([a-z]{1,20})(?=\s|$|[^\w])/gi; + let match: RegExpExecArray | null; + + while ((match = regex.exec(text)) && count < settings.store.maxIndicators) { + const indicator = match[1]; + const desc = getIndicator(indicator); + if (!desc) continue; + + if (match.index > lastIndex) { + nodes.push(text.slice(lastIndex, match.index)); + } + + nodes.push( + , + ); + + lastIndex = regex.lastIndex; + count++; + } + + if (lastIndex < text.length) nodes.push(text.slice(lastIndex)); + + return nodes; +} + +function patchChildrenTree(children: any): any { + const transform = (node: any): any => { + if (node == null) return node; + + if (typeof node === "string") { + if (!/\/[a-z]+/i.test(node)) return node; + const parts = splitTextWithIndicators(node); + return parts.length === 1 ? parts[0] : parts; + } + + if (node?.props?.children != null) { + const c = node.props.children; + if (Array.isArray(c)) { + node.props.children = c.map(transform).flat(); + } else { + node.props.children = transform(c); + } + return node; + } + + return node; + }; + + if (Array.isArray(children)) return children.map(transform).flat(); + return transform(children); +} + +export default definePlugin({ + name: "ToneIndicators", + description: "Show tooltips for tone indicators like /srs, /gen, etc. in sent messages.", + authors: [EquicordDevs.justjxke], + settings, + + patches: [ + { + find: '["strong","em","u","text","inlineCode","s","spoiler"]', + replacement: [ + { + match: /(?=return\{hasSpoilerEmbeds:\i,content:(\i)\})/, + replace: (_: any, content: string) => + `${content}=$self.patchToneIndicators(${content});`, + }, + ], + }, + ], + + patchToneIndicators(content: any): any { + try { + return patchChildrenTree(content); + } catch { + return content; + } + }, +}); diff --git a/src/equicordplugins/toneIndicators/indicators.ts b/src/equicordplugins/toneIndicators/indicators.ts new file mode 100644 index 000000000..e03a45015 --- /dev/null +++ b/src/equicordplugins/toneIndicators/indicators.ts @@ -0,0 +1,62 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +const defaultIndicators = new Map( + Object.entries({ + j: "Joking", + hj: "Half Joking", + s: "Sarcastic", + _sarc: "Sarcastic", // Alias for s + gen: "Genuine", + _g: "Genuine", // Alias for gen, + genq: "Genuine Question", + _gq: "Genuine Question", // Alias for genq + srs: "Serious", + nsrs: "Non-Serious", + pos: "Positive Connotation", + _pc: "Positive Connotation", // Alias for pos + neu: "Neutral Connotation", + neg: "Negative Connotation", + _nc: "Negative Connotation", // Alias for neg + p: "Platonic", + r: "Romantic", + a: "Alterous", + c: "Copypasta", + l: "Lyrics", + _ly: "Lyrics", // Alias for l + lh: "Light-Hearted", + nm: "Not Mad", + lu: "A Little Upset", + nbh: "Not Directed At Anybody Here", + nay: "Not Directed At You", + ay: "Directed At You", + nsb: "Not Subtweeting", + x: "Sexual Intent", + _sx: "Sexual Intent", // Alias for x + nx: "Non-Sexual Intent", + _nsx: "Non-Sexual Intent", // Alias for nx + rh: "Rhetorical Question", + _rt: "Rhetorical Question", // Alias for rh + t: "Teasing", + ij: "Inside Joke", + m: "Metaphorically", + li: "Literally", + hyp: "Hyperbole", + ex: "Exaggeration", + f: "Fake", + q: "Quote", + th: "Threat", + cb: "Clickbait", + ref: "Reference", + nf: "Not Forced", + nbr: "Not Being Rude", + ot: "Off-Topic", + nj: "Not Joking", + naj: "Not A Joke", + }), +); + +export default defaultIndicators; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 81425d7d1..bfb5a4bc3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1145,6 +1145,10 @@ export const EquicordDevs = Object.freeze({ name: "Mocha", id: 808802000224518264n }, + justjxke: { + name: "justjxke", + id: 852558183087472640n + } } satisfies Record); // iife so #__PURE__ works correctly