Merge branch 'dev' into betterQuickReact

This commit is contained in:
Sqaaakoi 2024-03-31 23:16:41 +13:00
commit efde7bf917
No known key found for this signature in database
62 changed files with 1370 additions and 403 deletions

View file

@ -1,6 +1,6 @@
> [!WARNING] > [!WARNING]
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead. > These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
> No support will be provided for installing in this fashion. If you cannot figure it out, you should just stick to a regular install. > No support will be provided for installing in this fashion. If you cannot figure it out, you should just stick to a regular install.
# Installation Guide # Installation Guide
@ -95,5 +95,3 @@ Simply run:
```shell ```shell
pnpm uninject pnpm uninject
``` ```
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.7.2", "version": "1.7.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": {

View file

@ -67,7 +67,8 @@ const IGNORED_DISCORD_ERRORS = [
"Unable to process domain list delta: Client revision number is null", "Unable to process domain list delta: Client revision number is null",
"Downloading the full bad domains file", "Downloading the full bad domains file",
/\[GatewaySocket\].{0,110}Cannot access '/, /\[GatewaySocket\].{0,110}Cannot access '/,
"search for 'name' in undefined" "search for 'name' in undefined",
"Attempting to set fast connect zstd when unsupported"
] as Array<string | RegExp>; ] as Array<string | RegExp>;
function toCodeBlock(s: string) { function toCodeBlock(s: string) {

View file

@ -22,9 +22,9 @@ import { debounce } from "@shared/debounce";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import { ReplaceFn } from "@utils/types"; import { Patch, ReplaceFn } from "@utils/types";
import { search } from "@webpack"; import { search } from "@webpack";
import { Button, Clipboard, Forms, Parser, React, Switch, TextInput } from "@webpack/common"; import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared"; import { SettingsTab, wrapTab } from "./shared";
@ -218,6 +218,60 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
); );
} }
interface FullPatchInputProps {
setFind(v: string): void;
setMatch(v: string): void;
setReplacement(v: string | ReplaceFn): void;
}
function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputProps) {
const [fullPatch, setFullPatch] = React.useState<string>("");
const [fullPatchError, setFullPatchError] = React.useState<string>("");
function update() {
if (fullPatch === "") {
setFullPatchError("");
setFind("");
setMatch("");
setReplacement("");
return;
}
try {
const parsed = (0, eval)(`(${fullPatch})`) as Patch;
if (!parsed.find) throw new Error("No 'find' field");
if (!parsed.replacement) throw new Error("No 'replacement' field");
if (parsed.replacement instanceof Array) {
if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
parsed.replacement = {
match: parsed.replacement[0].match,
replace: parsed.replacement[0].replace
};
}
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
setFind(parsed.find);
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
setReplacement(parsed.replacement.replace);
setFullPatchError("");
} catch (e) {
setFullPatchError((e as Error).message);
}
}
return <>
<Forms.FormText>Paste your full JSON patch here to fill out the fields</Forms.FormText>
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
</>;
}
function PatchHelper() { function PatchHelper() {
const [find, setFind] = React.useState<string>(""); const [find, setFind] = React.useState<string>("");
const [match, setMatch] = React.useState<string>(""); const [match, setMatch] = React.useState<string>("");
@ -260,6 +314,13 @@ function PatchHelper() {
return ( return (
<SettingsTab title="Patch Helper"> <SettingsTab title="Patch Helper">
<Forms.FormTitle>full patch</Forms.FormTitle>
<FullPatchInput
setFind={onFindChange}
setMatch={onMatchChange}
setReplacement={setReplacement}
/>
<Forms.FormTitle>find</Forms.FormTitle> <Forms.FormTitle>find</Forms.FormTitle>
<TextInput <TextInput
type="text" type="text"

View file

@ -28,7 +28,7 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
const execFile = promisify(cpExecFile); const execFile = promisify(cpExecFile);
const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord")); const isFlatpak = process.platform === "linux" && !!process.env.FLATPAK_ID;
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`; if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
@ -60,7 +60,8 @@ async function calculateGitChanges() {
return commits ? commits.split("\n").map(line => { return commits ? commits.split("\n").map(line => {
const [author, hash, ...rest] = line.split("/"); const [author, hash, ...rest] = line.split("/");
return { return {
hash, author, message: rest.join("/") hash, author,
message: rest.join("/").split("\n")[0]
}; };
}) : []; }) : [];
} }

View file

@ -53,7 +53,7 @@ async function calculateGitChanges() {
// github api only sends the long sha // github api only sends the long sha
hash: c.sha.slice(0, 7), hash: c.sha.slice(0, 7),
author: c.author.login, author: c.author.login,
message: c.commit.message.substring(c.commit.message.indexOf("\n") + 1) message: c.commit.message.split("\n")[0]
})); }));
} }

View file

@ -13,10 +13,10 @@ export default definePlugin({
authors: [Devs.Ven], authors: [Devs.Ven],
patches: [{ patches: [{
find: 'location:"ChannelTextAreaButtons"', find: '"sticker")',
replacement: { replacement: {
match: /if\(!\i\.isMobile\)\{(?=.+?&&(\i)\.push\(.{0,50}"gift")/, match: /!\i\.isMobile(?=.+?(\i)\.push\(.{0,50}"gift")/,
replace: "$&Vencord.Api.ChatButtons._injectButtons($1,arguments[0]);" replace: "$& &&(Vencord.Api.ChatButtons._injectButtons($1,arguments[0]),true)"
} }
}] }]
}); });

View file

@ -31,7 +31,7 @@ export default definePlugin({
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/, match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
replace: "$&vencordProps=$1," replace: "$&vencordProps=$1,"
}, { }, {
match: /decorators:.{0,100}?children:\[/, match: /\.Messages\.GUILD_OWNER(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps))," replace: "$&...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
} }
] ]

View file

@ -35,7 +35,7 @@ export default definePlugin({
} }
}, },
{ {
find: ".handleSendMessage=", find: ".handleSendMessage",
replacement: { replacement: {
// props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply); // props.chatInputType...then((function(isMessageValid)... var parsedMessage = b.c.parse(channel,... var replyOptions = f.g.getSendMessageOptionsForReply(pendingReply);
// Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid) // Lookbehind: validateMessage)({openWarningPopout:..., type: i.props.chatInputType, content: t, stickers: r, ...}).then((function(isMessageValid)

View file

@ -26,7 +26,7 @@ export default definePlugin({
required: true, required: true,
patches: [ patches: [
{ {
find: 'displayName="NoticeStore"', find: '"NoticeStore"',
replacement: [ replacement: [
{ {
match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g, match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g,

View file

@ -16,11 +16,10 @@
* 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 { findGroupChildrenByChildId } from "@api/ContextMenu";
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { React, SettingsRouter } from "@webpack/common"; import { React } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
@ -30,23 +29,6 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Megu], authors: [Devs.Ven, Devs.Megu],
required: true, required: true,
contextMenus: {
// The settings shortcuts in the user settings cog context menu
// read the elements from a hardcoded map which for obvious reason
// doesn't contain our sections. This patches the actions of our
// sections to manually use SettingsRouter (which only works on desktop
// but the context menu is usually not available on mobile anyway)
"user-settings-cog"(children) {
const section = findGroupChildrenByChildId("VencordSettings", children);
section?.forEach(c => {
const id = c?.props?.id;
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
c!.props.action = () => SettingsRouter.open(id);
}
});
}
},
patches: [{ patches: [{
find: ".versionHash", find: ".versionHash",
replacement: [ replacement: [
@ -75,6 +57,12 @@ export default definePlugin({
}, },
replace: "...$self.makeSettingsCategories($1),$&" replace: "...$self.makeSettingsCategories($1),$&"
} }
}, {
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
replace: "$2.default.open($1);return;"
}
}], }],
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[], customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],

View file

@ -16,27 +16,46 @@
* 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 { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
domain: {
type: OptionType.BOOLEAN,
default: true,
description: "Remove the untrusted domain popup when opening links",
restartNeeded: true
},
file: {
type: OptionType.BOOLEAN,
default: true,
description: "Remove the 'Potentially Dangerous Download' popup when opening links",
restartNeeded: true
}
});
export default definePlugin({ export default definePlugin({
name: "AlwaysTrust", name: "AlwaysTrust",
description: "Removes the annoying untrusted domain and suspicious file popup", description: "Removes the annoying untrusted domain and suspicious file popup",
authors: [Devs.zt], authors: [Devs.zt, Devs.Trwy],
patches: [ patches: [
{ {
find: ".displayName=\"MaskedLinkStore\"", find: '="MaskedLinkStore",',
replacement: { replacement: {
match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/, match: /(?<=isTrustedDomain\(\i\){)return \i\(\i\)/,
replace: "return true" replace: "return true"
} },
predicate: () => settings.store.domain
}, },
{ {
find: "isSuspiciousDownload:", find: "isSuspiciousDownload:",
replacement: { replacement: {
match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/, match: /function \i\(\i\){(?=.{0,60}\.parse\(\i\))/,
replace: "$&return null;" replace: "$&return null;"
} },
predicate: () => settings.store.file
} }
] ],
settings
}); });

View file

@ -78,6 +78,13 @@ export default definePlugin({
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),", "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),",
}, },
}, },
{
find: "message.attachments",
replacement: {
match: /(\i.uploadFiles\((\i),)/,
replace: "$2.forEach(f=>f.filename=$self.anonymise(f)),$1"
}
},
{ {
find: ".Messages.ATTACHMENT_UTILITIES_SPOILER", find: ".Messages.ATTACHMENT_UTILITIES_SPOILER",
replacement: { replacement: {

View file

@ -26,7 +26,7 @@ export default definePlugin({
"Change GIF alt text from simply being 'GIF' to containing the gif tags / filename", "Change GIF alt text from simply being 'GIF' to containing the gif tags / filename",
patches: [ patches: [
{ {
find: "onCloseImage=", find: '"onCloseImage",',
replacement: { replacement: {
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/, match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
replace: replace:

View file

@ -15,8 +15,8 @@ export default definePlugin({
{ {
find: ".GIFPickerResultTypes.SEARCH", find: ".GIFPickerResultTypes.SEARCH",
replacement: [{ replacement: [{
match: "this.state={resultType:null}", match: /(?<="state",{resultType:)null/,
replace: 'this.state={resultType:"Favorites"}' replace: '"Favorites"'
}] }]
} }
] ]

View file

@ -18,6 +18,7 @@
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
@ -39,8 +40,12 @@ export default definePlugin({
match: /hideNote:.+?(?=([,}].*?\)))/g, match: /hideNote:.+?(?=([,}].*?\)))/g,
replace: (m, rest) => { replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/); const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch == null) return "hideNote:!0"; if (destructuringMatch) {
return m; const defaultValueMatch = m.match(canonicalizeMatch(/hideNote:(\i)=!?\d/));
return defaultValueMatch ? `hideNote:${defaultValueMatch[1]}=!0` : m;
}
return "hideNote:!0";
} }
} }
}, },
@ -52,10 +57,10 @@ export default definePlugin({
} }
}, },
{ {
find: ".Messages.NOTE}", find: ".popularApplicationCommandIds,",
replacement: { replacement: {
match: /(?<=return \i\?)null(?=:\(0,\i\.jsxs)/, match: /lastSection:(!?\i)}\),/,
replace: "$self.patchPadding(arguments[0])" replace: "$&$self.patchPadding($1),"
} }
} }
], ],
@ -75,8 +80,8 @@ export default definePlugin({
} }
}, },
patchPadding(e: any) { patchPadding(lastSection: any) {
if (!e.lastSection) return; if (!lastSection) return;
return ( return (
<div className={UserPopoutSectionCssClasses.lastSection}></div> <div className={UserPopoutSectionCssClasses.lastSection}></div>
); );

View file

@ -0,0 +1,9 @@
# BetterSettings
Improves Discord's Settings via multiple (toggleable) changes:
- makes opening settings much faster
- removes the scuffed transition animation
- organises the settings cog context menu into categories
![](https://github.com/Vendicated/Vencord/assets/45497981/e8d67a95-3909-4be5-8281-8cf9d2f1c30e)

View file

@ -0,0 +1,177 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
import type { HTMLAttributes, ReactElement } from "react";
type SettingsEntry = { section: string, label: string; };
const cl = classNameFactory("");
const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers");
const settings = definePluginSettings({
disableFade: {
description: "Disable the crossfade animation",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
},
organizeMenu: {
description: "Organizes the settings cog context menu into categories",
type: OptionType.BOOLEAN,
default: true
},
eagerLoad: {
description: "Removes the loading delay when opening the menu for the first time",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
}
});
interface LayerProps extends HTMLAttributes<HTMLDivElement> {
mode: "SHOWN" | "HIDDEN";
baseLayer?: boolean;
}
function Layer({ mode, baseLayer = false, ...props }: LayerProps) {
const hidden = mode === "HIDDEN";
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => () => {
ComponentDispatch.dispatch("LAYER_POP_START");
ComponentDispatch.dispatch("LAYER_POP_COMPLETE");
}, []);
const node = (
<div
ref={containerRef}
aria-hidden={hidden}
className={cl({
[Classes.layer]: true,
[Classes.baseLayer]: baseLayer,
"stop-animations": hidden
})}
style={{ opacity: hidden ? 0 : undefined }}
{...props}
/>
);
return baseLayer
? node
: <FocusLock containerRef={containerRef}>{node}</FocusLock>;
}
export default definePlugin({
name: "BetterSettings",
description: "Enhances your settings-menu-opening experience",
authors: [Devs.Kyuuhachi],
settings,
patches: [
{
find: "this.renderArtisanalHack()",
replacement: [
{ // Fade in on layer
match: /(?<=\((\i),"contextType",\i\.AccessibilityPreferencesContext\);)/,
replace: "$1=$self.Layer;",
predicate: () => settings.store.disableFade
},
{ // Lazy-load contents
match: /createPromise:\(\)=>([^:}]*?),webpackId:"\d+",name:(?!="CollectiblesShop")"[^"]+"/g,
replace: "$&,_:$1",
predicate: () => settings.store.eagerLoad
}
]
},
{ // For some reason standardSidebarView also has a small fade-in
find: "DefaultCustomContentScroller:function()",
replacement: [
{
match: /\(0,\i\.useTransition\)\((\i)/,
replace: "(_cb=>_cb(void 0,$1))||$&"
},
{
match: /\i\.animated\.div/,
replace: '"div"'
}
],
predicate: () => settings.store.disableFade
},
{ // Load menu TOC eagerly
find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format",
replacement: {
match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/,
replace: "(async ()=>$2)(),"
},
predicate: () => settings.store.eagerLoad
},
{ // Settings cog context menu
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
replacement: {
match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
replace: "$self.wrapMenu($&)"
}
}
],
Layer(props: LayerProps) {
return (
<ErrorBoundary fallback={() => props.children as any}>
<Layer {...props} />
</ErrorBoundary>
);
},
wrapMenu(list: SettingsEntry[]) {
if (!settings.store.organizeMenu) return list;
const items = [{ label: null as string | null, items: [] as SettingsEntry[] }];
for (const item of list) {
if (item.section === "HEADER") {
items.push({ label: item.label, items: [] });
} else if (item.section === "DIVIDER") {
items.push({ label: i18n.Messages.OTHER_OPTIONS, items: [] });
} else {
items.at(-1)!.items.push(item);
}
}
return {
filter(predicate: (item: SettingsEntry) => boolean) {
for (const category of items) {
category.items = category.items.filter(predicate);
}
return this;
},
map(render: (item: SettingsEntry) => ReactElement) {
return items
.filter(a => a.items.length > 0)
.map(({ label, items }) => {
const children = items.map(render);
if (label) {
return (
<Menu.MenuItem
id={label.replace(/\W/, "_")}
label={label}
children={children}
action={children[0].props.action}
/>);
} else {
return children;
}
});
}
};
}
});

View file

@ -43,13 +43,13 @@ export default definePlugin({
{ {
find: "DefaultCustomizationSections", find: "DefaultCustomizationSections",
replacement: { replacement: {
match: /(?<={user:\i},"decoration"\),)/, match: /(?<=USER_SETTINGS_AVATAR_DECORATION},"decoration"\),)/,
replace: "$self.DecorSection()," replace: "$self.DecorSection(),"
} }
}, },
// Decoration modal module // Decoration modal module
{ {
find: ".decorationGridItem", find: ".decorationGridItem,",
replacement: [ replacement: [
{ {
match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/, match: /(?<==)\i=>{let{children.{20,100}decorationGridItem/,
@ -61,8 +61,8 @@ export default definePlugin({
}, },
// Remove NEW label from decor avatar decorations // Remove NEW label from decor avatar decorations
{ {
match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i;if\()(?<=avatarDecoration:(\i).+?)/, match: /(?<=\.Section\.PREMIUM_PURCHASE&&\i)(?<=avatarDecoration:(\i).+?)/,
replace: "$1.skuId===$self.SKU_ID||" replace: "||$1.skuId===$self.SKU_ID"
} }
] ]
}, },

View file

@ -29,7 +29,7 @@ export default definePlugin({
{ {
find: ".Messages.BOT_CALL_IDLE_DISCONNECT", find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
replacement: { replacement: {
match: /,?(?=this\.idleTimeout=new \i\.Timeout)/, match: /,?(?=\i\(this,"idleTimeout",new \i\.Timeout\))/,
replace: ";return;" replace: ";return;"
} }
}, },

View file

@ -56,7 +56,7 @@ function getUrl(data: Data) {
if (data.t === "Emoji") if (data.t === "Emoji")
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`; return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${data.id}.${data.isAnimated ? "gif" : "png"}`;
return `${location.origin}/stickers/${data.id}.${StickerExt[data.format_type]}`; return `${window.GLOBAL_ENV.MEDIA_PROXY_ENDPOINT}/stickers/${data.id}.${StickerExt[data.format_type]}`;
} }
async function fetchSticker(id: string) { async function fetchSticker(id: string) {

View file

@ -64,7 +64,7 @@ export default definePlugin({
} }
}, },
{ {
find: ".isStaff=()", find: '"isStaff",',
predicate: () => settings.store.enableIsStaff, predicate: () => settings.store.enableIsStaff,
replacement: [ replacement: [
{ {

View file

@ -25,6 +25,7 @@ import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
import type { CustomEmoji } from "@webpack/types";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc"; import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react"; import type { ReactElement, ReactNode } from "react";
@ -277,7 +278,7 @@ export default definePlugin({
} }
}, },
{ {
find: '.displayName="UserSettingsProtoStore"', find: '"UserSettingsProtoStore"',
replacement: [ replacement: [
{ {
// Overwrite incoming connection settings proto with our local settings // Overwrite incoming connection settings proto with our local settings
@ -388,6 +389,14 @@ export default definePlugin({
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/, match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true" replace: "true"
} }
},
// Make all Soundboard sounds available
{
find: 'type:"GUILD_SOUNDBOARD_SOUND_CREATE"',
replacement: {
match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g,
replace: "true"
}
} }
], ],
@ -776,6 +785,16 @@ export default definePlugin({
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE); UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
}, },
canUseEmote(e: CustomEmoji, channelId: string) {
if (e.require_colons === false) return true;
if (e.available === false) return false;
if (this.canUseEmotes)
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
else
return !e.animated && e.guildId === this.guildId;
},
start() { start() {
const s = settings.store; const s = settings.store;
@ -874,12 +893,8 @@ export default definePlugin({
} }
if (s.enableEmojiBypass) { if (s.enableEmojiBypass) {
const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId);
for (const emoji of messageObj.validNonShortcutEmojis) { for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue; if (this.canUseEmote(emoji, channelId)) continue;
if (emoji.available !== false && canUseEmotes) continue;
if (emoji.guildId === guildId && !emoji.animated) continue;
hasBypass = true; hasBypass = true;
@ -909,18 +924,12 @@ export default definePlugin({
this.preEdit = addPreEditListener(async (channelId, __, messageObj) => { this.preEdit = addPreEditListener(async (channelId, __, messageObj) => {
if (!s.enableEmojiBypass) return; if (!s.enableEmojiBypass) return;
const { guildId } = this;
let hasBypass = false; let hasBypass = false;
const canUseEmotes = this.canUseEmotes && hasExternalEmojiPerms(channelId);
messageObj.content = messageObj.content.replace(/(?<!\\)<a?:(?:\w+):(\d+)>/ig, (emojiStr, emojiId, offset, origStr) => { messageObj.content = messageObj.content.replace(/(?<!\\)<a?:(?:\w+):(\d+)>/ig, (emojiStr, emojiId, offset, origStr) => {
const emoji = EmojiStore.getCustomEmojiById(emojiId); const emoji = EmojiStore.getCustomEmojiById(emojiId);
if (emoji == null) return emojiStr; if (emoji == null) return emojiStr;
if (!emoji.require_colons) return emojiStr; if (this.canUseEmote(emoji, channelId)) return emojiStr;
if (emoji.available !== false && canUseEmotes) return emojiStr;
if (emoji.guildId === guildId && !emoji.animated) return emojiStr;
hasBypass = true; hasBypass = true;

View file

@ -29,10 +29,10 @@ export default definePlugin({
authors: [Devs.Ven], authors: [Devs.Ven],
patches: [{ patches: [{
find: ".handleSelectGIF=", find: '"handleSelectGIF",',
replacement: { replacement: {
match: /\.handleSelectGIF=(\i)=>\{/, match: /"handleSelectGIF",(\i)=>\{/,
replace: ".handleSelectGIF=$1=>{if (!this.props.className) return $self.handleSelect($1);" replace: '"handleSelectGIF",$1=>{if (!this.props.className) return $self.handleSelect($1);'
} }
}], }],

View file

@ -210,10 +210,10 @@ export default definePlugin({
patches: [ patches: [
{ {
find: '.displayName="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

@ -23,7 +23,7 @@ import { makeRange } from "@components/PluginSettings/components";
import { debounce } from "@shared/debounce"; import { debounce } from "@shared/debounce";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Menu, React, ReactDOM } from "@webpack/common"; import { Menu, ReactDOM } from "@webpack/common";
import type { Root } from "react-dom/client"; import type { Root } from "react-dom/client";
import { Magnifier, MagnifierProps } from "./components/Magnifier"; import { Magnifier, MagnifierProps } from "./components/Magnifier";
@ -168,7 +168,7 @@ export default definePlugin({
}, },
{ {
find: "handleImageLoad=", find: ".handleImageLoad)",
replacement: [ replacement: [
{ {
match: /placeholderVersion:\i,/, match: /placeholderVersion:\i,/,

View file

@ -81,11 +81,11 @@ export default definePlugin({
find: ".LOADING_DID_YOU_KNOW}", find: ".LOADING_DID_YOU_KNOW}",
replacement: [ replacement: [
{ {
match: /\._loadingText=function\(\)\{/, match: /"_loadingText",function\(\)\{/,
replace: "$&return $self.quote;", replace: "$&return $self.quote;",
}, },
{ {
match: /\._eventLoadingText=function\(\)\{/, match: /"_eventLoadingText",function\(\)\{/,
replace: "$&return $self.quote;", replace: "$&return $self.quote;",
predicate: () => settings.store.replaceEvents predicate: () => settings.store.replaceEvents
} }

View file

@ -27,7 +27,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
const { groups } = useStateFromStores( const { groups } = useStateFromStores(
[ChannelMemberStore], [ChannelMemberStore],
() => ChannelMemberStore.getProps(guildId, currentChannel.id) () => ChannelMemberStore.getProps(guildId, currentChannel?.id)
); );
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) { if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {

View file

@ -255,7 +255,7 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
delete msg.embeds; delete msg.embeds;
delete msg.interaction; delete msg.interaction;
messageFetchQueue.push(() => fetchMessage(channelID, messageID) messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)
.then(m => m && FluxDispatcher.dispatch({ .then(m => m && FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE", type: "MESSAGE_UPDATE",
message: msg message: msg

View file

@ -137,6 +137,16 @@ export default definePlugin({
], ],
onChange: () => addDeleteStyle() onChange: () => addDeleteStyle()
}, },
logDeletes: {
type: OptionType.BOOLEAN,
description: "Whether to log deleted messages",
default: true,
},
logEdits: {
type: OptionType.BOOLEAN,
description: "Whether to log edited messages",
default: true,
},
ignoreBots: { ignoreBots: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Whether to ignore messages by bots", description: "Whether to ignore messages by bots",
@ -197,8 +207,8 @@ export default definePlugin({
return cache; return cache;
}, },
shouldIgnore(message: any) { shouldIgnore(message: any, isEdit = false) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger; const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds, logEdits, logDeletes } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id; const myId = UserStore.getCurrentUser().id;
return ignoreBots && message.author?.bot || return ignoreBots && message.author?.bot ||
@ -206,6 +216,7 @@ export default definePlugin({
ignoreUsers.includes(message.author?.id) || ignoreUsers.includes(message.author?.id) ||
ignoreChannels.includes(message.channel_id) || ignoreChannels.includes(message.channel_id) ||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) || ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
(isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id); ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
}, },
@ -214,7 +225,7 @@ export default definePlugin({
{ {
// MessageStore // MessageStore
// Module 171447 // Module 171447
find: "displayName=\"MessageStore\"", find: '"MessageStore"',
replacement: [ replacement: [
{ {
// Add deleted=true to all target messages in the MESSAGE_DELETE event // Add deleted=true to all target messages in the MESSAGE_DELETE event
@ -241,7 +252,7 @@ export default definePlugin({
match: /(MESSAGE_UPDATE:function\((\i)\).+?)\.update\((\i)/, match: /(MESSAGE_UPDATE:function\((\i)\).+?)\.update\((\i)/,
replace: "$1" + replace: "$1" +
".update($3,m =>" + ".update($3,m =>" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" + " (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" +
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" + " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" + " m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" + " m" +
@ -369,7 +380,7 @@ export default definePlugin({
{ {
// ReferencedMessageStore // ReferencedMessageStore
// Module 778667 // Module 778667
find: "displayName=\"ReferencedMessageStore\"", find: '"ReferencedMessageStore"',
replacement: [ replacement: [
{ {
match: /MESSAGE_DELETE:function\((\i)\).+?},/, match: /MESSAGE_DELETE:function\((\i)\).+?},/,

View file

@ -38,8 +38,8 @@ export default definePlugin({
] ]
}, },
...[ ...[
'displayName="MessageStore"', '="MessageStore",',
'displayName="ReadStateStore"' '"displayName","ReadStateStore")'
].map(find => ({ ].map(find => ({
find, find,
predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true, predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true,

View file

@ -27,7 +27,7 @@ export default definePlugin({
{ {
find: '.ensureModule("discord_rpc")', find: '.ensureModule("discord_rpc")',
replacement: { replacement: {
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)\)}/, match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/,
replace: '.ensureModule("discord_rpc")}', replace: '.ensureModule("discord_rpc")}',
}, },
}, },

View file

@ -0,0 +1,54 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
defaultLayout: {
type: OptionType.SELECT,
options: [
{ label: "List", value: 1, default: true },
{ label: "Gallery", value: 2 }
],
description: "Which layout to use as default"
},
defaultSortOrder: {
type: OptionType.SELECT,
options: [
{ label: "Recently Active", value: 0, default: true },
{ label: "Date Posted", value: 1 }
],
description: "Which sort order to use as default"
}
});
export default definePlugin({
name: "OverrideForumDefaults",
description: "Allows you to override default forum layout/sort order. you can still change it on a per-channel basis",
authors: [Devs.Inbestigator],
patches: [
{
find: "getDefaultLayout(){",
replacement: [
{
match: /getDefaultLayout\(\){/,
replace: "$&return $self.getLayout();"
},
{
match: /getDefaultSortOrder\(\){/,
replace: "$&return $self.getSortOrder();"
}
]
}
],
getLayout: () => settings.store.defaultLayout,
getSortOrder: () => settings.store.defaultSortOrder,
settings
});

View file

@ -0,0 +1,134 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Forms, Text, TextInput, Toasts, useEffect, useState } from "@webpack/common";
import { DEFAULT_COLOR, SWATCHES } from "../constants";
import { categories, Category, createCategory, getCategory, updateCategory } from "../data";
import { forceUpdate } from "../index";
interface ColorPickerProps {
color: number | null;
showEyeDropper?: boolean;
suggestedColors?: string[];
onChange(value: number | null): void;
}
interface ColorPickerWithSwatchesProps {
defaultColor: number;
colors: number[];
value: number;
disabled?: boolean;
onChange(value: number | null): void;
renderDefaultButton?: () => React.ReactNode;
renderCustomButton?: () => React.ReactNode;
}
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>("presets,", "customColor:");
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\).{0,50}"UserSettings"/);
const cl = classNameFactory("vc-pindms-modal-");
interface Props {
categoryId: string | null;
initalChannelId: string | null;
modalProps: ModalProps;
}
function useCategory(categoryId: string | null, initalChannelId: string | null) {
const [category, setCategory] = useState<Category | null>(null);
useEffect(() => {
if (categoryId)
setCategory(getCategory(categoryId)!);
else if (initalChannelId)
setCategory({
id: Toasts.genId(),
name: `Pin Category ${categories.length + 1}`,
color: DEFAULT_COLOR,
collapsed: false,
channels: [initalChannelId]
});
}, [categoryId, initalChannelId]);
return {
category,
setCategory
};
}
export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) {
const { category, setCategory } = useCategory(categoryId, initalChannelId);
if (!category) return null;
const onSave = async (e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
if (!categoryId)
await createCategory(category);
else
await updateCategory(category);
forceUpdate();
modalProps.onClose();
};
return (
<ModalRoot {...modalProps}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{categoryId ? "Edit" : "New"} Category</Text>
</ModalHeader>
{/* form is here so when you press enter while in the text input it submits */}
<form onSubmit={onSave}>
<ModalContent className={cl("content")}>
<Forms.FormSection>
<Forms.FormTitle>Name</Forms.FormTitle>
<TextInput
value={category.name}
onChange={e => setCategory({ ...category, name: e })}
/>
</Forms.FormSection>
<Forms.FormDivider />
<Forms.FormSection>
<Forms.FormTitle>Color</Forms.FormTitle>
<ColorPickerWithSwatches
key={category.name}
defaultColor={DEFAULT_COLOR}
colors={SWATCHES}
onChange={c => setCategory({ ...category, color: c! })}
value={category.color}
renderDefaultButton={() => null}
renderCustomButton={() => (
<ColorPicker
color={category.color}
onChange={c => setCategory({ ...category, color: c! })}
key={category.name}
showEyeDropper={false}
/>
)}
/>
</Forms.FormSection>
</ModalContent>
<ModalFooter>
<Button type="submit" onClick={onSave} disabled={!category.name}>{categoryId ? "Save" : "Create"}</Button>
</ModalFooter>
</form>
</ModalRoot>
);
}
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
openModalLazy(async () => {
await requireSettingsMenu();
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initalChannelId={channelId} />;
});

View file

@ -0,0 +1,96 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Menu } from "@webpack/common";
import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data";
import { forceUpdate, PinOrder, settings } from "../index";
import { openCategoryModal } from "./CreateCategoryModal";
function createPinMenuItem(channelId: string) {
const pinned = isPinned(channelId);
return (
<Menu.MenuItem
id="pin-dm"
label="Pin DMs"
>
{!pinned && (
<>
<Menu.MenuItem
id="vc-add-category"
label="Add Category"
color="brand"
action={() => openCategoryModal(null, channelId)}
/>
<Menu.MenuSeparator />
{
categories.map(category => (
<Menu.MenuItem
id={`pin-category-${category.name}`}
label={category.name}
action={() => addChannelToCategory(channelId, category.id).then(forceUpdate)}
/>
))
}
</>
)}
{pinned && (
<>
<Menu.MenuItem
id="unpin-dm"
label="Unpin DM"
color="danger"
action={() => removeChannelFromCategory(channelId).then(forceUpdate)}
/>
{
settings.store.pinOrder === PinOrder.Custom && canMoveChannelInDirection(channelId, -1) && (
<Menu.MenuItem
id="move-up"
label="Move Up"
action={() => moveChannel(channelId, -1).then(forceUpdate)}
/>
)
}
{
settings.store.pinOrder === PinOrder.Custom && canMoveChannelInDirection(channelId, 1) && (
<Menu.MenuItem
id="move-down"
label="Move Down"
action={() => moveChannel(channelId, 1).then(forceUpdate)}
/>
)
}
</>
)}
</Menu.MenuItem>
);
}
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("leave-channel", children);
container?.unshift(createPinMenuItem(props.channel.id));
};
const UserContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("close-dm", children);
if (container) {
const idx = container.findIndex(c => c?.props?.id === "close-dm");
container.splice(idx, 0, createPinMenuItem(props.channel.id));
}
};
export const contextMenus = {
"gdm-context": GroupDMContext,
"user-context": UserContext
};

View file

@ -0,0 +1,32 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const DEFAULT_CHUNK_SIZE = 256;
export const DEFAULT_COLOR = 10070709;
export const SWATCHES = [
1752220,
3066993,
3447003,
10181046,
15277667,
15844367,
15105570,
15158332,
9807270,
6323595,
1146986,
2067276,
2123412,
7419530,
11342935,
12745742,
11027200,
10038562,
9936031,
5533306
];

View file

@ -1,70 +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 { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Menu } from "@webpack/common";
import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings";
function PinMenuItem(channelId: string) {
const pinned = isPinned(channelId);
const canMove = pinned && settings.store.pinOrder === PinOrder.Custom;
return (
<>
<Menu.MenuItem
id="pin-dm"
label={pinned ? "Unpin DM" : "Pin DM"}
action={() => togglePin(channelId)}
/>
{canMove && snapshotArray[0] !== channelId && (
<Menu.MenuItem
id="move-pin-up"
label="Move Pin Up"
action={() => movePin(channelId, -1)}
/>
)}
{canMove && snapshotArray[snapshotArray.length - 1] !== channelId && (
<Menu.MenuItem
id="move-pin-down"
label="Move Pin Down"
action={() => movePin(channelId, +1)}
/>
)}
</>
);
}
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("leave-channel", children);
if (container)
container.unshift(PinMenuItem(props.channel.id));
};
const UserContext: NavContextMenuPatchCallback = (children, props) => {
const container = findGroupChildrenByChildId("close-dm", children);
if (container) {
const idx = container.findIndex(c => c?.props?.id === "close-dm");
container.splice(idx, 0, PinMenuItem(props.channel.id));
}
};
export const contextMenus = {
"gdm-context": GroupDMContext,
"user-context": UserContext
};

214
src/plugins/pinDms/data.ts Normal file
View file

@ -0,0 +1,214 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { UserStore } from "@webpack/common";
import { DEFAULT_COLOR } from "./constants";
import { forceUpdate } from "./index";
export interface Category {
id: string;
name: string;
color: number;
channels: string[];
collapsed?: boolean;
}
const CATEGORY_BASE_KEY = "PinDMsCategories-";
const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs";
const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories";
const OLD_CATEGORY_KEY = "BetterPinDMsCategories-";
export let categories: Category[] = [];
export async function saveCats(cats: Category[]) {
const { id } = UserStore.getCurrentUser();
await DataStore.set(CATEGORY_BASE_KEY + id, cats);
}
export async function init() {
const id = UserStore.getCurrentUser()?.id;
await initCategories(id);
await migrateData(id);
forceUpdate();
}
export async function initCategories(userId: string) {
categories = await DataStore.get<Category[]>(CATEGORY_BASE_KEY + userId) ?? [];
}
export function getCategory(id: string) {
return categories.find(c => c.id === id);
}
export async function createCategory(category: Category) {
categories.push(category);
await saveCats(categories);
}
export async function updateCategory(category: Category) {
const index = categories.findIndex(c => c.id === category.id);
if (index === -1) return;
categories[index] = category;
await saveCats(categories);
}
export async function addChannelToCategory(channelId: string, categoryId: string) {
const category = categories.find(c => c.id === categoryId);
if (!category) return;
if (category.channels.includes(channelId)) return;
category.channels.push(channelId);
await saveCats(categories);
}
export async function removeChannelFromCategory(channelId: string) {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return;
category.channels = category.channels.filter(c => c !== channelId);
await saveCats(categories);
}
export async function removeCategory(categoryId: string) {
const catagory = categories.find(c => c.id === categoryId);
if (!catagory) return;
// catagory?.channels.forEach(c => removeChannelFromCategory(c));
categories = categories.filter(c => c.id !== categoryId);
await saveCats(categories);
}
export async function collapseCategory(id: string, value = true) {
const category = categories.find(c => c.id === id);
if (!category) return;
category.collapsed = value;
await saveCats(categories);
}
// utils
export function isPinned(id: string) {
return categories.some(c => c.channels.includes(id));
}
export function categoryLen() {
return categories.length;
}
export function getAllUncollapsedChannels() {
return categories.filter(c => !c.collapsed).map(c => c.channels).flat();
}
export function getSections() {
return categories.reduce((acc, category) => {
acc.push(category.channels.length === 0 ? 1 : category.channels.length);
return acc;
}, [] as number[]);
}
// move categories
export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => {
const a = array[index];
const b = array[index + direction];
return a && b;
};
export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => {
const index = categories.findIndex(m => m.id === id);
return canMoveArrayInDirection(categories, index, direction);
};
export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1);
export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return false;
const index = category.channels.indexOf(channelId);
return canMoveArrayInDirection(category.channels, index, direction);
};
function swapElementsInArray(array: any[], index1: number, index2: number) {
if (!array[index1] || !array[index2]) return;
[array[index1], array[index2]] = [array[index2], array[index1]];
}
// stolen from PinDMs
export async function moveCategory(id: string, direction: -1 | 1) {
const a = categories.findIndex(m => m.id === id);
const b = a + direction;
swapElementsInArray(categories, a, b);
await saveCats(categories);
}
export async function moveChannel(channelId: string, direction: -1 | 1) {
const category = categories.find(c => c.channels.includes(channelId));
if (!category) return;
const a = category.channels.indexOf(channelId);
const b = a + direction;
swapElementsInArray(category.channels, a, b);
await saveCats(categories);
}
// migrate data
const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
async function migratePinDMs() {
if (categories.some(m => m.id === "oldPins")) {
return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
}
const pindmspins = getPinDMsPins();
// we dont want duplicate pins
const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m)));
if (difference?.length) {
categories.push({
id: "oldPins",
name: "Pins",
color: DEFAULT_COLOR,
channels: difference
});
}
await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true);
}
async function migrateOldCategories(userId: string) {
const oldCats = await DataStore.get<Category[]>(OLD_CATEGORY_KEY + userId);
// dont want to migrate if the user has already has categories.
if (categories.length === 0 && oldCats?.length) {
categories.push(...(oldCats.filter(m => m.id !== "oldPins")));
}
await DataStore.set(CATEGORY_MIGRATED_KEY, true);
}
export async function migrateData(userId: string) {
const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY);
if (m1 && m2) return;
// want to migrate the old categories first and then slove any conflicts with the PinDMs pins
if (!m1) await migrateOldCategories(userId);
if (!m2) await migratePinDMs();
await saveCats(categories);
}

View file

@ -1,116 +1,134 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common";
import { Channel } from "discord-types/general"; import { Channel } from "discord-types/general";
import { contextMenus } from "./contextMenus"; import { contextMenus } from "./components/contextMenu";
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings"; import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal";
import { DEFAULT_CHUNK_SIZE } from "./constants";
import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data";
interface ChannelComponentProps {
children: React.ReactNode,
channel: Channel,
selected: boolean;
}
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
export let instance: any;
export const forceUpdate = () => instance?.props?._forceUpdate?.();
export const enum PinOrder {
LastMessage,
Custom
}
export const settings = definePluginSettings({
pinOrder: {
type: OptionType.SELECT,
description: "Which order should pinned DMs be displayed in?",
options: [
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
],
onChange: () => forceUpdate()
},
dmSectioncollapsed: {
type: OptionType.BOOLEAN,
description: "Collapse DM sections",
default: false,
onChange: () => forceUpdate()
}
});
export default definePlugin({ export default definePlugin({
name: "PinDMs", name: "PinDMs",
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs", description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
authors: [Devs.Ven], authors: [Devs.Ven, Devs.Aria],
settings, settings,
contextMenus, contextMenus,
usePinCount(channelIds: string[]) {
const pinnedDms = usePinnedDms();
// See comment on 2nd patch for reasoning
return channelIds.length ? [pinnedDms.size] : [];
},
getChannel(channels: Record<string, Channel>, idx: number) {
return channels[getPinAt(idx)];
},
isPinned,
getSnapshot: sortedSnapshot,
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
if (!isPinned(channelId))
return (
(rowHeight + padding) * 2 // header
+ rowHeight * snapshotArray.length // pins
+ originalOffset // original pin offset minus pins
);
return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding;
},
patches: [ patches: [
// Patch DM list
{ {
find: ".privateChannelsHeaderContainer,", find: ".privateChannelsHeaderContainer,",
replacement: [ replacement: [
{ {
// filter Discord's privateChannelIds list to remove pins, and pass // Filter out pinned channels from the private channel list
// pinCount as prop. This needs to be here so that the entire DM list receives match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/,
// updates on pin/unpin replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))"
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i),/,
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1),"
}, },
{ {
// sections is an array of numbers, where each element is a section and // Insert the pinned channels to sections
// the number is the amount of rows. Add our pinCount in second place match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/,
// - Section 1: buttons for pages like Friends & Library replace: "...$self.makeProps(this,{$&})"
// - Section 2: our pinned dms },
// - Section 3: the normal dm list
match: /(?<=renderRow:this\.renderRow,)sections:\[\i,/, // Rendering
// For some reason, adding our sections when no private channels are ready yet {
// makes DMs infinitely load. Thus usePinCount returns either a single element match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
// array with the count, or an empty array. Due to spreading, only in the former replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);"
// case will an element be added to the outer array
// Thanks for the fix, Strencher!
replace: "$&...this.props.pinCount??[],"
}, },
{ {
// Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages" match: /"renderSection",(\i)=>{/,
// lookbehind is used to lookup parameter name. We could use arguments[0], but replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);"
// if children ever is wrapped in an iife, it will break
match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=(\i)=>{.+?)/,
replace: "children:$2.section===1?'Pinned DMs':$1"
}, },
{ {
// Patch channel lookup inside renderDM match: /(?<=span",{)className:\i\.headerText,/,
// channel=channels[channelIds[row]]; replace: "...$self.makeSpanProps(),$&"
match: /(?<=renderDM=\((\i),(\i)\)=>{.*?this.state,\i=\i\[\i\],\i=)((\i)\[\i\]);/, },
// section 1 is us, manually get our own channel
// section === 1 ? getChannel(channels, row) : channels[channelIds[row]]; // Fix Row Height
replace: "$1===1?$self.getChannel($4,$2):$3;" {
match: /(?<="getRowHeight",.{1,100}return 1===)\i/,
replace: "($&-$self.categoryLen())"
}, },
{ {
// Fix getRowHeight's check for whether this is the DMs section match: /"getRowHeight",\((\i),(\i)\)=>{/,
// DMS (inlined) === section replace: "$&if($self.isChannelHidden($1,$2))return 0;"
match: /(?<=getRowHeight=\(.{2,50}?)1===\i/,
// DMS (inlined) === section - 1
replace: "$&-1"
}, },
// Fix ScrollTo
{ {
// Override scrollToChannel to properly account for pinned channels // Override scrollToChannel to properly account for pinned channels
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/, match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)" replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
} },
{
match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/,
replace: "[...$&,...$self.getAllUncollapsedChannels()]"
},
] ]
}, },
// forceUpdate moment
// https://regex101.com/r/kDN9fO/1
{
find: ".FRIENDS},\"friends\"",
replacement: {
match: /(?<=\i=\i=>{).{1,100}premiumTabSelected.{1,800}showDMHeader:.+?,/,
replace: "let forceUpdate = Vencord.Util.useForceUpdater();$&_forceUpdate:forceUpdate,"
}
},
// Fix Alt Up/Down navigation // Fix Alt Up/Down navigation
{ {
find: ".Routes.APPLICATION_STORE&&", find: ".Routes.APPLICATION_STORE&&",
@ -118,16 +136,225 @@ export default definePlugin({
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)] // channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/, match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c))) // ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))" replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
} }
}, },
// fix alt+shift+up/down // fix alt+shift+up/down
{ {
find: ".getFlattenedGuildIds()],", find: ".getFlattenedGuildIds()],",
replacement: { replacement: {
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/, match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))" replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
} }
}, },
] ],
sections: null as number[] | null,
set _instance(i: any) {
this.instance = i;
instance = i;
},
startAt: StartAt.WebpackReady,
start: init,
flux: {
CONNECTION_OPEN: init,
},
isPinned,
categoryLen,
getSections,
getAllUncollapsedChannels,
requireSettingsMenu,
makeProps(instance, { sections }: { sections: number[]; }) {
this._instance = instance;
this.sections = sections;
this.sections.splice(1, 0, ...this.getSections());
if (this.instance?.props?.privateChannelIds?.length === 0) {
// dont render direct messages header
this.sections[this.sections.length - 1] = 0;
}
return {
sections: this.sections,
chunkSize: this.getChunkSize(),
};
},
makeSpanProps() {
return {
onClick: () => this.collapseDMList(),
role: "button",
style: { cursor: "pointer" }
};
},
getChunkSize() {
// the chunk size is the amount of rows (measured in pixels) that are rendered at once (probably)
// the higher the chunk size, the more rows are rendered at once
// also if the chunk size is 0 it will render everything at once
const sections = this.getSections();
const sectionHeaderSizePx = sections.length * 40;
// (header heights + DM heights + DEFAULT_CHUNK_SIZE) * 1.5
// we multiply everything by 1.5 so it only gets unmounted after the entire list is off screen
return (sectionHeaderSizePx + sections.reduce((acc, v) => acc += v + 44, 0) + DEFAULT_CHUNK_SIZE) * 1.5;
},
isCategoryIndex(sectionIndex: number) {
return this.sections && sectionIndex > 0 && sectionIndex < this.sections.length - 1;
},
isChannelIndex(sectionIndex: number, channelIndex: number) {
if (settings.store.dmSectioncollapsed && sectionIndex !== 0)
return true;
const cat = categories[sectionIndex - 1];
return this.isCategoryIndex(sectionIndex) && (cat?.channels?.length === 0 || cat?.channels[channelIndex]);
},
isDMSectioncollapsed() {
return settings.store.dmSectioncollapsed;
},
collapseDMList() {
settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed;
forceUpdate();
},
isChannelHidden(categoryIndex: number, channelIndex: number) {
if (categoryIndex === 0) return false;
if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex)
return true;
if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false;
const category = categories[categoryIndex - 1];
if (!category) return false;
return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex];
},
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
if (!isPinned(channelId))
return (
(rowHeight + padding) * 2 // header
+ rowHeight * this.getAllUncollapsedChannels().length // pins
+ originalOffset // original pin offset minus pins
);
return rowHeight * (this.getAllUncollapsedChannels().indexOf(channelId) + preRenderedChildren) + padding;
},
renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => {
const category = categories[section - 1];
if (!category) return null;
return (
<h2
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
onClick={async () => {
await collapseCategory(category.id, !category.collapsed);
forceUpdate();
}}
onContextMenu={e => {
ContextMenuApi.openContextMenu(e, () => (
<Menu.Menu
navId="vc-pindms-header-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
color="danger"
aria-label="Pin DMs Category Menu"
>
<Menu.MenuItem
id="vc-pindms-edit-category"
label="Edit Category"
action={() => openCategoryModal(category.id, null)}
/>
{
canMoveCategory(category.id) && (
<>
{
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
id="vc-pindms-move-category-up"
label="Move Up"
action={() => moveCategory(category.id, -1).then(() => forceUpdate())}
/>
}
{
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
id="vc-pindms-move-category-down"
label="Move Down"
action={() => moveCategory(category.id, 1).then(() => forceUpdate())}
/>
}
</>
)
}
<Menu.MenuSeparator />
<Menu.MenuItem
id="vc-pindms-delete-category"
color="danger"
label="Delete Category"
action={() => removeCategory(category.id).then(() => forceUpdate())}
/>
</Menu.Menu>
));
}}
>
<span className={headerClasses.headerText}>
{category?.name ?? "uh oh"}
</span>
<svg className="vc-pindms-collapse-icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
</svg>
</h2>
);
}),
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
if (!channel || !category) return null;
if (this.isChannelHidden(sectionIndex, index)) return null;
return (
<ChannelComponent
channel={channel}
selected={this.instance.props.selectedChannelId === channel.id}
>
{channel.id}
</ChannelComponent>
);
},
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
const category = categories[sectionIndex - 1];
if (!category) return { channel: null, category: null };
const channelId = this.getCategoryChannels(category)[index];
return { channel: channels[channelId], category };
},
getCategoryChannels(category: Category) {
if (category.channels.length === 0) return [];
if (settings.store.pinOrder === PinOrder.LastMessage) {
return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category.channels.includes(c));
}
return category?.channels ?? [];
}
}); });

View file

@ -1,94 +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 { definePluginSettings, Settings, useSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
export const enum PinOrder {
LastMessage,
Custom
}
export const settings = definePluginSettings({
pinOrder: {
type: OptionType.SELECT,
description: "Which order should pinned DMs be displayed in?",
options: [
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
]
}
});
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore");
export let snapshotArray: string[];
let snapshot: Set<string> | undefined;
const getArray = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
const save = (pins: string[]) => {
snapshot = void 0;
Settings.plugins.PinDMs.pinnedDMs = pins.join(",");
};
const takeSnapshot = () => {
snapshotArray = getArray() ?? [];
return snapshot = new Set<string>(snapshotArray);
};
const requireSnapshot = () => snapshot ?? takeSnapshot();
export function usePinnedDms() {
useSettings(["plugins.PinDMs.pinnedDMs"]);
return requireSnapshot();
}
export function isPinned(id: string) {
return requireSnapshot().has(id);
}
export function togglePin(id: string) {
const snapshot = requireSnapshot();
if (!snapshot.delete(id)) {
snapshot.add(id);
}
save([...snapshot]);
}
export function sortedSnapshot() {
requireSnapshot();
if (settings.store.pinOrder === PinOrder.LastMessage)
return PrivateChannelSortStore.getPrivateChannelIds().filter(isPinned);
return snapshotArray;
}
export function getPinAt(idx: number) {
return sortedSnapshot()[idx];
}
export function movePin(id: string, direction: -1 | 1) {
const pins = getArray()!;
const a = pins.indexOf(id);
const b = a + direction;
[pins[a], pins[b]] = [pins[b], pins[a]];
save(pins);
}

View file

@ -0,0 +1,37 @@
.vc-pindms-section-container {
box-sizing: border-box;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-transform: uppercase;
font-size: 12px;
line-height: 16px;
letter-spacing: .02em;
font-family: var(--font-display);
font-weight: 600;
flex: 1 1 auto;
color: var(--channels-default);
cursor: pointer;
}
.vc-pindms-modal-content {
display: grid;
justify-content: center;
padding: 1rem;
gap: 1.5rem;
}
.vc-pindms-modal-content [class^="defaultContainer"] {
display: none;
}
.vc-pindms-collapse-icon {
width: 16px;
height: 16px;
color: var(--interactive-normal);
transform: rotate(90deg)
}
.vc-pindms-collapsed .vc-pindms-collapse-icon {
transform: rotate(0deg);
}

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 "./style.css";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
@ -49,9 +51,11 @@ const ReadAllButton = () => (
<Button <Button
onClick={onClick} onClick={onClick}
size={Button.Sizes.MIN} size={Button.Sizes.MIN}
color={Button.Colors.BRAND} color={Button.Colors.CUSTOM}
style={{ marginTop: "2px", marginBottom: "8px", marginLeft: "9px" }} className="vc-ranb-button"
>Read all</Button> >
Read All
</Button>
); );
export default definePlugin({ export default definePlugin({

View file

@ -0,0 +1,11 @@
.vc-ranb-button {
color: var(--interactive-normal);
padding: 0 0.5em;
margin-bottom: 0.5em;
width: 100%;
box-sizing: border-box;
}
.vc-ranb-button:hover {
color: var(--interactive-active);
}

View file

@ -59,7 +59,7 @@ export default definePlugin({
find: "GuildHomeFeedbackExperiment.definition.id", find: "GuildHomeFeedbackExperiment.definition.id",
replacement: [ replacement: [
{ {
match: /return{showFeedback:\i,setOnDismissedFeedback:(\i)}/, match: /return{showFeedback:.+?,setOnDismissedFeedback:(\i)}/,
replace: "return{showFeedback:false,setOnDismissedFeedback:$1}" replace: "return{showFeedback:false,setOnDismissedFeedback:$1}"
} }
] ]

View file

@ -30,9 +30,9 @@ export default definePlugin({
patches: [ patches: [
{ {
find: ".removeObscurity=", find: ".removeObscurity,",
replacement: { replacement: {
match: /(?<=\.removeObscurity=(\i)=>{)/, match: /(?<="removeObscurity",(\i)=>{)/,
replace: (_, event) => `$self.reveal(${event});` replace: (_, event) => `$self.reveal(${event});`
} }
} }

View file

@ -31,7 +31,7 @@ import ReviewComponent from "./ReviewComponent";
const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms"); const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms");
const { ChatInputTypes } = findByPropsLazy("ChatInputTypes"); const { ChatInputTypes } = findByPropsLazy("ChatInputTypes");
const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA"); const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA", "input");
const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer"); const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer");
interface UserProps { interface UserProps {

View file

@ -71,7 +71,7 @@ export const settings = definePluginSettings({
</Button> </Button>
<Button onClick={async () => { <Button onClick={async () => {
let url = "https://reviewdb.mantikafasi.dev/"; let url = "https://reviewdb.mantikafasi.dev";
const token = await getToken(); const token = await getToken();
if (token) if (token)
url += "/api/redirect?token=" + encodeURIComponent(token); url += "/api/redirect?token=" + encodeURIComponent(token);

View file

@ -16,7 +16,7 @@ export default definePlugin({
{ {
find: "call_ringing_beat\"", find: "call_ringing_beat\"",
replacement: { replacement: {
match: /500===\i\.random\(1,1e3\)/, match: /500===\i\(\)\.random\(1,1e3\)/,
replace: "true" replace: "true"
} }
}, },

View file

@ -138,7 +138,7 @@ export default definePlugin({
all: true, all: true,
// Render null instead of the buttons if the channel is hidden // Render null instead of the buttons if the channel is hidden
replacement: { replacement: {
match: /(?<=renderOpenChatButton=\(\)=>{)/, match: /(?<="renderOpenChatButton",\(\)=>{)/,
replace: "if($self.isHiddenChannel(this.props.channel))return null;" replace: "if($self.isHiddenChannel(this.props.channel))return null;"
} }
}, },
@ -191,10 +191,10 @@ export default definePlugin({
}, },
{ {
// Hide the new version of unreads box for hidden channels // Hide the new version of unreads box for hidden channels
find: '.displayName="ChannelListUnreadsStore"', find: '="ChannelListUnreadsStore",',
replacement: { replacement: {
match: /(?<=if\(null==(\i))(?=.{0,160}?getHasImportantUnread\)\(\i\))/g, // Global because Discord has multiple methods like that in the same module match: /(?=&&\(0,\i\.getHasImportantUnread\)\((\i)\))/g, // Global because Discord has multiple methods like that in the same module
replace: (_, channel) => `||$self.isHiddenChannel(${channel})` replace: (_, channel) => `&&!$self.isHiddenChannel(${channel})`
} }
}, },
{ {
@ -218,19 +218,19 @@ export default definePlugin({
find: "Missing channel in Channel.renderHeaderToolbar", find: "Missing channel in Channel.renderHeaderToolbar",
replacement: [ replacement: [
{ {
match: /(?<=renderHeaderToolbar=\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
}, },
{ {
match: /(?<=renderHeaderToolbar=\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/,
replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}`
}, },
{ {
match: /renderMobileToolbar=\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/, match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/,
replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;` replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;`
}, },
{ {
match: /(?<=renderHeaderBar=\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/, match: /(?<="renderHeaderBar",\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/,
replace: (_, channel) => `||$self.isHiddenChannel(${channel})` replace: (_, channel) => `||$self.isHiddenChannel(${channel})`
}, },
{ {
@ -442,7 +442,7 @@ export default definePlugin({
} }
}, },
{ {
find: '.displayName="GuildChannelStore"', find: '="GuildChannelStore",',
replacement: [ replacement: [
{ {
// Make GuildChannelStore contain hidden channels // Make GuildChannelStore contain hidden channels
@ -465,7 +465,7 @@ export default definePlugin({
} }
}, },
{ {
find: '.displayName="NowPlayingViewStore"', find: '="NowPlayingViewStore",',
replacement: { replacement: {
// Make active now voice states on hidden channels // Make active now voice states on hidden channels
match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/, match: /(getVoiceStateForUser.{0,150}?)&&\i\.\i\.canWithPartialContext.{0,20}VIEW_CHANNEL.+?}\)(?=\?)/,

View file

@ -51,7 +51,7 @@ export default definePlugin({
}, },
}, },
{ {
find: '.displayName="SpotifyStore"', find: '"displayName","SpotifyStore")',
replacement: [ replacement: [
{ {
predicate: () => settings.store.noSpotifyAutoPause, predicate: () => settings.store.noSpotifyAutoPause,

View file

@ -98,8 +98,8 @@ export default definePlugin({
{ {
find: ".popularApplicationCommandIds,", find: ".popularApplicationCommandIds,",
replacement: { replacement: {
match: /\(0,\i\.jsx\)\(\i\.\i,{user:\i,setNote/, match: /applicationId:\i\.id}\),(?=.{0,50}setNote:\i)/,
replace: "$self.patchPopout(arguments[0]),$&", replace: "$&$self.patchPopout(arguments[0]),",
} }
}, },
// below username // below username

View file

@ -174,7 +174,7 @@ export default definePlugin({
find: ".NITRO_BANNER,", find: ".NITRO_BANNER,",
replacement: { replacement: {
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl, // style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
match: /style:\{(?=backgroundImage:(\i&&\i)\?"url\("\.concat\((\i),)/, match: /style:\{(?=backgroundImage:(\i)\?"url\("\.concat\((\i),)/,
replace: replace:
// onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0, // onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,' 'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'

View file

@ -71,7 +71,7 @@ export default definePlugin({
}, },
// Prevent the MediaEngineStore from overwriting our LocalVolumes above 200 with the ones the Discord Audio Context Settings sync sends // Prevent the MediaEngineStore from overwriting our LocalVolumes above 200 with the ones the Discord Audio Context Settings sync sends
{ {
find: '.displayName="MediaEngineStore"', find: '="MediaEngineStore",',
replacement: [ replacement: [
{ {
match: /(\.settings\.audioContextSettings.+?)(\i\[\i\])=(\i\.volume)(.+?setLocalVolume\(\i,).+?\)/, match: /(\.settings\.audioContextSettings.+?)(\i\[\i\])=(\i\.volume)(.+?setLocalVolume\(\i,).+?\)/,

View file

@ -195,7 +195,7 @@ export default definePlugin({
// Add back "Show My Camera" context menu // Add back "Show My Camera" context menu
{ {
find: '.default("MediaEngineWebRTC");', find: '"MediaEngineWebRTC");',
replacement: { replacement: {
match: /supports\(\i\)\{switch\(\i\)\{case (\i).Features/, match: /supports\(\i\)\{switch\(\i\)\{case (\i).Features/,
replace: "$&.DISABLE_VIDEO:return true;case $1.Features" replace: "$&.DISABLE_VIDEO:return true;case $1.Features"

View file

@ -29,7 +29,7 @@ import { Message, ReactionEmoji, User } from "discord-types/general";
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
let Scroll: any = null;
const queue = new Queue(); const queue = new Queue();
let reactions: Record<string, ReactionCacheEntry>; let reactions: Record<string, ReactionCacheEntry>;
@ -91,21 +91,35 @@ function handleClickAvatar(event: React.MouseEvent<HTMLElement, MouseEvent>) {
export default definePlugin({ export default definePlugin({
name: "WhoReacted", name: "WhoReacted",
description: "Renders the avatars of users who reacted to a message", description: "Renders the avatars of users who reacted to a message",
authors: [Devs.Ven, Devs.KannaDev], authors: [Devs.Ven, Devs.KannaDev, Devs.newwares],
patches: [{ patches: [
find: ",reactionRef:", {
replacement: { find: ",reactionRef:",
match: /(\i)\?null:\(0,\i\.jsx\)\(\i\.\i,{className:\i\.reactionCount,.*?}\),/, replacement: {
replace: "$&$1?null:$self.renderUsers(this.props)," match: /(\i)\?null:\(0,\i\.jsx\)\(\i\.\i,{className:\i\.reactionCount,.*?}\),/,
replace: "$&$1?null:$self.renderUsers(this.props),"
}
}, {
find: '"MessageReactionsStore"',
replacement: {
match: /(?<=CONNECTION_OPEN:function\(\){)(\i)={}/,
replace: "$&;$self.reactions=$1"
}
},
{
find: "cleanAutomaticAnchor(){",
replacement: {
match: /constructor\(\i\)\{(?=.{0,100}automaticAnchor)/,
replace: "$&$self.setScrollObj(this);"
}
} }
}, { ],
find: '.displayName="MessageReactionsStore";',
replacement: { setScrollObj(scroll: any) {
match: /(?<=CONNECTION_OPEN:function\(\){)(\i)={}/, Scroll = scroll;
replace: "$&;$self.reactions=$1" },
}
}],
renderUsers(props: RootObject) { renderUsers(props: RootObject) {
return props.message.reactions.length > 10 ? null : ( return props.message.reactions.length > 10 ? null : (
@ -114,9 +128,13 @@ export default definePlugin({
</ErrorBoundary> </ErrorBoundary>
); );
}, },
_renderUsers({ message, emoji, type }: RootObject) { _renderUsers({ message, emoji, type }: RootObject) {
const forceUpdate = useForceUpdater(); const forceUpdate = useForceUpdater();
React.useLayoutEffect(() => { // bc need to prevent autoscrolling
if (Scroll?.scrollCounter > 0) {
Scroll.setAutomaticAnchor(null);
}
});
React.useEffect(() => { React.useEffect(() => {
const cb = (e: any) => { const cb = (e: any) => {
if (e.messageId === message.id) if (e.messageId === message.id)

View file

@ -49,6 +49,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Cynosphere", name: "Cynosphere",
id: 150745989836308480n id: 150745989836308480n
}, },
Trwy: {
name: "trey",
id: 354427199023218689n
},
Megu: { Megu: {
name: "Megumin", name: "Megumin",
id: 545581357812678656n id: 545581357812678656n
@ -151,7 +155,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
}, },
kemo: { kemo: {
name: "kemo", name: "kemo",
id: 299693897859465228n id: 715746190813298788n
}, },
dzshn: { dzshn: {
name: "dzshn", name: "dzshn",
@ -414,6 +418,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Elvyra", name: "Elvyra",
id: 708275751816003615n, id: 708275751816003615n,
}, },
Inbestigator: {
name: "Inbestigator",
id: 761777382041714690n
},
newwares: {
name: "newwares",
id: 421405303951851520n
},
Sqaaakoi: { Sqaaakoi: {
name: "Sqaaakoi", name: "Sqaaakoi",
id: 259558259491340288n, id: 259558259491340288n,

View file

@ -47,6 +47,7 @@ export let Paginator: t.Paginator;
export let ScrollerThin: t.ScrollerThin; export let ScrollerThin: t.ScrollerThin;
export let Clickable: t.Clickable; export let Clickable: t.Clickable;
export let Avatar: t.Avatar; export let Avatar: t.Avatar;
export let FocusLock: t.FocusLock;
// token lagger real // token lagger real
/** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */ /** 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;
@ -58,6 +59,6 @@ export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal"); export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
waitFor(["FormItem", "Button"], m => { waitFor(["FormItem", "Button"], m => {
({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar } = m); ({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar, FocusLock } = m);
Forms = m; Forms = m;
}); });

View file

@ -44,7 +44,6 @@ export let PermissionStore: GenericStore;
export let GuildChannelStore: GenericStore; export let GuildChannelStore: GenericStore;
export let ReadStateStore: GenericStore; export let ReadStateStore: GenericStore;
export let PresenceStore: GenericStore; export let PresenceStore: GenericStore;
export let PoggerModeSettingsStore: GenericStore;
export let GuildStore: t.GuildStore; export let GuildStore: t.GuildStore;
export let UserStore: Stores.UserStore & t.FluxStore; export let UserStore: Stores.UserStore & t.FluxStore;

View file

@ -453,3 +453,7 @@ export type Avatar = ComponentType<PropsWithChildren<{
"aria-hidden"?: boolean; "aria-hidden"?: boolean;
"aria-label"?: string; "aria-label"?: string;
}>>; }>>;
type FocusLock = ComponentType<PropsWithChildren<{
containerRef: RefObject<HTMLElement>
}>>;

View file

@ -81,11 +81,7 @@ interface RestRequestData {
retries?: number; retries?: number;
} }
export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>> & { export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>>;
V6OrEarlierAPIError: Error;
V8APIError: Error;
getAPIBaseURL(withVersion?: boolean): string;
};
export type Permissions = "CREATE_INSTANT_INVITE" export type Permissions = "CREATE_INSTANT_INVITE"
| "KICK_MEMBERS" | "KICK_MEMBERS"

View file

@ -19,7 +19,7 @@
import type { Channel, User } from "discord-types/general"; import type { Channel, User } from "discord-types/general";
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
import { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, waitFor } from "../webpack"; import { _resolveReady, filters, findByCodeLazy, findByProps, findByPropsLazy, findLazy, proxyLazyWebpack, waitFor } from "../webpack";
import type * as t from "./types/utils"; import type * as t from "./types/utils";
export let FluxDispatcher: t.FluxDispatcher; export let FluxDispatcher: t.FluxDispatcher;
@ -37,7 +37,10 @@ export let ComponentDispatch;
waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch); waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch);
export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get"); export const RestAPI: t.RestAPI = proxyLazyWebpack(() => {
const mod = findByProps("getAPIBaseURL");
return mod.HTTP ?? mod;
});
export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear");
export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage"); export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage");

View file

@ -110,13 +110,13 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
for (const key in cache) { for (const key in cache) {
const mod = cache[key]; const mod = cache[key];
if (!mod?.exports) continue; if (!mod?.exports || mod.exports === window) continue;
if (filter(mod.exports)) { if (filter(mod.exports)) {
return isWaitFor ? [mod.exports, key] : mod.exports; return isWaitFor ? [mod.exports, key] : mod.exports;
} }
if (mod.exports.default && filter(mod.exports.default)) { if (mod.exports.default && mod.exports.default !== window && filter(mod.exports.default)) {
const found = mod.exports.default; const found = mod.exports.default;
return isWaitFor ? [found, key] : found; return isWaitFor ? [found, key] : found;
} }
@ -408,10 +408,11 @@ export function findExportedComponentLazy<T extends object = any>(...props: stri
/** /**
* Extract and load chunks using their entry point * Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the entry point (as of using it to load chunks) must include * @param code An array of all the code the module factory containing the lazy chunk loading must include
* @param matcher A RegExp that returns the entry point id as the first capture group. Defaults to a matcher that captures the first entry point found in the module factory * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory
* @returns A promise that resolves when the chunks were loaded
*/ */
export async function extractAndLoadChunks(code: string[], matcher: RegExp = /\.el\("(.+?)"\)(?<=(\i)\.el.+?)\.then\(\2\.bind\(\2,"\1"\)\)/) { export async function extractAndLoadChunks(code: string[], matcher: RegExp = /Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/) {
const module = findModuleFactory(...code); const module = findModuleFactory(...code);
if (!module) { if (!module) {
const err = new Error("extractAndLoadChunks: Couldn't find module factory"); const err = new Error("extractAndLoadChunks: Couldn't find module factory");
@ -432,9 +433,9 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = /\.
return; return;
} }
const [, id] = match; const [, rawChunkIds, entryPointId] = match;
if (!id || !Number(id)) { if (!rawChunkIds || Number.isNaN(entryPointId)) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the entry point, or the entry point returned wasn't a number"); const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
logger.warn(err, "Code:", code, "Matcher:", matcher); logger.warn(err, "Code:", code, "Matcher:", matcher);
// Strict behaviour in DevBuilds to fail early and make sure the issue is found // Strict behaviour in DevBuilds to fail early and make sure the issue is found
@ -444,19 +445,21 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = /\.
return; return;
} }
await (wreq as any).el(id); const chunkIds = Array.from(rawChunkIds.matchAll(/\("(.+?)"\)/g)).map((m: any) => m[1]);
return wreq(id as any);
await Promise.all(chunkIds.map(id => wreq.e(id)));
wreq(entryPointId);
} }
/** /**
* This is just a wrapper around {@link extractAndLoadChunks} to make our reporter test for your webpack finds. * This is just a wrapper around {@link extractAndLoadChunks} to make our reporter test for your webpack finds.
* *
* Extract and load chunks using their entry point * Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the entry point (as of using it to load chunks) must include * @param code An array of all the code the module factory containing the lazy chunk loading must include
* @param matcher A RegExp that returns the entry point id as the first capture group. Defaults to a matcher that captures the first entry point found in the module factory * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory
* @returns A function that loads the chunks on first call * @returns A function that returns a promise that resolves when the chunks were loaded, on first call
*/ */
export function extractAndLoadChunksLazy(code: string[], matcher: RegExp = /\.el\("(.+?)"\)(?<=(\i)\.el.+?)\.then\(\2\.bind\(\2,"\1"\)\)/) { export function extractAndLoadChunksLazy(code: string[], matcher: RegExp = /Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/) {
if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
return () => extractAndLoadChunks(code, matcher); return () => extractAndLoadChunks(code, matcher);