Merge branch 'main' into BypassDND

This commit is contained in:
Inbestigator 2025-02-07 19:30:00 -08:00 committed by GitHub
commit 6ef0c0232d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
223 changed files with 5031 additions and 4242 deletions

View file

@ -31,6 +31,7 @@ Before starting your plugin:
- No FakeDeafen or FakeMute - No FakeDeafen or FakeMute
- No StereoMic - No StereoMic
- No plugins that simply hide or redesign ui elements. This can be done with CSS - No plugins that simply hide or redesign ui elements. This can be done with CSS
- No plugins that interact with specific Discord bots (official Discord apps like Youtube WatchTogether are okay)
- No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc) - No selfbots or API spam (animated status, message pruner, auto reply, nitro snipers, etc)
- No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones - No untrusted third party APIs. Popular services like Google or GitHub are fine, but absolutely no self hosted ones
- No plugins that require the user to enter their own API key - No plugins that require the user to enter their own API key

View file

@ -4,10 +4,9 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
// @ts-check
import stylistic from "@stylistic/eslint-plugin"; import stylistic from "@stylistic/eslint-plugin";
import pathAlias from "eslint-plugin-path-alias"; import pathAlias from "eslint-plugin-path-alias";
import react from "eslint-plugin-react";
import header from "eslint-plugin-simple-header"; import header from "eslint-plugin-simple-header";
import simpleImportSort from "eslint-plugin-simple-import-sort"; import simpleImportSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports"; import unusedImports from "eslint-plugin-unused-imports";
@ -15,6 +14,22 @@ import tseslint from "typescript-eslint";
export default tseslint.config( export default tseslint.config(
{ ignores: ["dist", "browser", "packages/vencord-types"] }, { ignores: ["dist", "browser", "packages/vencord-types"] },
{
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"],
settings: {
react: {
version: "18"
}
},
...react.configs.flat.recommended,
rules: {
...react.configs.flat.recommended.rules,
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/display-name": "off",
"react/no-unescaped-entities": "off",
}
},
{ {
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"], files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}", "eslint.config.mjs"],
plugins: { plugins: {
@ -23,7 +38,7 @@ export default tseslint.config(
"@typescript-eslint": tseslint.plugin, "@typescript-eslint": tseslint.plugin,
"simple-import-sort": simpleImportSort, "simple-import-sort": simpleImportSort,
"unused-imports": unusedImports, "unused-imports": unusedImports,
"path-alias": pathAlias, "path-alias": pathAlias
}, },
settings: { settings: {
"import/resolver": { "import/resolver": {
@ -90,7 +105,13 @@ export default tseslint.config(
"no-invalid-regexp": "error", "no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }], "no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error", "no-duplicate-imports": "error",
"dot-notation": "error", "@typescript-eslint/dot-notation": [
"error",
{
"allowPrivateClassPropertyAccess": true,
"allowProtectedClassPropertyAccess": true
}
],
"no-useless-escape": [ "no-useless-escape": [
"error", "error",
{ {

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.10.7", "version": "1.11.4",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -41,48 +41,49 @@
"@vap/shiki": "0.10.5", "@vap/shiki": "0.10.5",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.50.0", "monaco-editor": "^0.52.2",
"nanoid": "^5.0.7", "nanoid": "^5.0.9",
"virtual-merge": "^1.0.1" "virtual-merge": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^2.6.1", "@stylistic/eslint-plugin": "^2.12.1",
"@types/chrome": "^0.0.269", "@types/chrome": "^0.0.287",
"@types/diff": "^5.2.1", "@types/diff": "^6.0.0",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.14",
"@types/node": "^22.0.3", "@types/node": "^22.10.5",
"@types/react": "^18.3.3", "@types/react": "^19.0.2",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^19.0.2",
"@types/yazl": "^2.4.5", "@types/yazl": "^2.4.5",
"diff": "^5.2.0", "diff": "^7.0.0",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
"esbuild": "^0.15.18", "esbuild": "^0.15.18",
"eslint": "^9.8.0", "eslint": "^9.17.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "2.1.0", "eslint-plugin-path-alias": "2.1.0",
"eslint-plugin-simple-header": "^1.1.1", "eslint-plugin-react": "^7.37.3",
"eslint-plugin-simple-header": "^1.2.1",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.0.1", "eslint-plugin-unused-imports": "^4.1.4",
"highlight.js": "10.7.3", "highlight.js": "11.7.0",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"moment": "^2.30.1", "moment": "^2.22.2",
"puppeteer-core": "^22.15.0", "puppeteer-core": "^23.11.1",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",
"stylelint": "^16.8.1", "stylelint": "^16.12.0",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^36.0.1",
"ts-patch": "^3.2.1", "ts-patch": "^3.3.0",
"ts-pattern": "^5.3.1", "ts-pattern": "^5.6.0",
"tsx": "^4.16.5", "tsx": "^4.19.2",
"type-fest": "^4.23.0", "type-fest": "^4.31.0",
"typescript": "^5.5.4", "typescript": "^5.7.2",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.19.0",
"typescript-transform-paths": "^3.4.7", "typescript-transform-paths": "^3.5.3",
"zip-local": "^0.3.5" "zip-local": "^0.3.5"
}, },
"packageManager": "pnpm@9.1.0", "packageManager": "pnpm@9.1.0",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"eslint@9.8.0": "patches/eslint@9.8.0.patch", "eslint@9.17.0": "patches/eslint@9.17.0.patch",
"eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch" "eslint-plugin-path-alias@2.1.0": "patches/eslint-plugin-path-alias@2.1.0.patch"
}, },
"peerDependencyRules": { "peerDependencyRules": {

3259
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -37,7 +37,8 @@ const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({ const browser = await pup.launch({
headless: true, headless: true,
executablePath: process.env.CHROMIUM_BIN executablePath: process.env.CHROMIUM_BIN,
args: ["--no-sandbox"]
}); });
const page = await browser.newPage(); const page = await browser.newPage();

View file

@ -57,7 +57,7 @@ const Badges = new Set<ProfileBadge>();
* Register a new badge with the Badges API * Register a new badge with the Badges API
* @param badge The badge to register * @param badge The badge to register
*/ */
export function addBadge(badge: ProfileBadge) { export function addProfileBadge(badge: ProfileBadge) {
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true }); badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
Badges.add(badge); Badges.add(badge);
} }
@ -66,7 +66,7 @@ export function addBadge(badge: ProfileBadge) {
* Unregister a badge from the Badges API * Unregister a badge from the Badges API
* @param badge The badge to remove * @param badge The badge to remove
*/ */
export function removeBadge(badge: ProfileBadge) { export function removeProfileBadge(badge: ProfileBadge) {
return Badges.delete(badge); return Badges.delete(badge);
} }
@ -100,20 +100,3 @@ export interface BadgeUserArgs {
userId: string; userId: string;
guildId: string; guildId: string;
} }
interface ConnectedAccount {
type: string;
id: string;
name: string;
verified: boolean;
}
interface Profile {
connectedAccounts: ConnectedAccount[];
premiumType: number;
premiumSince: string;
premiumGuildSince?: any;
lastFetched: number;
profileFetchFailed: boolean;
application?: any;
}

View file

@ -9,9 +9,9 @@ import "./ChatButton.css";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { waitFor } from "@webpack"; import { waitFor } from "@webpack";
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general"; import { Channel } from "discord-types/general";
import { HTMLProps, MouseEventHandler, ReactNode } from "react"; import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>; let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;
waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m); waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m);
@ -74,9 +74,9 @@ export interface ChatBarProps {
}; };
} }
export type ChatBarButton = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null; export type ChatBarButtonFactory = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
const buttonFactories = new Map<string, ChatBarButton>(); const buttonFactories = new Map<string, ChatBarButtonFactory>();
const logger = new Logger("ChatButtons"); const logger = new Logger("ChatButtons");
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) { export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
@ -91,7 +91,7 @@ export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
} }
} }
export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button); export const addChatBarButton = (id: string, button: ChatBarButtonFactory) => buttonFactories.set(id, button);
export const removeChatBarButton = (id: string) => buttonFactories.delete(id); export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
export interface ChatBarButtonProps { export interface ChatBarButtonProps {
@ -99,7 +99,8 @@ export interface ChatBarButtonProps {
tooltip: string; tooltip: string;
onClick: MouseEventHandler<HTMLButtonElement>; onClick: MouseEventHandler<HTMLButtonElement>;
onContextMenu?: MouseEventHandler<HTMLButtonElement>; onContextMenu?: MouseEventHandler<HTMLButtonElement>;
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu">; onAuxClick?: MouseEventHandler<HTMLButtonElement>;
buttonProps?: Omit<HTMLProps<HTMLButtonElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
} }
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
return ( return (
@ -109,12 +110,13 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
<Button <Button
aria-label={props.tooltip} aria-label={props.tooltip}
size="" size=""
look={ButtonLooks.BLANK} look={Button.Looks.BLANK}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`} innerClassName={`${ButtonWrapperClasses.button} ${ChannelTextAreaClasses?.button}`}
onClick={props.onClick} onClick={props.onClick}
onContextMenu={props.onContextMenu} onContextMenu={props.onContextMenu}
onAuxClick={props.onAuxClick}
{...props.buttonProps} {...props.buttonProps}
> >
<div className={ButtonWrapperClasses.buttonWrapper}> <div className={ButtonWrapperClasses.buttonWrapper}>

View file

@ -54,5 +54,5 @@ export function sendBotMessage(channelId: string, message: PartialDeep<Message>)
export function findOption<T>(args: Argument[], name: string): T & {} | undefined; export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {}; export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
export function findOption(args: Argument[], name: string, fallbackValue?: any) { export function findOption(args: Argument[], name: string, fallbackValue?: any) {
return (args.find(a => a.name === name)?.value || fallbackValue) as any; return (args.find(a => a.name === name)?.value ?? fallbackValue) as any;
} }

View file

@ -110,6 +110,7 @@ function registerSubCommands(cmd: Command, plugin: string) {
const subCmd = { const subCmd = {
...cmd, ...cmd,
...o, ...o,
options: o.options !== undefined ? o.options : undefined,
type: ApplicationCommandType.CHAT_INPUT, type: ApplicationCommandType.CHAT_INPUT,
name: `${cmd.name} ${o.name}`, name: `${cmd.name} ${o.name}`,
id: `${o.name}-${cmd.id}`, id: `${o.name}-${cmd.id}`,

View file

@ -24,13 +24,13 @@ import type { ReactElement } from "react";
* @param children The rendered context menu elements * @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
*/ */
export type NavContextMenuPatchCallback = (children: Array<ReactElement | null>, ...args: Array<any>) => void; export type NavContextMenuPatchCallback = (children: Array<ReactElement<any> | null>, ...args: Array<any>) => void;
/** /**
* @param navId The navId of the context menu being patched * @param navId The navId of the context menu being patched
* @param children The rendered context menu elements * @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
*/ */
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement | null>, ...args: Array<any>) => void; export type GlobalContextMenuPatchCallback = (navId: string, children: Array<ReactElement<any> | null>, ...args: Array<any>) => void;
const ContextMenuLogger = new Logger("ContextMenu"); const ContextMenuLogger = new Logger("ContextMenu");
@ -70,7 +70,7 @@ export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback)
* @returns Whether the patch was successfully removed from the context menu(s) * @returns Whether the patch was successfully removed from the context menu(s)
*/ */
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> { export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
const navIds = Array.isArray(navId) ? navId : [navId as string]; const navIds: string[] = Array.isArray(navId) ? navId : [navId];
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false); const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
@ -92,7 +92,7 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
* @param children The context menu children * @param children The context menu children
* @param matchSubstring Whether to check if the id is a substring of the child id * @param matchSubstring Whether to check if the id is a substring of the child id
*/ */
export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement | null | undefined>, matchSubstring = false): Array<ReactElement | null | undefined> | null { export function findGroupChildrenByChildId(id: string | string[], children: Array<ReactElement<any> | null | undefined>, matchSubstring = false): Array<ReactElement<any> | null | undefined> | null {
for (const child of children) { for (const child of children) {
if (child == null) continue; if (child == null) continue;
@ -122,9 +122,9 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
} }
interface ContextMenuProps { interface ContextMenuProps {
contextMenuApiArguments?: Array<any>; contextMenuAPIArguments?: Array<any>;
navId: string; navId: string;
children: Array<ReactElement | null>; children: Array<ReactElement<any> | null>;
"aria-label": string; "aria-label": string;
onSelect: (() => void) | undefined; onSelect: (() => void) | undefined;
onClose: (callback: (...args: Array<any>) => any) => void; onClose: (callback: (...args: Array<any>) => any) => void;
@ -136,7 +136,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
children: cloneMenuChildren(props.children), children: cloneMenuChildren(props.children),
}; };
props.contextMenuApiArguments ??= []; props.contextMenuAPIArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId); const contextMenuPatches = navPatches.get(props.navId);
if (!Array.isArray(props.children)) props.children = [props.children]; if (!Array.isArray(props.children)) props.children = [props.children];
@ -144,7 +144,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
if (contextMenuPatches) { if (contextMenuPatches) {
for (const patch of contextMenuPatches) { for (const patch of contextMenuPatches) {
try { try {
patch(props.children, ...props.contextMenuApiArguments); patch(props.children, ...props.contextMenuAPIArguments);
} catch (err) { } catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
} }
@ -153,7 +153,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) { for (const patch of globalPatches) {
try { try {
patch(props.navId, props.children, ...props.contextMenuApiArguments); patch(props.navId, props.children, ...props.contextMenuAPIArguments);
} catch (err) { } catch (err) {
ContextMenuLogger.error("Global patch errored,", err); ContextMenuLogger.error("Global patch errored,", err);
} }
@ -162,7 +162,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
return props; return props;
} }
function cloneMenuChildren(obj: ReactElement | Array<ReactElement | null> | null) { function cloneMenuChildren(obj: ReactElement<any> | Array<ReactElement<any> | null> | null) {
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.map(cloneMenuChildren); return obj.map(cloneMenuChildren);
} }

View file

@ -16,7 +16,9 @@
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { Channel, User } from "discord-types/general/index.js"; import { Channel, User } from "discord-types/general/index.js";
import { JSX } from "react";
interface DecoratorProps { interface DecoratorProps {
activities: any[]; activities: any[];
@ -38,27 +40,39 @@ interface DecoratorProps {
user: User; user: User;
[key: string]: any; [key: string]: any;
} }
export type Decorator = (props: DecoratorProps) => JSX.Element | null; export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;
type OnlyIn = "guilds" | "dms"; type OnlyIn = "guilds" | "dms";
export const decorators = new Map<string, { decorator: Decorator, onlyIn?: OnlyIn; }>(); export const decoratorsFactories = new Map<string, { render: MemberListDecoratorFactory, onlyIn?: OnlyIn; }>();
export function addDecorator(identifier: string, decorator: Decorator, onlyIn?: OnlyIn) { export function addMemberListDecorator(identifier: string, render: MemberListDecoratorFactory, onlyIn?: OnlyIn) {
decorators.set(identifier, { decorator, onlyIn }); decoratorsFactories.set(identifier, { render, onlyIn });
} }
export function removeDecorator(identifier: string) { export function removeMemberListDecorator(identifier: string) {
decorators.delete(identifier); decoratorsFactories.delete(identifier);
} }
export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] { export function __getDecorators(props: DecoratorProps): JSX.Element {
const isInGuild = !!(props.guildId); const isInGuild = !!(props.guildId);
return Array.from(decorators.values(), decoratorObj => {
const { decorator, onlyIn } = decoratorObj; const decorators = Array.from(
// this can most likely be done cleaner decoratorsFactories.entries(),
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) { ([key, { render: Decorator, onlyIn }]) => {
return decorator(props); if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild))
}
return null; return null;
});
return (
<ErrorBoundary noop key={key} message={`Failed to render ${key} Member List Decorator`}>
<Decorator {...props} />
</ErrorBoundary>
);
}
);
return (
<div className="vc-member-list-decorators-wrapper">
{decorators}
</div>
);
} }

View file

@ -16,26 +16,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element | null | Array<JSX.Element | null>; import ErrorBoundary from "@components/ErrorBoundary";
export type Accessory = { import { JSX, ReactNode } from "react";
callback: AccessoryCallback;
export type MessageAccessoryFactory = (props: Record<string, any>) => ReactNode;
export type MessageAccessory = {
render: MessageAccessoryFactory;
position?: number; position?: number;
}; };
export const accessories = new Map<String, Accessory>(); export const accessories = new Map<string, MessageAccessory>();
export function addAccessory( export function addMessageAccessory(
identifier: string, identifier: string,
callback: AccessoryCallback, render: MessageAccessoryFactory,
position?: number position?: number
) { ) {
accessories.set(identifier, { accessories.set(identifier, {
callback, render,
position, position,
}); });
} }
export function removeAccessory(identifier: string) { export function removeMessageAccessory(identifier: string) {
accessories.delete(identifier); accessories.delete(identifier);
} }
@ -43,15 +46,12 @@ export function _modifyAccessories(
elements: JSX.Element[], elements: JSX.Element[],
props: Record<string, any> props: Record<string, any>
) { ) {
for (const accessory of accessories.values()) { for (const [key, accessory] of accessories.entries()) {
let accessories = accessory.callback(props); const res = (
if (accessories == null) <ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}>
continue; <accessory.render {...props} />
</ErrorBoundary>
if (!Array.isArray(accessories)) );
accessories = [accessories];
else if (accessories.length === 0)
continue;
elements.splice( elements.splice(
accessory.position != null accessory.position != null
@ -60,7 +60,7 @@ export function _modifyAccessories(
: accessory.position : accessory.position
: elements.length, : elements.length,
0, 0,
...accessories.filter(e => e != null) as JSX.Element[] res
); );
} }

View file

@ -16,9 +16,11 @@
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { Channel, Message } from "discord-types/general/index.js"; import { Channel, Message } from "discord-types/general/index.js";
import { JSX } from "react";
interface DecorationProps { export interface MessageDecorationProps {
author: { author: {
/** /**
* Will be username if the user has no nickname * Will be username if the user has no nickname
@ -44,20 +46,31 @@ interface DecorationProps {
message: Message; message: Message;
[key: string]: any; [key: string]: any;
} }
export type Decoration = (props: DecorationProps) => JSX.Element | null; export type MessageDecorationFactory = (props: MessageDecorationProps) => JSX.Element | null;
export const decorations = new Map<string, Decoration>(); export const decorationsFactories = new Map<string, MessageDecorationFactory>();
export function addDecoration(identifier: string, decoration: Decoration) { export function addMessageDecoration(identifier: string, decoration: MessageDecorationFactory) {
decorations.set(identifier, decoration); decorationsFactories.set(identifier, decoration);
} }
export function removeDecoration(identifier: string) { export function removeMessageDecoration(identifier: string) {
decorations.delete(identifier); decorationsFactories.delete(identifier);
} }
export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] { export function __addDecorationsToMessage(props: MessageDecorationProps): JSX.Element {
return [...decorations.values()].map(decoration => { const decorations = Array.from(
return decoration(props); decorationsFactories.entries(),
}); ([key, Decoration]) => (
<ErrorBoundary noop message={`Failed to render ${key} Message Decoration`} key={key}>
<Decoration {...props} />
</ErrorBoundary>
)
);
return (
<div className="vc-message-decorations-wrapper">
{decorations}
</div>
);
} }

View file

@ -73,11 +73,11 @@ export interface MessageExtra {
openWarningPopout: (props: any) => any; openWarningPopout: (props: any) => any;
} }
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>; export type MessageSendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>; export type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
const sendListeners = new Set<SendListener>(); const sendListeners = new Set<MessageSendListener>();
const editListeners = new Set<EditListener>(); const editListeners = new Set<MessageEditListener>();
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) { export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
extra.replyOptions = replyOptions; extra.replyOptions = replyOptions;
@ -111,29 +111,29 @@ export async function _handlePreEdit(channelId: string, messageId: string, messa
/** /**
* Note: This event fires off before a message is sent, allowing you to edit the message. * Note: This event fires off before a message is sent, allowing you to edit the message.
*/ */
export function addPreSendListener(listener: SendListener) { export function addMessagePreSendListener(listener: MessageSendListener) {
sendListeners.add(listener); sendListeners.add(listener);
return listener; return listener;
} }
/** /**
* Note: This event fires off before a message's edit is applied, allowing you to further edit the message. * Note: This event fires off before a message's edit is applied, allowing you to further edit the message.
*/ */
export function addPreEditListener(listener: EditListener) { export function addMessagePreEditListener(listener: MessageEditListener) {
editListeners.add(listener); editListeners.add(listener);
return listener; return listener;
} }
export function removePreSendListener(listener: SendListener) { export function removeMessagePreSendListener(listener: MessageSendListener) {
return sendListeners.delete(listener); return sendListeners.delete(listener);
} }
export function removePreEditListener(listener: EditListener) { export function removeMessagePreEditListener(listener: MessageEditListener) {
return editListeners.delete(listener); return editListeners.delete(listener);
} }
// Message clicks // Message clicks
type ClickListener = (message: Message, channel: Channel, event: MouseEvent) => void; export type MessageClickListener = (message: Message, channel: Channel, event: MouseEvent) => void;
const listeners = new Set<ClickListener>(); const listeners = new Set<MessageClickListener>();
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) { export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
// message object may be outdated, so (try to) fetch latest one // message object may be outdated, so (try to) fetch latest one
@ -147,11 +147,11 @@ export function _handleClick(message: Message, channel: Channel, event: MouseEve
} }
} }
export function addClickListener(listener: ClickListener) { export function addMessageClickListener(listener: MessageClickListener) {
listeners.add(listener); listeners.add(listener);
return listener; return listener;
} }
export function removeClickListener(listener: ClickListener) { export function removeMessageClickListener(listener: MessageClickListener) {
return listeners.delete(listener); return listeners.delete(listener);
} }

View file

@ -23,7 +23,7 @@ import type { ComponentType, MouseEventHandler } from "react";
const logger = new Logger("MessagePopover"); const logger = new Logger("MessagePopover");
export interface ButtonItem { export interface MessagePopoverButtonItem {
key?: string, key?: string,
label: string, label: string,
icon: ComponentType<any>, icon: ComponentType<any>,
@ -33,23 +33,23 @@ export interface ButtonItem {
onContextMenu?: MouseEventHandler<HTMLButtonElement>; onContextMenu?: MouseEventHandler<HTMLButtonElement>;
} }
export type getButtonItem = (message: Message) => ButtonItem | null; export type MessagePopoverButtonFactory = (message: Message) => MessagePopoverButtonItem | null;
export const buttons = new Map<string, getButtonItem>(); export const buttons = new Map<string, MessagePopoverButtonFactory>();
export function addButton( export function addMessagePopoverButton(
identifier: string, identifier: string,
item: getButtonItem, item: MessagePopoverButtonFactory,
) { ) {
buttons.set(identifier, item); buttons.set(identifier, item);
} }
export function removeButton(identifier: string) { export function removeMessagePopoverButton(identifier: string) {
buttons.delete(identifier); buttons.delete(identifier);
} }
export function _buildPopoverElements( export function _buildPopoverElements(
Component: React.ComponentType<ButtonItem>, Component: React.ComponentType<MessagePopoverButtonItem>,
message: Message message: Message
) { ) {
const items: React.ReactNode[] = []; const items: React.ReactNode[] = [];

View file

@ -16,40 +16,36 @@
* 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 { Logger } from "@utils/Logger"; import ErrorBoundary from "@components/ErrorBoundary";
import { ComponentType } from "react";
const logger = new Logger("ServerListAPI");
export const enum ServerListRenderPosition { export const enum ServerListRenderPosition {
Above, Above,
In, In,
} }
const renderFunctionsAbove = new Set<Function>(); const componentsAbove = new Set<ComponentType>();
const renderFunctionsIn = new Set<Function>(); const componentsBelow = new Set<ComponentType>();
function getRenderFunctions(position: ServerListRenderPosition) { function getRenderFunctions(position: ServerListRenderPosition) {
return position === ServerListRenderPosition.Above ? renderFunctionsAbove : renderFunctionsIn; return position === ServerListRenderPosition.Above ? componentsAbove : componentsBelow;
} }
export function addServerListElement(position: ServerListRenderPosition, renderFunction: Function) { export function addServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
getRenderFunctions(position).add(renderFunction); getRenderFunctions(position).add(renderFunction);
} }
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) { export function removeServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
getRenderFunctions(position).delete(renderFunction); getRenderFunctions(position).delete(renderFunction);
} }
export const renderAll = (position: ServerListRenderPosition) => { export const renderAll = (position: ServerListRenderPosition) => {
const ret: Array<JSX.Element> = []; return Array.from(
getRenderFunctions(position),
for (const renderFunction of getRenderFunctions(position)) { (Component, i) => (
try { <ErrorBoundary noop key={i}>
ret.unshift(renderFunction()); <Component />
} catch (e) { </ErrorBoundary>
logger.error("Failed to render server list element:", e); )
} );
}
return ret;
}; };

View file

@ -23,7 +23,7 @@ import { Logger } from "@utils/Logger";
import { mergeDefaults } from "@utils/mergeDefaults"; import { mergeDefaults } from "@utils/mergeDefaults";
import { putCloudSettings } from "@utils/settingsSync"; import { putCloudSettings } from "@utils/settingsSync";
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types"; import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common"; import { React, useEffect } from "@webpack/common";
import plugins from "~plugins"; import plugins from "~plugins";
@ -192,7 +192,7 @@ export const Settings = SettingsStore.store;
export function useSettings(paths?: UseSettings<Settings>[]) { export function useSettings(paths?: UseSettings<Settings>[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {}); const [, forceUpdate] = React.useReducer(() => ({}), {});
React.useEffect(() => { useEffect(() => {
if (paths) { if (paths) {
paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate)); paths.forEach(p => SettingsStore.addChangeListener(p, forceUpdate));
return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate)); return () => paths.forEach(p => SettingsStore.removeChangeListener(p, forceUpdate));
@ -200,7 +200,7 @@ export function useSettings(paths?: UseSettings<Settings>[]) {
SettingsStore.addGlobalChangeListener(forceUpdate); SettingsStore.addGlobalChangeListener(forceUpdate);
return () => SettingsStore.removeGlobalChangeListener(forceUpdate); return () => SettingsStore.removeGlobalChangeListener(forceUpdate);
} }
}, []); }, [paths]);
return SettingsStore.store; return SettingsStore.store;
} }
@ -220,6 +220,17 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
} }
} }
export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) {
const settings = SettingsStore.plain.plugins[pluginName];
if (!settings) return;
if (!Object.hasOwn(settings, oldSetting) || Object.hasOwn(settings, newSetting)) return;
settings[newSetting] = settings[oldSetting];
delete settings[oldSetting];
SettingsStore.markAsChanged();
}
export function definePluginSettings< export function definePluginSettings<
Def extends SettingsDefinition, Def extends SettingsDefinition,
Checks extends SettingsChecks<Def>, Checks extends SettingsChecks<Def>,

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export function Badge({ text, color }): JSX.Element { export function Badge({ text, color }) {
return ( return (
<div className="vc-plugins-badge" style={{ <div className="vc-plugins-badge" style={{
backgroundColor: color, backgroundColor: color,

View file

@ -17,16 +17,22 @@
*/ */
import { Button } from "@webpack/common"; import { Button } from "@webpack/common";
import { ButtonProps } from "@webpack/types";
import { Heart } from "./Heart"; import { Heart } from "./Heart";
export default function DonateButton(props: any) { export default function DonateButton({
look = Button.Looks.LINK,
color = Button.Colors.TRANSPARENT,
...props
}: Partial<ButtonProps>) {
return ( return (
<Button <Button
{...props} {...props}
look={Button.Looks.LINK} look={look}
color={Button.Colors.TRANSPARENT} color={color}
onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")} onClick={() => VencordNative.native.openExternal("https://github.com/sponsors/Vendicated")}
innerClassName="vc-donate-button"
> >
<Heart /> <Heart />
Donate Donate

View file

@ -27,7 +27,7 @@ interface Props<T = any> {
/** Render nothing if an error occurs */ /** Render nothing if an error occurs */
noop?: boolean; noop?: boolean;
/** Fallback component to render if an error occurs */ /** Fallback component to render if an error occurs */
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>; fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; wrappedProps: T; }>>;
/** called when an error occurs. The props property is only available if using .wrap */ /** called when an error occurs. The props property is only available if using .wrap */
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void; onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
/** Custom error message */ /** Custom error message */
@ -70,8 +70,7 @@ const ErrorBoundary = LazyComponent(() => {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps }); this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
logger.error("A component threw an Error\n", error); logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
logger.error("Component Stack", errorInfo.componentStack);
} }
render() { render() {
@ -80,10 +79,14 @@ const ErrorBoundary = LazyComponent(() => {
if (this.props.noop) return null; if (this.props.noop) return null;
if (this.props.fallback) if (this.props.fallback)
return <this.props.fallback return (
children={this.props.children} <this.props.fallback
wrappedProps={this.props.wrappedProps}
{...this.state} {...this.state}
/>; >
{this.props.children}
</this.props.fallback>
);
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console."; const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";

View file

@ -1,11 +0,0 @@
.vc-expandableheader-center-flex {
display: flex;
place-items: center;
}
.vc-expandableheader-btn {
all: unset;
cursor: pointer;
width: 24px;
height: 24px;
}

View file

@ -1,121 +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 "./ExpandableHeader.css";
import { classNameFactory } from "@api/Styles";
import { Text, Tooltip, useState } from "@webpack/common";
const cl = classNameFactory("vc-expandableheader-");
export interface ExpandableHeaderProps {
onMoreClick?: () => void;
moreTooltipText?: string;
onDropDownClick?: (state: boolean) => void;
defaultState?: boolean;
headerText: string;
children: React.ReactNode;
buttons?: React.ReactNode[];
forceOpen?: boolean;
}
export function ExpandableHeader({
children,
onMoreClick,
buttons,
moreTooltipText,
onDropDownClick,
headerText,
defaultState = false,
forceOpen = false,
}: ExpandableHeaderProps) {
const [showContent, setShowContent] = useState(defaultState || forceOpen);
return (
<>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "8px"
}}>
<Text
tag="h2"
variant="eyebrow"
style={{
color: "var(--header-primary)",
display: "inline"
}}
>
{headerText}
</Text>
<div className={cl("center-flex")}>
{
buttons ?? null
}
{
onMoreClick && // only show more button if callback is provided
<Tooltip text={moreTooltipText}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("btn")}
onClick={onMoreClick}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
</svg>
</button>
)}
</Tooltip>
}
<Tooltip text={showContent ? "Hide " + headerText : "Show " + headerText}>
{tooltipProps => (
<button
{...tooltipProps}
className={cl("btn")}
onClick={() => {
setShowContent(v => !v);
onDropDownClick?.(showContent);
}}
disabled={forceOpen}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
transform={showContent ? "scale(1 -1)" : "scale(1 1)"}
>
<path fill="var(--text-normal)" d="M16.59 8.59003L12 13.17L7.41 8.59003L6 10L12 16L18 10L16.59 8.59003Z" />
</svg>
</button>
)}
</Tooltip>
</div>
</div>
{showContent && children}
</>
);
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { CSSProperties } from "react"; import { CSSProperties, JSX } from "react";
interface Props { interface Props {
columns: number; columns: number;

View file

@ -16,18 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export function Heart() { import { classes } from "@utils/misc";
import { SVGProps } from "react";
export function Heart(props: SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg
aria-hidden="true" aria-hidden="true"
height="16"
viewBox="0 0 16 16" viewBox="0 0 16 16"
height="16"
width="16" width="16"
style={{ marginRight: "0.5em", transform: "translateY(2px)" }} {...props}
className={classes("vc-heart-icon", props.className)}
> >
<path <path
fill="#db61a2" fill="#db61a2"
fill-rule="evenodd" fillRule="evenodd"
d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"
/> />
</svg> </svg>

View file

@ -18,9 +18,9 @@
import "./iconStyles.css"; import "./iconStyles.css";
import { getIntlMessage, getTheme, Theme } from "@utils/discord"; import { getIntlMessage } from "@utils/discord";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import type { PropsWithChildren } from "react"; import type { JSX, PropsWithChildren } from "react";
interface BaseIconProps extends IconProps { interface BaseIconProps extends IconProps {
viewBox: string; viewBox: string;
@ -55,7 +55,7 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
className={classes(className, "vc-link-icon")} className={classes(className, "vc-link-icon")}
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<g fill="none" fill-rule="evenodd"> <g fill="none" fillRule="evenodd">
<path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z" /> <path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0 5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24 2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0 4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0 5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24 2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24.973.973 0 0 1 0-1.42z" />
<rect width={width} height={height} /> <rect width={width} height={height} />
</g> </g>
@ -122,8 +122,8 @@ export function InfoIcon(props: IconProps) {
> >
<path <path
fill="currentColor" fill="currentColor"
transform="translate(2 2)" fillRule="evenodd"
d="M9,7 L11,7 L11,5 L9,5 L9,7 Z M10,18 C5.59,18 2,14.41 2,10 C2,5.59 5.59,2 10,2 C14.41,2 18,5.59 18,10 C18,14.41 14.41,18 10,18 L10,18 Z M10,4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16,4.4771525 0,10 C-1.33226763e-15,12.6521649 1.0535684,15.195704 2.92893219,17.0710678 C4.80429597,18.9464316 7.3478351,20 10,20 C12.6521649,20 15.195704,18.9464316 17.0710678,17.0710678 C18.9464316,15.195704 20,12.6521649 20,10 C20,7.3478351 18.9464316,4.80429597 17.0710678,2.92893219 C15.195704,1.0535684 12.6521649,2.22044605e-16 10,0 L10,4.4408921e-16 Z M9,15 L11,15 L11,9 L9,9 L9,15 L9,15 Z" d="M23 12a11 11 0 1 1-22 0 11 11 0 0 1 22 0Zm-9.5-4.75a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm-.77 3.96a1 1 0 1 0-1.96-.42l-1.04 4.86a2.77 2.77 0 0 0 4.31 2.83l.24-.17a1 1 0 1 0-1.16-1.62l-.24.17a.77.77 0 0 1-1.2-.79l1.05-4.86Z" clipRule="evenodd"
/> />
</Icon> </Icon>
); );
@ -211,9 +211,10 @@ export function CogWheel(props: IconProps) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path <path
clipRule="evenodd"
fill="currentColor" fill="currentColor"
d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z" fillRule="evenodd"
d="M10.56 1.1c-.46.05-.7.53-.64.98.18 1.16-.19 2.2-.98 2.53-.8.33-1.79-.15-2.49-1.1-.27-.36-.78-.52-1.14-.24-.77.59-1.45 1.27-2.04 2.04-.28.36-.12.87.24 1.14.96.7 1.43 1.7 1.1 2.49-.33.8-1.37 1.16-2.53.98-.45-.07-.93.18-.99.64a11.1 11.1 0 0 0 0 2.88c.06.46.54.7.99.64 1.16-.18 2.2.19 2.53.98.33.8-.14 1.79-1.1 2.49-.36.27-.52.78-.24 1.14.59.77 1.27 1.45 2.04 2.04.36.28.87.12 1.14-.24.7-.95 1.7-1.43 2.49-1.1.8.33 1.16 1.37.98 2.53-.07.45.18.93.64.99a11.1 11.1 0 0 0 2.88 0c.46-.06.7-.54.64-.99-.18-1.16.19-2.2.98-2.53.8-.33 1.79.14 2.49 1.1.27.36.78.52 1.14.24.77-.59 1.45-1.27 2.04-2.04.28-.36.12-.87-.24-1.14-.96-.7-1.43-1.7-1.1-2.49.33-.8 1.37-1.16 2.53-.98.45.07.93-.18.99-.64a11.1 11.1 0 0 0 0-2.88c-.06-.46-.54-.7-.99-.64-1.16.18-2.2-.19-2.53-.98-.33-.8.14-1.79 1.1-2.49.36-.27.52-.78.24-1.14a11.07 11.07 0 0 0-2.04-2.04c-.36-.28-.87-.12-1.14.24-.7.96-1.7 1.43-2.49 1.1-.8-.33-1.16-1.37-.98-2.53.07-.45-.18-.93-.64-.99a11.1 11.1 0 0 0-2.88 0ZM16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"
clipRule="evenodd"
/> />
</Icon> </Icon>
); );
@ -261,7 +262,7 @@ export function PlusIcon(props: IconProps) {
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<polygon <polygon
fill-rule="nonzero" fillRule="nonzero"
fill="currentColor" fill="currentColor"
points="15 10 10 10 10 15 8 15 8 10 3 10 3 8 8 8 8 3 10 3 10 8 15 8" points="15 10 10 10 10 15 8 15 8 10 3 10 3 8 8 8 8 3 10 3 10 8 15 8"
/> />
@ -406,23 +407,30 @@ export function PencilIcon(props: IconProps) {
); );
} }
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg"; export function GithubIcon(props: IconProps) {
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg"; return (
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg"; <Icon
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg"; {...props}
viewBox="-3 -3 30 30"
export function GithubIcon(props: ImageProps) { >
const src = getTheme() === Theme.Light <path
? GithubIconLight fill={props.fill || "currentColor"}
: GithubIconDark; d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.745.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.998.108-.775.42-1.305.763-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.398 3-.403 1.02.005 2.043.137 3 .403 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.873.118 3.176.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.625-5.475 5.92.43.37.823 1.102.823 2.222v3.293c0 .32.218.694.825.577C20.565 21.797 24 17.298 24 12c0-6.63-5.37-12-12-12z"
/>
return <img {...props} src={src} />; </Icon>
);
} }
export function WebsiteIcon(props: ImageProps) { export function WebsiteIcon(props: IconProps) {
const src = getTheme() === Theme.Light return (
? WebsiteIconLight <Icon
: WebsiteIconDark; {...props}
viewBox="0 0 24 24"
return <img {...props} src={src} />; >
<path
fill={props.fill || "currentColor"}
d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zM4 12c0-.899.156-1.762.431-2.569L6 11l2 2v2l2 2 1 1v1.931C7.061 19.436 4 16.072 4 12zm14.33 4.873C17.677 16.347 16.687 16 16 16v-1a2 2 0 0 0-2-2h-4v-3a2 2 0 0 0 2-2V7h1a2 2 0 0 0 2-2v-.411C17.928 5.778 20 8.65 20 12a7.947 7.947 0 0 1-1.67 4.873z"
/>
</Icon>
);
} }

View file

@ -44,7 +44,7 @@ function ContributorModal({ user }: { user: User; }) {
useEffect(() => { useEffect(() => {
if (!profile && !user.bot && user.id) if (!profile && !user.bot && user.id)
fetchUserProfile(user.id); fetchUserProfile(user.id);
}, [user.id]); }, [user.id, user.bot, profile]);
const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name; const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name;
const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name; const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name;

View file

@ -6,16 +6,19 @@
import "./LinkIconButton.css"; import "./LinkIconButton.css";
import { getTheme, Theme } from "@utils/discord";
import { MaskedLink, Tooltip } from "@webpack/common"; import { MaskedLink, Tooltip } from "@webpack/common";
import { GithubIcon, WebsiteIcon } from ".."; import { GithubIcon, WebsiteIcon } from "..";
export function GithubLinkIcon() { export function GithubLinkIcon() {
return <GithubIcon aria-hidden className={"vc-settings-modal-link-icon"} />; const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
return <GithubIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
} }
export function WebsiteLinkIcon() { export function WebsiteLinkIcon() {
return <WebsiteIcon aria-hidden className={"vc-settings-modal-link-icon"} />; const theme = getTheme() === Theme.Light ? "#000000" : "#FFFFFF";
return <WebsiteIcon aria-hidden fill={theme} className={"vc-settings-modal-link-icon"} />;
} }
interface Props { interface Props {

View file

@ -37,6 +37,7 @@ import { Constructor } from "type-fest";
import { PluginMeta } from "~plugins"; import { PluginMeta } from "~plugins";
import { import {
ISettingCustomElementProps,
ISettingElementProps, ISettingElementProps,
SettingBooleanComponent, SettingBooleanComponent,
SettingCustomComponent, SettingCustomComponent,
@ -74,14 +75,15 @@ function makeDummyUser(user: { username: string; id?: string; avatar?: string; }
return newUser; return newUser;
} }
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = { const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any> | ISettingCustomElementProps<any>>> = {
[OptionType.STRING]: SettingTextComponent, [OptionType.STRING]: SettingTextComponent,
[OptionType.NUMBER]: SettingNumericComponent, [OptionType.NUMBER]: SettingNumericComponent,
[OptionType.BIGINT]: SettingNumericComponent, [OptionType.BIGINT]: SettingNumericComponent,
[OptionType.BOOLEAN]: SettingBooleanComponent, [OptionType.BOOLEAN]: SettingBooleanComponent,
[OptionType.SELECT]: SettingSelectComponent, [OptionType.SELECT]: SettingSelectComponent,
[OptionType.SLIDER]: SettingSliderComponent, [OptionType.SLIDER]: SettingSliderComponent,
[OptionType.COMPONENT]: SettingCustomComponent [OptionType.COMPONENT]: SettingCustomComponent,
[OptionType.CUSTOM]: () => null,
}; };
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) { export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
@ -109,7 +111,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
setAuthors(a => [...a, author]); setAuthors(a => [...a, author]);
} }
})(); })();
}, []); }, [plugin.authors]);
async function saveAndClose() { async function saveAndClose() {
if (!plugin.options) { if (!plugin.options) {
@ -129,7 +131,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
for (const [key, value] of Object.entries(tempSettings)) { for (const [key, value] of Object.entries(tempSettings)) {
const option = plugin.options[key]; const option = plugin.options[key];
pluginSettings[key] = value; pluginSettings[key] = value;
option?.onChange?.(value);
if (option.type === OptionType.CUSTOM) continue;
if (option?.restartNeeded) restartNeeded = true; if (option?.restartNeeded) restartNeeded = true;
} }
if (restartNeeded) onRestartNeeded(); if (restartNeeded) onRestartNeeded();
@ -141,7 +144,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>; return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
} else { } else {
const options = Object.entries(plugin.options).map(([key, setting]) => { const options = Object.entries(plugin.options).map(([key, setting]) => {
if (setting.hidden) return null; if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
function onChange(newValue: any) { function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue })); setTempSettings(s => ({ ...s, [key]: newValue }));

View file

@ -18,8 +18,8 @@
import { PluginOptionComponent } from "@utils/types"; import { PluginOptionComponent } from "@utils/types";
import { ISettingElementProps } from "."; import { ISettingCustomElementProps } from ".";
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) { export function SettingCustomComponent({ option, onChange, onError }: ISettingCustomElementProps<PluginOptionComponent>) {
return option.component({ setValue: onChange, setError: onError, option }); return option.component({ setValue: onChange, setError: onError, option });
} }

View file

@ -18,7 +18,7 @@
import { DefinedSettings, PluginOptionBase } from "@utils/types"; import { DefinedSettings, PluginOptionBase } from "@utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> { interface ISettingElementPropsBase<T> {
option: T; option: T;
onChange(newValue: any): void; onChange(newValue: any): void;
pluginSettings: { pluginSettings: {
@ -30,6 +30,9 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
definedSettings?: DefinedSettings; definedSettings?: DefinedSettings;
} }
export type ISettingElementProps<T extends PluginOptionBase> = ISettingElementPropsBase<T>;
export type ISettingCustomElementProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = ISettingElementPropsBase<T>;
export * from "../../Badge"; export * from "../../Badge";
export * from "./SettingBooleanComponent"; export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent"; export * from "./SettingCustomComponent";

View file

@ -35,6 +35,7 @@ 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, useMemo } from "@webpack/common"; import { Alerts, Button, Card, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common";
import { JSX } from "react";
import Plugins, { ExcludedPlugins } from "~plugins"; import Plugins, { ExcludedPlugins } from "~plugins";
@ -68,7 +69,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) {
<Forms.FormText className={cl("dep-text")}> <Forms.FormText className={cl("dep-text")}>
Restart now to apply new plugins and their settings Restart now to apply new plugins and their settings
</Forms.FormText> </Forms.FormText>
<Button onClick={() => location.reload()}> <Button onClick={() => location.reload()} className={cl("restart-button")}>
Restart Restart
</Button> </Button>
</> </>
@ -157,8 +158,8 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
className={classes(ButtonClasses.button, cl("info-button"))} className={classes(ButtonClasses.button, cl("info-button"))}
> >
{plugin.options && !isObjectEmpty(plugin.options) {plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel /> ? <CogWheel className={cl("info-icon")} />
: <InfoIcon />} : <InfoIcon className={cl("info-icon")} />}
</button> </button>
} }
/> />
@ -387,7 +388,7 @@ function makeDependencyList(deps: string[]) {
return ( return (
<React.Fragment> <React.Fragment>
<Forms.FormText>This plugin is required by:</Forms.FormText> <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 key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)}
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -63,10 +63,7 @@
height: 8em; height: 8em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} gap: 0.25em;
.vc-plugins-info-card div {
line-height: 32px;
} }
.vc-plugins-restart-card { .vc-plugins-restart-card {
@ -76,11 +73,11 @@
color: var(--info-warning-text); color: var(--info-warning-text);
} }
.vc-plugins-restart-card button { .vc-plugins-restart-button {
margin-top: 0.5em; margin-top: 0.5em;
background: var(--info-warning-foreground) !important; background: var(--info-warning-foreground) !important;
} }
.vc-plugins-info-button svg:not(:hover, :focus) { .vc-plugins-info-icon:not(:hover, :focus) {
color: var(--text-muted); color: var(--text-muted);
} }

View file

@ -111,9 +111,9 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
} }
function renderDiff() { function renderDiff() {
return diff?.map(p => { return diff?.map((p, idx) => {
const color = p.added ? "lime" : p.removed ? "red" : "grey"; const color = p.added ? "lime" : p.removed ? "red" : "grey";
return <div style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>; return <div key={idx} style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>;
}); });
} }

View file

@ -0,0 +1,77 @@
/*
* 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 "./specialCard.css";
import { classNameFactory } from "@api/Styles";
import { Card, Clickable, Forms, React } from "@webpack/common";
import type { PropsWithChildren } from "react";
const cl = classNameFactory("vc-special-");
interface StyledCardProps {
title: string;
subtitle?: string;
description: string;
cardImage?: string;
backgroundImage?: string;
backgroundColor?: string;
buttonTitle?: string;
buttonOnClick?: () => void;
}
export function SpecialCard({ title, subtitle, description, cardImage, backgroundImage, backgroundColor, buttonTitle, buttonOnClick: onClick, children }: PropsWithChildren<StyledCardProps>) {
const cardStyle: React.CSSProperties = {
backgroundColor: backgroundColor || "#9c85ef",
backgroundImage: `url(${backgroundImage || ""})`,
};
return (
<Card className={cl("card", "card-special")} style={cardStyle}>
<div className={cl("card-flex")}>
<div className={cl("card-flex-main")}>
<Forms.FormTitle className={cl("title")} tag="h5">{title}</Forms.FormTitle>
<Forms.FormText className={cl("subtitle")}>{subtitle}</Forms.FormText>
<Forms.FormText className={cl("text")}>{description}</Forms.FormText>
{children}
</div>
{cardImage && (
<div className={cl("image-container")}>
<img
role="presentation"
src={cardImage}
alt=""
className={cl("image")}
/>
</div>
)}
</div>
{buttonTitle && (
<>
<Forms.FormDivider className={cl("seperator")} />
<Clickable onClick={onClick} className={cl("hyperlink")}>
<Forms.FormText className={cl("hyperlink-text")}>
{buttonTitle}
</Forms.FormText>
</Clickable>
</>
)}
</Card>
);
}

View file

@ -61,7 +61,7 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
title: "Oops!", title: "Oops!",
body: ( body: (
<ErrorCard> <ErrorCard>
{err.split("\n").map(line => <div>{Parser.parse(line)}</div>)} {err.split("\n").map((line, idx) => <div key={idx}>{Parser.parse(line)}</div>)}
</ErrorCard> </ErrorCard>
) )
}); });
@ -87,7 +87,7 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
return ( return (
<Card style={{ padding: "0 0.5em" }}> <Card style={{ padding: "0 0.5em" }}>
{updates.map(({ hash, author, message }) => ( {updates.map(({ hash, author, message }) => (
<div style={{ <div key={hash} style={{
marginTop: "0.5em", marginTop: "0.5em",
marginBottom: "0.5em" marginBottom: "0.5em"
}}> }}>

View file

@ -20,29 +20,38 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/Settings"; import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
import { openPluginModal } from "@components/PluginSettings/PluginModal"; import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { gitRemote } from "@shared/vencordUserAgent"; import { gitRemote } from "@shared/vencordUserAgent";
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity } from "@utils/misc"; import { identity, isPluginDev } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native"; import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import { Button, Card, Forms, React, Select, Switch } from "@webpack/common"; import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
import BadgeAPI from "../../plugins/_api/badges";
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from ".."; import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
import { openNotificationSettingsModal } from "./NotificationSettings"; import { openNotificationSettingsModal } from "./NotificationSettings";
import { QuickAction, QuickActionCard } from "./quickActions"; import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
import { SpecialCard } from "./SpecialCard";
const cl = classNameFactory("vc-settings-"); const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png"; const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png"; const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
const VENNIE_DONATOR_IMAGE = "https://cdn.discordapp.com/emojis/1238120638020063377.png";
const COZY_CONTRIB_IMAGE = "https://cdn.discordapp.com/emojis/1026533070955872337.png";
const DONOR_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070116305436712.png?size=2048";
const CONTRIB_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070166481895484.png?size=2048";
type KeysOfType<Object, Type> = { type KeysOfType<Object, Type> = {
[K in keyof Object]: Object[K] extends Type ? K : never; [K in keyof Object]: Object[K] extends Type ? K : never;
}[keyof Object]; }[keyof Object];
function VencordSettings() { function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, { const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
fallbackValue: "Loading..." fallbackValue: "Loading..."
@ -55,6 +64,8 @@ function VencordSettings() {
const isMac = navigator.platform.toLowerCase().startsWith("mac"); const isMac = navigator.platform.toLowerCase().startsWith("mac");
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac; const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
const user = UserStore.getCurrentUser();
const Switches: Array<false | { const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>; key: KeysOfType<typeof settings, boolean>;
title: string; title: string;
@ -99,7 +110,44 @@ function VencordSettings() {
return ( return (
<SettingsTab title="Vencord Settings"> <SettingsTab title="Vencord Settings">
<DonateCard image={donateImage} /> {isDonor(user?.id)
? (
<SpecialCard
title="Donations"
subtitle="Thank you for donating!"
description="All Vencord users can see your badge! You can change it at any time by messaging @vending.machine."
cardImage={VENNIE_DONATOR_IMAGE}
backgroundImage={DONOR_BACKGROUND_IMAGE}
backgroundColor="#ED87A9"
>
<DonateButtonComponent />
</SpecialCard>
)
: (
<SpecialCard
title="Support the Project"
description="Please consider supporting the development of Vencord by donating!"
cardImage={donateImage}
backgroundImage={DONOR_BACKGROUND_IMAGE}
backgroundColor="#c3a3ce"
>
<DonateButtonComponent />
</SpecialCard>
)
}
{isPluginDev(user?.id) && (
<SpecialCard
title="Contributions"
subtitle="Thank you for contributing!"
description="Since you've contributed to Vencord you now have a cool new badge!"
cardImage={COZY_CONTRIB_IMAGE}
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
backgroundColor="#EDCC87"
buttonTitle="See what you've contributed to"
buttonOnClick={() => openContributorModal(user)}
/>
)}
<Forms.FormSection title="Quick Actions"> <Forms.FormSection title="Quick Actions">
<QuickActionCard> <QuickActionCard>
<QuickAction <QuickAction
@ -239,31 +287,19 @@ function VencordSettings() {
); );
} }
interface DonateCardProps { function DonateButtonComponent() {
image: string;
}
function DonateCard({ image }: DonateCardProps) {
return ( return (
<Card className={cl("card", "donate")}> <DonateButton
<div> look={Button.Looks.FILLED}
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle> color={Button.Colors.WHITE}
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText> style={{ marginTop: "1em" }}
<DonateButton style={{ transform: "translateX(-1em)" }} />
</div>
<img
role="presentation"
src={image}
alt=""
height={128}
style={{
imageRendering: image === SHIGGY_DONATE_IMAGE ? "pixelated" : void 0,
marginLeft: "auto",
transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : void 0
}}
/> />
</Card>
); );
} }
function isDonor(userId: string): boolean {
const donorBadges = BadgeAPI.getDonorBadges(userId);
return GuildMemberStore.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID) || !!donorBadges;
}
export default wrapTab(VencordSettings, "Vencord Settings"); export default wrapTab(VencordSettings, "Vencord Settings");

View file

@ -1,12 +1,17 @@
.vc-settings-quickActions-card { .vc-settings-quickActions-card {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, max-content)); grid-template-columns: repeat(3, 1fr);
gap: 0.5em; gap: 0.5em;
justify-content: center; padding: 0.5em;
padding: 0.5em 0;
margin-bottom: 1em; margin-bottom: 1em;
} }
@media (width <=1040px) {
.vc-settings-quickActions-card {
grid-template-columns: repeat(2, 1fr);
}
}
.vc-settings-quickActions-pill { .vc-settings-quickActions-pill {
all: unset; all: unset;
background: var(--background-secondary); background: var(--background-secondary);
@ -14,12 +19,16 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
padding: 8px 12px; padding: 8px 9px;
border-radius: 9999px; border-radius: 8px;
transition: 0.1s ease-out;
box-sizing: border-box;
} }
.vc-settings-quickActions-pill:hover { .vc-settings-quickActions-pill:hover {
background: var(--background-secondary-alt); background: var(--background-secondary-alt);
transform: translateY(-1px);
box-shadow: var(--elevation-high);
} }
.vc-settings-quickActions-pill:focus-visible { .vc-settings-quickActions-pill:focus-visible {

View file

@ -0,0 +1,92 @@
.vc-donate-button {
overflow: visible !important;
}
.vc-donate-button .vc-heart-icon {
transition: transform 0.3s;
}
.vc-donate-button:hover .vc-heart-icon {
transform: scale(1.1);
z-index: 10;
position: relative;
}
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
}
.vc-special-card-special {
padding: 1em 1.5em;
margin-bottom: 1em;
background-size: cover;
background-position: center;
}
.vc-special-card-flex {
display: flex;
flex-direction: row;
}
.vc-special-card-flex-main {
width: 100%;
}
.vc-special-title {
color: black;
}
.vc-special-subtitle {
color: black;
font-size: 1.2em;
font-weight: bold;
margin-top: 0.5em;
}
.vc-special-text {
color: black;
font-size: 1em;
margin-top: .75em;
white-space: pre-line;
}
.vc-special-seperator {
margin-top: .75em;
border-top: 1px solid white;
opacity: 0.4;
}
.vc-special-hyperlink {
margin-top: 1em;
cursor: pointer;
.vc-special-hyperlink-text {
color: black;
font-size: 1em;
font-weight: bold;
text-align: center;
transition: text-decoration 0.5s;
cursor: pointer;
}
&:hover .vc-special-hyperlink-text {
text-decoration: underline;
}
}
.vc-special-image-container {
display: flex;
justify-content: center;
align-items: center;
margin-left: 1em;
flex-shrink: 0;
width: 100px;
height: 100px;
border-radius: 50%;
background-color: white;
}
.vc-special-image {
width: 65%;
}

View file

@ -5,3 +5,8 @@
.vc-owner-crown-icon { .vc-owner-crown-icon {
color: var(--text-warning); color: var(--text-warning);
} }
.vc-heart-icon {
margin-right: 0.5em;
translate: 0 2px;
}

View file

@ -10,7 +10,6 @@ export * from "./CodeBlock";
export * from "./DonateButton"; export * from "./DonateButton";
export { default as ErrorBoundary } from "./ErrorBoundary"; export { default as ErrorBoundary } from "./ErrorBoundary";
export * from "./ErrorCard"; export * from "./ErrorCard";
export * from "./ExpandableHeader";
export * from "./Flex"; export * from "./Flex";
export * from "./Heart"; export * from "./Heart";
export * from "./Icons"; export * from "./Icons";

View file

@ -62,14 +62,21 @@ async function runReporter() {
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail"); 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") {
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; if (args[0].$$vencordProps != null) {
else if (method === "mapMangledModule") { logMessage += `(${args[0].$$vencordProps.map(arg => `"${arg}"`).join(", ")})`;
} else {
logMessage += `(${args[0].toString().slice(0, 147)}...)`;
}
} 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); 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})`; 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);
} }

