Merge branch 'dev' into patcher-rewrite

This commit is contained in:
Nuckyz 2025-01-29 02:21:43 -03:00
commit 172c372e40
No known key found for this signature in database
GPG key ID: 440BF8296E1C4AD9
48 changed files with 591 additions and 652 deletions

View file

@ -9,7 +9,7 @@ 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, JSX, MouseEventHandler, ReactNode } from "react"; import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
@ -110,7 +110,7 @@ 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}`}

View file

@ -122,7 +122,7 @@ 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<any> | null>; children: Array<ReactElement<any> | null>;
"aria-label": string; "aria-label": string;
@ -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);
} }

View file

@ -222,6 +222,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

@ -83,14 +83,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

@ -28,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";
@ -144,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"
@ -159,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"
@ -183,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,15 @@ 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")/, match: /return\((!)?\i\.\i(?:\|\||&&)(?=\(\i\.isDM.+?(\i)\.push)/,
replace: "$&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)&&" 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

@ -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

@ -65,7 +65,7 @@ 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(?=})/, 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

@ -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
{ {

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

@ -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/,

View file

@ -69,8 +69,8 @@ export default definePlugin({
{ {
find: "https://github.com/highlightjs/highlight.js/issues/2277", find: "https://github.com/highlightjs/highlight.js/issues/2277",
replacement: { replacement: {
match: /(?<=&&\()console.log\(`Deprecated.+?`\),/, match: /\(console.log\(`Deprecated.+?`\),/,
replace: "" replace: "("
} }
}, },
{ {

View file

@ -63,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];
} }
@ -165,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;
} }

View file

@ -42,10 +42,10 @@ 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)/, match: /(?<=(\i)\.which(?:!==|===)\i\.\i.ENTER(\|\||&&)).{0,100}(\(0,\i\.\i\)\(\i\)).{0,100}(?=(?:\|\||&&)\(\i\.preventDefault)/,
replace: "$self.shouldSubmit($1, $2)" replace: (_, event, condition, codeblock) => `${condition === "||" ? "!" : ""}$self.shouldSubmit(${event},${codeblock})`
} }
}, },
{ {

View file

@ -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,10 @@ 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\)/, match: /(!)?(\i\.\i\.canUseEmojisEverywhere\(\i\))/,
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})` 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

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

@ -26,7 +26,7 @@ import { findComponentByCodeLazy } from "@webpack";
import style from "./style.css?managed"; import style 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")!;

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;$&",
}, },
}, },

View file

