-
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
+
("native", NATIVE_SETTINGS_FILE);
+mergeDefaults(nativeSettings, DefaultNativeSettings);
+
+export const NativeSettings = new SettingsStore(nativeSettings);
NativeSettings.addGlobalChangeListener(() => {
try {
diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts
index 6c076c328..9513da51c 100644
--- a/src/main/utils/constants.ts
+++ b/src/main/utils/constants.ts
@@ -35,6 +35,7 @@ export const ALLOWED_PROTOCOLS = [
"steam:",
"spotify:",
"com.epicgames.launcher:",
+ "tidal:"
];
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx
index 772ee9b64..81d1d1797 100644
--- a/src/plugins/_core/settings.tsx
+++ b/src/plugins/_core/settings.tsx
@@ -26,55 +26,63 @@ import UpdaterTab from "@components/VencordSettings/UpdaterTab";
import VencordTab from "@components/VencordSettings/VencordTab";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
-import { React } from "@webpack/common";
+import { i18n, React } from "@webpack/common";
import gitHash from "~git-hash";
+type SectionType = "HEADER" | "DIVIDER" | "CUSTOM";
+type SectionTypes = Record;
+
export default definePlugin({
name: "Settings",
description: "Adds Settings UI and debug info",
authors: [Devs.Ven, Devs.Megu],
required: true,
- patches: [{
- find: ".versionHash",
- replacement: [
- {
- match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
- replace: (m, component, props) => {
- props = props.replace(/children:\[.+\]/, "");
- return `${m},$self.makeInfoElements(${component}, ${props})`;
+ patches: [
+ {
+ find: ".versionHash",
+ replacement: [
+ {
+ 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 Canary
+ {
+ find: "Messages.ACTIVITY_SETTINGS",
+ replacement: {
+ match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
+ 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: {
+ match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
+ replace: "$2.default.open($1);return;"
}
- ]
- }, {
- 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),$&"
}
- }, {
- find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
- replacement: {
- match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
- replace: "$2.default.open($1);return;"
- }
- }],
+ ],
- customSections: [] as ((SectionTypes: Record) => any)[],
+ customSections: [] as ((SectionTypes: SectionTypes) => any)[],
- makeSettingsCategories(SectionTypes: Record) {
+ makeSettingsCategories(SectionTypes: SectionTypes) {
return [
{
section: SectionTypes.HEADER,
@@ -130,19 +138,63 @@ export default definePlugin({
].filter(Boolean);
},
+ isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
+ const firstChild = settings?.[0];
+ // lowest two elements... sanity backup
+ if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
+
+ const { settingsLocation } = Settings.plugins.Settings;
+
+ 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,
+ belowNitro: i18n.Messages.APP_SETTINGS,
+ aboveActivity: i18n.Messages.ACTIVITY_SETTINGS
+ };
+ return header === names[settingsLocation];
+ },
+
+ 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,
description: "Where to put the Vencord settings section",
options: [
{ label: "At the very top", value: "top" },
- { label: "Above the Nitro section", value: "aboveNitro" },
+ { label: "Above the Nitro section", value: "aboveNitro", default: true },
{ label: "Below the Nitro section", value: "belowNitro" },
- { label: "Above Activity Settings", value: "aboveActivity", default: true },
+ { label: "Above Activity Settings", value: "aboveActivity" },
{ label: "Below Activity Settings", value: "belowActivity" },
{ label: "At the very bottom", value: "bottom" },
- ],
- restartNeeded: true
+ ]
},
},
@@ -169,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/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx
index 674be8e53..c7377833a 100644
--- a/src/plugins/_core/supportHelper.tsx
+++ b/src/plugins/_core/supportHelper.tsx
@@ -16,20 +16,24 @@
* along with this program. If not, see .
*/
-import { DataStore } from "@api/index";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Link } from "@components/Link";
+import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
+import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
+import { relaunch } from "@utils/native";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
-import { isOutdated } from "@utils/updater";
-import { Alerts, Forms, UserStore } from "@webpack/common";
+import { isOutdated, update } from "@utils/updater";
+import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common";
import gitHash from "~git-hash";
import plugins from "~plugins";
import settings from "./settings";
-const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
+const VENCORD_GUILD_ID = "1015060230222131221";
const AllowedChannelIds = [
SUPPORT_CHANNEL_ID,
@@ -37,6 +41,12 @@ const AllowedChannelIds = [
"1033680203433660458", // Vencord > #v
];
+const TrustedRolesIds = [
+ "1026534353167208489", // contributor
+ "1026504932959977532", // regular
+ "1042507929485586532", // donor
+];
+
export default definePlugin({
name: "SupportHelper",
required: true,
@@ -44,6 +54,14 @@ export default definePlugin({
authors: [Devs.Ven],
dependencies: ["CommandsAPI"],
+ patches: [{
+ find: ".BEGINNING_DM.format",
+ replacement: {
+ match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
+ replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
+ }
+ }],
+
commands: [{
name: "vencord-debug",
description: "Send Vencord Debug info",
@@ -64,15 +82,13 @@ export default definePlugin({
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
- const enabledApiPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && isApiPlugin(p));
const info = {
- Vencord: `v${VERSION} • ${gitHash}${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
- "Discord Branch": RELEASE_CHANNEL,
- Client: client,
- Platform: window.navigator.platform,
- Outdated: isOutdated,
- OpenAsar: "openasar" in window,
+ Vencord:
+ `v${VERSION} • [${gitHash}]()` +
+ `${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
+ Client: `${RELEASE_CHANNEL} ~ ${client}`,
+ Platform: window.navigator.platform
};
if (IS_DISCORD_DESKTOP) {
@@ -80,11 +96,10 @@ export default definePlugin({
}
const debugInfo = `
-**Vencord Debug Info**
->>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
+>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}
-Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}):
-${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))}
+Enabled Plugins (${enabledPlugins.length}):
+${makeCodeblock(enabledPlugins.join(", "))}
`;
return {
@@ -97,24 +112,75 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
async CHANNEL_SELECT({ channelId }) {
if (channelId !== SUPPORT_CHANNEL_ID) return;
- if (isPluginDev(UserStore.getCurrentUser().id)) return;
+ const selfId = UserStore.getCurrentUser()?.id;
+ if (!selfId || isPluginDev(selfId)) return;
- if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) {
- const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
-
- Alerts.show({
+ if (isOutdated) {
+ return Alerts.show({
title: "Hold on!",
body:
You are using an outdated version of Vencord! Chances are, your issue is already fixed.
-
- Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button)
- to do so, in case you can't access the Updater page.
+
+ Please first update before asking for support!
,
- onCancel: rememberDismiss,
- onConfirm: rememberDismiss
+ onCancel: () => openUpdaterModal!(),
+ cancelText: "View Updates",
+ confirmText: "Update & Restart Now",
+ async onConfirm() {
+ await update();
+ relaunch();
+ },
+ secondaryConfirmText: "I know what I'm doing or I can't update"
+ });
+ }
+
+ // @ts-ignore outdated type
+ const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
+ if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
+
+ if (!IS_WEB && IS_UPDATER_DISABLED) {
+ return Alerts.show({
+ title: "Hold on!",
+ body:
+ You are using an externally updated Vencord version, which we do not provide support for!
+
+ Please either switch to an officially supported version of Vencord, or
+ contact your package maintainer for support instead.
+
+
,
+ onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
+ });
+ }
+
+ const repo = await VencordNative.updater.getRepo();
+ if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
+ return Alerts.show({
+ title: "Hold on!",
+ body:
+ You are using a fork of Vencord, which we do not provide support for!
+
+ Please either switch to an officially supported version of Vencord, or
+ contact your package maintainer for support instead.
+
+
,
+ onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
});
}
}
- }
+ },
+
+ ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
+ if (!isPluginDev(userId)) return null;
+ if (RelationshipStore.isFriend(userId)) return null;
+
+ return (
+
+ Please do not private message Vencord plugin developers for support!
+
+ Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
+ {!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
+
+ );
+ }, { noop: true })
});
diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx
index 70e4070cd..795f19901 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
{
@@ -127,7 +127,7 @@ export default definePlugin({
},
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
{
- match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
+ match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
},
// Export the isBetterFolders variable to the folders component
@@ -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) {
@@ -279,7 +281,7 @@ export default definePlugin({
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
return child => {
if (isBetterFolders) {
- return "onScroll" in child.props;
+ return child?.props?.onScroll != null;
}
return true;
};
diff --git a/src/plugins/betterNotes/index.tsx b/src/plugins/betterNotes/index.tsx
index 2183d98e2..cacdba5fd 100644
--- a/src/plugins/betterNotes/index.tsx
+++ b/src/plugins/betterNotes/index.tsx
@@ -17,6 +17,7 @@
*/
import { Settings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
@@ -60,7 +61,7 @@ export default definePlugin({
find: ".popularApplicationCommandIds,",
replacement: {
match: /lastSection:(!?\i)}\),/,
- replace: "$&$self.patchPadding($1),"
+ replace: "$&$self.patchPadding({lastSection:$1}),"
}
}
],
@@ -80,10 +81,10 @@ export default definePlugin({
}
},
- patchPadding(lastSection: any) {
- if (!lastSection) return;
+ patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
+ if (!lastSection) return null;
return (
-
+
);
- }
+ })
});
diff --git a/src/plugins/betterSessions/index.tsx b/src/plugins/betterSessions/index.tsx
index 539508f80..9c93289c8 100644
--- a/src/plugins/betterSessions/index.tsx
+++ b/src/plugins/betterSessions/index.tsx
@@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
-import { React, RestAPI, Tooltip } from "@webpack/common";
+import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
import { RenameButton } from "./components/RenameButton";
import { Session, SessionInfo } from "./types";
@@ -168,7 +168,7 @@ export default definePlugin({
async checkNewSessions() {
const data = await RestAPI.get({
- url: "/auth/sessions"
+ url: Constants.Endpoints.AUTH_SESSIONS
});
for (const session of data.body.user_sessions) {
diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx
index 7d81c6f5c..e90e5c82a 100644
--- a/src/plugins/betterSettings/index.tsx
+++ b/src/plugins/betterSettings/index.tsx
@@ -6,17 +6,18 @@
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
-import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
+import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
-import { findByPropsLazy } from "@webpack";
+import { waitFor } from "@webpack";
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
import type { HTMLAttributes, ReactElement } from "react";
type SettingsEntry = { section: string, label: string; };
const cl = classNameFactory("");
-const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers");
+let Classes: Record;
+waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
const settings = definePluginSettings({
disableFade: {
@@ -118,18 +119,25 @@ 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($&)"
}
}
],
+ // This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
+ // without possibly also catching unrelated errors of children.
+ //
+ // Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
+ // try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
+ // not in children
Layer(props: LayerProps) {
- return (
- props.children as any}>
-
-
- );
+ if (!FocusLock || !ComponentDispatch || !Classes) {
+ new Logger("BetterSettings").error("Failed to find some components");
+ return props.children;
+ }
+
+ return ;
},
wrapMenu(list: SettingsEntry[]) {
diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts
index f8c76d7f7..3297ca300 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,
};
});
@@ -104,7 +102,7 @@ export default definePlugin({
shouldAttemptRecover = false;
// This is enough to avoid a crash loop
- setTimeout(() => shouldAttemptRecover = true, 500);
+ setTimeout(() => shouldAttemptRecover = true, 1000);
} catch { }
try {
@@ -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 000000000..4b9dd8e06
--- /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/customRPC/index.tsx b/src/plugins/customRPC/index.tsx
index 334372e38..f1b2fbf53 100644
--- a/src/plugins/customRPC/index.tsx
+++ b/src/plugins/customRPC/index.tsx
@@ -17,13 +17,16 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
+import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
+import { Margins } from "@utils/margins";
+import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
-import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
+import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
@@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
export default definePlugin({
name: "CustomRPC",
description: "Allows you to set a custom rich presence.",
- authors: [Devs.captain, Devs.AutumnVN],
+ authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
start: setRpc,
stop: () => setRpc(true),
settings,
settingsAboutComponent: () => {
const activity = useAwaiter(createActivity);
+ const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
const { profileThemeStyle } = useProfileThemeStyle({});
return (
<>
+ {!gameActivityEnabled && (
+
+ Notice
+ Game activity isn't enabled, people won't be able to see your custom rich presence!
+
+ StatusSettingsStores.ShowCurrentGame.updateSetting(true)}
+ >
+ Enable
+
+
+ )}
+
Go to Discord Developer Portal to create an application and
get the application ID.
@@ -407,7 +429,9 @@ export default definePlugin({
If you want to use image link, download your image and reupload the image to Imgur and get the image link by right-clicking the image and select "Copy image address".
-
+
+
+
{activity[0] &&
) {
try {
const { embed } = this.props;
+ const { replaceElements } = settings.store;
+
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
@@ -58,12 +67,12 @@ async function embedDidMount(this: Component) {
enabled: true
};
- if (hasTitle) {
+ if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
}
- if (hasThumb) {
+ if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
}
@@ -128,10 +137,30 @@ function DearrowButton({ component }: { component: Component; }) {
);
}
+const settings = definePluginSettings({
+ hideButton: {
+ description: "Hides the Dearrow button from YouTube embeds",
+ type: OptionType.BOOLEAN,
+ default: false,
+ restartNeeded: true
+ },
+ replaceElements: {
+ description: "Choose which elements of the embed will be replaced",
+ type: OptionType.SELECT,
+ restartNeeded: true,
+ options: [
+ { label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },
+ { label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
+ { label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
+ ],
+ }
+});
+
export default definePlugin({
name: "Dearrow",
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
authors: [Devs.Ven],
+ settings,
embedDidMount,
renderButton(component: Component) {
@@ -154,7 +183,8 @@ export default definePlugin({
// add dearrow button
{
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
- replace: "children:[$self.renderButton(this),"
+ replace: "children:[$self.renderButton(this),",
+ predicate: () => !settings.store.hideButton
}
]
}],
diff --git a/src/plugins/emoteCloner/index.tsx b/src/plugins/emoteCloner/index.tsx
index cd9890a80..b456c351e 100644
--- a/src/plugins/emoteCloner/index.tsx
+++ b/src/plugins/emoteCloner/index.tsx
@@ -24,7 +24,7 @@ import { Margins } from "@utils/margins";
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
-import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
+import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
import { Promisable } from "type-fest";
const StickersStore = findStoreLazy("StickersStore");
@@ -64,7 +64,7 @@ async function fetchSticker(id: string) {
if (cached) return cached;
const { body } = await RestAPI.get({
- url: `/stickers/${id}`
+ url: Constants.Endpoints.STICKER(id)
});
FluxDispatcher.dispatch({
@@ -83,7 +83,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) {
data.append("file", await fetchBlob(getUrl(sticker)));
const { body } = await RestAPI.post({
- url: `/guilds/${guildId}/stickers`,
+ url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId),
body: data,
});
@@ -322,8 +322,9 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
switch (favoriteableType) {
case "emoji":
const match = props.message.content.match(RegExp(`|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
- if (!match) return;
- const name = match[1] ?? "FakeNitroEmoji";
+ const reaction = props.message.reactions.find(reaction => reaction.emoji.id === favoriteableId);
+ if (!match && !reaction) return;
+ const name = (match && match[1]) ?? reaction?.emoji.name ?? "FakeNitroEmoji";
return buildMenuItem("Emoji", () => ({
id: favoriteableId,
diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx
index 03feda0a8..a55a7771e 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;
@@ -39,6 +38,7 @@ const StickerStore = findStoreLazy("StickersStore") as {
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
+const RoleSubscriptionEmojiUtils = findByPropsLazy("isUnusableRoleSubscriptionEmoji");
function searchProtoClassField(localName: string, protoClass: any) {
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
@@ -111,7 +111,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
const settings = definePluginSettings({
enableEmojiBypass: {
- description: "Allow sending fake emojis",
+ description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
@@ -129,7 +129,7 @@ const settings = definePluginSettings({
restartNeeded: true
},
enableStickerBypass: {
- description: "Allow sending fake stickers",
+ description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
@@ -190,7 +190,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
- description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
+ description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
dependencies: ["MessageEventsAPI"],
settings,
@@ -408,6 +408,15 @@ export default definePlugin({
match: /canUseCustomNotificationSounds:function\(\i\){/,
replace: "$&return true;"
}
+ },
+ // Allows the usage of subscription-locked emojis
+ {
+ find: "isUnusableRoleSubscriptionEmoji:function",
+ replacement: {
+ match: /isUnusableRoleSubscriptionEmoji:function/,
+ // replace the original export with a func that always returns false and alias the original
+ replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
+ }
}
],
@@ -797,13 +806,16 @@ 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) {
if (e.require_colons === false) return true;
if (e.available === false) return false;
+ const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
+ if (isUnusableRoleSubEmoji(e, this.guildId)) return false;
+
if (this.canUseEmotes)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
else
diff --git a/src/plugins/friendInvites/index.ts b/src/plugins/friendInvites/index.ts
index e5ff447ed..47e312c31 100644
--- a/src/plugins/friendInvites/index.ts
+++ b/src/plugins/friendInvites/index.ts
@@ -20,7 +20,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption,
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { RestAPI, UserStore } from "@webpack/common";
+import { Constants, RestAPI, UserStore } from "@webpack/common";
const FriendInvites = findByPropsLazy("createFriendInvite");
const { uuid4 } = findByPropsLazy("uuid4");
@@ -58,7 +58,7 @@ export default definePlugin({
if (uses === 1) {
const random = uuid4();
const { body: { invite_suggestions } } = await RestAPI.post({
- url: "/friend-finder/find-friends",
+ url: Constants.Endpoints.FRIEND_FINDER,
body: {
modified_contacts: {
[random]: [1, "", ""]
diff --git a/src/plugins/imageZoom/components/Magnifier.tsx b/src/plugins/imageZoom/components/Magnifier.tsx
index 816717350..aadd0903a 100644
--- a/src/plugins/imageZoom/components/Magnifier.tsx
+++ b/src/plugins/imageZoom/components/Magnifier.tsx
@@ -17,6 +17,7 @@
*/
import { classNameFactory } from "@api/Styles";
+import ErrorBoundary from "@components/ErrorBoundary";
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
import { ELEMENT_ID } from "../constants";
@@ -36,7 +37,7 @@ export interface MagnifierProps {
const cl = classNameFactory("vc-imgzoom-");
-export const Magnifier: React.FC = ({ instance, size: initialSize, zoom: initalZoom }) => {
+export const Magnifier = ErrorBoundary.wrap(({ instance, size: initialSize, zoom: initalZoom }) => {
const [ready, setReady] = useState(false);
const [lensPosition, setLensPosition] = useState({ x: 0, y: 0 });
@@ -199,4 +200,4 @@ export const Magnifier: React.FC = ({ instance, size: initialSiz
)}
);
-};
+}, { noop: true });
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index 488847d15..3291885c1 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -20,6 +20,7 @@ import { registerCommand, unregisterCommand } from "@api/Commands";
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger";
+import { canonicalizeFind } from "@utils/patches";
import { Patch, Plugin, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
@@ -83,8 +84,12 @@ for (const p of pluginsValues) {
if (p.patches && isPluginEnabled(p.name)) {
for (const patch of p.patches) {
patch.plugin = p.name;
- if (!Array.isArray(patch.replacement))
+
+ canonicalizeFind(patch);
+ if (!Array.isArray(patch.replacement)) {
patch.replacement = [patch.replacement];
+ }
+
patches.push(patch);
}
}
diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx
index fcb0af712..3dfe51e77 100644
--- a/src/plugins/invisibleChat.desktop/index.tsx
+++ b/src/plugins/invisibleChat.desktop/index.tsx
@@ -23,7 +23,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getStegCloak } from "@utils/dependencies";
import definePlugin, { OptionType } from "@utils/types";
-import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
+import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
import { buildDecModal } from "./components/DecryptionModal";
@@ -153,7 +153,7 @@ export default definePlugin({
// Gets the Embed of a Link
async getEmbed(url: URL): Promise {
const { body } = await RestAPI.post({
- url: "/unfurler/embed-urls",
+ url: Constants.Endpoints.UNFURL_EMBED_URLS,
body: {
urls: [url]
}
diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx
index 5dfec8a32..1213ece29 100644
--- a/src/plugins/lastfm/index.tsx
+++ b/src/plugins/lastfm/index.tsx
@@ -77,7 +77,8 @@ const enum NameFormat {
ArtistFirst = "artist-first",
SongFirst = "song-first",
ArtistOnly = "artist",
- SongOnly = "song"
+ SongOnly = "song",
+ AlbumName = "album"
}
const applicationId = "1108588077900898414";
@@ -147,6 +148,10 @@ const settings = definePluginSettings({
{
label: "Use song name only",
value: NameFormat.SongOnly
+ },
+ {
+ label: "Use album name (falls back to custom status text if song has no album)",
+ value: NameFormat.AlbumName
}
],
},
@@ -313,6 +318,8 @@ export default definePlugin({
return trackData.artist;
case NameFormat.SongOnly:
return trackData.name;
+ case NameFormat.AlbumName:
+ return trackData.album || settings.store.statusName;
default:
return settings.store.statusName;
}
diff --git a/src/plugins/messageLatency/README.md b/src/plugins/messageLatency/README.md
new file mode 100644
index 000000000..8d2a776cd
--- /dev/null
+++ b/src/plugins/messageLatency/README.md
@@ -0,0 +1,31 @@
+# MessageLatency
+
+Displays an indicator for messages that took ≥n seconds to send.
+
+> **NOTE**
+>
+> - This plugin only applies to messages received after opening the channel
+> - False positives can exist if the user's system clock has drifted.
+> - Grouped messages only display latency of the first message
+
+## Demo
+
+### Chat View
+
+![chat-view](https://github.com/Vendicated/Vencord/assets/82430093/69430881-60b3-422f-aa3d-c62953837566)
+
+### Clock -ve Drift
+
+![pissbot-on-top](https://github.com/Vendicated/Vencord/assets/82430093/d9248b66-e761-4872-8829-e8bf4fea6ec8)
+
+### Clock +ve Drift
+
+![dumb-ai](https://github.com/Vendicated/Vencord/assets/82430093/0e9783cf-51d5-4559-ae10-42399e7d4099)
+
+### Connection Delay
+
+![who-this](https://github.com/Vendicated/Vencord/assets/82430093/fd68873d-8630-42cc-a166-e9063d2718b2)
+
+### Icons
+
+![icons](https://github.com/Vendicated/Vencord/assets/82430093/17630bd9-44ee-4967-bcdf-3315eb6eca85)
diff --git a/src/plugins/messageLatency/index.tsx b/src/plugins/messageLatency/index.tsx
new file mode 100644
index 000000000..301e605fb
--- /dev/null
+++ b/src/plugins/messageLatency/index.tsx
@@ -0,0 +1,193 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import { isNonNullish } from "@utils/guards";
+import definePlugin, { OptionType } from "@utils/types";
+import { findExportedComponentLazy } from "@webpack";
+import { SnowflakeUtils, Tooltip } from "@webpack/common";
+import { Message } from "discord-types/general";
+
+type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
+type Fill = [FillValue, FillValue, FillValue];
+type DiffKey = keyof Diff;
+
+interface Diff {
+ days: number,
+ hours: number,
+ minutes: number,
+ seconds: number;
+}
+
+const DISCORD_KT_DELAY = 1471228.928;
+const HiddenVisually = findExportedComponentLazy("HiddenVisually");
+
+export default definePlugin({
+ name: "MessageLatency",
+ description: "Displays an indicator for messages that took ≥n seconds to send",
+ authors: [Devs.arHSM],
+
+ settings: definePluginSettings({
+ latency: {
+ type: OptionType.NUMBER,
+ description: "Threshold in seconds for latency indicator",
+ default: 2
+ },
+ detectDiscordKotlin: {
+ type: OptionType.BOOLEAN,
+ description: "Detect old Discord Android clients",
+ default: true
+ }
+ }),
+
+ patches: [
+ {
+ find: "showCommunicationDisabledStyles",
+ replacement: {
+ match: /(message:(\i),avatar:\i,username:\(0,\i.jsxs\)\(\i.Fragment,\{children:\[)(\i&&)/,
+ replace: "$1$self.Tooltip()({ message: $2 }),$3"
+ }
+ }
+ ],
+
+ stringDelta(delta: number) {
+ const diff: Diff = {
+ days: Math.round(delta / (60 * 60 * 24)),
+ hours: Math.round((delta / (60 * 60)) % 24),
+ minutes: Math.round((delta / (60)) % 60),
+ seconds: Math.round(delta % 60),
+ };
+
+ const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
+ const keys = Object.keys(diff) as DiffKey[];
+
+ const ts = keys.reduce((prev, k) => {
+ const s = str(k);
+
+ return prev + (
+ isNonNullish(s)
+ ? (prev !== ""
+ ? k === "seconds"
+ ? " and "
+ : " "
+ : "") + s
+ : ""
+ );
+ }, "");
+
+ return ts || "0 seconds";
+ },
+
+ latencyTooltipData(message: Message) {
+ const { latency, detectDiscordKotlin } = this.settings.store;
+ const { id, nonce } = message;
+
+ // Message wasn't received through gateway
+ if (!isNonNullish(nonce)) return null;
+
+ let isDiscordKotlin = false;
+ let delta = Math.round((SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce)) / 1000);
+
+ // Old Discord Android clients have a delay of around 17 days
+ // This is a workaround for that
+ if (-delta >= DISCORD_KT_DELAY - 86400) { // One day of padding for good measure
+ isDiscordKotlin = detectDiscordKotlin;
+ delta += DISCORD_KT_DELAY;
+ }
+
+ // Thanks dziurwa (I hate you)
+ // This is when the user's clock is ahead
+ // Can't do anything if the clock is behind
+ const abs = Math.abs(delta);
+ const ahead = abs !== delta;
+
+ const stringDelta = abs >= latency ? this.stringDelta(abs) : null;
+
+ // Also thanks dziurwa
+ // 2 minutes
+ const TROLL_LIMIT = 2 * 60;
+
+ const fill: Fill = isDiscordKotlin
+ ? ["status-positive", "status-positive", "text-muted"]
+ : delta >= TROLL_LIMIT || ahead
+ ? ["text-muted", "text-muted", "text-muted"]
+ : delta >= (latency * 2)
+ ? ["status-danger", "text-muted", "text-muted"]
+ : ["status-warning", "status-warning", "text-muted"];
+
+ return (abs >= latency || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
+ },
+
+ Tooltip() {
+ return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
+ const d = this.latencyTooltipData(message);
+
+ if (!isNonNullish(d)) return null;
+
+ let text: string;
+ if (!d.delta) {
+ text = "User is suspected to be on an old Discord Android client";
+ } else {
+ text = (d.ahead ? `This user's clock is ${d.delta} ahead.` : `This message was sent with a delay of ${d.delta}.`) + (d.isDiscordKotlin ? " User is suspected to be on an old Discord Android client." : "");
+ }
+
+ return
+ {
+ props => <>
+ { }
+ {/* Time Out indicator uses this, I think this is for a11y */}
+ Delayed Message
+ >
+ }
+ ;
+ });
+ },
+
+ Icon({ delta, fill, props }: {
+ delta: string | null;
+ fill: Fill,
+ props: {
+ onClick(): void;
+ onMouseEnter(): void;
+ onMouseLeave(): void;
+ onContextMenu(): void;
+ onFocus(): void;
+ onBlur(): void;
+ "aria-label"?: string;
+ };
+ }) {
+ return
+
+
+
+ ;
+ }
+});
diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx
index 2a5f88282..5c3063628 100644
--- a/src/plugins/messageLinkEmbeds/index.tsx
+++ b/src/plugins/messageLinkEmbeds/index.tsx
@@ -27,6 +27,7 @@ import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import {
Button,
ChannelStore,
+ Constants,
FluxDispatcher,
GuildStore,
IconUtils,
@@ -132,7 +133,7 @@ async function fetchMessage(channelID: string, messageID: string) {
messageCache.set(messageID, { fetched: false });
const res = await RestAPI.get({
- url: `/channels/${channelID}/messages`,
+ url: Constants.Endpoints.MESSAGES(channelID),
query: {
limit: 1,
around: messageID
diff --git a/src/plugins/messageLogger/deleteStyleOverlay.css b/src/plugins/messageLogger/deleteStyleOverlay.css
index 3778e80b3..59e4ac666 100644
--- a/src/plugins/messageLogger/deleteStyleOverlay.css
+++ b/src/plugins/messageLogger/deleteStyleOverlay.css
@@ -1,3 +1,3 @@
.messagelogger-deleted {
- background-color: rgba(240 71 71 / 15%) !important;
+ background-color: hsla(var(--red-430-hsl, 0 85% 61%) / 15%) !important;
}
diff --git a/src/plugins/messageLogger/deleteStyleText.css b/src/plugins/messageLogger/deleteStyleText.css
index 8fb8bf129..3477ef229 100644
--- a/src/plugins/messageLogger/deleteStyleText.css
+++ b/src/plugins/messageLogger/deleteStyleText.css
@@ -1,19 +1,19 @@
/* Message content highlighting */
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
- color: #f04747 !important;
+ color: var(--status-danger, #f04747) !important;
}
/* Bot "thinking" text highlighting */
.messagelogger-deleted [class*="colorStandard"] {
- color: #f04747 !important;
+ color: var(--status-danger, #f04747) !important;
}
/* Embed highlighting */
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
- color: #f04747 !important;
+ color: var(--status-danger, #f04747) !important;
}
.messagelogger-deleted a {
- color: #be3535 !important;
+ color: var(--red-460, #be3535) !important;
text-decoration: underline;
}
diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx
index 988cd8e39..c3a25e1b6 100644
--- a/src/plugins/messageLogger/index.tsx
+++ b/src/plugins/messageLogger/index.tsx
@@ -255,7 +255,7 @@ export default definePlugin({
replace: "$1" +
".update($3,m =>" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" +
- " $2.message.content !== m.content ?" +
+ " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" +
")" +
diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx
index df47e5456..1257b4524 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/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx
index 1753fefbc..94998677b 100644
--- a/src/plugins/mutualGroupDMs/index.tsx
+++ b/src/plugins/mutualGroupDMs/index.tsx
@@ -16,6 +16,7 @@
* along with this program. If not, see .
*/
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types";
@@ -55,12 +56,12 @@ export default definePlugin({
find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
replacement: {
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
- replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);"
+ replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs({user: $1, onClose: $2});"
}
}
],
- renderMutualGDMs(user: User, onClose: () => void) {
+ renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
);
- }
+ })
});
diff --git a/src/plugins/noServerEmojis/index.ts b/src/plugins/noServerEmojis/index.ts
new file mode 100644
index 000000000..ed843769c
--- /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 0835c0612..83da5f3c3 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 .
*/
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { GuildStore, RestAPI } from "@webpack/common";
+import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
-const Messages = findByPropsLazy("GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION");
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
+function showDisableInvites(guildId: string) {
+ // Once the experiment is removed, this should keep working
+ const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
+ // @ts-ignore
+ return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
+}
+
+function disableInvites(guildId: string) {
+ const guild = GuildStore.getGuild(guildId);
+ const features = [...guild.features, "INVITES_DISABLED"];
+ RestAPI.patch({
+ url: Constants.Endpoints.GUILD(guildId),
+ body: { features },
+ });
+}
+
export default definePlugin({
name: "PauseInvitesForever",
tags: ["DisableInvitesForever"],
@@ -33,42 +49,29 @@ export default definePlugin({
patches: [
{
find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION",
- replacement: [{
- match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/,
- replace: "children: $self.renderInvitesLabel(arguments[0].guildId, setChecked)",
- },
- {
- match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/,
- replace: "$1,setChecked=$2"
- }]
+ group: true,
+ replacement: [
+ {
+ match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/,
+ replace: "children: $self.renderInvitesLabel({guildId:arguments[0].guildId,setChecked})",
+ },
+ {
+ match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/,
+ replace: "$1,setChecked=$2"
+ }
+ ]
}
],
- showDisableInvites(guildId: string) {
- // Once the experiment is removed, this should keep working
- const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
- // @ts-ignore
- return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
- },
-
- disableInvites(guildId: string) {
- const guild = GuildStore.getGuild(guildId);
- const features = [...guild.features, "INVITES_DISABLED"];
- RestAPI.patch({
- url: `/guilds/${guild.id}`,
- body: { features },
- });
- },
-
- renderInvitesLabel(guildId: string, setChecked: Function) {
+ renderInvitesLabel: ErrorBoundary.wrap(({ guildId, setChecked }) => {
return (
);
- }
+ })
});
diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
index c2e50cedd..963750fa3 100644
--- a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
+++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
@@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
-import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
+import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { settings } from "..";
@@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
{
- if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
+ if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => (
));
+ else if (permission.type === PermissionType.User) {
+ ContextMenuApi.openContextMenu(e, () => (
+
+ ));
+ }
}}
>
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
@@ -200,24 +208,53 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
aria-label="Role Options"
>
{
- const role = GuildStore.getRole(guild.id, roleId);
- if (!role) return;
+ Clipboard.copy(roleId);
+ }}
+ />
- onClose();
+ {(settings.store as any).unsafeViewAsRole && (
+ {
+ const role = GuildStore.getRole(guild.id, roleId);
+ if (!role) return;
- FluxDispatcher.dispatch({
- type: "IMPERSONATE_UPDATE",
- guildId: guild.id,
- data: {
- type: "ROLES",
- roles: {
- [roleId]: role
+ onClose();
+
+ FluxDispatcher.dispatch({
+ type: "IMPERSONATE_UPDATE",
+ guildId: guild.id,
+ data: {
+ type: "ROLES",
+ roles: {
+ [roleId]: role
+ }
}
- }
- });
+ });
+ }
+ }
+ />
+ )}
+
+ );
+}
+
+function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
+ return (
+
+ {
+ Clipboard.copy(userId);
}}
/>
diff --git a/src/plugins/petpet/index.ts b/src/plugins/petpet/index.ts
index 3f9743255..2e06d0b17 100644
--- a/src/plugins/petpet/index.ts
+++ b/src/plugins/petpet/index.ts
@@ -21,10 +21,9 @@ import { Devs } from "@utils/constants";
import { makeLazy } from "@utils/lazy";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { UploadHandler, UserUtils } from "@webpack/common";
+import { DraftType, UploadHandler, UploadManager, UserUtils } from "@webpack/common";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
-const DRAFT_TYPE = 0;
const DEFAULT_DELAY = 20;
const DEFAULT_RESOLUTION = 128;
const FRAMES = 10;
@@ -59,9 +58,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
for (const opt of options) {
switch (opt.name) {
case "image":
- const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
+ const upload = UploadStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);
if (upload) {
- if (!upload.isImage) throw "Upload is not an image";
+ if (!upload.isImage) {
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
+ throw "Upload is not an image";
+ }
return upload.item.file;
}
break;
@@ -73,10 +75,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
} catch (err) {
console.error("[petpet] Failed to fetch user\n", err);
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
throw "Failed to fetch user. Check the console for more info.";
}
}
}
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
return null;
}
@@ -130,6 +134,7 @@ export default definePlugin({
var url = await resolveImage(opts, cmdCtx, noServerPfp);
if (!url) throw "No Image specified!";
} catch (err) {
+ UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
sendBotMessage(cmdCtx.channel.id, {
content: String(err),
});
@@ -147,6 +152,8 @@ export default definePlugin({
canvas.width = canvas.height = resolution;
const ctx = canvas.getContext("2d")!;
+ UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
+
for (let i = 0; i < FRAMES; i++) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -174,7 +181,7 @@ export default definePlugin({
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
// Immediately after the command finishes, Discord clears all input, including pending attachments.
// Thus, setTimeout is needed to make this execute after Discord cleared the input
- setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
+ setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
},
},
]
diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx
index 010b5506c..60484561a 100644
--- a/src/plugins/pinDms/index.tsx
+++ b/src/plugins/pinDms/index.tsx
@@ -83,7 +83,7 @@ export default definePlugin({
// Rendering
{
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
- replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);"
+ replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2)();"
},
{
match: /"renderSection",(\i)=>{/,
@@ -320,25 +320,26 @@ export default definePlugin({
);
- }),
+ }, { noop: true }),
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType) {
- const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
+ return ErrorBoundary.wrap(() => {
+ const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
- if (!channel || !category) return null;
- if (this.isChannelHidden(sectionIndex, index)) return null;
+ if (!channel || !category) return null;
+ if (this.isChannelHidden(sectionIndex, index)) return null;
- return (
-
- {channel.id}
-
- );
+ return (
+
+ {channel.id}
+
+ );
+ }, { noop: true });
},
-
getChannel(sectionIndex: number, index: number, channels: Record) {
const category = categories[sectionIndex - 1];
if (!category) return { channel: null, category: null };
diff --git a/src/plugins/pronoundb/index.ts b/src/plugins/pronoundb/index.ts
index b14b26572..a5891d2e8 100644
--- a/src/plugins/pronoundb/index.ts
+++ b/src/plugins/pronoundb/index.ts
@@ -33,7 +33,7 @@ const PRONOUN_TOOLTIP_PATCH = {
export default definePlugin({
name: "PronounDB",
- authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven],
+ authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven, Devs.Elvyra],
description: "Adds pronouns to user messages using pronoundb",
patches: [
{
diff --git a/src/plugins/pronoundb/pronoundbUtils.ts b/src/plugins/pronoundb/pronoundbUtils.ts
index 6373c56a0..d4fdb09d3 100644
--- a/src/plugins/pronoundb/pronoundbUtils.ts
+++ b/src/plugins/pronoundb/pronoundbUtils.ts
@@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react";
import { UserProfileStore, UserStore } from "@webpack/common";
import { settings } from "./settings";
-import { PronounCode, PronounMapping, PronounsResponse } from "./types";
+import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types";
type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""];
@@ -40,9 +40,9 @@ export const enum PronounSource {
}
// A map of cached pronouns so the same request isn't sent twice
-const cache: Record = {};
+const cache: Record = {};
// A map of ids and callbacks that should be triggered on fetch
-const requestQueue: Record void)[]> = {};
+const requestQueue: Record void)[]> = {};
// Executes all queued requests and calls their callbacks
const bulkFetch = debounce(async () => {
@@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => {
const pronouns = await bulkFetchPronouns(ids);
for (const id of ids) {
// Call all callbacks for the id
- requestQueue[id]?.forEach(c => c(pronouns[id]));
+ requestQueue[id]?.forEach(c => c(pronouns[id] ? extractPronouns(pronouns[id].sets) : ""));
delete requestQueue[id];
}
});
@@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
return [discordPronouns, "Discord"];
- if (result && result !== "unspecified")
- return [formatPronouns(result), "PronounDB"];
+ if (result && result !== PronounMapping.unspecified)
+ return [result, "PronounDB"];
return [discordPronouns, "Discord"];
}
@@ -98,8 +98,9 @@ const NewLineRe = /\n+/g;
// Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string): string | null {
- const cached = cache[id];
- if (cached && cached !== "unspecified") return cached;
+ const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined;
+
+ if (cached && cached !== PronounMapping.unspecified) return cached;
return cached || null;
}
@@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise {
params.append("ids", ids.join(","));
try {
- const req = await fetch("https://pronoundb.org/api/v1/lookup-bulk?" + params.toString(), {
+ const req = await fetch("https://pronoundb.org/api/v2/lookup?" + params.toString(), {
method: "GET",
headers: {
"Accept": "application/json",
@@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise {
} catch (e) {
// If the request errors, treat it as if no pronouns were found for all ids, and log it
console.error("PronounDB fetching failed: ", e);
- const dummyPronouns = Object.fromEntries(ids.map(id => [id, "unspecified"] as const));
+ const dummyPronouns = Object.fromEntries(ids.map(id => [id, { sets: {} }] as const));
Object.assign(cache, dummyPronouns);
return dummyPronouns;
}
}
-export function formatPronouns(pronouns: string): string {
+export function extractPronouns(pronounSet?: { [locale: string]: PronounCode[] }): string {
+ if (!pronounSet || !pronounSet.en) return PronounMapping.unspecified;
+ // PronounDB returns an empty set instead of {sets: {en: ["unspecified"]}}.
+ const pronouns = pronounSet.en;
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
- // For capitalized pronouns, just return the mapping (it is by default capitalized)
- if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns];
- // If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text
- else if (
- pronounsFormat === PronounsFormat.Lowercase
- && ["any", "ask", "avoid", "other"].includes(pronouns)
- ) return PronounMapping[pronouns];
- // Otherwise (lowercase and not a special code), then convert the mapping to lowercase
- else return PronounMapping[pronouns].toLowerCase();
+
+ if (pronouns.length === 1) {
+ // For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string
+ if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]))
+ return PronounMapping[pronouns[0]];
+ else return PronounMapping[pronouns[0]].toLowerCase();
+ }
+ const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/");
+ return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase();
}
diff --git a/src/plugins/pronoundb/types.ts b/src/plugins/pronoundb/types.ts
index 9cfd77c8a..d099a7de8 100644
--- a/src/plugins/pronoundb/types.ts
+++ b/src/plugins/pronoundb/types.ts
@@ -26,31 +26,29 @@ export interface UserProfilePronounsProps {
}
export interface PronounsResponse {
- [id: string]: PronounCode;
+ [id: string]: {
+ sets?: {
+ [locale: string]: PronounCode[];
+ }
+ }
+}
+
+export interface CachePronouns {
+ sets?: {
+ [locale: string]: PronounCode[];
+ }
}
export type PronounCode = keyof typeof PronounMapping;
export const PronounMapping = {
- hh: "He/Him",
- hi: "He/It",
- hs: "He/She",
- ht: "He/They",
- ih: "It/Him",
- ii: "It/Its",
- is: "It/She",
- it: "It/They",
- shh: "She/He",
- sh: "She/Her",
- si: "She/It",
- st: "She/They",
- th: "They/He",
- ti: "They/It",
- ts: "They/She",
- tt: "They/Them",
+ he: "He/Him",
+ it: "It/Its",
+ she: "She/Her",
+ they: "They/Them",
any: "Any pronouns",
other: "Other pronouns",
ask: "Ask me my pronouns",
avoid: "Avoid pronouns, use my name",
- unspecified: "Unspecified"
+ unspecified: "No pronouns specified.",
} as const;
diff --git a/src/plugins/readAllNotificationsButton/index.tsx b/src/plugins/readAllNotificationsButton/index.tsx
index ae66e11a4..3bf53f993 100644
--- a/src/plugins/readAllNotificationsButton/index.tsx
+++ b/src/plugins/readAllNotificationsButton/index.tsx
@@ -19,6 +19,7 @@
import "./style.css";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
@@ -64,7 +65,7 @@ export default definePlugin({
authors: [Devs.kemo],
dependencies: ["ServerListAPI"],
- renderReadAllButton: () => ,
+ renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),
start() {
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
diff --git a/src/plugins/replyTimestamp/README.md b/src/plugins/replyTimestamp/README.md
new file mode 100644
index 000000000..b7952bf3a
--- /dev/null
+++ b/src/plugins/replyTimestamp/README.md
@@ -0,0 +1,5 @@
+# ReplyTimestamp
+
+Shows timestamps on the previews of replied-to messages. Pretty simple.
+
+![](https://github.com/Vendicated/Vencord/assets/1547062/62e2b67a-e567-4c7a-884d-4640f897f7e0)
diff --git a/src/plugins/replyTimestamp/index.tsx b/src/plugins/replyTimestamp/index.tsx
new file mode 100644
index 000000000..05ec28b1b
--- /dev/null
+++ b/src/plugins/replyTimestamp/index.tsx
@@ -0,0 +1,77 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./style.css";
+
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { Timestamp } from "@webpack/common";
+import type { Message } from "discord-types/general";
+import type { HTMLAttributes } from "react";
+
+const { getMessageTimestampId } = findByPropsLazy("getMessageTimestampId");
+const { calendarFormat, dateFormat, isSameDay } = findByPropsLazy("calendarFormat", "dateFormat", "isSameDay", "accessibilityLabelCalendarFormat");
+const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp");
+
+function Sep(props: HTMLAttributes) {
+ return ;
+}
+
+const enum ReferencedMessageState {
+ LOADED = 0,
+ NOT_LOADED = 1,
+ DELETED = 2,
+}
+
+type ReferencedMessage = { state: ReferencedMessageState.LOADED; message: Message; } | { state: ReferencedMessageState.NOT_LOADED | ReferencedMessageState.DELETED; };
+
+function ReplyTimestamp({
+ referencedMessage,
+ baseMessage,
+}: {
+ referencedMessage: ReferencedMessage,
+ baseMessage: Message;
+}) {
+ if (referencedMessage.state !== ReferencedMessageState.LOADED) return null;
+ const refTimestamp = referencedMessage.message.timestamp as any;
+ const baseTimestamp = baseMessage.timestamp as any;
+ return (
+
+ [
+ {isSameDay(refTimestamp, baseTimestamp)
+ ? dateFormat(refTimestamp, "LT")
+ : calendarFormat(refTimestamp)
+ }
+ ]
+
+ );
+}
+
+export default definePlugin({
+ name: "ReplyTimestamp",
+ description: "Shows a timestamp on replied-message previews",
+ authors: [Devs.Kyuuhachi],
+
+ patches: [
+ {
+ find: "renderSingleLineMessage:function()",
+ replacement: {
+ match: /(?<="aria-label":\i,children:\[)(?=\i,\i,\i\])/,
+ replace: "$self.ReplyTimestamp(arguments[0]),"
+ }
+ }
+ ],
+
+ ReplyTimestamp: ErrorBoundary.wrap(ReplyTimestamp, { noop: true }),
+});
diff --git a/src/plugins/replyTimestamp/style.css b/src/plugins/replyTimestamp/style.css
new file mode 100644
index 000000000..f42371717
--- /dev/null
+++ b/src/plugins/replyTimestamp/style.css
@@ -0,0 +1,3 @@
+.vc-reply-timestamp {
+ margin-right: 0.25em;
+}
diff --git a/src/plugins/showHiddenThings/README.md b/src/plugins/showHiddenThings/README.md
index b41e2d94d..753e5c148 100644
--- a/src/plugins/showHiddenThings/README.md
+++ b/src/plugins/showHiddenThings/README.md
@@ -1,6 +1,6 @@
# ShowHiddenThings
-Displays various moderator-only elements regardless of permissions.
+Displays various hidden & moderator-only things regardless of permissions.
## Features
@@ -9,3 +9,11 @@ Displays various moderator-only elements regardless of permissions.
- Show the invites paused tooltip in the server list
![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b)
+
+- Show the member mod view context menu item in all servers
+
+![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab)
+
+- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria
+
+- Disable filters in Server Discovery search that hide NSFW & disallowed servers
diff --git a/src/plugins/showHiddenThings/index.ts b/src/plugins/showHiddenThings/index.ts
index e7be929bf..8de70aca9 100644
--- a/src/plugins/showHiddenThings/index.ts
+++ b/src/plugins/showHiddenThings/index.ts
@@ -31,13 +31,28 @@ const settings = definePluginSettings({
description: "Show the invites paused tooltip in the server list.",
default: true,
},
+ showModView: {
+ type: OptionType.BOOLEAN,
+ description: "Show the member mod view context menu item in all servers.",
+ default: true,
+ },
+ disableDiscoveryFilters: {
+ type: OptionType.BOOLEAN,
+ description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.",
+ default: true,
+ },
+ disableDisallowedDiscoveryFilters: {
+ type: OptionType.BOOLEAN,
+ description: "Disable filters in Server Discovery search that hide NSFW & disallowed servers.",
+ default: true,
+ },
});
migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
export default definePlugin({
name: "ShowHiddenThings",
- tags: ["ShowTimeouts", "ShowInvitesPaused"],
- description: "Displays various moderator-only elements regardless of permissions.",
+ tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
+ description: "Displays various hidden & moderator-only things regardless of permissions.",
authors: [Devs.Dolfies],
patches: [
{
@@ -55,6 +70,39 @@ export default definePlugin({
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
replace: "true",
},
+ },
+ {
+ find: "canAccessGuildMemberModViewWithExperiment:",
+ predicate: () => settings.store.showModView,
+ replacement: {
+ match: /return \i\.hasAny\(\i\.computePermissions\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.MemberSafetyPagePermissions\)/,
+ replace: "return true",
+ }
+ },
+ {
+ find: "auto_removed:",
+ predicate: () => settings.store.disableDiscoveryFilters,
+ replacement: {
+ match: /filters:\i\.join\(" AND "\),facets:\[/,
+ replace: "facets:["
+ }
+ },
+ {
+ find: "DiscoveryBannedSearchWords.includes",
+ predicate: () => settings.store.disableDisallowedDiscoveryFilters,
+ replacement: {
+ match: /(?<=function\(\){)(?=.{0,130}DiscoveryBannedSearchWords\.includes)/,
+ replace: "return false;"
+ }
+ },
+ {
+ find: "Endpoints.GUILD_DISCOVERY_VALID_TERM",
+ predicate: () => settings.store.disableDisallowedDiscoveryFilters,
+ all: true,
+ replacement: {
+ match: /\i\.HTTP\.get\(\{url:\i\.Endpoints\.GUILD_DISCOVERY_VALID_TERM,query:\{term:\i\},oldFormErrors:!0\}\);/g,
+ replace: "Promise.resolve({ body: { valid: true } });"
+ }
}
],
settings,
diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx
index a9db1af9a..7ba245da5 100644
--- a/src/plugins/showMeYourName/index.tsx
+++ b/src/plugins/showMeYourName/index.tsx
@@ -7,6 +7,7 @@
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Message, User } from "discord-types/general";
@@ -56,7 +57,7 @@ export default definePlugin({
],
settings,
- renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
+ renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
try {
const user = userOverride ?? message.author;
let { username } = user;
@@ -66,14 +67,14 @@ export default definePlugin({
const { nick } = author;
const prefix = withMentionPrefix ? "@" : "";
if (username === nick || isRepliedMessage && !settings.store.inReplies)
- return prefix + nick;
+ return <>{prefix}{nick}>;
if (settings.store.mode === "user-nick")
return <>{prefix}{username} {nick} >;
if (settings.store.mode === "nick-user")
return <>{prefix}{nick} {username} >;
- return prefix + username;
+ return <>{prefix}{username}>;
} catch {
- return author?.nick;
+ return <>{author?.nick}>;
}
- },
+ }, { noop: true }),
});
diff --git a/src/plugins/showTimeoutDuration/README.md b/src/plugins/showTimeoutDuration/README.md
new file mode 100644
index 000000000..137802473
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/README.md
@@ -0,0 +1,8 @@
+# ShowTimeoutDuration
+
+Displays how much longer a user's timeout will last.
+Either in the timeout icon tooltip, or next to it, configurable via settings!
+
+![indicator in tooltip](https://github.com/Vendicated/Vencord/assets/45497981/606588a3-2646-40d9-8800-b6307f650136)
+
+![indicator next to timeout icon](https://github.com/Vendicated/Vencord/assets/45497981/ab9d2101-0fdc-4143-9310-9488f056eeee)
diff --git a/src/plugins/showTimeoutDuration/index.tsx b/src/plugins/showTimeoutDuration/index.tsx
new file mode 100644
index 000000000..f57ee0fc9
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/index.tsx
@@ -0,0 +1,106 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./styles.css";
+
+import { definePluginSettings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import { Margins } from "@utils/margins";
+import definePlugin, { OptionType } from "@utils/types";
+import { findComponentLazy } from "@webpack";
+import { ChannelStore, Forms, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
+import { Message } from "discord-types/general";
+
+const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER"));
+
+const enum DisplayStyle {
+ Tooltip = "tooltip",
+ Inline = "ssalggnikool"
+}
+
+const settings = definePluginSettings({
+ displayStyle: {
+ description: "How to display the timeout duration",
+ type: OptionType.SELECT,
+ restartNeeded: true,
+ options: [
+ { label: "In the Tooltip", value: DisplayStyle.Tooltip },
+ { label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true },
+ ],
+ }
+});
+
+function renderTimeout(message: Message, inline: boolean) {
+ const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id;
+ if (!guildId) return null;
+
+ const member = GuildMemberStore.getMember(guildId, message.author.id);
+ if (!member?.communicationDisabledUntil) return null;
+
+ const countdown = () => (
+
+ );
+
+ return inline
+ ? countdown()
+ : i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({
+ username: message.author.username,
+ countdown
+ });
+}
+
+export default definePlugin({
+ name: "ShowTimeoutDuration",
+ description: "Shows how much longer a user's timeout will last, either in the timeout icon tooltip or next to it",
+ authors: [Devs.Ven],
+
+ settings,
+
+ patches: [
+ {
+ find: ".GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY",
+ replacement: [
+ {
+ match: /(\i)\.Tooltip,{(text:.{0,30}\.Messages\.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY)/,
+ get replace() {
+ if (settings.store.displayStyle === DisplayStyle.Inline)
+ return "$self.TooltipWrapper,{vcProps:arguments[0],$2";
+
+ return "$1.Tooltip,{text:$self.renderTimeoutDuration(arguments[0])";
+ }
+ }
+ ]
+ }
+ ],
+
+ renderTimeoutDuration: ErrorBoundary.wrap(({ message }: { message: Message; }) => {
+ return (
+ <>
+ {i18n.Messages.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}
+
+ {renderTimeout(message, false)}
+
+ >
+ );
+ }, { noop: true }),
+
+ TooltipWrapper: ErrorBoundary.wrap(({ vcProps: { message }, ...tooltipProps }: { vcProps: { message: Message; }; }) => {
+ return (
+
+
+
+
+ {renderTimeout(message, true)} timeout remaining
+
+
+ );
+ }, { noop: true })
+});
diff --git a/src/plugins/showTimeoutDuration/styles.css b/src/plugins/showTimeoutDuration/styles.css
new file mode 100644
index 000000000..70a826e10
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/styles.css
@@ -0,0 +1,4 @@
+.vc-std-wrapper {
+ display: flex;
+ align-items: center;
+}
diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx
index 8b59c6ace..2a6a64283 100644
--- a/src/plugins/silentTyping/index.tsx
+++ b/src/plugins/silentTyping/index.tsx
@@ -18,10 +18,11 @@
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
+import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
-import { FluxDispatcher, React } from "@webpack/common";
+import { FluxDispatcher, Menu, React } from "@webpack/common";
const settings = definePluginSettings({
showIcon: {
@@ -30,6 +31,11 @@ const settings = definePluginSettings({
description: "Show an icon for toggling the plugin",
restartNeeded: true,
},
+ contextMenu: {
+ type: OptionType.BOOLEAN,
+ description: "Add option to toggle the functionality in the chat input context menu",
+ default: true
+ },
isEnabled: {
type: OptionType.BOOLEAN,
description: "Toggle functionality",
@@ -56,13 +62,37 @@ const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
);
};
+
+const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {
+ const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]);
+ if (!contextMenu) return;
+
+ const group = findGroupChildrenByChildId("submit-button", children);
+
+ if (!group) return;
+
+ const idx = group.findIndex(c => c?.props?.id === "submit-button");
+
+ group.splice(idx + 1, 0,
+ settings.store.isEnabled = !settings.store.isEnabled}
+ />
+ );
+};
+
+
export default definePlugin({
name: "SilentTyping",
- authors: [Devs.Ven, Devs.Rini],
+ authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
description: "Hide that you are typing",
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
settings,
-
+ contextMenus: {
+ "textarea-context": ChatBarContextCheckbox
+ },
patches: [
{
find: '.dispatch({type:"TYPING_START_LOCAL"',
diff --git a/src/plugins/startupTimings/index.tsx b/src/plugins/startupTimings/index.tsx
index 742d822ae..cf366df38 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 110eca574..89001aae4 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
@@ -15,6 +15,15 @@ This allows themes to more easily theme those elements or even do things that ot
### Chat Messages
- `data-author-id` contains the id of the author
+- `data-author-username` contains the username of the author
- `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 8afc2121f..b80844546 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,14 +33,43 @@ 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 authorId = props.message?.author?.id;
+ const author = props.message?.author;
+ const authorId = author?.id;
return {
"data-author-id": authorId,
- "data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id
+ "data-author-username": author?.username,
+ "data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id,
};
}
});
diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx
index cc0ed5e93..b22c488eb 100644
--- a/src/plugins/translate/TranslateIcon.tsx
+++ b/src/plugins/translate/TranslateIcon.tsx
@@ -40,9 +40,9 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
}
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
- const { autoTranslate } = settings.use(["autoTranslate"]);
+ const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
- if (!isMainChat) return null;
+ if (!isMainChat || !showChatBarButton) return null;
const toggle = () => {
const newState = !autoTranslate;
diff --git a/src/plugins/translate/settings.ts b/src/plugins/translate/settings.ts
index cef003a83..65d845353 100644
--- a/src/plugins/translate/settings.ts
+++ b/src/plugins/translate/settings.ts
@@ -48,6 +48,11 @@ export const settings = definePluginSettings({
type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
default: false
+ },
+ showChatBarButton: {
+ type: OptionType.BOOLEAN,
+ description: "Show translate button in chat bar",
+ default: true
}
}).withPrivateSettings<{
showAutoTranslateAlert: boolean;
diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx
index 0e87201c6..16debf711 100644
--- a/src/plugins/unsuppressEmbeds/index.tsx
+++ b/src/plugins/unsuppressEmbeds/index.tsx
@@ -20,7 +20,7 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/Co
import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
-import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
+import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
const EMBED_SUPPRESSED = 1 << 2;
@@ -44,7 +44,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channe
icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}
action={() =>
RestAPI.patch({
- url: `/channels/${channel.id}/messages/${messageId}`,
+ url: Constants.Endpoints.MESSAGE(channel.id, messageId),
body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }
})
}
diff --git a/src/plugins/validReply/README.md b/src/plugins/validReply/README.md
new file mode 100644
index 000000000..49e313cf5
--- /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 000000000..21a1bdd1f
--- /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/plugins/validUser/index.tsx b/src/plugins/validUser/index.tsx
index 7a21ac86b..4825cdaa3 100644
--- a/src/plugins/validUser/index.tsx
+++ b/src/plugins/validUser/index.tsx
@@ -18,28 +18,30 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
+import { isNonNullish } from "@utils/guards";
import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue";
import definePlugin from "@utils/types";
import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
-import type { ComponentType, ReactNode } from "react";
+import { type ComponentType, type ReactNode } from "react";
// LYING to the type checker here
const UserFlags = Constants.UserFlags as Record;
const badges: Record = {
- "active_developer": { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
- "bug_hunter_level_1": { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
- "bug_hunter_level_2": { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
- "certified_moderator": { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" },
- "discord_employee": { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" },
- "hypesquad": { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" },
- "hypesquad_online_house_1": { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" },
- "hypesquad_online_house_2": { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" },
- "hypesquad_online_house_3": { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" },
- "partner": { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" },
- "premium": { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" },
- "premium_early_supporter": { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" },
- "verified_developer": { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" },
+ active_developer: { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
+ bug_hunter_level_1: { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
+ bug_hunter_level_2: { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
+ certified_moderator: { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" },
+ discord_employee: { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" },
+ get staff() { return this.discord_employee; },
+ hypesquad: { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" },
+ hypesquad_online_house_1: { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" },
+ hypesquad_online_house_2: { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" },
+ hypesquad_online_house_3: { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" },
+ partner: { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" },
+ premium: { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" },
+ premium_early_supporter: { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" },
+ verified_developer: { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" },
};
const fetching = new Set();
@@ -73,7 +75,7 @@ async function getUser(id: string) {
if (userObj)
return userObj;
- const user: any = await RestAPI.get({ url: `/users/${id}` }).then(response => {
+ const user: any = await RestAPI.get({ url: Constants.Endpoints.USER(id) }).then(response => {
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user: response.body,
@@ -93,7 +95,8 @@ async function getUser(id: string) {
userObj = UserStore.getUser(id);
const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)
.filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))
- .map(([key]) => badges[key.toLowerCase()]);
+ .map(([key]) => badges[key.toLowerCase()])
+ .filter(isNonNullish);
if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_")))
fakeBadges.push(badges.premium);
@@ -202,6 +205,7 @@ export default definePlugin({
return (
e.stopPropagation()}
+ aria-label="Download voice message"
+ {...IS_DISCORD_DESKTOP
+ ? { target: "_blank" } // open externally
+ : { download: "voice-message.ogg" } // download directly (not supported on discord desktop)
+ }
+ >
+
+
+ );
+ },
+
+ Icon: () => (
+
+
+
+ ),
+});
diff --git a/src/plugins/voiceDownload/style.css b/src/plugins/voiceDownload/style.css
new file mode 100644
index 000000000..2b776023f
--- /dev/null
+++ b/src/plugins/voiceDownload/style.css
@@ -0,0 +1,12 @@
+.vc-voice-download {
+ width: 24px;
+ height: 24px;
+ color: var(--interactive-normal);
+ margin-left: 12px;
+ cursor: pointer;
+ position: relative;
+}
+
+.vc-voice-download:hover {
+ color: var(--interactive-active);
+}
diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx
index 2f232f341..40e877df9 100644
--- a/src/plugins/voiceMessages/index.tsx
+++ b/src/plugins/voiceMessages/index.tsx
@@ -28,7 +28,7 @@ import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types";
import { chooseFile } from "@utils/web";
import { findByPropsLazy, findStoreLazy } from "@webpack";
-import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
+import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react";
import { VoiceRecorderDesktop } from "./DesktopRecorder";
@@ -98,7 +98,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
upload.on("complete", () => {
RestAPI.post({
- url: `/channels/${channelId}/messages`,
+ url: Constants.Endpoints.MESSAGES(channelId),
body: {
flags: 1 << 13,
channel_id: channelId,
diff --git a/src/plugins/webScreenShareFixes.web/index.ts b/src/plugins/webScreenShareFixes.web/index.ts
new file mode 100644
index 000000000..8d1ab5821
--- /dev/null
+++ b/src/plugins/webScreenShareFixes.web/index.ts
@@ -0,0 +1,30 @@
+/*
+ * 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";
+
+export default definePlugin({
+ name: "WebScreenShareFixes",
+ authors: [Devs.Kaitlyn],
+ description: "Removes 2500kbps bitrate cap on chromium and vesktop clients.",
+ enabledByDefault: true,
+ patches: [
+ {
+ find: "x-google-max-bitrate",
+ replacement: [
+ {
+ match: /"x-google-max-bitrate=".concat\(\i\)/,
+ replace: '"x-google-max-bitrate=".concat("80_000")'
+ },
+ {
+ match: /;level-asymmetry-allowed=1/,
+ replace: ";b=AS:800000;level-asymmetry-allowed=1"
+ }
+ ]
+ }
+ ]
+});
diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx
index b3728c215..5721dc912 100644
--- a/src/plugins/whoReacted/index.tsx
+++ b/src/plugins/whoReacted/index.tsx
@@ -23,7 +23,7 @@ import { Queue } from "@utils/Queue";
import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
-import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
+import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
import { CustomEmoji } from "@webpack/types";
import { Message, ReactionEmoji, User } from "discord-types/general";
@@ -36,7 +36,7 @@ let reactions: Record;
function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
return RestAPI.get({
- url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`,
+ url: Constants.Endpoints.REACTIONS(msg.channel_id, msg.id, key),
query: {
limit: 100,
type
diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts
index 763f6a782..5251959f2 100644
--- a/src/plugins/xsOverlay.desktop/index.ts
+++ b/src/plugins/xsOverlay.desktop/index.ts
@@ -1,6 +1,6 @@
/*
* Vencord, a Discord client mod
- * Copyright (c) 2023 Vendicated and contributors
+ * Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -13,10 +13,7 @@ import { findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
-const enum ChannelTypes {
- DM = 1,
- GROUP_DM = 3
-}
+const { ChannelTypes } = findByPropsLazy("ChannelTypes");
interface Message {
guild_id: string,
@@ -71,15 +68,35 @@ interface Call {
ringing: string[];
}
-const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled");
+const Notifs = findByPropsLazy("makeTextChatNotification");
const XSLog = new Logger("XSOverlay");
const settings = definePluginSettings({
- ignoreBots: {
+ botNotifications: {
type: OptionType.BOOLEAN,
- description: "Ignore messages from bots",
+ description: "Allow bot notifications",
default: false
},
+ serverNotifications: {
+ type: OptionType.BOOLEAN,
+ description: "Allow server notifications",
+ default: true
+ },
+ dmNotifications: {
+ type: OptionType.BOOLEAN,
+ description: "Allow Direct Message notifications",
+ default: true
+ },
+ groupDmNotifications: {
+ type: OptionType.BOOLEAN,
+ description: "Allow Group DM notifications",
+ default: true
+ },
+ callNotifications: {
+ type: OptionType.BOOLEAN,
+ description: "Allow call notifications",
+ default: true
+ },
pingColor: {
type: OptionType.STRING,
description: "User mention color",
@@ -97,8 +114,13 @@ const settings = definePluginSettings({
},
timeout: {
type: OptionType.NUMBER,
- description: "Notif duration (secs)",
- default: 1.0,
+ description: "Notification duration (secs)",
+ default: 3,
+ },
+ lengthBasedTimeout: {
+ type: OptionType.BOOLEAN,
+ description: "Extend duration with message length",
+ default: true
},
opacity: {
type: OptionType.SLIDER,
@@ -124,7 +146,7 @@ export default definePlugin({
settings,
flux: {
CALL_UPDATE({ call }: { call: Call; }) {
- if (call?.ringing?.includes(UserStore.getCurrentUser().id)) {
+ if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {
const channel = ChannelStore.getChannel(call.channel_id);
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
}
@@ -134,7 +156,7 @@ export default definePlugin({
try {
if (optimistic) return;
const channel = ChannelStore.getChannel(message.channel_id);
- if (!shouldNotify(message, channel)) return;
+ if (!shouldNotify(message, message.channel_id)) return;
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
@@ -194,6 +216,7 @@ export default definePlugin({
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `@${UserStore.getUser(id)?.username || "unknown-user"} `);
}
+ // color role mentions (unity styling btw lol)
if (message.mention_roles.length > 0) {
for (const roleId of message.mention_roles) {
const role = GuildStore.getRole(channel.guild_id, roleId);
@@ -213,6 +236,7 @@ export default definePlugin({
}
}
+ // color channel mentions
if (channelMatches) {
for (const cMatch of channelMatches) {
let channelId = cMatch.split("<#")[1];
@@ -221,6 +245,7 @@ export default definePlugin({
}
}
+ if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message);
} catch (err) {
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
@@ -229,13 +254,19 @@ export default definePlugin({
}
});
+function shouldIgnoreForChannelType(channel: Channel) {
+ if (channel.type === ChannelTypes.DM && settings.store.dmNotifications) return false;
+ if (channel.type === ChannelTypes.GROUP_DM && settings.store.groupDmNotifications) return false;
+ else return !settings.store.serverNotifications;
+}
+
function sendMsgNotif(titleString: string, content: string, message: Message) {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
const msgData = {
messageType: 1,
index: 0,
- timeout: settings.store.timeout,
- height: calculateHeight(cleanMessage(content)),
+ timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
+ height: calculateHeight(content),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
@@ -253,8 +284,8 @@ function sendOtherNotif(content: string, titleString: string) {
const msgData = {
messageType: 1,
index: 0,
- timeout: settings.store.timeout,
- height: calculateHeight(cleanMessage(content)),
+ timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
+ height: calculateHeight(content),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
@@ -267,13 +298,11 @@ function sendOtherNotif(content: string, titleString: string) {
Native.sendToOverlay(msgData);
}
-function shouldNotify(message: Message, channel: Channel) {
+function shouldNotify(message: Message, channel: string) {
const currentUser = UserStore.getCurrentUser();
if (message.author.id === currentUser.id) return false;
- if (message.author.bot && settings.store.ignoreBots) return false;
- if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true;
-
- return message.mentions.some(m => m.id === currentUser.id);
+ if (message.author.bot && !settings.store.botNotifications) return false;
+ return Notifs.shouldNotify(message, channel);
}
function calculateHeight(content: string) {
@@ -283,6 +312,9 @@ function calculateHeight(content: string) {
return 250;
}
-function cleanMessage(content: string) {
- return content.replace(new RegExp("<[^>]*>", "g"), "");
+function calculateTimeout(content: string) {
+ if (content.length <= 100) return 3;
+ if (content.length <= 200) return 4;
+ if (content.length <= 300) return 5;
+ return 6;
}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index cce276ef8..a77edf7d5 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -266,6 +266,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Dziurwa",
id: 1001086404203389018n
},
+ arHSM: {
+ name: "arHSM",
+ id: 841509053422632990n
+ },
F53: {
name: "F53",
id: 280411966126948353n
@@ -374,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
@@ -414,6 +426,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Kyuuhachi",
id: 236588665420251137n,
},
+ nin0dev: {
+ name: "nin0dev",
+ id: 886685857560539176n
+ },
Elvyra: {
name: "Elvyra",
id: 708275751816003615n,
@@ -426,6 +442,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "newwares",
id: 421405303951851520n
},
+ puv: {
+ name: "puv",
+ id: 469441552251355137n
+ },
Kodarru: {
name: "Kodarru",
id: 785227396218748949n
@@ -450,10 +470,26 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Oleh Polisan",
id: 242305263313485825n
},
+ HAHALOSAH: {
+ name: "HAHALOSAH",
+ id: 903418691268513883n
+ },
GabiRP: {
name: "GabiRP",
id: 507955112027750401n
- }
+ },
+ ImBanana: {
+ name: "Im_Banana",
+ id: 635250116688871425n
+ },
+ xocherry: {
+ name: "xocherry",
+ id: 221288171013406720n
+ },
+ ScattrdBlade: {
+ name: "ScattrdBlade",
+ id: 678007540608532491n
+ },
} satisfies Record);
// iife so #__PURE__ works correctly
diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx
index 74e1aefe8..57202ba3c 100644
--- a/src/utils/discord.tsx
+++ b/src/utils/discord.tsx
@@ -17,7 +17,7 @@
*/
import { MessageObject } from "@api/MessageEvents";
-import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
+import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
@@ -162,7 +162,7 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt
FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id });
const { body } = await RestAPI.get({
- url: `/users/${id}/profile`,
+ url: Constants.Endpoints.USER_PROFILE(id),
query: {
with_mutual_guilds: false,
with_mutual_friends_count: false,
diff --git a/src/utils/mergeDefaults.ts b/src/utils/mergeDefaults.ts
new file mode 100644
index 000000000..58ba136dd
--- /dev/null
+++ b/src/utils/mergeDefaults.ts
@@ -0,0 +1,24 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Recursively merges defaults into an object and returns the same object
+ * @param obj Object
+ * @param defaults Defaults
+ * @returns obj
+ */
+export function mergeDefaults(obj: T, defaults: T): T {
+ for (const key in defaults) {
+ const v = defaults[key];
+ if (typeof v === "object" && !Array.isArray(v)) {
+ obj[key] ??= {} as any;
+ mergeDefaults(obj[key], v);
+ } else {
+ obj[key] ??= v;
+ }
+ }
+ return obj;
+}
diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx
index 32010e59b..fb08c93f6 100644
--- a/src/utils/misc.tsx
+++ b/src/utils/misc.tsx
@@ -20,25 +20,6 @@ import { Clipboard, Toasts } from "@webpack/common";
import { DevsById } from "./constants";
-/**
- * Recursively merges defaults into an object and returns the same object
- * @param obj Object
- * @param defaults Defaults
- * @returns obj
- */
-export function mergeDefaults(obj: T, defaults: T): T {
- for (const key in defaults) {
- const v = defaults[key];
- if (typeof v === "object" && !Array.isArray(v)) {
- obj[key] ??= {} as any;
- mergeDefaults(obj[key], v);
- } else {
- obj[key] ??= v;
- }
- }
- return obj;
-}
-
/**
* Calls .join(" ") on the arguments
* classes("one", "two") => "one two"
diff --git a/src/utils/patches.ts b/src/utils/patches.ts
index 99f0595d6..87f3ce78c 100644
--- a/src/utils/patches.ts
+++ b/src/utils/patches.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { PatchReplacement, ReplaceFn } from "./types";
+import { Patch, PatchReplacement, ReplaceFn } from "./types";
export function canonicalizeMatch(match: T): T {
if (typeof match === "string") return match;
@@ -55,3 +55,9 @@ export function canonicalizeReplacement(replacement: Pick(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 c03b60507..5273df30b 100644
--- a/src/webpack/common/stores.ts
+++ b/src/webpack/common/stores.ts
@@ -28,12 +28,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;
@@ -65,7 +60,6 @@ export let DraftStore: t.DraftStore;
/**
* React hook that returns stateful data for one or more stores
* You might need a custom comparator (4th argument) if your store data is an object
- *
* @param stores The stores to listen to
* @param mapper A function that returns the data you need
* @param dependencies An array of reactive values which the hook depends on. Use this if your mapper or equality function depends on the value of another hook
@@ -73,13 +67,13 @@ export let DraftStore: t.DraftStore;
*
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/
+
export const useStateFromStores = proxyLazy(() => findByProps("useStateFromStores").useStateFromStores) as (
stores: t.FluxStore[],
mapper: () => T,
dependencies?: any,
isEqual?: (old: T, newer: T) => boolean
) => T;
-// why the fuck cant i get rid of this stupid fucking conflict
waitForStore("DraftStore", s => DraftStore = s);
waitForStore("UserStore", s => UserStore = s);
diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts
index 8e89a6e20..27715b5ee 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;
@@ -182,3 +191,10 @@ export class GuildStore extends FluxStore {
getRoles(guildId: string): Record;
getAllGuildRoles(): Record>;
}
+
+export type useStateFromStores = (
+ stores: t.FluxStore[],
+ mapper: () => T,
+ dependencies?: any,
+ isEqual?: (old: T, newer: T) => boolean
+) => T;
diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts
index 6d74e9b25..2cd636d8e 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 d3de20727..c7e424671 100644
--- a/src/webpack/patchWebpack.ts
+++ b/src/webpack/patchWebpack.ts
@@ -257,7 +257,12 @@ function patchFactories(factories: Record