diff --git a/package.json b/package.json index e63c0bcf..a9bfcd87 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 other cutest Discord client mod", "homepage": "https://github.com/Equicord/Equicord#readme", "bugs": { diff --git a/src/api/Commands/commandHelpers.ts b/src/api/Commands/commandHelpers.ts index dc5ecfd6..2f703913 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 99cba491..7cadd7a4 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 39acde2e..f2e44be6 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-"); @@ -43,6 +43,8 @@ interface Props { } export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave, hideSwitch }: 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 f2dee11d..e46e4c29 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 96efdd67..3d367a94 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/betterNotes/index.tsx b/src/plugins/betterNotes/index.tsx index 2183d98e..5ebca1f8 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/customRPC/index.tsx b/src/plugins/customRPC/index.tsx index 334372e3..f1b2fbf5 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 (titles[0]?.votes >= 0) { + if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) { embed.dearrow.oldTitle = embed.rawTitle; embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1"); } - if (thumbnails[0]?.votes >= 0 && thumbnails[0].timestamp) { + 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 e40eb73f..91212b0f 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 81671735..aadd0903 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 0b6d7503..48b57863 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 40068165..d218c42c 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 81f18fd6..1e71a4ed 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 c2e50ced..963750fa 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/pronoundbUtils.ts b/src/plugins/pronoundb/pronoundbUtils.ts index 789cd530..f4bd27ac 100644 --- a/src/plugins/pronoundb/pronoundbUtils.ts +++ b/src/plugins/pronoundb/pronoundbUtils.ts @@ -78,9 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns) return [discordPronouns, "Discord"]; - if (result && result !== PronounMapping.unspecified) { + if (result && result !== PronounMapping.unspecified) return [result, "PronounDB"]; - } return [discordPronouns, "Discord"]; } @@ -142,34 +141,25 @@ 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, { sets: { en: ["unspecified"] } }] as const)); + const dummyPronouns = Object.fromEntries(ids.map(id => [id, { sets: {} }] as const)); Object.assign(cache, dummyPronouns); // @ts-ignore return dummyPronouns; } } -export function extractPronouns(pronounSet: { [locale: string]: PronounCode[]; }): string { - if (!pronounSet || !pronounSet?.en) return PronounMapping.unspecified; - // for some reason pronounDB returns empty sets sometimes instead of nothing? +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; }; - const shouldCapitalise = (pronoun: string) => { - // if a pronoun is a sentence we keep the capitalisation. - return pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronoun); - }; if (pronouns.length === 1) { - if (shouldCapitalise(pronouns[0])) + // 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 if ( - pronounsFormat === PronounsFormat.Lowercase - && ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]) - ) return PronounMapping[pronouns[0]]; else return PronounMapping[pronouns[0]].toLowerCase(); } - return pronouns.map(pronoun => { - const mappedPronoun = PronounMapping[pronoun + "S"]; - return shouldCapitalise(mappedPronoun) ? mappedPronoun : mappedPronoun.toLowerCase(); - }).join("/"); + 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 123a846b..d099a7de 100644 --- a/src/plugins/pronoundb/types.ts +++ b/src/plugins/pronoundb/types.ts @@ -27,16 +27,16 @@ export interface UserProfilePronounsProps { export interface PronounsResponse { [id: string]: { - sets: { + sets?: { [locale: string]: PronounCode[]; - }; - }; + } + } } export interface CachePronouns { - sets: { + sets?: { [locale: string]: PronounCode[]; - }; + } } export type PronounCode = keyof typeof PronounMapping; @@ -51,15 +51,4 @@ export const PronounMapping = { ask: "Ask me my pronouns", avoid: "Avoid pronouns, use my name", unspecified: "No pronouns specified.", - // neither avoid nor unspecified can occur when there is any other pronoun set, hence there is no short for needed. - - // S for short form, used for when there is multiple different pronouns. - // pronounDB actually allows you to select any/other/ask as 2nd or 3rd set, so these are included here. - anyS: "Any", - otherS: "Other", - askS: "Ask", - heS: "He", - itS: "It", - sheS: "She", - theyS: "They", } as const; diff --git a/src/plugins/readAllNotificationsButton/index.tsx b/src/plugins/readAllNotificationsButton/index.tsx index bb975afd..c97e765a 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, EquicordDevs } 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, EquicordDevs.KrystalSkull], 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 76acedcd..7ba245da 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -7,14 +7,14 @@ 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 { GuildMemberStore, RelationshipStore } from "@webpack/common"; import { Message, User } from "discord-types/general"; interface UsernameProps { author: { nick: string; }; - message?: Message; + message: Message; withMentionPrefix?: boolean; isRepliedMessage: boolean; userOverride?: User; @@ -40,11 +40,6 @@ const settings = definePluginSettings({ default: false, description: "Also apply functionality to reply previews", }, - whenTyping: { - type: OptionType.BOOLEAN, - default: false, - description: "Also apply functionality to when someone is typing", - }, }); export default definePlugin({ @@ -59,27 +54,12 @@ export default definePlugin({ replace: "$self.renderUsername(arguments[0])}" } }, - { - find: "getCooldownTextStyle", - predicate: () => settings.store.whenTyping, - replacement: [ - { - match: /\.map\(\i=>\i\.\i\.getName\(\i,this\.props\.channel\.id,\i\)\)/, - replace: "" - }, - { - match: /(?<=children:\[(\i)\.length>0.{0,200}?"aria-atomic":!0,children:)\i/, - replace: "$self.renderTypingNames(this.props, $1, $&)" - } - ] - }, ], settings, - renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => { + renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => { try { - const user = userOverride ?? message?.author; - if (!user) return author?.nick; + const user = userOverride ?? message.author; let { username } = user; if (settings.store.displayNames) username = (user as any).globalName || username; @@ -87,42 +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}; } - }, - - renderTypingNames(props: any, users: User[], children: any) { - if (!Array.isArray(children)) return children; - - let index = 0; - - return children.map(c => { - if (c.type === "strong") { - const user = users[index++]; - if (!user) return c; - - const nick = GuildMemberStore.getNick(props.guildId!, user.id) - || (!props.guildId && RelationshipStore.getNickname(user.id)) - || (user as any).globalName - || user.username; - if (!nick) return c; - - return <>{this.renderUsername({ - author: { nick }, - message: undefined, - isRepliedMessage: false, - withMentionPrefix: false, - userOverride: user - })}; - } - return c; - }); - }, + }, { noop: true }), }); diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index 8b59c6ac..2a6a6428 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 110eca57..87cb803c 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 8afc2121..b8ceac62 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 cc0ed5e9..b22c488e 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 cef003a8..65d84535 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/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts index 763f6a78..b666d116 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 99ae3ef0..55620898 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -420,6 +420,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Kyuuhachi", id: 236588665420251137n, }, + nin0dev: { + name: "nin0dev", + id: 886685857560539176n + }, Elvyra: { name: "Elvyra", id: 708275751816003615n, @@ -460,9 +464,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 00000000..58ba136d --- /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 1c7f63b5..7842f79a 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -20,25 +20,6 @@ import { Clipboard, Toasts } from "@webpack/common"; import { DevsById, EquicordDevsById } 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 52dc4422..15653e50 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);