@ -243,7 +243,7 @@ export default definePlugin({
find: '"LocalActivityStore"', find: '"LocalActivityStore"',
replacement: [ replacement: [
{ {
match: /HANG_STATUS.+?(?=!\i\(\)\(\i,\i\)&&)(?<=(\i)\.push.+?)/, match: /HANG_STATUS.+?(?=!?\i\(\)\(\i,\i\))(?<=(\i)\.push.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);` replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
} }
] ]

View file

@ -50,7 +50,7 @@ export default definePlugin({
{ {
find: "#{intl::FRIENDS_SECTION_ONLINE}", find: "#{intl::FRIENDS_SECTION_ONLINE}",
replacement: { replacement: {
match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.string\(\i\.\i#{intl::BLOCKED}\)\}\)/, match: /(\(0,\i\.jsx\)\(\i\.\i\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.string\(\i\.\i#{intl::BLOCKED}\)\}\)/,
replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&" replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&"
}, },
}, },

View file

@ -0,0 +1,17 @@
# IrcColors
Makes username colors in chat unique, like in IRC clients
![Chat with IrcColors and Compact++ enabled](https://github.com/Vendicated/Vencord/assets/33988779/88e05c0b-a60a-4d10-949e-8b46e1d7226c)
Improves chat readability by assigning every user an unique nickname color,
making distinguishing between different users easier. Inspired by the feature
in many IRC clients, such as HexChat or WeeChat.
Keep in mind this overrides role colors in chat, so if you wish to know
someone's role color without checking their profile, enable the role dot: go to
**User Settings**, **Accessibility** and switch **Role Colors** to **Show role
colors next to names**.
Created for use with the [Compact++](https://gitlab.com/Grzesiek11/compactplusplus-discord-theme)
theme.

View file

@ -0,0 +1,85 @@
/*
* 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 { definePluginSettings } from "@api/Settings";
import { hash as h64 } from "@intrnl/xxhash64";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { useMemo } from "@webpack/common";
// Calculate a CSS color string based on the user ID
function calculateNameColorForUser(id: string) {
const { lightness } = settings.use(["lightness"]);
const idHash = useMemo(() => h64(id), [id]);
return `hsl(${idHash % 360n}, 100%, ${lightness}%)`;
}
const settings = definePluginSettings({
lightness: {
description: "Lightness, in %. Change if the colors are too light or too dark",
type: OptionType.NUMBER,
default: 70,
},
memberListColors: {
description: "Replace role colors in the member list",
restartNeeded: true,
type: OptionType.BOOLEAN,
default: true
}
});
export default definePlugin({
name: "IrcColors",
description: "Makes username colors in chat unique, like in IRC clients",
authors: [Devs.Grzesiek11],
settings,
patches: [
{
find: '="SYSTEM_TAG"',
replacement: {
match: /(?<=className:\i\.username,style:.{0,50}:void 0,)/,
replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])},"
}
},
{
find: "#{intl::GUILD_OWNER}),children:",
replacement: {
match: /(?<=\.MEMBER_LIST}\),\[\]\),)(.+?color:)null!=.{0,50}?(?=,)/,
replace: (_, rest) => `ircColor=$self.calculateNameColorForListContext(arguments[0]),${rest}ircColor`
},
predicate: () => settings.store.memberListColors
}
],
calculateNameColorForMessageContext(context: any) {
const id = context?.message?.author?.id;
if (id == null) {
return null;
}
return calculateNameColorForUser(id);
},
calculateNameColorForListContext(context: any) {
const id = context?.user?.id;
if (id == null) {
return null;
}
return calculateNameColorForUser(id);
}
});

View file

@ -86,7 +86,7 @@ const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
const logger = new Logger("LastFMRichPresence"); const logger = new Logger("LastFMRichPresence");
const presenceStore = findByPropsLazy("getLocalPresence"); const PresenceStore = findByPropsLazy("getLocalPresence");
async function getApplicationAsset(key: string): Promise<string> { async function getApplicationAsset(key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
@ -124,6 +124,11 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true, default: true,
}, },
hideWithActivity: {
description: "Hide Last.fm presence if you have any other presence",
type: OptionType.BOOLEAN,
default: false,
},
statusName: { statusName: {
description: "custom status text", description: "custom status text",
type: OptionType.STRING, type: OptionType.STRING,
@ -274,13 +279,17 @@ export default definePlugin({
}, },
async getActivity(): Promise<Activity | null> { async getActivity(): Promise<Activity | null> {
if (settings.store.hideWithSpotify) { if (settings.store.hideWithActivity) {
for (const activity of presenceStore.getActivities()) { if (PresenceStore.getActivities().some(a => a.application_id !== applicationId)) {
if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) {
// there is already music status because of Spotify or richerCider (probably more)
return null; return null;
} }
} }
if (settings.store.hideWithSpotify) {
if (PresenceStore.getActivities().some(a => a.type === ActivityType.LISTENING && a.application_id !== applicationId)) {
// there is already music status because of Spotify or richerCider (probably more)
return null;
}
} }
const trackData = await this.fetchTrackData(); const trackData = await this.fetchTrackData();

View file

@ -57,7 +57,7 @@ export default definePlugin({
{ {
find: ".ROLE_MENTION)", find: ".ROLE_MENTION)",
replacement: { replacement: {
match: /children:\[\i&&.{0,50}\.RoleDot.{0,300},\i(?=\])/, match: /children:\[\i&&.{0,100}className:\i.roleDot,.{0,200},\i(?=\])/,
replace: "$&,$self.renderRoleIcon(arguments[0])" replace: "$&,$self.renderRoleIcon(arguments[0])"
} }
}], }],

View file

@ -9,7 +9,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards"; import { isNonNullish } from "@utils/guards";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findExportedComponentLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { SnowflakeUtils, Tooltip } from "@webpack/common"; import { SnowflakeUtils, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
@ -26,7 +26,7 @@ interface Diff {
} }
const DISCORD_KT_DELAY = 1471228928; const DISCORD_KT_DELAY = 1471228928;
const HiddenVisually = findExportedComponentLazy("HiddenVisually"); const HiddenVisually = findComponentByCodeLazy(".hiddenVisually]:");
export default definePlugin({ export default definePlugin({
name: "MessageLatency", name: "MessageLatency",
@ -162,7 +162,7 @@ export default definePlugin({
</> </>
} }
</Tooltip>; </Tooltip>;
}); }, { noop: true });
}, },
Icon({ delta, fill, props }: { Icon({ delta, fill, props }: {

View file

@ -501,7 +501,7 @@ export default definePlugin({
{ {
// Message context base menu // Message context base menu
find: "useMessageMenu:", find: ".MESSAGE,commandTargetId:",
replacement: [ replacement: [
{ {
// Remove the first section if message is deleted // Remove the first section if message is deleted

View file

@ -1,372 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack";
import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip } from "@webpack/common";
import type { Permissions, RC } from "@webpack/types";
import type { Channel, Guild, Message, User } from "discord-types/general";
interface Tag {
// name used for identifying, must be alphanumeric + underscores
name: string;
// name shown on the tag itself, can be anything probably; automatically uppercase'd
displayName: string;
description: string;
permissions?: Permissions[];
condition?(message: Message | null, user: User, channel: Channel): boolean;
}
interface TagSetting {
text: string;
showInChat: boolean;
showInNotChat: boolean;
}
interface TagSettings {
WEBHOOK: TagSetting,
OWNER: TagSetting,
ADMINISTRATOR: TagSetting,
MODERATOR_STAFF: TagSetting,
MODERATOR: TagSetting,
VOICE_MODERATOR: TagSetting,
TRIAL_MODERATOR: TagSetting,
[k: string]: TagSetting;
}
// PermissionStore.computePermissions will not work here since it only gets permissions for the current user
const computePermissions: (options: {
user?: { id: string; } | string | null;
context?: Guild | Channel | null;
overwrites?: Channel["permissionOverwrites"] | null;
checkElevated?: boolean /* = true */;
excludeGuildPermissions?: boolean /* = false */;
}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()");
const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number, className?: string, useRemSizes?: boolean; }> & { Types: Record<string, number>; };
const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
const tags: Tag[] = [
{
name: "WEBHOOK",
displayName: "Webhook",
description: "Messages sent by webhooks",
condition: isWebhook
}, {
name: "OWNER",
displayName: "Owner",
description: "Owns the server",
condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id
}, {
name: "ADMINISTRATOR",
displayName: "Admin",
description: "Has the administrator permission",
permissions: ["ADMINISTRATOR"]
}, {
name: "MODERATOR_STAFF",
displayName: "Staff",
description: "Can manage the server, channels or roles",
permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"]
}, {
name: "MODERATOR",
displayName: "Mod",
description: "Can manage messages or kick/ban people",
permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"]
}, {
name: "VOICE_MODERATOR",
displayName: "VC Mod",
description: "Can manage voice chats",
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
}, {
name: "CHAT_MODERATOR",
displayName: "Chat Mod",
description: "Can timeout people",
permissions: ["MODERATE_MEMBERS"]
}
];
const defaultSettings = Object.fromEntries(
tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }])
) as TagSettings;
function SettingsComponent() {
const tagSettings = settings.store.tagSettings ??= defaultSettings;
return (
<Flex flexDirection="column">
{tags.map(t => (
<Card key={t.name} style={{ padding: "1em 1em 0" }}>
<Forms.FormTitle style={{ width: "fit-content" }}>
<Tooltip text={t.description}>
{({ onMouseEnter, onMouseLeave }) => (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{t.displayName} Tag <Tag type={Tag.Types[t.name]} />
</div>
)}
</Tooltip>
</Forms.FormTitle>
<TextInput
type="text"
value={tagSettings[t.name]?.text ?? t.displayName}
placeholder={`Text on tag (default: ${t.displayName})`}
onChange={v => tagSettings[t.name].text = v}
className={Margins.bottom16}
/>
<Switch
value={tagSettings[t.name]?.showInChat ?? true}
onChange={v => tagSettings[t.name].showInChat = v}
hideBorder
>
Show in messages
</Switch>
<Switch
value={tagSettings[t.name]?.showInNotChat ?? true}
onChange={v => tagSettings[t.name].showInNotChat = v}
hideBorder
>
Show in member list and profiles
</Switch>
</Card>
))}
</Flex>
);
}
const settings = definePluginSettings({
dontShowForBots: {
description: "Don't show extra tags for bots (excluding webhooks)",
type: OptionType.BOOLEAN
},
dontShowBotTag: {
description: "Only show extra tags for bots / Hide [BOT] text",
type: OptionType.BOOLEAN
},
tagSettings: {
type: OptionType.COMPONENT,
component: SettingsComponent,
description: "fill me"
}
});
export default definePlugin({
name: "MoreUserTags",
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN],
settings,
patches: [
// add tags to the tag list
{
find: ".ORIGINAL_POSTER=",
replacement: {
match: /(?=(\i)\[\i\.BOT)/,
replace: "$self.genTagTypes($1);"
}
},
{
find: "#{intl::DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP_OFFICIAL}",
replacement: [
// make the tag show the right text
{
match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(.{0,40}#{intl::APP_TAG}\))/,
replace: (_, origSwitch, variant, tags, displayedText, originalText) =>
`${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}],${originalText})}`
},
// show OP tags correctly
{
match: /(\i)=(\i)===\i(?:\.\i)?\.ORIGINAL_POSTER/,
replace: "$1=$self.isOPTag($2)"
},
// add HTML data attributes (for easier theming)
{
match: /.botText,children:(\i)}\)]/,
replace: "$&,'data-tag':$1.toLowerCase()"
}
],
},
// in messages
{
find: ".Types.ORIGINAL_POSTER",
replacement: {
match: /;return\((\(null==\i\?void 0:\i\.isSystemDM\(\).+?.Types.ORIGINAL_POSTER\)),null==(\i)\)/,
replace: ";$1;$2=$self.getTag({...arguments[0],origType:$2,location:'chat'});return $2 == null"
}
},
// in the member list
{
find: "#{intl::GUILD_OWNER}),children:",
replacement: {
match: /(?<type>\i)=\(null==.{0,100}\.BOT;return null!=(?<user>\i)&&\i\.bot/,
replace: "$<type> = $self.getTag({user: $<user>, channel: arguments[0].channel, origType: $<user>.bot ? 0 : null, location: 'not-chat' }); return typeof $<type> === 'number'"
}
},
// pass channel id down props to be used in profiles
{
find: ".hasAvatarForGuild(null==",
replacement: {
match: /(?=usernameIcon:)/,
replace: "moreTags_channelId:arguments[0].channelId,"
}
},
{
find: "#{intl::USER_PROFILE_PRONOUNS}",
replacement: {
match: /(?=,hideBotTag:!0)/,
replace: ",moreTags_channelId:arguments[0].moreTags_channelId"
}
},
// in profiles
{
find: ",overrideDiscriminator:",
group: true,
replacement: [
{
// prevent channel id from getting ghosted
// it's either this or extremely long lookbehind
match: /user:\i,nick:\i,/,
replace: "$&moreTags_channelId,"
}, {
match: /,botType:(\i),botVerified:(\i),(?!discriminatorClass:)(?<=user:(\i).+?)/g,
replace: ",botType:$self.getTag({user:$3,channelId:moreTags_channelId,origType:$1,location:'not-chat'}),botVerified:$2,"
}
]
},
],
start() {
settings.store.tagSettings ??= defaultSettings;
// newly added field might be missing from old users
settings.store.tagSettings.CHAT_MODERATOR ??= {
text: "Chat Mod",
showInChat: true,
showInNotChat: true
};
},
getPermissions(user: User, channel: Channel): string[] {
const guild = GuildStore.getGuild(channel?.guild_id);
if (!guild) return [];
const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites });
return Object.entries(PermissionsBits)
.map(([perm, permInt]) =>
permissions & permInt ? perm : ""
)
.filter(Boolean);
},
genTagTypes(obj) {
let i = 100;
tags.forEach(({ name }) => {
obj[name] = ++i;
obj[i] = name;
obj[`${name}-BOT`] = ++i;
obj[i] = `${name}-BOT`;
obj[`${name}-OP`] = ++i;
obj[i] = `${name}-OP`;
});
},
isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]),
getTagText(passedTagName: string, originalText: string) {
try {
const [tagName, variant] = passedTagName.split("-");
if (!passedTagName) return getIntlMessage("APP_TAG");
const tag = tags.find(({ name }) => tagName === name);
if (!tag) return getIntlMessage("APP_TAG");
if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return getIntlMessage("APP_TAG");
const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName;
switch (variant) {
case "OP":
return `${getIntlMessage("BOT_TAG_FORUM_ORIGINAL_POSTER")}${tagText}`;
case "BOT":
return `${getIntlMessage("APP_TAG")}${tagText}`;
default:
return tagText;
}
} catch {
return originalText;
}
},
getTag({
message, user, channelId, origType, location, channel
}: {
message?: Message,
user: User & { isClyde(): boolean; },
channel?: Channel & { isForumPost(): boolean; isMediaPost(): boolean; },
channelId?: string;
origType?: number;
location: "chat" | "not-chat";
}): number | null {
if (!user)
return null;
if (location === "chat" && user.id === "1")
return Tag.Types.OFFICIAL;
if (user.isClyde())
return Tag.Types.AI;
let type = typeof origType === "number" ? origType : null;
channel ??= ChannelStore.getChannel(channelId!) as any;
if (!channel) return type;
const settings = this.settings.store;
const perms = this.getPermissions(user, channel);
for (const tag of tags) {
if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue;
if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue;
// If the owner tag is disabled, and the user is the owner of the guild,
// avoid adding other tags because the owner will always match the condition for them
if (
tag.name !== "OWNER" &&
GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id &&
(location === "chat" && !settings.tagSettings.OWNER.showInChat) ||
(location === "not-chat" && !settings.tagSettings.OWNER.showInNotChat)
) continue;
if (
tag.permissions?.some(perm => perms.includes(perm)) ||
(tag.condition?.(message!, user, channel))
) {
if ((channel.isForumPost() || channel.isMediaPost()) && channel.ownerId === user.id)
type = Tag.Types[`${tag.name}-OP`];
else if (user.bot && !isWebhook(message!, user) && !settings.dontShowBotTag)
type = Tag.Types[`${tag.name}-BOT`];
else
type = Tag.Types[tag.name];
break;
}
}
return type;
}
});

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 { Settings } from "@api/Settings"; import { definePluginSettings, migratePluginSetting } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { runtimeHashMessageKey } from "@utils/intlHash"; import { runtimeHashMessageKey } from "@utils/intlHash";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
@ -32,10 +32,29 @@ interface MessageDeleteProps {
collapsedReason: () => any; collapsedReason: () => any;
} }
// Remove this migration once enough time has passed
migratePluginSetting("NoBlockedMessages", "ignoreBlockedMessages", "ignoreMessages");
const settings = definePluginSettings({
ignoreMessages: {
description: "Completely ignores incoming messages from blocked and ignored (if enabled) users",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
applyToIgnoredUsers: {
description: "Additionally apply to 'ignored' users",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
}
});
export default definePlugin({ export default definePlugin({
name: "NoBlockedMessages", name: "NoBlockedMessages",
description: "Hides all blocked messages from chat completely.", description: "Hides all blocked/ignored messages from chat completely",
authors: [Devs.rushii, Devs.Samu], authors: [Devs.rushii, Devs.Samu, Devs.jamesbt365],
settings,
patches: [ patches: [
{ {
find: "#{intl::BLOCKED_MESSAGES_HIDE}", find: "#{intl::BLOCKED_MESSAGES_HIDE}",
@ -51,38 +70,40 @@ export default definePlugin({
'"ReadStateStore"' '"ReadStateStore"'
].map(find => ({ ].map(find => ({
find, find,
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true, predicate: () => settings.store.ignoreMessages,
replacement: [ replacement: [
{ {
match: /(?<=function (\i)\((\i)\){)(?=.*MESSAGE_CREATE:\1)/, match: /(?<=function (\i)\((\i)\){)(?=.*MESSAGE_CREATE:\1)/,
replace: (_, _funcName, props) => `if($self.isBlocked(${props}.message))return;` replace: (_, _funcName, props) => `if($self.shouldIgnoreMessage(${props}.message))return;`
} }
] ]
})) }))
], ],
options: {
ignoreBlockedMessages: {
description: "Completely ignores (recent) incoming messages from blocked users (locally).",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true,
},
},
isBlocked(message: Message) { shouldIgnoreMessage(message: Message) {
try { try {
return RelationshipStore.isBlocked(message.author.id); if (RelationshipStore.isBlocked(message.author.id)) {
} catch (e) { return true;
new Logger("NoBlockedMessages").error("Failed to check if user is blocked:", e);
} }
}, return settings.store.applyToIgnoredUsers && RelationshipStore.isIgnored(message.author.id);
shouldHide(props: MessageDeleteProps) {
try {
return props.collapsedReason() === i18n.t[runtimeHashMessageKey("BLOCKED_MESSAGE_COUNT")]();
} catch (e) { } catch (e) {
console.error(e); new Logger("NoBlockedMessages").error("Failed to check if user is blocked or ignored:", e);
}
return false; return false;
} }
},
shouldHide(props: MessageDeleteProps): boolean {
try {
const collapsedReason = props.collapsedReason();
const blockedReason = i18n.t[runtimeHashMessageKey("BLOCKED_MESSAGE_COUNT")]();
const ignoredReason = settings.store.applyToIgnoredUsers
? i18n.t[runtimeHashMessageKey("IGNORED_MESSAGE_COUNT")]()
: null;
return collapsedReason === blockedReason || collapsedReason === ignoredReason;
} catch (e) {
console.error(e);
return false;
}
}
}); });

View file

@ -100,8 +100,8 @@ export default definePlugin({
replace: "true" replace: "true"
}, },
{ {
match: /!\(0,\i\.isDesktop\)\(\)/, match: /(!)?\(0,\i\.isDesktop\)\(\)/,
replace: "false" replace: (_, not) => not ? "false" : "true"
} }
] ]
}, },

View file

@ -46,8 +46,8 @@ export default definePlugin({
find: "#{intl::ONBOARDING_CHANNEL_THRESHOLD_WARNING}", find: "#{intl::ONBOARDING_CHANNEL_THRESHOLD_WARNING}",
replacement: [ replacement: [
{ {
match: /{(\i:function\(\){return \i},?){2}}/, match: /{(?:\i:(?:function\(\){return |\(\)=>)\i}?,?){2}}/,
replace: m => m.replaceAll(canonicalizeMatch(/return \i/g), "return ()=>Promise.resolve(true)") replace: m => m.replaceAll(canonicalizeMatch(/(function\(\){return |\(\)=>)\i/g), "$1()=>Promise.resolve(true)")
} }
], ],
predicate: () => settings.store.onboarding predicate: () => settings.store.onboarding

View file

@ -6,7 +6,7 @@
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
import { extractAndLoadChunksLazy, findComponentByCodeLazy, findExportedComponentLazy } from "@webpack"; import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common"; import { Button, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
import { DEFAULT_COLOR, SWATCHES } from "../constants"; import { DEFAULT_COLOR, SWATCHES } from "../constants";
@ -30,7 +30,7 @@ interface ColorPickerWithSwatchesProps {
} }
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const ColorPickerWithSwatches = findExportedComponentLazy<ColorPickerWithSwatchesProps>("ColorPicker", "CustomColorPicker"); const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>('id:"color-picker"');
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/); export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/);

View file

@ -25,7 +25,7 @@ import { Settings } 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, findStoreLazy } from "@webpack"; import { filters, findStoreLazy, mapMangledModuleLazy } from "@webpack";
import { PresenceStore, Tooltip, UserStore } from "@webpack/common"; import { PresenceStore, Tooltip, UserStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
@ -70,7 +70,9 @@ const Icons = {
}; };
type Platform = keyof typeof Icons; type Platform = keyof typeof Icons;
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes"); const { useStatusFillColor } = mapMangledModuleLazy(".concat(.5625*", {
useStatusFillColor: filters.byCode(".hex")
});
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => { const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
const tooltip = platform === "embedded" const tooltip = platform === "embedded"
@ -79,7 +81,7 @@ const PlatformIcon = ({ platform, status, small }: { platform: Platform, status:
const Icon = Icons[platform] ?? Icons.desktop; const Icon = Icons[platform] ?? Icons.desktop;
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />; return <Icon color={useStatusFillColor(status)} tooltip={tooltip} small={small} />;
}; };
function ensureOwnStatus(user: User) { function ensureOwnStatus(user: User) {

View file

@ -108,8 +108,10 @@ export default definePlugin({
}, },
{ {
// Prevent Discord from trying to connect to hidden voice channels // Prevent Discord from trying to connect to hidden voice channels
match: /(?=&&\i\.\i\.selectVoiceChannel\((\i)\.id\))/, match: /(?=(\|\||&&)\i\.\i\.selectVoiceChannel\((\i)\.id\))/,
replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})` replace: (_, condition, channel) => condition === "||"
? `||$self.isHiddenChannel(${channel})`
: `&&!$self.isHiddenChannel(${channel})`
}, },
{ {
// Make Discord show inside the channel if clicking on a hidden or locked channel // Make Discord show inside the channel if clicking on a hidden or locked channel
@ -122,8 +124,10 @@ export default definePlugin({
{ {
find: ".AUDIENCE),{isSubscriptionGated", find: ".AUDIENCE),{isSubscriptionGated",
replacement: { replacement: {
match: /!(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/, match: /(!)?(\i)\.isRoleSubscriptionTemplatePreviewChannel\(\)/,
replace: (m, channel) => `${m}&&!$self.isHiddenChannel(${channel})` replace: (m, not, channel) => not
? `${m}&&!$self.isHiddenChannel(${channel})`
: `${m}||$self.isHiddenChannel(${channel})`
} }
}, },
{ {
@ -173,8 +177,10 @@ export default definePlugin({
}, },
// Make voice channels also appear as muted if they are muted // Make voice channels also appear as muted if they are muted
{ {
match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)if\((\i)\)return (\i\.MUTED);/, match: /(?<=\.wrapper:\i\.notInteractive,)(.+?)(if\()?(\i)(?:\)return |\?)(\i\.MUTED)/,
replace: (_, otherClasses, isMuted, mutedClassExpression) => `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return "";` replace: (_, otherClasses, isIf, isMuted, mutedClassExpression) => isIf
? `${isMuted}?${mutedClassExpression}:"",${otherClasses}if(${isMuted})return ""`
: `${isMuted}?${mutedClassExpression}:"",${otherClasses}${isMuted}?""`
} }
] ]
}, },
@ -184,8 +190,8 @@ export default definePlugin({
{ {
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden // Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle, predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
match: /\.LOCKED;if\((?<={channel:(\i).+?)/, match: /(?<=\.LOCKED(?:;if\(|:))(?<={channel:(\i).+?)/,
replace: (m, channel) => `${m}!$self.isHiddenChannel(${channel})&&` replace: (_, channel) => `!$self.isHiddenChannel(${channel})&&`
}, },
{ {
// Hide unreads // Hide unreads

View file

@ -76,8 +76,8 @@ export default definePlugin({
find: "#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}", find: "#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}",
replacement: [ replacement: [
{ {
match: /(\i)\.Tooltip,{(text:.{0,30}#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\))/, match: /\i\.\i,{(text:.{0,30}#{intl::GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}\))/,
replace: "$self.TooltipWrapper,{message:arguments[0].message,$2" replace: "$self.TooltipWrapper,{message:arguments[0].message,$1"
} }
] ]
} }

View file

@ -23,12 +23,12 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord"; import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy, findExportedComponentLazy, findStoreLazy } from "@webpack"; import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; import { GuildMemberStore, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { buildSeveralUsers } from "../typingTweaks"; import { buildSeveralUsers } from "../typingTweaks";
const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots"); const ThreeDots = findComponentByCodeLazy(".dots,", "dotRadius:");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const TypingStore = findStoreLazy("TypingStore"); const TypingStore = findStoreLazy("TypingStore");

View file

@ -22,7 +22,7 @@ const VoiceStateStore = findStoreLazy("VoiceStateStore");
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const Avatar = findComponentByCodeLazy(".status)/2):0"); const Avatar = findComponentByCodeLazy(".status)/2):0");
const GroupDMAvatars = findComponentByCodeLazy(".AvatarSizeSpecs[", "getAvatarURL"); const GroupDMAvatars = findComponentByCodeLazy("frontSrc:", "getAvatarURL");
const ActionButtonClasses = findByPropsLazy("actionButton", "highlight"); const ActionButtonClasses = findByPropsLazy("actionButton", "highlight");

View file

@ -23,11 +23,11 @@ import { Settings, useSettings } 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 from "@utils/types"; import definePlugin from "@utils/types";
import { findExportedComponentLazy } from "@webpack"; import { findComponentByCodeLazy } from "@webpack";
import { Menu, Popout, useState } from "@webpack/common"; import { Menu, Popout, useState } from "@webpack/common";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider"); const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
function VencordPopout(onClose: () => void) { function VencordPopout(onClose: () => void) {
const { useQuickCss } = useSettings(["useQuickCss"]); const { useQuickCss } = useSettings(["useQuickCss"]);

View file

@ -220,16 +220,16 @@ export default definePlugin({
{ {
find: ".cursorPointer:null,children", find: ".cursorPointer:null,children",
replacement: { replacement: {
match: /.Avatar,.+?src:(.+?\))(?=[,}])/, match: /(?=,src:(\i.getAvatarURL\(.+?[)]))/,
replace: (m, avatarUrl) => `${m},onClick:()=>$self.openAvatar(${avatarUrl})` replace: (_, avatarUrl) => `,onClick:()=>$self.openAvatar(${avatarUrl})`
} }
}, },
// User Dms top large icon // User Dms top large icon
{ {
find: 'experimentLocation:"empty_messages"', find: 'experimentLocation:"empty_messages"',
replacement: { replacement: {
match: /.Avatar,.+?src:(.+?\))(?=[,}])/, match: /(?<=SIZE_80,)(?=src:(.+?\))[,}])/,
replace: (m, avatarUrl) => `${m},onClick:()=>$self.openAvatar(${avatarUrl})` replace: (_, avatarUrl) => `onClick:()=>$self.openAvatar(${avatarUrl}),`
} }
} }
] ]

View file

@ -22,12 +22,12 @@ import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord"; import { getCurrentGuild, getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc"; import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common"; import { Button, ChannelStore, Forms, GuildStore, Menu, Text } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
@ -118,7 +118,7 @@ const settings = definePluginSettings({
} }
}); });
function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback { function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel"): NavContextMenuPatchCallback {
return (children, props) => { return (children, props) => {
const value = props[name.toLowerCase()]; const value = props[name.toLowerCase()];
if (!value) return; if (!value) return;
@ -144,6 +144,23 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenu
}; };
} }
const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id: string; }) => {
const guild = getCurrentGuild();
if (!guild) return;
const role = GuildStore.getRole(guild.id, id);
if (!role) return;
children.push(
<Menu.MenuItem
id={"vc-view-role-raw"}
label="View Raw"
action={() => openViewRawModal(JSON.stringify(role, null, 4), "Role")}
icon={CopyIcon}
/>
);
};
export default definePlugin({ export default definePlugin({
name: "ViewRaw", name: "ViewRaw",
description: "Copy and view the raw content/data of any message, channel or guild", description: "Copy and view the raw content/data of any message, channel or guild",
@ -152,10 +169,12 @@ export default definePlugin({
contextMenus: { contextMenus: {
"guild-context": MakeContextCallback("Guild"), "guild-context": MakeContextCallback("Guild"),
"guild-settings-role-context": MakeContextCallback("Role"),
"channel-context": MakeContextCallback("Channel"), "channel-context": MakeContextCallback("Channel"),
"thread-context": MakeContextCallback("Channel"), "thread-context": MakeContextCallback("Channel"),
"gdm-context": MakeContextCallback("Channel"), "gdm-context": MakeContextCallback("Channel"),
"user-context": MakeContextCallback("User") "user-context": MakeContextCallback("User"),
"dev-context": devContextCallback
}, },
renderMessagePopoverButton(msg) { renderMessagePopoverButton(msg) {

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 { findByPropsLazy, findModuleId, proxyLazyWebpack, wreq } from "@webpack"; import { filters, findModuleId, mapMangledModuleLazy, proxyLazyWebpack, wreq } from "@webpack";
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react"; import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
import { LazyComponent } from "./react"; import { LazyComponent } from "./react";
@ -49,7 +49,7 @@ export interface ModalOptions {
type RenderFunction = (props: ModalProps) => ReactNode | Promise<ReactNode>; type RenderFunction = (props: ModalProps) => ReactNode | Promise<ReactNode>;
export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as { interface Modals {
ModalRoot: ComponentType<PropsWithChildren<{ ModalRoot: ComponentType<PropsWithChildren<{
transitionState: ModalTransitionState; transitionState: ModalTransitionState;
size?: ModalSize; size?: ModalSize;
@ -99,7 +99,21 @@ export const Modals = findByPropsLazy("ModalRoot", "ModalCloseButton") as {
hideOnFullscreen?: boolean; hideOnFullscreen?: boolean;
className?: string; className?: string;
}>; }>;
}; }
const Modals: Modals = mapMangledModuleLazy(':"thin")', {
ModalRoot: filters.componentByCode('.MODAL,"aria-labelledby":'),
ModalHeader: filters.componentByCode(",id:"),
ModalContent: filters.componentByCode(".content,"),
ModalFooter: filters.componentByCode(".footer,"),
ModalCloseButton: filters.componentByCode(".close]:")
});
export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
export const ModalContent = LazyComponent(() => Modals.ModalContent);
export const ModalFooter = LazyComponent(() => Modals.ModalFooter);
export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);
export type MediaModalItem = { export type MediaModalItem = {
url: string; url: string;
@ -135,38 +149,33 @@ export const openMediaModal: (props: MediaModalProps) => void = proxyLazyWebpack
return Object.values<any>(openMediaModalModule).find(v => String(v).includes("modalKey:")); return Object.values<any>(openMediaModalModule).find(v => String(v).includes("modalKey:"));
}); });
export const ModalRoot = LazyComponent(() => Modals.ModalRoot); interface ModalAPI {
export const ModalHeader = LazyComponent(() => Modals.ModalHeader); /**
export const ModalContent = LazyComponent(() => Modals.ModalContent);
export const ModalFooter = LazyComponent(() => Modals.ModalFooter);
export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton);
export const ModalAPI = findByPropsLazy("openModalLazy");
/**
* Wait for the render promise to resolve, then open a modal with it. * Wait for the render promise to resolve, then open a modal with it.
* This is equivalent to render().then(openModal) * This is equivalent to render().then(openModal)
* You should use the Modal components exported by this file * You should use the Modal components exported by this file
*/ */
export const openModalLazy: (render: () => Promise<RenderFunction>, options?: ModalOptions & { contextKey?: string; }) => Promise<string> openModalLazy: (render: () => Promise<RenderFunction>, options?: ModalOptions & { contextKey?: string; }) => Promise<string>;
= proxyLazyWebpack(() => ModalAPI.openModalLazy); /**
/**
* Open a Modal with the given render function. * Open a Modal with the given render function.
* You should use the Modal components exported by this file * You should use the Modal components exported by this file
*/ */
export const openModal: (render: RenderFunction, options?: ModalOptions, contextKey?: string) => string openModal: (render: RenderFunction, options?: ModalOptions, contextKey?: string) => string;
= proxyLazyWebpack(() => ModalAPI.openModal); /**
/**
* Close a modal by its key * Close a modal by its key
*/ */
export const closeModal: (modalKey: string, contextKey?: string) => void closeModal: (modalKey: string, contextKey?: string) => void;
= proxyLazyWebpack(() => ModalAPI.closeModal); /**
/**
* Close all open modals * Close all open modals
*/ */
export const closeAllModals: () => void closeAllModals: () => void;
= proxyLazyWebpack(() => ModalAPI.closeAllModals); }
export const ModalAPI: ModalAPI = mapMangledModuleLazy(".modalKey?", {
openModalLazy: filters.byCode(".modalKey?"),
openModal: filters.byCode(",instant:"),
closeModal: filters.byCode(".onCloseCallback()"),
closeAllModals: filters.byCode(".getState();for")
});
export const { openModalLazy, openModal, closeModal, closeAllModals } = ModalAPI;

View file

@ -16,76 +16,82 @@
* 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 { filters, findByPropsLazy, waitFor } from "@webpack"; import { LazyComponent } from "@utils/lazyReact";
import { filters, mapMangledModuleLazy, waitFor } from "@webpack";
import { waitForComponent } from "./internal"; import { waitForComponent } from "./internal";
import * as t from "./types/components"; import * as t from "./types/components";
export let Forms = {} as {
FormTitle: t.FormTitle, const FormTitle = waitForComponent<t.FormTitle>("FormTitle", filters.componentByCode('["defaultMargin".concat', '="h5"'));
FormSection: t.FormSection, const FormText = waitForComponent<t.FormText>("FormText", filters.componentByCode(".SELECTABLE),", ".DISABLED:"));
FormDivider: t.FormDivider, const FormSection = waitForComponent<t.FormSection>("FormSection", filters.componentByCode(".titleId)&&"));
FormText: t.FormText, const FormDivider = waitForComponent<t.FormDivider>("FormDivider", filters.componentByCode(".divider,", ",style:", '"div"', /\.divider,\i\),style:/));
export const Forms = {
FormTitle,
FormText,
FormSection,
FormDivider
}; };
export let Icons = {} as t.Icons; export const Card = waitForComponent<t.Card>("Card", filters.componentByCode(".editable),", ".outline:"));
export const Button = waitForComponent<t.Button>("Button", filters.componentByCode("#{intl::A11Y_LOADING_STARTED}))),!1"));
export const Switch = waitForComponent<t.Switch>("Switch", filters.componentByCode(".labelRow,ref:", ".disabledText"));
const Tooltips = mapMangledModuleLazy(".tooltipTop,bottom:", {
Tooltip: filters.componentByCode("this.renderTooltip()]"),
TooltipContainer: filters.componentByCode('="div",')
}) as {
Tooltip: t.Tooltip,
TooltipContainer: t.TooltipContainer;
};
export const Tooltip = LazyComponent(() => Tooltips.Tooltip);
export const TooltipContainer = LazyComponent(() => Tooltips.TooltipContainer);
export const TextInput = waitForComponent<t.TextInput>("TextInput", filters.componentByCode(".error]:this.hasError()"));
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.componentByCode("this.getPaddingRight()},id:"));
export const Text = waitForComponent<t.Text>("Text", filters.componentByCode('case"always-white"'));
export const Heading = waitForComponent<t.Heading>("Heading", filters.componentByCode(">6?{", "variant:"));
export const Select = waitForComponent<t.Select>("Select", filters.componentByCode('.selectPositionTop]:"top"===', '"Escape"==='));
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", filters.componentByCode('.selectPositionTop]:"top"===', ".multi]:"));
export const Slider = waitForComponent<t.Slider>("Slider", filters.componentByCode('"markDash".concat('));
export const Popout = waitForComponent<t.Popout>("Popout", filters.componentByCode("ref:this.ref,preload:"));
export const Dialog = waitForComponent<t.Dialog>("Dialog", filters.componentByCode('role:"dialog",tabIndex:-1'));
export const TabBar = waitForComponent("TabBar", filters.componentByCode("ref:this.tabBarRef,className:"));
export const Paginator = waitForComponent<t.Paginator>("Paginator", filters.componentByCode('rel:"prev",children:'));
export const Clickable = waitForComponent<t.Clickable>("Clickable", filters.componentByCode("this.context?this.renderNonInteractive():"));
export const Avatar = waitForComponent<t.Avatar>("Avatar", filters.componentByCode(".size-1.375*"));
export let createScroller: (scrollbarClassName: string, fadeClassName: string, customThemeClassName: string) => t.ScrollerThin;
export let scrollerClasses: Record<string, string>;
waitFor(filters.byCode('="ltr",orientation:', "customTheme:", "forwardRef"), m => createScroller = m);
waitFor(["thin", "auto", "customTheme"], m => scrollerClasses = m);
export const ScrollerNone = LazyComponent(() => createScroller(scrollerClasses.none, scrollerClasses.fade, scrollerClasses.customTheme));
export const ScrollerThin = LazyComponent(() => createScroller(scrollerClasses.thin, scrollerClasses.fade, scrollerClasses.customTheme));
export const ScrollerAuto = LazyComponent(() => createScroller(scrollerClasses.auto, scrollerClasses.fade, scrollerClasses.customTheme));
const { FocusLock_ } = mapMangledModuleLazy("attachTo:null!==", {
FocusLock_: filters.componentByCode(".containerRef")
}) as {
FocusLock_: t.FocusLock;
};
export const FocusLock = LazyComponent(() => FocusLock_);
export let Card: t.Card;
export let Button: t.Button;
export let Switch: t.Switch;
export let Tooltip: t.Tooltip;
export let TooltipContainer: t.TooltipContainer;
export let TextInput: t.TextInput;
export let TextArea: t.TextArea;
export let Text: t.Text;
export let Heading: t.Heading;
export let Select: t.Select;
export let SearchableSelect: t.SearchableSelect;
export let Slider: t.Slider;
export let ButtonLooks: t.ButtonLooks;
export let Popout: t.Popout;
export let Dialog: t.Dialog;
export let TabBar: any;
export let Paginator: t.Paginator;
export let ScrollerThin: t.ScrollerThin;
export let Clickable: t.Clickable;
export let Avatar: t.Avatar;
export let FocusLock: t.FocusLock;
// token lagger real
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */
export let useToken: t.useToken; export let useToken: t.useToken;
waitFor(m => {
if (typeof m !== "function") {
return false;
}
const str = String(m);
return str.includes(".resolve({theme:null") && !str.includes("useMemo");
}, m => useToken = m);
export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", filters.componentByCode("MASKED_LINK)")); export const MaskedLink = waitForComponent<t.MaskedLink>("MaskedLink", filters.componentByCode("MASKED_LINK)"));
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}")); export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.componentByCode("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]); export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
export const OAuth2AuthorizeModal = waitForComponent("OAuth2AuthorizeModal", filters.componentByCode(".authorize),children:", ".contentBackground"));
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
waitFor(["FormItem", "Button"], m => {
({
useToken,
Card,
Button,
FormSwitch: Switch,
Tooltip,
TooltipContainer,
TextInput,
TextArea,
Text,
Select,
SearchableSelect,
Slider,
ButtonLooks,
TabBar,
Popout,
Dialog,
Paginator,
ScrollerThin,
Clickable,
Avatar,
FocusLock,
Heading
} = m);
Forms = m;
Icons = m;
});

View file

@ -17,16 +17,28 @@
*/ */
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { filters, mapMangledModuleLazy, waitFor } from "../webpack"; import { filters, mapMangledModuleLazy, waitFor, wreq } from "../webpack";
import type * as t from "./types/menu"; import type * as t from "./types/menu";
export let Menu = {} as t.Menu; export const Menu = {} as t.Menu;
waitFor(["MenuItem", "MenuSliderControl"], m => Menu = m); // Relies on .name properties added by the MenuItemDemanglerAPI
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
// we have to do this manual require by ID because m is in this case the MenuCheckBoxItem instead of the entire module
const module = wreq(id as any);
for (const e of Object.values(module)) {
if (typeof e === "function" && e.name.startsWith("Menu")) {
Menu[e.name] = e;
}
}
});
waitFor(filters.componentByCode('path:["empty"]'), m => Menu.Menu = m);
waitFor(filters.componentByCode("sliderContainer", "slider", "handleSize:16", "=100"), m => Menu.MenuSliderControl = m);
export const ContextMenuApi: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', { export const ContextMenuApi: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN', {
closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"), closeContextMenu: filters.byCode("CONTEXT_MENU_CLOSE"),
openContextMenu: filters.byCode("renderLazy:"), openContextMenu: filters.byCode("renderLazy:"),
openContextMenuLazy: e => typeof e === "function" && e.toString().length < 100 openContextMenuLazy: e => typeof e === "function" && e.toString().length < 100
}); });

