{
- 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;
-
- onClose();
-
- FluxDispatcher.dispatch({
- type: "IMPERSONATE_UPDATE",
- guildId: guild.id,
- data: {
- type: "ROLES",
- roles: {
- [roleId]: role
+ Clipboard.copy(roleId);
+ }}
+ />
+
+ {(settings.store as any).unsafeViewAsRole && (
+ {
+ const role = GuildStore.getRole(guild.id, roleId);
+ if (!role) return;
+
+ 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 bcd6bdf071..869a6a1eee 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 3f97432551..2e06d0b178 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 010b5506cb..60484561a4 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 b14b265725..a5891d2e82 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 6373c56a0c..d4fdb09d3d 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 9cfd77c8a3..d099a7de86 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/readAllNotificationsButton/index.tsx b/src/plugins/readAllNotificationsButton/index.tsx
index ae66e11a45..7a6737a8a6 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 0000000000..1ab30212eb
--- /dev/null
+++ b/src/plugins/replaceGoogleSearch/README.md
@@ -0,0 +1,5 @@
+# ReplaceGoogleSearch
+
+Replaces the Google search with different Engines
+
+![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/8b8158d2-0407-4d7b-9dff-a8b9bdc1a122)
diff --git a/src/plugins/replaceGoogleSearch/index.tsx b/src/plugins/replaceGoogleSearch/index.tsx
new file mode 100644
index 0000000000..dff593a3b4
--- /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 0000000000..b7952bf3a0
--- /dev/null
+++ b/src/plugins/replyTimestamp/README.md
@@ -0,0 +1,5 @@
+# ReplyTimestamp
+
+Shows timestamps on the previews of replied-to messages. Pretty simple.
+
+![](https://github.com/Vendicated/Vencord/assets/1547062/62e2b67a-e567-4c7a-884d-4640f897f7e0)
diff --git a/src/plugins/replyTimestamp/index.tsx b/src/plugins/replyTimestamp/index.tsx
new file mode 100644
index 0000000000..05ec28b1b5
--- /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 0000000000..f423717171
--- /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 2042ed9c7f..70827e08f5 100644
--- a/src/plugins/resurrectHome/index.tsx
+++ b/src/plugins/resurrectHome/index.tsx
@@ -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
diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx
index bcd0f163fa..2ae74c388e 100644
--- a/src/plugins/reviewDB/index.tsx
+++ b/src/plugins/reviewDB/index.tsx
@@ -20,7 +20,7 @@ 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 definePlugin from "@utils/types";
@@ -36,6 +36,7 @@ import { settings } from "./settings";
import { showToast } from "./utils";
const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => {
+ if (!guild) return;
children.push(
settings.store.showModView,
+ replacement: {
+ match: /return \i\.hasAny\(\i\.computePermissions\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.MemberSafetyPagePermissions\)/,
+ replace: "return true",
+ }
+ },
+ {
+ find: "auto_removed:",
+ predicate: () => settings.store.disableDiscoveryFilters,
+ replacement: {
+ match: /filters:\i\.join\(" AND "\),facets:\[/,
+ replace: "facets:["
+ }
+ },
+ {
+ 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 a9db1af9ab..7ba245da5b 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 0000000000..137802473d
--- /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!
+
+![indicator in tooltip](https://github.com/Vendicated/Vencord/assets/45497981/606588a3-2646-40d9-8800-b6307f650136)
+
+![indicator next to timeout icon](https://github.com/Vendicated/Vencord/assets/45497981/ab9d2101-0fdc-4143-9310-9488f056eeee)
diff --git a/src/plugins/showTimeoutDuration/index.tsx b/src/plugins/showTimeoutDuration/index.tsx
new file mode 100644
index 0000000000..bfe806802f
--- /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 0000000000..a6f830c38b
--- /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 8b59c6ace4..2a6a642833 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/startupTimings/index.tsx b/src/plugins/startupTimings/index.tsx
index 742d822aef..5051fdf4a9 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 110eca5743..89001aae4c 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
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
+
+## 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`.
+
+![image](https://github.com/Vendicated/Vencord/assets/26598490/192ddac0-c827-472f-9933-fa99ff36f723)
diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts
index 8afc2121fd..b80844546d 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 cc0ed5e938..b22c488eb6 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 cef003a83a..65d845353a 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 0e87201c63..16debf7119 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/validReply/README.md b/src/plugins/validReply/README.md
new file mode 100644
index 0000000000..49e313cf51
--- /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!
+
+![](https://github.com/Vendicated/Vencord/assets/45801973/d3286acf-e822-4b7f-a4e7-8ced18f581af)
diff --git a/src/plugins/validReply/index.ts b/src/plugins/validReply/index.ts
new file mode 100644
index 0000000000..21a1bdd1f2
--- /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 7a21ac86b9..4825cdaa35 100644
--- a/src/plugins/validUser/index.tsx
+++ b/src/plugins/validUser/index.tsx
@@ -18,28 +18,30 @@
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 { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
-import type { ComponentType, ReactNode } from "react";
+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" },
- "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" },
+ 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();
@@ -73,7 +75,7 @@ async function getUser(id: string) {
if (userObj)
return userObj;
- const user: any = await RestAPI.get({ url: `/users/${id}` }).then(response => {
+ const user: any = await RestAPI.get({ url: Constants.Endpoints.USER(id) }).then(response => {
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user: response.body,
@@ -93,7 +95,8 @@ async function getUser(id: string) {
userObj = UserStore.getUser(id);
const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)
.filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))
- .map(([key]) => badges[key.toLowerCase()]);
+ .map(([key]) => badges[key.toLowerCase()])
+ .filter(isNonNullish);
if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_")))
fakeBadges.push(badges.premium);
@@ -202,6 +205,7 @@ export default definePlugin({
return (
""
diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx
index f71777ad71..359365ee4d 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 0000000000..571c3d0e90
--- /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 0000000000..2b776023f4
--- /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 2f232f3417..40e877df9d 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 0000000000..8d1ab58214
--- /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 b3728c2153..5721dc9125 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 763f6a782c..5251959f2f 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 53a060d8bb..7f08460508 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
@@ -450,14 +478,42 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Oleh Polisan",
id: 242305263313485825n
},
+ HAHALOSAH: {
+ name: "HAHALOSAH",
+ id: 903418691268513883n
+ },
GabiRP: {
name: "GabiRP",
id: 507955112027750401n
},
- Johannes7k75: {
+ ImBanana: {
+ name: "Im_Banana",
+ id: 635250116688871425n
+ },
+ xocherry: {
+ name: "xocherry",
+ id: 221288171013406720n
+ },
+ ScattrdBlade: {
+ name: "ScattrdBlade",
+ id: 678007540608532491n
+ },
+ Moxxie: {
+ name: "Moxxie",
+ id: 712653921692155965n,
+ },
+ Ethan: {
+ name: "Ethan",
+ id: 721717126523781240n,
+ },
+ nyx: {
+ name: "verticalsync",
+ id: 328165170536775680n
+ },
+ Johannes7k75: {
name: "Johannes7k75",
id: 587701169103699994n
- }
+ }
} satisfies Record);
// iife so #__PURE__ works correctly
diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx
index 74e1aefe8f..57202ba3ca 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 ea4adce4a9..62f3f6e96c 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 0000000000..58ba136dd5
--- /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 32010e59b6..fb08c93f6f 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"
diff --git a/src/utils/patches.ts b/src/utils/patches.ts
index 99f0595d6f..87f3ce78c8 100644
--- a/src/utils/patches.ts
+++ b/src/utils/patches.ts
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { PatchReplacement, ReplaceFn } from "./types";
+import { Patch, PatchReplacement, ReplaceFn } from "./types";
export function canonicalizeMatch(match: T): T {
if (typeof match === "string") return match;
@@ -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/stores.ts b/src/webpack/common/stores.ts
index 2f9786bc5c..123c62b053 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/stores.d.ts b/src/webpack/common/types/stores.d.ts
index 8e89a6e209..059924f5aa 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 6d74e9b250..72a71f31c0 100644
--- a/src/webpack/common/utils.ts
+++ b/src/webpack/common/utils.ts
@@ -119,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;
};
@@ -136,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 d3de20727d..f891e38dfb 100644
--- a/src/webpack/patchWebpack.ts
+++ b/src/webpack/patchWebpack.ts
@@ -122,7 +122,7 @@ Object.defineProperty(Function.prototype, "m", {
// 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")) {
+ 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);
}
@@ -257,7 +257,12 @@ function patchFactories(factories: Record .
*/
-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";
@@ -432,7 +432,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
}
const [, rawChunkIds, entryPointId] = match;
- if (Number.isNaN(entryPointId)) {
+ 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);
@@ -462,7 +462,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));
}
/**
diff --git a/tsconfig.json b/tsconfig.json
index e9c9264084..8db0ab3c1e 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/**/*"]
}