1
0
Fork 1
mirror of https://github.com/Vendicated/Vencord.git synced 2025-01-10 18:06:22 +00:00

Merge branch 'dev' into TablistApi

This commit is contained in:
programminglaboratorys 2024-06-24 04:01:00 +03:00 committed by GitHub
commit 3a916b37ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 713 additions and 312 deletions

View file

@ -2,23 +2,22 @@ if (typeof browser === "undefined") {
var browser = chrome; var browser = chrome;
} }
const script = document.createElement("script");
script.src = browser.runtime.getURL("dist/Vencord.js");
script.id = "vencord-script";
Object.assign(script.dataset, {
extensionBaseUrl: browser.runtime.getURL(""),
version: browser.runtime.getManifest().version
});
const style = document.createElement("link"); const style = document.createElement("link");
style.type = "text/css"; style.type = "text/css";
style.rel = "stylesheet"; style.rel = "stylesheet";
style.href = browser.runtime.getURL("dist/Vencord.css"); style.href = browser.runtime.getURL("dist/Vencord.css");
document.documentElement.append(script);
document.addEventListener( document.addEventListener(
"DOMContentLoaded", "DOMContentLoaded",
() => document.documentElement.append(style), () => {
document.documentElement.append(style);
window.postMessage({
type: "vencord:meta",
meta: {
EXTENSION_VERSION: browser.runtime.getManifest().version,
EXTENSION_BASE_URL: browser.runtime.getURL(""),
}
});
},
{ once: true } { once: true }
); );

View file

@ -22,7 +22,15 @@
"run_at": "document_start", "run_at": "document_start",
"matches": ["*://*.discord.com/*"], "matches": ["*://*.discord.com/*"],
"js": ["content.js"], "js": ["content.js"],
"all_frames": true "all_frames": true,
"world": "ISOLATED"
},
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["dist/Vencord.js"],
"all_frames": true,
"world": "MAIN"
} }
], ],

View file

@ -22,7 +22,15 @@
"run_at": "document_start", "run_at": "document_start",
"matches": ["*://*.discord.com/*"], "matches": ["*://*.discord.com/*"],
"js": ["content.js"], "js": ["content.js"],
"all_frames": true "all_frames": true,
"world": "ISOLATED"
},
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["dist/Vencord.js"],
"all_frames": true,
"world": "MAIN"
} }
], ],

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.9.0", "version": "1.9.1",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -43,7 +43,7 @@
"eslint-plugin-simple-header": "^1.0.2", "eslint-plugin-simple-header": "^1.0.2",
"fflate": "^0.7.4", "fflate": "^0.7.4",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.43.0", "monaco-editor": "^0.50.0",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"virtual-merge": "^1.0.1" "virtual-merge": "^1.0.1"
}, },

View file

