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