{
- if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
+ if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => (
));
+ else if (permission.type === PermissionType.User) {
+ ContextMenuApi.openContextMenu(e, () => (
+
+ ));
+ }
}}
>
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
@@ -200,24 +208,53 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
aria-label="Role Options"
>
{
- const role = GuildStore.getRole(guild.id, roleId);
- if (!role) return;
+ Clipboard.copy(roleId);
+ }}
+ />
- onClose();
+ {(settings.store as any).unsafeViewAsRole && (
+ {
+ const role = GuildStore.getRole(guild.id, roleId);
+ if (!role) return;
- FluxDispatcher.dispatch({
- type: "IMPERSONATE_UPDATE",
- guildId: guild.id,
- data: {
- type: "ROLES",
- roles: {
- [roleId]: role
+ onClose();
+
+ FluxDispatcher.dispatch({
+ type: "IMPERSONATE_UPDATE",
+ guildId: guild.id,
+ data: {
+ type: "ROLES",
+ roles: {
+ [roleId]: role
+ }
}
- }
- });
+ });
+ }
+ }
+ />
+ )}
+
+ );
+}
+
+function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
+ return (
+
+ {
+ Clipboard.copy(userId);
}}
/>
diff --git a/src/plugins/permissionsViewer/components/UserPermissions.tsx b/src/plugins/permissionsViewer/components/UserPermissions.tsx
index bcd6bdf07..869a6a1ee 100644
--- a/src/plugins/permissionsViewer/components/UserPermissions.tsx
+++ b/src/plugins/permissionsViewer/components/UserPermissions.tsx
@@ -17,7 +17,7 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
-import ExpandableHeader from "@components/ExpandableHeader";
+import { ExpandableHeader } from "@components/ExpandableHeader";
import { classes } from "@utils/misc";
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common";
diff --git a/src/plugins/petpet/index.ts b/src/plugins/petpet/index.ts
index 3f9743255..2e06d0b17 100644
--- a/src/plugins/petpet/index.ts
+++ b/src/plugins/petpet/index.ts
@@ -21,10 +21,9 @@ import { Devs } from "@utils/constants";
import { makeLazy } from "@utils/lazy";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { UploadHandler, UserUtils } from "@webpack/common";
+import { DraftType, UploadHandler, UploadManager, UserUtils } from "@webpack/common";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
-const DRAFT_TYPE = 0;
const DEFAULT_DELAY = 20;
const DEFAULT_RESOLUTION = 128;
const FRAMES = 10;
@@ -59,9 +58,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
for (const opt of options) {
switch (opt.name) {
case "image":
- const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
+ const upload = UploadStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);
if (upload) {
- if (!upload.isImage) throw "Upload is not an image";
+ if (!upload.isImage) {
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
+ throw "Upload is not an image";
+ }
return upload.item.file;
}
break;
@@ -73,10 +75,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
} catch (err) {
console.error("[petpet] Failed to fetch user\n", err);
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
throw "Failed to fetch user. Check the console for more info.";
}
}
}
+ UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
return null;
}
@@ -130,6 +134,7 @@ export default definePlugin({
var url = await resolveImage(opts, cmdCtx, noServerPfp);
if (!url) throw "No Image specified!";
} catch (err) {
+ UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
sendBotMessage(cmdCtx.channel.id, {
content: String(err),
});
@@ -147,6 +152,8 @@ export default definePlugin({
canvas.width = canvas.height = resolution;
const ctx = canvas.getContext("2d")!;
+ UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
+
for (let i = 0; i < FRAMES; i++) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -174,7 +181,7 @@ export default definePlugin({
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
// Immediately after the command finishes, Discord clears all input, including pending attachments.
// Thus, setTimeout is needed to make this execute after Discord cleared the input
- setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
+ setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
},
},
]
diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx
index 010b5506c..60484561a 100644
--- a/src/plugins/pinDms/index.tsx
+++ b/src/plugins/pinDms/index.tsx
@@ -83,7 +83,7 @@ export default definePlugin({
// Rendering
{
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
- replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);"
+ replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2)();"
},
{
match: /"renderSection",(\i)=>{/,
@@ -320,25 +320,26 @@ export default definePlugin({
);
- }),
+ }, { noop: true }),
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType) {
- const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
+ return ErrorBoundary.wrap(() => {
+ const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
- if (!channel || !category) return null;
- if (this.isChannelHidden(sectionIndex, index)) return null;
+ if (!channel || !category) return null;
+ if (this.isChannelHidden(sectionIndex, index)) return null;
- return (
-
- {channel.id}
-
- );
+ return (
+
+ {channel.id}
+
+ );
+ }, { noop: true });
},
-
getChannel(sectionIndex: number, index: number, channels: Record) {
const category = categories[sectionIndex - 1];
if (!category) return { channel: null, category: null };
diff --git a/src/plugins/pronoundb/index.ts b/src/plugins/pronoundb/index.ts
index b14b26572..a5891d2e8 100644
--- a/src/plugins/pronoundb/index.ts
+++ b/src/plugins/pronoundb/index.ts
@@ -33,7 +33,7 @@ const PRONOUN_TOOLTIP_PATCH = {
export default definePlugin({
name: "PronounDB",
- authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven],
+ authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven, Devs.Elvyra],
description: "Adds pronouns to user messages using pronoundb",
patches: [
{
diff --git a/src/plugins/pronoundb/pronoundbUtils.ts b/src/plugins/pronoundb/pronoundbUtils.ts
index 6373c56a0..d4fdb09d3 100644
--- a/src/plugins/pronoundb/pronoundbUtils.ts
+++ b/src/plugins/pronoundb/pronoundbUtils.ts
@@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react";
import { UserProfileStore, UserStore } from "@webpack/common";
import { settings } from "./settings";
-import { PronounCode, PronounMapping, PronounsResponse } from "./types";
+import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types";
type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""];
@@ -40,9 +40,9 @@ export const enum PronounSource {
}
// A map of cached pronouns so the same request isn't sent twice
-const cache: Record = {};
+const cache: Record = {};
// A map of ids and callbacks that should be triggered on fetch
-const requestQueue: Record void)[]> = {};
+const requestQueue: Record void)[]> = {};
// Executes all queued requests and calls their callbacks
const bulkFetch = debounce(async () => {
@@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => {
const pronouns = await bulkFetchPronouns(ids);
for (const id of ids) {
// Call all callbacks for the id
- requestQueue[id]?.forEach(c => c(pronouns[id]));
+ requestQueue[id]?.forEach(c => c(pronouns[id] ? extractPronouns(pronouns[id].sets) : ""));
delete requestQueue[id];
}
});
@@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
return [discordPronouns, "Discord"];
- if (result && result !== "unspecified")
- return [formatPronouns(result), "PronounDB"];
+ if (result && result !== PronounMapping.unspecified)
+ return [result, "PronounDB"];
return [discordPronouns, "Discord"];
}
@@ -98,8 +98,9 @@ const NewLineRe = /\n+/g;
// Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string): string | null {
- const cached = cache[id];
- if (cached && cached !== "unspecified") return cached;
+ const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined;
+
+ if (cached && cached !== PronounMapping.unspecified) return cached;
return cached || null;
}
@@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise {
params.append("ids", ids.join(","));
try {
- const req = await fetch("https://pronoundb.org/api/v1/lookup-bulk?" + params.toString(), {
+ const req = await fetch("https://pronoundb.org/api/v2/lookup?" + params.toString(), {
method: "GET",
headers: {
"Accept": "application/json",
@@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise {
} catch (e) {
// If the request errors, treat it as if no pronouns were found for all ids, and log it
console.error("PronounDB fetching failed: ", e);
- const dummyPronouns = Object.fromEntries(ids.map(id => [id, "unspecified"] as const));
+ const dummyPronouns = Object.fromEntries(ids.map(id => [id, { sets: {} }] as const));
Object.assign(cache, dummyPronouns);
return dummyPronouns;
}
}
-export function formatPronouns(pronouns: string): string {
+export function extractPronouns(pronounSet?: { [locale: string]: PronounCode[] }): string {
+ if (!pronounSet || !pronounSet.en) return PronounMapping.unspecified;
+ // PronounDB returns an empty set instead of {sets: {en: ["unspecified"]}}.
+ const pronouns = pronounSet.en;
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
- // For capitalized pronouns, just return the mapping (it is by default capitalized)
- if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns];
- // If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text
- else if (
- pronounsFormat === PronounsFormat.Lowercase
- && ["any", "ask", "avoid", "other"].includes(pronouns)
- ) return PronounMapping[pronouns];
- // Otherwise (lowercase and not a special code), then convert the mapping to lowercase
- else return PronounMapping[pronouns].toLowerCase();
+
+ if (pronouns.length === 1) {
+ // For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string
+ if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]))
+ return PronounMapping[pronouns[0]];
+ else return PronounMapping[pronouns[0]].toLowerCase();
+ }
+ const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/");
+ return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase();
}
diff --git a/src/plugins/pronoundb/types.ts b/src/plugins/pronoundb/types.ts
index 9cfd77c8a..d099a7de8 100644
--- a/src/plugins/pronoundb/types.ts
+++ b/src/plugins/pronoundb/types.ts
@@ -26,31 +26,29 @@ export interface UserProfilePronounsProps {
}
export interface PronounsResponse {
- [id: string]: PronounCode;
+ [id: string]: {
+ sets?: {
+ [locale: string]: PronounCode[];
+ }
+ }
+}
+
+export interface CachePronouns {
+ sets?: {
+ [locale: string]: PronounCode[];
+ }
}
export type PronounCode = keyof typeof PronounMapping;
export const PronounMapping = {
- hh: "He/Him",
- hi: "He/It",
- hs: "He/She",
- ht: "He/They",
- ih: "It/Him",
- ii: "It/Its",
- is: "It/She",
- it: "It/They",
- shh: "She/He",
- sh: "She/Her",
- si: "She/It",
- st: "She/They",
- th: "They/He",
- ti: "They/It",
- ts: "They/She",
- tt: "They/Them",
+ he: "He/Him",
+ it: "It/Its",
+ she: "She/Her",
+ they: "They/Them",
any: "Any pronouns",
other: "Other pronouns",
ask: "Ask me my pronouns",
avoid: "Avoid pronouns, use my name",
- unspecified: "Unspecified"
+ unspecified: "No pronouns specified.",
} as const;
diff --git a/src/plugins/quickReply/index.ts b/src/plugins/quickReply/index.ts
index 620e1a33f..ac2a38705 100644
--- a/src/plugins/quickReply/index.ts
+++ b/src/plugins/quickReply/index.ts
@@ -24,6 +24,7 @@ import { ChannelStore, FluxDispatcher as Dispatcher, MessageStore, PermissionsBi
import { Message } from "discord-types/general";
const Kangaroo = findByPropsLazy("jumpToMessage");
+const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
const isMac = navigator.platform.includes("Mac"); // bruh
let replyIdx = -1;
@@ -139,6 +140,10 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
messages = messages.filter(m => m.author.id === meId);
}
+ if (Vencord.Plugins.isPluginEnabled("NoBlockedMessages")) {
+ messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
+ }
+
const mutate = (i: number) => isUp
? Math.min(messages.length - 1, i + 1)
: Math.max(-1, i - 1);
diff --git a/src/plugins/readAllNotificationsButton/index.tsx b/src/plugins/readAllNotificationsButton/index.tsx
index ae66e11a4..7a6737a8a 100644
--- a/src/plugins/readAllNotificationsButton/index.tsx
+++ b/src/plugins/readAllNotificationsButton/index.tsx
@@ -19,16 +19,37 @@
import "./style.css";
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
+import { findStoreLazy } from "@webpack";
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
+import { Channel } from "discord-types/general";
+
+interface ThreadJoined {
+ channel: Channel;
+ joinTimestamp: number;
+}
+
+type ThreadsJoined = Record;
+type ThreadsJoinedByParent = Record;
+
+interface ActiveJoinedThreadsStore {
+ getActiveJoinedThreadsForGuild(guildId: string): ThreadsJoinedByParent;
+}
+
+const ActiveJoinedThreadsStore: ActiveJoinedThreadsStore = findStoreLazy("ActiveJoinedThreadsStore");
function onClick() {
const channels: Array = [];
Object.values(GuildStore.getGuilds()).forEach(guild => {
- GuildChannelStore.getChannels(guild.id).SELECTABLE
- .concat(GuildChannelStore.getChannels(guild.id).VOCAL)
+ GuildChannelStore.getChannels(guild.id).SELECTABLE // Array<{ channel, comparator }>
+ .concat(GuildChannelStore.getChannels(guild.id).VOCAL) // Array<{ channel, comparator }>
+ .concat(
+ Object.values(ActiveJoinedThreadsStore.getActiveJoinedThreadsForGuild(guild.id))
+ .flatMap(threadChannels => Object.values(threadChannels))
+ )
.forEach((c: { channel: { id: string; }; }) => {
if (!ReadStateStore.hasUnread(c.channel.id)) return;
@@ -64,7 +85,7 @@ export default definePlugin({
authors: [Devs.kemo],
dependencies: ["ServerListAPI"],
- renderReadAllButton: () => ,
+ renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),
start() {
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
diff --git a/src/plugins/replaceGoogleSearch/README.md b/src/plugins/replaceGoogleSearch/README.md
new file mode 100644
index 000000000..1ab30212e
--- /dev/null
+++ b/src/plugins/replaceGoogleSearch/README.md
@@ -0,0 +1,5 @@
+# ReplaceGoogleSearch
+
+Replaces the Google search with different Engines
+
+
diff --git a/src/plugins/replaceGoogleSearch/index.tsx b/src/plugins/replaceGoogleSearch/index.tsx
new file mode 100644
index 000000000..dff593a3b
--- /dev/null
+++ b/src/plugins/replaceGoogleSearch/index.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { definePluginSettings } from "@api/Settings";
+import { Devs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+import { Flex, Menu } from "@webpack/common";
+
+const DefaultEngines = {
+ Google: "https://www.google.com/search?q=",
+ DuckDuckGo: "https://duckduckgo.com/",
+ Bing: "https://www.bing.com/search?q=",
+ Yahoo: "https://search.yahoo.com/search?p=",
+ GitHub: "https://github.com/search?q=",
+ Kagi: "https://kagi.com/search?q=",
+ Yandex: "https://yandex.com/search/?text=",
+ AOL: "https://search.aol.com/aol/search?q=",
+ Baidu: "https://www.baidu.com/s?wd=",
+ Wikipedia: "https://wikipedia.org/w/index.php?search=",
+} as const;
+
+const settings = definePluginSettings({
+ customEngineName: {
+ description: "Name of the custom search engine",
+ type: OptionType.STRING,
+ placeholder: "Google"
+ },
+ customEngineURL: {
+ description: "The URL of your Engine",
+ type: OptionType.STRING,
+ placeholder: "https://google.com/search?q="
+ }
+});
+
+function search(src: string, engine: string) {
+ open(engine + encodeURIComponent(src), "_blank");
+}
+
+function makeSearchItem(src: string) {
+ let Engines = {};
+
+ if (settings.store.customEngineName && settings.store.customEngineURL) {
+ Engines[settings.store.customEngineName] = settings.store.customEngineURL;
+ }
+
+ Engines = { ...Engines, ...DefaultEngines };
+
+ return (
+
+ {Object.keys(Engines).map((engine, i) => {
+ const key = "vc-search-content-" + engine;
+ return (
+
+
+ {engine}
+
+ }
+ action={() => search(src, Engines[engine])}
+ />
+ );
+ })}
+
+ );
+}
+
+const messageContextMenuPatch: NavContextMenuPatchCallback = (children, _props) => {
+ const selection = document.getSelection()?.toString();
+ if (!selection) return;
+
+ const group = findGroupChildrenByChildId("search-google", children);
+ if (group) {
+ const idx = group.findIndex(c => c?.props?.id === "search-google");
+ if (idx !== -1) group[idx] = makeSearchItem(selection);
+ }
+};
+
+export default definePlugin({
+ name: "ReplaceGoogleSearch",
+ description: "Replaces the Google search with different Engines",
+ authors: [Devs.Moxxie, Devs.Ethan],
+
+ settings,
+
+ contextMenus: {
+ "message": messageContextMenuPatch
+ }
+});
diff --git a/src/plugins/replyTimestamp/README.md b/src/plugins/replyTimestamp/README.md
new file mode 100644
index 000000000..b7952bf3a
--- /dev/null
+++ b/src/plugins/replyTimestamp/README.md
@@ -0,0 +1,5 @@
+# ReplyTimestamp
+
+Shows timestamps on the previews of replied-to messages. Pretty simple.
+
+
diff --git a/src/plugins/replyTimestamp/index.tsx b/src/plugins/replyTimestamp/index.tsx
new file mode 100644
index 000000000..05ec28b1b
--- /dev/null
+++ b/src/plugins/replyTimestamp/index.tsx
@@ -0,0 +1,77 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./style.css";
+
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { Timestamp } from "@webpack/common";
+import type { Message } from "discord-types/general";
+import type { HTMLAttributes } from "react";
+
+const { getMessageTimestampId } = findByPropsLazy("getMessageTimestampId");
+const { calendarFormat, dateFormat, isSameDay } = findByPropsLazy("calendarFormat", "dateFormat", "isSameDay", "accessibilityLabelCalendarFormat");
+const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp");
+
+function Sep(props: HTMLAttributes) {
+ return ;
+}
+
+const enum ReferencedMessageState {
+ LOADED = 0,
+ NOT_LOADED = 1,
+ DELETED = 2,
+}
+
+type ReferencedMessage = { state: ReferencedMessageState.LOADED; message: Message; } | { state: ReferencedMessageState.NOT_LOADED | ReferencedMessageState.DELETED; };
+
+function ReplyTimestamp({
+ referencedMessage,
+ baseMessage,
+}: {
+ referencedMessage: ReferencedMessage,
+ baseMessage: Message;
+}) {
+ if (referencedMessage.state !== ReferencedMessageState.LOADED) return null;
+ const refTimestamp = referencedMessage.message.timestamp as any;
+ const baseTimestamp = baseMessage.timestamp as any;
+ return (
+
+ [
+ {isSameDay(refTimestamp, baseTimestamp)
+ ? dateFormat(refTimestamp, "LT")
+ : calendarFormat(refTimestamp)
+ }
+ ]
+
+ );
+}
+
+export default definePlugin({
+ name: "ReplyTimestamp",
+ description: "Shows a timestamp on replied-message previews",
+ authors: [Devs.Kyuuhachi],
+
+ patches: [
+ {
+ find: "renderSingleLineMessage:function()",
+ replacement: {
+ match: /(?<="aria-label":\i,children:\[)(?=\i,\i,\i\])/,
+ replace: "$self.ReplyTimestamp(arguments[0]),"
+ }
+ }
+ ],
+
+ ReplyTimestamp: ErrorBoundary.wrap(ReplyTimestamp, { noop: true }),
+});
diff --git a/src/plugins/replyTimestamp/style.css b/src/plugins/replyTimestamp/style.css
new file mode 100644
index 000000000..f42371717
--- /dev/null
+++ b/src/plugins/replyTimestamp/style.css
@@ -0,0 +1,3 @@
+.vc-reply-timestamp {
+ margin-right: 0.25em;
+}
diff --git a/src/plugins/resurrectHome/index.tsx b/src/plugins/resurrectHome/index.tsx
index 980629126..70827e08f 100644
--- a/src/plugins/resurrectHome/index.tsx
+++ b/src/plugins/resurrectHome/index.tsx
@@ -91,7 +91,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "ResurrectHome",
- description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.",
+ description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking a server.",
authors: [Devs.Dolfies, Devs.Nuckyz],
settings,
@@ -135,7 +135,7 @@ export default definePlugin({
find: '"MessageActionCreators"',
replacement: {
match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/,
- replace: "before:$1,"
+ replace: "after:$1,"
}
},
// Force Server Home instead of Server Guide
@@ -151,7 +151,7 @@ export default definePlugin({
find: "487e85_1",
replacement: {
match: /(?<=text:(\i)\?\i\.\i\.Messages\.SERVER_GUIDE:\i\.\i\.Messages\.GUILD_HOME,)/,
- replace: "badge:$self.ViewServerHomeButton({serverGuide:$1}),"
+ replace: "trailing:$self.ViewServerHomeButton({serverGuide:$1}),"
}
},
// Disable view Server Home override when the Server Home is unmouted
diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx
index ad24e9696..2ae74c388 100644
--- a/src/plugins/reviewDB/index.tsx
+++ b/src/plugins/reviewDB/index.tsx
@@ -20,10 +20,9 @@ import "./style.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary";
-import ExpandableHeader from "@components/ExpandableHeader";
+import { ExpandableHeader } from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
-import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { Alerts, Menu, Parser, useState } from "@webpack/common";
import { Guild, User } from "discord-types/general";
@@ -36,13 +35,26 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings";
import { showToast } from "./utils";
-const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => {
+const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {
+ if (!guild) return;
children.push(
openReviewsModal(props.guild.id, props.guild.name)}
+ action={() => openReviewsModal(guild.id, guild.name)}
+ />
+ );
+};
+
+const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: User, onClose(): void; }) => {
+ if (!user) return;
+ children.push(
+ openReviewsModal(user.id, user.username)}
/>
);
};
@@ -54,7 +66,10 @@ export default definePlugin({
settings,
contextMenus: {
- "guild-header-popout": guildPopoutPatch
+ "guild-header-popout": guildPopoutPatch,
+ "guild-context": guildPopoutPatch,
+ "user-context": userContextPatch,
+ "user-profile-actions": userContextPatch
},
patches: [
@@ -75,13 +90,6 @@ export default definePlugin({
const s = settings.store;
const { lastReviewId, notifyReviews } = s;
- const legacy = s as any as { token?: string; };
- if (legacy.token) {
- await updateAuth({ token: legacy.token });
- legacy.token = undefined;
- new Logger("ReviewDB").info("Migrated legacy settings");
- }
-
await initAuth();
setTimeout(async () => {
diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx
index dafdd1a27..56b224da8 100644
--- a/src/plugins/roleColorEverywhere/index.tsx
+++ b/src/plugins/roleColorEverywhere/index.tsx
@@ -94,7 +94,7 @@ export default definePlugin({
find: "renderPrioritySpeaker",
replacement: [
{
- match: /renderName\(\).{0,100}speaking:.+?\.clanTag.+?"div",{/,
+ match: /renderName\(\){.+?usernameSpeaking\]:.+?(?=children)/,
replace: "$&...$self.getVoiceProps(this.props),"
}
],
diff --git a/src/plugins/secretRingTone/index.ts b/src/plugins/secretRingTone/index.ts
index 8f0176f88..9c3956a80 100644
--- a/src/plugins/secretRingTone/index.ts
+++ b/src/plugins/secretRingTone/index.ts
@@ -14,10 +14,11 @@ export default definePlugin({
authors: [Devs.AndrewDLO, Devs.FieryFlames],
patches: [
{
- find: "call_ringing_beat\"",
+ find: '"call_ringing_beat"',
replacement: {
- match: /500===\i\(\)\.random\(1,1e3\)/,
- replace: "true"
+ // FIXME Remove === alternative when it hits stable
+ match: /500(!==|===)\i\(\)\.random\(1,1e3\)/,
+ replace: (_, predicate) => predicate === "!==" ? "false" : "true",
}
},
],
diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx
index ce4a47ade..f08bc2d1d 100644
--- a/src/plugins/showHiddenChannels/index.tsx
+++ b/src/plugins/showHiddenChannels/index.tsx
@@ -36,6 +36,8 @@ const enum ShowMode {
HiddenIconWithMutedStyle
}
+const CONNECT = 1n << 20n;
+
export const settings = definePluginSettings({
hideUnreads: {
description: "Hide Unreads",
@@ -273,12 +275,12 @@ export default definePlugin({
{
// Change the role permission check to CONNECT if the channel is locked
match: /ADMINISTRATOR\)\|\|(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
- replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${PermissionsBits.CONNECT}n,${channel})?${permCheck}CONNECT):`
+ replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
},
{
// Change the permissionOverwrite check to CONNECT if the channel is locked
match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
- replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${PermissionsBits.CONNECT}n,${channel})?${permCheck}CONNECT):`
+ replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
},
{
// Include the @everyone role in the allowed roles list for Hidden Channels
@@ -434,7 +436,7 @@ export default definePlugin({
},
},
{
- find: ".shouldCloseDefaultModals",
+ find: 'className:"channelMention",children',
replacement: {
// Show inside voice channel instead of trying to join them when clicking on a channel mention
match: /(?<=getChannel\(\i\);if\(null!=(\i))(?=.{0,100}?selectVoiceChannel)/,
diff --git a/src/plugins/showHiddenThings/README.md b/src/plugins/showHiddenThings/README.md
index b41e2d94d..753e5c148 100644
--- a/src/plugins/showHiddenThings/README.md
+++ b/src/plugins/showHiddenThings/README.md
@@ -1,6 +1,6 @@
# ShowHiddenThings
-Displays various moderator-only elements regardless of permissions.
+Displays various hidden & moderator-only things regardless of permissions.
## Features
@@ -9,3 +9,11 @@ Displays various moderator-only elements regardless of permissions.
- Show the invites paused tooltip in the server list

+
+- Show the member mod view context menu item in all servers
+
+
+
+- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria
+
+- Disable filters in Server Discovery search that hide NSFW & disallowed servers
diff --git a/src/plugins/showHiddenThings/index.ts b/src/plugins/showHiddenThings/index.ts
index e7be929bf..db4fe5aa6 100644
--- a/src/plugins/showHiddenThings/index.ts
+++ b/src/plugins/showHiddenThings/index.ts
@@ -31,13 +31,28 @@ const settings = definePluginSettings({
description: "Show the invites paused tooltip in the server list.",
default: true,
},
+ showModView: {
+ type: OptionType.BOOLEAN,
+ description: "Show the member mod view context menu item in all servers.",
+ default: true,
+ },
+ disableDiscoveryFilters: {
+ type: OptionType.BOOLEAN,
+ description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.",
+ default: true,
+ },
+ disableDisallowedDiscoveryFilters: {
+ type: OptionType.BOOLEAN,
+ description: "Disable filters in Server Discovery search that hide NSFW & disallowed servers.",
+ default: true,
+ },
});
migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
export default definePlugin({
name: "ShowHiddenThings",
- tags: ["ShowTimeouts", "ShowInvitesPaused"],
- description: "Displays various moderator-only elements regardless of permissions.",
+ tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
+ description: "Displays various hidden & moderator-only things regardless of permissions.",
authors: [Devs.Dolfies],
patches: [
{
@@ -55,6 +70,47 @@ export default definePlugin({
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
replace: "true",
},
+ },
+ {
+ find: "canAccessGuildMemberModViewWithExperiment:",
+ predicate: () => settings.store.showModView,
+ replacement: {
+ match: /return \i\.hasAny\(\i\.computePermissions\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.MemberSafetyPagePermissions\)/,
+ replace: "return true",
+ }
+ },
+ {
+ find: "prod_discoverable_guilds",
+ predicate: () => settings.store.disableDiscoveryFilters,
+ replacement: {
+ match: /\{"auto_removed:.*?\}/,
+ replace: "{}"
+ }
+ },
+ {
+ find: "MINIMUM_MEMBER_COUNT:",
+ predicate: () => settings.store.disableDiscoveryFilters,
+ replacement: {
+ match: /MINIMUM_MEMBER_COUNT:function\(\)\{return \i}/,
+ replace: "MINIMUM_MEMBER_COUNT:() => \">0\""
+ }
+ },
+ {
+ find: "DiscoveryBannedSearchWords.includes",
+ predicate: () => settings.store.disableDisallowedDiscoveryFilters,
+ replacement: {
+ match: /(?<=function\(\){)(?=.{0,130}DiscoveryBannedSearchWords\.includes)/,
+ replace: "return false;"
+ }
+ },
+ {
+ find: "Endpoints.GUILD_DISCOVERY_VALID_TERM",
+ predicate: () => settings.store.disableDisallowedDiscoveryFilters,
+ all: true,
+ replacement: {
+ match: /\i\.HTTP\.get\(\{url:\i\.Endpoints\.GUILD_DISCOVERY_VALID_TERM,query:\{term:\i\},oldFormErrors:!0\}\);/g,
+ replace: "Promise.resolve({ body: { valid: true } });"
+ }
}
],
settings,
diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx
index a9db1af9a..7ba245da5 100644
--- a/src/plugins/showMeYourName/index.tsx
+++ b/src/plugins/showMeYourName/index.tsx
@@ -7,6 +7,7 @@
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Message, User } from "discord-types/general";
@@ -56,7 +57,7 @@ export default definePlugin({
],
settings,
- renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
+ renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
try {
const user = userOverride ?? message.author;
let { username } = user;
@@ -66,14 +67,14 @@ export default definePlugin({
const { nick } = author;
const prefix = withMentionPrefix ? "@" : "";
if (username === nick || isRepliedMessage && !settings.store.inReplies)
- return prefix + nick;
+ return <>{prefix}{nick}>;
if (settings.store.mode === "user-nick")
return <>{prefix}{username} {nick} >;
if (settings.store.mode === "nick-user")
return <>{prefix}{nick} {username} >;
- return prefix + username;
+ return <>{prefix}{username}>;
} catch {
- return author?.nick;
+ return <>{author?.nick}>;
}
- },
+ }, { noop: true }),
});
diff --git a/src/plugins/showTimeoutDuration/README.md b/src/plugins/showTimeoutDuration/README.md
new file mode 100644
index 000000000..137802473
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/README.md
@@ -0,0 +1,8 @@
+# ShowTimeoutDuration
+
+Displays how much longer a user's timeout will last.
+Either in the timeout icon tooltip, or next to it, configurable via settings!
+
+
+
+
diff --git a/src/plugins/showTimeoutDuration/index.tsx b/src/plugins/showTimeoutDuration/index.tsx
new file mode 100644
index 000000000..bfe806802
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/index.tsx
@@ -0,0 +1,92 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./styles.css";
+
+import { definePluginSettings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Devs } from "@utils/constants";
+import definePlugin, { OptionType } from "@utils/types";
+import { findComponentLazy } from "@webpack";
+import { ChannelStore, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
+import { Message } from "discord-types/general";
+import { FunctionComponent, ReactNode } from "react";
+
+const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER"));
+
+const enum DisplayStyle {
+ Tooltip = "tooltip",
+ Inline = "ssalggnikool"
+}
+
+const settings = definePluginSettings({
+ displayStyle: {
+ description: "How to display the timeout duration",
+ type: OptionType.SELECT,
+ options: [
+ { label: "In the Tooltip", value: DisplayStyle.Tooltip },
+ { label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true },
+ ],
+ }
+});
+
+function renderTimeout(message: Message, inline: boolean) {
+ const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id;
+ if (!guildId) return null;
+
+ const member = GuildMemberStore.getMember(guildId, message.author.id);
+ if (!member?.communicationDisabledUntil) return null;
+
+ const countdown = () => (
+
+ );
+
+ return inline
+ ? countdown()
+ : i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({
+ username: message.author.username,
+ countdown
+ });
+}
+
+export default definePlugin({
+ name: "ShowTimeoutDuration",
+ description: "Shows how much longer a user's timeout will last, either in the timeout icon tooltip or next to it",
+ authors: [Devs.Ven, Devs.Sqaaakoi],
+
+ settings,
+
+ patches: [
+ {
+ find: ".GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY",
+ replacement: [
+ {
+ match: /(\i)\.Tooltip,{(text:.{0,30}\.Messages\.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY)/,
+ replace: "$self.TooltipWrapper,{message:arguments[0].message,$2"
+ }
+ ]
+ }
+ ],
+
+ TooltipWrapper: ErrorBoundary.wrap(({ message, children, text }: { message: Message; children: FunctionComponent; text: ReactNode; }) => {
+ if (settings.store.displayStyle === DisplayStyle.Tooltip) return ;
+ return (
+
+
+
+ {renderTimeout(message, true)} timeout remaining
+
+
+ );
+ }, { noop: true })
+});
diff --git a/src/plugins/showTimeoutDuration/styles.css b/src/plugins/showTimeoutDuration/styles.css
new file mode 100644
index 000000000..a6f830c38
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/styles.css
@@ -0,0 +1,8 @@
+.vc-std-wrapper {
+ display: flex;
+ align-items: center;
+}
+
+.vc-std-wrapper [class*="communicationDisabled"] {
+ margin-right: 0;
+}
diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx
index 8b59c6ace..2a6a64283 100644
--- a/src/plugins/silentTyping/index.tsx
+++ b/src/plugins/silentTyping/index.tsx
@@ -18,10 +18,11 @@
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
+import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
-import { FluxDispatcher, React } from "@webpack/common";
+import { FluxDispatcher, Menu, React } from "@webpack/common";
const settings = definePluginSettings({
showIcon: {
@@ -30,6 +31,11 @@ const settings = definePluginSettings({
description: "Show an icon for toggling the plugin",
restartNeeded: true,
},
+ contextMenu: {
+ type: OptionType.BOOLEAN,
+ description: "Add option to toggle the functionality in the chat input context menu",
+ default: true
+ },
isEnabled: {
type: OptionType.BOOLEAN,
description: "Toggle functionality",
@@ -56,13 +62,37 @@ const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
);
};
+
+const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {
+ const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]);
+ if (!contextMenu) return;
+
+ const group = findGroupChildrenByChildId("submit-button", children);
+
+ if (!group) return;
+
+ const idx = group.findIndex(c => c?.props?.id === "submit-button");
+
+ group.splice(idx + 1, 0,
+ settings.store.isEnabled = !settings.store.isEnabled}
+ />
+ );
+};
+
+
export default definePlugin({
name: "SilentTyping",
- authors: [Devs.Ven, Devs.Rini],
+ authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
description: "Hide that you are typing",
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
settings,
-
+ contextMenus: {
+ "textarea-context": ChatBarContextCheckbox
+ },
patches: [
{
find: '.dispatch({type:"TYPING_START_LOCAL"',
diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx
index 32579a803..7033591bd 100644
--- a/src/plugins/sortFriendRequests/index.tsx
+++ b/src/plugins/sortFriendRequests/index.tsx
@@ -41,8 +41,8 @@ export default definePlugin({
patches: [{
find: "getRelationshipCounts(){",
replacement: {
- match: /\.sortBy\(\i=>\i\.comparator\)/,
- replace: "$&.sortBy((row) => $self.sortList(row))"
+ match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
+ replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
}
}, {
find: ".Messages.FRIEND_REQUEST_CANCEL",
@@ -53,10 +53,10 @@ export default definePlugin({
}
}],
- sortList(row: any) {
+ wrapSort(comparator: Function, row: any) {
return row.type === 3 || row.type === 4
? -this.getSince(row.user)
- : row.comparator;
+ : comparator(row);
},
getSince(user: User) {
diff --git a/src/plugins/startupTimings/index.tsx b/src/plugins/startupTimings/index.tsx
index 742d822ae..5051fdf4a 100644
--- a/src/plugins/startupTimings/index.tsx
+++ b/src/plugins/startupTimings/index.tsx
@@ -26,10 +26,12 @@ export default definePlugin({
description: "Adds Startup Timings to the Settings menu",
authors: [Devs.Megu],
patches: [{
- find: "UserSettingsSections.PAYMENT_FLOW_MODAL_TEST_PAGE,",
+ find: "Messages.ACTIVITY_SETTINGS",
replacement: {
- match: /{section:\i\.UserSettingsSections\.PAYMENT_FLOW_MODAL_TEST_PAGE/,
- replace: '{section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage},$&'
+ match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\)}\))/,
+ replace: (_, commaOrSemi, settings, elements) => "" +
+ `${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
+ `&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`
}
}],
StartupTimingPage
diff --git a/src/plugins/themeAttributes/README.md b/src/plugins/themeAttributes/README.md
index 110eca574..89001aae4 100644
--- a/src/plugins/themeAttributes/README.md
+++ b/src/plugins/themeAttributes/README.md
@@ -1,6 +1,6 @@
# ThemeAttributes
-This plugin adds data attributes to various elements inside Discord
+This plugin adds data attributes and CSS variables to various elements inside Discord
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
@@ -15,6 +15,15 @@ This allows themes to more easily theme those elements or even do things that ot
### Chat Messages
- `data-author-id` contains the id of the author
+- `data-author-username` contains the username of the author
- `data-is-self` is a boolean indicating whether this is the current user's message

+
+## CSS Variables
+
+### Avatars
+
+`--avatar-url-` contains a URL for the users avatar with the size attribute adjusted for the resolutions `128, 256, 512, 1024, 2048, 4096`.
+
+
diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts
index 8afc2121f..b80844546 100644
--- a/src/plugins/themeAttributes/index.ts
+++ b/src/plugins/themeAttributes/index.ts
@@ -9,10 +9,11 @@ import definePlugin from "@utils/types";
import { UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
+
export default definePlugin({
name: "ThemeAttributes",
description: "Adds data attributes to various elements for theming purposes",
- authors: [Devs.Ven],
+ authors: [Devs.Ven, Devs.Board],
patches: [
// Add data-tab-id to all tab bar items
@@ -32,14 +33,43 @@ export default definePlugin({
match: /\.messageListItem(?=,"aria)/,
replace: "$&,...$self.getMessageProps(arguments[0])"
}
+ },
+
+ // add --avatar-url- css variable to avatar img elements
+ // popout profiles
+ {
+ find: ".LABEL_WITH_ONLINE_STATUS",
+ replacement: {
+ match: /src:null!=\i\?(\i).{1,50}"aria-hidden":!0/,
+ replace: "$&,style:$self.getAvatarStyles($1)"
+ }
+ },
+ // chat avatars
+ {
+ find: "showCommunicationDisabledStyles",
+ replacement: {
+ match: /src:(\i),"aria-hidden":!0/,
+ replace: "$&,style:$self.getAvatarStyles($1)"
+ }
}
],
+ getAvatarStyles(src: string) {
+ return Object.fromEntries(
+ [128, 256, 512, 1024, 2048, 4096].map(size => [
+ `--avatar-url-${size}`,
+ `url(${src.replace(/\d+$/, String(size))})`
+ ])
+ );
+ },
+
getMessageProps(props: { message: Message; }) {
- const authorId = props.message?.author?.id;
+ const author = props.message?.author;
+ const authorId = author?.id;
return {
"data-author-id": authorId,
- "data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id
+ "data-author-username": author?.username,
+ "data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id,
};
}
});
diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx
index cc0ed5e93..b22c488eb 100644
--- a/src/plugins/translate/TranslateIcon.tsx
+++ b/src/plugins/translate/TranslateIcon.tsx
@@ -40,9 +40,9 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
}
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
- const { autoTranslate } = settings.use(["autoTranslate"]);
+ const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
- if (!isMainChat) return null;
+ if (!isMainChat || !showChatBarButton) return null;
const toggle = () => {
const newState = !autoTranslate;
diff --git a/src/plugins/translate/settings.ts b/src/plugins/translate/settings.ts
index cef003a83..65d845353 100644
--- a/src/plugins/translate/settings.ts
+++ b/src/plugins/translate/settings.ts
@@ -48,6 +48,11 @@ export const settings = definePluginSettings({
type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
default: false
+ },
+ showChatBarButton: {
+ type: OptionType.BOOLEAN,
+ description: "Show translate button in chat bar",
+ default: true
}
}).withPrivateSettings<{
showAutoTranslateAlert: boolean;
diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx
index 0e87201c6..16debf711 100644
--- a/src/plugins/unsuppressEmbeds/index.tsx
+++ b/src/plugins/unsuppressEmbeds/index.tsx
@@ -20,7 +20,7 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/Co
import { ImageInvisible, ImageVisible } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
-import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
+import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
const EMBED_SUPPRESSED = 1 << 2;
@@ -44,7 +44,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channe
icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}
action={() =>
RestAPI.patch({
- url: `/channels/${channel.id}/messages/${messageId}`,
+ url: Constants.Endpoints.MESSAGE(channel.id, messageId),
body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }
})
}
diff --git a/src/plugins/usrbg/index.tsx b/src/plugins/usrbg/index.tsx
index ce6bd709b..b92839a9a 100644
--- a/src/plugins/usrbg/index.tsx
+++ b/src/plugins/usrbg/index.tsx
@@ -24,9 +24,14 @@ import definePlugin, { OptionType } from "@utils/types";
import style from "./index.css?managed";
-const BASE_URL = "https://raw.githubusercontent.com/AutumnVN/usrbg/main/usrbg.json";
+const API_URL = "https://usrbg.is-hardly.online/users";
-let data = {} as Record;
+interface UsrbgApiReturn {
+ endpoint: string
+ bucket: string
+ prefix: string
+ users: Record
+}
const settings = definePluginSettings({
nitroFirst: {
@@ -48,7 +53,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "USRBG",
description: "Displays user banners from USRBG, allowing anyone to get a banner without Nitro",
- authors: [Devs.AutumnVN, Devs.pylix, Devs.TheKodeToad],
+ authors: [Devs.AutumnVN, Devs.katlyn, Devs.pylix, Devs.TheKodeToad],
settings,
patches: [
{
@@ -80,8 +85,7 @@ export default definePlugin({
}
],
-
- data,
+ data: null as UsrbgApiReturn | null,
settingsAboutComponent: () => {
return (
@@ -91,9 +95,9 @@ export default definePlugin({
voiceBackgroundHook({ className, participantUserId }: any) {
if (className.includes("tile_")) {
- if (data[participantUserId]) {
+ if (this.userHasBackground(participantUserId)) {
return {
- backgroundImage: `url(${data[participantUserId]})`,
+ backgroundImage: `url(${this.getImageUrl(participantUserId)})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat"
@@ -104,24 +108,35 @@ export default definePlugin({
useBannerHook({ displayProfile, user }: any) {
if (displayProfile?.banner && settings.store.nitroFirst) return;
- if (data[user.id]) return data[user.id];
+ if (this.userHasBackground(user.id)) return this.getImageUrl(user.id);
},
premiumHook({ userId }: any) {
- if (data[userId]) return 2;
+ if (this.userHasBackground(userId)) return 2;
},
shouldShowBadge({ displayProfile, user }: any) {
- return displayProfile?.banner && (!data[user.id] || settings.store.nitroFirst);
+ return displayProfile?.banner && (!this.userHasBackground(user.id) || settings.store.nitroFirst);
+ },
+
+ userHasBackground(userId: string) {
+ return !!this.data?.users[userId];
+ },
+
+ getImageUrl(userId: string): string|null {
+ if (!this.userHasBackground(userId)) return null;
+
+ // We can assert that data exists because userHasBackground returned true
+ const { endpoint, bucket, prefix, users: { [userId]: etag } } = this.data!;
+ return `${endpoint}/${bucket}/${prefix}${userId}?${etag}`;
},
async start() {
enableStyle(style);
- const res = await fetch(BASE_URL);
+ const res = await fetch(API_URL);
if (res.ok) {
- data = await res.json();
- this.data = data;
+ this.data = await res.json();
}
}
});
diff --git a/src/plugins/validReply/README.md b/src/plugins/validReply/README.md
new file mode 100644
index 000000000..49e313cf5
--- /dev/null
+++ b/src/plugins/validReply/README.md
@@ -0,0 +1,7 @@
+# ValidReply
+
+Fixes referenced (replied to) messages showing as "Message could not be loaded".
+
+Hover the text to load the message!
+
+
diff --git a/src/plugins/validReply/index.ts b/src/plugins/validReply/index.ts
new file mode 100644
index 000000000..21a1bdd1f
--- /dev/null
+++ b/src/plugins/validReply/index.ts
@@ -0,0 +1,106 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { FluxDispatcher, RestAPI } from "@webpack/common";
+import { Message, User } from "discord-types/general";
+import { Channel } from "discord-types/general/index.js";
+
+const enum ReferencedMessageState {
+ Loaded,
+ NotLoaded,
+ Deleted
+}
+
+interface Reply {
+ baseAuthor: User,
+ baseMessage: Message;
+ channel: Channel;
+ referencedMessage: { state: ReferencedMessageState; };
+ compact: boolean;
+ isReplyAuthorBlocked: boolean;
+}
+
+const fetching = new Map();
+let ReplyStore: any;
+
+const { createMessageRecord } = findByPropsLazy("createMessageRecord");
+
+export default definePlugin({
+ name: "ValidReply",
+ description: 'Fixes "Message could not be loaded" upon hovering over the reply',
+ authors: [Devs.newwares],
+ patches: [
+ {
+ find: "Messages.REPLY_QUOTE_MESSAGE_NOT_LOADED",
+ replacement: {
+ match: /Messages\.REPLY_QUOTE_MESSAGE_NOT_LOADED/,
+ replace: "$&,onMouseEnter:()=>$self.fetchReply(arguments[0])"
+ }
+ },
+ {
+ find: "ReferencedMessageStore",
+ replacement: {
+ match: /constructor\(\)\{\i\(this,"_channelCaches",new Map\)/,
+ replace: "$&;$self.setReplyStore(this);"
+ }
+ }
+ ],
+
+ setReplyStore(store: any) {
+ ReplyStore = store;
+ },
+
+ async fetchReply(reply: Reply) {
+ const { channel_id: channelId, message_id: messageId } = reply.baseMessage.messageReference!;
+
+ if (fetching.has(messageId)) {
+ return;
+ }
+ fetching.set(messageId, channelId);
+
+ RestAPI.get({
+ url: `/channels/${channelId}/messages`,
+ query: {
+ limit: 1,
+ around: messageId
+ },
+ retries: 2
+ })
+ .then(res => {
+ const reply: Message | undefined = res?.body?.[0];
+ if (!reply) return;
+
+ if (reply.id !== messageId) {
+ ReplyStore.set(channelId, messageId, {
+ state: ReferencedMessageState.Deleted
+ });
+
+ FluxDispatcher.dispatch({
+ type: "MESSAGE_DELETE",
+ channelId: channelId,
+ message: messageId
+ });
+ } else {
+ ReplyStore.set(reply.channel_id, reply.id, {
+ state: ReferencedMessageState.Loaded,
+ message: createMessageRecord(reply)
+ });
+
+ FluxDispatcher.dispatch({
+ type: "MESSAGE_UPDATE",
+ message: reply
+ });
+ }
+ })
+ .catch(() => { })
+ .finally(() => {
+ fetching.delete(messageId);
+ });
+ }
+});
diff --git a/src/plugins/validUser/index.tsx b/src/plugins/validUser/index.tsx
index 2fce693e8..4825cdaa3 100644
--- a/src/plugins/validUser/index.tsx
+++ b/src/plugins/validUser/index.tsx
@@ -18,15 +18,42 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
+import { isNonNullish } from "@utils/guards";
import { sleep } from "@utils/misc";
import { Queue } from "@utils/Queue";
import definePlugin from "@utils/types";
-import { UserStore, UserUtils, useState } from "@webpack/common";
-import type { ComponentType, ReactNode } from "react";
+import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
+import { type ComponentType, type ReactNode } from "react";
+
+// LYING to the type checker here
+const UserFlags = Constants.UserFlags as Record;
+const badges: Record = {
+ active_developer: { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
+ bug_hunter_level_1: { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
+ bug_hunter_level_2: { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
+ certified_moderator: { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" },
+ discord_employee: { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" },
+ get staff() { return this.discord_employee; },
+ hypesquad: { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" },
+ hypesquad_online_house_1: { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" },
+ hypesquad_online_house_2: { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" },
+ hypesquad_online_house_3: { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" },
+ partner: { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" },
+ premium: { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" },
+ premium_early_supporter: { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" },
+ verified_developer: { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" },
+};
const fetching = new Set();
const queue = new Queue(5);
+interface ProfileBadge {
+ id: string;
+ description: string;
+ icon: string;
+ link?: string;
+}
+
interface MentionProps {
data: {
userId?: string;
@@ -43,6 +70,46 @@ interface MentionProps {
UserMention: ComponentType;
}
+async function getUser(id: string) {
+ let userObj = UserStore.getUser(id);
+ if (userObj)
+ return userObj;
+
+ const user: any = await RestAPI.get({ url: Constants.Endpoints.USER(id) }).then(response => {
+ FluxDispatcher.dispatch({
+ type: "USER_UPDATE",
+ user: response.body,
+ });
+
+ return response.body;
+ });
+
+ // Populate the profile
+ await FluxDispatcher.dispatch(
+ {
+ type: "USER_PROFILE_FETCH_FAILURE",
+ userId: id,
+ }
+ );
+
+ userObj = UserStore.getUser(id);
+ const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)
+ .filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))
+ .map(([key]) => badges[key.toLowerCase()])
+ .filter(isNonNullish);
+ if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_")))
+ fakeBadges.push(badges.premium);
+
+ // Fill in what we can deduce
+ const profile = UserProfileStore.getUserProfile(id);
+ profile.accentColor = user.accent_color;
+ profile.badges = fakeBadges;
+ profile.banner = user.banner;
+ profile.premiumType = user.premium_type;
+
+ return userObj;
+}
+
function MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) {
const [userId, setUserId] = useState(data.userId);
@@ -85,14 +152,14 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
fetching.add(id);
queue.unshift(() =>
- UserUtils.getUser(id)
+ getUser(id)
.then(() => {
setUserId(id);
fetching.delete(id);
})
.catch(e => {
if (e?.status === 429) {
- queue.unshift(() => sleep(1000).then(fetch));
+ queue.unshift(() => sleep(e?.body?.retry_after ?? 1000).then(fetch));
fetching.delete(id);
}
})
@@ -112,7 +179,7 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio
export default definePlugin({
name: "ValidUser",
description: "Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)",
- authors: [Devs.Ven],
+ authors: [Devs.Ven, Devs.Dolfies],
tags: ["MentionCacheFix"],
patches: [
@@ -138,6 +205,7 @@ export default definePlugin({
return (
""
diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx
index f71777ad7..359365ee4 100644
--- a/src/plugins/viewIcons/index.tsx
+++ b/src/plugins/viewIcons/index.tsx
@@ -36,6 +36,10 @@ interface GuildContextProps {
guild?: Guild;
}
+interface GroupDMContextProps {
+ channel: Channel;
+}
+
const settings = definePluginSettings({
format: {
type: OptionType.SELECT,
@@ -145,10 +149,27 @@ const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildCon
));
};
+const GroupDMContext: NavContextMenuPatchCallback = (children, { channel }: GroupDMContextProps) => {
+ if (!channel) return;
+
+ children.splice(-1, 0, (
+
+
+ openImage(IconUtils.getChannelIconURL(channel)!)
+ }
+ icon={ImageIcon}
+ />
+
+ ));
+};
+
export default definePlugin({
name: "ViewIcons",
- authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz],
- description: "Makes avatars and banners in user profiles clickable, and adds View Icon/Banner entries in the user and server context menu",
+ authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz, Devs.nyx],
+ description: "Makes avatars and banners in user profiles clickable, adds View Icon/Banner entries in the user, server and group channel context menu.",
tags: ["ImageUtilities"],
settings,
@@ -157,11 +178,12 @@ export default definePlugin({
contextMenus: {
"user-context": UserContext,
- "guild-context": GuildContext
+ "guild-context": GuildContext,
+ "gdm-context": GroupDMContext
},
patches: [
- // Make pfps clickable
+ // Profiles Modal pfp
{
find: "User Profile Modal - Context Menu",
replacement: {
@@ -169,7 +191,7 @@ export default definePlugin({
replace: "{src:$1,onClick:()=>$self.openImage($1)"
}
},
- // Make banners clickable
+ // Banners
{
find: ".NITRO_BANNER,",
replacement: {
@@ -180,12 +202,37 @@ export default definePlugin({
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'
}
},
+ // User DMs "User Profile" popup in the right
{
find: ".avatarPositionPanel",
replacement: {
match: /(?<=avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/,
replace: "style:($1)?{cursor:\"pointer\"}:{},onClick:$1?()=>{$self.openImage($2)}"
}
+ },
+ // Group DMs top small & large icon
+ {
+ find: /\.recipients\.length>=2(?! `${m},onClick:()=>$self.openImage(${iconUrl})`
+ }
+ },
+ // User DMs top small icon
+ {
+ find: ".cursorPointer:null,children",
+ replacement: {
+ match: /.Avatar,.+?src:(.+?\))(?=[,}])/,
+ replace: (m, avatarUrl) => `${m},onClick:()=>$self.openImage(${avatarUrl})`
+ }
+ },
+ // User Dms top large icon
+ {
+ find: 'experimentLocation:"empty_messages"',
+ replacement: {
+ match: /.Avatar,.+?src:(.+?\))(?=[,}])/,
+ replace: (m, avatarUrl) => `${m},onClick:()=>$self.openImage(${avatarUrl})`
+ }
}
]
});
diff --git a/src/plugins/voiceDownload/index.tsx b/src/plugins/voiceDownload/index.tsx
new file mode 100644
index 000000000..571c3d0e9
--- /dev/null
+++ b/src/plugins/voiceDownload/index.tsx
@@ -0,0 +1,55 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import "./style.css";
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+
+export default definePlugin({
+ name: "VoiceDownload",
+ description: "Adds a download to voice messages. (Opens a new browser tab)",
+ authors: [Devs.puv],
+ patches: [
+ {
+ find: "rippleContainer,children",
+ replacement: {
+ match: /\(0,\i\.jsx\).{0,150},children:.{0,50}\("source",{src:(\i)}\)}\)/,
+ replace: "[$&, $self.renderDownload($1)]"
+ }
+ }
+ ],
+
+ renderDownload(src: string) {
+ return (
+ e.stopPropagation()}
+ aria-label="Download voice message"
+ {...IS_DISCORD_DESKTOP
+ ? { target: "_blank" } // open externally
+ : { download: "voice-message.ogg" } // download directly (not supported on discord desktop)
+ }
+ >
+
+
+ );
+ },
+
+ Icon: () => (
+
+
+
+ ),
+});
diff --git a/src/plugins/voiceDownload/style.css b/src/plugins/voiceDownload/style.css
new file mode 100644
index 000000000..2b776023f
--- /dev/null
+++ b/src/plugins/voiceDownload/style.css
@@ -0,0 +1,12 @@
+.vc-voice-download {
+ width: 24px;
+ height: 24px;
+ color: var(--interactive-normal);
+ margin-left: 12px;
+ cursor: pointer;
+ position: relative;
+}
+
+.vc-voice-download:hover {
+ color: var(--interactive-active);
+}
diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx
index 2f232f341..40e877df9 100644
--- a/src/plugins/voiceMessages/index.tsx
+++ b/src/plugins/voiceMessages/index.tsx
@@ -28,7 +28,7 @@ import { useAwaiter } from "@utils/react";
import definePlugin from "@utils/types";
import { chooseFile } from "@utils/web";
import { findByPropsLazy, findStoreLazy } from "@webpack";
-import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
+import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
import { ComponentType } from "react";
import { VoiceRecorderDesktop } from "./DesktopRecorder";
@@ -98,7 +98,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
upload.on("complete", () => {
RestAPI.post({
- url: `/channels/${channelId}/messages`,
+ url: Constants.Endpoints.MESSAGES(channelId),
body: {
flags: 1 << 13,
channel_id: channelId,
diff --git a/src/plugins/webScreenShareFixes.web/index.ts b/src/plugins/webScreenShareFixes.web/index.ts
new file mode 100644
index 000000000..8d1ab5821
--- /dev/null
+++ b/src/plugins/webScreenShareFixes.web/index.ts
@@ -0,0 +1,30 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+
+export default definePlugin({
+ name: "WebScreenShareFixes",
+ authors: [Devs.Kaitlyn],
+ description: "Removes 2500kbps bitrate cap on chromium and vesktop clients.",
+ enabledByDefault: true,
+ patches: [
+ {
+ find: "x-google-max-bitrate",
+ replacement: [
+ {
+ match: /"x-google-max-bitrate=".concat\(\i\)/,
+ replace: '"x-google-max-bitrate=".concat("80_000")'
+ },
+ {
+ match: /;level-asymmetry-allowed=1/,
+ replace: ";b=AS:800000;level-asymmetry-allowed=1"
+ }
+ ]
+ }
+ ]
+});
diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx
index b3728c215..5721dc912 100644
--- a/src/plugins/whoReacted/index.tsx
+++ b/src/plugins/whoReacted/index.tsx
@@ -23,7 +23,7 @@ import { Queue } from "@utils/Queue";
import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
-import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
+import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
import { CustomEmoji } from "@webpack/types";
import { Message, ReactionEmoji, User } from "discord-types/general";
@@ -36,7 +36,7 @@ let reactions: Record;
function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
return RestAPI.get({
- url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`,
+ url: Constants.Endpoints.REACTIONS(msg.channel_id, msg.id, key),
query: {
limit: 100,
type
diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts
index 763f6a782..5251959f2 100644
--- a/src/plugins/xsOverlay.desktop/index.ts
+++ b/src/plugins/xsOverlay.desktop/index.ts
@@ -1,6 +1,6 @@
/*
* 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
*/
@@ -13,10 +13,7 @@ import { findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
-const enum ChannelTypes {
- DM = 1,
- GROUP_DM = 3
-}
+const { ChannelTypes } = findByPropsLazy("ChannelTypes");
interface Message {
guild_id: string,
@@ -71,15 +68,35 @@ interface Call {
ringing: string[];
}
-const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled");
+const Notifs = findByPropsLazy("makeTextChatNotification");
const XSLog = new Logger("XSOverlay");
const settings = definePluginSettings({
- ignoreBots: {
+ botNotifications: {
type: OptionType.BOOLEAN,
- description: "Ignore messages from bots",
+ description: "Allow bot notifications",
default: false
},
+ serverNotifications: {
+ type: OptionType.BOOLEAN,
+ description: "Allow server notifications",
+ default: true
+ },
+ dmNotifications: {
+ type: OptionType.BOOLEAN,
+ description: "Allow Direct Message notifications",
+ default: true
+ },
+ groupDmNotifications: {
+ type: OptionType.BOOLEAN,
+ description: "Allow Group DM notifications",
+ default: true
+ },
+ callNotifications: {
+ type: OptionType.BOOLEAN,
+ description: "Allow call notifications",
+ default: true
+ },
pingColor: {
type: OptionType.STRING,
description: "User mention color",
@@ -97,8 +114,13 @@ const settings = definePluginSettings({
},
timeout: {
type: OptionType.NUMBER,
- description: "Notif duration (secs)",
- default: 1.0,
+ description: "Notification duration (secs)",
+ default: 3,
+ },
+ lengthBasedTimeout: {
+ type: OptionType.BOOLEAN,
+ description: "Extend duration with message length",
+ default: true
},
opacity: {
type: OptionType.SLIDER,
@@ -124,7 +146,7 @@ export default definePlugin({
settings,
flux: {
CALL_UPDATE({ call }: { call: Call; }) {
- if (call?.ringing?.includes(UserStore.getCurrentUser().id)) {
+ if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {
const channel = ChannelStore.getChannel(call.channel_id);
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
}
@@ -134,7 +156,7 @@ export default definePlugin({
try {
if (optimistic) return;
const channel = ChannelStore.getChannel(message.channel_id);
- if (!shouldNotify(message, channel)) return;
+ if (!shouldNotify(message, message.channel_id)) return;
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
@@ -194,6 +216,7 @@ export default definePlugin({
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `@${UserStore.getUser(id)?.username || "unknown-user"} `);
}
+ // color role mentions (unity styling btw lol)
if (message.mention_roles.length > 0) {
for (const roleId of message.mention_roles) {
const role = GuildStore.getRole(channel.guild_id, roleId);
@@ -213,6 +236,7 @@ export default definePlugin({
}
}
+ // color channel mentions
if (channelMatches) {
for (const cMatch of channelMatches) {
let channelId = cMatch.split("<#")[1];
@@ -221,6 +245,7 @@ export default definePlugin({
}
}
+ if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message);
} catch (err) {
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
@@ -229,13 +254,19 @@ export default definePlugin({
}
});
+function shouldIgnoreForChannelType(channel: Channel) {
+ if (channel.type === ChannelTypes.DM && settings.store.dmNotifications) return false;
+ if (channel.type === ChannelTypes.GROUP_DM && settings.store.groupDmNotifications) return false;
+ else return !settings.store.serverNotifications;
+}
+
function sendMsgNotif(titleString: string, content: string, message: Message) {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
const msgData = {
messageType: 1,
index: 0,
- timeout: settings.store.timeout,
- height: calculateHeight(cleanMessage(content)),
+ timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
+ height: calculateHeight(content),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
@@ -253,8 +284,8 @@ function sendOtherNotif(content: string, titleString: string) {
const msgData = {
messageType: 1,
index: 0,
- timeout: settings.store.timeout,
- height: calculateHeight(cleanMessage(content)),
+ timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
+ height: calculateHeight(content),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
@@ -267,13 +298,11 @@ function sendOtherNotif(content: string, titleString: string) {
Native.sendToOverlay(msgData);
}
-function shouldNotify(message: Message, channel: Channel) {
+function shouldNotify(message: Message, channel: string) {
const currentUser = UserStore.getCurrentUser();
if (message.author.id === currentUser.id) return false;
- if (message.author.bot && settings.store.ignoreBots) return false;
- if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true;
-
- return message.mentions.some(m => m.id === currentUser.id);
+ if (message.author.bot && !settings.store.botNotifications) return false;
+ return Notifs.shouldNotify(message, channel);
}
function calculateHeight(content: string) {
@@ -283,6 +312,9 @@ function calculateHeight(content: string) {
return 250;
}
-function cleanMessage(content: string) {
- return content.replace(new RegExp("<[^>]*>", "g"), "");
+function calculateTimeout(content: string) {
+ if (content.length <= 100) return 3;
+ if (content.length <= 200) return 4;
+ if (content.length <= 300) return 5;
+ return 6;
}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 8ab0bffb3..2f686d69d 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -266,6 +266,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Dziurwa",
id: 1001086404203389018n
},
+ arHSM: {
+ name: "arHSM",
+ id: 841509053422632990n
+ },
F53: {
name: "F53",
id: 280411966126948353n
@@ -374,10 +378,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ProffDea",
id: 609329952180928513n
},
+ UlyssesZhan: {
+ name: "UlyssesZhan",
+ id: 586808226058862623n
+ },
ant0n: {
name: "ant0n",
id: 145224646868860928n
},
+ Board: {
+ name: "BoardTM",
+ id: 285475344817848320n,
+ },
philipbry: {
name: "philipbry",
id: 554994003318276106n
@@ -410,10 +422,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Av32000",
id: 593436735380127770n,
},
+ Noxillio: {
+ name: "Noxillio",
+ id: 138616536502894592n,
+ },
Kyuuhachi: {
name: "Kyuuhachi",
id: 236588665420251137n,
},
+ nin0dev: {
+ name: "nin0dev",
+ id: 886685857560539176n
+ },
Elvyra: {
name: "Elvyra",
id: 708275751816003615n,
@@ -426,6 +446,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "newwares",
id: 421405303951851520n
},
+ JohnyTheCarrot: {
+ name: "JohnyTheCarrot",
+ id: 132819036282159104n
+ },
+ puv: {
+ name: "puv",
+ id: 469441552251355137n
+ },
Kodarru: {
name: "Kodarru",
id: 785227396218748949n
@@ -449,7 +477,43 @@ export const Devs = /* #__PURE__*/ Object.freeze({
PolisanTheEasyNick: {
name: "Oleh Polisan",
id: 242305263313485825n
- }
+ },
+ HAHALOSAH: {
+ name: "HAHALOSAH",
+ id: 903418691268513883n
+ },
+ GabiRP: {
+ name: "GabiRP",
+ id: 507955112027750401n
+ },
+ ImBanana: {
+ name: "Im_Banana",
+ id: 635250116688871425n
+ },
+ xocherry: {
+ name: "xocherry",
+ id: 221288171013406720n
+ },
+ ScattrdBlade: {
+ name: "ScattrdBlade",
+ id: 678007540608532491n
+ },
+ goodbee: {
+ name: "goodbee",
+ id: 658968552606400512n
+ },
+ Moxxie: {
+ name: "Moxxie",
+ id: 712653921692155965n,
+ },
+ Ethan: {
+ name: "Ethan",
+ id: 721717126523781240n,
+ },
+ nyx: {
+ name: "verticalsync",
+ id: 328165170536775680n
+ },
} satisfies Record);
// iife so #__PURE__ works correctly
diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx
index 74e1aefe8..57202ba3c 100644
--- a/src/utils/discord.tsx
+++ b/src/utils/discord.tsx
@@ -17,7 +17,7 @@
*/
import { MessageObject } from "@api/MessageEvents";
-import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
+import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
@@ -162,7 +162,7 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt
FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id });
const { body } = await RestAPI.get({
- url: `/users/${id}/profile`,
+ url: Constants.Endpoints.USER_PROFILE(id),
query: {
with_mutual_guilds: false,
with_mutual_friends_count: false,
diff --git a/src/utils/index.ts b/src/utils/index.ts
index ea4adce4a..62f3f6e96 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -23,9 +23,11 @@ export * from "./constants";
export * from "./discord";
export * from "./guards";
export * from "./lazy";
+export * from "./lazyReact";
export * from "./localStorage";
export * from "./Logger";
export * from "./margins";
+export * from "./mergeDefaults";
export * from "./misc";
export * from "./modal";
export * from "./onlyOnce";
diff --git a/src/utils/mergeDefaults.ts b/src/utils/mergeDefaults.ts
new file mode 100644
index 000000000..58ba136dd
--- /dev/null
+++ b/src/utils/mergeDefaults.ts
@@ -0,0 +1,24 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Recursively merges defaults into an object and returns the same object
+ * @param obj Object
+ * @param defaults Defaults
+ * @returns obj
+ */
+export function mergeDefaults(obj: T, defaults: T): T {
+ for (const key in defaults) {
+ const v = defaults[key];
+ if (typeof v === "object" && !Array.isArray(v)) {
+ obj[key] ??= {} as any;
+ mergeDefaults(obj[key], v);
+ } else {
+ obj[key] ??= v;
+ }
+ }
+ return obj;
+}
diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx
index 2b8ccf8a7..fb08c93f6 100644
--- a/src/utils/misc.tsx
+++ b/src/utils/misc.tsx
@@ -20,25 +20,6 @@ import { Clipboard, Toasts } from "@webpack/common";
import { DevsById } from "./constants";
-/**
- * Recursively merges defaults into an object and returns the same object
- * @param obj Object
- * @param defaults Defaults
- * @returns obj
- */
-export function mergeDefaults(obj: T, defaults: T): T {
- for (const key in defaults) {
- const v = defaults[key];
- if (typeof v === "object" && !Array.isArray(v)) {
- obj[key] ??= {} as any;
- mergeDefaults(obj[key], v);
- } else {
- obj[key] ??= v;
- }
- }
- return obj;
-}
-
/**
* Calls .join(" ") on the arguments
* classes("one", "two") => "one two"
@@ -114,3 +95,7 @@ export function identity(value: T): T {
export const isMobile = navigator.userAgent.includes("Mobi");
export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id);
+
+export function pluralise(amount: number, singular: string, plural = singular + "s") {
+ return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
+}
diff --git a/src/utils/patches.ts b/src/utils/patches.ts
index c30f7b17c..87f3ce78c 100644
--- a/src/utils/patches.ts
+++ b/src/utils/patches.ts
@@ -16,22 +16,22 @@
* along with this program. If not, see .
*/
-import { PatchReplacement, ReplaceFn } from "./types";
+import { Patch, PatchReplacement, ReplaceFn } from "./types";
-export function canonicalizeMatch(match: RegExp | string) {
+export function canonicalizeMatch(match: T): T {
if (typeof match === "string") return match;
const canonSource = match.source
.replaceAll("\\i", "[A-Za-z_$][\\w$]*");
- return new RegExp(canonSource, match.flags);
+ return new RegExp(canonSource, match.flags) as T;
}
-export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string): string | ReplaceFn {
+export function canonicalizeReplace(replace: T, pluginName: string): T {
const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
if (typeof replace !== "function")
- return replace.replaceAll("$self", self);
+ return replace.replaceAll("$self", self) as T;
- return (...args) => replace(...args).replaceAll("$self", self);
+ return ((...args) => replace(...args).replaceAll("$self", self)) as T;
}
export function canonicalizeDescriptor(descriptor: TypedPropertyDescriptor, canonicalize: (value: T) => T) {
@@ -55,3 +55,9 @@ export function canonicalizeReplacement(replacement: Pick(p: P & Record string;
export interface PatchReplacement {
+ /** The match for the patch replacement. If you use a string it will be implicitly converted to a RegExp */
match: string | RegExp;
+ /** The replacement string or function which returns the string for the patch replacement */
replace: string | ReplaceFn;
+ /** A function which returns whether this patch replacement should be applied */
predicate?(): boolean;
}
export interface Patch {
plugin: string;
- find: string;
+ /** A string or RegExp which is only include/matched in the module code you wish to patch. Prefer only using a RegExp if a simple string test is not enough */
+ find: string | RegExp;
+ /** The replacement(s) for the module being patched */
replacement: PatchReplacement | PatchReplacement[];
/** Whether this patch should apply to multiple modules */
all?: boolean;
@@ -44,6 +49,7 @@ export interface Patch {
noWarn?: boolean;
/** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */
group?: boolean;
+ /** A function which returns whether this patch should be applied */
predicate?(): boolean;
}
@@ -238,7 +244,7 @@ export interface PluginSettingSliderDef {
stickToMarkers?: boolean;
}
-interface IPluginOptionComponentProps {
+export interface IPluginOptionComponentProps {
/**
* Run this when the value changes.
*
diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts
index 24477c725..46f843ce6 100644
--- a/src/webpack/common/components.ts
+++ b/src/webpack/common/components.ts
@@ -36,6 +36,7 @@ export let Tooltip: t.Tooltip;
export let TextInput: t.TextInput;
export let TextArea: t.TextArea;
export let Text: t.Text;
+export let Heading: t.Heading;
export let Select: t.Select;
export let SearchableSelect: t.SearchableSelect;
export let Slider: t.Slider;
@@ -59,6 +60,28 @@ export const Flex = waitForComponent("Flex", ["Justify", "Align", "Wrap"
export const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal");
waitFor(["FormItem", "Button"], m => {
- ({ useToken, Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar, Popout, Dialog, Paginator, ScrollerThin, Clickable, Avatar, FocusLock } = m);
+ ({
+ useToken,
+ Card,
+ Button,
+ FormSwitch: Switch,
+ Tooltip,
+ TextInput,
+ TextArea,
+ Text,
+ Select,
+ SearchableSelect,
+ Slider,
+ ButtonLooks,
+ TabBar,
+ Popout,
+ Dialog,
+ Paginator,
+ ScrollerThin,
+ Clickable,
+ Avatar,
+ FocusLock,
+ Heading
+ } = m);
Forms = m;
});
diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts
index 2f9786bc5..123c62b05 100644
--- a/src/webpack/common/stores.ts
+++ b/src/webpack/common/stores.ts
@@ -27,12 +27,7 @@ export const Flux: t.Flux = findByPropsLazy("connectStores");
export type GenericStore = t.FluxStore & Record;
-export enum DraftType {
- ChannelMessage = 0,
- ThreadSettings = 1,
- FirstThreadMessage = 2,
- ApplicationLauncherCommand = 3
-}
+export const { DraftType }: { DraftType: typeof t.DraftType; } = findByPropsLazy("DraftType");
export let MessageStore: Omit & {
getMessages(chanId: string): any;
@@ -64,23 +59,15 @@ export let DraftStore: t.DraftStore;
/**
* React hook that returns stateful data for one or more stores
* You might need a custom comparator (4th argument) if your store data is an object
- *
* @param stores The stores to listen to
* @param mapper A function that returns the data you need
- * @param idk some thing, idk just pass null
+ * @param dependencies An array of reactive values which the hook depends on. Use this if your mapper or equality function depends on the value of another hook
* @param isEqual A custom comparator for the data returned by mapper
*
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/
-export const { useStateFromStores }: {
- useStateFromStores: (
- stores: t.FluxStore[],
- mapper: () => T,
- idk?: any,
- isEqual?: (old: T, newer: T) => boolean
- ) => T;
-}
- = findByPropsLazy("useStateFromStores");
+// eslint-disable-next-line prefer-destructuring
+export const useStateFromStores: t.useStateFromStores = findByPropsLazy("useStateFromStores").useStateFromStores;
waitForStore("DraftStore", s => DraftStore = s);
waitForStore("UserStore", s => UserStore = s);
diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts
index 3e3ffa4bd..c51264370 100644
--- a/src/webpack/common/types/components.d.ts
+++ b/src/webpack/common/types/components.d.ts
@@ -20,23 +20,24 @@ import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttribute
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
-export type Heading = `h${1 | 2 | 3 | 4 | 5 | 6}`;
+export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
export type Margins = Record<"marginTop16" | "marginTop8" | "marginBottom8" | "marginTop20" | "marginBottom20", string>;
export type ButtonLooks = Record<"FILLED" | "INVERTED" | "OUTLINED" | "LINK" | "BLANK", string>;
export type TextProps = PropsWithChildren & {
variant?: TextVariant;
- tag?: "div" | "span" | "p" | "strong" | Heading;
+ tag?: "div" | "span" | "p" | "strong" | HeadingTag;
selectable?: boolean;
lineClamp?: number;
}>;
export type Text = ComponentType;
+export type Heading = ComponentType;
export type FormTitle = ComponentType & PropsWithChildren<{
/** default is h5 */
- tag?: Heading;
+ tag?: HeadingTag;
faded?: boolean;
disabled?: boolean;
required?: boolean;
@@ -45,7 +46,7 @@ export type FormTitle = ComponentType & PropsWithChi
export type FormSection = ComponentType>;
type FocusLock = ComponentType
+ containerRef: RefObject;
}>>;
diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts
index 8e89a6e20..059924f5a 100644
--- a/src/webpack/common/types/stores.d.ts
+++ b/src/webpack/common/types/stores.d.ts
@@ -63,7 +63,7 @@ export interface CustomEmoji {
originalName?: string;
require_colons: boolean;
roles: string[];
- url: string;
+ type: "GUILD_EMOJI";
}
export interface UnicodeEmoji {
@@ -75,6 +75,7 @@ export interface UnicodeEmoji {
};
index: number;
surrogates: string;
+ type: "UNICODE";
uniqueName: string;
useSpriteSheet: boolean;
get allNamesString(): string;
@@ -173,6 +174,15 @@ export class DraftStore extends FluxStore {
getThreadSettings(channelId: string): any | null;
}
+export enum DraftType {
+ ChannelMessage,
+ ThreadSettings,
+ FirstThreadMessage,
+ ApplicationLauncherCommand,
+ Poll,
+ SlashCommand,
+}
+
export class GuildStore extends FluxStore {
getGuild(guildId: string): Guild;
getGuildCount(): number;
@@ -182,3 +192,10 @@ export class GuildStore extends FluxStore {
getRoles(guildId: string): Record;
getAllGuildRoles(): Record>;
}
+
+export type useStateFromStores = (
+ stores: t.FluxStore[],
+ mapper: () => T,
+ dependencies?: any,
+ isEqual?: (old: T, newer: T) => boolean
+) => T;
diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts
index ec6c0e1ed..72a71f31c 100644
--- a/src/webpack/common/utils.ts
+++ b/src/webpack/common/utils.ts
@@ -23,9 +23,11 @@ import { _resolveReady, filters, findByCodeLazy, findByProps, findByPropsLazy, f
import type * as t from "./types/utils";
export let FluxDispatcher: t.FluxDispatcher;
-
waitFor(["dispatch", "subscribe"], m => {
FluxDispatcher = m;
+ // Non import call to avoid circular dependency
+ Vencord.Plugins.subscribeAllPluginsFluxEvents(m);
+
const cb = () => {
m.unsubscribe("CONNECTION_OPEN", cb);
_resolveReady();
@@ -37,6 +39,8 @@ export let ComponentDispatch;
waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch);
+export const Constants = findByPropsLazy("Endpoints");
+
export const RestAPI: t.RestAPI = proxyLazyWebpack(() => {
const mod = findByProps("getAPIBaseURL");
return mod.HTTP ?? mod;
@@ -115,6 +119,8 @@ export function showToast(message: string, type = ToastType.MESSAGE) {
}
export const UserUtils = findByPropsLazy("getUser", "fetchCurrentUser") as { getUser: (id: string) => Promise; };
+
+export const UploadManager = findByPropsLazy("clearAll", "addFile");
export const UploadHandler = findByPropsLazy("showUploadFileSizeExceededError", "promptToUpload") as {
promptToUpload: (files: File[], channel: Channel, draftType: Number) => void;
};
@@ -132,10 +138,10 @@ waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m);
export const { Permissions: PermissionsBits } = findLazy(m => typeof m.Permissions?.ADMINISTRATOR === "bigint") as { Permissions: t.PermissionsBits; };
-export const zustandCreate: typeof import("zustand").default = findByCodeLazy("will be removed in v4");
+export const zustandCreate = findByCodeLazy("will be removed in v4");
const persistFilter = filters.byCode("[zustand persist middleware]");
-export const { persist: zustandPersist }: typeof import("zustand/middleware") = findLazy(m => m.persist && persistFilter(m.persist));
+export const { persist: zustandPersist } = findLazy(m => m.persist && persistFilter(m.persist));
export const MessageActions = findByPropsLazy("editMessage", "sendMessage");
export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal");
diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts
index db47c875a..311e6f2bc 100644
--- a/src/webpack/patchWebpack.ts
+++ b/src/webpack/patchWebpack.ts
@@ -18,66 +18,131 @@
import { WEBPACK_CHUNK } from "@utils/constants";
import { Logger } from "@utils/Logger";
-import { canonicalizeReplacement } from "@utils/patches";
+import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types";
+import { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer";
-import { _initWebpack } from ".";
+import { patches } from "../plugins";
+import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from ".";
+
+const logger = new Logger("WebpackInterceptor", "#8caaee");
+const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
let webpackChunk: any[];
-const logger = new Logger("WebpackInterceptor", "#8caaee");
+// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed
+// This way we can patch the factory of everything being pushed to the modules array
+Object.defineProperty(window, WEBPACK_CHUNK, {
+ configurable: true,
-if (window[WEBPACK_CHUNK]) {
- logger.info(`Patching ${WEBPACK_CHUNK}.push (was already existent, likely from cache!)`);
- _initWebpack(window[WEBPACK_CHUNK]);
- patchPush(window[WEBPACK_CHUNK]);
-} else {
- Object.defineProperty(window, WEBPACK_CHUNK, {
- get: () => webpackChunk,
- set: v => {
- if (v?.push) {
- if (!v.push.$$vencordOriginal) {
- logger.info(`Patching ${WEBPACK_CHUNK}.push`);
- patchPush(v);
+ get: () => webpackChunk,
+ set: v => {
+ if (v?.push) {
+ if (!v.push.$$vencordOriginal) {
+ logger.info(`Patching ${WEBPACK_CHUNK}.push`);
+ patchPush(v);
+
+ // @ts-ignore
+ delete window[WEBPACK_CHUNK];
+ window[WEBPACK_CHUNK] = v;
+ }
+ }
+
+ webpackChunk = v;
+ }
+});
+
+// wreq.O is the webpack onChunksLoaded function
+// Discord uses it to await for all the chunks to be loaded before initializing the app
+// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it
+Object.defineProperty(Function.prototype, "O", {
+ configurable: true,
+
+ set(onChunksLoaded: any) {
+ // When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
+ // This ensures we actually got the right one
+ // this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
+ const { stack } = new Error();
+ if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && String(this.e).includes("Promise.all")) {
+ logger.info("Found main WebpackRequire.onChunksLoaded");
+
+ delete (Function.prototype as any).O;
+
+ const originalOnChunksLoaded = onChunksLoaded;
+ onChunksLoaded = function (this: unknown, result: any, chunkIds: string[], callback: () => any, priority: number) {
+ if (callback != null && initCallbackRegex.test(callback.toString())) {
+ Object.defineProperty(this, "O", {
+ value: originalOnChunksLoaded,
+ configurable: true
+ });
+
+ const wreq = this as WebpackInstance;
+
+ const originalCallback = callback;
+ callback = function (this: unknown) {
+ logger.info("Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners");
+ _initWebpack(wreq);
+
+ for (const beforeInitListener of beforeInitListeners) {
+ beforeInitListener(wreq);
+ }
+
+ originalCallback.apply(this, arguments as any);
+ };
+
+ callback.toString = originalCallback.toString.bind(originalCallback);
+ arguments[2] = callback;
}
- if (_initWebpack(v)) {
- logger.info("Successfully initialised Vencord webpack");
- // @ts-ignore
- delete window[WEBPACK_CHUNK];
- window[WEBPACK_CHUNK] = v;
- }
- }
- webpackChunk = v;
- },
- configurable: true
- });
+ originalOnChunksLoaded.apply(this, arguments as any);
+ };
- // wreq.m is the webpack module factory.
- // normally, this is populated via webpackGlobal.push, which we patch below.
- // However, Discord has their .m prepopulated.
- // Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
- //
- // Update: Discord now has TWO webpack instances. Their normal one and sentry
- // Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
- Object.defineProperty(Function.prototype, "m", {
- set(v: any) {
- // When using react devtools or other extensions, we may also catch their webpack here.
- // This ensures we actually got the right one
- if (new Error().stack?.includes("discord.com")) {
- logger.info("Found webpack module factory");
- patchFactories(v);
- }
+ onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded);
- Object.defineProperty(this, "m", {
- value: v,
- configurable: true,
+ // Returns whether a chunk has been loaded
+ Object.defineProperty(onChunksLoaded, "j", {
+ set(v) {
+ delete onChunksLoaded.j;
+ onChunksLoaded.j = v;
+ originalOnChunksLoaded.j = v;
+ },
+ configurable: true
});
- },
- configurable: true
- });
-}
+ }
+
+ Object.defineProperty(this, "O", {
+ value: onChunksLoaded,
+ configurable: true
+ });
+ }
+});
+
+// wreq.m is the webpack module factory.
+// normally, this is populated via webpackGlobal.push, which we patch below.
+// However, Discord has their .m prepopulated.
+// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
+//
+// Update: Discord now has TWO webpack instances. Their normal one and sentry
+// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules
+Object.defineProperty(Function.prototype, "m", {
+ configurable: true,
+
+ set(v: any) {
+ // When using react devtools or other extensions, we may also catch their webpack here.
+ // This ensures we actually got the right one
+ const { stack } = new Error();
+ if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(v)) {
+ logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "");
+ patchFactories(v);
+ }
+
+ Object.defineProperty(this, "m", {
+ value: v,
+ configurable: true
+ });
+ }
+});
function patchPush(webpackGlobal: any) {
function handlePush(chunk: any) {
@@ -91,6 +156,7 @@ function patchPush(webpackGlobal: any) {
}
handlePush.$$vencordOriginal = webpackGlobal.push;
+ handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal);
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches
@@ -99,41 +165,41 @@ function patchPush(webpackGlobal: any) {
handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args);
Object.defineProperty(webpackGlobal, "push", {
+ configurable: true,
+
get: () => handlePush,
set(v) {
handlePush.$$vencordOriginal = v;
- },
- configurable: true
+ }
});
}
-function patchFactories(factories: Record void>) {
- const { subscriptions, listeners } = Vencord.Webpack;
- const { patches } = Vencord.Plugins;
+let webpackNotInitializedLogged = false;
+function patchFactories(factories: Record void>) {
for (const id in factories) {
let mod = factories[id];
- // Discords Webpack chunks for some ungodly reason contain random
- // newlines. Cyn recommended this workaround and it seems to work fine,
- // however this could potentially break code, so if anything goes weird,
- // this is probably why.
- // Additionally, `[actual newline]` is one less char than "\n", so if Discord
- // ever targets newer browsers, the minifier could potentially use this trick and
- // cause issues.
- //
- // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
- let code: string = "0," + mod.toString().replaceAll("\n", "");
+
const originalMod = mod;
const patchedBy = new Set();
- const factory = factories[id] = function (module, exports, require) {
+ const factory = factories[id] = function (module: any, exports: any, require: WebpackInstance) {
+ if (wreq == null && IS_DEV) {
+ if (!webpackNotInitializedLogged) {
+ webpackNotInitializedLogged = true;
+ logger.error("WebpackRequire was not initialized, running modules without patches instead.");
+ }
+
+ return void originalMod(module, exports, require);
+ }
+
try {
mod(module, exports, require);
} catch (err) {
// Just rethrow discord errors
if (mod === originalMod) throw err;
- logger.error("Error in patched chunk", err);
+ logger.error("Error in patched module", err);
return void originalMod(module, exports, require);
}
@@ -153,11 +219,11 @@ function patchFactories(factories: Record string, original: any, (...args: any[]): void; };
- // for some reason throws some error on which calling .toString() leads to infinite recursion
- // when you force load all chunks???
- factory.toString = () => mod.toString();
+ factory.toString = originalMod.toString.bind(originalMod);
factory.original = originalMod;
+ for (const factoryListener of factoryListeners) {
+ try {
+ factoryListener(originalMod);
+ } catch (err) {
+ logger.error("Error in Webpack factory listener:\n", err, factoryListener);
+ }
+ }
+
+ // Discords Webpack chunks for some ungodly reason contain random
+ // newlines. Cyn recommended this workaround and it seems to work fine,
+ // however this could potentially break code, so if anything goes weird,
+ // this is probably why.
+ // Additionally, `[actual newline]` is one less char than "\n", so if Discord
+ // ever targets newer browsers, the minifier could potentially use this trick and
+ // cause issues.
+ //
+ // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
+ let code: string = "0," + mod.toString().replaceAll("\n", "");
+
for (let i = 0; i < patches.length; i++) {
const patch = patches[i];
- const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
if (patch.predicate && !patch.predicate()) continue;
- if (code.includes(patch.find)) {
- patchedBy.add(patch.plugin);
+ const moduleMatches = typeof patch.find === "string"
+ ? code.includes(patch.find)
+ : patch.find.test(code);
- const previousMod = mod;
- const previousCode = code;
+ if (!moduleMatches) continue;
- // we change all patch.replacement to array in plugins/index
- for (const replacement of patch.replacement as PatchReplacement[]) {
- if (replacement.predicate && !replacement.predicate()) continue;
- const lastMod = mod;
- const lastCode = code;
+ patchedBy.add(patch.plugin);
- canonicalizeReplacement(replacement, patch.plugin);
+ const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
+ const previousMod = mod;
+ const previousCode = code;
- try {
- const newCode = executePatch(replacement.match, replacement.replace as string);
- if (newCode === code) {
- if (!patch.noWarn) {
- logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
- if (IS_DEV) {
- logger.debug("Function Source:\n", code);
- }
+ // We change all patch.replacement to array in plugins/index
+ for (const replacement of patch.replacement as PatchReplacement[]) {
+ if (replacement.predicate && !replacement.predicate()) continue;
+
+ const lastMod = mod;
+ const lastCode = code;
+
+ canonicalizeReplacement(replacement, patch.plugin);
+
+ try {
+ const newCode = executePatch(replacement.match, replacement.replace as string);
+ if (newCode === code) {
+ if (!patch.noWarn) {
+ logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
+ if (IS_DEV) {
+ logger.debug("Function Source:\n", code);
}
-
- if (patch.group) {
- logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
- code = previousCode;
- mod = previousMod;
- patchedBy.delete(patch.plugin);
- break;
- }
- } else {
- code = newCode;
- mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
- }
- } catch (err) {
- logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
-
- if (IS_DEV) {
- const changeSize = code.length - lastCode.length;
- const match = lastCode.match(replacement.match)!;
-
- // Use 200 surrounding characters of context
- const start = Math.max(0, match.index! - 200);
- const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
- // (changeSize may be negative)
- const endPatched = end + changeSize;
-
- const context = lastCode.slice(start, end);
- const patchedContext = code.slice(start, endPatched);
-
- // inline require to avoid including it in !IS_DEV builds
- const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
- let fmt = "%c %s ";
- const elements = [] as string[];
- for (const d of diff) {
- const color = d.removed
- ? "red"
- : d.added
- ? "lime"
- : "grey";
- fmt += "%c%s";
- elements.push("color:" + color, d.value);
- }
-
- logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
- logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
- const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
- logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
}
- patchedBy.delete(patch.plugin);
if (patch.group) {
- logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
- code = previousCode;
+ logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
mod = previousMod;
+ code = previousCode;
+ patchedBy.delete(patch.plugin);
break;
}
- code = lastCode;
- mod = lastMod;
+ continue;
}
- }
- if (!patch.all) patches.splice(i--, 1);
+ code = newCode;
+ mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
+ } catch (err) {
+ logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
+
+ if (IS_DEV) {
+ const changeSize = code.length - lastCode.length;
+ const match = lastCode.match(replacement.match)!;
+
+ // Use 200 surrounding characters of context
+ const start = Math.max(0, match.index! - 200);
+ const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
+ // (changeSize may be negative)
+ const endPatched = end + changeSize;
+
+ const context = lastCode.slice(start, end);
+ const patchedContext = code.slice(start, endPatched);
+
+ // inline require to avoid including it in !IS_DEV builds
+ const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
+ let fmt = "%c %s ";
+ const elements = [] as string[];
+ for (const d of diff) {
+ const color = d.removed
+ ? "red"
+ : d.added
+ ? "lime"
+ : "grey";
+ fmt += "%c%s";
+ elements.push("color:" + color, d.value);
+ }
+
+ logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
+ logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
+ const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
+ logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
+ }
+
+ patchedBy.delete(patch.plugin);
+
+ if (patch.group) {
+ logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
+ mod = previousMod;
+ code = previousCode;
+ break;
+ }
+
+ mod = lastMod;
+ code = lastCode;
+ }
}
+
+ if (!patch.all) patches.splice(i--, 1);
}
}
}
diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts
index 564da4813..854820851 100644
--- a/src/webpack/webpack.ts
+++ b/src/webpack/webpack.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { proxyLazy } from "@utils/lazy";
+import { makeLazy, proxyLazy } from "@utils/lazy";
import { LazyComponent } from "@utils/lazyReact";
import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
@@ -68,20 +68,16 @@ export const filters = {
}
};
-export const subscriptions = new Map();
-export const listeners = new Set();
-
export type CallbackFn = (mod: any, id: string) => void;
-export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
- if (cache !== void 0) throw "no.";
+export const subscriptions = new Map();
+export const moduleListeners = new Set();
+export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>();
+export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
- instance.push([[Symbol("Vencord")], {}, r => wreq = r]);
- instance.pop();
- if (!wreq) return false;
-
- cache = wreq.c;
- return true;
+export function _initWebpack(webpackRequire: WebpackInstance) {
+ wreq = webpackRequire;
+ cache = webpackRequire.c;
}
let devToolsOpen = false;
@@ -406,7 +402,8 @@ export function findExportedComponentLazy(...props: stri
});
}
-const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\((\[\i\.\i\(".+?"\).+?\])\)|Promise\.resolve\(\)).then\(\i\.bind\(\i,"(.+?)"\)\)/;
+export const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\))|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/;
+export const ChunkIdsRegex = /\("(.+?)"\)/g;
/**
* Extract and load chunks using their entry point
@@ -425,7 +422,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
const match = module.toString().match(canonicalizeMatch(matcher));
if (!match) {
- const err = new Error("extractAndLoadChunks: Couldn't find entry point id in module factory code");
+ const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code");
logger.warn(err, "Code:", code, "Matcher:", matcher);
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
@@ -435,8 +432,8 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
return;
}
- const [, rawChunkIds, entryPointId] = match;
- if (Number.isNaN(entryPointId)) {
+ const [, rawChunkIdsArray, rawChunkIdsSingle, entryPointId] = match;
+ if (Number.isNaN(Number(entryPointId))) {
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);
@@ -447,8 +444,9 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
return;
}
+ const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
if (rawChunkIds) {
- const chunkIds = Array.from(rawChunkIds.matchAll(/\("(.+?)"\)/g)).map((m: any) => m[1]);
+ const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]);
await Promise.all(chunkIds.map(id => wreq.e(id)));
}
@@ -466,7 +464,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) {
if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
- return () => extractAndLoadChunks(code, matcher);
+ return makeLazy(() => extractAndLoadChunks(code, matcher));
}
/**
@@ -491,14 +489,6 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback
subscriptions.set(filter, callback);
}
-export function addListener(callback: CallbackFn) {
- listeners.add(callback);
-}
-
-export function removeListener(callback: CallbackFn) {
- listeners.delete(callback);
-}
-
/**
* Search modules by keyword. This searches the factory methods,
* meaning you can search all sorts of things, displayName, methodName, strings somewhere in the code, etc
diff --git a/tsconfig.json b/tsconfig.json
index e9c926408..8db0ab3c1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
@@ -28,7 +29,15 @@
"@webpack/types": ["./webpack/common/types"],
"@webpack/common": ["./webpack/common"],
"@webpack": ["./webpack/webpack"]
- }
+ },
+
+ "plugins": [
+ // Transform paths in output .d.ts files (Include this line if you output declarations files)
+ {
+ "transform": "typescript-transform-paths",
+ "afterDeclarations": true
+ }
+ ]
},
"include": ["src/**/*", "browser/**/*", "scripts/**/*"]
}