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 <Jake0300@protonmai.com>
This commit is contained in:
Jake 2025-09-10 07:36:02 +01:00 • committed by GitHub
parent 7167aead16
commit 8d6bff6c45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 242 additions and 0 deletions

View file

@ -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 (
<Tooltip text={desc}>
{tooltipProps => (
<span
{...tooltipProps}
style={{
color: "var(--text-normal)",
userSelect: "text",
}}
>
/{indicator}
</span>
)}
</Tooltip>
);
}

View file

@ -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<string, string> {
const raw = settings.store.customIndicators || "";
const result: Record<string, string> = {};
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(
<ToneIndicator
key={`ti-${match.index}`}
indicator={indicator}
desc={desc}
/>,
);
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;
}
},
});

View file

@ -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;

View file

@ -1145,6 +1145,10 @@ export const EquicordDevs = Object.freeze({
name: "Mocha",
id: 808802000224518264n
},
justjxke: {
name: "justjxke",
id: 852558183087472640n
}
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly