Merge branch 'dev' of https://github.com/Vendicated/Vencord into discord-types

This commit is contained in:
ryan-0324 2024-06-22 06:56:33 -04:00
commit 98afd551fe
41 changed files with 867 additions and 476 deletions

View file

@ -2,23 +2,22 @@ if (typeof browser === "undefined") {
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");
style.type = "text/css";
style.rel = "stylesheet";
style.href = browser.runtime.getURL("dist/Vencord.css");
document.documentElement.append(script);
document.addEventListener(
"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 }
);

View file

@ -22,7 +22,15 @@
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"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",
"matches": ["*://*.discord.com/*"],
"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",
"private": "true",
"version": "1.9.0",
"version": "1.9.1",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -43,7 +43,7 @@
"eslint-plugin-simple-header": "^1.0.2",
"fflate": "^0.7.4",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.43.0",
"monaco-editor": "^0.50.0",
"nanoid": "^4.0.2",
"virtual-merge": "^1.0.1"
},

View file

@ -35,8 +35,8 @@ importers:
specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3
version: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3
monaco-editor:
specifier: ^0.43.0
version: 0.43.0
specifier: ^0.50.0
version: 0.50.0
nanoid:
specifier: ^4.0.2
version: 4.0.2
@ -1706,6 +1706,10 @@ packages:
is-core-module@2.13.1:
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:
resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==}
engines: {node: '>=0.10.0'}
@ -1990,8 +1994,8 @@ packages:
moment@2.29.4:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
monaco-editor@0.43.0:
resolution: {integrity: sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q==}
monaco-editor@0.50.0:
resolution: {integrity: sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==}
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@ -3853,7 +3857,7 @@ snapshots:
eslint-import-resolver-node@0.3.9:
dependencies:
debug: 3.2.7
is-core-module: 2.13.1
is-core-module: 2.14.0
resolve: 1.22.8
transitivePeerDependencies:
- supports-color
@ -3906,7 +3910,7 @@ snapshots:
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.13.1(eslint@8.57.0(patch_hash=wy5a2dwvtxac2ygzwebqqjurgi))(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0(patch_hash=wy5a2dwvtxac2ygzwebqqjurgi))
hasown: 2.0.2
is-core-module: 2.13.1
is-core-module: 2.14.0
is-glob: 4.0.3
minimatch: 3.1.2
object.fromentries: 2.0.8
@ -4444,6 +4448,10 @@ snapshots:
dependencies:
hasown: 2.0.2
is-core-module@2.14.0:
dependencies:
hasown: 2.0.2
is-data-descriptor@0.1.4:
dependencies:
kind-of: 3.2.2
@ -4695,7 +4703,7 @@ snapshots:
moment@2.29.4: {}
monaco-editor@0.43.0: {}
monaco-editor@0.50.0: {}
ms@2.0.0: {}

View file

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

View file

