Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Seaswimmer 2024-06-22 01:08:12 -04:00
commit 7f29a10ecb
No known key found for this signature in database
46 changed files with 838 additions and 458 deletions

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,31 +114,48 @@ 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 metaCode = "\n";
let excludedCode = "\n";
let i = 0; let i = 0;
for (const dir of pluginDirs) { for (const dir of pluginDirs) {
if (!await exists(`./src/${dir}`)) continue; const userPlugin = dir === "userplugins";
const files = await readdir(`./src/${dir}`);
for (const file of files) { const fullDir = `./src/${dir}`;
if (file.startsWith("_") || file.startsWith(".")) continue; if (!await exists(fullDir)) continue;
if (file === "index.ts") 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 && !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 mod = `p${i}`; const mod = `p${i}`;
code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`; code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`;
plugins += `[${mod}.name]:${mod},\n`; pluginsCode += `[${mod}.name]:${mod},\n`;
metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI?
i++; i++;
} }
} }
code += `export default {${plugins}};`; 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;
} }

View file

@ -1,69 +0,0 @@
/*
* 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";
import { Settings } from "./Settings";
interface Setting<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T | ((old: T) => T)): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
*/
useSetting(): T;
settingsStoreApiGroup: string;
settingsStoreApiName: string;
}
export const SettingsStores: Array<Setting<any>> | undefined = proxyLazyWebpack(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"') as any;
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
});
/**
* Get the store for a setting
* @param group The setting group
* @param name The name of the setting
*/
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
}
/**
* getSettingStore but lazy
*/
export function getSettingStoreLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getSettingStore<T>(group, name));
}

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