View file

@ -17,7 +17,7 @@
*/ */
import { onceDefined } from "@shared/onceDefined"; import { onceDefined } from "@shared/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu, nativeTheme } from "electron"; import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import { dirname, join } from "path"; import { dirname, join } from "path";
import { initIpc } from "./ipcMain"; import { initIpc } from "./ipcMain";
@ -100,19 +100,6 @@ if (!IS_VANILLA) {
super(options); super(options);
initIpc(this); initIpc(this);
// Workaround for https://github.com/electron/electron/issues/43367. Vesktop also has its own workaround
// @TODO: Remove this when the issue is fixed
if (IS_DISCORD_DESKTOP) {
this.webContents.on("devtools-opened", () => {
if (!nativeTheme.shouldUseDarkColors) return;
nativeTheme.themeSource = "light";
setTimeout(() => {
nativeTheme.themeSource = "dark";
}, 100);
});
}
} else super(options); } else super(options);
} }
} }

View file

@ -71,13 +71,16 @@ export async function installExt(id: string) {
// React Devtools v4.25 // React Devtools v4.25
// v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843 // v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
// Unfortunately, Google does not serve old versions, so this is the only way // Unfortunately, Google does not serve old versions, so this is the only way
// This zip file is pinned to long commit hash so it cannot be changed remotely
? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip" ? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
: `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`; : `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
const buf = await get(url, { const buf = await get(url, {
headers: { headers: {
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)" "User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)`
} }
}); });
await extract(crxToZip(buf), extDir).catch(console.error); await extract(crxToZip(buf), extDir).catch(console.error);
} }

View file

@ -0,0 +1,5 @@
/* the profile popout badge container(s) */
[class*="biteSize_"] [class*="tags_"] [class*="container_"] {
/* Discord has padding set to 2px instead of 1px, which causes the 12th badge to wrap to a new line. */
padding: 0 1px;
}

View file

@ -16,6 +16,8 @@
* 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 "./fixDiscordBadgePadding.css";
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges"; import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -26,7 +28,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { isPluginDev } from "@utils/misc"; import { isPluginDev } from "@utils/misc";
import { closeModal, Modals, openModal } from "@utils/modal"; import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Forms, Toasts, UserStore } from "@webpack/common"; import { Forms, Toasts, UserStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
@ -77,7 +79,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??" replace: "...$1.props,$& $1.image??"
}, },
{ {
match: /(?<=text:(\i)\.description,.{0,200})children:/, match: /(?<="aria-label":(\i)\.description,.{0,200})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :" replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
}, },
// conditionally override their onClick with badge.onClick if it exists // conditionally override their onClick with badge.onClick if it exists
@ -100,8 +102,9 @@ export default definePlugin({
} }
}, },
userProfileBadge: ContributorBadge,
async start() { async start() {
Vencord.Api.Badges.addBadge(ContributorBadge);
await loadBadges(); await loadBadges();
}, },
@ -141,8 +144,8 @@ export default definePlugin({
closeModal(modalKey); closeModal(modalKey);
VencordNative.native.openExternal("https://github.com/sponsors/Vendicated"); VencordNative.native.openExternal("https://github.com/sponsors/Vendicated");
}}> }}>
<Modals.ModalRoot {...props}> <ModalRoot {...props}>
<Modals.ModalHeader> <ModalHeader>
<Flex style={{ width: "100%", justifyContent: "center" }}> <Flex style={{ width: "100%", justifyContent: "center" }}>
<Forms.FormTitle <Forms.FormTitle
tag="h2" tag="h2"
@ -156,8 +159,8 @@ export default definePlugin({
Vencord Donor Vencord Donor
</Forms.FormTitle> </Forms.FormTitle>
</Flex> </Flex>
</Modals.ModalHeader> </ModalHeader>
<Modals.ModalContent> <ModalContent>
<Flex> <Flex>
<img <img
role="presentation" role="presentation"
@ -180,13 +183,13 @@ export default definePlugin({
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!! Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText> </Forms.FormText>
</div> </div>
</Modals.ModalContent> </ModalContent>
<Modals.ModalFooter> <ModalFooter>
<Flex style={{ width: "100%", justifyContent: "center" }}> <Flex style={{ width: "100%", justifyContent: "center" }}>
<DonateButton /> <DonateButton />
</Flex> </Flex>
</Modals.ModalFooter> </ModalFooter>
</Modals.ModalRoot> </ModalRoot>
</ErrorBoundary> </ErrorBoundary>
)); ));
}, },

View file

@ -12,11 +12,16 @@ export default definePlugin({
description: "API to add buttons to the chat input", description: "API to add buttons to the chat input",
authors: [Devs.Ven], authors: [Devs.Ven],
patches: [{ patches: [
{
find: '"sticker")', find: '"sticker")',
replacement: { replacement: {
match: /return\(!\i\.\i&&(?=\(\i\.isDM.+?(\i)\.push\(.{0,50}"gift")/, // FIXME(Bundler change related): Remove old compatiblity once enough time has passed
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)&&" match: /return\((!)?\i\.\i(?:\|\||&&)(?=\(\i\.isDM.+?(\i)\.push)/,
replace: (m, not, children) => not
? `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),true)&&`
: `${m}(Vencord.Api.ChatButtons._injectButtons(${children},arguments[0]),false)||`
} }
}] }
]
}); });

View file

@ -34,12 +34,22 @@ export default definePlugin({
} }
}, },
{ {
find: ".Menu,{", find: "navId:",
all: true, all: true,
replacement: { noWarn: true,
match: /Menu,{(?<=\.jsxs?\)\(\i\.Menu,{)/g, replacement: [
replace: "$&contextMenuApiArguments:typeof arguments!=='undefined'?arguments:[]," {
match: /navId:(?=.+?([,}].*?\)))/g,
replace: (m, rest) => {
// Check if this navId: match is a destructuring statement, ignore it if it is
const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) {
return `contextMenuAPIArguments:typeof arguments!=='undefined'?arguments:[],${m}`;
}
return m;
} }
} }
] ]
}
]
}); });

