diff --git a/src/plugins/betterNotes/index.tsx b/src/plugins/betterNotes/index.tsx
index 2183d98e2..5ebca1f8b 100644
--- a/src/plugins/betterNotes/index.tsx
+++ b/src/plugins/betterNotes/index.tsx
@@ -17,6 +17,7 @@
*/
import { Settings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
@@ -83,7 +84,9 @@ export default definePlugin({
patchPadding(lastSection: any) {
if (!lastSection) return;
return (
-
+
+
+
);
}
});
diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx
index 7d81c6f5c..5064bd538 100644
--- a/src/plugins/betterSettings/index.tsx
+++ b/src/plugins/betterSettings/index.tsx
@@ -6,17 +6,18 @@
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
-import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
+import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
-import { findByPropsLazy } from "@webpack";
+import { waitFor } from "@webpack";
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
import type { HTMLAttributes, ReactElement } from "react";
type SettingsEntry = { section: string, label: string; };
const cl = classNameFactory("");
-const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers");
+let Classes: Record
;
+waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
const settings = definePluginSettings({
disableFade: {
@@ -124,12 +125,19 @@ export default definePlugin({
}
],
+ // This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
+ // without possibly also catching unrelated errors of children.
+ //
+ // Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
+ // try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
+ // not in children
Layer(props: LayerProps) {
- return (
- props.children as any}>
-
-
- );
+ if (!FocusLock || !ComponentDispatch || !Classes) {
+ new Logger("BetterSettings").error("Failed to find some components");
+ return props.children;
+ }
+
+ return ;
},
wrapMenu(list: SettingsEntry[]) {
diff --git a/src/plugins/crashHandler/index.ts b/src/plugins/crashHandler/index.ts
index f8c76d7f7..10053021f 100644
--- a/src/plugins/crashHandler/index.ts
+++ b/src/plugins/crashHandler/index.ts
@@ -104,7 +104,7 @@ export default definePlugin({
shouldAttemptRecover = false;
// This is enough to avoid a crash loop
- setTimeout(() => shouldAttemptRecover = true, 500);
+ setTimeout(() => shouldAttemptRecover = true, 1000);
} catch { }
try {
diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx
index 334372e38..f1b2fbf53 100644
--- a/src/plugins/customRPC/index.tsx
+++ b/src/plugins/customRPC/index.tsx
@@ -17,13 +17,16 @@
*/
import { definePluginSettings, Settings } from "@api/Settings";
+import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
+import { Margins } from "@utils/margins";
+import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
-import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
+import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
@@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
export default definePlugin({
name: "CustomRPC",
description: "Allows you to set a custom rich presence.",
- authors: [Devs.captain, Devs.AutumnVN],
+ authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
start: setRpc,
stop: () => setRpc(true),
settings,
settingsAboutComponent: () => {
const activity = useAwaiter(createActivity);
+ const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
const { profileThemeStyle } = useProfileThemeStyle({});
return (
<>
+ {!gameActivityEnabled && (
+
+ Notice
+ Game activity isn't enabled, people won't be able to see your custom rich presence!
+
+
+
+ )}
+
Go to Discord Developer Portal to create an application and
get the application ID.
@@ -407,7 +429,9 @@ export default definePlugin({
If you want to use image link, download your image and reupload the image to Imgur and get the image link by right-clicking the image and select "Copy image address".
-
+
+
+
{activity[0] &&
) {
try {
const { embed } = this.props;
+ const { replaceElements } = settings.store;
+
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
@@ -58,12 +67,12 @@ async function embedDidMount(this: Component) {
enabled: true
};
- if (hasTitle) {
+ if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
}
- if (hasThumb) {
+ if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
}
@@ -128,10 +137,30 @@ function DearrowButton({ component }: { component: Component; }) {
);
}
+const settings = definePluginSettings({
+ hideButton: {
+ description: "Hides the Dearrow button from YouTube embeds",
+ type: OptionType.BOOLEAN,
+ default: false,
+ restartNeeded: true
+ },
+ replaceElements: {
+ description: "Choose which elements of the embed will be replaced",
+ type: OptionType.SELECT,
+ restartNeeded: true,
+ options: [
+ { label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },
+ { label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
+ { label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
+ ],
+ }
+});
+
export default definePlugin({
name: "Dearrow",
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
authors: [Devs.Ven],
+ settings,
embedDidMount,
renderButton(component: Component) {
@@ -154,7 +183,8 @@ export default definePlugin({
// add dearrow button
{
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
- replace: "children:[$self.renderButton(this),"
+ replace: "children:[$self.renderButton(this),",
+ predicate: () => !settings.store.hideButton
}
]
}],
diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx
index 03feda0a8..087928e9f 100644
--- a/src/plugins/fakeNitro/index.tsx
+++ b/src/plugins/fakeNitro/index.tsx
@@ -111,7 +111,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
const settings = definePluginSettings({
enableEmojiBypass: {
- description: "Allow sending fake emojis",
+ description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
@@ -129,7 +129,7 @@ const settings = definePluginSettings({
restartNeeded: true
},
enableStickerBypass: {
- description: "Allow sending fake stickers",
+ description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
@@ -190,7 +190,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
- description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
+ description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
dependencies: ["MessageEventsAPI"],
settings,
diff --git a/src/plugins/imageZoom/components/Magnifier.tsx b/src/plugins/imageZoom/components/Magnifier.tsx
index 816717350..aadd0903a 100644
--- a/src/plugins/imageZoom/components/Magnifier.tsx
+++ b/src/plugins/imageZoom/components/Magnifier.tsx
@@ -17,6 +17,7 @@
*/
import { classNameFactory } from "@api/Styles";
+import ErrorBoundary from "@components/ErrorBoundary";
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
import { ELEMENT_ID } from "../constants";
@@ -36,7 +37,7 @@ export interface MagnifierProps {
const cl = classNameFactory("vc-imgzoom-");
-export const Magnifier: React.FC = ({ instance, size: initialSize, zoom: initalZoom }) => {
+export const Magnifier = ErrorBoundary.wrap(({ instance, size: initialSize, zoom: initalZoom }) => {
const [ready, setReady] = useState(false);
const [lensPosition, setLensPosition] = useState({ x: 0, y: 0 });
@@ -199,4 +200,4 @@ export const Magnifier: React.FC = ({ instance, size: initialSiz
)}
);
-};
+}, { noop: true });
diff --git a/src/plugins/messageLatency/index.tsx b/src/plugins/messageLatency/index.tsx
index 0b6d75033..48b57863e 100644
--- a/src/plugins/messageLatency/index.tsx
+++ b/src/plugins/messageLatency/index.tsx
@@ -13,7 +13,7 @@ import { findExportedComponentLazy } from "@webpack";
import { SnowflakeUtils, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
-type FillValue = ("status-danger" | "status-warning" | "text-muted");
+type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
type Fill = [FillValue, FillValue, FillValue];
type DiffKey = keyof Diff;
@@ -54,10 +54,24 @@ export default definePlugin({
seconds: Math.round(delta % 60),
};
- const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${k}` : null;
+ const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
const keys = Object.keys(diff) as DiffKey[];
- return keys.map(str).filter(isNonNullish).join(" ") || "0 seconds";
+ const ts = keys.reduce((prev, k) => {
+ const s = str(k);
+
+ return prev + (
+ isNonNullish(s)
+ ? (prev !== ""
+ ? k === "seconds"
+ ? " and "
+ : " "
+ : "") + s
+ : ""
+ );
+ }, "");
+
+ return [ts || "0 seconds", diff.days === 17 && diff.hours === 1] as const;
},
latencyTooltipData(message: Message) {
const { id, nonce } = message;
@@ -73,16 +87,23 @@ export default definePlugin({
const abs = Math.abs(delta);
const ahead = abs !== delta;
- const stringDelta = this.stringDelta(abs);
+ const [stringDelta, isSuspectedKotlinDiscord] = this.stringDelta(abs);
+ const isKotlinDiscord = ahead && isSuspectedKotlinDiscord;
// Also thanks dziurwa
// 2 minutes
const TROLL_LIMIT = 2 * 60;
const { latency } = this.settings.store;
- const fill: Fill = delta >= TROLL_LIMIT || ahead ? ["text-muted", "text-muted", "text-muted"] : delta >= (latency * 2) ? ["status-danger", "text-muted", "text-muted"] : ["status-warning", "status-warning", "text-muted"];
+ const fill: Fill = isKotlinDiscord
+ ? ["status-positive", "status-positive", "text-muted"]
+ : delta >= TROLL_LIMIT || ahead
+ ? ["text-muted", "text-muted", "text-muted"]
+ : delta >= (latency * 2)
+ ? ["status-danger", "text-muted", "text-muted"]
+ : ["status-warning", "status-warning", "text-muted"];
- return abs >= latency ? { delta: stringDelta, ahead: abs !== delta, fill } : null;
+ return abs >= latency ? { delta: stringDelta, ahead, fill, isKotlinDiscord } : null;
},
Tooltip() {
return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
@@ -92,7 +113,7 @@ export default definePlugin({
if (!isNonNullish(d)) return null;
return
{
diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx
index 1753fefbc..e787fefb4 100644
--- a/src/plugins/mutualGroupDMs/index.tsx
+++ b/src/plugins/mutualGroupDMs/index.tsx
@@ -16,6 +16,7 @@
* along with this program. If not, see .
*/
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types";
@@ -60,7 +61,7 @@ export default definePlugin({
}
],
- renderMutualGDMs(user: User, onClose: () => void) {
+ renderMutualGDMs: ErrorBoundary.wrap((user: User, onClose: () => void) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
);
- }
+ })
});
diff --git a/src/plugins/pauseInvitesForever/index.tsx b/src/plugins/pauseInvitesForever/index.tsx
index 81f18fd6e..1e71a4ed1 100644
--- a/src/plugins/pauseInvitesForever/index.tsx
+++ b/src/plugins/pauseInvitesForever/index.tsx
@@ -16,12 +16,12 @@
* along with this program. If not, see .
*/
+import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { GuildStore, RestAPI } from "@webpack/common";
+import { GuildStore, i18n, RestAPI } from "@webpack/common";
-const Messages = findByPropsLazy("GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION");
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
export default definePlugin({
@@ -62,13 +62,15 @@ export default definePlugin({
renderInvitesLabel(guildId: string, setChecked: Function) {
return (
-
+
+
+
);
}
});
diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
index c2e50cedd..963750fa3 100644
--- a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
+++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx
@@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
-import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
+import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { settings } from "..";
@@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
{
- 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/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/readAllNotificationsButton/index.tsx b/src/plugins/readAllNotificationsButton/index.tsx
index ae66e11a4..3bf53f993 100644
--- a/src/plugins/readAllNotificationsButton/index.tsx
+++ b/src/plugins/readAllNotificationsButton/index.tsx
@@ -19,6 +19,7 @@
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 { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
@@ -64,7 +65,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/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!
+
+![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 000000000..f57ee0fc9
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/index.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 { Margins } from "@utils/margins";
+import definePlugin, { OptionType } from "@utils/types";
+import { findComponentLazy } from "@webpack";
+import { ChannelStore, Forms, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
+import { Message } from "discord-types/general";
+
+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,
+ restartNeeded: true,
+ 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],
+
+ settings,
+
+ patches: [
+ {
+ find: ".GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY",
+ replacement: [
+ {
+ match: /(\i)\.Tooltip,{(text:.{0,30}\.Messages\.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY)/,
+ get replace() {
+ if (settings.store.displayStyle === DisplayStyle.Inline)
+ return "$self.TooltipWrapper,{vcProps:arguments[0],$2";
+
+ return "$1.Tooltip,{text:$self.renderTimeoutDuration(arguments[0])";
+ }
+ }
+ ]
+ }
+ ],
+
+ renderTimeoutDuration: ErrorBoundary.wrap(({ message }: { message: Message; }) => {
+ return (
+ <>
+ {i18n.Messages.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}
+
+ {renderTimeout(message, false)}
+
+ >
+ );
+ }, { noop: true }),
+
+ TooltipWrapper: ErrorBoundary.wrap(({ vcProps: { message }, ...tooltipProps }: { vcProps: { message: Message; }; }) => {
+ 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..70a826e10
--- /dev/null
+++ b/src/plugins/showTimeoutDuration/styles.css
@@ -0,0 +1,4 @@
+.vc-std-wrapper {
+ display: flex;
+ align-items: center;
+}
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/themeAttributes/README.md b/src/plugins/themeAttributes/README.md
index 110eca574..87cb803c5 100644
--- a/src/plugins/themeAttributes/README.md
+++ b/src/plugins/themeAttributes/README.md
@@ -15,6 +15,7 @@ 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)
diff --git a/src/plugins/themeAttributes/index.ts b/src/plugins/themeAttributes/index.ts
index 8afc2121f..b8ceac621 100644
--- a/src/plugins/themeAttributes/index.ts
+++ b/src/plugins/themeAttributes/index.ts
@@ -36,10 +36,12 @@ export default definePlugin({
],
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/voiceDownload/index.tsx b/src/plugins/voiceDownload/index.tsx
index 8586b9f91..571c3d0e9 100644
--- a/src/plugins/voiceDownload/index.tsx
+++ b/src/plugins/voiceDownload/index.tsx
@@ -28,9 +28,12 @@ export default definePlugin({
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)
+ }
>
diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts
index 763f6a782..b666d1168 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,
@@ -72,14 +69,35 @@ interface Call {
}
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",
@@ -100,6 +118,11 @@ const settings = definePluginSettings({
description: "Notif duration (secs)",
default: 1.0,
},
+ timeoutPerCharacter: {
+ type: OptionType.NUMBER,
+ description: "Duration multiplier per character",
+ default: 0.5
+ },
opacity: {
type: OptionType.SLIDER,
description: "Notif opacity",
@@ -124,7 +147,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 +157,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 +217,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 +237,7 @@ export default definePlugin({
}
}
+ // color channel mentions
if (channelMatches) {
for (const cMatch of channelMatches) {
let channelId = cMatch.split("<#")[1];
@@ -221,6 +246,7 @@ export default definePlugin({
}
}
+ if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message);
} catch (err) {
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
@@ -229,13 +255,20 @@ 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) {
+ const timeout = Math.max(settings.store.timeout, content.length * settings.store.timeoutPerCharacter);
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,
+ height: calculateHeight(content),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
@@ -254,7 +287,7 @@ function sendOtherNotif(content: string, titleString: string) {
messageType: 1,
index: 0,
timeout: settings.store.timeout,
- height: calculateHeight(cleanMessage(content)),
+ height: calculateHeight(content),
opacity: settings.store.opacity,
volume: settings.store.volume,
audioPath: settings.store.soundPath,
@@ -267,13 +300,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) {
@@ -282,7 +313,3 @@ function calculateHeight(content: string) {
if (content.length <= 300) return 200;
return 250;
}
-
-function cleanMessage(content: string) {
- return content.replace(new RegExp("<[^>]*>", "g"), "");
-}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 6f72824b0..d377cbefd 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -423,6 +423,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
id: 347096063569559553n
},
+ nin0dev: {
+ name: "nin0dev",
+ id: 886685857560539176n
+ },
Elvyra: {
name: "Elvyra",
id: 708275751816003615n,
@@ -463,9 +467,17 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Oleh Polisan",
id: 242305263313485825n
},
+ HAHALOSAH: {
+ name: "HAHALOSAH",
+ id: 903418691268513883n
+ },
GabiRP: {
name: "GabiRP",
id: 507955112027750401n
+ },
+ ImBanana: {
+ name: "Im_Banana",
+ id: 635250116688871425n
}
} satisfies Record);
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 32010e59b..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"
diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts
index 843922f2f..f19928ac4 100644
--- a/src/utils/settingsSync.ts
+++ b/src/utils/settingsSync.ts
@@ -18,7 +18,7 @@
import { showNotification } from "@api/Notifications";
import { PlainSettings, Settings } from "@api/Settings";
-import { Toasts } from "@webpack/common";
+import { moment, Toasts } from "@webpack/common";
import { deflateSync, inflateSync } from "fflate";
import { getCloudAuth, getCloudUrl } from "./cloud";
@@ -49,7 +49,7 @@ export async function exportSettings({ minify }: { minify?: boolean; } = {}) {
}
export async function downloadSettingsBackup() {
- const filename = "vencord-settings-backup.json";
+ const filename = `vencord-settings-backup-${moment().format("YYYY-MM-DD")}.json`;
const backup = await exportSettings();
const data = new TextEncoder().encode(backup);