@ -53,6 +53,32 @@ export const banner = {
`.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`);
})();
}
/** @param {string} path */
export async function exists(path) {
return await access(path, FsConstants.F_OK)
@ -87,31 +113,48 @@ export const globPlugins = kind => ({
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"];
let code = "";
let plugins = "\n";
let pluginsCode = "\n";
let metaCode = "\n";
let excludedCode = "\n";
let i = 0;
for (const dir of pluginDirs) {
if (!await exists(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`);
for (const file of files) {
if (file.startsWith("_") || file.startsWith(".")) continue;
if (file === "index.ts") continue;
const userPlugin = dir === "userplugins";
const fullDir = `./src/${dir}`;
if (!await exists(fullDir)) continue;
const files = await readdir(fullDir, { withFileTypes: true });
for (const file of files) {
const fileName = file.name;
if (fileName.startsWith("_") || fileName.startsWith(".")) continue;
if (fileName === "index.ts") continue;
const target = getPluginTarget(fileName);
const target = getPluginTarget(file);
if (target && !IS_REPORTER) {
if (target === "dev" && !watch) continue;
if (target === "web" && kind === "discordDesktop") continue;
if (target === "desktop" && kind === "web") continue;
if (target === "discordDesktop" && kind !== "discordDesktop") continue;
if (target === "vencordDesktop" && kind !== "vencordDesktop") continue;
const excluded =
(target === "dev" && !IS_DEV) ||
(target === "web" && kind === "discordDesktop") ||
(target === "desktop" && kind === "web") ||
(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 mod = `p${i}`;
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
plugins += `[${mod}.name]:${mod},\n`;
code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`;
pluginsCode += `[${mod}.name]:${mod},\n`;
metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI?
i++;
}
}
code += `export default {${plugins}};`;
code += `export default {${pluginsCode}};export const PluginMeta={${metaCode}};export const ExcludedPlugins={${excludedCode}};`;
return {
contents: code,
resolveDir: "./src"

View file

@ -20,54 +20,64 @@ import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { findModuleId, proxyLazyWebpack, wreq } from "@webpack";
import { Settings } from "./Settings";
export interface UserSettingDefinition<T = any> {
interface UserSettingDefinition<T> {
/**
* Get the setting value
* Gets the setting value
*/
getSetting: () => T;
getSetting(): T;
/**
* Update the setting value
* Updates the setting value
* @param value The new value
*/
updateSetting: (value: T | ((old: T) => T)) => Promise<void>;
updateSetting(value: T): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
* Updates the setting value
* @param value A callback that accepts the old value as the first argument, and returns the new value
*/
useSetting: () => T;
userSettingApiGroup: string;
userSettingApiName: string;
updateSetting(value: (old: T) => T): Promise<void>;
/**
* Stateful React hook for this setting value
*/
useSetting(): T;
userSettingsAPIGroup: string;
userSettingsAPIName: string;
}
export const UserSettings: UserSettingDefinition[] | undefined = proxyLazyWebpack(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"') as any;
export const UserSettings: Record<PropertyKey, UserSettingDefinition<any>> | undefined = proxyLazyWebpack(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"');
if (modId == null) {
new Logger("UserSettingsAPI").error("Didn't find settings module.");
new Logger("UserSettingsAPI ").error("Didn't find settings module.");
return;
}
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.userSettingApiGroup) as any;
return wreq(modId as any);
});
/**
* Gets the setting with the given setting group and name
* Gets 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 (!Settings.plugins.UserSettingsAPI!.enabled)
if (!Vencord.Plugins.isPluginEnabled("UserSettingsAPI"))
throw new Error("Cannot use UserSettingsAPI without setting as dependency.");
return UserSettings?.find(s => s.userSettingApiGroup === group && s.userSettingApiName === name);
for (const key in UserSettings) {
const userSetting = UserSettings[key];
if (userSetting!.userSettingsAPIGroup === group && userSetting!.userSettingsAPIName === name)
return userSetting;
}
}
/**
* {@link getUserSetting} but lazy
* {@link getUserSetting}, lazy.
*
* Gets 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

@ -118,4 +118,7 @@ export const ChatButtons = $ChatButtons;
*/
export const MessageUpdater = $MessageUpdater;
/**
* An API allowing you to get an user setting
*/
export const UserSettings = $UserSettings;

View file

@ -11,20 +11,16 @@ import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { DevsById } from "@utils/constants";
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
import { pluralise } from "@utils/misc";
import { fetchUserProfile } from "@utils/discord";
import { classes, pluralise } from "@utils/misc";
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
import type { UserRecord } from "@vencord/discord-types";
import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import Plugins from "~plugins";
import { PluginCard } from ".";
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
import { GithubButton, WebsiteButton } from "./LinkIconButton";
const cl = classNameFactory("vc-author-modal-");
@ -40,16 +36,6 @@ export function openContributorModal(user: UserRecord) {
));
}
function GithubIcon() {
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
return <img src={src} alt="GitHub" />;
}
function WebsiteIcon() {
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
return <img src={src} alt="Website" />;
}
function ContributorModal({ user }: { user: UserRecord; }) {
useSettings();
@ -86,24 +72,18 @@ function ContributorModal({ user }: { user: UserRecord; }) {
/>
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
<div className={cl("links")}>
<div className={classes("vc-settings-modal-links", cl("links"))}>
{website && (
<Tooltip text={website}>
{props => (
<MaskedLink {...props} href={"https://" + website}>
<WebsiteIcon />
</MaskedLink>
)}
</Tooltip>
<WebsiteButton
text={website}
href={`https://${website}`}
/>
)}
{githubName && (
<Tooltip text={githubName}>
{props => (
<MaskedLink {...props} href={`https://github.com/${githubName}`}>
<GithubIcon />
</MaskedLink>
)}
</Tooltip>
<GithubButton
text={githubName}
href={`https://github.com/${githubName}`}
/>
)}
</div>
</div>

View file

@ -0,0 +1,12 @@
.vc-settings-modal-link-icon {
height: 32px;
width: 32px;
border-radius: 50%;
border: 4px solid var(--background-tertiary);
box-sizing: border-box
}
.vc-settings-modal-links {
display: flex;
gap: 0.2em;
}

View file

@ -0,0 +1,45 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./LinkIconButton.css";
import { getTheme, Theme } from "@utils/discord";
import { MaskedLink, Tooltip } from "@webpack/common";
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
export function GithubIcon() {
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
return <img src={src} aria-hidden className={"vc-settings-modal-link-icon"} />;
}
export function WebsiteIcon() {
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
return <img src={src} aria-hidden className={"vc-settings-modal-link-icon"} />;
}
interface Props {
text: string;
href: string;
}
function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; }) {
return (
<Tooltip text={text}>
{props => (
<MaskedLink {...props} href={href}>
<Icon />
</MaskedLink>
)}
</Tooltip>
);
}
export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteIcon} />;
export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubIcon} />;

View file

@ -0,0 +1,7 @@
.vc-plugin-modal-info {
align-items: center;
}
.vc-plugin-modal-description {
flex-grow: 1;
}

View file

@ -16,10 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./PluginModal.css";
import { generateId } from "@api/Commands";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { gitRemote } from "@shared/vencordUserAgent";
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc";
@ -30,6 +34,8 @@ import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Clickable, FluxDispatcher, Forms, Text, Tooltip, useEffect, UserActionCreators, UserStore, useState } from "@webpack/common";
import type { ComponentType } from "react";
import { PluginMeta } from "~plugins";
import {
type ISettingElementProps,
SettingBooleanComponent,
@ -40,6 +46,9 @@ import {
SettingTextComponent
} from "./components";
import { openContributorModal } from "./ContributorModal";
import { GithubButton, WebsiteButton } from "./LinkIconButton";
const cl = classNameFactory("vc-plugin-modal-");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const AvatarStyles: Record<string, string> = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
@ -180,16 +189,54 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
);
}
/*
function switchToPopout() {
onClose();
const PopoutKey = `DISCORD_VENCORD_PLUGIN_SETTINGS_MODAL_${plugin.name}`;
PopoutActions.open(
PopoutKey,
() => <PluginModal
transitionState={transitionState}
plugin={plugin}
onRestartNeeded={onRestartNeeded}
onClose={() => PopoutActions.close(PopoutKey)}
/>
);
}
*/
const pluginMeta = PluginMeta[plugin.name];
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
{/*
<Button look={Button.Looks.BLANK} onClick={switchToPopout}>
<OpenExternalIcon aria-label="Open in Popout" />
</Button>
*/}
<ModalCloseButton onClick={onClose} />
</ModalHeader>
<ModalContent>
<Forms.FormSection>
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
<Forms.FormText>{plugin.description}</Forms.FormText>
<Flex className={cl("info")}>
<Forms.FormText className={cl("description")}>{plugin.description}</Forms.FormText>
{!pluginMeta.userPlugin && (
<div className="vc-settings-modal-links">
<WebsiteButton
text="View more info"
href={`https://vencord.dev/plugins/${plugin.name}`}
/>
<GithubButton
text="View source code"
href={`https://github.com/${gitRemote}/tree/main/src/plugins/${pluginMeta.folderName}`}
/>
</div>
)}
</Flex>
<Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
<div style={{ width: "fit-content", marginBottom: 8 }}>
<UserSummaryItem

