diff --git a/browser/userscript.meta.js b/browser/userscript.meta.js
index 5b2a39be6..1d986aaee 100644
--- a/browser/userscript.meta.js
+++ b/browser/userscript.meta.js
@@ -5,6 +5,7 @@
// @author Vendicated (https://github.com/Vendicated)
// @namespace https://github.com/Vendicated/Vencord
// @supportURL https://github.com/Vendicated/Vencord
+// @icon https://raw.githubusercontent.com/Vendicated/Vencord/refs/heads/main/browser/icon.png
// @license GPL-3.0
// @match *://*.discord.com/*
// @grant GM_xmlhttpRequest
diff --git a/package.json b/package.json
index 65a2f4512..d5b23e57c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
- "version": "1.9.9",
+ "version": "1.10.7",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@@ -35,6 +35,7 @@
"testTsc": "tsc --noEmit"
},
"dependencies": {
+ "@intrnl/xxhash64": "^0.1.2",
"@sapphi-red/web-noise-suppressor": "0.3.5",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eaa6b537c..a62c40cd6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,6 +16,9 @@ importers:
.:
dependencies:
+ '@intrnl/xxhash64':
+ specifier: ^0.1.2
+ version: 0.1.2
'@sapphi-red/web-noise-suppressor':
specifier: 0.3.5
version: 0.3.5
@@ -537,6 +540,9 @@ packages:
resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==}
engines: {node: '>=18.18'}
+ '@intrnl/xxhash64@0.1.2':
+ resolution: {integrity: sha512-1+lx7j99fdph+uy3EnjQyr39KQZ7LP56+aWOr6finJWpgYpvb7XrhFUqDwnEk/wpPC98nCjAT6RulpW3crWjlg==}
+
'@jridgewell/gen-mapping@0.3.5':
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
@@ -2939,6 +2945,8 @@ snapshots:
'@humanwhocodes/retry@0.3.0': {}
+ '@intrnl/xxhash64@0.1.2': {}
+
'@jridgewell/gen-mapping@0.3.5':
dependencies:
'@jridgewell/set-array': 1.2.1
diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts
index 2ec9fba7c..c18bc14a3 100644
--- a/scripts/generateReport.ts
+++ b/scripts/generateReport.ts
@@ -225,7 +225,7 @@ page.on("console", async e => {
plugin,
type,
id,
- match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
+ match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"),
error: await maybeGetError(e.args()[3])
});
diff --git a/src/api/Commands/index.ts b/src/api/Commands/index.ts
index ef4db171c..e5803ba02 100644
--- a/src/api/Commands/index.ts
+++ b/src/api/Commands/index.ts
@@ -16,6 +16,7 @@
* along with this program. If not, see .
*/
+import { Logger } from "@utils/Logger";
import { makeCodeblock } from "@utils/text";
import { sendBotMessage } from "./commandHelpers";
@@ -46,10 +47,10 @@ export let RequiredMessageOption: Option = ReqPlaceholder;
export const _init = function (cmds: Command[]) {
try {
BUILT_IN = cmds;
- OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
- RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
+ OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
+ RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
} catch (e) {
- console.error("Failed to load CommandsApi");
+ new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
}
return cmds;
} as never;
@@ -138,6 +139,8 @@ export function registerCommand(command: C, plugin: string) {
throw new Error(`Command '${command.name}' already exists.`);
command.isVencordCommand = true;
+ command.untranslatedName ??= command.name;
+ command.untranslatedDescription ??= command.description;
command.id ??= `-${BUILT_IN.length + 1}`;
command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT;
diff --git a/src/api/Commands/types.ts b/src/api/Commands/types.ts
index bd349e250..70b73775a 100644
--- a/src/api/Commands/types.ts
+++ b/src/api/Commands/types.ts
@@ -93,8 +93,10 @@ export interface Command {
isVencordCommand?: boolean;
name: string;
+ untranslatedName?: string;
displayName?: string;
description: string;
+ untranslatedDescription?: string;
displayDescription?: string;
options?: Option[];
diff --git a/src/api/ContextMenu.ts b/src/api/ContextMenu.ts
index fdd4facf4..114942ff6 100644
--- a/src/api/ContextMenu.ts
+++ b/src/api/ContextMenu.ts
@@ -90,19 +90,20 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
* A helper function for finding the children array of a group nested inside a context menu based on the id(s) of its children
* @param id The id of the child. If an array is specified, all ids will be tried
* @param children The context menu children
+ * @param matchSubstring Whether to check if the id is a substring of the child id
*/
-export function findGroupChildrenByChildId(id: string | string[], children: Array): Array | null {
+export function findGroupChildrenByChildId(id: string | string[], children: Array, matchSubstring = false): Array | null {
for (const child of children) {
if (child == null) continue;
if (Array.isArray(child)) {
- const found = findGroupChildrenByChildId(id, child);
+ const found = findGroupChildrenByChildId(id, child, matchSubstring);
if (found !== null) return found;
}
if (
- (Array.isArray(id) && id.some(id => child.props?.id === id))
- || child.props?.id === id
+ (Array.isArray(id) && id.some(id => matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id))
+ || (matchSubstring ? child.props?.id?.includes(id) : child.props?.id === id)
) return children;
let nextChildren = child.props?.children;
@@ -112,7 +113,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
child.props.children = nextChildren;
}
- const found = findGroupChildrenByChildId(id, nextChildren);
+ const found = findGroupChildrenByChildId(id, nextChildren, matchSubstring);
if (found !== null) return found;
}
}
diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx
index fa142a18c..d0d2ecbe8 100644
--- a/src/components/Icons.tsx
+++ b/src/components/Icons.tsx
@@ -18,9 +18,8 @@
import "./iconStyles.css";
-import { getTheme, Theme } from "@utils/discord";
+import { getIntlMessage, getTheme, Theme } from "@utils/discord";
import { classes } from "@utils/misc";
-import { i18n } from "@webpack/common";
import type { PropsWithChildren } from "react";
interface BaseIconProps extends IconProps {
@@ -133,7 +132,7 @@ export function InfoIcon(props: IconProps) {
export function OwnerCrownIcon(props: IconProps) {
return (
.
*/
+import { Margins } from "@utils/margins";
+import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { OptionType, PluginOptionNumber } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
@@ -54,7 +56,8 @@ export function SettingNumericComponent({ option, pluginSettings, definedSetting
return (
- {option.description}
+ {wordsToTitle(wordsFromCamel(id))}
+ {option.description}
.
*/
+import { Margins } from "@utils/margins";
+import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSelect } from "@utils/types";
import { Forms, React, Select } from "@webpack/common";
@@ -44,7 +46,8 @@ export function SettingSelectComponent({ option, pluginSettings, definedSettings
return (
- {option.description}
+ {wordsToTitle(wordsFromCamel(id))}
+ {option.description}
.
*/
+import { Margins } from "@utils/margins";
+import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSlider } from "@utils/types";
import { Forms, React, Slider } from "@webpack/common";
@@ -50,7 +52,8 @@ export function SettingSliderComponent({ option, pluginSettings, definedSettings
return (
- {option.description}
+ {wordsToTitle(wordsFromCamel(id))}
+ {option.description}
.
*/
+import { Margins } from "@utils/margins";
+import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionString } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
@@ -41,7 +43,8 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
return (
- {option.description}
+ {wordsToTitle(wordsFromCamel(id))}
+ {option.description}
{
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = Settings.plugins[plugin.name];
- const isEnabled = () => settings.enabled ?? false;
+ const isEnabled = () => Vencord.Plugins.isPluginEnabled(plugin.name);
function toggleEnabled() {
const wasEnabled = isEnabled();
@@ -292,10 +292,10 @@ export default function PluginSettings() {
if (!pluginFilter(p)) continue;
- const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
+ const isRequired = p.required || p.isDependency || depMap[p.name]?.some(d => settings.plugins[d].enabled);
if (isRequired) {
- const tooltipText = p.required
+ const tooltipText = p.required || !depMap[p.name]
? "This plugin is required for Vencord to function."
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx
index fd33c09df..c11551873 100644
--- a/src/components/VencordSettings/PatchHelperTab.tsx
+++ b/src/components/VencordSettings/PatchHelperTab.tsx
@@ -247,7 +247,7 @@ function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: Fu
}
try {
- const parsed = (0, eval)(`(${fullPatch})`) as Patch;
+ const parsed = (0, eval)(`([${fullPatch}][0])`) as Patch;
if (!parsed.find) throw new Error("No 'find' field");
if (!parsed.replacement) throw new Error("No 'replacement' field");
diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts
index 73a89504f..c7f8047db 100644
--- a/src/debug/loadLazyChunks.ts
+++ b/src/debug/loadLazyChunks.ts
@@ -27,13 +27,16 @@ export async function loadLazyChunks() {
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
+ let foundCssDebuggingLoad = false;
+
async function searchAndLoadLazyChunks(factoryCode: string) {
+ // Workaround to avoid loading the CSS debugging chunk which turns the app pink
+ const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
+
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
- // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
- // the chunk containing the component
- const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
+ const shouldForceDefer = false;
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
@@ -45,6 +48,16 @@ export async function loadLazyChunks() {
let invalidChunkGroup = false;
for (const id of chunkIds) {
+ if (hasCssDebuggingLoad) {
+ if (chunkIds.length > 1) {
+ throw new Error("Found multiple chunks in factory that loads the CSS debugging chunk");
+ }
+
+ invalidChunks.add(id);
+ invalidChunkGroup = true;
+ break;
+ }
+
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
diff --git a/src/main/patcher.ts b/src/main/patcher.ts
index e5b87290d..e858f3fcd 100644
--- a/src/main/patcher.ts
+++ b/src/main/patcher.ts
@@ -17,7 +17,7 @@
*/
import { onceDefined } from "@shared/onceDefined";
-import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
+import electron, { app, BrowserWindowConstructorOptions, Menu, nativeTheme } from "electron";
import { dirname, join } from "path";
import { initIpc } from "./ipcMain";
@@ -100,6 +100,19 @@ if (!IS_VANILLA) {
super(options);
initIpc(this);
+
+ // Workaround for https://github.com/electron/electron/issues/43367. Vesktop also has its own workaround
+ // @TODO: Remove this when the issue is fixed
+ if (IS_DISCORD_DESKTOP) {
+ this.webContents.on("devtools-opened", () => {
+ if (!nativeTheme.shouldUseDarkColors) return;
+
+ nativeTheme.themeSource = "light";
+ setTimeout(() => {
+ nativeTheme.themeSource = "dark";
+ }, 100);
+ });
+ }
} else super(options);
}
}
diff --git a/src/plugins/_api/badges/fixBadgeOverflow.css b/src/plugins/_api/badges/fixBadgeOverflow.css
deleted file mode 100644
index 348d0ff38..000000000
--- a/src/plugins/_api/badges/fixBadgeOverflow.css
+++ /dev/null
@@ -1,3 +0,0 @@
-[class*="profileBadges"] {
- flex: none;
-}
diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx
index cf00a0e29..c44d98b90 100644
--- a/src/plugins/_api/badges/index.tsx
+++ b/src/plugins/_api/badges/index.tsx
@@ -16,8 +16,6 @@
* along with this program. If not, see .
*/
-import "./fixBadgeOverflow.css";
-
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
@@ -79,7 +77,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??"
},
{
- match: /(?<=text:(\i)\.description,.{0,50})children:/,
+ match: /(?<=text:(\i)\.description,.{0,200})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
},
// conditionally override their onClick with badge.onClick if it exists
diff --git a/src/plugins/_api/dynamicImageModalApi.ts b/src/plugins/_api/dynamicImageModalApi.ts
new file mode 100644
index 000000000..2ce51400d
--- /dev/null
+++ b/src/plugins/_api/dynamicImageModalApi.ts
@@ -0,0 +1,24 @@
+/*
+ * 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: "DynamicImageModalAPI",
+ authors: [Devs.sadan, Devs.Nuckyz],
+ description: "Allows you to omit either width or height when opening an image modal",
+ patches: [
+ {
+ find: "SCALE_DOWN:",
+ replacement: {
+ match: /!\(null==(\i)\|\|0===\i\|\|null==(\i)\|\|0===\i\)/,
+ replace: (_, width, height) => `!((null == ${width} || 0 === ${width}) && (null == ${height} || 0 === ${height}))`
+ }
+ }
+ ]
+});
diff --git a/src/plugins/_api/memberListDecorators.ts b/src/plugins/_api/memberListDecorators.ts
index 5e3e5ed18..0dba3608f 100644
--- a/src/plugins/_api/memberListDecorators.ts
+++ b/src/plugins/_api/memberListDecorators.ts
@@ -31,7 +31,7 @@ export default definePlugin({
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
replace: "$&vencordProps=$1,"
}, {
- match: /\.Messages\.GUILD_OWNER(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
+ match: /#{intl::GUILD_OWNER}(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
}
]
diff --git a/src/plugins/_api/messageAccessories.ts b/src/plugins/_api/messageAccessories.ts
index a98fdb32b..0ba2a031d 100644
--- a/src/plugins/_api/messageAccessories.ts
+++ b/src/plugins/_api/messageAccessories.ts
@@ -25,7 +25,7 @@ export default definePlugin({
authors: [Devs.Cyn],
patches: [
{
- find: ".Messages.REMOVE_ATTACHMENT_BODY",
+ find: "#{intl::REMOVE_ATTACHMENT_BODY}",
replacement: {
match: /(?<=.container\)?,children:)(\[.+?\])/,
replace: "Vencord.Api.MessageAccessories._modifyAccessories($1,this.props)",
diff --git a/src/plugins/_api/messageDecorations.ts b/src/plugins/_api/messageDecorations.ts
index b41ec0be9..fb63a6dde 100644
--- a/src/plugins/_api/messageDecorations.ts
+++ b/src/plugins/_api/messageDecorations.ts
@@ -27,7 +27,7 @@ export default definePlugin({
{
find: '"Message Username"',
replacement: {
- match: /\.Messages\.GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE.+?}\),\i(?=\])/,
+ match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?}\),\i(?=\])/,
replace: "$&,...Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
}
}
diff --git a/src/plugins/_api/messageEvents.ts b/src/plugins/_api/messageEvents.ts
index 0347d5445..0101b02c8 100644
--- a/src/plugins/_api/messageEvents.ts
+++ b/src/plugins/_api/messageEvents.ts
@@ -25,7 +25,7 @@ export default definePlugin({
authors: [Devs.Arjix, Devs.hunt, Devs.Ven],
patches: [
{
- find: ".Messages.EDIT_TEXTAREA_HELP",
+ find: "#{intl::EDIT_TEXTAREA_HELP}",
replacement: {
match: /(?<=,channel:\i\}\)\.then\().+?(?=return \i\.content!==this\.props\.message\.content&&\i\((.+?)\))/,
replace: (match, args) => "" +
diff --git a/src/plugins/_api/messagePopover.ts b/src/plugins/_api/messagePopover.ts
index 57b9b1193..21e5accde 100644
--- a/src/plugins/_api/messagePopover.ts
+++ b/src/plugins/_api/messagePopover.ts
@@ -24,9 +24,9 @@ export default definePlugin({
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
patches: [{
- find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
+ find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: {
- match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.Messages\.MESSAGE_ACTION_REPLY.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/,
+ match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.string\(\i\.\i#{intl::MESSAGE_ACTION_REPLY}.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/,
replace: "$&,Vencord.Api.MessagePopover._buildPopoverElements($1,$2)"
}
}],
diff --git a/src/plugins/_api/serverList.ts b/src/plugins/_api/serverList.ts
index f45bbf104..dfd40de74 100644
--- a/src/plugins/_api/serverList.ts
+++ b/src/plugins/_api/serverList.ts
@@ -25,16 +25,16 @@ export default definePlugin({
description: "Api required for plugins that modify the server list",
patches: [
{
- find: "Messages.DISCODO_DISABLED",
+ find: "#{intl::DISCODO_DISABLED}",
replacement: {
- match: /(?<=Messages\.DISCODO_DISABLED.+?return)(\(.{0,75}?tutorialContainer.+?}\))(?=}function)/,
+ match: /(?<=#{intl::DISCODO_DISABLED}.+?return)(\(.{0,75}?tutorialContainer.+?}\))(?=}function)/,
replace: "[$1].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))"
}
},
{
- find: "Messages.SERVERS,children",
+ find: "#{intl::SERVERS}),children",
replacement: {
- match: /(?<=Messages\.SERVERS,children:).+?default:return null\}\}\)/,
+ match: /(?<=#{intl::SERVERS}\),children:)\i\.map\(\i\)/,
replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)"
}
}
diff --git a/src/plugins/_core/noTrack.ts b/src/plugins/_core/noTrack.ts
index de1c20562..802e1c95a 100644
--- a/src/plugins/_core/noTrack.ts
+++ b/src/plugins/_core/noTrack.ts
@@ -48,7 +48,7 @@ export default definePlugin({
},
},
{
- find: ".METRICS,",
+ find: ".METRICS",
replacement: [
{
match: /this\._intervalId=/,
@@ -59,15 +59,7 @@ export default definePlugin({
replace: "$&return;"
}
]
- },
- {
- find: ".installedLogHooks)",
- replacement: {
- // if getDebugLogging() returns false, the hooks don't get installed.
- match: "getDebugLogging(){",
- replace: "getDebugLogging(){return false;"
- }
- },
+ }
],
startAt: StartAt.Init,
diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx
index be220db1a..d58c7a98c 100644
--- a/src/plugins/_core/settings.tsx
+++ b/src/plugins/_core/settings.tsx
@@ -25,8 +25,9 @@ import ThemesTab from "@components/VencordSettings/ThemesTab";
import UpdaterTab from "@components/VencordSettings/UpdaterTab";
import VencordTab from "@components/VencordSettings/VencordTab";
import { Devs } from "@utils/constants";
+import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
-import { i18n, React } from "@webpack/common";
+import { React } from "@webpack/common";
import gitHash from "~git-hash";
@@ -57,20 +58,20 @@ export default definePlugin({
]
},
{
- find: "Messages.ACTIVITY_SETTINGS",
+ find: ".SEARCH_NO_RESULTS&&0===",
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}`
},
{
- match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,60}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
+ match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
}
]
},
{
- find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
+ find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}",
replacement: {
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.\i\).*?(\i\.\i)\.open\()/,
replace: "$2.open($1);return;"
@@ -148,13 +149,18 @@ export default definePlugin({
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];
+ try {
+ const names = {
+ top: getIntlMessage("USER_SETTINGS"),
+ aboveNitro: getIntlMessage("BILLING_SETTINGS"),
+ belowNitro: getIntlMessage("APP_SETTINGS"),
+ aboveActivity: getIntlMessage("ACTIVITY_SETTINGS")
+ };
+
+ return header === names[settingsLocation];
+ } catch {
+ return firstChild === "PREMIUM";
+ }
},
patchedSettings: new WeakSet(),
@@ -197,7 +203,7 @@ export default definePlugin({
},
get electronVersion() {
- return VencordNative.native.getVersions().electron || window.armcord?.electron || null;
+ return VencordNative.native.getVersions().electron || window.legcord?.electron || null;
},
get chromiumVersion() {
diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx
index de8e37c79..1b9ce162b 100644
--- a/src/plugins/_core/supportHelper.tsx
+++ b/src/plugins/_core/supportHelper.tsx
@@ -77,7 +77,7 @@ async function generateDebugInfoMessage() {
const client = (() => {
if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;
if (IS_VESKTOP) return `Vesktop v${VesktopNative.app.getVersion()}`;
- if ("armcord" in window) return `ArmCord v${window.armcord.version}`;
+ if ("legcord" in window) return `Legcord v${window.legcord.version}`;
// @ts-expect-error
const name = typeof unsafeWindow !== "undefined" ? "UserScript" : "Web";
@@ -142,15 +142,15 @@ export default definePlugin({
required: true,
description: "Helps us provide support to you",
authors: [Devs.Ven],
- dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
+ dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"],
settings,
patches: [{
- find: ".BEGINNING_DM.format",
+ find: "#{intl::BEGINNING_DM}",
replacement: {
- match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
- replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
+ match: /#{intl::BEGINNING_DM},{.+?}\),(?=.{0,300}(\i)\.isMultiUserDM)/,
+ replace: "$& $self.renderContributorDmWarningCard({ channel: $1 }),"
}
}],
@@ -235,7 +235,8 @@ export default definePlugin({
}
},
- ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
+ renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
+ const userId = channel.getRecipientId();
if (!isPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
diff --git a/src/plugins/accountPanelServerProfile/README.md b/src/plugins/accountPanelServerProfile/README.md
new file mode 100644
index 000000000..f837864b7
--- /dev/null
+++ b/src/plugins/accountPanelServerProfile/README.md
@@ -0,0 +1,7 @@
+# AccountPanelServerProfile
+
+Right click your account panel in the bottom left to view your profile in the current server
+
+![](https://github.com/user-attachments/assets/3228497d-488f-479c-93d2-a32ccdb08f0f)
+
+![](https://github.com/user-attachments/assets/6fc45363-d95f-4810-812f-2f9fb28b41b5)
diff --git a/src/plugins/accountPanelServerProfile/index.tsx b/src/plugins/accountPanelServerProfile/index.tsx
new file mode 100644
index 000000000..fcecffb17
--- /dev/null
+++ b/src/plugins/accountPanelServerProfile/index.tsx
@@ -0,0 +1,134 @@
+/*
+ * 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 { getCurrentChannel } from "@utils/discord";
+import definePlugin, { OptionType } from "@utils/types";
+import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
+import { ContextMenuApi, Menu, useEffect, useRef } from "@webpack/common";
+import { User } from "discord-types/general";
+
+interface UserProfileProps {
+ popoutProps: Record;
+ currentUser: User;
+ originalPopout: () => React.ReactNode;
+}
+
+const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined");
+const styles = findByPropsLazy("accountProfilePopoutWrapper");
+
+let openAlternatePopout = false;
+let accountPanelRef: React.MutableRefObject | null> = { current: null };
+
+const AccountPanelContextMenu = ErrorBoundary.wrap(() => {
+ const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]);
+
+ return (
+
+ {
+ openAlternatePopout = true;
+ accountPanelRef.current?.props.onMouseDown();
+ accountPanelRef.current?.props.onClick(e);
+ }}
+ />
+ settings.store.prioritizeServerProfile = !prioritizeServerProfile}
+ />
+
+ );
+}, { noop: true });
+
+const settings = definePluginSettings({
+ prioritizeServerProfile: {
+ type: OptionType.BOOLEAN,
+ description: "Prioritize Server Profile when left clicking your account panel",
+ default: false
+ }
+});
+
+export default definePlugin({
+ name: "AccountPanelServerProfile",
+ description: "Right click your account panel in the bottom left to view your profile in the current server",
+ authors: [Devs.Nuckyz, Devs.relitrix],
+ settings,
+
+ patches: [
+ {
+ find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
+ group: true,
+ replacement: [
+ {
+ match: /(?<=\.SIZE_32\)}\);)/,
+ replace: "$self.useAccountPanelRef();"
+ },
+ {
+ match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
+ replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalPopout:()=>{${originalPopout}}})`
+ },
+ {
+ match: /\.AVATAR,children:.+?(?=renderPopout:)/,
+ replace: "$&onRequestClose:$self.onPopoutClose,"
+ },
+ {
+ match: /(?<=.avatarWrapper,)/,
+ replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu,"
+ }
+ ]
+ }
+ ],
+
+ get accountPanelRef() {
+ return accountPanelRef;
+ },
+
+ useAccountPanelRef() {
+ useEffect(() => () => {
+ accountPanelRef.current = null;
+ }, []);
+
+ return (accountPanelRef = useRef(null));
+ },
+
+ openAccountPanelContextMenu(event: React.UIEvent) {
+ ContextMenuApi.openContextMenu(event, AccountPanelContextMenu);
+ },
+
+ onPopoutClose() {
+ openAlternatePopout = false;
+ },
+
+ UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalPopout }: UserProfileProps) => {
+ if (
+ (settings.store.prioritizeServerProfile && openAlternatePopout) ||
+ (!settings.store.prioritizeServerProfile && !openAlternatePopout)
+ ) {
+ return originalPopout();
+ }
+
+ const currentChannel = getCurrentChannel();
+ if (currentChannel?.getGuildId() == null) {
+ return originalPopout();
+ }
+
+ return (
+
+
+
+ );
+ }, { noop: true })
+});
diff --git a/src/plugins/alwaysAnimate/index.ts b/src/plugins/alwaysAnimate/index.ts
index 20cb4f974..97593990c 100644
--- a/src/plugins/alwaysAnimate/index.ts
+++ b/src/plugins/alwaysAnimate/index.ts
@@ -41,7 +41,7 @@ export default definePlugin({
},
{
// Status emojis
- find: ".Messages.GUILD_OWNER,",
+ find: "#{intl::GUILD_OWNER}",
replacement: {
match: /(?<=\.activityEmoji,.+?animate:)\i/,
replace: "!0"
diff --git a/src/plugins/alwaysExpandRoles/index.ts b/src/plugins/alwaysExpandRoles/index.ts
index 1c20b9777..c674f90c5 100644
--- a/src/plugins/alwaysExpandRoles/index.ts
+++ b/src/plugins/alwaysExpandRoles/index.ts
@@ -28,10 +28,18 @@ export default definePlugin({
patches: [
{
find: 'action:"EXPAND_ROLES"',
- replacement: {
- match: /(roles:\i(?=.+?(\i)\(!0\)[,;]\i\({action:"EXPAND_ROLES"}\)).+?\[\i,\2\]=\i\.useState\()!1\)/,
- replace: (_, rest, setExpandedRoles) => `${rest}!0)`
- }
+ replacement: [
+ {
+ match: /(roles:\i(?=.+?(\i)\(!0\)[,;]\i\({action:"EXPAND_ROLES"}\)).+?\[\i,\2\]=\i\.useState\()!1\)/,
+ replace: (_, rest, setExpandedRoles) => `${rest}!0)`
+ },
+ {
+ // Fix not calculating non-expanded roles because the above patch makes the default "expanded",
+ // which makes the collapse button never show up and calculation never occur
+ match: /(?<=useLayoutEffect\(\(\)=>{if\()\i/,
+ replace: isExpanded => "false"
+ }
+ ]
}
]
});
diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx
index 526ccd12e..21f4e5c8a 100644
--- a/src/plugins/anonymiseFileNames/index.tsx
+++ b/src/plugins/anonymiseFileNames/index.tsx
@@ -71,7 +71,7 @@ export default definePlugin({
description: "Anonymise uploaded file names",
patches: [
{
- find: "instantBatchUpload:function",
+ find: "instantBatchUpload:",
replacement: {
match: /uploadFiles:(\i),/,
replace:
@@ -86,9 +86,9 @@ export default definePlugin({
}
},
{
- find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
+ find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",
replacement: {
- match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/,
+ match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}#{intl::ATTACHMENT_UTILITIES_SPOILER})/,
replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null,"
},
},
diff --git a/src/plugins/appleMusic.desktop/index.tsx b/src/plugins/appleMusic.desktop/index.tsx
index 6fa989cdd..f3148c36d 100644
--- a/src/plugins/appleMusic.desktop/index.tsx
+++ b/src/plugins/appleMusic.desktop/index.tsx
@@ -24,7 +24,7 @@ interface ActivityButton {
}
interface Activity {
- state: string;
+ state?: string;
details?: string;
timestamps?: {
start?: number;
@@ -52,8 +52,8 @@ const enum ActivityFlag {
export interface TrackData {
name: string;
- album: string;
- artist: string;
+ album?: string;
+ artist?: string;
appleMusicLink?: string;
songLink?: string;
@@ -61,8 +61,8 @@ export interface TrackData {
albumArtwork?: string;
artistArtwork?: string;
- playerPosition: number;
- duration: number;
+ playerPosition?: number;
+ duration?: number;
}
const enum AssetImageType {
@@ -120,7 +120,7 @@ const settings = definePluginSettings({
stateString: {
type: OptionType.STRING,
description: "Activity state format string",
- default: "{artist}"
+ default: "{artist} ยท {album}"
},
largeImageType: {
type: OptionType.SELECT,
@@ -155,8 +155,8 @@ const settings = definePluginSettings({
function customFormat(formatStr: string, data: TrackData) {
return formatStr
.replaceAll("{name}", data.name)
- .replaceAll("{album}", data.album)
- .replaceAll("{artist}", data.artist);
+ .replaceAll("{album}", data.album ?? "")
+ .replaceAll("{artist}", data.artist ?? "");
}
function getImageAsset(type: AssetImageType, data: TrackData) {
@@ -212,14 +212,16 @@ export default definePlugin({
const assets: ActivityAssets = {};
+ const isRadio = Number.isNaN(trackData.duration) && (trackData.playerPosition === 0);
+
if (settings.store.largeImageType !== AssetImageType.Disabled) {
assets.large_image = largeImageAsset;
- assets.large_text = customFormat(settings.store.largeTextString, trackData);
+ if (!isRadio) assets.large_text = customFormat(settings.store.largeTextString, trackData);
}
if (settings.store.smallImageType !== AssetImageType.Disabled) {
assets.small_image = smallImageAsset;
- assets.small_text = customFormat(settings.store.smallTextString, trackData);
+ if (!isRadio) assets.small_text = customFormat(settings.store.smallTextString, trackData);
}
const buttons: ActivityButton[] = [];
@@ -243,17 +245,17 @@ export default definePlugin({
name: customFormat(settings.store.nameString, trackData),
details: customFormat(settings.store.detailsString, trackData),
- state: customFormat(settings.store.stateString, trackData),
+ state: isRadio ? undefined : customFormat(settings.store.stateString, trackData),
- timestamps: (settings.store.enableTimestamps ? {
+ timestamps: (trackData.playerPosition && trackData.duration && settings.store.enableTimestamps) ? {
start: Date.now() - (trackData.playerPosition * 1000),
end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
- } : undefined),
+ } : undefined,
assets,
- buttons: buttons.length ? buttons.map(v => v.label) : undefined,
- metadata: { button_urls: buttons.map(v => v.url) || undefined, },
+ buttons: !isRadio && buttons.length ? buttons.map(v => v.label) : undefined,
+ metadata: !isRadio && buttons.length ? { button_urls: buttons.map(v => v.url) } : undefined,
type: settings.store.activityType,
flags: ActivityFlag.INSTANCE,
diff --git a/src/plugins/appleMusic.desktop/native.ts b/src/plugins/appleMusic.desktop/native.ts
index 2eb2a0757..7d69a85ae 100644
--- a/src/plugins/appleMusic.desktop/native.ts
+++ b/src/plugins/appleMusic.desktop/native.ts
@@ -11,37 +11,11 @@ import type { TrackData } from ".";
const exec = promisify(execFile);
-// function exec(file: string, args: string[] = []) {
-// return new Promise<{ code: number | null, stdout: string | null, stderr: string | null; }>((resolve, reject) => {
-// const process = spawn(file, args, { stdio: [null, "pipe", "pipe"] });
-
-// let stdout: string | null = null;
-// process.stdout.on("data", (chunk: string) => { stdout ??= ""; stdout += chunk; });
-// let stderr: string | null = null;
-// process.stderr.on("data", (chunk: string) => { stdout ??= ""; stderr += chunk; });
-
-// process.on("exit", code => { resolve({ code, stdout, stderr }); });
-// process.on("error", err => reject(err));
-// });
-// }
-
async function applescript(cmds: string[]) {
const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
return stdout;
}
-function makeSearchUrl(type: string, query: string) {
- const url = new URL("https://tools.applemediaservices.com/api/apple-media/music/US/search.json");
- url.searchParams.set("types", type);
- url.searchParams.set("limit", "1");
- url.searchParams.set("term", query);
- return url;
-}
-
-const requestOptions: RequestInit = {
- headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; rv:125.0) Gecko/20100101 Firefox/125.0" },
-};
-
interface RemoteData {
appleMusicLink?: string,
songLink?: string,
@@ -51,6 +25,24 @@ interface RemoteData {
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
+const APPLE_MUSIC_BUNDLE_REGEX = /