@ -35,8 +35,8 @@ importers:
specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3 specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3
version: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3 version: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3
monaco-editor: monaco-editor:
specifier: ^0.43.0 specifier: ^0.50.0
version: 0.43.0 version: 0.50.0
nanoid: nanoid:
specifier: ^4.0.2 specifier: ^4.0.2
version: 4.0.2 version: 4.0.2
@ -1531,6 +1531,10 @@ packages:
is-core-module@2.13.1: is-core-module@2.13.1:
resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
is-core-module@2.14.0:
resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==}
engines: {node: '>= 0.4'}
is-data-descriptor@0.1.4: is-data-descriptor@0.1.4:
resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1791,8 +1795,8 @@ packages:
moment@2.29.4: moment@2.29.4:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
monaco-editor@0.43.0: monaco-editor@0.50.0:
resolution: {integrity: sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q==} resolution: {integrity: sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==}
ms@2.0.0: ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@ -3463,7 +3467,7 @@ snapshots:
eslint-import-resolver-node@0.3.9: eslint-import-resolver-node@0.3.9:
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
is-core-module: 2.13.1 is-core-module: 2.14.0
resolve: 1.22.8 resolve: 1.22.8
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -3490,7 +3494,7 @@ snapshots:
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.1(eslint@8.46.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.46.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)) eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.1(eslint@8.46.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.46.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.13.1 is-core-module: 2.14.0
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 3.1.2 minimatch: 3.1.2
object.fromentries: 2.0.8 object.fromentries: 2.0.8
@ -3934,6 +3938,10 @@ snapshots:
dependencies: dependencies:
hasown: 2.0.2 hasown: 2.0.2
is-core-module@2.14.0:
dependencies:
hasown: 2.0.2
is-data-descriptor@0.1.4: is-data-descriptor@0.1.4:
dependencies: dependencies:
kind-of: 3.2.2 kind-of: 3.2.2
@ -4169,7 +4177,7 @@ snapshots:
moment@2.29.4: {} moment@2.29.4: {}
monaco-editor@0.43.0: {} monaco-editor@0.50.0: {}
ms@2.0.0: {} ms@2.0.0: {}

View file

@ -21,7 +21,7 @@ import esbuild from "esbuild";
import { readdir } from "fs/promises"; import { readdir } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, VERSION, watch } from "./common.mjs"; import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, watch } from "./common.mjs";
const defines = { const defines = {
IS_STANDALONE, IS_STANDALONE,
@ -76,22 +76,20 @@ const globNativesPlugin = {
for (const dir of pluginDirs) { for (const dir of pluginDirs) {
const dirPath = join("src", dir); const dirPath = join("src", dir);
if (!await exists(dirPath)) continue; if (!await exists(dirPath)) continue;
const plugins = await readdir(dirPath); const plugins = await readdir(dirPath, { withFileTypes: true });
for (const p of plugins) { for (const file of plugins) {
const nativePath = join(dirPath, p, "native.ts"); const fileName = file.name;
const indexNativePath = join(dirPath, p, "native/index.ts"); const nativePath = join(dirPath, fileName, "native.ts");
const indexNativePath = join(dirPath, fileName, "native/index.ts");
if (!(await exists(nativePath)) && !(await exists(indexNativePath))) if (!(await exists(nativePath)) && !(await exists(indexNativePath)))
continue; continue;
const nameParts = p.split("."); const pluginName = await resolvePluginName(dirPath, file);
const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);
// pluginName.thing.desktop -> PluginName.thing
const cleanPluginName = p[0].toUpperCase() + namePartsWithoutTarget.join(".").slice(1);
const mod = `p${i}`; const mod = `p${i}`;
code += `import * as ${mod} from "./${dir}/${p}/native";\n`; code += `import * as ${mod} from "./${dir}/${fileName}/native";\n`;
natives += `${JSON.stringify(cleanPluginName)}:${mod},\n`; natives += `${JSON.stringify(pluginName)}:${mod},\n`;
i++; i++;
} }
} }

View file

@ -53,6 +53,32 @@ export const banner = {
`.trim() `.trim()
}; };
const PluginDefinitionNameMatcher = /definePlugin\(\{\s*(["'])?name\1:\s*(["'`])(.+?)\2/;
/**
* @param {string} base
* @param {import("fs").Dirent} dirent
*/
export async function resolvePluginName(base, dirent) {
const fullPath = join(base, dirent.name);
const content = dirent.isFile()
? await readFile(fullPath, "utf-8")
: await (async () => {
for (const file of ["index.ts", "index.tsx"]) {
try {
return await readFile(join(fullPath, file), "utf-8");
} catch {
continue;
}
}
throw new Error(`Invalid plugin ${fullPath}: could not resolve entry point`);
})();
return PluginDefinitionNameMatcher.exec(content)?.[3]
?? (() => {
throw new Error(`Invalid plugin ${fullPath}: must contain definePlugin call with simple string name property as first property`);
})();
}
export async function exists(path) { export async function exists(path) {
return await access(path, FsConstants.F_OK) return await access(path, FsConstants.F_OK)
.then(() => true) .then(() => true)
@ -88,14 +114,16 @@ export const globPlugins = kind => ({
build.onLoad({ filter, namespace: "import-plugins" }, async () => { build.onLoad({ filter, namespace: "import-plugins" }, async () => {
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"]; const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"];
let code = ""; let code = "";
let plugins = "\n"; let pluginsCode = "\n";
let meta = "\n"; let metaCode = "\n";
let excludedCode = "\n";
let i = 0; let i = 0;
for (const dir of pluginDirs) { for (const dir of pluginDirs) {
const userPlugin = dir === "userplugins"; const userPlugin = dir === "userplugins";
if (!await exists(`./src/${dir}`)) continue; const fullDir = `./src/${dir}`;
const files = await readdir(`./src/${dir}`, { withFileTypes: true }); if (!await exists(fullDir)) continue;
const files = await readdir(fullDir, { withFileTypes: true });
for (const file of files) { for (const file of files) {
const fileName = file.name; const fileName = file.name;
if (fileName.startsWith("_") || fileName.startsWith(".")) continue; if (fileName.startsWith("_") || fileName.startsWith(".")) continue;
@ -104,23 +132,30 @@ export const globPlugins = kind => ({
const target = getPluginTarget(fileName); const target = getPluginTarget(fileName);
if (target && !IS_REPORTER) { if (target && !IS_REPORTER) {
if (target === "dev" && !watch) continue; const excluded =
if (target === "web" && kind === "discordDesktop") continue; (target === "dev" && !IS_DEV) ||
if (target === "desktop" && kind === "web") continue; (target === "web" && kind === "discordDesktop") ||
if (target === "discordDesktop" && kind !== "discordDesktop") continue; (target === "desktop" && kind === "web") ||
if (target === "vencordDesktop" && kind !== "vencordDesktop") continue; (target === "discordDesktop" && kind !== "discordDesktop") ||
(target === "vencordDesktop" && kind !== "vencordDesktop");
if (excluded) {
const name = await resolvePluginName(fullDir, file);
excludedCode += `${JSON.stringify(name)}:${JSON.stringify(target)},\n`;
continue;
}
} }
const folderName = `src/${dir}/${fileName}`.replace(/^src\/plugins\//, ""); const folderName = `src/${dir}/${fileName}`.replace(/^src\/plugins\//, "");
const mod = `p${i}`; const mod = `p${i}`;
code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`; code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`;
plugins += `[${mod}.name]:${mod},\n`; pluginsCode += `[${mod}.name]:${mod},\n`;
meta += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI? metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI?
i++; i++;
} }
} }
code += `export default {${plugins}};export const PluginMeta={${meta}};`; code += `export default {${pluginsCode}};export const PluginMeta={${metaCode}};export const ExcludedPlugins={${excludedCode}};`;
return { return {
contents: code, contents: code,
resolveDir: "./src" resolveDir: "./src"

View file

@ -39,7 +39,7 @@ interface PluginData {
hasCommands: boolean; hasCommands: boolean;
required: boolean; required: boolean;
enabledByDefault: boolean; enabledByDefault: boolean;
target: "discordDesktop" | "vencordDesktop" | "web" | "dev"; target: "discordDesktop" | "vencordDesktop" | "desktop" | "web" | "dev";
filePath: string; filePath: string;
} }

81
src/api/UserSettings.ts Normal file
View file

@ -0,0 +1,81 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { findModuleId, proxyLazyWebpack, wreq } from "@webpack";
interface UserSettingDefinition<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T): Promise<void>;
/**
* Update the setting value
* @param value A callback that accepts the old value as the first argument, and returns the new value
*/
updateSetting(value: (old: T) => T): Promise<void>;
/**
* Stateful React hook for this setting value
*/
useSetting(): T;
userSettingsAPIGroup: string;
userSettingsAPIName: string;
}
export const UserSettings: Record<PropertyKey, UserSettingDefinition<any>> | undefined = proxyLazyWebpack(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"');
if (modId == null) return new Logger("UserSettingsAPI ").error("Didn't find settings module.");
return wreq(modId as any);
});
/**
* Get the setting with the given setting group and name.
*
* @param group The setting group
* @param name The name of the setting
*/
export function getUserSetting<T = any>(group: string, name: string): UserSettingDefinition<T> | undefined {
if (!Vencord.Plugins.isPluginEnabled("UserSettingsAPI")) throw new Error("Cannot use UserSettingsAPI without setting as dependency.");
for (const key in UserSettings) {
const userSetting = UserSettings[key];
if (userSetting.userSettingsAPIGroup === group && userSetting.userSettingsAPIName === name) {
return userSetting;
}
}
}
/**
* {@link getUserSettingDefinition}, lazy.
*
* Get the setting with the given setting group and name.
*
* @param group The setting group
* @param name The name of the setting
*/
export function getUserSettingLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getUserSetting<T>(group, name));
}

View file

@ -33,7 +33,7 @@ import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList"; import * as $ServerList from "./ServerList";
import * as $Settings from "./Settings"; import * as $Settings from "./Settings";
import * as $Styles from "./Styles"; import * as $Styles from "./Styles";
import * as $UserSettingDefinitions from "./UserSettingDefinitions"; import * as $UserSettings from "./UserSettings";
/** /**
* An API allowing you to listen to Message Clicks or run your own logic * An API allowing you to listen to Message Clicks or run your own logic
@ -125,6 +125,6 @@ export const MessageUpdater = $MessageUpdater;
export const ExpressionPickerTabs = $ExpressionPickerTabs; export const ExpressionPickerTabs = $ExpressionPickerTabs;
/** /**
* An API allowing you to get the definition for an user setting * An API allowing you to get an user setting
*/ */
export const UserSettingDefinitions = $UserSettingDefinitions; export const UserSettings = $UserSettings;

View file

@ -35,9 +35,9 @@ import { openModalLazy } from "@utils/modal";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common"; import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common";
import Plugins from "~plugins"; import Plugins, { ExcludedPlugins } from "~plugins";
// Avoid circular dependency // Avoid circular dependency
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins")); const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
@ -177,6 +177,37 @@ const enum SearchStatus {
NEW NEW
} }
function ExcludedPluginsList({ search }: { search: string; }) {
const matchingExcludedPlugins = Object.entries(ExcludedPlugins)
.filter(([name]) => name.toLowerCase().includes(search));
const ExcludedReasons: Record<"web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev", string> = {
desktop: "Discord Desktop app or Vesktop",
discordDesktop: "Discord Desktop app",
vencordDesktop: "Vesktop app",
web: "Vesktop app and the Web version of Discord",
dev: "Developer version of Vencord"
};
return (
<Text variant="text-md/normal" className={Margins.top16}>
{matchingExcludedPlugins.length
? <>
<Forms.FormText>Are you looking for:</Forms.FormText>
<ul>
{matchingExcludedPlugins.map(([name, reason]) => (
<li key={name}>
<b>{name}</b>: Only available on the {ExcludedReasons[reason]}
</li>
))}
</ul>
</>
: "No plugins meet the search criteria."
}
</Text>
);
}
export default function PluginSettings() { export default function PluginSettings() {
const settings = useSettings(); const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []); const changes = React.useMemo(() => new ChangeList<string>(), []);
@ -215,26 +246,27 @@ export default function PluginSettings() {
return o; return o;
}, []); }, []);
const sortedPlugins = React.useMemo(() => Object.values(Plugins) const sortedPlugins = useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []); .sort((a, b) => a.name.localeCompare(b.name)), []);
const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL }); const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
const search = searchValue.value.toLowerCase();
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query })); const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status })); const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => { const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
const enabled = settings.plugins[plugin.name]?.enabled; const { status } = searchValue;
if (enabled && searchValue.status === SearchStatus.DISABLED) return false; const enabled = Vencord.Plugins.isPluginEnabled(plugin.name);
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false; if (enabled && status === SearchStatus.DISABLED) return false;
if (searchValue.status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false; if (!enabled && status === SearchStatus.ENABLED) return false;
if (!searchValue.value.length) return true; if (status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
if (!search.length) return true;
const v = searchValue.value.toLowerCase();
return ( return (
plugin.name.toLowerCase().includes(v) || plugin.name.toLowerCase().includes(search) ||
plugin.description.toLowerCase().includes(v) || plugin.description.toLowerCase().includes(search) ||
plugin.tags?.some(t => t.toLowerCase().includes(v)) plugin.tags?.some(t => t.toLowerCase().includes(search))
); );
}; };
@ -255,54 +287,48 @@ export default function PluginSettings() {
return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins; return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
})); }));
type P = JSX.Element | JSX.Element[]; const plugins = [] as JSX.Element[];
let plugins: P, requiredPlugins: P; const requiredPlugins = [] as JSX.Element[];
if (sortedPlugins?.length) {
plugins = [];
requiredPlugins = [];
const showApi = searchValue.value === "API"; const showApi = searchValue.value.includes("API");
for (const p of sortedPlugins) { for (const p of sortedPlugins) {
if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi)) if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
continue; continue;
if (!pluginFilter(p)) continue; if (!pluginFilter(p)) continue;
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled); const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
if (isRequired) { if (isRequired) {
const tooltipText = p.required const tooltipText = p.required
? "This plugin is required for Vencord to function." ? "This plugin is required for Vencord to function."
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled)); : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
requiredPlugins.push(
<Tooltip text={tooltipText} key={p.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.handleChange(name)}
disabled={true}
plugin={p}
/>
)}
</Tooltip>
);
} else {
plugins.push(
<PluginCard
onRestartNeeded={name => changes.handleChange(name)}
disabled={false}
plugin={p}
isNew={newPlugins?.includes(p.name)}
key={p.name}
/>
);
}
requiredPlugins.push(
<Tooltip text={tooltipText} key={p.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.handleChange(name)}
disabled={true}
plugin={p}
key={p.name}
/>
)}
</Tooltip>
);
} else {
plugins.push(
<PluginCard
onRestartNeeded={name => changes.handleChange(name)}
disabled={false}
plugin={p}
isNew={newPlugins?.includes(p.name)}
key={p.name}
/>
);
} }
} else {
plugins = requiredPlugins = <Text variant="text-md/normal">No plugins meet search criteria.</Text>;
} }
return ( return (
@ -333,9 +359,18 @@ export default function PluginSettings() {
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle> <Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
<div className={cl("grid")}> {plugins.length || requiredPlugins.length
{plugins} ? (
</div> <div className={cl("grid")}>
{plugins.length
? plugins
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
}
</div>
)
: <ExcludedPluginsList search={search} />
}
<Forms.FormDivider className={Margins.top20} /> <Forms.FormDivider className={Margins.top20} />
@ -343,7 +378,10 @@ export default function PluginSettings() {
Required Plugins Required Plugins
</Forms.FormTitle> </Forms.FormTitle>
<div className={cl("grid")}> <div className={cl("grid")}>
{requiredPlugins} {requiredPlugins.length
? requiredPlugins
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
}
</div> </div>
</SettingsTab > </SettingsTab >
); );

View file

@ -5,8 +5,8 @@
<title>Vencord QuickCSS Editor</title> <title>Vencord QuickCSS Editor</title>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs/editor/editor.main.min.css" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs/editor/editor.main.css"
integrity="sha512-MOoQ02h80hklccfLrXFYkCzG+WVjORflOp9Zp8dltiaRP+35LYnO4LKOklR64oMGfGgJDLO8WJpkM1o5gZXYZQ==" integrity="sha256-tiJPQ2O04z/pZ/AwdyIghrOMzewf+PIvEl1YKbQvsZk="
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />
@ -29,8 +29,8 @@
<body> <body>
<div id="container"></div> <div id="container"></div>
<script <script
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs/loader.min.js" src="https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs/loader.js"
integrity="sha512-QzMpXeCPciAHP4wbYlV2PYgrQcaEkDQUjzkPU4xnjyVSD9T36/udamxtNBqb4qK4/bMQMPZ8ayrBe9hrGdBFjQ==" integrity="sha256-KcU48TGr84r7unF7J5IgBo95aeVrEbrGe04S7TcFUjs="
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
></script> ></script>
@ -38,7 +38,7 @@
<script> <script>
require.config({ require.config({
paths: { paths: {
vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.40.0/min/vs", vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs",
}, },
}); });

1
src/modules.d.ts vendored
View file

@ -26,6 +26,7 @@ declare module "~plugins" {
folderName: string; folderName: string;
userPlugin: boolean; userPlugin: boolean;
}>; }>;
export const ExcludedPlugins: Record<string, "web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev">;
} }
declare module "~pluginNatives" { declare module "~pluginNatives" {

View file

@ -0,0 +1,50 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "UserSettingsAPI",
description: "Patches Discord's UserSettings to expose their group and name.",
authors: [Devs.Nuckyz],
patches: [
{
find: ",updateSetting:",
replacement: [
// Main setting definition
{
match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/,
replace: "userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1],$&"
},
// Selective wrapper
{
match: /updateSetting:.{0,100}SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE/,
replace: "userSettingsAPIGroup:arguments[0].userSettingsAPIGroup,userSettingsAPIName:arguments[0].userSettingsAPIName,$&"
},
// Override wrapper
{
match: /updateSetting:.{0,60}USER_SETTINGS_OVERRIDE_CLEAR/,
replace: "userSettingsAPIGroup:arguments[0].userSettingsAPIGroup,userSettingsAPIName:arguments[0].userSettingsAPIName,$&"
}
]
}
]
});