View file

@ -42,16 +42,6 @@
.vc-author-modal-links {
margin-left: auto;
display: flex;
gap: 0.2em;
}
.vc-author-modal-links img {
height: 32px;
width: 32px;
border-radius: 50%;
border: 4px solid var(--background-tertiary);
box-sizing: border-box
}
.vc-author-modal-plugins {

View file

@ -38,7 +38,7 @@ import { findByPropsLazy } from "@webpack";
import { AlertActionCreators, Button, Card, Forms, lodash, MarkupUtils, Select, Text, TextInput, Toasts, Tooltip, useEffect, useMemo, useState } from "@webpack/common";
import type { HTMLProps, JSX } from "react";
import Plugins from "~plugins";
import Plugins, { ExcludedPlugins } from "~plugins";
// Avoid circular dependency
const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() => require("../../plugins"));
@ -183,6 +183,37 @@ const enum SearchStatus {
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() {
const settings = useSettings();
const changes = useMemo(() => new ChangeList<string>(), []);
@ -229,23 +260,24 @@ export default function PluginSettings() {
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
const search = searchValue.value.toLowerCase();
const onSearch = (query: string) => { setSearchValue(prev => ({ ...prev, value: query })); };
const onStatusChange = (status: SearchStatus) => { setSearchValue(prev => ({ ...prev, status })); };
const pluginFilter = (plugin: Plugin) => {
const enabled = settings.plugins[plugin.name]?.enabled;
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
if (searchValue.status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
if (!searchValue.value.length) return true;
function pluginFilter(plugin: Plugin) {
const { status } = searchValue;
const enabled = Vencord.Plugins.isPluginEnabled(plugin.name);
if (enabled && status === SearchStatus.DISABLED) return false;
if (!enabled && status === SearchStatus.ENABLED) return false;
if (status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false;
if (!search.length) return true;
const v = searchValue.value.toLowerCase();
return (
plugin.name.toLowerCase().includes(v) ||
plugin.description.toLowerCase().includes(v) ||
plugin.tags?.some(t => t.toLowerCase().includes(v))
plugin.name.toLowerCase().includes(search) ||
plugin.description.toLowerCase().includes(search) ||
plugin.tags?.some(t => t.toLowerCase().includes(search))
);
};
}
const [newPlugins] = useAwaiter(() => DataStore.get("Vencord_existingPlugins").then((cachedPlugins: Record<string, number> | undefined) => {
const now = Date.now() / 1000;
@ -264,54 +296,48 @@ export default function PluginSettings() {
return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
}));
type P = JSX.Element | JSX.Element[];
let plugins: P, requiredPlugins: P;
if (sortedPlugins.length) {
plugins = [];
requiredPlugins = [];
const plugins: JSX.Element[] = [];
const requiredPlugins: JSX.Element[] = [];
const showAPI = searchValue.value === "API";
for (const p of sortedPlugins) {
if (p.hidden || (!p.options && p.name.endsWith("API") && !showAPI))
continue;
const showApi = searchValue.value.includes("API");
for (const p of sortedPlugins) {
if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
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) {
const tooltipText = p.required
? "This plugin is required for Vencord to function."
: 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}
/>
);
}
if (isRequired) {
const tooltipText = p.required
? "This plugin is required for Vencord to function."
: 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}
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 (
@ -342,9 +368,18 @@ export default function PluginSettings() {
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
<div className={cl("grid")}>
{plugins}
</div>
{plugins.length || requiredPlugins.length
? (
<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} />
@ -352,15 +387,18 @@ export default function PluginSettings() {
Required Plugins
</Forms.FormTitle>
<div className={cl("grid")}>
{requiredPlugins}
{requiredPlugins.length
? requiredPlugins
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
}
</div>
</SettingsTab >
);
}
const makeDependencyList = (deps: string[]) => (
const makeDependencyList = (deps?: string[]) => (
<>
<Forms.FormText>This plugin is required by:</Forms.FormText>
{deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
{deps?.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
</>
);

View file

@ -39,9 +39,8 @@ async function runReporter() {
}
if (searchType === "waitForStore") method = "findStore";
let result: any;
try {
let result: any;
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
const [factory] = args;
result = factory();
@ -50,16 +49,26 @@ async function runReporter() {
result = await Webpack.extractAndLoadChunks(code, matcher);
if (result === false) result = null;
} else if (method === "mapMangledModule") {
const [code, mapper] = args;
result = Webpack.mapMangledModule(code, mapper);
if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
} else {
// @ts-ignore
result = Webpack[method](...args);
}
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
} catch (e) {
let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map((arg: any) => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
else if (method === "mapMangledModule") {
const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null);
logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`;
}
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
ReporterLogger.log("Webpack Find Fail:", logMessage);

View file

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

5
src/modules.d.ts vendored
View file

@ -22,6 +22,11 @@
declare module "~plugins" {
const plugins: Record<string, import("./utils/types").Plugin>;
export default plugins;
export const PluginMeta: Record<string, {
folderName: string;
userPlugin: boolean;
}>;
export const ExcludedPlugins: Record<string, "web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev">;
}
declare module "~pluginNatives" {

View file

@ -21,21 +21,27 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "UserSettingsAPI",
description: "Patches Discord's UserSettings to expose their group and name",
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]$&"
match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/,
replace: "userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1],$&"
},
// some wrapper. just make it copy the group and name
// Selective wrapper
{
match: /updateSetting:.{0,20}shouldSync/,
replace: "userSettingsApiGroup:arguments[0].userSettingsApiGroup,userSettingsApiName:arguments[0].userSettingsApiName,$&"
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

@ -57,33 +57,18 @@ export default definePlugin({
}
]
},
// Discord Stable
// FIXME: remove once change merged to stable
{
find: "Messages.ACTIVITY_SETTINGS",
noWarn: true,
replacement: {
get match() {
switch (Settings.plugins.Settings!.settingsLocation) {
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
case "aboveActivity":
default:
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
}
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}`
},
replace: "...$self.makeSettingsCategories($1),$&"
}
},
{
find: "Messages.ACTIVITY_SETTINGS",
replacement: {
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}
{
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,30}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
}
]
},
{
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
@ -251,8 +236,8 @@ export default definePlugin({
},
makeInfoElements(Component: ComponentType<PropsWithChildren>, props: PropsWithChildren) {
return this.getInfoRows().map((text, i) =>
return this.getInfoRows().map((text, i) => (
<Component key={i} {...props}>{text}</Component>
);
));
}
});

View file

@ -16,24 +16,34 @@
* 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 { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { sendMessage } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc";
import { isPluginDev, tryOrElse } from "@utils/misc";
import { relaunch } from "@utils/native";
import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
import { isOutdated, update } from "@utils/updater";
import { AlertActionCreators, Card, ChannelStore, Forms, GuildMemberStore, MarkupUtils, RelationshipStore, RouterUtils, UserStore } from "@webpack/common";
import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { AlertActionCreators, Button, Card, ChannelStore, Forms, GuildMemberStore, MarkupUtils, RelationshipStore, showToast, Toasts, UserStore } from "@webpack/common";
import type { JSX } from "react";
import gitHash from "~git-hash";
import plugins from "~plugins";
import plugins, { PluginMeta } from "~plugins";
import settings from "./settings";
const VENCORD_GUILD_ID = "1015060230222131221";
const VENBOT_USER_ID = "1017176847865352332";
const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
const CodeBlockRe = /```js\n(.+?)```/s;
const AllowedChannelIds = [
SUPPORT_CHANNEL_ID,
@ -47,12 +57,88 @@ const TrustedRolesIds = [
"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: Record<"Vencord" | "Client" | "Platform", string> & { "Last Crash Reason"?: string; } = {
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({
name: "SupportHelper",
required: true,
description: "Helps us provide support to you",
authors: [Devs.Ven],
dependencies: ["CommandsAPI"],
dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
patches: [{
find: ".BEGINNING_DM.format",
@ -62,54 +148,20 @@ export default definePlugin({
}
}],
commands: [{
name: "vencord-debug",
description: "Send Vencord Debug info",
predicate: ctx => {
const meId = UserStore.getCurrentUser()?.id;
return meId && isPluginDev(meId) || AllowedChannelIds.includes(ctx.channel.id);
commands: [
{
name: "vencord-debug",
description: "Send Vencord debug info",
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
execute: async () => ({ content: await generateDebugInfoMessage() })
},
async execute() {
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 isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin]!.required;
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
const info: Record<string, string> = {
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", "```")
};
{
name: "vencord-plugins",
description: "Send Vencord plugin list",
predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
execute: () => ({ content: generatePluginList() })
}
}],
],
flux: {
async CHANNEL_SELECT({ channelId }) {
@ -118,25 +170,28 @@ ${makeCodeblock(enabledPlugins.join(", "))}
const selfId = UserStore.getCurrentUser()?.id;
if (!selfId || isPluginDev(selfId)) return;
if (isOutdated) {
AlertActionCreators.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please first update before asking for support!
</Forms.FormText>
</div>,
onCancel: () => { openUpdaterModal!(); },
cancelText: "View Updates",
confirmText: "Update & Restart Now",
async onConfirm() {
await update();
relaunch();
},
secondaryConfirmText: "I know what I'm doing or I can't update"
});
return;
if (!IS_UPDATER_DISABLED) {
await checkForUpdatesOnce().catch(() => { });
if (isOutdated) {
AlertActionCreators.show({
title: "Hold on!",
body: (
<div>
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please first update before asking for support!
</Forms.FormText>
</div>
),
onCancel: () => { openUpdaterModal!(); },
cancelText: "View Updates",
confirmText: "Update & Restart Now",
onConfirm: forceUpdate,
secondaryConfirmText: "I know what I'm doing or I can't update"
});
return;
}
}
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
@ -145,14 +200,15 @@ ${makeCodeblock(enabledPlugins.join(", "))}
if (!IS_WEB && IS_UPDATER_DISABLED) {
AlertActionCreators.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
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.
</Forms.FormText>
</div>,
onCloseCallback: () => setTimeout(() => { RouterUtils.back(); }, 50)
body: (
<div>
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
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.
</Forms.FormText>
</div>
)
});
return;
}
@ -161,14 +217,15 @@ ${makeCodeblock(enabledPlugins.join(", "))}
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
AlertActionCreators.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
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.
</Forms.FormText>
</div>,
onCloseCallback: () => setTimeout(() => { RouterUtils.back(); }, 50)
body: (
<div>
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
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.
</Forms.FormText>
</div>
)
});
return;
}
@ -177,7 +234,7 @@ ${makeCodeblock(enabledPlugins.join(", "))}
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
if (!isPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId)) return null;
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
return (
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
@ -187,5 +244,86 @@ ${makeCodeblock(enabledPlugins.join(", "))}
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
</Card>
);
}, { noop: true })
}, { noop: true }),
start() {
addAccessory("vencord-debug", props => {
const buttons: 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={() => { 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

@ -10,7 +10,7 @@ import definePlugin, { OptionType, type PluginNative, ReporterTestable } from "@
import { type Activity, type ActivityAssets, ActivityFlags, ActivityType } from "@vencord/discord-types";
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 ActivityButton {
label: string;

View file

@ -26,7 +26,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { ActivityFlags, ActivityType } from "@vencord/discord-types";
import { type Activity, ActivityFlags, ActivityType } from "@vencord/discord-types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
@ -38,34 +38,8 @@ const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")
async function getApplicationAsset(key: string) {
if (/^https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key))
return "mp:" + key.replace(/^https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
}
interface ActivityAssets {
large_image?: string;
large_text?: string;
small_image?: string;
small_text?: string;
}
interface Activity {
state?: string;
details?: string;
timestamps?: {
start?: number;
end?: number;
};
assets?: ActivityAssets;
buttons?: string[];
name: string;
application_id: string;
metadata?: {
button_urls?: string[];
};
type: ActivityType;
url?: string;
flags: ActivityFlags;
return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0]!;
}
const enum TimestampMode {
@ -315,6 +289,8 @@ async function createActivity() {
if (!appName) return;
const activity: Activity = {
id: "custom",
created_at: Date.now(),
application_id: appID || "0",
name: appName,
state,

View file

@ -24,7 +24,7 @@ import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, type Patch } from "@utils/types";
import { DraftType, type Emoji, EmojiIntention, EmojiType, type FluxPersistedStore, type FluxStore, type MessageAttachment, type MessageEmbed, type MessageRecord, type Sticker, StickerFormat, UserPremiumType } from "@vencord/discord-types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { AlertActionCreators, ChannelStore, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, MarkupUtils, Permissions, PermissionStore, promptToUpload, UserSettingsProtoActionCreators, UserStore } from "@webpack/common";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
@ -51,6 +51,8 @@ const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsP
const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
const isUnusableRoleSubscriptionEmoji = findByCodeLazy(".getUserIsAdmin(");
const IS_BYPASSEABLE_INTENTION = `[${EmojiIntention.CHAT},${EmojiIntention.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;
interface StickerPack {
@ -192,16 +194,14 @@ export default definePlugin({
}
]
},
// FIXME
// Allows the usage of subscription-locked emojis
/* {
{
find: ".getUserIsAdmin(",
replacement: {
match: /(?=.+?\.getUserIsAdmin\((?<=function (\i)\(\i,\i\){.+?))(\i):function\(\){return \1}/,
// Replace the original export with a func that always returns false and alias the original
replace: "$2:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function(){return $1}"
match: /(function \i\(\i,\i)\){(.{0,250}.getUserIsAdmin\(.+?return!1})/,
replace: (_, rest1, rest2) => `${rest1},fakeNitroOriginal){if(!fakeNitroOriginal)return false;${rest2}`
}
}, */
},
// Allow stickers to be sent everywhere
{
find: "canUseCustomStickersEverywhere:function",
@ -500,7 +500,7 @@ export default definePlugin({
let nextIndex = content.length;
const transformLinkChild = (child: ReactElement) => {
function transformLinkChild(child: ReactElement) {
if (settings.store.transformEmojis) {
const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex);
if (fakeNitroMatch) {
@ -532,9 +532,9 @@ export default definePlugin({
}
return child;
};
}
const transformChild = (child?: ReactElement) => {
function transformChild(child?: ReactElement) {
if (child?.props?.trusted != null) return transformLinkChild(child);
if (child?.props?.children != null) {
if (!Array.isArray(child.props.children)) {
@ -548,7 +548,7 @@ export default definePlugin({
}
return child;
};
}
const modifyChild = (child: ReactElement) => {
const newChild = transformChild(child);
@ -776,9 +776,7 @@ export default definePlugin({
if (emoji.type === EmojiType.UNICODE) return true;
if (emoji.available === false) return false;
// FIXME
/* const isUnusableRoleSubEmoji = isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
if (isUnusableRoleSubEmoji(e, this.guildId)) return false; */
if (isUnusableRoleSubscriptionEmoji(emoji, this.guildId, true)) return false;
if (this.canUseEmotes)
return emoji.guildId === this.guildId || hasExternalEmojiPerms(channelId);

View file

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

View file

@ -35,9 +35,9 @@ export const PMLogger = logger;
export const plugins = Plugins;
export const patches: Patch[] = [];
/** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */
/** Whether we have subscribed to Flux actions of all the enabled plugins when FluxDispatcher was ready */
let enabledPluginsSubscribedFlux = false;
const subscribedFluxEventsPlugins = new Set<string>();
const subscribedFluxActionsPlugins = new Set<string>();
const pluginsValues = Object.values(Plugins);
const settings = Settings.plugins;
@ -163,20 +163,33 @@ export function startDependenciesRecursive(plugin: Plugin) {
return { restartNeeded, failures };
}
export function subscribePluginFluxEvents(plugin: Plugin, fluxDispatcher: typeof FluxDispatcher) {
if (plugin.flux && !subscribedFluxEventsPlugins.has(plugin.name) && (!IS_REPORTER || isReporterTestable(plugin, ReporterTestable.FluxActions))) {
subscribedFluxEventsPlugins.add(plugin.name);
export function subscribePluginFluxActions(plugin: Plugin, fluxDispatcher: typeof FluxDispatcher) {
if (plugin.flux && !subscribedFluxActionsPlugins.has(plugin.name) && (!IS_REPORTER || isReporterTestable(plugin, ReporterTestable.FluxActions))) {
subscribedFluxActionsPlugins.add(plugin.name);
logger.debug("Subscribing to Flux actions of plugin", plugin.name);
for (const [action, handler] of Object.entries(plugin.flux)) {
fluxDispatcher.subscribe(action as FluxActionType, handler);
const wrappedHandler = plugin.flux[action as FluxActionType] = function () {
try {
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
const res = handler.apply(plugin, arguments as any);
// @ts-ignore
return res instanceof Promise
? res.catch(e => { logger.error(`${plugin.name}: Error while handling ${action}\n`, e); })
: res;
} catch (e) {
logger.error(`${plugin.name}: Error while handling ${action}\n`, e);
}
};
fluxDispatcher.subscribe(action as FluxActionType, wrappedHandler);
}
}
}
export function unsubscribePluginFluxEvents(plugin: Plugin, fluxDispatcher: typeof FluxDispatcher) {
export function unsubscribePluginFluxActions(plugin: Plugin, fluxDispatcher: typeof FluxDispatcher) {
if (plugin.flux) {
subscribedFluxEventsPlugins.delete(plugin.name);
subscribedFluxActionsPlugins.delete(plugin.name);
logger.debug("Unsubscribing from Flux action of plugin", plugin.name);
for (const [action, handler] of Object.entries(plugin.flux)) {
@ -185,12 +198,12 @@ export function unsubscribePluginFluxEvents(plugin: Plugin, fluxDispatcher: type
}
}
export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatcher) {
export function subscribeAllPluginsFluxActions(fluxDispatcher: typeof FluxDispatcher) {
enabledPluginsSubscribedFlux = true;
for (const name in Plugins) {
if (!isPluginEnabled(name)) continue;
subscribePluginFluxEvents(Plugins[name]!, fluxDispatcher);
subscribePluginFluxActions(Plugins[name]!, fluxDispatcher);
}
}
@ -226,7 +239,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(plu
}
if (enabledPluginsSubscribedFlux) {
subscribePluginFluxEvents(plugin, FluxDispatcher);
subscribePluginFluxActions(plugin, FluxDispatcher);
}
@ -271,7 +284,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(plugin
}
}
unsubscribePluginFluxEvents(plugin, FluxDispatcher);
unsubscribePluginFluxActions(plugin, FluxDispatcher);
if (contextMenus) {
logger.debug("Removing context menus patches of plugin", name);

View file

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

View file

@ -13,7 +13,7 @@ import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore } from "@webpack/common";
const SummaryStore: FluxPersistedStore & Record<string, any> = findByPropsLazy("allSummaries", "findSummary");
const createSummaryFromServer = findByCodeLazy(".people)),startId:");
const createSummaryFromServer = findByCodeLazy(".people)),startId:", ".type}");
const settings = definePluginSettings({
summaryExpiryThresholdDays: {
@ -72,12 +72,15 @@ export default definePlugin({
],
flux: {
CONVERSATION_SUMMARY_UPDATE(data) {
const incomingSummaries: ChannelSummaries[] = data.summaries.map((summary: any) => ({ ...createSummaryFromServer(summary), time: Date.now() }));
const incomingSummaries: ChannelSummaries[] = data.summaries
.map((summary: any) => ({ ...createSummaryFromServer(summary), time: Date.now() }));
// idk if this is good for performance but it doesnt seem to be a problem in my experience
DataStore.update("summaries-data", summaries => {
summaries ??= {};
summaries[data.channel_id] ? summaries[data.channel_id].unshift(...incomingSummaries) : (summaries[data.channel_id] = incomingSummaries);
summaries[data.channel_id]
? summaries[data.channel_id].unshift(...incomingSummaries)
: (summaries[data.channel_id] = incomingSummaries);
if (summaries[data.channel_id].length > 50)
summaries[data.channel_id] = summaries[data.channel_id].slice(0, 50);

View file

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

View file

@ -8,7 +8,6 @@ import type { MessageJSON } from "@api/Commands";
import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, type PluginNative, ReporterTestable } from "@utils/types";
import { type ChannelRecord, ChannelType } from "@vencord/discord-types";
import { findByCodeLazy } from "@webpack";
@ -24,8 +23,6 @@ interface Call {
const notificationsShouldNotify = findByCodeLazy(".SUPPRESS_NOTIFICATIONS))return!1");
const XSLog = new Logger("XSOverlay");
const settings = definePluginSettings({
botNotifications: {
type: OptionType.BOOLEAN,
@ -91,7 +88,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({
name: "XSOverlay",
@ -109,110 +106,102 @@ export default definePlugin({
}
},
MESSAGE_CREATE({ message, optimistic }: { message: MessageJSON; optimistic: boolean; }) {
// Apparently without this try/catch, discord's socket connection dies if any part of this errors
try {
if (optimistic) return;
const channel = ChannelStore.getChannel(message.channel_id)!;
if (!shouldNotify(message, message.channel_id)) return;
if (optimistic) return;
const channel = ChannelStore.getChannel(message.channel_id)!;
if (!shouldNotify(message, message.channel_id)) return;
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
let finalMsg = message.content;
let titleString = "";
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
let finalMsg = message.content;
let titleString = "";
if (channel.guild_id) {
const guild = GuildStore.getGuild(channel.guild_id)!;
titleString = `${message.author.username} (${guild.name}, #${channel.name})`;
if (channel.guild_id) {
const guild = GuildStore.getGuild(channel.guild_id)!;
titleString = `${message.author.username} (${guild.name}, #${channel.name})`;
}
switch (channel.type) {
case ChannelType.DM:
titleString = message.author.username.trim();
break;
case ChannelType.GROUP_DM:
const channelName = channel.name.trim();
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)";
}
}
switch (channel.type) {
case ChannelType.DM:
titleString = message.author.username.trim();
break;
case ChannelType.GROUP_DM:
const channelName = channel.name.trim();
titleString = `${message.author.username} (${channelName})`;
break;
if (message.sticker_items) {
finalMsg += " [sticker] ";
if (message.content === "") {
finalMsg = "sent a sticker";
}
}
if (message.referenced_message) {
titleString += " (reply)";
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.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 = channel.guild_id && 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>`);
}
}
if (message.embeds.length > 0) {
finalMsg += " [embed] ";
if (message.content === "") {
finalMsg = "sent message embed(s)";
}
// 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]}:`);
}
}
if (message.sticker_items) {
finalMsg += " [sticker] ";
if (message.content === "") {
finalMsg = "sent a sticker";
}
}
const images = message.attachments.filter(a =>
typeof a.content_type === "string"
&& a.content_type.startsWith("image")
);
images.forEach(img => {
finalMsg += ` [image: ${img.filename}] `;
});
message.attachments.filter(a => !a.content_type?.startsWith("image")).forEach(a => {
finalMsg += ` [attachment: ${a.filename}] `;
});
// make mentions readable
if (message.mentions.length > 0) {
// 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(
/<@!?(\d{17,20})>/g,
(_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`
new RegExp(`${cMatch}`, "g"),
`<b><color=#${channelPingColor}>#${ChannelStore.getChannel(channelId)!.name}</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}`);
}
if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message);
}
}
});

View file

@ -118,7 +118,7 @@ export const openImageModal = (url: string, props?: Partial<ComponentProps<Image
placeholder={url}
src={url}
renderLinkComponent={props => <MaskedLink {...props} />}
// FIXME: wtf is this? do we need to pass some proper component??
// Don't render forward message button
renderForwardComponent={() => null}
shouldHideMediaOptions={false}
shouldAnimate

View file

@ -96,3 +96,14 @@ export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id);
export function pluralise(amount: number, singular: string, plural = singular + "s") {
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

@ -38,7 +38,7 @@ const enum ModalTransitionState {
export interface ModalProps {
transitionState: ModalTransitionState;
onClose: () => Promise<void>;
onClose: () => void;
}
export interface ModalOptions {

View file

@ -131,3 +131,18 @@ export function makeCodeblock(text: string, language?: string) {
const 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

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

View file

@ -21,8 +21,8 @@ export * from "./components";
export * from "./menu";
export * from "./react";
export * from "./stores";
export type * as ComponentTypes from "./types/components";
export type * as MenuTypes from "./types/menu";
export type * as UtilTypes from "./types/utils";
export * from "./UserSettings";
export type * as ComponentTypes from "./types/components.d";
export type * as MenuTypes from "./types/menu.d";
export type * as UtilTypes from "./types/utils.d";
export * from "./userSettings";
export * from "./utils";

View file

@ -18,6 +18,7 @@
import type { GuildMember, GuildRecord, UserRecord } from "@vencord/discord-types";
import type { ReactNode } from "react";
import { LiteralUnion } from "type-fest";
export type MarkupUtils = Record<
| "parse"
@ -70,7 +71,7 @@ interface RestRequestData {
retries?: number;
}
export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>>;
export type RestAPI = Record<"del" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>>;
export type PermissionsKeys = "CREATE_INSTANT_INVITE" | "KICK_MEMBERS" | "BAN_MEMBERS" | "ADMINISTRATOR" | "MANAGE_CHANNELS" | "MANAGE_GUILD" | "ADD_REACTIONS" | "VIEW_AUDIT_LOG" | "PRIORITY_SPEAKER" | "STREAM" | "VIEW_CHANNEL" | "SEND_MESSAGES" | "SEND_TTS_MESSAGES" | "MANAGE_MESSAGES" | "EMBED_LINKS" | "ATTACH_FILES" | "READ_MESSAGE_HISTORY" | "MENTION_EVERYONE" | "USE_EXTERNAL_EMOJIS" | "VIEW_GUILD_ANALYTICS" | "CONNECT" | "SPEAK" | "MUTE_MEMBERS" | "DEAFEN_MEMBERS" | "MOVE_MEMBERS" | "USE_VAD" | "CHANGE_NICKNAME" | "MANAGE_NICKNAMES" | "MANAGE_ROLES" | "MANAGE_WEBHOOKS" | "MANAGE_GUILD_EXPRESSIONS" | "USE_APPLICATION_COMMANDS" | "REQUEST_TO_SPEAK" | "MANAGE_EVENTS" | "MANAGE_THREADS" | "CREATE_PUBLIC_THREADS" | "CREATE_PRIVATE_THREADS" | "USE_EXTERNAL_STICKERS" | "SEND_MESSAGES_IN_THREADS" | "USE_EMBEDDED_ACTIVITIES" | "MODERATE_MEMBERS" | "VIEW_CREATOR_MONETIZATION_ANALYTICS" | "USE_SOUNDBOARD" | "CREATE_GUILD_EXPRESSIONS" | "CREATE_EVENTS" | "USE_EXTERNAL_SOUNDS" | "SEND_VOICE_MESSAGES" | "USE_CLYDE_AI" | "SET_VOICE_CHANNEL_STATUS" | "SEND_POLLS" | "USE_EXTERNAL_APPS";
@ -137,3 +138,37 @@ export interface Constants {
UserFlags: Record<string, number>;
FriendsSections: Record<string, string>;
}
export interface ExpressionPickerStore {
closeExpressionPicker(activeViewType?: any): void;
openExpressionPicker(activeView: LiteralUnion<"emoji" | "gif" | "sticker", string>, activeViewType?: any): void;
}
export interface BrowserWindowFeatures {
toolbar?: boolean;
menubar?: boolean;
location?: boolean;
directories?: boolean;
width?: number;
height?: number;
defaultWidth?: number;
defaultHeight?: number;
left?: number;
top?: number;
defaultAlwaysOnTop?: boolean;
movable?: boolean;
resizable?: boolean;
frame?: boolean;
alwaysOnTop?: boolean;
hasShadow?: boolean;
transparent?: boolean;
skipTaskbar?: boolean;
titleBarStyle?: string | null;
backgroundColor?: string;
}
export interface PopoutActions {
open(key: string, render: (windowKey: string) => ReactNode, features?: BrowserWindowFeatures);
close(key: string): void;
setAlwaysOnTop(key: string, alwaysOnTop: boolean): void;
}

View file

@ -28,7 +28,7 @@ export let FluxDispatcher: $FluxDispatcher;
waitFor(["dispatch", "subscribe"], (m: $FluxDispatcher) => {
FluxDispatcher = m;
// Non import call to avoid circular dependency
Vencord.Plugins.subscribeAllPluginsFluxEvents(m);
Vencord.Plugins.subscribeAllPluginsFluxActions(m);
const cb = () => {
m.unsubscribe("CONNECTION_OPEN", cb);
@ -120,7 +120,7 @@ waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m
export const Constants = findByPropsLazy("Endpoints");
const openExpressionPickerMatcher = canonicalizeMatch(/setState\({activeView:\i/);
const openExpressionPickerMatcher = canonicalizeMatch(/setState\({activeView:\i,activeViewType:/);
// TODO: type
// zustand store
@ -150,6 +150,12 @@ export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYea
export const Permissions: t.Permissions = findLazy(m => typeof m.ADMINISTRATOR === "bigint");
export const PopoutWindowActionCreators: t.PopoutWindowActionCreators = mapMangledModuleLazy('type:"POPOUT_WINDOW_OPEN"', {
open: filters.byCode('type:"POPOUT_WINDOW_OPEN"'),
close: filters.byCode('type:"POPOUT_WINDOW_CLOSE"'),
setAlwaysOnTop: filters.byCode('type:"POPOUT_WINDOW_SET_ALWAYS_ON_TOP"'),
});
export const promptToUpload: (files: File[], channel: ChannelRecord, draftType: DraftType) => void
= findByCodeLazy(".ATTACHMENT_TOO_MANY_ERROR_TITLE,");

View file

@ -288,7 +288,7 @@ export function findModuleFactory(...code: [string, ...string[]]) {
return wreq.m[id];
}
export const lazyWebpackSearchHistory: ["find" | "findByProps" | "findByCode" | "findStore" | "findComponent" | "findComponentByCode" | "findExportedComponent" | "waitFor" | "waitForComponent" | "waitForStore" | "proxyLazyWebpack" | "LazyComponentWebpack" | "extractAndLoadChunks", any[]][] = [];
export const lazyWebpackSearchHistory: ["find" | "findByProps" | "findByCode" | "findStore" | "findComponent" | "findComponentByCode" | "findExportedComponent" | "waitFor" | "waitForComponent" | "waitForStore" | "proxyLazyWebpack" | "LazyComponentWebpack" | "extractAndLoadChunks" | "mapMangledModule", any[]][] = [];
/**
* This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds.
@ -493,6 +493,8 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa
* })
*/
export function mapMangledModuleLazy<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers]]);
return proxyLazy(() => mapMangledModule(code, mappers));
}