@ -31,8 +31,8 @@ import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications"; 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 $SettingsStores from "./SettingsStores";
import * as $Styles from "./Styles"; import * as $Styles from "./Styles";
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
@ -118,4 +118,7 @@ export const ChatButtons = $ChatButtons;
*/ */
export const MessageUpdater = $MessageUpdater; export const MessageUpdater = $MessageUpdater;
export const SettingsStores = $SettingsStores; /**
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { DevsById } from "@utils/constants"; import { DevsById } from "@utils/constants";
import { fetchUserProfile, getTheme, Theme } from "@utils/discord"; import { fetchUserProfile } from "@utils/discord";
import { pluralise } from "@utils/misc"; import { classes, pluralise } from "@utils/misc";
import { ModalContent, ModalRoot, openModal } from "@utils/modal"; import { ModalContent, ModalRoot, openModal } from "@utils/modal";
import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import Plugins from "~plugins"; import Plugins from "~plugins";
import { PluginCard } from "."; import { PluginCard } from ".";
import { GithubButton, WebsiteButton } from "./LinkIconButton";
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
const cl = classNameFactory("vc-author-modal-"); const cl = classNameFactory("vc-author-modal-");
@ -40,16 +36,6 @@ export function openContributorModal(user: User) {
); );
} }
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: User; }) { function ContributorModal({ user }: { user: User; }) {
useSettings(); useSettings();
@ -86,24 +72,18 @@ function ContributorModal({ user }: { user: User; }) {
/> />
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle> <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 && ( {website && (
<Tooltip text={website}> <WebsiteButton
{props => ( text={website}
<MaskedLink {...props} href={"https://" + website}> href={`https://${website}`}
<WebsiteIcon /> />
</MaskedLink>
)}
</Tooltip>
)} )}
{githubName && ( {githubName && (
<Tooltip text={githubName}> <GithubButton
{props => ( text={githubName}
<MaskedLink {...props} href={`https://github.com/${githubName}`}> href={`https://github.com/${githubName}`}
<GithubIcon /> />
</MaskedLink>
)}
</Tooltip>
)} )}
</div> </div>
</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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./PluginModal.css";
import { generateId } from "@api/Commands"; import { generateId } from "@api/Commands";
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { gitRemote } from "@shared/vencordUserAgent";
import { proxyLazy } from "@utils/lazy"; import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc"; import { classes, isObjectEmpty } from "@utils/misc";
@ -30,6 +34,8 @@ import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserSto
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import { Constructor } from "type-fest"; import { Constructor } from "type-fest";
import { PluginMeta } from "~plugins";
import { import {
ISettingElementProps, ISettingElementProps,
SettingBooleanComponent, SettingBooleanComponent,
@ -40,6 +46,9 @@ import {
SettingTextComponent SettingTextComponent
} from "./components"; } from "./components";
import { openContributorModal } from "./ContributorModal"; import { openContributorModal } from "./ContributorModal";
import { GithubButton, WebsiteButton } from "./LinkIconButton";
const cl = classNameFactory("vc-plugin-modal-");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); const AvatarStyles = 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 ( return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable"> <ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
<ModalHeader separator={false}> <ModalHeader separator={false}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text> <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} /> <ModalCloseButton onClick={onClose} />
</ModalHeader> </ModalHeader>
<ModalContent> <ModalContent>
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle> <Flex className={cl("info")}>
<Forms.FormText>{plugin.description}</Forms.FormText> <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> <Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
<div style={{ width: "fit-content", marginBottom: 8 }}> <div style={{ width: "fit-content", marginBottom: 8 }}>
<UserSummaryItem <UserSummaryItem

View file

@ -42,16 +42,6 @@
.vc-author-modal-links { .vc-author-modal-links {
margin-left: auto; 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 { .vc-author-modal-plugins {

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

@ -39,9 +39,8 @@ async function runReporter() {
} }
if (searchType === "waitForStore") method = "findStore"; if (searchType === "waitForStore") method = "findStore";
let result: any;
try { try {
let result: any;
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
const [factory] = args; const [factory] = args;
result = factory(); result = factory();
@ -50,16 +49,26 @@ async function runReporter() {
result = await Webpack.extractAndLoadChunks(code, matcher); result = await Webpack.extractAndLoadChunks(code, matcher);
if (result === false) result = null; 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 { } else {
// @ts-ignore // @ts-ignore
result = Webpack[method](...args); 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) { } catch (e) {
let logMessage = searchType; let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${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(", ")})`; else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
ReporterLogger.log("Webpack Find Fail:", logMessage); ReporterLogger.log("Webpack Find Fail:", logMessage);

5
src/modules.d.ts vendored
View file

@ -22,6 +22,11 @@
declare module "~plugins" { declare module "~plugins" {
const plugins: Record<string, import("./utils/types").Plugin>; const plugins: Record<string, import("./utils/types").Plugin>;
export default plugins; 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" { declare module "~pluginNatives" {

View file

@ -20,23 +20,30 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "SettingsStoreAPI", name: "UserSettingsAPI",
description: "Patches Discord's SettingsStores to expose their group and name", description: "Patches Discord's UserSettings to expose their group and name.",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
patches: [ patches: [
{ {
find: ",updateSetting:", find: ",updateSetting:",
replacement: [ replacement: [
// Main setting definition
{ {
match: /(?<=INFREQUENT_USER_ACTION.{0,20}),useSetting:/, match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/,
replace: ",settingsStoreApiGroup:arguments[0],settingsStoreApiName:arguments[1]$&" replace: "userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1],$&"
}, },
// some wrapper. just make it copy the group and name // Selective wrapper
{ {
match: /updateSetting:.{0,20}shouldSync/, match: /updateSetting:.{0,100}SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE/,
replace: "settingsStoreApiGroup:arguments[0].settingsStoreApiGroup,settingsStoreApiName:arguments[0].settingsStoreApiName,$&" 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

@ -56,33 +56,18 @@ export default definePlugin({
} }
] ]
}, },
// Discord Stable
// FIXME: remove once change merged to stable
{ {
find: "Messages.ACTIVITY_SETTINGS", find: "Messages.ACTIVITY_SETTINGS",
noWarn: true, replacement: [
replacement: { {
get match() { match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
switch (Settings.plugins.Settings.settingsLocation) { replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
case "aboveActivity":
default:
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
}
}, },
replace: "...$self.makeSettingsCategories($1),$&" {
} 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.ACTIVITY_SETTINGS", ]
replacement: {
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}
}, },
{ {
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL", find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",

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"],
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 { getSettingStoreLazy } from "@api/SettingsStores"; 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 = getSettingStoreLazy("appearance", "developerMode")!; const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!;
function PencilIcon() { function PencilIcon() {
return ( return (
@ -65,7 +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: ["SettingsStoreAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,

View file

@ -17,7 +17,7 @@
*/ */
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores"; 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,8 +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 = getSettingStoreLazy<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)\//, "");
@ -394,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: ["SettingsStoreAPI"], dependencies: ["UserSettingsAPI"],
start: setRpc, start: setRpc,
stop: () => setRpc(true), stop: () => setRpc(true),
settings, settings,