View file

@ -16,24 +16,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addAccessory } from "@api/MessageAccessories";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab"; import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants"; import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { sendMessage } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc"; import { isPluginDev, tryOrElse } from "@utils/misc";
import { relaunch } from "@utils/native"; import { relaunch } from "@utils/native";
import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { isOutdated, update } from "@utils/updater"; import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common"; import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Toasts, UserStore } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import plugins from "~plugins"; import plugins, { PluginMeta } from "~plugins";
import settings from "./settings"; import settings from "./settings";
const VENCORD_GUILD_ID = "1015060230222131221"; const VENCORD_GUILD_ID = "1015060230222131221";
const VENBOT_USER_ID = "1017176847865352332";
const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
const CodeBlockRe = /```js\n(.+?)```/s;
const AllowedChannelIds = [ const AllowedChannelIds = [
SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_ID,
@ -47,12 +56,88 @@ const TrustedRolesIds = [
"1042507929485586532", // donor "1042507929485586532", // donor
]; ];
const AsyncFunction = async function () { }.constructor;
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
async function forceUpdate() {
const outdated = await checkForUpdates();
if (outdated) {
await update();
relaunch();
}
return outdated;
}
async function generateDebugInfoMessage() {
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
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}`;
// @ts-expect-error
const name = typeof unsafeWindow !== "undefined" ? "UserScript" : "Web";
return `${name} (${navigator.userAgent})`;
})();
const info = {
Vencord:
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
Client: `${RELEASE_CHANNEL} ~ ${client}`,
Platform: window.navigator.platform
};
if (IS_DISCORD_DESKTOP) {
info["Last Crash Reason"] = (await tryOrElse(() => DiscordNative.processUtils.getLastCrash(), undefined))?.rendererCrashReason ?? "N/A";
}
const commonIssues = {
"NoRPC enabled": Vencord.Plugins.isPluginEnabled("NoRPC"),
"Activity Sharing disabled": tryOrElse(() => !ShowCurrentGame.getSetting(), false),
"Vencord DevBuild": !IS_STANDALONE,
"Has UserPlugins": Object.values(PluginMeta).some(m => m.userPlugin),
"More than two weeks out of date": BUILD_TIMESTAMP < Date.now() - 12096e5,
};
let content = `>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}`;
content += "\n" + Object.entries(commonIssues)
.filter(([, v]) => v).map(([k]) => `⚠️ ${k}`)
.join("\n");
return content.trim();
}
function generatePluginList() {
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
const enabledPlugins = Object.keys(plugins)
.filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin);
const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin);
let content = `**Enabled Plugins (${enabledStockPlugins.length}):**\n${makeCodeblock(enabledStockPlugins.join(", "))}`;
if (enabledUserPlugins.length) {
content += `**Enabled UserPlugins (${enabledUserPlugins.length}):**\n${makeCodeblock(enabledUserPlugins.join(", "))}`;
}
return content;
}
const checkForUpdatesOnce = onlyOnce(checkForUpdates);
export default definePlugin({ export default definePlugin({
name: "SupportHelper", name: "SupportHelper",
required: true, required: true,
description: "Helps us provide support to you", description: "Helps us provide support to you",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["CommandsAPI"], dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
patches: [{ patches: [{
find: ".BEGINNING_DM.format", find: ".BEGINNING_DM.format",
@ -62,51 +147,20 @@ export default definePlugin({
} }
}], }],
commands: [{ commands: [
name: "vencord-debug", {
description: "Send Vencord Debug info", name: "vencord-debug",
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id), description: "Send Vencord debug info",
async execute() { predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
const { RELEASE_CHANNEL } = window.GLOBAL_ENV; execute: async () => ({ content: await generateDebugInfoMessage() })
},
const client = (() => { {
if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`; name: "vencord-plugins",
if (IS_VESKTOP) return `Vesktop v${VesktopNative.app.getVersion()}`; description: "Send Vencord plugin list",
if ("armcord" in window) return `ArmCord v${window.armcord.version}`; predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
execute: () => ({ content: generatePluginList() })
// @ts-expect-error
const name = typeof unsafeWindow !== "undefined" ? "UserScript" : "Web";
return `${name} (${navigator.userAgent})`;
})();
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
const info = {
Vencord:
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
Client: `${RELEASE_CHANNEL} ~ ${client}`,
Platform: window.navigator.platform
};
if (IS_DISCORD_DESKTOP) {
info["Last Crash Reason"] = (await DiscordNative.processUtils.getLastCrash())?.rendererCrashReason ?? "N/A";
}
const debugInfo = `
>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}
Enabled Plugins (${enabledPlugins.length}):
${makeCodeblock(enabledPlugins.join(", "))}
`;
return {
content: debugInfo.trim().replaceAll("```\n", "```")
};
} }
}], ],
flux: { flux: {
async CHANNEL_SELECT({ channelId }) { async CHANNEL_SELECT({ channelId }) {
@ -115,24 +169,25 @@ ${makeCodeblock(enabledPlugins.join(", "))}
const selfId = UserStore.getCurrentUser()?.id; const selfId = UserStore.getCurrentUser()?.id;
if (!selfId || isPluginDev(selfId)) return; if (!selfId || isPluginDev(selfId)) return;
if (isOutdated) { if (!IS_UPDATER_DISABLED) {
return Alerts.show({ await checkForUpdatesOnce().catch(() => { });
title: "Hold on!",
body: <div> if (isOutdated) {
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText> return Alerts.show({
<Forms.FormText className={Margins.top8}> title: "Hold on!",
Please first update before asking for support! body: <div>
</Forms.FormText> <Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
</div>, <Forms.FormText className={Margins.top8}>
onCancel: () => openUpdaterModal!(), Please first update before asking for support!
cancelText: "View Updates", </Forms.FormText>
confirmText: "Update & Restart Now", </div>,
async onConfirm() { onCancel: () => openUpdaterModal!(),
await update(); cancelText: "View Updates",
relaunch(); confirmText: "Update & Restart Now",
}, onConfirm: forceUpdate,
secondaryConfirmText: "I know what I'm doing or I can't update" secondaryConfirmText: "I know what I'm doing or I can't update"
}); });
}
} }
// @ts-ignore outdated type // @ts-ignore outdated type
@ -148,8 +203,7 @@ ${makeCodeblock(enabledPlugins.join(", "))}
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
contact your package maintainer for support instead. contact your package maintainer for support instead.
</Forms.FormText> </Forms.FormText>
</div>, </div>
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
}); });
} }
@ -163,8 +217,7 @@ ${makeCodeblock(enabledPlugins.join(", "))}
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
contact your package maintainer for support instead. contact your package maintainer for support instead.
</Forms.FormText> </Forms.FormText>
</div>, </div>
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
}); });
} }
} }
@ -172,7 +225,7 @@ ${makeCodeblock(enabledPlugins.join(", "))}
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => { ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
if (!isPluginDev(userId)) return null; if (!isPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId)) return null; if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
return ( return (
<Card className={`vc-plugins-restart-card ${Margins.top8}`}> <Card className={`vc-plugins-restart-card ${Margins.top8}`}>
@ -182,5 +235,86 @@ ${makeCodeblock(enabledPlugins.join(", "))}
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"} {!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
</Card> </Card>
); );
}, { noop: true }) }, { noop: true }),
start() {
addAccessory("vencord-debug", props => {
const buttons = [] as JSX.Element[];
const shouldAddUpdateButton =
!IS_UPDATER_DISABLED
&& (
(props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
(props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
)
&& props.message.content?.includes("update");
if (shouldAddUpdateButton) {
buttons.push(
<Button
key="vc-update"
color={Button.Colors.GREEN}
onClick={async () => {
try {
if (await forceUpdate())
showToast("Success! Restarting...", Toasts.Type.SUCCESS);
else
showToast("Already up to date!", Toasts.Type.MESSAGE);
} catch (e) {
new Logger(this.name).error("Error while updating:", e);
showToast("Failed to update :(", Toasts.Type.FAILURE);
}
}}
>
Update Now
</Button>
);
}
if (props.channel.id === SUPPORT_CHANNEL_ID) {
if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) {
buttons.push(
<Button
key="vc-dbg"
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}
>
Run /vencord-debug
</Button>,
<Button
key="vc-plg-list"
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
>
Run /vencord-plugins
</Button>
);
}
if (props.message.author.id === VENBOT_USER_ID) {
const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || "");
if (match) {
buttons.push(
<Button
key="vc-run-snippet"
onClick={async () => {
try {
await AsyncFunction(match[1])();
showToast("Success!", Toasts.Type.SUCCESS);
} catch (e) {
new Logger(this.name).error("Error while running snippet:", e);
showToast("Failed to run snippet :(", Toasts.Type.FAILURE);
}
}}
>
Run Snippet
</Button>
);
}
}
}
return buttons.length
? <Flex>{buttons}</Flex>
: null;
});
},
}); });

