diff --git a/package.json b/package.json index 9fd84f9b8..0e5845987 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.8.2", + "version": "1.8.3", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/src/api/Commands/commandHelpers.ts b/src/api/Commands/commandHelpers.ts index dc5ecfd67..2f7039137 100644 --- a/src/api/Commands/commandHelpers.ts +++ b/src/api/Commands/commandHelpers.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { mergeDefaults } from "@utils/misc"; +import { mergeDefaults } from "@utils/mergeDefaults"; import { findByPropsLazy } from "@webpack"; import { MessageActions, SnowflakeUtils } from "@webpack/common"; import { Message } from "discord-types/general"; diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 696c12c28..490e6ef7f 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -20,7 +20,7 @@ import { debounce } from "@shared/debounce"; import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore"; import { localStorage } from "@utils/localStorage"; import { Logger } from "@utils/Logger"; -import { mergeDefaults } from "@utils/misc"; +import { mergeDefaults } from "@utils/mergeDefaults"; import { putCloudSettings } from "@utils/settingsSync"; import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types"; import { React } from "@webpack/common"; diff --git a/src/components/VencordSettings/AddonCard.tsx b/src/components/VencordSettings/AddonCard.tsx index c4c3aaca9..1161a6411 100644 --- a/src/components/VencordSettings/AddonCard.tsx +++ b/src/components/VencordSettings/AddonCard.tsx @@ -21,7 +21,7 @@ import "./addonCard.css"; import { classNameFactory } from "@api/Styles"; import { Badge } from "@components/Badge"; import { Switch } from "@components/Switch"; -import { Text } from "@webpack/common"; +import { Text, useRef } from "@webpack/common"; import type { MouseEventHandler, ReactNode } from "react"; const cl = classNameFactory("vc-addon-"); @@ -42,6 +42,8 @@ interface Props { } export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) { + const titleRef = useRef(null); + const titleContainerRef = useRef(null); return (
- {name}{isNew && } +
+
{ + const title = titleRef.current!; + const titleContainer = titleContainerRef.current!; + + title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`); + title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`); + }} + > + {name} +
+
{isNew && }
{!!author && ( diff --git a/src/components/VencordSettings/addonCard.css b/src/components/VencordSettings/addonCard.css index f2dee11d9..e46e4c29c 100644 --- a/src/components/VencordSettings/addonCard.css +++ b/src/components/VencordSettings/addonCard.css @@ -62,3 +62,36 @@ .vc-addon-author::before { content: "by "; } + +.vc-addon-title-container { + width: 100%; + overflow: hidden; + height: 1.25em; + position: relative; +} + +.vc-addon-title { + position: absolute; + inset: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +@keyframes vc-addon-title { + 0% { + transform: translateX(0); + } + + 50% { + transform: translateX(var(--offset)); + } + + 100% { + transform: translateX(0); + } +} + +.vc-addon-title:hover { + overflow: visible; + animation: vc-addon-title var(--duration) linear infinite; +} diff --git a/src/main/settings.ts b/src/main/settings.ts index 96efdd672..3d367a945 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -7,6 +7,7 @@ import type { Settings } from "@api/Settings"; import { IpcEvents } from "@shared/IpcEvents"; import { SettingsStore } from "@shared/SettingsStore"; +import { mergeDefaults } from "@utils/mergeDefaults"; import { ipcMain } from "electron"; import { mkdirSync, readFileSync, writeFileSync } from "fs"; @@ -42,7 +43,22 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string RendererSettings.setData(data, pathToNotify); }); -export const NativeSettings = new SettingsStore(readSettings("native", NATIVE_SETTINGS_FILE)); +export interface NativeSettings { + plugins: { + [plugin: string]: { + [setting: string]: any; + }; + }; +} + +const DefaultNativeSettings: NativeSettings = { + plugins: {} +}; + +const nativeSettings = readSettings("native", NATIVE_SETTINGS_FILE); +mergeDefaults(nativeSettings, DefaultNativeSettings); + +export const NativeSettings = new SettingsStore(nativeSettings); NativeSettings.addGlobalChangeListener(() => { try { diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index 63f32cbd2..c7377833a 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -139,7 +139,7 @@ ${makeCodeblock(enabledPlugins.join(", "))} const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles; if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return; - if (IS_UPDATER_DISABLED) { + if (!IS_WEB && IS_UPDATER_DISABLED) { return Alerts.show({ title: "Hold on!", body:
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 ( -
- {Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION} - {this.showDisableInvites(guildId) && { - setChecked(true); - this.disableInvites(guildId); - }}> Pause Indefinitely.} -
+ +
+ {i18n.Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION} + {this.showDisableInvites(guildId) && { + setChecked(true); + this.disableInvites(guildId); + }}> Pause Indefinitely.} +
+
); } }); 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);