diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index f16f1e27..e86effb1 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,11 +1,9 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
- "eamodio.gitlens",
"EditorConfig.EditorConfig",
- "ExodiusStudios.comment-anchors",
- "formulahendry.auto-rename-tag",
"GregorBiswanger.json2ts",
- "stylelint.vscode-stylelint"
+ "stylelint.vscode-stylelint",
+ "Vendicated.vencord-companion"
]
}
diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx
index 064c872a..9e2980e7 100644
--- a/src/components/VencordSettings/PatchHelperTab.tsx
+++ b/src/components/VencordSettings/PatchHelperTab.tsx
@@ -180,7 +180,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
return (
<>
- replacement
+ {/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
+ replacement
{!isFunc && (
-
Cheat Sheet
+
Cheat Sheet
{Object.entries({
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
"$$": "Insert a $",
@@ -220,11 +221,12 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
interface FullPatchInputProps {
setFind(v: string): void;
+ setParsedFind(v: string | RegExp): void;
setMatch(v: string): void;
setReplacement(v: string | ReplaceFn): void;
}
-function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputProps) {
+function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
const [fullPatch, setFullPatch] = React.useState
("");
const [fullPatchError, setFullPatchError] = React.useState("");
@@ -233,6 +235,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
setFullPatchError("");
setFind("");
+ setParsedFind("");
setMatch("");
setReplacement("");
return;
@@ -256,7 +259,8 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
- setFind(parsed.find);
+ setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
+ setParsedFind(parsed.find);
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
setReplacement(parsed.replacement.replace);
setFullPatchError("");
@@ -266,7 +270,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
}
return <>
- Paste your full JSON patch here to fill out the fields
+ Paste your full JSON patch here to fill out the fields
{fullPatchError !== "" && {fullPatchError}}
>;
@@ -274,6 +278,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
function PatchHelper() {
const [find, setFind] = React.useState("");
+ const [parsedFind, setParsedFind] = React.useState("");
const [match, setMatch] = React.useState("");
const [replacement, setReplacement] = React.useState("");
@@ -285,20 +290,34 @@ function PatchHelper() {
const code = React.useMemo(() => {
return `
{
- find: ${JSON.stringify(find)},
+ find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
replacement: {
match: /${match.replace(/(?full patch
- find
+ find
- match
+ match
+
;
+
export default definePlugin({
name: "Settings",
description: "Adds Settings UI and debug info",
@@ -41,34 +44,18 @@ export default definePlugin({
find: ".versionHash",
replacement: [
{
- match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
+ match: /\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
replace: (m, component, props) => {
props = props.replace(/children:\[.+\]/, "");
return `${m},$self.makeInfoElements(${component}, ${props})`;
}
+ },
+ {
+ match: /copyValue:\i\.join\(" "\)/,
+ replace: "$& + $self.getInfoString()"
}
]
},
- // Discord Stable
- // FIXME: remove once change merged to stable
- {
- find: "Messages.ACTIVITY_SETTINGS",
- replacement: {
- get match() {
- switch (Settings.plugins.Settings.settingsLocation) {
- case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
- case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
- case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
- case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
- case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
- case "aboveActivity":
- default:
- return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
- }
- },
- replace: "...$self.makeSettingsCategories($1),$&"
- }
- },
// Discord Canary
{
find: "Messages.ACTIVITY_SETTINGS",
@@ -77,6 +64,13 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}
},
+ {
+ find: "useDefaultUserSettingsSections:function",
+ replacement: {
+ match: /(?<=useDefaultUserSettingsSections:function\(\){return )(\i)\}/,
+ replace: "$self.wrapSettingsHook($1)}"
+ }
+ },
{
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
@@ -86,9 +80,9 @@ export default definePlugin({
}
],
- customSections: [] as ((SectionTypes: Record) => any)[],
+ customSections: [] as ((SectionTypes: SectionTypes) => any)[],
- makeSettingsCategories(SectionTypes: Record) {
+ makeSettingsCategories(SectionTypes: SectionTypes) {
return [
{
section: SectionTypes.HEADER,
@@ -154,6 +148,8 @@ export default definePlugin({
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
+ if (!header) return;
+
const names = {
top: i18n.Messages.USER_SETTINGS,
aboveNitro: i18n.Messages.BILLING_SETTINGS,
@@ -163,12 +159,30 @@ export default definePlugin({
return header === names[settingsLocation];
},
- addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: Record) {
- if (!this.isRightSpot(element)) return;
+ patchedSettings: new WeakSet(),
+
+ addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: SectionTypes) {
+ if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return;
+
+ this.patchedSettings.add(elements);
elements.push(...this.makeSettingsCategories(sectionTypes));
},
+ wrapSettingsHook(originalHook: (...args: any[]) => Record[]) {
+ return (...args: any[]) => {
+ const elements = originalHook(...args);
+ if (!this.patchedSettings.has(elements))
+ elements.unshift(...this.makeSettingsCategories({
+ HEADER: "HEADER",
+ DIVIDER: "DIVIDER",
+ CUSTOM: "CUSTOM"
+ }));
+
+ return elements;
+ };
+ },
+
options: {
settingsLocation: {
type: OptionType.SELECT,
@@ -207,15 +221,24 @@ export default definePlugin({
return "";
},
- makeInfoElements(Component: React.ComponentType, props: React.PropsWithChildren) {
+ getInfoRows() {
const { electronVersion, chromiumVersion, additionalInfo } = this;
- return (
- <>
- Vencord {gitHash}{additionalInfo}
- {electronVersion && Electron {electronVersion}}
- {chromiumVersion && Chromium {chromiumVersion}}
- >
+ const rows = [`Vencord ${gitHash}${additionalInfo}`];
+
+ if (electronVersion) rows.push(`Electron ${electronVersion}`);
+ if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);
+
+ return rows;
+ },
+
+ getInfoString() {
+ return "\n" + this.getInfoRows().join("\n");
+ },
+
+ makeInfoElements(Component: React.ComponentType, props: React.PropsWithChildren) {
+ return this.getInfoRows().map((text, i) =>
+ {text}
);
}
});
diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx
index d252682f..795f1990 100644
--- a/src/plugins/betterFolders/index.tsx
+++ b/src/plugins/betterFolders/index.tsx
@@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
-import { FluxDispatcher, i18n } from "@webpack/common";
+import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
@@ -117,8 +117,8 @@ export default definePlugin({
},
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
{
- match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
- replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},arguments[0].betterFoldersExpandedIds)`
+ match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
+ replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
},
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{
@@ -252,19 +252,21 @@ export default definePlugin({
}
},
- getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set) {
- if (!isBetterFolders || expandedFolderIds == null) return oldTree;
+ getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set) {
+ return useMemo(() => {
+ if (!isBetterFolders || expandedFolderIds == null) return originalTree;
- const newTree = new GuildsTree();
- // Children is every folder and guild which is not in a folder, this filters out only the expanded folders
- newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
- // Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
- newTree.nodes = Object.fromEntries(
- Object.entries(oldTree.nodes)
- .filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
- );
+ const newTree = new GuildsTree();
+ // Children is every folder and guild which is not in a folder, this filters out only the expanded folders
+ newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
+ // Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
+ newTree.nodes = Object.fromEntries(
+ Object.entries(originalTree.nodes)
+ .filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
+ );
- return newTree;
+ return newTree;
+ }, [isBetterFolders, originalTree, expandedFolderIds]);
},
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx
index 5064bd53..e90e5c82 100644
--- a/src/plugins/betterSettings/index.tsx
+++ b/src/plugins/betterSettings/index.tsx
@@ -119,7 +119,7 @@ export default definePlugin({
{ // Settings cog context menu
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
- match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
+ match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$self.wrapMenu($&)"
}
}
diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts
index 10053021..3297ca30 100644
--- a/src/plugins/crashHandler/index.ts
+++ b/src/plugins/crashHandler/index.ts
@@ -24,22 +24,20 @@ import { closeAllModals } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { maybePromptToUpdate } from "@utils/updater";
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
-import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
+import { DraftType, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
const CrashHandlerLogger = new Logger("CrashHandler");
-const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
- const modules = findBulk(
+
+const { ModalStack, DraftManager, closeExpressionPicker } = proxyLazyWebpack(() => {
+ const [ModalStack, DraftManager, ExpressionManager] = findBulk(
filters.byProps("pushLazy", "popAll"),
filters.byProps("clearDraft", "saveDraft"),
- filters.byProps("DraftType"),
- filters.byProps("closeExpressionPicker", "openExpressionPicker"),
- );
+ filters.byProps("closeExpressionPicker", "openExpressionPicker"),);
return {
- ModalStack: modules[0],
- DraftManager: modules[1],
- DraftType: modules[2]?.DraftType,
- closeExpressionPicker: modules[3]?.closeExpressionPicker,
+ ModalStack,
+ DraftManager,
+ closeExpressionPicker: ExpressionManager?.closeExpressionPicker,
};
});
@@ -137,8 +135,11 @@ export default definePlugin({
try {
const channelId = SelectedChannelStore.getChannelId();
- DraftManager.clearDraft(channelId, DraftType.ChannelMessage);
- DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage);
+ for (const key in DraftType) {
+ if (!Number.isNaN(Number(key))) continue;
+
+ DraftManager.clearDraft(channelId, DraftType[key]);
+ }
} catch (err) {
CrashHandlerLogger.debug("Failed to clear drafts.", err);
}
diff --git a/src/plugins/ctrlEnterSend/index.ts b/src/plugins/ctrlEnterSend/index.ts
new file mode 100644
index 00000000..4b9dd8e0
--- /dev/null
+++ b/src/plugins/ctrlEnterSend/index.ts
@@ -0,0 +1,68 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2023 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import { Devs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+
+export default definePlugin({
+ name: "CtrlEnterSend",
+ authors: [Devs.UlyssesZhan],
+ description: "Use Ctrl+Enter to send messages (customizable)",
+ settings: definePluginSettings({
+ submitRule: {
+ description: "The way to send a message",
+ type: OptionType.SELECT,
+ options: [
+ {
+ label: "Ctrl+Enter (Enter or Shift+Enter for new line)",
+ value: "ctrl+enter"
+ },
+ {
+ label: "Shift+Enter (Enter for new line)",
+ value: "shift+enter"
+ },
+ {
+ label: "Enter (Shift+Enter for new line; Discord default)",
+ value: "enter"
+ }
+ ],
+ default: "ctrl+enter"
+ },
+ sendMessageInTheMiddleOfACodeBlock: {
+ description: "Whether to send a message in the middle of a code block",
+ type: OptionType.BOOLEAN,
+ default: true,
+ }
+ }),
+ patches: [
+ {
+ find: "KeyboardKeys.ENTER&&(!",
+ replacement: {
+ match: /(?<=(\i)\.which===\i\.KeyboardKeys.ENTER&&).{0,100}(\(0,\i\.hasOpenPlainTextCodeBlock\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
+ replace: "$self.shouldSubmit($1, $2)"
+ }
+ }
+ ],
+ shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean {
+ let result = false;
+ switch (this.settings.store.submitRule) {
+ case "shift+enter":
+ result = event.shiftKey;
+ break;
+ case "ctrl+enter":
+ result = event.ctrlKey;
+ break;
+ case "enter":
+ result = !event.shiftKey && !event.ctrlKey;
+ break;
+ }
+ if (!this.settings.store.sendMessageInTheMiddleOfACodeBlock) {
+ result &&= !codeblock;
+ }
+ return result;
+ }
+});
diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx
index 29318366..a55a7771 100644
--- a/src/plugins/fakeNitro/index.tsx
+++ b/src/plugins/fakeNitro/index.tsx
@@ -24,13 +24,12 @@ import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
-import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
+import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { CustomEmoji } from "@webpack/types";
import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
-const DRAFT_TYPE = 0;
const StickerStore = findStoreLazy("StickersStore") as {
getPremiumPacks(): StickerPack[];
getAllGuildStickers(): Map;
@@ -807,7 +806,7 @@ export default definePlugin({
gif.finish();
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
- UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
+ UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);
},
canUseEmote(e: CustomEmoji, channelId: string) {
diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx
index df47e545..1257b452 100644
--- a/src/plugins/moreUserTags/index.tsx
+++ b/src/plugins/moreUserTags/index.tsx
@@ -50,6 +50,7 @@ interface TagSettings {
MODERATOR_STAFF: TagSetting,
MODERATOR: TagSetting,
VOICE_MODERATOR: TagSetting,
+ TRIAL_MODERATOR: TagSetting,
[k: string]: TagSetting;
}
@@ -93,6 +94,11 @@ const tags: Tag[] = [
displayName: "VC Mod",
description: "Can manage voice chats",
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
+ }, {
+ name: "CHAT_MODERATOR",
+ displayName: "Chat Mod",
+ description: "Can timeout people",
+ permissions: ["MODERATE_MEMBERS"]
}
];
const defaultSettings = Object.fromEntries(
@@ -263,34 +269,14 @@ export default definePlugin({
],
start() {
- if (settings.store.tagSettings) return;
- // @ts-ignore
- if (!settings.store.visibility_WEBHOOK) settings.store.tagSettings = defaultSettings;
- else {
- const newSettings = { ...defaultSettings };
- Object.entries(Vencord.PlainSettings.plugins.MoreUserTags).forEach(([name, value]) => {
- const [setting, tag] = name.split("_");
- if (setting === "visibility") {
- switch (value) {
- case "always":
- // its the default
- break;
- case "chat":
- newSettings[tag].showInNotChat = false;
- break;
- case "not-chat":
- newSettings[tag].showInChat = false;
- break;
- case "never":
- newSettings[tag].showInChat = false;
- newSettings[tag].showInNotChat = false;
- break;
- }
- }
- settings.store.tagSettings = newSettings;
- delete Vencord.Settings.plugins.MoreUserTags[name];
- });
- }
+ settings.store.tagSettings ??= defaultSettings;
+
+ // newly added field might be missing from old users
+ settings.store.tagSettings.CHAT_MODERATOR ??= {
+ text: "Chat Mod",
+ showInChat: true,
+ showInNotChat: true
+ };
},
getPermissions(user: User, channel: Channel): string[] {
diff --git a/src/plugins/noServerEmojis/index.ts b/src/plugins/noServerEmojis/index.ts
new file mode 100644
index 00000000..ed843769
--- /dev/null
+++ b/src/plugins/noServerEmojis/index.ts
@@ -0,0 +1,50 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2023 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import { Devs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+
+const settings = definePluginSettings({
+ shownEmojis: {
+ description: "The types of emojis to show in the autocomplete menu.",
+ type: OptionType.SELECT,
+ default: "onlyUnicode",
+ options: [
+ { label: "Only unicode emojis", value: "onlyUnicode" },
+ { label: "Unicode emojis and server emojis from current server", value: "currentServer" },
+ { label: "Unicode emojis and all server emojis (Discord default)", value: "all" }
+ ]
+ }
+});
+
+export default definePlugin({
+ name: "NoServerEmojis",
+ authors: [Devs.UlyssesZhan],
+ description: "Do not show server emojis in the autocomplete menu.",
+ settings,
+ patches: [
+ {
+ find: "}searchWithoutFetchingLatest(",
+ replacement: {
+ match: /searchWithoutFetchingLatest.{20,300}get\((\i).{10,40}?reduce\(\((\i),(\i)\)=>\{/,
+ replace: "$& if ($self.shouldSkip($1, $3)) return $2;"
+ }
+ }
+ ],
+ shouldSkip(guildId: string, emoji: any) {
+ if (emoji.type !== "GUILD_EMOJI") {
+ return false;
+ }
+ if (settings.store.shownEmojis === "onlyUnicode") {
+ return true;
+ }
+ if (settings.store.shownEmojis === "currentServer") {
+ return emoji.guildId !== guildId;
+ }
+ return false;
+ }
+});
diff --git a/src/plugins/openInApp/index.ts b/src/plugins/openInApp/index.ts
index 0835c061..83da5f3c 100644
--- a/src/plugins/openInApp/index.ts
+++ b/src/plugins/openInApp/index.ts
@@ -26,6 +26,7 @@ const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
+const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/;
const settings = definePluginSettings({
spotify: {
@@ -42,6 +43,11 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN,
description: "Open Epic Games links in the Epic Games Launcher",
default: true,
+ },
+ tidal: {
+ type: OptionType.BOOLEAN,
+ description: "Open Tidal links in the Tidal app",
+ default: true,
}
});
@@ -49,7 +55,7 @@ const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
+ setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
},
},
]
diff --git a/src/plugins/startupTimings/index.tsx b/src/plugins/startupTimings/index.tsx
index 742d822a..cf366df3 100644
--- a/src/plugins/startupTimings/index.tsx
+++ b/src/plugins/startupTimings/index.tsx
@@ -26,10 +26,12 @@ export default definePlugin({
description: "Adds Startup Timings to the Settings menu",
authors: [Devs.Megu],
patches: [{
- find: "UserSettingsSections.PAYMENT_FLOW_MODAL_TEST_PAGE,",
+ find: "Messages.ACTIVITY_SETTINGS",
replacement: {
- match: /{section:\i\.UserSettingsSections\.PAYMENT_FLOW_MODAL_TEST_PAGE/,
- replace: '{section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage},$&'
+ match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\))/,
+ replace: (_, commaOrSemi, settings, elements) => "" +
+ `${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
+ `&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`
}
}],
StartupTimingPage
diff --git a/src/plugins/themeAttributes/README.md b/src/plugins/themeAttributes/README.md
index 87cb803c..89001aae 100644
--- a/src/plugins/themeAttributes/README.md
+++ b/src/plugins/themeAttributes/README.md
@@ -1,6 +1,6 @@
# ThemeAttributes
-This plugin adds data attributes to various elements inside Discord
+This plugin adds data attributes and CSS variables to various elements inside Discord
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
@@ -19,3 +19,11 @@ This allows themes to more easily theme those elements or even do things that ot
- `data-is-self` is a boolean indicating whether this is the current user's message
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
+
+## CSS Variables
+
+### Avatars
+
+`--avatar-url-` contains a URL for the users avatar with the size attribute adjusted for the resolutions `128, 256, 512, 1024, 2048, 4096`.
+
+![image](https://github.com/Vendicated/Vencord/assets/26598490/192ddac0-c827-472f-9933-fa99ff36f723)
diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts
index b8ceac62..b8084454 100644
--- a/src/plugins/themeAttributes/index.ts
+++ b/src/plugins/themeAttributes/index.ts
@@ -9,10 +9,11 @@ import definePlugin from "@utils/types";
import { UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
+
export default definePlugin({
name: "ThemeAttributes",
description: "Adds data attributes to various elements for theming purposes",
- authors: [Devs.Ven],
+ authors: [Devs.Ven, Devs.Board],
patches: [
// Add data-tab-id to all tab bar items
@@ -32,9 +33,36 @@ export default definePlugin({
match: /\.messageListItem(?=,"aria)/,
replace: "$&,...$self.getMessageProps(arguments[0])"
}
+ },
+
+ // add --avatar-url- css variable to avatar img elements
+ // popout profiles
+ {
+ find: ".LABEL_WITH_ONLINE_STATUS",
+ replacement: {
+ match: /src:null!=\i\?(\i).{1,50}"aria-hidden":!0/,
+ replace: "$&,style:$self.getAvatarStyles($1)"
+ }
+ },
+ // chat avatars
+ {
+ find: "showCommunicationDisabledStyles",
+ replacement: {
+ match: /src:(\i),"aria-hidden":!0/,
+ replace: "$&,style:$self.getAvatarStyles($1)"
+ }
}
],
+ getAvatarStyles(src: string) {
+ return Object.fromEntries(
+ [128, 256, 512, 1024, 2048, 4096].map(size => [
+ `--avatar-url-${size}`,
+ `url(${src.replace(/\d+$/, String(size))})`
+ ])
+ );
+ },
+
getMessageProps(props: { message: Message; }) {
const author = props.message?.author;
const authorId = author?.id;
diff --git a/src/plugins/validReply/README.md b/src/plugins/validReply/README.md
new file mode 100644
index 00000000..49e313cf
--- /dev/null
+++ b/src/plugins/validReply/README.md
@@ -0,0 +1,7 @@
+# ValidReply
+
+Fixes referenced (replied to) messages showing as "Message could not be loaded".
+
+Hover the text to load the message!
+
+![](https://github.com/Vendicated/Vencord/assets/45801973/d3286acf-e822-4b7f-a4e7-8ced18f581af)
diff --git a/src/plugins/validReply/index.ts b/src/plugins/validReply/index.ts
new file mode 100644
index 00000000..21a1bdd1
--- /dev/null
+++ b/src/plugins/validReply/index.ts
@@ -0,0 +1,106 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { FluxDispatcher, RestAPI } from "@webpack/common";
+import { Message, User } from "discord-types/general";
+import { Channel } from "discord-types/general/index.js";
+
+const enum ReferencedMessageState {
+ Loaded,
+ NotLoaded,
+ Deleted
+}
+
+interface Reply {
+ baseAuthor: User,
+ baseMessage: Message;
+ channel: Channel;
+ referencedMessage: { state: ReferencedMessageState; };
+ compact: boolean;
+ isReplyAuthorBlocked: boolean;
+}
+
+const fetching = new Map();
+let ReplyStore: any;
+
+const { createMessageRecord } = findByPropsLazy("createMessageRecord");
+
+export default definePlugin({
+ name: "ValidReply",
+ description: 'Fixes "Message could not be loaded" upon hovering over the reply',
+ authors: [Devs.newwares],
+ patches: [
+ {
+ find: "Messages.REPLY_QUOTE_MESSAGE_NOT_LOADED",
+ replacement: {
+ match: /Messages\.REPLY_QUOTE_MESSAGE_NOT_LOADED/,
+ replace: "$&,onMouseEnter:()=>$self.fetchReply(arguments[0])"
+ }
+ },
+ {
+ find: "ReferencedMessageStore",
+ replacement: {
+ match: /constructor\(\)\{\i\(this,"_channelCaches",new Map\)/,
+ replace: "$&;$self.setReplyStore(this);"
+ }
+ }
+ ],
+
+ setReplyStore(store: any) {
+ ReplyStore = store;
+ },
+
+ async fetchReply(reply: Reply) {
+ const { channel_id: channelId, message_id: messageId } = reply.baseMessage.messageReference!;
+
+ if (fetching.has(messageId)) {
+ return;
+ }
+ fetching.set(messageId, channelId);
+
+ RestAPI.get({
+ url: `/channels/${channelId}/messages`,
+ query: {
+ limit: 1,
+ around: messageId
+ },
+ retries: 2
+ })
+ .then(res => {
+ const reply: Message | undefined = res?.body?.[0];
+ if (!reply) return;
+
+ if (reply.id !== messageId) {
+ ReplyStore.set(channelId, messageId, {
+ state: ReferencedMessageState.Deleted
+ });
+
+ FluxDispatcher.dispatch({
+ type: "MESSAGE_DELETE",
+ channelId: channelId,
+ message: messageId
+ });
+ } else {
+ ReplyStore.set(reply.channel_id, reply.id, {
+ state: ReferencedMessageState.Loaded,
+ message: createMessageRecord(reply)
+ });
+
+ FluxDispatcher.dispatch({
+ type: "MESSAGE_UPDATE",
+ message: reply
+ });
+ }
+ })
+ .catch(() => { })
+ .finally(() => {
+ fetching.delete(messageId);
+ });
+ }
+});
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index e446a27b..a77edf7d 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -378,10 +378,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ProffDea",
id: 609329952180928513n
},
+ UlyssesZhan: {
+ name: "UlyssesZhan",
+ id: 586808226058862623n
+ },
ant0n: {
name: "ant0n",
id: 145224646868860928n
},
+ Board: {
+ name: "BoardTM",
+ id: 285475344817848320n,
+ },
philipbry: {
name: "philipbry",
id: 554994003318276106n
@@ -477,7 +485,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({
xocherry: {
name: "xocherry",
id: 221288171013406720n
- }
+ },
+ ScattrdBlade: {
+ name: "ScattrdBlade",
+ id: 678007540608532491n
+ },
} satisfies Record);
// iife so #__PURE__ works correctly
diff --git a/src/utils/types.ts b/src/utils/types.ts
index bec7cb0b..8f2ee863 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -29,14 +29,19 @@ export default function definePlugin(p: P & Record string;
export interface PatchReplacement {
+ /** The match for the patch replacement. If you use a string it will be implicitly converted to a RegExp */
match: string | RegExp;
+ /** The replacement string or function which returns the string for the patch replacement */
replace: string | ReplaceFn;
+ /** A function which returns whether this patch replacement should be applied */
predicate?(): boolean;
}
export interface Patch {
plugin: string;
- find: string;
+ /** A string or RegExp which is only include/matched in the module code you wish to patch. Prefer only using a RegExp if a simple string test is not enough */
+ find: string | RegExp;
+ /** The replacement(s) for the module being patched */
replacement: PatchReplacement | PatchReplacement[];
/** Whether this patch should apply to multiple modules */
all?: boolean;
@@ -44,6 +49,7 @@ export interface Patch {
noWarn?: boolean;
/** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */
group?: boolean;
+ /** A function which returns whether this patch should be applied */
predicate?(): boolean;
}
diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts
index be5721ff..123c62b0 100644
--- a/src/webpack/common/stores.ts
+++ b/src/webpack/common/stores.ts
@@ -27,12 +27,7 @@ export const Flux: t.Flux = findByPropsLazy("connectStores");
export type GenericStore = t.FluxStore & Record;
-export enum DraftType {
- ChannelMessage = 0,
- ThreadSettings = 1,
- FirstThreadMessage = 2,
- ApplicationLauncherCommand = 3
-}
+export const { DraftType }: { DraftType: typeof t.DraftType; } = findByPropsLazy("DraftType");
export let MessageStore: Omit & {
getMessages(chanId: string): any;
diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts
index d6bf3aaf..27715b5e 100644
--- a/src/webpack/common/types/stores.d.ts
+++ b/src/webpack/common/types/stores.d.ts
@@ -173,6 +173,15 @@ export class DraftStore extends FluxStore {
getThreadSettings(channelId: string): any | null;
}
+export enum DraftType {
+ ChannelMessage,
+ ThreadSettings,
+ FirstThreadMessage,
+ ApplicationLauncherCommand,
+ Poll,
+ SlashCommand,
+}
+
export class GuildStore extends FluxStore {
getGuild(guildId: string): Guild;
getGuildCount(): number;
diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts
index 6d74e9b2..2cd636d8 100644
--- a/src/webpack/common/utils.ts
+++ b/src/webpack/common/utils.ts
@@ -119,6 +119,8 @@ export function showToast(message: string, type = ToastType.MESSAGE) {
}
export const UserUtils = findByPropsLazy("getUser", "fetchCurrentUser") as { getUser: (id: string) => Promise; };
+
+export const UploadManager = findByPropsLazy("clearAll", "addFile");
export const UploadHandler = findByPropsLazy("showUploadFileSizeExceededError", "promptToUpload") as {
promptToUpload: (files: File[], channel: Channel, draftType: Number) => void;
};
diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts
index d3de2072..c7e42467 100644
--- a/src/webpack/patchWebpack.ts
+++ b/src/webpack/patchWebpack.ts
@@ -257,7 +257,12 @@ function patchFactories(factories: Record