View file

@ -9,7 +9,7 @@ import { Devs } from "@utils/constants";
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types"; import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common"; import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
const Native = VencordNative.pluginHelpers.AppleMusic as PluginNative<typeof import("./native")>; const Native = VencordNative.pluginHelpers.AppleMusicRichPresence as PluginNative<typeof import("./native")>;
interface ActivityAssets { interface ActivityAssets {
large_image?: string; large_image?: string;

View file

@ -5,7 +5,7 @@
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions"; import { getUserSettingLazy } from "@api/UserSettings";
import { ImageIcon } from "@components/Icons"; import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentGuild, openImageModal } from "@utils/discord"; import { getCurrentGuild, openImageModal } from "@utils/discord";
@ -15,7 +15,7 @@ import { Clipboard, GuildStore, Menu, PermissionStore } from "@webpack/common";
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild"); const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
const DeveloperMode = getUserSettingDefinitionLazy("appearance", "developerMode")!; const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!;
function PencilIcon() { function PencilIcon() {
return ( return (
@ -65,8 +65,7 @@ export default definePlugin({
name: "BetterRoleContext", name: "BetterRoleContext",
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile", description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
authors: [Devs.Ven, Devs.goodbee], authors: [Devs.Ven, Devs.goodbee],
dependencies: ["UserSettingDefinitionsAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,
start() { start() {

View file

@ -17,7 +17,7 @@
*/ */
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions"; import { getUserSettingLazy } from "@api/UserSettings";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -33,7 +33,7 @@ const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gra
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile"); const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor"); const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const ShowCurrentGame = getUserSettingDefinitionLazy<boolean>("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
async function getApplicationAsset(key: string): Promise<string> { async function getApplicationAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, ""); if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
@ -393,7 +393,7 @@ export default definePlugin({
name: "CustomRPC", name: "CustomRPC",
description: "Allows you to set a custom rich presence.", description: "Allows you to set a custom rich presence.",
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev], authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
dependencies: ["UserSettingDefinitionsAPI"], dependencies: ["UserSettingsAPI"],
start: setRpc, start: setRpc,
stop: () => setRpc(true), stop: () => setRpc(true),
settings, settings,

View file

@ -18,7 +18,7 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles"; import { disableStyle, enableStyle } from "@api/Styles";
import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions"; import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -28,7 +28,7 @@ import style from "./style.css?managed";
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:"); const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
const ShowCurrentGame = getUserSettingDefinitionLazy<boolean>("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
function makeIcon(showCurrentGame?: boolean) { function makeIcon(showCurrentGame?: boolean) {
const { oldIcon } = settings.use(["oldIcon"]); const { oldIcon } = settings.use(["oldIcon"]);
@ -87,7 +87,7 @@ export default definePlugin({
name: "GameActivityToggle", name: "GameActivityToggle",
description: "Adds a button next to the mic and deafen button to toggle game activity.", description: "Adds a button next to the mic and deafen button to toggle game activity.",
authors: [Devs.Nuckyz, Devs.RuukuLada], authors: [Devs.Nuckyz, Devs.RuukuLada],
dependencies: ["UserSettingDefinitionsAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,
patches: [ patches: [

View file

@ -1,3 +1,3 @@
[class*="withTagAsButton"] { [class*="panels"] [class*="avatarWrapper"] {
min-width: 88px !important; min-width: 88px;
} }

View file

@ -6,7 +6,7 @@
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions"; import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -28,7 +28,7 @@ interface IgnoredActivity {
const RunningGameStore = findStoreLazy("RunningGameStore"); const RunningGameStore = findStoreLazy("RunningGameStore");
const ShowCurrentGame = getUserSettingDefinitionLazy("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!;
function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) { function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {
return ( return (
@ -208,8 +208,7 @@ export default definePlugin({
name: "IgnoreActivities", name: "IgnoreActivities",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.", description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.",
dependencies: ["UserSettingDefinitionsAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,
patches: [ patches: [
@ -222,6 +221,13 @@ export default definePlugin({
} }
] ]
}, },
{
find: '="ActivityTrackingStore",',
replacement: {
match: /getVisibleRunningGames\(\).+?;(?=for)(?<=(\i)=\i\.\i\.getVisibleRunningGames.+?)/,
replace: (m, runningGames) => `${m}${runningGames}=${runningGames}.filter(({id,name})=>$self.isActivityNotIgnored({type:0,application_id:id,name}));`
}
},
{ {
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: { replacement: {

View file

@ -169,7 +169,18 @@ export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof Flux
logger.debug("Subscribing to flux events of plugin", p.name); logger.debug("Subscribing to flux events of plugin", p.name);
for (const [event, handler] of Object.entries(p.flux)) { for (const [event, handler] of Object.entries(p.flux)) {
fluxDispatcher.subscribe(event as FluxEvents, handler); const wrappedHandler = p.flux[event] = function () {
try {
const res = handler.apply(p, arguments as any);
return res instanceof Promise
? res.catch(e => logger.error(`${p.name}: Error while handling ${event}\n`, e))
: res;
} catch (e) {
logger.error(`${p.name}: Error while handling ${event}\n`, e);
}
};
fluxDispatcher.subscribe(event as FluxEvents, wrappedHandler);
} }
} }
} }

View file

@ -19,7 +19,7 @@
import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { updateMessage } from "@api/MessageUpdater"; import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions"; import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js"; import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
@ -54,7 +54,7 @@ const ChannelMessage = findComponentByCodeLazy("childrenExecutedCommand:", ".hid
const SearchResultClasses = findByPropsLazy("message", "searchResult"); const SearchResultClasses = findByPropsLazy("message", "searchResult");
const EmbedClasses = findByPropsLazy("embedAuthorIcon", "embedAuthor", "embedAuthor"); const EmbedClasses = findByPropsLazy("embedAuthorIcon", "embedAuthor", "embedAuthor");
const MessageDisplayCompact = getUserSettingDefinitionLazy("textAndImages", "messageDisplayCompact")!; const MessageDisplayCompact = getUserSettingLazy("textAndImages", "messageDisplayCompact")!;
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(?:\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g; const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(?:\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
const tenorRegex = /^https:\/\/(?:www\.)?tenor\.com\//; const tenorRegex = /^https:\/\/(?:www\.)?tenor\.com\//;
@ -366,8 +366,7 @@ export default definePlugin({
name: "MessageLinkEmbeds", name: "MessageLinkEmbeds",
description: "Adds a preview to messages that link another message", description: "Adds a preview to messages that link another message",
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev], authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI", "UserSettingDefinitionsAPI"], dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI", "UserSettingsAPI"],
settings, settings,
start() { start() {

View file

@ -108,7 +108,7 @@ export default definePlugin({
patches: [ patches: [
{ {
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL,shouldHideMediaOptions",
replacement: { replacement: {
match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/, match: /favoriteableType:\i,(?<=(\i)\.getAttribute\("data-type"\).+?)/,
replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),` replace: (m, target) => `${m}reverseImageSearchType:${target}.getAttribute("data-role"),`

View file

@ -12,7 +12,7 @@ import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore } from "@webpack/common"; import { ChannelStore, GuildStore } from "@webpack/common";
const SummaryStore = findByPropsLazy("allSummaries", "findSummary"); const SummaryStore = findByPropsLazy("allSummaries", "findSummary");
const createSummaryFromServer = findByCodeLazy(".people)),startId:"); const createSummaryFromServer = findByCodeLazy(".people)),startId:", ".type}");
const settings = definePluginSettings({ const settings = definePluginSettings({
summaryExpiryThresholdDays: { summaryExpiryThresholdDays: {

View file

@ -48,10 +48,10 @@ export default definePlugin({
}, },
patches: [ patches: [
{ {
find: "showTaglessAccountPanel:", find: '"AccountConnected"',
replacement: { replacement: {
// react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah }) // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah })
match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?showTaglessAccountPanel:)/, match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/,
// react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ... // react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ...
replace: "$self.PanelWrapper,{VencordOriginal:$1," replace: "$self.PanelWrapper,{VencordOriginal:$1,"
} }

View file

@ -136,7 +136,7 @@ const settings = definePluginSettings({
}, },
}); });
const Native = VencordNative.pluginHelpers.XsOverlay as PluginNative<typeof import("./native")>; const Native = VencordNative.pluginHelpers.XSOverlay as PluginNative<typeof import("./native")>;
export default definePlugin({ export default definePlugin({
name: "XSOverlay", name: "XSOverlay",
@ -154,104 +154,99 @@ export default definePlugin({
} }
}, },
MESSAGE_CREATE({ message, optimistic }: { message: Message; optimistic: boolean; }) { MESSAGE_CREATE({ message, optimistic }: { message: Message; optimistic: boolean; }) {
// Apparently without this try/catch, discord's socket connection dies if any part of this errors if (optimistic) return;
try { const channel = ChannelStore.getChannel(message.channel_id);
if (optimistic) return; if (!shouldNotify(message, message.channel_id)) return;
const channel = ChannelStore.getChannel(message.channel_id);
if (!shouldNotify(message, message.channel_id)) return;
const pingColor = settings.store.pingColor.replaceAll("#", "").trim(); const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim(); const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
let finalMsg = message.content; let finalMsg = message.content;
let titleString = ""; let titleString = "";
if (channel.guild_id) { if (channel.guild_id) {
const guild = GuildStore.getGuild(channel.guild_id); const guild = GuildStore.getGuild(channel.guild_id);
titleString = `${message.author.username} (${guild.name}, #${channel.name})`; titleString = `${message.author.username} (${guild.name}, #${channel.name})`;
}
switch (channel.type) {
case ChannelTypes.DM:
titleString = message.author.username.trim();
break;
case ChannelTypes.GROUP_DM:
const channelName = channel.name.trim() ?? channel.rawRecipients.map(e => e.username).join(", ");
titleString = `${message.author.username} (${channelName})`;
break;
}
if (message.referenced_message) {
titleString += " (reply)";
}
if (message.embeds.length > 0) {
finalMsg += " [embed] ";
if (message.content === "") {
finalMsg = "sent message embed(s)";
}
}
if (message.sticker_items) {
finalMsg += " [sticker] ";
if (message.content === "") {
finalMsg = "sent a sticker";
}
}
const images = message.attachments.filter(e =>
typeof e?.content_type === "string"
&& e?.content_type.startsWith("image")
);
images.forEach(img => {
finalMsg += ` [image: ${img.filename}] `;
});
message.attachments.filter(a => a && !a.content_type?.startsWith("image")).forEach(a => {
finalMsg += ` [attachment: ${a.filename}] `;
});
// make mentions readable
if (message.mentions.length > 0) {
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
}
// 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);
if (!role) continue;
const roleColor = role.colorString ?? `#${pingColor}`;
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);
}
}
// make emotes and channel mentions readable
const emoteMatches = finalMsg.match(new RegExp("(<a?:\\w+:\\d+>)", "g"));
const channelMatches = finalMsg.match(new RegExp("<(#\\d+)>", "g"));
if (emoteMatches) {
for (const eMatch of emoteMatches) {
finalMsg = finalMsg.replace(new RegExp(`${eMatch}`, "g"), `:${eMatch.split(":")[1]}:`);
}
}
// color channel mentions
if (channelMatches) {
for (const cMatch of channelMatches) {
let channelId = cMatch.split("<#")[1];
channelId = channelId.substring(0, channelId.length - 1);
finalMsg = finalMsg.replace(new RegExp(`${cMatch}`, "g"), `<b><color=#${channelPingColor}>#${ChannelStore.getChannel(channelId).name}</color></b>`);
}
}
if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message);
} catch (err) {
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
} }
switch (channel.type) {
case ChannelTypes.DM:
titleString = message.author.username.trim();
break;
case ChannelTypes.GROUP_DM:
const channelName = channel.name.trim() ?? channel.rawRecipients.map(e => e.username).join(", ");
titleString = `${message.author.username} (${channelName})`;
break;
}
if (message.referenced_message) {
titleString += " (reply)";
}
if (message.embeds.length > 0) {
finalMsg += " [embed] ";
if (message.content === "") {
finalMsg = "sent message embed(s)";
}
}
if (message.sticker_items) {
finalMsg += " [sticker] ";
if (message.content === "") {
finalMsg = "sent a sticker";
}
}
const images = message.attachments.filter(e =>
typeof e?.content_type === "string"
&& e?.content_type.startsWith("image")
);
images.forEach(img => {
finalMsg += ` [image: ${img.filename}] `;
});
message.attachments.filter(a => a && !a.content_type?.startsWith("image")).forEach(a => {
finalMsg += ` [attachment: ${a.filename}] `;
});
// make mentions readable
if (message.mentions.length > 0) {
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
}
// 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);
if (!role) continue;
const roleColor = role.colorString ?? `#${pingColor}`;
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);
}
}
// make emotes and channel mentions readable
const emoteMatches = finalMsg.match(new RegExp("(<a?:\\w+:\\d+>)", "g"));
const channelMatches = finalMsg.match(new RegExp("<(#\\d+)>", "g"));
if (emoteMatches) {
for (const eMatch of emoteMatches) {
finalMsg = finalMsg.replace(new RegExp(`${eMatch}`, "g"), `:${eMatch.split(":")[1]}:`);
}
}
// color channel mentions
if (channelMatches) {
for (const cMatch of channelMatches) {
let channelId = cMatch.split("<#")[1];
channelId = channelId.substring(0, channelId.length - 1);
finalMsg = finalMsg.replace(new RegExp(`${cMatch}`, "g"), `<b><color=#${channelPingColor}>#${ChannelStore.getChannel(channelId).name}</color></b>`);
}
}
if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message);
} }
} }
}); });

View file

@ -99,3 +99,14 @@ export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id);
export function pluralise(amount: number, singular: string, plural = singular + "s") { export function pluralise(amount: number, singular: string, plural = singular + "s") {
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`; return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
} }
export function tryOrElse<T>(func: () => T, fallback: T): T {
try {
const res = func();
return res instanceof Promise
? res.catch(() => fallback) as T
: res;
} catch {
return fallback;
}
}

View file

@ -131,3 +131,18 @@ export function makeCodeblock(text: string, language?: string) {
const chars = "```"; const chars = "```";
return `${chars}${language || ""}\n${text.replaceAll("```", "\\`\\`\\`")}\n${chars}`; return `${chars}${language || ""}\n${text.replaceAll("```", "\\`\\`\\`")}\n${chars}`;
} }
export function stripIndent(strings: TemplateStringsArray, ...values: any[]) {
const string = String.raw({ raw: strings }, ...values);
const match = string.match(/^[ \t]*(?=\S)/gm);
if (!match) return string.trim();
const minIndent = match.reduce((r, a) => Math.min(r, a.length), Infinity);
return string.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "").trim();
}
export const ZWSP = "\u200b";
export function toInlineCode(s: string) {
return "``" + ZWSP + s.replaceAll("`", ZWSP + "`" + ZWSP) + ZWSP + "``";
}

View file

@ -128,7 +128,7 @@ export interface PluginDef {
* Allows you to subscribe to Flux events * Allows you to subscribe to Flux events
*/ */
flux?: { flux?: {
[E in FluxEvents]?: (event: any) => void; [E in FluxEvents]?: (event: any) => void | Promise<void>;
}; };
/** /**
* Allows you to manipulate context menus * Allows you to manipulate context menus

View file

@ -8,7 +8,12 @@ export let EXTENSION_BASE_URL: string;
export let EXTENSION_VERSION: string; export let EXTENSION_VERSION: string;
if (IS_EXTENSION) { if (IS_EXTENSION) {
const script = document.querySelector("#vencord-script") as HTMLScriptElement; const listener = (e: MessageEvent) => {
EXTENSION_BASE_URL = script.dataset.extensionBaseUrl!; if (e.data?.type === "vencord:meta") {
EXTENSION_VERSION = script.dataset.version!; ({ EXTENSION_BASE_URL, EXTENSION_VERSION } = e.data.meta);
window.removeEventListener("message", listener);
}
};
window.addEventListener("message", listener);
} }