diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css
index 971e9a87..c25022a5 100644
--- a/src/components/VencordSettings/settingsStyles.css
+++ b/src/components/VencordSettings/settingsStyles.css
@@ -59,7 +59,7 @@
}
.vc-text-selectable,
-.vc-text-selectable :not(a, button) {
+.vc-text-selectable :not(a, button, a *, button *) {
/* make text selectable, silly discord makes the entirety of settings not selectable */
user-select: text;
diff --git a/src/plugins/textReplace.tsx b/src/plugins/textReplace.tsx
new file mode 100644
index 00000000..73ac1f2f
--- /dev/null
+++ b/src/plugins/textReplace.tsx
@@ -0,0 +1,240 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { DataStore } from "@api/index";
+import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
+import { definePluginSettings } from "@api/settings";
+import { Flex } from "@components/Flex";
+import { Devs } from "@utils/constants";
+import Logger from "@utils/Logger";
+import { useForceUpdater } from "@utils/misc";
+import definePlugin, { OptionType } from "@utils/types";
+import { Button, Forms, React, TextInput, useState } from "@webpack/common";
+
+const STRING_RULES_KEY = "TextReplace_rulesString";
+const REGEX_RULES_KEY = "TextReplace_rulesRegex";
+
+type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>;
+
+interface TextReplaceProps {
+ title: string;
+ rulesArray: Rule[];
+ rulesKey: string;
+}
+
+const makeEmptyRule: () => Rule = () => ({
+ find: "",
+ replace: "",
+ onlyIfIncludes: ""
+});
+const makeEmptyRuleArray = () => [makeEmptyRule()];
+
+let stringRules = makeEmptyRuleArray();
+let regexRules = makeEmptyRuleArray();
+
+const settings = definePluginSettings({
+ replace: {
+ type: OptionType.COMPONENT,
+ description: "",
+ component: () =>
+ <>
+
+
+ >
+ },
+});
+
+function stringToRegex(str: string) {
+ const match = str.match(/^(\/)?(.+?)(?:\/([gimsuy]*))?$/); // Regex to match regex
+ return match
+ ? new RegExp(
+ match[2], // Pattern
+ match[3]
+ ?.split("") // Remove duplicate flags
+ .filter((char, pos, flagArr) => flagArr.indexOf(char) === pos)
+ .join("")
+ ?? "g"
+ )
+ : new RegExp(str); // Not a regex, return string
+}
+
+function renderFindError(find: string) {
+ try {
+ stringToRegex(find);
+ return null;
+ } catch (e) {
+ return (
+
+ {String(e)}
+
+ );
+ }
+}
+
+function Input({ initialValue, onChange, placeholder }: {
+ placeholder: string;
+ initialValue: string;
+ onChange(value: string): void;
+}) {
+ const [value, setValue] = useState(initialValue);
+ return (
+ value !== initialValue && onChange(value)}
+ />
+ );
+}
+
+function TextReplace({ title, rulesArray, rulesKey }: TextReplaceProps) {
+ const isRegexRules = title === "Using Regex";
+
+ const update = useForceUpdater();
+
+ async function onClickRemove(index: number) {
+ rulesArray.splice(index, 1);
+
+ await DataStore.set(rulesKey, rulesArray);
+ update();
+ }
+
+ async function onChange(e: string, index: number, key: string) {
+ if (index === rulesArray.length - 1)
+ rulesArray.push(makeEmptyRule());
+
+ rulesArray[index][key] = e;
+
+ if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1)
+ rulesArray.splice(index, 1);
+
+ await DataStore.set(rulesKey, rulesArray);
+ update();
+ }
+
+ return (
+ <>
+ {title}
+
+ {
+ rulesArray.map((rule, index) =>
+
+
+
+ onChange(e, index, "find")}
+ />
+ onChange(e, index, "replace")}
+ />
+ onChange(e, index, "onlyIfIncludes")}
+ />
+
+
+
+ {isRegexRules && renderFindError(rule.find)}
+
+ )
+ }
+
+ >
+ );
+}
+
+export default definePlugin({
+ name: "TextReplace",
+ description: "Replace text in your messages",
+ authors: [Devs.Samu, Devs.AutumnVN],
+ dependencies: ["MessageEventsAPI"],
+
+ settings,
+
+ async start() {
+ stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray();
+ regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray();
+
+ this.preSend = addPreSendListener((_, msg) => {
+ // pad so that rules can use " word " to only match whole "word"
+ msg.content = " " + msg.content + " ";
+
+ if (stringRules) {
+ for (const rule of stringRules) {
+ if (!rule.find || !rule.replace) continue;
+ if (rule.onlyIfIncludes && !msg.content.includes(rule.onlyIfIncludes)) continue;
+
+ msg.content = msg.content.replaceAll(rule.find, rule.replace);
+ }
+ }
+
+ if (regexRules) {
+ for (const rule of regexRules) {
+ if (!rule.find || !rule.replace) continue;
+ if (rule.onlyIfIncludes && !msg.content.includes(rule.onlyIfIncludes)) continue;
+
+ try {
+ const regex = stringToRegex(rule.find);
+ msg.content = msg.content.replace(regex, rule.replace);
+ } catch (e) {
+ new Logger("TextReplace").error(`Invalid regex: ${rule.find}`);
+ }
+ }
+ }
+
+ msg.content = msg.content.trim();
+ });
+ },
+
+ stop() {
+ removePreSendListener(this.preSend);
+ }
+});