View file

@ -19,10 +19,15 @@
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import managedStyle from "./style.css?managed";
export default definePlugin({ export default definePlugin({
name: "MemberListDecoratorsAPI", name: "MemberListDecoratorsAPI",
description: "API to add decorators to member list (both in servers and DMs)", description: "API to add decorators to member list (both in servers and DMs)",
authors: [Devs.TheSun, Devs.Ven], authors: [Devs.TheSun, Devs.Ven],
managedStyle,
patches: [ patches: [
{ {
find: ".lostPermission)", find: ".lostPermission)",
@ -32,7 +37,7 @@ export default definePlugin({
replace: "$&vencordProps=$1," replace: "$&vencordProps=$1,"
}, { }, {
match: /#{intl::GUILD_OWNER}(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/, match: /#{intl::GUILD_OWNER}(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps))," replace: "$&(typeof vencordProps=='undefined'?null:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
} }
] ]
}, },
@ -40,8 +45,8 @@ export default definePlugin({
find: "PrivateChannel.renderAvatar", find: "PrivateChannel.renderAvatar",
replacement: { replacement: {
match: /decorators:(\i\.isSystemDM\(\))\?(.+?):null/, match: /decorators:(\i\.isSystemDM\(\))\?(.+?):null/,
replace: "decorators:[...Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]), $1?$2:null]" replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]),$1?$2:null]"
} }
} }
], ]
}); });

View file

@ -0,0 +1,11 @@
.vc-member-list-decorators-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25em;
}
.vc-member-list-decorators-wrapper:not(:empty) {
/* Margin to match default Discord decorators */
margin-left: 0.25em;
}

View file

@ -0,0 +1,68 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin from "@utils/types";
// duplicate values have multiple branches with different types. Just include all to be safe
const nameMap = {
radio: "MenuRadioItem",
separator: "MenuSeparator",
checkbox: "MenuCheckboxItem",
groupstart: "MenuGroup",
control: "MenuControlItem",
compositecontrol: "MenuControlItem",
item: "MenuItem",
customitem: "MenuItem",
};
export default definePlugin({
name: "MenuItemDemanglerAPI",
description: "Demangles Discord's Menu Item module",
authors: [Devs.Ven],
required: true,
patches: [
{
find: '"Menu API',
replacement: {
match: /function.{0,80}type===(\i\.\i)\).{0,50}navigable:.+?Menu API/s,
replace: (m, mod) => {
const nameAssignments = [] as string[];
// if (t.type === m.MenuItem)
const typeCheckRe = canonicalizeMatch(/\(\i\.type===(\i\.\i)\)/g);
// push({type:"item"})
const pushTypeRe = /type:"(\w+)"/g;
let typeMatch: RegExpExecArray | null;
// for each if (t.type === ...)
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
// extract the current menu item
const item = typeMatch[1];
// Set the starting index of the second regex to that of the first to start
// matching from after the if
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
// extract the first type: "..."
const type = pushTypeRe.exec(m)?.[1];
if (type && type in nameMap) {
const name = nameMap[type];
nameAssignments.push(`Object.defineProperty(${item},"name",{value:"${name}"})`);
}
}
if (nameAssignments.length < 6) {
console.warn("[MenuItemDemanglerAPI] Expected to at least remap 6 items, only remapped", nameAssignments.length);
}
// Merge all our redefines with the actual module
return `${nameAssignments.join(";")};${m}`;
},
},
},
],
});

View file

@ -19,17 +19,22 @@
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import managedStyle from "./style.css?managed";
export default definePlugin({ export default definePlugin({
name: "MessageDecorationsAPI", name: "MessageDecorationsAPI",
description: "API to add decorations to messages", description: "API to add decorations to messages",
authors: [Devs.TheSun], authors: [Devs.TheSun],
managedStyle,
patches: [ patches: [
{ {
find: '"Message Username"', find: '"Message Username"',
replacement: { replacement: {
match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?}\),\i(?=\])/, match: /#{intl::GUILD_COMMUNICATION_DISABLED_BOTTOM_SHEET_TITLE}.+?}\),\i(?=\])/,
replace: "$&,...Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])" replace: "$&,Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0])"
} }
} }
], ]
}); });

View file

@ -0,0 +1,18 @@
.vc-message-decorations-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25em;
}
.vc-message-decorations-wrapper:not(:empty) {
/* Margin to match default Discord decorators */
margin-left: 0.25em;
/* Align vertically */
position: relative;
vertical-align: top;
top: 0.1rem;
height: calc(1rem + 4px);
max-height: calc(1rem + 4px)
}

View file