View file

@ -18,14 +18,12 @@
import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react";
import { IconNames } from "./iconNames";
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>; export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`; export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>; export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>;
export type ButtonLooks = Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>;
export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & { export type TextProps = PropsWithChildren<HtmlHTMLAttributes<HTMLDivElement> & {
variant?: TextVariant; variant?: TextVariant;
@ -504,4 +502,3 @@ export type Icon = ComponentType<JSX.IntrinsicElements["svg"] & {
colorClass?: string; colorClass?: string;
} & Record<string, any>>; } & Record<string, any>>;
export type Icons = Record<IconNames, Icon>;

File diff suppressed because one or more lines are too long

View file

@ -330,23 +330,35 @@ function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory)
// There are (at the time of writing) 11 modules exporting the window // There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance // Make these non enumerable to improve webpack search performance
if (typeof require === "function") { if (typeof require === "function") {
let foundWindow = false; let shouldMakeNonEnumerable = false;
if (exports === window) { nonEnumerableChecking: {
foundWindow = true; // There are (at the time of writing) 11 modules exporting the window,
} else if (typeof exports === "object") { // and also modules exporting DOMTokenList, which breaks webpack finding
if (exports.default === window) { // Make these non enumerable to improve search performance and avoid erros
foundWindow = true; if (exports === window || exports[Symbol.toStringTag] === "DOMTokenList") {
} else { shouldMakeNonEnumerable = true;
for (const exportKey in exports) if (exportKey.length <= 3) { break nonEnumerableChecking;
if (exports[exportKey] === window) {
foundWindow = true;
} }
if (typeof exports !== "object") {
break nonEnumerableChecking;
}
if (exports.default === window || exports.default?.[Symbol.toStringTag] === "DOMTokenList") {
shouldMakeNonEnumerable = true;
break nonEnumerableChecking;
}
for (const exportKey in exports) {
if (exports[exportKey] === window || exports[exportKey]?.[Symbol.toStringTag] === "DOMTokenList") {
shouldMakeNonEnumerable = true;
break nonEnumerableChecking;
} }
} }
} }
if (foundWindow) { if (shouldMakeNonEnumerable) {
if (require.c != null) { if (require.c != null) {
Object.defineProperty(require.c, id, { Object.defineProperty(require.c, id, {
value: require.c[id], value: require.c[id],
@ -387,7 +399,7 @@ function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory)
continue; continue;
} }
for (const exportKey in exports) if (exportKey.length <= 3) { for (const exportKey in exports) {
const exportValue = exports[exportKey]; const exportValue = exports[exportKey];
if (exportValue != null && filter(exportValue)) { if (exportValue != null && filter(exportValue)) {

View file

@ -56,11 +56,14 @@ export const filters = {
: m => props.every(p => m[p] !== void 0), : m => props.every(p => m[p] !== void 0),
byCode: (...code: CodeFilter): FilterFn => { byCode: (...code: CodeFilter): FilterFn => {
code = code.map(canonicalizeMatch); const parsedCode = code.map(canonicalizeMatch);
return m => { const filter = m => {
if (typeof m !== "function") return false; if (typeof m !== "function") return false;
return stringMatches(Function.prototype.toString.call(m), code); return stringMatches(Function.prototype.toString.call(m), parsedCode);
}; };
filter.$$vencordProps = [...code];
return filter;
}, },
byStoreName: (name: StoreNameFilter): FilterFn => m => byStoreName: (name: StoreNameFilter): FilterFn => m =>
m.constructor?.displayName === name, m.constructor?.displayName === name,
@ -140,8 +143,7 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
return isWaitFor ? [found, key] : found; return isWaitFor ? [found, key] : found;
} }
// the length check makes search about 20% faster for (const nestedMod in mod.exports) {
for (const nestedMod in mod.exports) if (nestedMod.length <= 3) {
const nested = mod.exports[nestedMod]; const nested = mod.exports[nestedMod];
if (nested && filter(nested)) { if (nested && filter(nested)) {
return isWaitFor ? [nested, key] : nested; return isWaitFor ? [nested, key] : nested;
@ -172,7 +174,7 @@ export function findAll(filter: FilterFn) {
if (mod.exports.default && filter(mod.exports.default)) if (mod.exports.default && filter(mod.exports.default))
ret.push(mod.exports.default); ret.push(mod.exports.default);
else for (const nestedMod in mod.exports) if (nestedMod.length <= 3) { else for (const nestedMod in mod.exports) {
const nested = mod.exports[nestedMod]; const nested = mod.exports[nestedMod];
if (nested && filter(nested)) ret.push(nested); if (nested && filter(nested)) ret.push(nested);
} }
@ -235,8 +237,7 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns
break; break;
} }
for (const nestedMod in mod.exports) for (const nestedMod in mod.exports) {
if (nestedMod.length <= 3) {
const nested = mod.exports[nestedMod]; const nested = mod.exports[nestedMod];
if (nested && filter(nested)) { if (nested && filter(nested)) {
results[j] = nested; results[j] = nested;