View file

@ -23,7 +23,7 @@ import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord"; import { getCurrentGuild } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { Emoji } from "@webpack/types"; import type { Emoji } from "@webpack/types";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
@ -52,6 +52,7 @@ const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsA
const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
const isUnusableRoleSubscriptionEmoji = findByCodeLazy(".getUserIsAdmin(");
const enum EmojiIntentions { const enum EmojiIntentions {
REACTION, REACTION,
@ -234,16 +235,14 @@ export default definePlugin({
} }
] ]
}, },
// FIXME
// Allows the usage of subscription-locked emojis // Allows the usage of subscription-locked emojis
/* { {
find: ".getUserIsAdmin(", find: ".getUserIsAdmin(",
replacement: { replacement: {
match: /(?=.+?\.getUserIsAdmin\((?<=function (\i)\(\i,\i\){.+?))(\i):function\(\){return \1}/, match: /(function \i\(\i,\i)\){(.{0,250}.getUserIsAdmin\(.+?return!1})/,
// Replace the original export with a func that always returns false and alias the original replace: (_, rest1, rest2) => `${rest1},fakeNitroOriginal){if(!fakeNitroOriginal)return false;${rest2}`
replace: "$2:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function(){return $1}"
} }
}, */ },
// Allow stickers to be sent everywhere // Allow stickers to be sent everywhere
{ {
find: "canUseCustomStickersEverywhere:function", find: "canUseCustomStickersEverywhere:function",
@ -817,9 +816,7 @@ export default definePlugin({
if (e.type === 0) return true; if (e.type === 0) return true;
if (e.available === false) return false; if (e.available === false) return false;
// FIXME if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false;
/* const isUnusableRoleSubEmoji = isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
if (isUnusableRoleSubEmoji(e, this.guildId)) return false; */
if (this.canUseEmotes) if (this.canUseEmotes)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId); return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);

View file

@ -17,8 +17,8 @@
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores";
import { disableStyle, enableStyle } from "@api/Styles"; import { disableStyle, enableStyle } from "@api/Styles";
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 = getSettingStoreLazy<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: ["SettingsStoreAPI"], 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 { getSettingStoreLazy } from "@api/SettingsStores"; 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 = getSettingStoreLazy("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,7 +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: ["SettingsStoreAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,

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 { getSettingStoreLazy } from "@api/SettingsStores"; 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 = getSettingStoreLazy("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,7 +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", "SettingsStoreAPI"], dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI", "UserSettingsAPI"],
settings, settings,

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

@ -21,6 +21,7 @@ import { Button, showToast, Toasts, useState } from "@webpack/common";
import type { VoiceRecorder } from "."; import type { VoiceRecorder } from ".";
import { settings } from "./settings"; import { settings } from "./settings";
import { MediaEngineStore } from "./utils";
const Native = VencordNative.pluginHelpers.VoiceMessages as PluginNative<typeof import("./native")>; const Native = VencordNative.pluginHelpers.VoiceMessages as PluginNative<typeof import("./native")>;
@ -41,6 +42,7 @@ export const VoiceRecorderDesktop: VoiceRecorder = ({ setAudioBlob, onRecordingC
{ {
echoCancellation: settings.store.echoCancellation, echoCancellation: settings.store.echoCancellation,
noiseCancellation: settings.store.noiseSuppression, noiseCancellation: settings.store.noiseSuppression,
deviceId: MediaEngineStore.getInputDeviceId(),
}, },
(success: boolean) => { (success: boolean) => {
if (success) if (success)

View file

@ -20,6 +20,7 @@ import { Button, useState } from "@webpack/common";
import type { VoiceRecorder } from "."; import type { VoiceRecorder } from ".";
import { settings } from "./settings"; import { settings } from "./settings";
import { MediaEngineStore } from "./utils";
export const VoiceRecorderWeb: VoiceRecorder = ({ setAudioBlob, onRecordingChange }) => { export const VoiceRecorderWeb: VoiceRecorder = ({ setAudioBlob, onRecordingChange }) => {
const [recording, setRecording] = useState(false); const [recording, setRecording] = useState(false);
@ -40,6 +41,7 @@ export const VoiceRecorderWeb: VoiceRecorder = ({ setAudioBlob, onRecordingChang
audio: { audio: {
echoCancellation: settings.store.echoCancellation, echoCancellation: settings.store.echoCancellation,
noiseSuppression: settings.store.noiseSuppression, noiseSuppression: settings.store.noiseSuppression,
deviceId: MediaEngineStore.getInputDeviceId()
} }
}).then(stream => { }).then(stream => {
const chunks = [] as Blob[]; const chunks = [] as Blob[];

View file

@ -17,5 +17,7 @@
*/ */
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { findStoreLazy } from "@webpack";
export const MediaEngineStore = findStoreLazy("MediaEngineStore");
export const cl = classNameFactory("vc-vmsg-"); export const cl = classNameFactory("vc-vmsg-");

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

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

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

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

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

@ -20,9 +20,9 @@ export * from "./classes";
export * from "./components"; export * from "./components";
export * from "./menu"; export * from "./menu";
export * from "./react"; export * from "./react";
export * from "./settingsStores";
export * from "./stores"; export * from "./stores";
export * as ComponentTypes from "./types/components.d"; export * as ComponentTypes from "./types/components.d";
export * as MenuTypes from "./types/menu.d"; export * as MenuTypes from "./types/menu.d";
export * as UtilTypes from "./types/utils.d"; export * as UtilTypes from "./types/utils.d";
export * from "./userSettings";
export * from "./utils"; export * from "./utils";

View file

@ -21,6 +21,5 @@ export * from "./components";
export * from "./fluxEvents"; export * from "./fluxEvents";
export * from "./i18nMessages"; export * from "./i18nMessages";
export * from "./menu"; export * from "./menu";
export * from "./settingsStores";
export * from "./stores"; export * from "./stores";
export * from "./utils"; export * from "./utils";

View file

@ -1,11 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface SettingsStore<T = any> {
getSetting(): T;
updateSetting(value: T): void;
useSetting(): T;
}

View file

@ -18,6 +18,7 @@
import { Guild, GuildMember } from "discord-types/general"; import { Guild, GuildMember } from "discord-types/general";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { LiteralUnion } from "type-fest";
import type { FluxEvents } from "./fluxEvents"; import type { FluxEvents } from "./fluxEvents";
import { i18nMessages } from "./i18nMessages"; import { i18nMessages } from "./i18nMessages";
@ -81,7 +82,7 @@ interface RestRequestData {
retries?: number; 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 Permissions = "CREATE_INSTANT_INVITE" export type Permissions = "CREATE_INSTANT_INVITE"
| "KICK_MEMBERS" | "KICK_MEMBERS"
@ -221,3 +222,37 @@ export interface Constants {
UserFlags: Record<string, number>; UserFlags: Record<string, number>;
FriendsSections: Record<string, string>; 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

@ -160,9 +160,15 @@ export const InviteActions = findByPropsLazy("resolveInvite");
export const IconUtils: t.IconUtils = findByPropsLazy("getGuildBannerURL", "getUserAvatarURL"); export const IconUtils: t.IconUtils = findByPropsLazy("getGuildBannerURL", "getUserAvatarURL");
const openExpressionPickerMatcher = canonicalizeMatch(/setState\({activeView:\i/); const openExpressionPickerMatcher = canonicalizeMatch(/setState\({activeView:\i,activeViewType:/);
// TODO: type // TODO: type
export const ExpressionPickerStore = mapMangledModuleLazy("expression-picker-last-active-view", { export const ExpressionPickerStore: t.ExpressionPickerStore = mapMangledModuleLazy("expression-picker-last-active-view", {
closeExpressionPicker: filters.byCode("setState({activeView:null"), closeExpressionPicker: filters.byCode("setState({activeView:null"),
openExpressionPicker: m => typeof m === "function" && openExpressionPickerMatcher.test(m.toString()), openExpressionPicker: m => typeof m === "function" && openExpressionPickerMatcher.test(m.toString()),
}); });
export const PopoutActions: t.PopoutActions = 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"'),
});

View file

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