@ -31,20 +31,17 @@ export default definePlugin({
replace: (match, args) => "" + replace: (match, args) => "" +
`async ${match}` + `async ${match}` +
`if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` + `if(await Vencord.Api.MessageEvents._handlePreEdit(${args}))` +
"return Promise.resolve({shoudClear:true,shouldRefocus:true});" "return Promise.resolve({shouldClear:false,shouldRefocus:true});"
} }
}, },
{ {
find: ".handleSendMessage,onResize", find: ".handleSendMessage,onResize",
replacement: { replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); // https://regex101.com/r/hBlXpl/1
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid) match: /let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptions\(\{.+?\}\);(?<=\)\(({.+?})\)\.then.+?)/,
match: /(type:this\.props\.chatInputType.+?\.then\()(\i=>\{.+?let (\i)=\i\.\i\.parse\((\i),.+?let (\i)=\i\.\i\.getSendMessageOptionsForReply\(\i\);)(?<=\)\(({.+?})\)\.then.+?)/, replace: (m, parsedMessage, channel, replyOptions, extra) => m +
// props.chatInputType...then((async function(isMessageValid)... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); if(await Vencord.api...) return { shoudClear:true, shouldRefocus:true };
replace: (_, rest1, rest2, parsedMessage, channel, replyOptions, extra) => "" +
`${rest1}async ${rest2}` +
`if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` + `if(await Vencord.Api.MessageEvents._handlePreSend(${channel}.id,${parsedMessage},${extra},${replyOptions}))` +
"return{shoudClear:true,shouldRefocus:true};" "return{shouldClear:false,shouldRefocus:true};"
} }
}, },
{ {
@ -52,7 +49,6 @@ export default definePlugin({
replacement: { replacement: {
match: /let\{id:\i}=(\i),{id:\i}=(\i);return \i\.useCallback\((\i)=>\{/, match: /let\{id:\i}=(\i),{id:\i}=(\i);return \i\.useCallback\((\i)=>\{/,
replace: (m, message, channel, event) => replace: (m, message, channel, event) =>
// the message param is shadowed by the event param, so need to alias them
`const vcMsg=${message},vcChan=${channel};${m}Vencord.Api.MessageEvents._handleClick(vcMsg,vcChan,${event});` `const vcMsg=${message},vcChan=${channel};${m}Vencord.Api.MessageEvents._handleClick(vcMsg,vcChan,${event});`
} }
} }

View file

@ -23,11 +23,14 @@ export default definePlugin({
name: "MessagePopoverAPI", name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.", description: "API to add buttons to message popovers.",
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz], authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
patches: [{ patches: [
{
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}", find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: { replacement: {
match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.string\(\i\.\i#{intl::MESSAGE_ACTION_REPLY}.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/, match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
replace: "$&,Vencord.Api.MessagePopover._buildPopoverElements($1,$2)" replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" +
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},`
} }
}], }
]
}); });

View file

@ -34,7 +34,7 @@ export default definePlugin({
}, },
{ {
match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/, match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/,
replace: "if($1.id==\"VencordNotice\")return($1=null,Vencord.Api.Notices.nextNotice(),true);$&" replace: "if($1?.id==\"VencordNotice\")return($1=null,Vencord.Api.Notices.nextNotice(),true);$&"
} }
] ]
} }

View file

@ -59,6 +59,14 @@ export default definePlugin({
replace: "$&return;" replace: "$&return;"
} }
] ]
},
{
find: ".BetterDiscord||null!=",
replacement: {
// Make hasClientMods return false
match: /(?=let \i=window;)/,
replace: "return false;"
}
} }
], ],

View file

@ -65,7 +65,8 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}` replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
}, },
{ {
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/, // FIXME(Bundler change related): Remove old compatiblity once enough time has passed
match: /({(?=.+?function (\i).{0,160}(\i)=\i\.useMemo.{0,140}return \i\.useMemo\(\(\)=>\i\(\3).+?(?:function\(\){return |\(\)=>))\2/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})` replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
} }
] ]

View file

@ -16,14 +16,13 @@
* 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 { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; 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 { 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 { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, KNOWN_ISSUES_CHANNEL_ID, REGULAR_ROLE_ID, SUPPORT_CHANNEL_ID, VENBOT_USER_ID, VENCORD_GUILD_ID } from "@utils/constants";
import { sendMessage } from "@utils/discord"; import { sendMessage } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
@ -34,15 +33,13 @@ import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater"; import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common"; import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { JSX } from "react";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import plugins, { PluginMeta } from "~plugins"; import plugins, { PluginMeta } from "~plugins";
import SettingsPlugin from "./settings"; import SettingsPlugin from "./settings";
const VENCORD_GUILD_ID = "1015060230222131221";
const VENBOT_USER_ID = "1017176847865352332";
const KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
const CodeBlockRe = /```js\n(.+?)```/s; const CodeBlockRe = /```js\n(.+?)```/s;
const AllowedChannelIds = [ const AllowedChannelIds = [
@ -52,9 +49,9 @@ const AllowedChannelIds = [
]; ];
const TrustedRolesIds = [ const TrustedRolesIds = [
"1026534353167208489", // contributor CONTRIB_ROLE_ID, // contributor
"1026504932959977532", // regular REGULAR_ROLE_ID, // regular
"1042507929485586532", // donor DONOR_ROLE_ID, // donor
]; ];
const AsyncFunction = async function () { }.constructor; const AsyncFunction = async function () { }.constructor;
@ -142,7 +139,7 @@ export default definePlugin({
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: ["UserSettingsAPI", "MessageAccessoriesAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,
@ -235,23 +232,7 @@ export default definePlugin({
} }
}, },
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => { renderMessageAccessory(props) {
const userId = channel.getRecipientId();
if (!isPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
return (
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
Please do not private message Vencord plugin developers for support!
<br />
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
</Card>
);
}, { noop: true }),
start() {
addAccessory("vencord-debug", props => {
const buttons = [] as JSX.Element[]; const buttons = [] as JSX.Element[];
const shouldAddUpdateButton = const shouldAddUpdateButton =
@ -328,6 +309,20 @@ export default definePlugin({
return buttons.length return buttons.length
? <Flex>{buttons}</Flex> ? <Flex>{buttons}</Flex>
: null; : null;
});
}, },
renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
const userId = channel.getRecipientId();
if (!isPluginDev(userId)) return null;
if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
return (
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
Please do not private message Vencord plugin developers for support!
<br />
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
</Card>
);
}, { noop: true }),
}); });

View file

@ -16,14 +16,14 @@ import { User } from "discord-types/general";
interface UserProfileProps { interface UserProfileProps {
popoutProps: Record<string, any>; popoutProps: Record<string, any>;
currentUser: User; currentUser: User;
originalPopout: () => React.ReactNode; originalRenderPopout: () => React.ReactNode;
} }
const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined"); const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined");
const styles = findByPropsLazy("accountProfilePopoutWrapper"); const styles = findByPropsLazy("accountProfilePopoutWrapper");
let openAlternatePopout = false; let openAlternatePopout = false;
let accountPanelRef: React.MutableRefObject<Record<PropertyKey, any> | null> = { current: null }; let accountPanelRef: React.RefObject<Record<PropertyKey, any> | null> = { current: null };
const AccountPanelContextMenu = ErrorBoundary.wrap(() => { const AccountPanelContextMenu = ErrorBoundary.wrap(() => {
const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]); const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]);
@ -73,19 +73,19 @@ export default definePlugin({
group: true, group: true,
replacement: [ replacement: [
{ {
match: /(?<=\.SIZE_32\)}\);)/, match: /(?<=\.AVATAR_SIZE\);)/,
replace: "$self.useAccountPanelRef();" replace: "$self.useAccountPanelRef();"
}, },
{ {
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/, match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalPopout:()=>{${originalPopout}}})` replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})`
}, },
{ {
match: /\.AVATAR,children:.+?(?=renderPopout:)/, match: /\.AVATAR,children:.+?(?=renderPopout:)/,
replace: "$&onRequestClose:$self.onPopoutClose," replace: "$&onRequestClose:$self.onPopoutClose,"
}, },
{ {
match: /(?<=.avatarWrapper,)/, match: /(?<=\.avatarWrapper,)/,
replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu," replace: "ref:$self.accountPanelRef,onContextMenu:$self.openAccountPanelContextMenu,"
} }
] ]
@ -112,17 +112,17 @@ export default definePlugin({
openAlternatePopout = false; openAlternatePopout = false;
}, },
UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalPopout }: UserProfileProps) => { UserProfile: ErrorBoundary.wrap(({ popoutProps, currentUser, originalRenderPopout }: UserProfileProps) => {
if ( if (
(settings.store.prioritizeServerProfile && openAlternatePopout) || (settings.store.prioritizeServerProfile && openAlternatePopout) ||
(!settings.store.prioritizeServerProfile && !openAlternatePopout) (!settings.store.prioritizeServerProfile && !openAlternatePopout)
) { ) {
return originalPopout(); return originalRenderPopout();
} }
const currentChannel = getCurrentChannel(); const currentChannel = getCurrentChannel();
if (currentChannel?.getGuildId() == null) { if (currentChannel?.getGuildId() == null) {
return originalPopout(); return originalRenderPopout();
} }
return ( return (

View file

@ -41,10 +41,10 @@ export default definePlugin({
}, },
{ {
// Status emojis // Status emojis
find: "#{intl::GUILD_OWNER}", find: "#{intl::GUILD_OWNER}),children:",
replacement: { replacement: {
match: /(?<=\.activityEmoji,.+?animate:)\i/, match: /(\.CUSTOM_STATUS.+?animate:)\i/,
replace: "!0" replace: (_, rest) => `${rest}!0`
} }
}, },
{ {

View file

@ -51,7 +51,7 @@ export default definePlugin({
{ {
find: "bitbucket.org", find: "bitbucket.org",
replacement: { replacement: {
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/, match: /function \i\(\i\){(?=.{0,30}pathname:\i)/,
replace: "$&return null;" replace: "$&return null;"
}, },
predicate: () => settings.store.file predicate: () => settings.store.file

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { canonicalizeMatch } from "@utils/patches";
import { execFile } from "child_process"; import { execFile } from "child_process";
import { promisify } from "util"; import { promisify } from "util";
@ -26,7 +27,7 @@ interface RemoteData {
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null; let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
const APPLE_MUSIC_BUNDLE_REGEX = /<script type="module" crossorigin src="([a-zA-Z0-9.\-/]+)"><\/script>/; const APPLE_MUSIC_BUNDLE_REGEX = /<script type="module" crossorigin src="([a-zA-Z0-9.\-/]+)"><\/script>/;
const APPLE_MUSIC_TOKEN_REGEX = /\w+="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)",\w+="x-apple-jingle-correlation-key"/; const APPLE_MUSIC_TOKEN_REGEX = canonicalizeMatch(/Promise.allSettled\(\i\)\}const \i="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)"/);
let cachedToken: string | undefined = undefined; let cachedToken: string | undefined = undefined;

View file

@ -23,7 +23,7 @@ import definePlugin, { ReporterTestable } from "@utils/types";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common"; import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
const fetchApplicationsRPC = findByCodeLazy("APPLICATION_RPC(", "Client ID"); const fetchApplicationsRPC = findByCodeLazy('"Invalid Origin"', ".application");
async function lookupAsset(applicationId: string, key: string): Promise<string> { async function lookupAsset(applicationId: string, key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];

View file

@ -17,14 +17,13 @@
*/ */
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { useStateFromStores } from "@webpack/common"; import { Animations, useStateFromStores } from "@webpack/common";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { ExpandedGuildFolderStore, settings } from "."; import { ExpandedGuildFolderStore, settings } from ".";
const ChannelRTCStore = findStoreLazy("ChannelRTCStore"); const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const Animations = findByPropsLazy("a", "animated", "useTransition");
const GuildsBar = findComponentByCodeLazy('("guildsnav")'); const GuildsBar = findComponentByCodeLazy('("guildsnav")');
export default ErrorBoundary.wrap(guildsBarProps => { export default ErrorBoundary.wrap(guildsBarProps => {

View file

@ -123,7 +123,7 @@ export default definePlugin({
}, },
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children // If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
{ {
match: /lastTargetNode:\i\[\i\.length-1\].+?Fragment.+?\]}\)\]/, match: /lastTargetNode:\i\[\i\.length-1\].+?}\)(?::null)?\](?=}\))/,
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))" replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))"
}, },
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children // If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
@ -159,7 +159,7 @@ export default definePlugin({
] ]
}, },
{ {
find: ".FOLDER_ITEM_GUILD_ICON_MARGIN);", find: ".expandedFolderBackground,",
predicate: () => settings.store.sidebar, predicate: () => settings.store.sidebar,
replacement: [ replacement: [
// We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it) // We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)
@ -173,8 +173,8 @@ export default definePlugin({
// Disable expanding and collapsing folders transition in the normal GuildsBar sidebar // Disable expanding and collapsing folders transition in the normal GuildsBar sidebar
{ {
predicate: () => !settings.store.keepIcons, predicate: () => !settings.store.keepIcons,
match: /(?<=#{intl::SERVER_FOLDER_PLACEHOLDER}.+?useTransition\)\()/, match: /(?=,\{from:\{height)/,
replace: "$self.shouldShowTransition(arguments[0])&&" replace: "&&$self.shouldShowTransition(arguments[0])"
}, },
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded // If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
{ {
@ -185,7 +185,7 @@ export default definePlugin({
{ {
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar // Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always, predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
match: /(?<=\.wrapper,children:\[)/, match: /(?<=\.isExpanded\),children:\[)/,
replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&" replace: "$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
}, },
{ {
@ -276,23 +276,29 @@ export default definePlugin({
makeGuildsBarGuildListFilter(isBetterFolders: boolean) { makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
return child => { return child => {
if (isBetterFolders) { if (!isBetterFolders) return true;
try { try {
return child?.props?.["aria-label"] === getIntlMessage("SERVERS"); return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
}
return true; return true;
}; };
}, },
makeGuildsBarTreeFilter(isBetterFolders: boolean) { makeGuildsBarTreeFilter(isBetterFolders: boolean) {
return child => { return child => {
if (isBetterFolders) { if (!isBetterFolders) return true;
return child?.props?.onScroll != null;
} if (child?.props?.className?.includes("itemsContainer") && child.props.children != null) {
// Filter out everything but the scroller for the guild list
child.props.children = child.props.children.filter(child => child?.props?.onScroll != null);
return true; return true;
}
return false;
}; };
}, },

View file

@ -83,7 +83,7 @@ export default definePlugin({
if (!role) return; if (!role) return;
if (role.colorString) { if (role.colorString) {
children.push( children.unshift(
<Menu.MenuItem <Menu.MenuItem
id="vc-copy-role-color" id="vc-copy-role-color"
label="Copy Role Color" label="Copy Role Color"
@ -93,6 +93,20 @@ export default definePlugin({
); );
} }
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.unshift(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
if (role.icon) { if (role.icon) {
children.push( children.push(
<Menu.MenuItem <Menu.MenuItem
@ -110,20 +124,6 @@ export default definePlugin({
); );
} }
if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) {
children.push(
<Menu.MenuItem
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(id);
}}
icon={PencilIcon}
/>
);
}
} }
} }
}); });

View file

@ -99,7 +99,7 @@ export const UnknownIcon = (props: React.PropsWithChildren<SVGProps<SVGSVGElemen
fill="currentColor" fill="currentColor"
viewBox="0 0 16 16" viewBox="0 0 16 16"
> >
<path fill-rule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z" /> <path fillRule="evenodd" d="M4.475 5.458c-.284 0-.514-.237-.47-.517C4.28 3.24 5.576 2 7.825 2c2.25 0 3.767 1.36 3.767 3.215 0 1.344-.665 2.288-1.79 2.973-1.1.659-1.414 1.118-1.414 2.01v.03a.5.5 0 0 1-.5.5h-.77a.5.5 0 0 1-.5-.495l-.003-.2c-.043-1.221.477-2.001 1.645-2.712 1.03-.632 1.397-1.135 1.397-2.028 0-.979-.758-1.698-1.926-1.698-1.009 0-1.71.529-1.938 1.402-.066.254-.278.461-.54.461h-.777ZM7.496 14c.622 0 1.095-.474 1.095-1.09 0-.618-.473-1.092-1.095-1.092-.606 0-1.087.474-1.087 1.091S6.89 14 7.496 14Z" />
</svg> </svg>
); );

View file

@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
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";
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Constants, React, RestAPI, Tooltip } from "@webpack/common"; import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
import { RenameButton } from "./components/RenameButton"; import { RenameButton } from "./components/RenameButton";
@ -34,7 +34,7 @@ const UserSettingsModal = findByPropsLazy("saveAccountChanges", "open");
const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer"); const TimestampClasses = findByPropsLazy("timestampTooltip", "blockquoteContainer");
const SessionIconClasses = findByPropsLazy("sessionIcon"); const SessionIconClasses = findByPropsLazy("sessionIcon");
const BlobMask = findExportedComponentLazy("BlobMask"); const BlobMask = findComponentByCodeLazy("!1,lowerBadgeSize:");
const settings = definePluginSettings({ const settings = definePluginSettings({
backgroundCheck: { backgroundCheck: {

View file

@ -101,8 +101,8 @@ export default definePlugin({
find: 'minimal:"contentColumnMinimal"', find: 'minimal:"contentColumnMinimal"',
replacement: [ replacement: [
{ {
match: /\(0,\i\.useTransition\)\((\i)/, match: /(?=\(0,\i\.\i\)\((\i),\{from:\{position:"absolute")/,
replace: "(_cb=>_cb(void 0,$1))||$&" replace: "(_cb=>_cb(void 0,$1))||"
}, },
{ {
match: /\i\.animated\.div/, match: /\i\.animated\.div/,
@ -114,7 +114,7 @@ export default definePlugin({
{ // Load menu TOC eagerly { // Load menu TOC eagerly
find: "#{intl::USER_SETTINGS_WITH_BUILD_OVERRIDE}", find: "#{intl::USER_SETTINGS_WITH_BUILD_OVERRIDE}",
replacement: { replacement: {
match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/, match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?null!=\i&&.{0,100}?(await [^};]*?\)\)).*?,(?=\1\(this)/,
replace: "$&(async ()=>$2)()," replace: "$&(async ()=>$2)(),"
}, },
predicate: () => settings.store.eagerLoad predicate: () => settings.store.eagerLoad
@ -173,7 +173,7 @@ export default definePlugin({
} }
return this; return this;
}, },
map(render: (item: SettingsEntry) => ReactElement) { map(render: (item: SettingsEntry) => ReactElement<any>) {
return items return items
.filter(a => a.items.length > 0) .filter(a => a.items.length > 0)
.map(({ label, items }) => { .map(({ label, items }) => {
@ -181,11 +181,14 @@ export default definePlugin({
if (label) { if (label) {
return ( return (
<Menu.MenuItem <Menu.MenuItem
key={label}
id={label.replace(/\W/, "_")} id={label.replace(/\W/, "_")}
label={label} label={label}
children={children}
action={children[0].props.action} action={children[0].props.action}
/>); >
{children}
</Menu.MenuItem>
);
} else { } else {
return children; return children;
} }

View file

@ -27,7 +27,7 @@ export default definePlugin({
{ {
find: '"ChannelAttachButton"', find: '"ChannelAttachButton"',
replacement: { replacement: {
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/, match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,", replace: "$&onClick:$1,onContextMenu:$2.onClick,",
}, },
}, },

View file

@ -24,14 +24,14 @@ let style: HTMLStyleElement;
function setCss() { function setCss() {
style.textContent = ` style.textContent = `
.vc-nsfw-img [class^=imageWrapper] img, .vc-nsfw-img [class^=imageContainer],
.vc-nsfw-img [class^=wrapperPaused] video { .vc-nsfw-img [class^=wrapperPaused] {
filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px); filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px);
transition: filter 0.2s; transition: filter 0.2s;
&:hover {
filter: blur(0);
} }
.vc-nsfw-img [class^=imageWrapper]:hover img,
.vc-nsfw-img [class^=wrapperPaused]:hover video {
filter: unset;
} }
`; `;
} }
@ -54,7 +54,7 @@ export default definePlugin({
options: { options: {
blurAmount: { blurAmount: {
type: OptionType.NUMBER, type: OptionType.NUMBER,
description: "Blur Amount", description: "Blur Amount (in pixels)",
default: 10, default: 10,
onChange: setCss onChange: setCss
} }

View file

@ -75,10 +75,11 @@ export default definePlugin({
patches: [{ patches: [{
find: "renderConnectionStatus(){", find: "renderConnectionStatus(){",
replacement: { replacement: {
match: /(?<=renderConnectionStatus\(\)\{.+\.channel,children:)\i/, match: /(renderConnectionStatus\(\){.+\.channel,children:)(.+?}\):\i)(?=}\))/,
replace: "[$&, $self.renderTimer(this.props.channel.id)]" replace: "$1[$2,$self.renderTimer(this.props.channel.id)]"
} }
}], }],
renderTimer(channelId: string) { renderTimer(channelId: string) {
return <ErrorBoundary noop> return <ErrorBoundary noop>
<this.Timer channelId={channelId} /> <this.Timer channelId={channelId} />

View file

@ -17,11 +17,7 @@
*/ */
import { import {
addPreEditListener, MessageObject
addPreSendListener,
MessageObject,
removePreEditListener,
removePreSendListener
} from "@api/MessageEvents"; } from "@api/MessageEvents";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
@ -36,7 +32,18 @@ export default definePlugin({
name: "ClearURLs", name: "ClearURLs",
description: "Removes tracking garbage from URLs", description: "Removes tracking garbage from URLs",
authors: [Devs.adryd], authors: [Devs.adryd],
dependencies: ["MessageEventsAPI"],
start() {
this.createRules();
},
onBeforeMessageSend(_, msg) {
return this.onSend(msg);
},
onBeforeMessageEdit(_cid, _mid, msg) {
return this.onSend(msg);
},
escapeRegExp(str: string) { escapeRegExp(str: string) {
return (str && reHasRegExpChar.test(str)) return (str && reHasRegExpChar.test(str))
@ -133,17 +140,4 @@ export default definePlugin({
); );
} }
}, },
start() {
this.createRules();
this.preSend = addPreSendListener((_, msg) => this.onSend(msg));
this.preEdit = addPreEditListener((_cid, _mid, msg) =>
this.onSend(msg)
);
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
},
}); });

View file

@ -1,29 +1,29 @@
.client-theme-settings { .vc-clientTheme-settings {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.client-theme-container { .vc-clientTheme-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
} }
.client-theme-settings-labels { .vc-clientTheme-labels {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
} }
.client-theme-container > [class^="colorSwatch"] > [class^="swatch"] { .vc-clientTheme-container [class^="swatch"] {
border: thin solid var(--background-modifier-accent) !important; border: thin solid var(--background-modifier-accent) !important;
} }
.client-theme-warning * { .vc-clientTheme-warning-text {
color: var(--text-danger); color: var(--text-danger);
} }
.client-theme-contrast-warning { .vc-clientTheme-contrast-warning {
background-color: var(--background-primary); background-color: var(--background-primary);
padding: 0.5rem; padding: 0.5rem;
border-radius: .5rem; border-radius: .5rem;

View file

@ -7,6 +7,7 @@
import "./clientTheme.css"; import "./clientTheme.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
@ -14,6 +15,8 @@ import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common"; import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
const cl = classNameFactory("vc-clientTheme-");
const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const colorPresets = [ const colorPresets = [
@ -60,9 +63,9 @@ function ThemeSettings() {
} }
return ( return (
<div className="client-theme-settings"> <div className={cl("settings")}>
<div className="client-theme-container"> <div className={cl("container")}>
<div className="client-theme-settings-labels"> <div className={cl("settings-labels")}>
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle> <Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText> <Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
</div> </div>
@ -76,10 +79,10 @@ function ThemeSettings() {
{(contrastWarning || nitroThemeEnabled) && (<> {(contrastWarning || nitroThemeEnabled) && (<>
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} /> <Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}> <div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
<div className="client-theme-warning"> <div className={cl("warning")}>
<Forms.FormText>Warning, your theme won't look good:</Forms.FormText> <Forms.FormText className={cl("warning-text")}>Warning, your theme won't look good:</Forms.FormText>
{contrastWarning && <Forms.FormText>Selected color won't contrast well with text</Forms.FormText>} {contrastWarning && <Forms.FormText className={cl("warning-text")}>Selected color won't contrast well with text</Forms.FormText>}
{nitroThemeEnabled && <Forms.FormText>Nitro themes aren't supported</Forms.FormText>} {nitroThemeEnabled && <Forms.FormText className={cl("warning-text")}>Nitro themes aren't supported</Forms.FormText>}
</div> </div>
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>} {(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>} {(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
@ -91,15 +94,12 @@ function ThemeSettings() {
const settings = definePluginSettings({ const settings = definePluginSettings({
color: { color: {
description: "Color your Discord client theme will be based around. Light mode isn't supported",
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
default: "313338", default: "313338",
component: () => <ThemeSettings /> component: ThemeSettings
}, },
resetColor: { resetColor: {
description: "Reset Theme Color",
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
default: "313338",
component: () => ( component: () => (
<Button onClick={() => onPickColor(0x313338)}> <Button onClick={() => onPickColor(0x313338)}>
Reset Theme Color Reset Theme Color
@ -110,7 +110,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "ClientTheme", name: "ClientTheme",
authors: [Devs.F53, Devs.Nuckyz], authors: [Devs.Nuckyz],
description: "Recreation of the old client theme experiment. Add a color to your Discord client theme", description: "Recreation of the old client theme experiment. Add a color to your Discord client theme",
settings, settings,
@ -126,6 +126,7 @@ export default definePlugin({
stop() { stop() {
document.getElementById("clientThemeVars")?.remove(); document.getElementById("clientThemeVars")?.remove();
document.getElementById("clientThemeOffsets")?.remove(); document.getElementById("clientThemeOffsets")?.remove();
document.getElementById("clientThemeLightModeFixes")?.remove();
} }
}); });

View file

@ -67,9 +67,16 @@ export default definePlugin({
patches: [ patches: [
{ {
find: 'react-spring: The "interpolate" function', find: "https://github.com/highlightjs/highlight.js/issues/2277",
replacement: { replacement: {
match: /,console.warn\('react-spring: The "interpolate" function is deprecated in v10 \(use "to" instead\)'\)/, match: /\(console.log\(`Deprecated.+?`\),/,
replace: "("
}
},
{
find: 'The "interpolate" function is deprecated in v10 (use "to" instead)',
replacement: {
match: /,console.warn\(\i\+'The "interpolate" function is deprecated in v10 \(use "to" instead\)'\)/,
replace: "" replace: ""
} }
}, },
@ -88,10 +95,9 @@ export default definePlugin({
} }
}, },
{ {
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");', find: '"AppCrashedFatalReport: getLastCrash not supported."',
all: true,
replacement: { replacement: {
match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/, match: /console\.log\("AppCrashedFatalReport: getLastCrash not supported\."\);/,
replace: "" replace: ""
} }
}, },
@ -126,28 +132,7 @@ export default definePlugin({
{ {
find: "Slow dispatch on", find: "Slow dispatch on",
replacement: { replacement: {
match: /\i\.totalTime>100&&\i\.verbose\("Slow dispatch on ".+?\)\);/, match: /\i\.totalTime>\i&&\i\.verbose\("Slow dispatch on ".+?\)\);/,
replace: ""
}
},
// Zustand section
{
find: "[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.",
replacement: [
{
match: /&&console\.warn\("\[DEPRECATED\] Passing a vanilla store will be unsupported in a future version\. Instead use `import { useStore } from 'zustand'`\."\)/,
replace: ""
},
{
match: /console\.warn\("\[DEPRECATED\] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`\. They can be imported from 'zustand\/traditional'\. https:\/\/github\.com\/pmndrs\/zustand\/discussions\/1937"\),/,
replace: ""
}
]
},
{
find: "[DEPRECATED] `getStorage`, `serialize` and `deserialize` options are deprecated. Use `storage` option instead.",
replacement: {
match: /console\.warn\("\[DEPRECATED\] `getStorage`, `serialize` and `deserialize` options are deprecated\. Use `storage` option instead\."\),/,
replace: "" replace: ""
} }
}, },

View file

@ -20,6 +20,7 @@ import { Devs } from "@utils/constants";
import { getCurrentChannel, getCurrentGuild } from "@utils/discord"; import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
import { runtimeHashMessageKey } from "@utils/intlHash"; import { runtimeHashMessageKey } from "@utils/intlHash";
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy"; import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
import { ModalAPI } from "@utils/modal";
import { relaunch } from "@utils/native"; import { relaunch } from "@utils/native";
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
import definePlugin, { PluginNative, StartAt } from "@utils/types"; import definePlugin, { PluginNative, StartAt } from "@utils/types";
@ -62,7 +63,7 @@ function makeShortcuts() {
default: default:
const uniqueMatches = [...new Set(matches)]; const uniqueMatches = [...new Set(matches)];
if (uniqueMatches.length > 1) if (uniqueMatches.length > 1)
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches); console.warn(`Warning: This filter matches ${uniqueMatches.length} exports. Make it more specific!\n`, uniqueMatches);
return matches[0]; return matches[0];
} }
@ -132,7 +133,10 @@ function makeShortcuts() {
}); });
} }
Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div"))); const root = Common.ReactDOM.createRoot(doc.body.appendChild(document.createElement("div")));
root.render(Common.React.createElement(component, props));
doc.addEventListener("close", () => root.unmount(), { once: true });
}, },
preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true, preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true,
@ -144,6 +148,8 @@ function makeShortcuts() {
me: { getter: () => Common.UserStore.getCurrentUser(), preload: false }, me: { getter: () => Common.UserStore.getCurrentUser(), preload: false },
meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false }, meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false },
messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false }, messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false },
openModal: { getter: () => ModalAPI.openModal },
openModalLazy: { getter: () => ModalAPI.openModalLazy },
Stores: { Stores: {
getter: () => Object.fromEntries( getter: () => Object.fromEntries(
@ -159,11 +165,38 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
const currentVal = val.getter(); const currentVal = val.getter();
if (!currentVal || val.preload === false) return currentVal; if (!currentVal || val.preload === false) return currentVal;
const value = currentVal[SYM_LAZY_GET] function unwrapProxy(value: any) {
? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED] if (value[SYM_LAZY_GET]) {
: currentVal; forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];
} else if (value.$$vencordInternal) {
return forceLoad ? value.$$vencordInternal() : value;
}
if (value) define(window.shortcutList, key, { value }); return value;
}
const value = unwrapProxy(currentVal);
if (typeof value === "object" && value !== null) {
const descriptors = Object.getOwnPropertyDescriptors(value);
for (const propKey in descriptors) {
if (value[propKey] == null) continue;
const descriptor = descriptors[propKey];
if (descriptor.writable === true || descriptor.set != null) {
const currentValue = value[propKey];
const newValue = unwrapProxy(currentValue);
if (newValue != null && currentValue !== newValue) {
value[propKey] = newValue;
}
}
}
}
if (value != null) {
define(window.shortcutList, key, { value });
define(window, key, { value });
}
return value; return value;
} }
@ -173,6 +206,16 @@ export default definePlugin({
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.", description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
authors: [Devs.Ven], authors: [Devs.Ven],
patches: [
{
find: 'this,"_changeCallbacks",',
replacement: {
match: /\i\(this,"_changeCallbacks",/,
replace: "Reflect.defineProperty(this,Symbol.toStringTag,{value:this.getName(),configurable:!0,writable:!0,enumerable:!1}),$&"
}
}
],
startAt: StartAt.Init, startAt: StartAt.Init,
start() { start() {
const shortcuts = makeShortcuts(); const shortcuts = makeShortcuts();

View file

@ -33,11 +33,11 @@ function getEmojiMarkdown(target: Target, copyUnicode: boolean): string {
: `:${emojiName}:`; : `:${emojiName}:`;
} }
const extension = target?.firstChild.src.match( const url = new URL(target.firstChild.src);
/https:\/\/cdn\.discordapp\.com\/emojis\/\d+\.(\w+)/ const hasParam = url.searchParams.get("animated") === "true";
)?.[1]; const isGif = url.pathname.endsWith(".gif");
return `<${extension === "gif" ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`; return `<${(hasParam || isGif) ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
} }
const settings = definePluginSettings({ const settings = definePluginSettings({
@ -55,7 +55,7 @@ export default definePlugin({
settings, settings,
contextMenus: { contextMenus: {
"expression-picker"(children, { target }: { target: Target }) { "expression-picker"(children, { target }: { target: Target; }) {
if (target.dataset.type !== "emoji") return; if (target.dataset.type !== "emoji") return;
children.push( children.push(

View file

@ -42,10 +42,11 @@ export default definePlugin({
// Only one of the two patches will be at effect; Discord often updates to switch between them. // Only one of the two patches will be at effect; Discord often updates to switch between them.
// See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673 // See: https://discord.com/channels/1015060230222131221/1032770730703716362/1261398512017477673
{ {
find: ".ENTER&&(!", find: ".selectPreviousCommandOption(",
replacement: { replacement: {
match: /(?<=(\i)\.which===\i\.\i.ENTER&&).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/, // FIXME(Bundler change related): Remove old compatiblity once enough time has passed
replace: "$self.shouldSubmit($1, $2)" match: /(?<=(\i)\.which(?:!==|===)\i\.\i.ENTER(\|\||&&)).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=(?:\|\||&&)\(\i\.preventDefault)/,
replace: (_, event, condition, codeblock) => `${condition === "||" ? "!" : ""}$self.shouldSubmit(${event},${codeblock})`
} }
}, },
{ {

View file

@ -19,6 +19,7 @@
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings"; import { getUserSettingLazy } from "@api/UserSettings";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards"; import { isTruthy } from "@utils/guards";
@ -27,15 +28,14 @@ import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack"; import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color"); const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile"); const ActivityView = findComponentByCodeLazy(".party?(0", ".card");
const ShowCurrentGame = getUserSettingLazy<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)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0]; return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
} }
@ -169,7 +169,7 @@ const settings = definePluginSettings({
value: TimestampMode.NOW value: TimestampMode.NOW
}, },
{ {
label: "Same as your current time", label: "Same as your current time (not reset after 24h)",
value: TimestampMode.TIME value: TimestampMode.TIME
}, },
{ {
@ -269,6 +269,7 @@ function isStreamLinkDisabled() {
function isStreamLinkValid(value: string) { function isStreamLinkValid(value: string) {
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL."; if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
if (value && value.length > 512) return "Streaming link must be not longer than 512 characters.";
return true; return true;
} }
@ -277,8 +278,9 @@ function isTimestampDisabled() {
} }
function isImageKeyValid(value: string) { function isImageKeyValid(value: string) {
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)"; if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//.test(value)) return "Don't use a Discord link. Use an Imgur image link instead.";
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)"; if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image (e.g. https://i.imgur.com/...). Right click the image and click 'Copy image address'";
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image (e.g. https://media.tenor.com/...). Right click the GIF and click 'Copy image address'";
return true; return true;
} }
@ -390,13 +392,24 @@ async function setRpc(disable?: boolean) {
export default definePlugin({ export default definePlugin({
name: "CustomRPC", name: "CustomRPC",
description: "Allows you to set a custom rich presence.", description: "Add a fully customisable Rich Presence (Game status) to your Discord profile",
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev], authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
dependencies: ["UserSettingsAPI"], dependencies: ["UserSettingsAPI"],
start: setRpc, start: setRpc,
stop: () => setRpc(true), stop: () => setRpc(true),
settings, settings,
patches: [
{
find: ".party?(0",
all: true,
replacement: {
match: /\i\.id===\i\.id\?null:/,
replace: ""
}
}
],
settingsAboutComponent: () => { settingsAboutComponent: () => {
const activity = useAwaiter(createActivity); const activity = useAwaiter(createActivity);
const gameActivityEnabled = ShowCurrentGame.useSetting(); const gameActivityEnabled = ShowCurrentGame.useSetting();
@ -410,7 +423,7 @@ export default definePlugin({
style={{ padding: "1em" }} style={{ padding: "1em" }}
> >
<Forms.FormTitle>Notice</Forms.FormTitle> <Forms.FormTitle>Notice</Forms.FormTitle>
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText> <Forms.FormText>Activity Sharing isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
<Button <Button
color={Button.Colors.TRANSPARENT} color={Button.Colors.TRANSPARENT}
@ -422,24 +435,33 @@ export default definePlugin({
</ErrorCard> </ErrorCard>
)} )}
<Flex flexDirection="column" style={{ gap: ".5em" }} className={Margins.top16}>
<Forms.FormText> <Forms.FormText>
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and Go to the <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID. get the application ID.
</Forms.FormText> </Forms.FormText>
<Forms.FormText> <Forms.FormText>
Upload images in the Rich Presence tab to get the image keys. Upload images in the Rich Presence tab to get the image keys.
</Forms.FormText> </Forms.FormText>
<Forms.FormText> <Forms.FormText>
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address". If you want to use an image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and selecting "Copy image address".
</Forms.FormText> </Forms.FormText>
<Forms.FormText>
You can't see your own buttons on your profile, but everyone else can see it fine.
</Forms.FormText>
<Forms.FormText>
Some weird unicode text ("fonts" 𝖑𝖎𝖐𝖊 𝖙𝖍𝖎𝖘) may cause the rich presence to not show up, try using normal letters instead.
</Forms.FormText>
</Flex>
<Forms.FormDivider className={Margins.top8} /> <Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle, padding: 8, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}> <div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
{activity[0] && <ActivityComponent activity={activity[0]} channelId={SelectedChannelStore.getChannelId()} {activity[0] && <ActivityView
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())} activity={activity[0]}
application={{ id: settings.store.appID }} user={UserStore.getCurrentUser()}
user={UserStore.getCurrentUser()} />} currentUser={UserStore.getCurrentUser()}
/>}
</div> </div>
</> </>
); );

View file

@ -121,6 +121,7 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
height="24px" height="24px"
viewBox="0 0 36 36" viewBox="0 0 36 36"
aria-label="Toggle Dearrow" aria-label="Toggle Dearrow"
className="vc-dearrow-icon"
> >
<path <path
fill="#1213BD" fill="#1213BD"

View file

@ -1,4 +1,4 @@
.vc-dearrow-toggle-off svg { .vc-dearrow-toggle-off .vc-dearrow-icon {
filter: grayscale(1); filter: grayscale(1);
} }

View file

@ -17,7 +17,6 @@ import DecorSection from "./ui/components/DecorSection";
export const settings = definePluginSettings({ export const settings = definePluginSettings({
changeDecoration: { changeDecoration: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "Change your avatar decoration",
component() { component() {
if (!Vencord.Plugins.plugins.Decor.started) return <Forms.FormText> if (!Vencord.Plugins.plugins.Decor.started) return <Forms.FormText>
Enable Decor and restart your client to change your avatar decoration. Enable Decor and restart your client to change your avatar decoration.

View file

@ -5,6 +5,7 @@
*/ */
import { React } from "@webpack/common"; import { React } from "@webpack/common";
import { JSX } from "react";
import { cl } from "../"; import { cl } from "../";

View file

@ -7,6 +7,7 @@
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { React } from "@webpack/common"; import { React } from "@webpack/common";
import { JSX } from "react";
import { cl } from "../"; import { cl } from "../";
import Grid, { GridProps } from "./Grid"; import Grid, { GridProps } from "./Grid";

View file

@ -16,11 +16,9 @@
* 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 { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
migratePluginSettings("DisableCallIdle", "DisableDMCallIdle");
export default definePlugin({ export default definePlugin({
name: "DisableCallIdle", name: "DisableCallIdle",
description: "Disables automatically getting kicked from a DM voice call after 3 minutes and being moved to an AFK voice channel.", description: "Disables automatically getting kicked from a DM voice call after 3 minutes and being moved to an AFK voice channel.",

View file

@ -211,7 +211,7 @@ function CloneModal({ data }: { data: Sticker | Emoji; }) {
alignItems: "center" alignItems: "center"
}}> }}>
{guilds.map(g => ( {guilds.map(g => (
<Tooltip text={g.name}> <Tooltip key={g.id} text={g.name}>
{({ onMouseLeave, onMouseEnter }) => ( {({ onMouseLeave, onMouseEnter }) => (
<div <div
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
@ -310,7 +310,8 @@ function buildMenuItem(type: "Emoji" | "Sticker", fetchData: () => Promisable<Om
} }
function isGifUrl(url: string) { function isGifUrl(url: string) {
return new URL(url).pathname.endsWith(".gif"); const u = new URL(url);
return u.pathname.endsWith(".gif") || u.searchParams.get("animated") === "true";
} }
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => {

View file

@ -16,7 +16,7 @@
* 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 { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; import { addMessagePreEditListener, addMessagePreSendListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
@ -207,8 +207,8 @@ function makeBypassPatches(): Omit<Patch, "plugin"> {
return { return {
find: "canUseCustomStickersEverywhere:", find: "canUseCustomStickersEverywhere:",
replacement: mapping.map(({ func, predicate }) => ({ replacement: mapping.map(({ func, predicate }) => ({
match: new RegExp(String.raw`(?<=${func}:function\(\i(?:,\i)?\){)`), match: new RegExp(String.raw`(?<=${func}:)\i`),
replace: "return true;", replace: "() => true",
predicate predicate
})) }))
}; };
@ -235,7 +235,7 @@ export default definePlugin({
} }
}, },
{ {
find: ".PREMIUM_LOCKED;", find: ".GUILD_SUBSCRIPTION_UNAVAILABLE;",
group: true, group: true,
predicate: () => settings.store.enableEmojiBypass, predicate: () => settings.store.enableEmojiBypass,
replacement: [ replacement: [
@ -256,8 +256,11 @@ export default definePlugin({
}, },
{ {
// Disallow the emoji for premium locked if the intention doesn't allow it // Disallow the emoji for premium locked if the intention doesn't allow it
match: /!\i\.\i\.canUseEmojisEverywhere\(\i\)/, // FIXME(Bundler change related): Remove old compatiblity once enough time has passed
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})` match: /(!)?(\i\.\i\.canUseEmojisEverywhere\(\i\))/,
replace: (m, not) => not
? `(${m}&&!${IS_BYPASSEABLE_INTENTION})`
: `(${m}||${IS_BYPASSEABLE_INTENTION})`
}, },
{ {
// Allow animated emojis to be used if the intention allows it // Allow animated emojis to be used if the intention allows it
@ -297,8 +300,8 @@ export default definePlugin({
replacement: [ replacement: [
{ {
// Overwrite incoming connection settings proto with our local settings // Overwrite incoming connection settings proto with our local settings
match: /CONNECTION_OPEN:function\((\i)\){/, match: /function (\i)\((\i)\){(?=.*CONNECTION_OPEN:\1)/,
replace: (m, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);` replace: (m, funcName, props) => `${m}$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`
}, },
{ {
// Overwrite non local proto changes with our local settings // Overwrite non local proto changes with our local settings
@ -391,7 +394,7 @@ export default definePlugin({
}, },
// Separate patch for allowing using custom app icons // Separate patch for allowing using custom app icons
{ {
find: /\.getCurrentDesktopIcon.{0,25}\.isPremium/, find: "?24:30,",
replacement: { replacement: {
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/, match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true" replace: "true"
@ -514,7 +517,7 @@ export default definePlugin({
return array.filter(item => item != null); return array.filter(item => item != null);
}, },
ensureChildrenIsArray(child: ReactElement) { ensureChildrenIsArray(child: ReactElement<any>) {
if (!Array.isArray(child.props.children)) child.props.children = [child.props.children]; if (!Array.isArray(child.props.children)) child.props.children = [child.props.children];
}, },
@ -524,7 +527,7 @@ export default definePlugin({
let nextIndex = content.length; let nextIndex = content.length;
const transformLinkChild = (child: ReactElement) => { const transformLinkChild = (child: ReactElement<any>) => {
if (settings.store.transformEmojis) { if (settings.store.transformEmojis) {
const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex); const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex);
if (fakeNitroMatch) { if (fakeNitroMatch) {
@ -558,7 +561,7 @@ export default definePlugin({
return child; return child;
}; };
const transformChild = (child: ReactElement) => { const transformChild = (child: ReactElement<any>) => {
if (child?.props?.trusted != null) return transformLinkChild(child); if (child?.props?.trusted != null) return transformLinkChild(child);
if (child?.props?.children != null) { if (child?.props?.children != null) {
if (!Array.isArray(child.props.children)) { if (!Array.isArray(child.props.children)) {
@ -574,7 +577,7 @@ export default definePlugin({
return child; return child;
}; };
const modifyChild = (child: ReactElement) => { const modifyChild = (child: ReactElement<any>) => {
const newChild = transformChild(child); const newChild = transformChild(child);
if (newChild?.type === "ul" || newChild?.type === "ol") { if (newChild?.type === "ul" || newChild?.type === "ol") {
@ -601,7 +604,7 @@ export default definePlugin({
return newChild; return newChild;
}; };
const modifyChildren = (children: Array<ReactElement>) => { const modifyChildren = (children: Array<ReactElement<any>>) => {
for (const [index, child] of children.entries()) children[index] = modifyChild(child); for (const [index, child] of children.entries()) children[index] = modifyChild(child);
children = this.clearEmptyArrayItems(children); children = this.clearEmptyArrayItems(children);
@ -853,7 +856,7 @@ export default definePlugin({
}); });
} }
this.preSend = addPreSendListener(async (channelId, messageObj, extra) => { this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => {
const { guildId } = this; const { guildId } = this;
let hasBypass = false; let hasBypass = false;
@ -941,7 +944,7 @@ export default definePlugin({
return { cancel: false }; return { cancel: false };
}); });
this.preEdit = addPreEditListener(async (channelId, __, messageObj) => { this.preEdit = addMessagePreEditListener(async (channelId, __, messageObj) => {
if (!s.enableEmojiBypass) return; if (!s.enableEmojiBypass) return;
let hasBypass = false; let hasBypass = false;
@ -973,7 +976,7 @@ export default definePlugin({
}, },
stop() { stop() {
removePreSendListener(this.preSend); removeMessagePreSendListener(this.preSend);
removePreEditListener(this.preEdit); removeMessagePreEditListener(this.preEdit);
} }
}); });

View file

@ -29,6 +29,7 @@ import definePlugin, { OptionType } from "@utils/types";
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack"; import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common"; import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import { ReactElement } from "react";
import virtualMerge from "virtual-merge"; import virtualMerge from "virtual-merge";
interface UserProfile extends User { interface UserProfile extends User {
@ -87,7 +88,7 @@ const settings = definePluginSettings({
interface ColorPickerProps { interface ColorPickerProps {
color: number | null; color: number | null;
label: React.ReactElement; label: ReactElement<any>;
showEyeDropper?: boolean; showEyeDropper?: boolean;
suggestedColors?: string[]; suggestedColors?: string[];
onChange(value: number | null): void; onChange(value: number | null): void;

View file

@ -57,7 +57,7 @@ export default definePlugin({
{ {
// https://regex101.com/r/x2mobQ/1 // https://regex101.com/r/x2mobQ/1
// searchEmojis(...,maxCount: stuff) ... endEmojis = emojis.slice(0, maxCount - gifResults.length) // searchEmojis(...,maxCount: stuff) ... endEmojis = emojis.slice(0, maxCount - gifResults.length)
match: /,maxCount:(\i)(.{1,500}\i)=(\i)\.slice\(0,(\i-\i\.length)\)/, match: /,maxCount:(\i)(.{1,500}\i)=(\i)\.slice\(0,(Math\.max\(\i,\i(?:-\i\.length){2}\))\)/,
// ,maxCount:Infinity ... endEmojis = (emojis.sliceTo = n, emojis) // ,maxCount:Infinity ... endEmojis = (emojis.sliceTo = n, emojis)
replace: ",maxCount:Infinity$2=($3.sliceTo = $4, $3)" replace: ",maxCount:Infinity$2=($3.sliceTo = $4, $3)"
} }

View file

@ -13,7 +13,7 @@ export default definePlugin({
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
patches: [ patches: [
{ {
find: "getFormatQuality(){", find: ".handleImageLoad)",
replacement: { replacement: {
match: /(?<=null;return )\i\.\i&&\(\i\|\|!\i\.isAnimated.+?:(?=\i&&\(\i="png"\))/, match: /(?<=null;return )\i\.\i&&\(\i\|\|!\i\.isAnimated.+?:(?=\i&&\(\i="png"\))/,
replace: "" replace: ""

View file

@ -27,7 +27,7 @@ export default definePlugin({
authors: [Devs.D3SOX, Devs.Nickyux], authors: [Devs.D3SOX, Devs.Nickyux],
patches: [ patches: [
{ {
find: "#{intl::GUILD_OWNER}", find: "#{intl::GUILD_OWNER}),children:",
replacement: { replacement: {
match: /,isOwner:(\i),/, match: /,isOwner:(\i),/,
replace: ",_isOwner:$1=$self.isGuildOwner(e)," replace: ",_isOwner:$1=$self.isGuildOwner(e),"

View file

@ -22,11 +22,11 @@ import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord"; import { getIntlMessage } from "@utils/discord";
import { NoopComponent } from "@utils/react"; import { NoopComponent } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { filters, findByPropsLazy, waitFor } from "@webpack"; import { filters, findByCodeLazy, waitFor } from "@webpack";
import { ChannelStore, ContextMenuApi, UserStore } from "@webpack/common"; import { ChannelStore, ContextMenuApi, UserStore } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
const { useMessageMenu } = findByPropsLazy("useMessageMenu"); const useMessageMenu = findByCodeLazy(".MESSAGE,commandTargetId:");
interface CopyIdMenuItemProps { interface CopyIdMenuItemProps {
id: string; id: string;

View file

@ -0,0 +1,9 @@
# Full User In Chatbox
Adds the full user mention to the textbox
Adds the avatar if you have mentioned avatars enabled
Provides the full context menu to make it easy to access the users profile, and other common actions
https://github.com/user-attachments/assets/cd9edb33-99c8-4c8d-b669-8cddd05f4b45

View file

@ -0,0 +1,53 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { UserStore, useStateFromStores } from "@webpack/common";
import { ReactNode } from "react";
const UserMentionComponent = findComponentByCodeLazy(".USER_MENTION)");
interface UserMentionComponentProps {
id: string;
channelId: string;
guildId: string;
originalComponent: () => ReactNode;
}
export default definePlugin({
name: "FullUserInChatbox",
description: "Makes the user mention in the chatbox have more functionalities, like left/right clicking",
authors: [Devs.sadan],
patches: [
{
find: ':"text":',
replacement: {
match: /(hidePersonalInformation\).+?)(if\(null!=\i\){.+?return \i)(?=})/,
replace: "$1return $self.UserMentionComponent({...arguments[0],originalComponent:()=>{$2}});"
}
}
],
UserMentionComponent: ErrorBoundary.wrap((props: UserMentionComponentProps) => {
const user = useStateFromStores([UserStore], () => UserStore.getUser(props.id));
if (user == null) {
return props.originalComponent();
}
return <UserMentionComponent
// This seems to be constant
className="mention"
userId={props.id}
channelId={props.channelId}
/>;
}, {
fallback: ({ wrappedProps: { originalComponent } }) => originalComponent()
})
});

View file

@ -17,16 +17,15 @@
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { getUserSettingLazy } from "@api/UserSettings"; 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";
import { findComponentByCodeLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import style from "./style.css?managed"; import managedStyle from "./style.css?managed";
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:"); const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
@ -90,6 +89,8 @@ export default definePlugin({
dependencies: ["UserSettingsAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,
managedStyle,
patches: [ patches: [
{ {
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}", find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
@ -102,11 +103,4 @@ export default definePlugin({
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }), GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
}); });

View file

@ -16,80 +16,92 @@
* 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 "./styles.css";
import { get, set } from "@api/DataStore"; import { get, set } from "@api/DataStore";
import { addButton, removeButton } from "@api/MessagePopover"; import { updateMessage } from "@api/MessageUpdater";
import { migratePluginSettings } from "@api/Settings";
import { ImageInvisible, ImageVisible } from "@components/Icons"; import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { ChannelStore } from "@webpack/common"; import { ChannelStore } from "@webpack/common";
import { MessageSnapshot } from "@webpack/types";
let style: HTMLStyleElement;
const KEY = "HideAttachments_HiddenIds"; const KEY = "HideAttachments_HiddenIds";
let hiddenMessages: Set<string> = new Set(); let hiddenMessages = new Set<string>();
const getHiddenMessages = () => get(KEY).then(set => {
hiddenMessages = set ?? new Set<string>(); async function getHiddenMessages() {
hiddenMessages = await get(KEY) ?? new Set();
return hiddenMessages; return hiddenMessages;
}); }
const saveHiddenMessages = (ids: Set<string>) => set(KEY, ids); const saveHiddenMessages = (ids: Set<string>) => set(KEY, ids);
migratePluginSettings("HideMedia", "HideAttachments");
export default definePlugin({ export default definePlugin({
name: "HideAttachments", name: "HideMedia",
description: "Hide attachments and Embeds for individual messages via hover button", description: "Hide attachments and embeds for individual messages via hover button",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["MessagePopoverAPI"], dependencies: ["MessageUpdaterAPI"],
async start() { patches: [{
style = document.createElement("style"); find: "this.renderAttachments(",
style.id = "VencordHideAttachments"; replacement: {
document.head.appendChild(style); match: /(?<=\i=)this\.render(?:Attachments|Embeds|StickersAccessories)\((\i)\)/g,
replace: "$self.shouldHide($1?.id)?null:$&"
}
}],
await getHiddenMessages(); renderMessagePopoverButton(msg) {
await this.buildCss(); // @ts-ignore - discord-types lags behind discord.
const hasAttachmentsInShapshots = msg.messageSnapshots.some(
(snapshot: MessageSnapshot) => snapshot?.message.attachments.length
);
addButton("HideAttachments", msg => { if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length && !hasAttachmentsInShapshots) return null;
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
const isHidden = hiddenMessages.has(msg.id); const isHidden = hiddenMessages.has(msg.id);
return { return {
label: isHidden ? "Show Attachments" : "Hide Attachments", label: isHidden ? "Show Media" : "Hide Media",
icon: isHidden ? ImageVisible : ImageInvisible, icon: isHidden ? ImageVisible : ImageInvisible,
message: msg, message: msg,
channel: ChannelStore.getChannel(msg.channel_id), channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => this.toggleHide(msg.id) onClick: () => this.toggleHide(msg.channel_id, msg.id)
}; };
}); },
renderMessageAccessory({ message }) {
if (!this.shouldHide(message.id)) return null;
return (
<span className={classes("vc-hideAttachments-accessory", !message.content && "vc-hideAttachments-no-content")}>
Media Hidden
</span>
);
},
async start() {
await getHiddenMessages();
}, },
stop() { stop() {
style.remove();
hiddenMessages.clear(); hiddenMessages.clear();
removeButton("HideAttachments");
}, },
async buildCss() { shouldHide(messageId: string) {
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(","); return hiddenMessages.has(messageId);
style.textContent = `
:is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) {
/* important is not necessary, but add it to make sure bad themes won't break it */
display: none !important;
}
:is(${elements})::after {
content: "Attachments hidden";
color: var(--text-muted);
font-size: 80%;
}
`;
}, },
async toggleHide(id: string) { async toggleHide(channelId: string, messageId: string) {
const ids = await getHiddenMessages(); const ids = await getHiddenMessages();
if (!ids.delete(id)) if (!ids.delete(messageId))
ids.add(id); ids.add(messageId);
await saveHiddenMessages(ids); await saveHiddenMessages(ids);
await this.buildCss(); updateMessage(channelId, messageId);
} }
}); });

View file

@ -0,0 +1,10 @@
.vc-hideAttachments-accessory {
color: var(--text-muted);
margin-top: 0.5em;
font-style: italic;
font-weight: 400;
}
.vc-hideAttachments-no-content {
margin-top: 0;
}

View file

@ -27,7 +27,7 @@ export default definePlugin({
{ {
find: "hasFlag:{writable", find: "hasFlag:{writable",
replacement: { replacement: {
match: /if\((\i)<=(?:1<<30|1073741824)\)return/, match: /if\((\i)<=(?:0x40000000|(?:1<<30|1073741824))\)return/,
replace: "if($1===(1<<20))return false;$&", replace: "if($1===(1<<20))return false;$&",
}, },
}, },

Some files were not shown because too many files have changed in this diff Show more