From 317121fc08b33ae3a6ba8af5353568333c3025f3 Mon Sep 17 00:00:00 2001 From: v Date: Thu, 23 Jan 2025 02:48:44 +0100 Subject: [PATCH 01/28] Replace API add/remove funcs with methods in plugin definition (#3028) --- src/api/Badges.ts | 21 +-- src/api/ChatButtons.tsx | 6 +- ...Decorators.ts => MemberListDecorators.tsx} | 30 ++-- ...eAccessories.ts => MessageAccessories.tsx} | 36 ++-- ...eDecorations.ts => MessageDecorations.tsx} | 24 ++- src/api/MessageEvents.ts | 24 +-- src/api/MessagePopover.tsx | 14 +- src/api/{ServerList.ts => ServerList.tsx} | 35 ++-- src/components/ErrorBoundary.tsx | 3 +- src/plugins/_api/badges/index.tsx | 3 +- src/plugins/_core/supportHelper.tsx | 163 +++++++++--------- .../accountPanelServerProfile/index.tsx | 2 +- src/plugins/clearURLs/index.ts | 32 ++-- src/plugins/fakeNitro/index.tsx | 10 +- src/plugins/hideAttachments/index.tsx | 31 ++-- src/plugins/index.ts | 72 +++++++- src/plugins/invisibleChat.desktop/index.tsx | 50 +++--- src/plugins/messageClickActions/index.ts | 101 ++++++----- src/plugins/messageLinkEmbeds/index.tsx | 6 +- src/plugins/platformIndicators/index.tsx | 18 +- src/plugins/previewMessage/index.tsx | 8 +- src/plugins/quickMention/index.tsx | 27 ++- src/plugins/sendTimestamps/index.tsx | 23 +-- src/plugins/silentMessageToggle/index.tsx | 16 +- src/plugins/silentTyping/index.tsx | 10 +- src/plugins/textReplace/index.tsx | 18 +- src/plugins/translate/TranslateIcon.tsx | 4 +- src/plugins/translate/index.tsx | 68 +++----- src/plugins/unindent/index.ts | 14 +- src/plugins/userVoiceShow/index.tsx | 12 +- src/plugins/viewRaw/index.tsx | 69 ++++---- src/utils/types.ts | 21 +++ 32 files changed, 484 insertions(+), 487 deletions(-) rename src/api/{MemberListDecorators.ts => MemberListDecorators.tsx} (61%) rename src/api/{MessageAccessories.ts => MessageAccessories.tsx} (63%) rename src/api/{MessageDecorations.ts => MessageDecorations.tsx} (65%) rename src/api/{ServerList.ts => ServerList.tsx} (64%) diff --git a/src/api/Badges.ts b/src/api/Badges.ts index 7a041f1ee..ee2f3a30c 100644 --- a/src/api/Badges.ts +++ b/src/api/Badges.ts @@ -57,7 +57,7 @@ const Badges = new Set(); * Register a new badge with the Badges API * @param badge The badge to register */ -export function addBadge(badge: ProfileBadge) { +export function addProfileBadge(badge: ProfileBadge) { badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true }); Badges.add(badge); } @@ -66,7 +66,7 @@ export function addBadge(badge: ProfileBadge) { * Unregister a badge from the Badges API * @param badge The badge to remove */ -export function removeBadge(badge: ProfileBadge) { +export function removeProfileBadge(badge: ProfileBadge) { return Badges.delete(badge); } @@ -100,20 +100,3 @@ export interface BadgeUserArgs { userId: string; guildId: string; } - -interface ConnectedAccount { - type: string; - id: string; - name: string; - verified: boolean; -} - -interface Profile { - connectedAccounts: ConnectedAccount[]; - premiumType: number; - premiumSince: string; - premiumGuildSince?: any; - lastFetched: number; - profileFetchFailed: boolean; - application?: any; -} diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx index d38f4ff50..c24e3886f 100644 --- a/src/api/ChatButtons.tsx +++ b/src/api/ChatButtons.tsx @@ -74,9 +74,9 @@ export interface ChatBarProps { }; } -export type ChatBarButton = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null; +export type ChatBarButtonFactory = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null; -const buttonFactories = new Map(); +const buttonFactories = new Map(); const logger = new Logger("ChatButtons"); export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) { @@ -91,7 +91,7 @@ export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) { } } -export const addChatBarButton = (id: string, button: ChatBarButton) => buttonFactories.set(id, button); +export const addChatBarButton = (id: string, button: ChatBarButtonFactory) => buttonFactories.set(id, button); export const removeChatBarButton = (id: string) => buttonFactories.delete(id); export interface ChatBarButtonProps { diff --git a/src/api/MemberListDecorators.ts b/src/api/MemberListDecorators.tsx similarity index 61% rename from src/api/MemberListDecorators.ts rename to src/api/MemberListDecorators.tsx index ba5ec8d14..2199f4a6c 100644 --- a/src/api/MemberListDecorators.ts +++ b/src/api/MemberListDecorators.tsx @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import ErrorBoundary from "@components/ErrorBoundary"; import { Channel, User } from "discord-types/general/index.js"; import { JSX } from "react"; @@ -39,27 +40,32 @@ interface DecoratorProps { user: User; [key: string]: any; } -export type Decorator = (props: DecoratorProps) => JSX.Element | null; +export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null; type OnlyIn = "guilds" | "dms"; -export const decorators = new Map(); +export const decorators = new Map(); -export function addDecorator(identifier: string, decorator: Decorator, onlyIn?: OnlyIn) { - decorators.set(identifier, { decorator, onlyIn }); +export function addMemberListDecorator(identifier: string, render: MemberListDecoratorFactory, onlyIn?: OnlyIn) { + decorators.set(identifier, { render, onlyIn }); } -export function removeDecorator(identifier: string) { +export function removeMemberListDecorator(identifier: string) { decorators.delete(identifier); } export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] { const isInGuild = !!(props.guildId); - return Array.from(decorators.values(), decoratorObj => { - const { decorator, onlyIn } = decoratorObj; - // this can most likely be done cleaner - if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) { - return decorator(props); + return Array.from( + decorators.entries(), + ([key, { render: Decorator, onlyIn }]) => { + if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild)) + return null; + + return ( + + + + ); } - return null; - }); + ); } diff --git a/src/api/MessageAccessories.ts b/src/api/MessageAccessories.tsx similarity index 63% rename from src/api/MessageAccessories.ts rename to src/api/MessageAccessories.tsx index 8454732f4..71664e93a 100644 --- a/src/api/MessageAccessories.ts +++ b/src/api/MessageAccessories.tsx @@ -16,28 +16,29 @@ * along with this program. If not, see . */ -import { JSX } from "react"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { JSX, ReactNode } from "react"; -export type AccessoryCallback = (props: Record) => JSX.Element | null | Array; -export type Accessory = { - callback: AccessoryCallback; +export type MessageAccessoryFactory = (props: Record) => ReactNode; +export type MessageAccessory = { + render: MessageAccessoryFactory; position?: number; }; -export const accessories = new Map(); +export const accessories = new Map(); -export function addAccessory( +export function addMessageAccessory( identifier: string, - callback: AccessoryCallback, + render: MessageAccessoryFactory, position?: number ) { accessories.set(identifier, { - callback, + render, position, }); } -export function removeAccessory(identifier: string) { +export function removeMessageAccessory(identifier: string) { accessories.delete(identifier); } @@ -45,15 +46,12 @@ export function _modifyAccessories( elements: JSX.Element[], props: Record ) { - for (const accessory of accessories.values()) { - let accessories = accessory.callback(props); - if (accessories == null) - continue; - - if (!Array.isArray(accessories)) - accessories = [accessories]; - else if (accessories.length === 0) - continue; + for (const [key, accessory] of accessories.entries()) { + const res = ( + + + + ); elements.splice( accessory.position != null @@ -62,7 +60,7 @@ export function _modifyAccessories( : accessory.position : elements.length, 0, - ...accessories.filter(e => e != null) as JSX.Element[] + res ); } diff --git a/src/api/MessageDecorations.ts b/src/api/MessageDecorations.tsx similarity index 65% rename from src/api/MessageDecorations.ts rename to src/api/MessageDecorations.tsx index 0d69ab11c..740c95876 100644 --- a/src/api/MessageDecorations.ts +++ b/src/api/MessageDecorations.tsx @@ -16,10 +16,11 @@ * along with this program. If not, see . */ +import ErrorBoundary from "@components/ErrorBoundary"; import { Channel, Message } from "discord-types/general/index.js"; import { JSX } from "react"; -interface DecorationProps { +export interface MessageDecorationProps { author: { /** * Will be username if the user has no nickname @@ -45,20 +46,25 @@ interface DecorationProps { message: Message; [key: string]: any; } -export type Decoration = (props: DecorationProps) => JSX.Element | null; +export type MessageDecorationFactory = (props: MessageDecorationProps) => JSX.Element | null; -export const decorations = new Map(); +export const decorations = new Map(); -export function addDecoration(identifier: string, decoration: Decoration) { +export function addMessageDecoration(identifier: string, decoration: MessageDecorationFactory) { decorations.set(identifier, decoration); } -export function removeDecoration(identifier: string) { +export function removeMessageDecoration(identifier: string) { decorations.delete(identifier); } -export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] { - return [...decorations.values()].map(decoration => { - return decoration(props); - }); +export function __addDecorationsToMessage(props: MessageDecorationProps): (JSX.Element | null)[] { + return Array.from( + decorations.entries(), + ([key, Decoration]) => ( + + + + ) + ); } diff --git a/src/api/MessageEvents.ts b/src/api/MessageEvents.ts index d6eba748f..1b55ff340 100644 --- a/src/api/MessageEvents.ts +++ b/src/api/MessageEvents.ts @@ -73,11 +73,11 @@ export interface MessageExtra { openWarningPopout: (props: any) => any; } -export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable; -export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; +export type MessageSendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable; +export type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; -const sendListeners = new Set(); -const editListeners = new Set(); +const sendListeners = new Set(); +const editListeners = new Set(); export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) { extra.replyOptions = replyOptions; @@ -111,29 +111,29 @@ export async function _handlePreEdit(channelId: string, messageId: string, messa /** * Note: This event fires off before a message is sent, allowing you to edit the message. */ -export function addPreSendListener(listener: SendListener) { +export function addMessagePreSendListener(listener: MessageSendListener) { sendListeners.add(listener); return listener; } /** * Note: This event fires off before a message's edit is applied, allowing you to further edit the message. */ -export function addPreEditListener(listener: EditListener) { +export function addMessagePreEditListener(listener: MessageEditListener) { editListeners.add(listener); return listener; } -export function removePreSendListener(listener: SendListener) { +export function removeMessagePreSendListener(listener: MessageSendListener) { return sendListeners.delete(listener); } -export function removePreEditListener(listener: EditListener) { +export function removeMessagePreEditListener(listener: MessageEditListener) { return editListeners.delete(listener); } // Message clicks -type ClickListener = (message: Message, channel: Channel, event: MouseEvent) => void; +export type MessageClickListener = (message: Message, channel: Channel, event: MouseEvent) => void; -const listeners = new Set(); +const listeners = new Set(); export function _handleClick(message: Message, channel: Channel, event: MouseEvent) { // message object may be outdated, so (try to) fetch latest one @@ -147,11 +147,11 @@ export function _handleClick(message: Message, channel: Channel, event: MouseEve } } -export function addClickListener(listener: ClickListener) { +export function addMessageClickListener(listener: MessageClickListener) { listeners.add(listener); return listener; } -export function removeClickListener(listener: ClickListener) { +export function removeMessageClickListener(listener: MessageClickListener) { return listeners.delete(listener); } diff --git a/src/api/MessagePopover.tsx b/src/api/MessagePopover.tsx index eb68ed2d6..717879546 100644 --- a/src/api/MessagePopover.tsx +++ b/src/api/MessagePopover.tsx @@ -23,7 +23,7 @@ import type { ComponentType, MouseEventHandler } from "react"; const logger = new Logger("MessagePopover"); -export interface ButtonItem { +export interface MessagePopoverButtonItem { key?: string, label: string, icon: ComponentType, @@ -33,23 +33,23 @@ export interface ButtonItem { onContextMenu?: MouseEventHandler; } -export type getButtonItem = (message: Message) => ButtonItem | null; +export type MessagePopoverButtonFactory = (message: Message) => MessagePopoverButtonItem | null; -export const buttons = new Map(); +export const buttons = new Map(); -export function addButton( +export function addMessagePopoverButton( identifier: string, - item: getButtonItem, + item: MessagePopoverButtonFactory, ) { buttons.set(identifier, item); } -export function removeButton(identifier: string) { +export function removeMessagePopoverButton(identifier: string) { buttons.delete(identifier); } export function _buildPopoverElements( - Component: React.ComponentType, + Component: React.ComponentType, message: Message ) { const items: React.ReactNode[] = []; diff --git a/src/api/ServerList.ts b/src/api/ServerList.tsx similarity index 64% rename from src/api/ServerList.ts rename to src/api/ServerList.tsx index 462745b04..7a673c9df 100644 --- a/src/api/ServerList.ts +++ b/src/api/ServerList.tsx @@ -16,41 +16,36 @@ * along with this program. If not, see . */ -import { Logger } from "@utils/Logger"; -import { JSX } from "react"; - -const logger = new Logger("ServerListAPI"); +import ErrorBoundary from "@components/ErrorBoundary"; +import { ComponentType } from "react"; export const enum ServerListRenderPosition { Above, In, } -const renderFunctionsAbove = new Set(); -const renderFunctionsIn = new Set(); +const componentsAbove = new Set(); +const componentsBelow = new Set(); function getRenderFunctions(position: ServerListRenderPosition) { - return position === ServerListRenderPosition.Above ? renderFunctionsAbove : renderFunctionsIn; + return position === ServerListRenderPosition.Above ? componentsAbove : componentsBelow; } -export function addServerListElement(position: ServerListRenderPosition, renderFunction: Function) { +export function addServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) { getRenderFunctions(position).add(renderFunction); } -export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) { +export function removeServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) { getRenderFunctions(position).delete(renderFunction); } export const renderAll = (position: ServerListRenderPosition) => { - const ret: Array = []; - - for (const renderFunction of getRenderFunctions(position)) { - try { - ret.unshift(renderFunction()); - } catch (e) { - logger.error("Failed to render server list element:", e); - } - } - - return ret; + return Array.from( + getRenderFunctions(position), + (Component, i) => ( + + + + ) + ); }; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 60ff1faf2..bb2df3421 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -70,8 +70,7 @@ const ErrorBoundary = LazyComponent(() => { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps }); - logger.error("A component threw an Error\n", error); - logger.error("Component Stack", errorInfo.componentStack); + logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack); } render() { diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index acdfb1f1f..2a83809d6 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -102,8 +102,9 @@ export default definePlugin({ } }, + userProfileBadge: ContributorBadge, + async start() { - Vencord.Api.Badges.addBadge(ContributorBadge); await loadBadges(); }, diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index 8687f843b..72b683249 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import { addAccessory } from "@api/MessageAccessories"; import { definePluginSettings } from "@api/Settings"; import { getUserSettingLazy } from "@api/UserSettings"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -143,7 +142,7 @@ export default definePlugin({ required: true, description: "Helps us provide support to you", authors: [Devs.Ven], - dependencies: ["UserSettingsAPI", "MessageAccessoriesAPI"], + dependencies: ["UserSettingsAPI"], settings, @@ -236,6 +235,85 @@ export default definePlugin({ } }, + renderMessageAccessory(props) { + const buttons = [] as JSX.Element[]; + + const shouldAddUpdateButton = + !IS_UPDATER_DISABLED + && ( + (props.channel.id === KNOWN_ISSUES_CHANNEL_ID) || + (props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID) + ) + && props.message.content?.includes("update"); + + if (shouldAddUpdateButton) { + buttons.push( + + ); + } + + if (props.channel.id === SUPPORT_CHANNEL_ID) { + if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) { + buttons.push( + , + + ); + } + + if (props.message.author.id === VENBOT_USER_ID) { + const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || ""); + if (match) { + buttons.push( + + ); + } + } + } + + return buttons.length + ? {buttons} + : null; + }, + renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => { const userId = channel.getRecipientId(); if (!isPluginDev(userId)) return null; @@ -250,85 +328,4 @@ export default definePlugin({ ); }, { noop: true }), - - start() { - addAccessory("vencord-debug", props => { - const buttons = [] as JSX.Element[]; - - const shouldAddUpdateButton = - !IS_UPDATER_DISABLED - && ( - (props.channel.id === KNOWN_ISSUES_CHANNEL_ID) || - (props.channel.id === SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID) - ) - && props.message.content?.includes("update"); - - if (shouldAddUpdateButton) { - buttons.push( - - ); - } - - if (props.channel.id === SUPPORT_CHANNEL_ID) { - if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) { - buttons.push( - , - - ); - } - - if (props.message.author.id === VENBOT_USER_ID) { - const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || ""); - if (match) { - buttons.push( - - ); - } - } - } - - return buttons.length - ? {buttons} - : null; - }); - }, }); diff --git a/src/plugins/accountPanelServerProfile/index.tsx b/src/plugins/accountPanelServerProfile/index.tsx index e2dc220b7..a2fed5d79 100644 --- a/src/plugins/accountPanelServerProfile/index.tsx +++ b/src/plugins/accountPanelServerProfile/index.tsx @@ -23,7 +23,7 @@ const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cann const styles = findByPropsLazy("accountProfilePopoutWrapper"); let openAlternatePopout = false; -let accountPanelRef: React.MutableRefObject | null> = { current: null }; +let accountPanelRef: React.RefObject | null> = { current: null }; const AccountPanelContextMenu = ErrorBoundary.wrap(() => { const { prioritizeServerProfile } = settings.use(["prioritizeServerProfile"]); diff --git a/src/plugins/clearURLs/index.ts b/src/plugins/clearURLs/index.ts index d1be6c6f5..f00c751d4 100644 --- a/src/plugins/clearURLs/index.ts +++ b/src/plugins/clearURLs/index.ts @@ -17,11 +17,7 @@ */ import { - addPreEditListener, - addPreSendListener, - MessageObject, - removePreEditListener, - removePreSendListener + MessageObject } from "@api/MessageEvents"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -36,7 +32,18 @@ export default definePlugin({ name: "ClearURLs", description: "Removes tracking garbage from URLs", authors: [Devs.adryd], - dependencies: ["MessageEventsAPI"], + + start() { + this.createRules(); + }, + + onBeforeMessageSend(_, msg) { + return this.onSend(msg); + }, + + onBeforeMessageEdit(_cid, _mid, msg) { + return this.onSend(msg); + }, escapeRegExp(str: string) { return (str && reHasRegExpChar.test(str)) @@ -133,17 +140,4 @@ export default definePlugin({ ); } }, - - start() { - this.createRules(); - this.preSend = addPreSendListener((_, msg) => this.onSend(msg)); - this.preEdit = addPreEditListener((_cid, _mid, msg) => - this.onSend(msg) - ); - }, - - stop() { - removePreSendListener(this.preSend); - removePreEditListener(this.preEdit); - }, }); diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index df8b4cd87..d87170ad8 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; +import { addMessagePreEditListener, addMessagePreSendListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; @@ -853,7 +853,7 @@ export default definePlugin({ }); } - this.preSend = addPreSendListener(async (channelId, messageObj, extra) => { + this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => { const { guildId } = this; let hasBypass = false; @@ -941,7 +941,7 @@ export default definePlugin({ return { cancel: false }; }); - this.preEdit = addPreEditListener(async (channelId, __, messageObj) => { + this.preEdit = addMessagePreEditListener(async (channelId, __, messageObj) => { if (!s.enableEmojiBypass) return; let hasBypass = false; @@ -973,7 +973,7 @@ export default definePlugin({ }, stop() { - removePreSendListener(this.preSend); - removePreEditListener(this.preEdit); + removeMessagePreSendListener(this.preSend); + removeMessagePreEditListener(this.preEdit); } }); diff --git a/src/plugins/hideAttachments/index.tsx b/src/plugins/hideAttachments/index.tsx index fe7f4ab92..e122e3cb5 100644 --- a/src/plugins/hideAttachments/index.tsx +++ b/src/plugins/hideAttachments/index.tsx @@ -17,7 +17,6 @@ */ import { get, set } from "@api/DataStore"; -import { addButton, removeButton } from "@api/MessagePopover"; import { ImageInvisible, ImageVisible } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -38,7 +37,20 @@ export default definePlugin({ name: "HideAttachments", description: "Hide attachments and Embeds for individual messages via hover button", authors: [Devs.Ven], - dependencies: ["MessagePopoverAPI"], + + renderMessagePopoverButton(msg) { + if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null; + + const isHidden = hiddenMessages.has(msg.id); + + return { + label: isHidden ? "Show Attachments" : "Hide Attachments", + icon: isHidden ? ImageVisible : ImageInvisible, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => this.toggleHide(msg.id) + }; + }, async start() { style = document.createElement("style"); @@ -47,26 +59,11 @@ export default definePlugin({ await getHiddenMessages(); await this.buildCss(); - - addButton("HideAttachments", msg => { - if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null; - - const isHidden = hiddenMessages.has(msg.id); - - return { - label: isHidden ? "Show Attachments" : "Hide Attachments", - icon: isHidden ? ImageVisible : ImageInvisible, - message: msg, - channel: ChannelStore.getChannel(msg.channel_id), - onClick: () => this.toggleHide(msg.id) - }; - }); }, stop() { style.remove(); hiddenMessages.clear(); - removeButton("HideAttachments"); }, async buildCss() { diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 129e42a0d..9d672c634 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -16,12 +16,19 @@ * along with this program. If not, see . */ +import { addProfileBadge, removeProfileBadge } from "@api/Badges"; +import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { registerCommand, unregisterCommand } from "@api/Commands"; import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; +import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators"; +import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories"; +import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; +import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents"; +import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover"; import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; import { canonicalizeFind } from "@utils/patches"; -import { Patch, Plugin, ReporterTestable, StartAt } from "@utils/types"; +import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types"; import { FluxDispatcher } from "@webpack/common"; import { FluxEvents } from "@webpack/types"; @@ -83,6 +90,13 @@ function isReporterTestable(p: Plugin, part: ReporterTestable) { : (p.reporterTestable & part) === part; } +const pluginKeysToBind: Array = [ + "onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick", + "renderChatBarButton", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton" +]; + +const neededApiPlugins = new Set(); + // First round-trip to mark and force enable dependencies // // FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only @@ -106,12 +120,25 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) { dep.isDependency = true; }); - if (p.commands?.length) { - Plugins.CommandsAPI.isDependency = true; - settings.CommandsAPI.enabled = true; + if (p.commands?.length) neededApiPlugins.add("CommandsAPI"); + if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI"); + if (p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI"); + if (p.renderMemberListDecorator) neededApiPlugins.add("MemberListDecoratorsAPI"); + if (p.renderMessageAccessory) neededApiPlugins.add("MessageAccessoriesAPI"); + if (p.renderMessageDecoration) neededApiPlugins.add("MessageDecorationsAPI"); + if (p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI"); + if (p.userProfileBadge) neededApiPlugins.add("BadgeAPI"); + + for (const key of pluginKeysToBind) { + p[key] &&= p[key].bind(p) as any; } } +for (const p of neededApiPlugins) { + Plugins[p].isDependency = true; + settings[p].enabled = true; +} + for (const p of pluginsValues) { if (p.settings) { p.settings.pluginName = p.name; @@ -215,7 +242,11 @@ export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatc } export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { - const { name, commands, contextMenus } = p; + const { + name, commands, contextMenus, userProfileBadge, + onBeforeMessageEdit, onBeforeMessageSend, onMessageClick, + renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton + } = p; if (p.start) { logger.info("Starting plugin", name); @@ -249,7 +280,6 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: subscribePluginFluxEvents(p, FluxDispatcher); } - if (contextMenus) { logger.debug("Adding context menus patches of plugin", name); for (const navId in contextMenus) { @@ -257,11 +287,27 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } } + if (userProfileBadge) addProfileBadge(userProfileBadge); + + if (onBeforeMessageEdit) addMessagePreEditListener(onBeforeMessageEdit); + if (onBeforeMessageSend) addMessagePreSendListener(onBeforeMessageSend); + if (onMessageClick) addMessageClickListener(onMessageClick); + + if (renderChatBarButton) addChatBarButton(name, renderChatBarButton); + if (renderMemberListDecorator) addMemberListDecorator(name, renderMemberListDecorator); + if (renderMessageDecoration) addMessageDecoration(name, renderMessageDecoration); + if (renderMessageAccessory) addMessageAccessory(name, renderMessageAccessory); + if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton); + return true; }, p => `startPlugin ${p.name}`); export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) { - const { name, commands, contextMenus } = p; + const { + name, commands, contextMenus, userProfileBadge, + onBeforeMessageEdit, onBeforeMessageSend, onMessageClick, + renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton + } = p; if (p.stop) { logger.info("Stopping plugin", name); @@ -300,5 +346,17 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } } + if (userProfileBadge) removeProfileBadge(userProfileBadge); + + if (onBeforeMessageEdit) removeMessagePreEditListener(onBeforeMessageEdit); + if (onBeforeMessageSend) removeMessagePreSendListener(onBeforeMessageSend); + if (onMessageClick) removeMessageClickListener(onMessageClick); + + if (renderChatBarButton) removeChatBarButton(name); + if (renderMemberListDecorator) removeMemberListDecorator(name); + if (renderMessageDecoration) removeMessageDecoration(name); + if (renderMessageAccessory) removeMessageAccessory(name); + if (renderMessagePopoverButton) removeMessagePopoverButton(name); + return true; }, p => `stopPlugin ${p.name}`); diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx index 1af8f4e5d..d6a39cbaf 100644 --- a/src/plugins/invisibleChat.desktop/index.tsx +++ b/src/plugins/invisibleChat.desktop/index.tsx @@ -16,8 +16,7 @@ * along with this program. If not, see . */ -import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; -import { addButton, removeButton } from "@api/MessagePopover"; +import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { updateMessage } from "@api/MessageUpdater"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -66,7 +65,7 @@ function Indicator() { } -const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { +const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => { if (!isMainChat) return null; return ( @@ -104,7 +103,7 @@ export default definePlugin({ name: "InvisibleChat", description: "Encrypt your Messages in a non-suspicious way!", authors: [Devs.SammCheese], - dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI", "MessageUpdaterAPI"], + dependencies: ["MessageUpdaterAPI"], reporterTestable: ReporterTestable.Patches, settings, @@ -125,36 +124,31 @@ export default definePlugin({ /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/, ), async start() { - addButton("InvisibleChat", message => { - return this.INV_REGEX.test(message?.content) - ? { - label: "Decrypt Message", - icon: this.popOverIcon, - message: message, - channel: ChannelStore.getChannel(message.channel_id), - onClick: async () => { - const res = await iteratePasswords(message); - - if (res) - this.buildEmbed(message, res); - else - buildDecModal({ message }); - } - } - : null; - }); - - addChatBarButton("InvisibleChat", ChatBarIcon); - const { default: StegCloak } = await getStegCloak(); steggo = new StegCloak(true, false); }, - stop() { - removeButton("InvisibleChat"); - removeButton("InvisibleChat"); + renderMessagePopoverButton(message) { + return this.INV_REGEX.test(message?.content) + ? { + label: "Decrypt Message", + icon: this.popOverIcon, + message: message, + channel: ChannelStore.getChannel(message.channel_id), + onClick: async () => { + const res = await iteratePasswords(message); + + if (res) + this.buildEmbed(message, res); + else + buildDecModal({ message }); + } + } + : null; }, + renderChatBarButton: ChatBarIcon, + // Gets the Embed of a Link async getEmbed(url: URL): Promise { const { body } = await RestAPI.post({ diff --git a/src/plugins/messageClickActions/index.ts b/src/plugins/messageClickActions/index.ts index 7437cace7..19ccaa955 100644 --- a/src/plugins/messageClickActions/index.ts +++ b/src/plugins/messageClickActions/index.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import { addClickListener, removeClickListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -57,66 +56,64 @@ export default definePlugin({ name: "MessageClickActions", description: "Hold Backspace and click to delete, double click to edit/reply", authors: [Devs.Ven], - dependencies: ["MessageEventsAPI"], settings, start() { document.addEventListener("keydown", keydown); document.addEventListener("keyup", keyup); - - this.onClick = addClickListener((msg: any, channel, event) => { - const isMe = msg.author.id === UserStore.getCurrentUser().id; - if (!isDeletePressed) { - if (event.detail < 2) return; - if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return; - if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return; - if (msg.deleted === true) return; - - if (isMe) { - if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return; - - MessageActions.startEditMessage(channel.id, msg.id, msg.content); - event.preventDefault(); - } else { - if (!settings.store.enableDoubleClickToReply) return; - - const EPHEMERAL = 64; - if (msg.hasFlag(EPHEMERAL)) return; - - const isShiftPress = event.shiftKey && !settings.store.requireModifier; - const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default; - const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention") - ? NoReplyMention.shouldMention(msg, isShiftPress) - : !isShiftPress; - - FluxDispatcher.dispatch({ - type: "CREATE_PENDING_REPLY", - channel, - message: msg, - shouldMention, - showMentionToggle: channel.guild_id !== null - }); - } - } else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel))) { - if (msg.deleted) { - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: channel.id, - id: msg.id, - mlDeleted: true - }); - } else { - MessageActions.deleteMessage(channel.id, msg.id); - } - event.preventDefault(); - } - }); }, stop() { - removeClickListener(this.onClick); document.removeEventListener("keydown", keydown); document.removeEventListener("keyup", keyup); - } + }, + + onMessageClick(msg: any, channel, event) { + const isMe = msg.author.id === UserStore.getCurrentUser().id; + if (!isDeletePressed) { + if (event.detail < 2) return; + if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return; + if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return; + if (msg.deleted === true) return; + + if (isMe) { + if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id) || msg.state !== "SENT") return; + + MessageActions.startEditMessage(channel.id, msg.id, msg.content); + event.preventDefault(); + } else { + if (!settings.store.enableDoubleClickToReply) return; + + const EPHEMERAL = 64; + if (msg.hasFlag(EPHEMERAL)) return; + + const isShiftPress = event.shiftKey && !settings.store.requireModifier; + const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default; + const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention") + ? NoReplyMention.shouldMention(msg, isShiftPress) + : !isShiftPress; + + FluxDispatcher.dispatch({ + type: "CREATE_PENDING_REPLY", + channel, + message: msg, + shouldMention, + showMentionToggle: channel.guild_id !== null + }); + } + } else if (settings.store.enableDeleteOnClick && (isMe || PermissionStore.can(PermissionsBits.MANAGE_MESSAGES, channel))) { + if (msg.deleted) { + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: channel.id, + id: msg.id, + mlDeleted: true + }); + } else { + MessageActions.deleteMessage(channel.id, msg.id); + } + event.preventDefault(); + } + }, }); diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx index 98a153937..c28e78017 100644 --- a/src/plugins/messageLinkEmbeds/index.tsx +++ b/src/plugins/messageLinkEmbeds/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addAccessory, removeAccessory } from "@api/MessageAccessories"; +import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories"; import { updateMessage } from "@api/MessageUpdater"; import { definePluginSettings } from "@api/Settings"; import { getUserSettingLazy } from "@api/UserSettings"; @@ -373,7 +373,7 @@ export default definePlugin({ settings, start() { - addAccessory("messageLinkEmbed", props => { + addMessageAccessory("messageLinkEmbed", props => { if (!messageLinkRegex.test(props.message.content)) return null; @@ -391,6 +391,6 @@ export default definePlugin({ }, stop() { - removeAccessory("messageLinkEmbed"); + removeMessageAccessory("messageLinkEmbed"); } }); diff --git a/src/plugins/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx index 1dc76e9d3..d2b722eff 100644 --- a/src/plugins/platformIndicators/index.tsx +++ b/src/plugins/platformIndicators/index.tsx @@ -18,9 +18,9 @@ import "./style.css"; -import { addBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeBadge } from "@api/Badges"; -import { addDecorator, removeDecorator } from "@api/MemberListDecorators"; -import { addDecoration, removeDecoration } from "@api/MessageDecorations"; +import { addProfileBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeProfileBadge } from "@api/Badges"; +import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators"; +import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; import { Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; @@ -172,26 +172,26 @@ const badge: ProfileBadge = { const indicatorLocations = { list: { description: "In the member list", - onEnable: () => addDecorator("platform-indicator", props => + onEnable: () => addMemberListDecorator("platform-indicator", props => ), - onDisable: () => removeDecorator("platform-indicator") + onDisable: () => removeMemberListDecorator("platform-indicator") }, badges: { description: "In user profiles, as badges", - onEnable: () => addBadge(badge), - onDisable: () => removeBadge(badge) + onEnable: () => addProfileBadge(badge), + onDisable: () => removeProfileBadge(badge) }, messages: { description: "Inside messages", - onEnable: () => addDecoration("platform-indicator", props => + onEnable: () => addMessageDecoration("platform-indicator", props => ), - onDisable: () => removeDecoration("platform-indicator") + onDisable: () => removeMessageDecoration("platform-indicator") } }; diff --git a/src/plugins/previewMessage/index.tsx b/src/plugins/previewMessage/index.tsx index fe6b227a5..7b03e31d7 100644 --- a/src/plugins/previewMessage/index.tsx +++ b/src/plugins/previewMessage/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; +import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { generateId, sendBotMessage } from "@api/Commands"; import { Devs } from "@utils/constants"; import definePlugin, { StartAt } from "@utils/types"; @@ -73,7 +73,7 @@ const getAttachments = async (channelId: string) => ); -const PreviewButton: ChatBarButton = ({ isMainChat, isEmpty, type: { attachments } }) => { +const PreviewButton: ChatBarButtonFactory = ({ isMainChat, isEmpty, type: { attachments } }) => { const channelId = SelectedChannelStore.getChannelId(); const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); @@ -121,11 +121,9 @@ export default definePlugin({ name: "PreviewMessage", description: "Lets you preview your message before sending it.", authors: [Devs.Aria], - dependencies: ["ChatInputButtonAPI"], // start early to ensure we're the first plugin to add our button // This makes the popping in less awkward startAt: StartAt.Init, - start: () => addChatBarButton("previewMessage", PreviewButton), - stop: () => removeChatBarButton("previewMessage"), + renderChatBarButton: PreviewButton, }); diff --git a/src/plugins/quickMention/index.tsx b/src/plugins/quickMention/index.tsx index df86e9b70..8d275354f 100644 --- a/src/plugins/quickMention/index.tsx +++ b/src/plugins/quickMention/index.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import { addButton, removeButton } from "@api/MessagePopover"; import { Devs } from "@utils/constants"; import { insertTextIntoChatInputBox } from "@utils/discord"; import definePlugin from "@utils/types"; @@ -26,24 +25,18 @@ export default definePlugin({ name: "QuickMention", authors: [Devs.kemo], description: "Adds a quick mention button to the message actions bar", - dependencies: ["MessagePopoverAPI"], - start() { - addButton("QuickMention", msg => { - const channel = ChannelStore.getChannel(msg.channel_id); - if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null; + renderMessagePopoverButton(msg) { + const channel = ChannelStore.getChannel(msg.channel_id); + if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null; - return { - label: "Quick Mention", - icon: this.Icon, - message: msg, - channel, - onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `) - }; - }); - }, - stop() { - removeButton("QuickMention"); + return { + label: "Quick Mention", + icon: this.Icon, + message: msg, + channel, + onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `) + }; }, Icon: () => ( diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx index 2306c20cd..437df1a58 100644 --- a/src/plugins/sendTimestamps/index.tsx +++ b/src/plugins/sendTimestamps/index.tsx @@ -18,8 +18,7 @@ import "./styles.css"; -import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; -import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; +import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Devs } from "@utils/constants"; @@ -123,7 +122,7 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi ); } -const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { +const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => { if (!isMainChat) return null; return ( @@ -160,22 +159,14 @@ export default definePlugin({ name: "SendTimestamps", description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!", authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11], - dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], - settings, - start() { - addChatBarButton("SendTimestamps", ChatBarIcon); - this.listener = addPreSendListener((_, msg) => { - if (settings.store.replaceMessageContents) { - msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); - } - }); - }, + renderChatBarButton: ChatBarIcon, - stop() { - removeChatBarButton("SendTimestamps"); - removePreSendListener(this.listener); + onBeforeMessageSend(_, msg) { + if (settings.store.replaceMessageContents) { + msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); + } }, settingsAboutComponent() { diff --git a/src/plugins/silentMessageToggle/index.tsx b/src/plugins/silentMessageToggle/index.tsx index d7a7ed7f0..00e605099 100644 --- a/src/plugins/silentMessageToggle/index.tsx +++ b/src/plugins/silentMessageToggle/index.tsx @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; -import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; +import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; +import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -41,7 +41,7 @@ const settings = definePluginSettings({ } }); -const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => { +const SilentMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => { const [enabled, setEnabled] = useState(lastState); function setEnabledValue(value: boolean) { @@ -50,15 +50,15 @@ const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => { } useEffect(() => { - const listener: SendListener = (_, message) => { + const listener: MessageSendListener = (_, message) => { if (enabled) { if (settings.store.autoDisable) setEnabledValue(false); if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content; } }; - addPreSendListener(listener); - return () => void removePreSendListener(listener); + addMessagePreSendListener(listener); + return () => void removeMessagePreSendListener(listener); }, [enabled]); if (!isMainChat) return null; @@ -91,9 +91,7 @@ export default definePlugin({ name: "SilentMessageToggle", authors: [Devs.Nuckyz, Devs.CatNoir], description: "Adds a button to the chat bar to toggle sending a silent message.", - dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], settings, - start: () => addChatBarButton("SilentMessageToggle", SilentMessageToggle), - stop: () => removeChatBarButton("SilentMessageToggle") + renderChatBarButton: SilentMessageToggle, }); diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index d06ae8373..92bdbc49e 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; +import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; @@ -43,7 +43,7 @@ const settings = definePluginSettings({ } }); -const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => { +const SilentTypingToggle: ChatBarButtonFactory = ({ isMainChat }) => { const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]); const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; @@ -96,11 +96,12 @@ export default definePlugin({ name: "SilentTyping", authors: [Devs.Ven, Devs.Rini, Devs.ImBanana], description: "Hide that you are typing", - dependencies: ["ChatInputButtonAPI"], settings, + contextMenus: { "textarea-context": ChatBarContextCheckbox }, + patches: [ { find: '.dispatch({type:"TYPING_START_LOCAL"', @@ -136,6 +137,5 @@ export default definePlugin({ FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); }, - start: () => addChatBarButton("SilentTyping", SilentTypingToggle), - stop: () => removeChatBarButton("SilentTyping"), + renderChatBarButton: SilentTypingToggle, }); diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index 615477d07..bf5d62836 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -17,7 +17,6 @@ */ import { DataStore } from "@api/index"; -import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { Flex } from "@components/Flex"; import { DeleteIcon } from "@components/Icons"; @@ -244,22 +243,17 @@ export default definePlugin({ name: "TextReplace", description: "Replace text in your messages. You can find pre-made rules in the #textreplace-rules channel in Vencord's Server", authors: [Devs.AutumnVN, Devs.TheKodeToad], - dependencies: ["MessageEventsAPI"], settings, + onBeforeMessageSend(channelId, msg) { + // Channel used for sharing rules, applying rules here would be messy + if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return; + msg.content = applyRules(msg.content); + }, + async start() { stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray(); regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray(); - - this.preSend = addPreSendListener((channelId, msg) => { - // Channel used for sharing rules, applying rules here would be messy - if (channelId === TEXT_REPLACE_RULES_CHANNEL_ID) return; - msg.content = applyRules(msg.content); - }); - }, - - stop() { - removePreSendListener(this.preSend); } }); diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx index fa1d9abf6..1b77fb94c 100644 --- a/src/plugins/translate/TranslateIcon.tsx +++ b/src/plugins/translate/TranslateIcon.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { ChatBarButton } from "@api/ChatButtons"; +import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { classes } from "@utils/misc"; import { openModal } from "@utils/modal"; import { Alerts, Forms, Tooltip, useEffect, useState } from "@webpack/common"; @@ -40,7 +40,7 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?: export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void); -export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => { +export const TranslateChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => { const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]); const [shouldShowTranslateEnabledTooltip, setter] = useState(false); diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index de61cef9c..363897d1b 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -18,11 +18,7 @@ import "./styles.css"; -import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { addAccessory, removeAccessory } from "@api/MessageAccessories"; -import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; -import { addButton, removeButton } from "@api/MessagePopover"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { ChannelStore, Menu } from "@webpack/common"; @@ -51,11 +47,12 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => )); }; +let tooltipTimeout: any; + export default definePlugin({ name: "Translate", description: "Translate messages with Google Translate or DeepL", authors: [Devs.Ven, Devs.AshtonMemer], - dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], settings, contextMenus: { "message": messageCtxPatch @@ -63,45 +60,34 @@ export default definePlugin({ // not used, just here in case some other plugin wants it or w/e translate, - start() { - addAccessory("vc-translation", props => ); + renderMessageAccessory: props => , - addChatBarButton("vc-translate", TranslateChatBarIcon); + renderChatBarButton: TranslateChatBarIcon, - addButton("vc-translate", message => { - if (!message.content) return null; + renderMessagePopoverButton(message) { + if (!message.content) return null; - return { - label: "Translate", - icon: TranslateIcon, - message, - channel: ChannelStore.getChannel(message.channel_id), - onClick: async () => { - const trans = await translate("received", message.content); - handleTranslate(message.id, trans); - } - }; - }); - - let tooltipTimeout: any; - this.preSend = addPreSendListener(async (_, message) => { - if (!settings.store.autoTranslate) return; - if (!message.content) return; - - setShouldShowTranslateEnabledTooltip?.(true); - clearTimeout(tooltipTimeout); - tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000); - - const trans = await translate("sent", message.content); - message.content = trans.text; - - }); + return { + label: "Translate", + icon: TranslateIcon, + message, + channel: ChannelStore.getChannel(message.channel_id), + onClick: async () => { + const trans = await translate("received", message.content); + handleTranslate(message.id, trans); + } + }; }, - stop() { - removePreSendListener(this.preSend); - removeChatBarButton("vc-translate"); - removeButton("vc-translate"); - removeAccessory("vc-translation"); - }, + async onBeforeMessageSend(_, message) { + if (!settings.store.autoTranslate) return; + if (!message.content) return; + + setShouldShowTranslateEnabledTooltip?.(true); + clearTimeout(tooltipTimeout); + tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000); + + const trans = await translate("sent", message.content); + message.content = trans.text; + } }); diff --git a/src/plugins/unindent/index.ts b/src/plugins/unindent/index.ts index a197ef4e9..d8853a93f 100644 --- a/src/plugins/unindent/index.ts +++ b/src/plugins/unindent/index.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addPreEditListener, addPreSendListener, MessageObject, removePreEditListener, removePreSendListener } from "@api/MessageEvents"; +import { MessageObject } from "@api/MessageEvents"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -24,7 +24,7 @@ export default definePlugin({ name: "Unindent", description: "Trims leading indentation from codeblocks", authors: [Devs.Ven], - dependencies: ["MessageEventsAPI"], + patches: [ { find: "inQuote:", @@ -55,13 +55,11 @@ export default definePlugin({ }); }, - start() { - this.preSend = addPreSendListener((_, msg) => this.unindentMsg(msg)); - this.preEdit = addPreEditListener((_cid, _mid, msg) => this.unindentMsg(msg)); + onBeforeMessageSend(_, msg) { + return this.unindentMsg(msg); }, - stop() { - removePreSendListener(this.preSend); - removePreEditListener(this.preEdit); + onBeforeMessageEdit(_cid, _mid, msg) { + return this.unindentMsg(msg); } }); diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx index e0d5d8abd..f3063f590 100644 --- a/src/plugins/userVoiceShow/index.tsx +++ b/src/plugins/userVoiceShow/index.tsx @@ -18,8 +18,8 @@ import "./style.css"; -import { addDecorator, removeDecorator } from "@api/MemberListDecorators"; -import { addDecoration, removeDecoration } from "@api/MessageDecorations"; +import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators"; +import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -96,16 +96,16 @@ export default definePlugin({ start() { if (settings.store.showInMemberList) { - addDecorator("UserVoiceShow", ({ user }) => user == null ? null : ); + addMemberListDecorator("UserVoiceShow", ({ user }) => user == null ? null : ); } if (settings.store.showInMessages) { - addDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : ); + addMessageDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : ); } }, stop() { - removeDecorator("UserVoiceShow"); - removeDecoration("UserVoiceShow"); + removeMemberListDecorator("UserVoiceShow"); + removeMessageDecoration("UserVoiceShow"); }, VoiceChannelIndicator diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 8ee1ca8d7..b45919a21 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -17,7 +17,6 @@ */ import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import { CodeBlock } from "@components/CodeBlock"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -149,8 +148,8 @@ export default definePlugin({ name: "ViewRaw", description: "Copy and view the raw content/data of any message, channel or guild", authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna], - dependencies: ["MessagePopoverAPI"], settings, + contextMenus: { "guild-context": MakeContextCallback("Guild"), "channel-context": MakeContextCallback("Channel"), @@ -159,44 +158,38 @@ export default definePlugin({ "user-context": MakeContextCallback("User") }, - start() { - addButton("ViewRaw", msg => { - const handleClick = () => { - if (settings.store.clickMethod === "Right") { - copyWithToast(msg.content); - } else { - openViewRawModalMessage(msg); - } - }; + renderMessagePopoverButton(msg) { + const handleClick = () => { + if (settings.store.clickMethod === "Right") { + copyWithToast(msg.content); + } else { + openViewRawModalMessage(msg); + } + }; - const handleContextMenu = e => { - if (settings.store.clickMethod === "Left") { - e.preventDefault(); - e.stopPropagation(); - copyWithToast(msg.content); - } else { - e.preventDefault(); - e.stopPropagation(); - openViewRawModalMessage(msg); - } - }; + const handleContextMenu = e => { + if (settings.store.clickMethod === "Left") { + e.preventDefault(); + e.stopPropagation(); + copyWithToast(msg.content); + } else { + e.preventDefault(); + e.stopPropagation(); + openViewRawModalMessage(msg); + } + }; - const label = settings.store.clickMethod === "Right" - ? "Copy Raw (Left Click) / View Raw (Right Click)" - : "View Raw (Left Click) / Copy Raw (Right Click)"; + const label = settings.store.clickMethod === "Right" + ? "Copy Raw (Left Click) / View Raw (Right Click)" + : "View Raw (Left Click) / Copy Raw (Right Click)"; - return { - label, - icon: CopyIcon, - message: msg, - channel: ChannelStore.getChannel(msg.channel_id), - onClick: handleClick, - onContextMenu: handleContextMenu - }; - }); - }, - - stop() { - removeButton("ViewRaw"); + return { + label, + icon: CopyIcon, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: handleClick, + onContextMenu: handleContextMenu + }; } }); diff --git a/src/utils/types.ts b/src/utils/types.ts index 02760d964..b2210ffa5 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -16,8 +16,15 @@ * along with this program. If not, see . */ +import { ProfileBadge } from "@api/Badges"; +import { ChatBarButtonFactory } from "@api/ChatButtons"; import { Command } from "@api/Commands"; import { NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { MemberListDecoratorFactory } from "@api/MemberListDecorators"; +import { MessageAccessoryFactory } from "@api/MessageAccessories"; +import { MessageDecorationFactory } from "@api/MessageDecorations"; +import { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents"; +import { MessagePopoverButtonFactory } from "@api/MessagePopover"; import { FluxEvents } from "@webpack/types"; import { JSX } from "react"; import { Promisable } from "type-fest"; @@ -142,6 +149,20 @@ export interface PluginDef { toolboxActions?: Record void>; tags?: string[]; + + userProfileBadge?: ProfileBadge; + + onMessageClick?: MessageClickListener; + onBeforeMessageSend?: MessageSendListener; + onBeforeMessageEdit?: MessageEditListener; + + renderMessagePopoverButton?: MessagePopoverButtonFactory; + renderMessageAccessory?: MessageAccessoryFactory; + renderMessageDecoration?: MessageDecorationFactory; + + renderMemberListDecorator?: MemberListDecoratorFactory; + + renderChatBarButton?: ChatBarButtonFactory; } export const enum StartAt { From 5c8ba6e542c90903a17fd785864f618dcacc4e01 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 22 Jan 2025 22:51:11 -0300 Subject: [PATCH 02/28] Settings API: add support for custom objects / arrays (#3154) --- eslint.config.mjs | 8 +- src/components/PluginSettings/PluginModal.tsx | 8 +- src/plugins/ignoreActivities/index.tsx | 47 ++--- src/plugins/index.ts | 6 +- src/plugins/messageTags/index.ts | 85 +++++---- .../pinDms/components/CreateCategoryModal.tsx | 73 ++++---- src/plugins/pinDms/components/contextMenu.tsx | 14 +- src/plugins/pinDms/data.ts | 176 +++++++----------- src/plugins/pinDms/index.tsx | 93 ++++----- src/plugins/textReplace/index.tsx | 84 +++++---- src/shared/SettingsStore.ts | 150 +++++++++++---- src/utils/types.ts | 17 +- src/webpack/common/types/components.d.ts | 14 +- 13 files changed, 420 insertions(+), 355 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 07c70fa74..67327b938 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -105,7 +105,13 @@ export default tseslint.config( "no-invalid-regexp": "error", "no-constant-condition": ["error", { "checkLoops": false }], "no-duplicate-imports": "error", - "dot-notation": "error", + "@typescript-eslint/dot-notation": [ + "error", + { + "allowPrivateClassPropertyAccess": true, + "allowProtectedClassPropertyAccess": true + } + ], "no-useless-escape": [ "error", { diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 4dc92e42a..3f7965d58 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -81,7 +81,8 @@ const Components: Record null, }; export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) { @@ -129,7 +130,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti for (const [key, value] of Object.entries(tempSettings)) { const option = plugin.options[key]; pluginSettings[key] = value; - option?.onChange?.(value); + + if (option.type === OptionType.CUSTOM) continue; if (option?.restartNeeded) restartNeeded = true; } if (restartNeeded) onRestartNeeded(); @@ -141,7 +143,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti return There are no settings for this plugin.; } else { const options = Object.entries(plugin.options).map(([key, setting]) => { - if (setting.hidden) return null; + if (setting.type === OptionType.CUSTOM || setting.hidden) return null; function onChange(newValue: any) { setTempSettings(s => ({ ...s, [key]: newValue })); diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index fac83687d..08e146ab9 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import * as DataStore from "@api/DataStore"; import { definePluginSettings, Settings } from "@api/Settings"; import { getUserSettingLazy } from "@api/UserSettings"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -62,7 +61,7 @@ const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(ac function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) { const s = settings.use(["ignoredActivities"]); - const { ignoredActivities = [] } = s; + const { ignoredActivities } = s; if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)"); return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)"); @@ -71,9 +70,9 @@ function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) { function handleActivityToggle(e: React.MouseEvent, activity: IgnoredActivity) { e.stopPropagation(); - const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id); - if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity); - else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex); + const ignoredActivityIndex = settings.store.ignoredActivities.findIndex(act => act.id === activity.id); + if (ignoredActivityIndex === -1) settings.store.ignoredActivities.push(activity); + else settings.store.ignoredActivities.splice(ignoredActivityIndex, 1); recalculateActivities(); } @@ -209,14 +208,13 @@ const settings = definePluginSettings({ description: "Ignore all competing activities (These are normally special game activities)", default: false, onChange: recalculateActivities + }, + ignoredActivities: { + type: OptionType.CUSTOM, + default: [] as IgnoredActivity[], + onChange: recalculateActivities } -}).withPrivateSettings<{ - ignoredActivities: IgnoredActivity[]; -}>(); - -function getIgnoredActivities() { - return settings.store.ignoredActivities ??= []; -} +}); function isActivityTypeIgnored(type: number, id?: string) { if (id && settings.store.idsList.includes(id)) { @@ -284,29 +282,14 @@ export default definePlugin({ ], async start() { - // Migrate allowedIds - if (Settings.plugins.IgnoreActivities.allowedIds) { - settings.store.idsList = Settings.plugins.IgnoreActivities.allowedIds; - delete Settings.plugins.IgnoreActivities.allowedIds; // Remove allowedIds - } - - const oldIgnoredActivitiesData = await DataStore.get>("IgnoreActivities_ignoredActivities"); - - if (oldIgnoredActivitiesData != null) { - settings.store.ignoredActivities = Array.from(oldIgnoredActivitiesData.values()) - .map(activity => ({ ...activity, name: "Unknown Name" })); - - DataStore.del("IgnoreActivities_ignoredActivities"); - } - - if (getIgnoredActivities().length !== 0) { + if (settings.store.ignoredActivities.length !== 0) { const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[]; - for (const [index, ignoredActivity] of getIgnoredActivities().entries()) { + for (const [index, ignoredActivity] of settings.store.ignoredActivities.entries()) { if (ignoredActivity.type !== ActivitiesTypes.Game) continue; if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) { - getIgnoredActivities().splice(index, 1); + settings.store.ignoredActivities.splice(index, 1); } } } @@ -316,11 +299,11 @@ export default definePlugin({ if (isActivityTypeIgnored(props.type, props.application_id)) return false; if (props.application_id != null) { - return !getIgnoredActivities().some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id)); + return !settings.store.ignoredActivities.some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id)); } else { const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; if (exePath) { - return !getIgnoredActivities().some(activity => activity.id === exePath); + return !settings.store.ignoredActivities.some(activity => activity.id === exePath); } } diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 9d672c634..545169b1f 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -25,7 +25,7 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents"; import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover"; -import { Settings } from "@api/Settings"; +import { Settings, SettingsStore } from "@api/Settings"; import { Logger } from "@utils/Logger"; import { canonicalizeFind } from "@utils/patches"; import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types"; @@ -146,6 +146,10 @@ for (const p of pluginsValues) { for (const [name, def] of Object.entries(p.settings.def)) { const checks = p.settings.checks?.[name]; p.options[name] = { ...def, ...checks }; + + if (def.onChange != null) { + SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, def.onChange); + } } } diff --git a/src/plugins/messageTags/index.ts b/src/plugins/messageTags/index.ts index 5ba4ab94a..5a5d03fdb 100644 --- a/src/plugins/messageTags/index.ts +++ b/src/plugins/messageTags/index.ts @@ -18,7 +18,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands"; import * as DataStore from "@api/DataStore"; -import { Settings } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -29,23 +29,23 @@ const MessageTagsMarker = Symbol("MessageTags"); interface Tag { name: string; message: string; - enabled: boolean; } -const getTags = () => DataStore.get(DATA_KEY).then(t => t ?? []); -const getTag = (name: string) => DataStore.get(DATA_KEY).then((t: Tag[]) => (t ?? []).find((tt: Tag) => tt.name === name) ?? null); -const addTag = async (tag: Tag) => { - const tags = await getTags(); - tags.push(tag); - DataStore.set(DATA_KEY, tags); - return tags; -}; -const removeTag = async (name: string) => { - let tags = await getTags(); - tags = await tags.filter((t: Tag) => t.name !== name); - DataStore.set(DATA_KEY, tags); - return tags; -}; +function getTags() { + return settings.store.tagsList; +} + +function getTag(name: string) { + return settings.store.tagsList[name] ?? null; +} + +function addTag(tag: Tag) { + settings.store.tagsList[tag.name] = tag; +} + +function removeTag(name: string) { + delete settings.store.tagsList[name]; +} function createTagCommand(tag: Tag) { registerCommand({ @@ -53,14 +53,14 @@ function createTagCommand(tag: Tag) { description: tag.name, inputType: ApplicationCommandInputType.BUILT_IN_TEXT, execute: async (_, ctx) => { - if (!await getTag(tag.name)) { + if (!getTag(tag.name)) { sendBotMessage(ctx.channel.id, { content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)` }); return { content: `/${tag.name}` }; } - if (Settings.plugins.MessageTags.clyde) sendBotMessage(ctx.channel.id, { + if (settings.store.clyde) sendBotMessage(ctx.channel.id, { content: `${EMOTE} The tag **${tag.name}** has been sent!` }); return { content: tag.message.replaceAll("\\n", "\n") }; @@ -69,22 +69,38 @@ function createTagCommand(tag: Tag) { }, "CustomTags"); } +const settings = definePluginSettings({ + clyde: { + name: "Clyde message on send", + description: "If enabled, clyde will send you an ephemeral message when a tag was used.", + type: OptionType.BOOLEAN, + default: true + }, + tagsList: { + type: OptionType.CUSTOM, + default: {} as Record, + } +}); export default definePlugin({ name: "MessageTags", description: "Allows you to save messages and to use them with a simple command.", authors: [Devs.Luna], - options: { - clyde: { - name: "Clyde message on send", - description: "If enabled, clyde will send you an ephemeral message when a tag was used.", - type: OptionType.BOOLEAN, - default: true - } - }, + settings, async start() { - for (const tag of await getTags()) createTagCommand(tag); + // TODO: Remove DataStore tags migration once enough time has passed + const oldTags = await DataStore.get(DATA_KEY); + if (oldTags != null) { + // @ts-ignore + settings.store.tagsList = Object.fromEntries(oldTags.map(oldTag => (delete oldTag.enabled, [oldTag.name, oldTag]))); + await DataStore.del(DATA_KEY); + } + + const tags = getTags(); + for (const tagName in tags) { + createTagCommand(tags[tagName]); + } }, commands: [ @@ -153,19 +169,18 @@ export default definePlugin({ const name: string = findOption(args[0].options, "tag-name", ""); const message: string = findOption(args[0].options, "message", ""); - if (await getTag(name)) + if (getTag(name)) return sendBotMessage(ctx.channel.id, { content: `${EMOTE} A Tag with the name **${name}** already exists!` }); const tag = { name: name, - enabled: true, message: message }; createTagCommand(tag); - await addTag(tag); + addTag(tag); sendBotMessage(ctx.channel.id, { content: `${EMOTE} Successfully created the tag **${name}**!` @@ -175,13 +190,13 @@ export default definePlugin({ case "delete": { const name: string = findOption(args[0].options, "tag-name", ""); - if (!await getTag(name)) + if (!getTag(name)) return sendBotMessage(ctx.channel.id, { content: `${EMOTE} A Tag with the name **${name}** does not exist!` }); unregisterCommand(name); - await removeTag(name); + removeTag(name); sendBotMessage(ctx.channel.id, { content: `${EMOTE} Successfully deleted the tag **${name}**!` @@ -192,10 +207,8 @@ export default definePlugin({ sendBotMessage(ctx.channel.id, { embeds: [ { - // @ts-ignore title: "All Tags:", - // @ts-ignore - description: (await getTags()) + description: Object.values(getTags()) .map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`) .join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`, // @ts-ignore @@ -208,7 +221,7 @@ export default definePlugin({ } case "preview": { const name: string = findOption(args[0].options, "tag-name", ""); - const tag = await getTag(name); + const tag = getTag(name); if (!tag) return sendBotMessage(ctx.channel.id, { diff --git a/src/plugins/pinDms/components/CreateCategoryModal.tsx b/src/plugins/pinDms/components/CreateCategoryModal.tsx index 0568c1adb..17f7dfdd3 100644 --- a/src/plugins/pinDms/components/CreateCategoryModal.tsx +++ b/src/plugins/pinDms/components/CreateCategoryModal.tsx @@ -7,11 +7,10 @@ import { classNameFactory } from "@api/Styles"; import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal"; import { extractAndLoadChunksLazy, findComponentByCodeLazy, findExportedComponentLazy } from "@webpack"; -import { Button, Forms, Text, TextInput, Toasts, useEffect, useState } from "@webpack/common"; +import { Button, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common"; import { DEFAULT_COLOR, SWATCHES } from "../constants"; -import { categories, Category, createCategory, getCategory, updateCategory } from "../data"; -import { forceUpdate } from "../index"; +import { categoryLen, createCategory, getCategory } from "../data"; interface ColorPickerProps { color: number | null; @@ -39,45 +38,45 @@ const cl = classNameFactory("vc-pindms-modal-"); interface Props { categoryId: string | null; - initalChannelId: string | null; + initialChannelId: string | null; modalProps: ModalProps; } function useCategory(categoryId: string | null, initalChannelId: string | null) { - const [category, setCategory] = useState(null); - - useEffect(() => { - if (categoryId) - setCategory(getCategory(categoryId)!); - else if (initalChannelId) - setCategory({ + const category = useMemo(() => { + if (categoryId) { + return getCategory(categoryId); + } else if (initalChannelId) { + return { id: Toasts.genId(), - name: `Pin Category ${categories.length + 1}`, + name: `Pin Category ${categoryLen() + 1}`, color: DEFAULT_COLOR, collapsed: false, channels: [initalChannelId] - }); + }; + } }, [categoryId, initalChannelId]); - return { - category, - setCategory - }; + return category; } -export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) { - const { category, setCategory } = useCategory(categoryId, initalChannelId); - +export function NewCategoryModal({ categoryId, modalProps, initialChannelId }: Props) { + const category = useCategory(categoryId, initialChannelId); if (!category) return null; - const onSave = async (e: React.FormEvent | React.MouseEvent) => { - e.preventDefault(); - if (!categoryId) - await createCategory(category); - else - await updateCategory(category); + const [name, setName] = useState(category.name); + const [color, setColor] = useState(category.color); + + const onSave = (e: React.FormEvent | React.MouseEvent) => { + e.preventDefault(); + + category.name = name; + category.color = color; + + if (!categoryId) { + createCategory(category); + } - forceUpdate(); modalProps.onClose(); }; @@ -93,25 +92,25 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr Name setCategory({ ...category, name: e })} + value={name} + onChange={e => setName(e)} /> Color setCategory({ ...category, color: c! })} - value={category.color} + onChange={c => setColor(c!)} + value={color} renderDefaultButton={() => null} renderCustomButton={() => ( setCategory({ ...category, color: c! })} - key={category.name} + color={color} + onChange={c => setColor(c!)} + key={category.id} showEyeDropper={false} /> )} @@ -119,7 +118,7 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr - + @@ -129,6 +128,6 @@ export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Pr export const openCategoryModal = (categoryId: string | null, channelId: string | null) => openModalLazy(async () => { await requireSettingsMenu(); - return modalProps => ; + return modalProps => ; }); diff --git a/src/plugins/pinDms/components/contextMenu.tsx b/src/plugins/pinDms/components/contextMenu.tsx index aa5d1993e..6fc4e6743 100644 --- a/src/plugins/pinDms/components/contextMenu.tsx +++ b/src/plugins/pinDms/components/contextMenu.tsx @@ -7,8 +7,8 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Menu } from "@webpack/common"; -import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data"; -import { forceUpdate, PinOrder, settings } from "../index"; +import { addChannelToCategory, canMoveChannelInDirection, currentUserCategories, isPinned, moveChannel, removeChannelFromCategory } from "../data"; +import { PinOrder, settings } from "../index"; import { openCategoryModal } from "./CreateCategoryModal"; function createPinMenuItem(channelId: string) { @@ -31,12 +31,12 @@ function createPinMenuItem(channelId: string) { { - categories.map(category => ( + currentUserCategories.map(category => ( addChannelToCategory(channelId, category.id).then(forceUpdate)} + action={() => addChannelToCategory(channelId, category.id)} /> )) } @@ -49,7 +49,7 @@ function createPinMenuItem(channelId: string) { id="unpin-dm" label="Unpin DM" color="danger" - action={() => removeChannelFromCategory(channelId).then(forceUpdate)} + action={() => removeChannelFromCategory(channelId)} /> { @@ -57,7 +57,7 @@ function createPinMenuItem(channelId: string) { moveChannel(channelId, -1).then(forceUpdate)} + action={() => moveChannel(channelId, -1)} /> ) } @@ -67,7 +67,7 @@ function createPinMenuItem(channelId: string) { moveChannel(channelId, 1).then(forceUpdate)} + action={() => moveChannel(channelId, 1)} /> ) } diff --git a/src/plugins/pinDms/data.ts b/src/plugins/pinDms/data.ts index a4e40dde0..2f4a1156e 100644 --- a/src/plugins/pinDms/data.ts +++ b/src/plugins/pinDms/data.ts @@ -6,10 +6,10 @@ import * as DataStore from "@api/DataStore"; import { Settings } from "@api/Settings"; +import { useForceUpdater } from "@utils/react"; import { UserStore } from "@webpack/common"; -import { DEFAULT_COLOR } from "./constants"; -import { forceUpdate, PinOrder, PrivateChannelSortStore, settings } from "./index"; +import { PinOrder, PrivateChannelSortStore, settings } from "./index"; export interface Category { id: string; @@ -24,104 +24,92 @@ const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs"; const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories"; const OLD_CATEGORY_KEY = "BetterPinDMsCategories-"; - -export let categories: Category[] = []; - -export async function saveCats(cats: Category[]) { - const { id } = UserStore.getCurrentUser(); - await DataStore.set(CATEGORY_BASE_KEY + id, cats); -} +let forceUpdateDms: (() => void) | undefined = undefined; +export let currentUserCategories: Category[] = []; export async function init() { - const id = UserStore.getCurrentUser()?.id; - await initCategories(id); - await migrateData(id); - forceUpdate(); + await migrateData(); + + const userId = UserStore.getCurrentUser()?.id; + if (userId == null) return; + + currentUserCategories = settings.store.userBasedCategoryList[userId] ??= []; + forceUpdateDms?.(); } -export async function initCategories(userId: string) { - categories = await DataStore.get(CATEGORY_BASE_KEY + userId) ?? []; +export function usePinnedDms() { + forceUpdateDms = useForceUpdater(); + settings.use(["pinOrder", "canCollapseDmSection", "dmSectionCollapsed", "userBasedCategoryList"]); } export function getCategory(id: string) { - return categories.find(c => c.id === id); + return currentUserCategories.find(c => c.id === id); } -export async function createCategory(category: Category) { - categories.push(category); - await saveCats(categories); +export function getCategoryByIndex(index: number) { + return currentUserCategories[index]; } -export async function updateCategory(category: Category) { - const index = categories.findIndex(c => c.id === category.id); - if (index === -1) return; - - categories[index] = category; - await saveCats(categories); +export function createCategory(category: Category) { + currentUserCategories.push(category); } -export async function addChannelToCategory(channelId: string, categoryId: string) { - const category = categories.find(c => c.id === categoryId); - if (!category) return; +export function addChannelToCategory(channelId: string, categoryId: string) { + const category = currentUserCategories.find(c => c.id === categoryId); + if (category == null) return; if (category.channels.includes(channelId)) return; category.channels.push(channelId); - await saveCats(categories); - } -export async function removeChannelFromCategory(channelId: string) { - const category = categories.find(c => c.channels.includes(channelId)); - if (!category) return; +export function removeChannelFromCategory(channelId: string) { + const category = currentUserCategories.find(c => c.channels.includes(channelId)); + if (category == null) return; category.channels = category.channels.filter(c => c !== channelId); - await saveCats(categories); } -export async function removeCategory(categoryId: string) { - const catagory = categories.find(c => c.id === categoryId); - if (!catagory) return; +export function removeCategory(categoryId: string) { + const categoryIndex = currentUserCategories.findIndex(c => c.id === categoryId); + if (categoryIndex === -1) return; - // catagory?.channels.forEach(c => removeChannelFromCategory(c)); - categories = categories.filter(c => c.id !== categoryId); - await saveCats(categories); + currentUserCategories.splice(categoryIndex, 1); } -export async function collapseCategory(id: string, value = true) { - const category = categories.find(c => c.id === id); - if (!category) return; +export function collapseCategory(id: string, value = true) { + const category = currentUserCategories.find(c => c.id === id); + if (category == null) return; category.collapsed = value; - await saveCats(categories); } -// utils +// Utils export function isPinned(id: string) { - return categories.some(c => c.channels.includes(id)); + return currentUserCategories.some(c => c.channels.includes(id)); } export function categoryLen() { - return categories.length; + return currentUserCategories.length; } export function getAllUncollapsedChannels() { if (settings.store.pinOrder === PinOrder.LastMessage) { const sortedChannels = PrivateChannelSortStore.getPrivateChannelIds(); - return categories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel))); + return currentUserCategories.filter(c => !c.collapsed).flatMap(c => sortedChannels.filter(channel => c.channels.includes(channel))); } - return categories.filter(c => !c.collapsed).flatMap(c => c.channels); + return currentUserCategories.filter(c => !c.collapsed).flatMap(c => c.channels); } export function getSections() { - return categories.reduce((acc, category) => { + return currentUserCategories.reduce((acc, category) => { acc.push(category.channels.length === 0 ? 1 : category.channels.length); return acc; }, [] as number[]); } -// move categories +// Move categories export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => { const a = array[index]; const b = array[index + direction]; @@ -130,18 +118,18 @@ export const canMoveArrayInDirection = (array: any[], index: number, direction: }; export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => { - const index = categories.findIndex(m => m.id === id); - return canMoveArrayInDirection(categories, index, direction); + const categoryIndex = currentUserCategories.findIndex(m => m.id === id); + return canMoveArrayInDirection(currentUserCategories, categoryIndex, direction); }; export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1); export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => { - const category = categories.find(c => c.channels.includes(channelId)); - if (!category) return false; + const category = currentUserCategories.find(c => c.channels.includes(channelId)); + if (category == null) return false; - const index = category.channels.indexOf(channelId); - return canMoveArrayInDirection(category.channels, index, direction); + const channelIndex = category.channels.indexOf(channelId); + return canMoveArrayInDirection(category.channels, channelIndex, direction); }; @@ -150,70 +138,44 @@ function swapElementsInArray(array: any[], index1: number, index2: number) { [array[index1], array[index2]] = [array[index2], array[index1]]; } -// stolen from PinDMs -export async function moveCategory(id: string, direction: -1 | 1) { - const a = categories.findIndex(m => m.id === id); +export function moveCategory(id: string, direction: -1 | 1) { + const a = currentUserCategories.findIndex(m => m.id === id); const b = a + direction; - swapElementsInArray(categories, a, b); - - await saveCats(categories); + swapElementsInArray(currentUserCategories, a, b); } -export async function moveChannel(channelId: string, direction: -1 | 1) { - const category = categories.find(c => c.channels.includes(channelId)); - if (!category) return; +export function moveChannel(channelId: string, direction: -1 | 1) { + const category = currentUserCategories.find(c => c.channels.includes(channelId)); + if (category == null) return; const a = category.channels.indexOf(channelId); const b = a + direction; swapElementsInArray(category.channels, a, b); - - await saveCats(categories); } - - -// migrate data -const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined; - -async function migratePinDMs() { - if (categories.some(m => m.id === "oldPins")) { - return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true); +// TODO: Remove DataStore PinnedDms migration once enough time has passed +async function migrateData() { + if (Settings.plugins.PinDMs.dmSectioncollapsed != null) { + settings.store.dmSectionCollapsed = Settings.plugins.PinDMs.dmSectioncollapsed; + delete Settings.plugins.PinDMs.dmSectioncollapsed; } - const pindmspins = getPinDMsPins(); + const dataStoreKeys = await DataStore.keys(); + const pinDmsKeys = dataStoreKeys.map(key => String(key)).filter(key => key.startsWith(CATEGORY_BASE_KEY)); - // we dont want duplicate pins - const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m))); - if (difference?.length) { - categories.push({ - id: "oldPins", - name: "Pins", - color: DEFAULT_COLOR, - channels: difference - }); + if (pinDmsKeys.length === 0) return; + + for (const pinDmsKey of pinDmsKeys) { + const categories = await DataStore.get(pinDmsKey); + if (categories == null) continue; + + const userId = pinDmsKey.replace(CATEGORY_BASE_KEY, ""); + settings.store.userBasedCategoryList[userId] = categories; + + await DataStore.del(pinDmsKey); } - await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true); -} - -async function migrateOldCategories(userId: string) { - const oldCats = await DataStore.get(OLD_CATEGORY_KEY + userId); - // dont want to migrate if the user has already has categories. - if (categories.length === 0 && oldCats?.length) { - categories.push(...(oldCats.filter(m => m.id !== "oldPins"))); - } - await DataStore.set(CATEGORY_MIGRATED_KEY, true); -} - -export async function migrateData(userId: string) { - const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY); - if (m1 && m2) return; - - // want to migrate the old categories first and then slove any conflicts with the PinDMs pins - if (!m1) await migrateOldCategories(userId); - if (!m2) await migratePinDMs(); - - await saveCats(categories); + await Promise.all([DataStore.del(CATEGORY_MIGRATED_PINDMS_KEY), DataStore.del(CATEGORY_MIGRATED_KEY), DataStore.del(OLD_CATEGORY_KEY)]); } diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 8cbb03bfc..59fee9c0f 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -12,13 +12,13 @@ import { Devs } from "@utils/constants"; import { classes } from "@utils/misc"; import definePlugin, { OptionType, StartAt } from "@utils/types"; import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common"; +import { Clickable, ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common"; import { Channel } from "discord-types/general"; import { contextMenus } from "./components/contextMenu"; import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal"; import { DEFAULT_CHUNK_SIZE } from "./constants"; -import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data"; +import { canMoveCategory, canMoveCategoryInDirection, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getCategoryByIndex, getSections, init, isPinned, moveCategory, removeCategory, usePinnedDms } from "./data"; interface ChannelComponentProps { children: React.ReactNode, @@ -26,13 +26,11 @@ interface ChannelComponentProps { selected: boolean; } - const headerClasses = findByPropsLazy("privateChannelsHeaderContainer"); export const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; }; export let instance: any; -export const forceUpdate = () => instance?.props?._forceUpdate?.(); export const enum PinOrder { LastMessage, @@ -46,21 +44,28 @@ export const settings = definePluginSettings({ options: [ { label: "Most recent message", value: PinOrder.LastMessage, default: true }, { label: "Custom (right click channels to reorder)", value: PinOrder.Custom } - ], - onChange: () => forceUpdate() + ] }, - - dmSectioncollapsed: { + canCollapseDmSection: { type: OptionType.BOOLEAN, - description: "Collapse DM sections", + description: "Allow uncategorised DMs section to be collapsable", + default: false + }, + dmSectionCollapsed: { + type: OptionType.BOOLEAN, + description: "Collapse DM section", default: false, - onChange: () => forceUpdate() + hidden: true + }, + userBasedCategoryList: { + type: OptionType.CUSTOM, + default: {} as Record } }); export default definePlugin({ name: "PinDMs", - description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs", + description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or re-order pins, right click DMs", authors: [Devs.Ven, Devs.Aria], settings, contextMenus, @@ -124,8 +129,8 @@ export default definePlugin({ { find: ".FRIENDS},\"friends\"", replacement: { - match: /let{showLibrary:\i,.+?showDMHeader:.+?,/, - replace: "let forceUpdate = Vencord.Util.useForceUpdater();$&_forceUpdate:forceUpdate," + match: /let{showLibrary:\i,/, + replace: "$self.usePinnedDms();$&" } }, @@ -149,6 +154,7 @@ export default definePlugin({ } }, ], + sections: null as number[] | null, set _instance(i: any) { @@ -162,6 +168,7 @@ export default definePlugin({ CONNECTION_OPEN: init, }, + usePinnedDms, isPinned, categoryLen, getSections, @@ -186,11 +193,11 @@ export default definePlugin({ }, makeSpanProps() { - return { + return settings.store.canCollapseDmSection ? { onClick: () => this.collapseDMList(), role: "button", style: { cursor: "pointer" } - }; + } : undefined; }, getChunkSize() { @@ -210,30 +217,27 @@ export default definePlugin({ }, isChannelIndex(sectionIndex: number, channelIndex: number) { - if (settings.store.dmSectioncollapsed && sectionIndex !== 0) + if (settings.store.canCollapseDmSection && settings.store.dmSectionCollapsed && sectionIndex !== 0) { return true; - const cat = categories[sectionIndex - 1]; - return this.isCategoryIndex(sectionIndex) && (cat?.channels?.length === 0 || cat?.channels[channelIndex]); - }, + } - isDMSectioncollapsed() { - return settings.store.dmSectioncollapsed; + const category = getCategoryByIndex(sectionIndex - 1); + return this.isCategoryIndex(sectionIndex) && (category?.channels?.length === 0 || category?.channels[channelIndex]); }, collapseDMList() { - settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed; - forceUpdate(); + settings.store.dmSectionCollapsed = !settings.store.dmSectionCollapsed; }, isChannelHidden(categoryIndex: number, channelIndex: number) { if (categoryIndex === 0) return false; - if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex) + if (settings.store.canCollapseDmSection && settings.store.dmSectionCollapsed && this.getSections().length + 1 === categoryIndex) return true; if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false; - const category = categories[categoryIndex - 1]; + const category = getCategoryByIndex(categoryIndex - 1); if (!category) return false; return category.collapsed && this.instance.props.selectedChannelId !== this.getCategoryChannels(category)[channelIndex]; @@ -251,18 +255,12 @@ export default definePlugin({ }, renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => { - const category = categories[section - 1]; - + const category = getCategoryByIndex(section - 1); if (!category) return null; return ( -

{ - await collapseCategory(category.id, !category.collapsed); - forceUpdate(); - }} + collapseCategory(category.id, !category.collapsed)} onContextMenu={e => { ContextMenuApi.openContextMenu(e, () => ( moveCategory(category.id, -1).then(() => forceUpdate())} + action={() => moveCategory(category.id, -1)} /> } { canMoveCategoryInDirection(category.id, 1) && moveCategory(category.id, 1).then(() => forceUpdate())} + action={() => moveCategory(category.id, 1)} /> } @@ -304,7 +302,7 @@ export default definePlugin({ id="vc-pindms-delete-category" color="danger" label="Delete Category" - action={() => removeCategory(category.id).then(() => forceUpdate())} + action={() => removeCategory(category.id)} /> @@ -312,13 +310,18 @@ export default definePlugin({ )); }} > - - {category?.name ?? "uh oh"} - - -

+

+ + {category?.name ?? "uh oh"} + + +

+ ); }, { noop: true }), @@ -341,7 +344,7 @@ export default definePlugin({ }, getChannel(sectionIndex: number, index: number, channels: Record) { - const category = categories[sectionIndex - 1]; + const category = getCategoryByIndex(sectionIndex - 1); if (!category) return { channel: null, category: null }; const channelId = this.getCategoryChannels(category)[index]; diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index bf5d62836..3d1e891d1 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -22,7 +22,6 @@ import { Flex } from "@components/Flex"; import { DeleteIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; -import { useForceUpdater } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; import { Button, Forms, React, TextInput, useState } from "@webpack/common"; @@ -34,8 +33,6 @@ type Rule = Record<"find" | "replace" | "onlyIfIncludes", string>; interface TextReplaceProps { title: string; rulesArray: Rule[]; - rulesKey: string; - update: () => void; } const makeEmptyRule: () => Rule = () => ({ @@ -45,34 +42,36 @@ const makeEmptyRule: () => Rule = () => ({ }); const makeEmptyRuleArray = () => [makeEmptyRule()]; -let stringRules = makeEmptyRuleArray(); -let regexRules = makeEmptyRuleArray(); - const settings = definePluginSettings({ replace: { type: OptionType.COMPONENT, description: "", component: () => { - const update = useForceUpdater(); + const { stringRules, regexRules } = settings.use(["stringRules", "regexRules"]); + return ( <> ); } }, + stringRules: { + type: OptionType.CUSTOM, + default: makeEmptyRuleArray(), + }, + regexRules: { + type: OptionType.CUSTOM, + default: makeEmptyRuleArray(), + } }); function stringToRegex(str: string) { @@ -119,28 +118,24 @@ function Input({ initialValue, onChange, placeholder }: { ); } -function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) { +function TextReplace({ title, rulesArray }: TextReplaceProps) { const isRegexRules = title === "Using Regex"; async function onClickRemove(index: number) { if (index === rulesArray.length - 1) return; rulesArray.splice(index, 1); - - await DataStore.set(rulesKey, rulesArray); - update(); } async function onChange(e: string, index: number, key: string) { - if (index === rulesArray.length - 1) + if (index === rulesArray.length - 1) { rulesArray.push(makeEmptyRule()); + } rulesArray[index][key] = e; - if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) + if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) { rulesArray.splice(index, 1); - - await DataStore.set(rulesKey, rulesArray); - update(); + } } return ( @@ -207,29 +202,26 @@ function TextReplaceTesting() { } function applyRules(content: string): string { - if (content.length === 0) + if (content.length === 0) { return content; - - if (stringRules) { - for (const rule of stringRules) { - if (!rule.find) continue; - if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; - - content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, ""); - } } - if (regexRules) { - for (const rule of regexRules) { - if (!rule.find) continue; - if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; + for (const rule of settings.store.stringRules) { + if (!rule.find) continue; + if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; - try { - const regex = stringToRegex(rule.find); - content = content.replace(regex, rule.replace.replaceAll("\\n", "\n")); - } catch (e) { - new Logger("TextReplace").error(`Invalid regex: ${rule.find}`); - } + content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, ""); + } + + for (const rule of settings.store.regexRules) { + if (!rule.find) continue; + if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; + + try { + const regex = stringToRegex(rule.find); + content = content.replace(regex, rule.replace.replaceAll("\\n", "\n")); + } catch (e) { + new Logger("TextReplace").error(`Invalid regex: ${rule.find}`); } } @@ -253,7 +245,17 @@ export default definePlugin({ }, async start() { - stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray(); - regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray(); + // TODO: Remove DataStore rules migrations once enough time has passed + const oldStringRules = await DataStore.get(STRING_RULES_KEY); + if (oldStringRules != null) { + settings.store.stringRules = oldStringRules; + await DataStore.del(STRING_RULES_KEY); + } + + const oldRegexRules = await DataStore.get(REGEX_RULES_KEY); + if (oldRegexRules != null) { + settings.store.regexRules = oldRegexRules; + await DataStore.del(REGEX_RULES_KEY); + } } }); diff --git a/src/shared/SettingsStore.ts b/src/shared/SettingsStore.ts index 4109704bc..25dd05b19 100644 --- a/src/shared/SettingsStore.ts +++ b/src/shared/SettingsStore.ts @@ -6,6 +6,9 @@ import { LiteralUnion } from "type-fest"; +export const SYM_IS_PROXY = Symbol("SettingsStore.isProxy"); +export const SYM_GET_RAW_TARGET = Symbol("SettingsStore.getRawTarget"); + // Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop type ResolvePropDeep = P extends `${infer Pre}.${infer Suf}` ? Pre extends keyof T @@ -28,6 +31,11 @@ interface SettingsStoreOptions { // merges the SettingsStoreOptions type into the class export interface SettingsStore extends SettingsStoreOptions { } +interface ProxyContext { + root: T; + path: string; +} + /** * The SettingsStore allows you to easily create a mutable store that * has support for global and path-based change listeners. @@ -35,6 +43,90 @@ export interface SettingsStore extends SettingsStoreOptions { export class SettingsStore { private pathListeners = new Map void>>(); private globalListeners = new Set<(newData: T, path: string) => void>(); + private readonly proxyContexts = new WeakMap>(); + + private readonly proxyHandler: ProxyHandler = (() => { + const self = this; + + return { + get(target, key: any, receiver) { + if (key === SYM_IS_PROXY) { + return true; + } + + if (key === SYM_GET_RAW_TARGET) { + return target; + } + + let v = Reflect.get(target, key, receiver); + + const proxyContext = self.proxyContexts.get(target); + if (proxyContext == null) { + return v; + } + + const { root, path } = proxyContext; + + if (!(key in target) && self.getDefaultValue != null) { + v = self.getDefaultValue({ + target, + key, + root, + path + }); + } + + if (typeof v === "object" && v !== null && !v[SYM_IS_PROXY]) { + const getPath = `${path}${path && "."}${key}`; + return self.makeProxy(v, root, getPath); + } + + return v; + }, + set(target, key: string, value) { + if (value?.[SYM_IS_PROXY]) { + value = value[SYM_GET_RAW_TARGET]; + } + + if (target[key] === value) { + return true; + } + + if (!Reflect.set(target, key, value)) { + return false; + } + + const proxyContext = self.proxyContexts.get(target); + if (proxyContext == null) { + return true; + } + + const { root, path } = proxyContext; + + const setPath = `${path}${path && "."}${key}`; + self.notifyListeners(setPath, value, root); + + return true; + }, + deleteProperty(target, key: string) { + if (!Reflect.deleteProperty(target, key)) { + return false; + } + + const proxyContext = self.proxyContexts.get(target); + if (proxyContext == null) { + return true; + } + + const { root, path } = proxyContext; + + const deletePath = `${path}${path && "."}${key}`; + self.notifyListeners(deletePath, undefined, root); + + return true; + } + }; + })(); /** * The store object. Making changes to this object will trigger the applicable change listeners @@ -51,39 +143,33 @@ export class SettingsStore { Object.assign(this, options); } - private makeProxy(object: any, root: T = object, path: string = "") { - const self = this; - - return new Proxy(object, { - get(target, key: string) { - let v = target[key]; - - if (!(key in target) && self.getDefaultValue) { - v = self.getDefaultValue({ - target, - key, - root, - path - }); - } - - if (typeof v === "object" && v !== null && !Array.isArray(v)) - return self.makeProxy(v, root, `${path}${path && "."}${key}`); - - return v; - }, - set(target, key: string, value) { - if (target[key] === value) return true; - - Reflect.set(target, key, value); - const setPath = `${path}${path && "."}${key}`; - - self.globalListeners.forEach(cb => cb(value, setPath)); - self.pathListeners.get(setPath)?.forEach(cb => cb(value)); - - return true; - } + private makeProxy(object: any, root: T = object, path = "") { + this.proxyContexts.set(object, { + root, + path }); + + return new Proxy(object, this.proxyHandler); + } + + private notifyListeners(pathStr: string, value: any, root: T) { + const paths = pathStr.split("."); + + // Because we support any type of settings with OptionType.CUSTOM, and those objects get proxied recursively, + // the path ends up including all the nested paths (plugins.pluginName.settingName.example.one). + // So, we need to extract the top-level setting path (plugins.pluginName.settingName), + // to be able to notify globalListeners and top-level setting name listeners (let { settingName } = settings.use(["settingName"]), + // with the new value + if (paths.length > 2 && paths[0] === "plugins") { + const settingPath = paths.slice(0, 3); + const settingPathStr = settingPath.join("."); + const settingValue = settingPath.reduce((acc, curr) => acc[curr], root); + + this.globalListeners.forEach(cb => cb(root, settingPathStr)); + this.pathListeners.get(settingPathStr)?.forEach(cb => cb(settingValue)); + } + + this.pathListeners.get(pathStr)?.forEach(cb => cb(value)); } /** diff --git a/src/utils/types.ts b/src/utils/types.ts index b2210ffa5..54de59e34 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -189,6 +189,7 @@ export const enum OptionType { SELECT, SLIDER, COMPONENT, + CUSTOM } export type SettingsDefinition = Record; @@ -197,7 +198,7 @@ export type SettingsChecks = { (IsDisabled> & IsValid, DefinedSettings>); }; -export type PluginSettingDef = ( +export type PluginSettingDef = (PluginSettingCustomDef & Pick) | (( | PluginSettingStringDef | PluginSettingNumberDef | PluginSettingBooleanDef @@ -205,7 +206,7 @@ export type PluginSettingDef = ( | PluginSettingSliderDef | PluginSettingComponentDef | PluginSettingBigIntDef -) & PluginSettingCommon; +) & PluginSettingCommon); export interface PluginSettingCommon { description: string; @@ -259,12 +260,18 @@ export interface PluginSettingSelectDef { type: OptionType.SELECT; options: readonly PluginSettingSelectOption[]; } + export interface PluginSettingSelectOption { label: string; value: string | number | boolean; default?: boolean; } +export interface PluginSettingCustomDef { + type: OptionType.CUSTOM; + default?: any; +} + export interface PluginSettingSliderDef { type: OptionType.SLIDER; /** @@ -314,7 +321,9 @@ type PluginSettingType = O extends PluginSettingStri O extends PluginSettingSelectDef ? O["options"][number]["value"] : O extends PluginSettingSliderDef ? number : O extends PluginSettingComponentDef ? any : + O extends PluginSettingCustomDef ? O extends { default: infer Default; } ? Default : any : never; + type PluginSettingDefaultType = O extends PluginSettingSelectDef ? ( O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined ) : O extends { default: infer T; } ? T : undefined; @@ -366,13 +375,15 @@ export type PluginOptionsItem = | PluginOptionBoolean | PluginOptionSelect | PluginOptionSlider - | PluginOptionComponent; + | PluginOptionComponent + | PluginOptionCustom; export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon; +export type PluginOptionCustom = PluginSettingCustomDef & Pick; export type PluginNative any>> = { [key in keyof PluginExports]: diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index 260a763a7..f6a6c4ad7 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import type { ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; +import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; import { IconNames } from "./iconNames"; @@ -471,15 +471,9 @@ export type ScrollerThin = ComponentType>; -export type Clickable = ComponentType>; +export type Clickable = (props: PropsWithChildren> & { + tag?: T; +}) => ReactNode; export type Avatar = ComponentType Date: Wed, 22 Jan 2025 23:10:43 -0300 Subject: [PATCH 03/28] Optimize slow patches --- src/plugins/callTimer/index.tsx | 4 ++-- src/plugins/showHiddenChannels/index.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plugins/callTimer/index.tsx b/src/plugins/callTimer/index.tsx index e16abc4a1..bdcca7772 100644 --- a/src/plugins/callTimer/index.tsx +++ b/src/plugins/callTimer/index.tsx @@ -75,8 +75,8 @@ export default definePlugin({ patches: [{ find: "renderConnectionStatus(){", replacement: { - match: /(?<=renderConnectionStatus\(\){.+\.channel,children:).+?}\):\i(?=}\))/, - replace: "[$&, $self.renderTimer(this.props.channel.id)]" + match: /(renderConnectionStatus\(\){.+\.channel,children:)(.+?}\):\i)(?=}\))/, + replace: "$1[$2,$self.renderTimer(this.props.channel.id)]" } }], diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 181a6bc99..92cd3b50f 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -88,7 +88,7 @@ export default definePlugin({ }, // Make channels we dont have access to be the same level as normal ones { - match: /(activeJoinedRelevantThreads:.{0,50}VIEW_CHANNEL.+?renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g, + match: /(this\.record\)\?{renderLevel:(.+?),threadIds.+?renderLevel:).+?(?=,threadIds)/g, replace: (_, rest, defaultRenderLevel) => `${rest}${defaultRenderLevel}` }, // Remove permission checking for getRenderLevel function @@ -224,12 +224,12 @@ export default definePlugin({ find: "Missing channel in Channel.renderHeaderToolbar", replacement: [ { - match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:)(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, - replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` + match: /"renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_TEXT:(?=.+?(\i\.push.{0,50}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, + replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` }, { - match: /(?<="renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:)(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, - replace: (_, pushNotificationButtonExpression, channel, isLurking) => `if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` + match: /"renderHeaderToolbar",\(\)=>{.+?case \i\.\i\.GUILD_MEDIA:(?=.+?(\i\.push.{0,40}channel:(\i)},"notifications"\)\)))(?<=isLurking:(\i).+?)/, + replace: (m, pushNotificationButtonExpression, channel, isLurking) => `${m}if(!${isLurking}&&$self.isHiddenChannel(${channel})){${pushNotificationButtonExpression};break;}` }, { match: /"renderMobileToolbar",\(\)=>{.+?case \i\.\i\.GUILD_DIRECTORY:(?<=let{channel:(\i).+?)/, From 78c2f0d61a475d6a714918a54fc4f4c30720729f Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:43:33 -0300 Subject: [PATCH 04/28] Fix calling option onChange listeners for legacy settings --- src/plugins/ignoreActivities/index.tsx | 2 -- src/plugins/index.ts | 15 +++++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 08e146ab9..ee09ef432 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -73,8 +73,6 @@ function handleActivityToggle(e: React.MouseEvent const ignoredActivityIndex = settings.store.ignoredActivities.findIndex(act => act.id === activity.id); if (ignoredActivityIndex === -1) settings.store.ignoredActivities.push(activity); else settings.store.ignoredActivities.splice(ignoredActivityIndex, 1); - - recalculateActivities(); } function recalculateActivities() { diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 545169b1f..50523e98f 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -141,14 +141,21 @@ for (const p of neededApiPlugins) { for (const p of pluginsValues) { if (p.settings) { - p.settings.pluginName = p.name; p.options ??= {}; - for (const [name, def] of Object.entries(p.settings.def)) { + + p.settings.pluginName = p.name; + for (const name in p.settings.def) { + const def = p.settings.def[name]; const checks = p.settings.checks?.[name]; p.options[name] = { ...def, ...checks }; + } + } - if (def.onChange != null) { - SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, def.onChange); + if (p.options) { + for (const name in p.options) { + const opt = p.options[name]; + if (opt.onChange != null) { + SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, opt.onChange); } } } From e45b867ff0309aca02e6165230d8706c00bb41d9 Mon Sep 17 00:00:00 2001 From: jamesbt365 Date: Fri, 24 Jan 2025 23:42:05 +0000 Subject: [PATCH 05/28] ServerInfo: Add Ignored Users tab (#3127) --- src/plugins/serverInfo/GuildInfoModal.tsx | 23 ++++++++++++++++++++--- src/webpack/common/stores.ts | 1 + 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/plugins/serverInfo/GuildInfoModal.tsx b/src/plugins/serverInfo/GuildInfoModal.tsx index be77ca1c7..14b7e1dc8 100644 --- a/src/plugins/serverInfo/GuildInfoModal.tsx +++ b/src/plugins/serverInfo/GuildInfoModal.tsx @@ -31,7 +31,8 @@ export function openGuildInfoModal(guild: Guild) { const enum Tabs { ServerInfo, Friends, - BlockedUsers + BlockedUsers, + IgnoredUsers } interface GuildProps { @@ -44,7 +45,8 @@ interface RelationshipProps extends GuildProps { const fetched = { friends: false, - blocked: false + blocked: false, + ignored: false }; function renderTimestamp(timestamp: number) { @@ -56,10 +58,12 @@ function renderTimestamp(timestamp: number) { function GuildInfoModal({ guild }: GuildProps) { const [friendCount, setFriendCount] = useState(); const [blockedCount, setBlockedCount] = useState(); + const [ignoredCount, setIgnoredCount] = useState(); useEffect(() => { fetched.friends = false; fetched.blocked = false; + fetched.ignored = false; }, []); const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo); @@ -132,12 +136,19 @@ function GuildInfoModal({ guild }: GuildProps) { > Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""} + + Ignored Users{ignoredCount !== undefined ? ` (${ignoredCount})` : ""} +
{currentTab === Tabs.ServerInfo && } {currentTab === Tabs.Friends && } {currentTab === Tabs.BlockedUsers && } + {currentTab === Tabs.IgnoredUsers && }
); @@ -211,7 +222,13 @@ function BlockedUsersTab({ guild, setCount }: RelationshipProps) { return UserList("blocked", guild, blockedIds, setCount); } -function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) { +function IgnoredUserTab({ guild, setCount }: RelationshipProps) { + const ignoredIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isIgnored(id)); + return UserList("ignored", guild, ignoredIds, setCount); +} + + +function UserList(type: "friends" | "blocked" | "ignored", guild: Guild, ids: string[], setCount: (count: number) => void) { const missing = [] as string[]; const members = [] as string[]; diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 8579f8b92..ff668425f 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -50,6 +50,7 @@ export let GuildMemberStore: Stores.GuildMemberStore & t.FluxStore; export let RelationshipStore: Stores.RelationshipStore & t.FluxStore & { /** Get the date (as a string) that the relationship was created */ getSince(userId: string): string; + isIgnored(userId: string): boolean; }; export let EmojiStore: t.EmojiStore; From 7ee70e831a00b6ca48f73667e6e8f220415abf9e Mon Sep 17 00:00:00 2001 From: jamesbt365 Date: Fri, 24 Jan 2025 23:46:47 +0000 Subject: [PATCH 06/28] MessageLogger: Make collapseDeleted require a restart (#2923) --- src/plugins/messageLogger/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 333de9a4e..9b6b864a6 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -211,7 +211,8 @@ export default definePlugin({ collapseDeleted: { type: OptionType.BOOLEAN, description: "Whether to collapse deleted messages, similar to blocked messages", - default: false + default: false, + restartNeeded: true, }, logEdits: { type: OptionType.BOOLEAN, From 79cbfe96c8a444a51644d366ec6ecae00ad6a735 Mon Sep 17 00:00:00 2001 From: jamesbt365 Date: Fri, 24 Jan 2025 23:56:39 +0000 Subject: [PATCH 07/28] HideAttachments, UnsupressEmbeds: Work with forwarded messages (#2928) --- src/plugins/hideAttachments/index.tsx | 8 +++++++- src/plugins/unsuppressEmbeds/index.tsx | 10 ++++++++-- src/webpack/common/types/utils.d.ts | 6 +++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/plugins/hideAttachments/index.tsx b/src/plugins/hideAttachments/index.tsx index e122e3cb5..39935d038 100644 --- a/src/plugins/hideAttachments/index.tsx +++ b/src/plugins/hideAttachments/index.tsx @@ -21,6 +21,7 @@ import { ImageInvisible, ImageVisible } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { ChannelStore } from "@webpack/common"; +import { MessageSnapshot } from "@webpack/types"; let style: HTMLStyleElement; @@ -39,7 +40,12 @@ export default definePlugin({ authors: [Devs.Ven], renderMessagePopoverButton(msg) { - if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null; + // @ts-ignore - discord-types lags behind discord. + const hasAttachmentsInShapshots = msg.messageSnapshots.some( + (snapshot: MessageSnapshot) => snapshot?.message.attachments.length + ); + + if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length && !hasAttachmentsInShapshots) return null; const isHidden = hiddenMessages.has(msg.id); diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx index 16debf711..2df64b72e 100644 --- a/src/plugins/unsuppressEmbeds/index.tsx +++ b/src/plugins/unsuppressEmbeds/index.tsx @@ -21,12 +21,18 @@ import { ImageInvisible, ImageVisible } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common"; +import { MessageSnapshot } from "@webpack/types"; + const EMBED_SUPPRESSED = 1 << 2; -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, messageSnapshots, embeds, flags, id: messageId } }) => { const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0; - if (!isEmbedSuppressed && !embeds.length) return; + const hasEmbedsInSnapshots = messageSnapshots.some( + (snapshot: MessageSnapshot) => snapshot?.message.embeds.length + ); + + if (!isEmbedSuppressed && !embeds.length && !hasEmbedsInSnapshots) return; const hasEmbedPerms = channel.isPrivate() || !!(PermissionStore.getChannelPermissions({ id: channel.id }) & PermissionsBits.EMBED_LINKS); if (author.id === UserStore.getCurrentUser().id && !hasEmbedPerms) return; diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index de1ce1829..cfea5d760 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Channel, Guild, GuildMember, User } from "discord-types/general"; +import { Channel, Guild, GuildMember, Message, User } from "discord-types/general"; import type { ReactNode } from "react"; import { LiteralUnion } from "type-fest"; @@ -133,6 +133,10 @@ export type Permissions = "CREATE_INSTANT_INVITE" export type PermissionsBits = Record; +export interface MessageSnapshot { + message: Message; +} + export interface Locale { name: string; value: string; From 4036fbab92d5534417dedd6a5e8e7ecdc4e41dc6 Mon Sep 17 00:00:00 2001 From: sadan4 <117494111+sadan4@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:01:12 -0500 Subject: [PATCH 08/28] ConsoleJanitor: Remove old patch and add getLastCrash (#3151) --- src/plugins/consoleJanitor/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugins/consoleJanitor/index.ts b/src/plugins/consoleJanitor/index.ts index d251ff740..aa28e8856 100644 --- a/src/plugins/consoleJanitor/index.ts +++ b/src/plugins/consoleJanitor/index.ts @@ -95,10 +95,9 @@ export default definePlugin({ } }, { - find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");', - all: true, + find: '"AppCrashedFatalReport: getLastCrash not supported."', replacement: { - match: /console\.warn\("\[DEPRECATED\] Please use `subscribeWithSelector` middleware"\);/, + match: /console\.log\("AppCrashedFatalReport: getLastCrash not supported\."\);/, replace: "" } }, From aac5242dc8e4cbac6a7fc99a084707b973eabfbc Mon Sep 17 00:00:00 2001 From: sadan4 <117494111+sadan4@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:11:26 -0500 Subject: [PATCH 09/28] ImageZoom: Fix incorrectly adding context menu in some places (#3150) --- src/plugins/imageZoom/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 06e1dcd55..8c50ae9f4 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -81,7 +81,12 @@ export const settings = definePluginSettings({ }); -const imageContextMenuPatch: NavContextMenuPatchCallback = children => { +const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { + // Discord re-uses the image context menu for links to for the copy and open buttons + if ("href" in props) return; + // emojis in user statuses + if (props.target?.classList?.contains("emoji")) return; + const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]); children.push( From 87cb1fd930811b28784b70a5f8f7a58e5aaf6282 Mon Sep 17 00:00:00 2001 From: Suffocate <70031311+lolsuffocate@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:32:34 +0000 Subject: [PATCH 10/28] Fix top level settings notifying global listeners (#3166) --- src/shared/SettingsStore.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/SettingsStore.ts b/src/shared/SettingsStore.ts index 25dd05b19..0b6aa25b6 100644 --- a/src/shared/SettingsStore.ts +++ b/src/shared/SettingsStore.ts @@ -167,6 +167,8 @@ export class SettingsStore { this.globalListeners.forEach(cb => cb(root, settingPathStr)); this.pathListeners.get(settingPathStr)?.forEach(cb => cb(settingValue)); + } else { + this.globalListeners.forEach(cb => cb(root, pathStr)); } this.pathListeners.get(pathStr)?.forEach(cb => cb(value)); From cf28c65374ea9ac2c71358640b9d979221360b40 Mon Sep 17 00:00:00 2001 From: Grzesiek11 Date: Mon, 27 Jan 2025 03:34:00 +0100 Subject: [PATCH 11/28] Add IrcColors plugin (#2048) Co-authored-by: V --- src/plugins/ircColors/README.md | 17 ++++++ src/plugins/ircColors/index.ts | 94 +++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 src/plugins/ircColors/README.md create mode 100644 src/plugins/ircColors/index.ts diff --git a/src/plugins/ircColors/README.md b/src/plugins/ircColors/README.md new file mode 100644 index 000000000..9d9c7634b --- /dev/null +++ b/src/plugins/ircColors/README.md @@ -0,0 +1,17 @@ +# IrcColors + +Makes username colors in chat unique, like in IRC clients + +![Chat with IrcColors and Compact++ enabled](https://github.com/Vendicated/Vencord/assets/33988779/88e05c0b-a60a-4d10-949e-8b46e1d7226c) + +Improves chat readability by assigning every user an unique nickname color, +making distinguishing between different users easier. Inspired by the feature +in many IRC clients, such as HexChat or WeeChat. + +Keep in mind this overrides role colors in chat, so if you wish to know +someone's role color without checking their profile, enable the role dot: go to +**User Settings**, **Accessibility** and switch **Role Colors** to **Show role +colors next to names**. + +Created for use with the [Compact++](https://gitlab.com/Grzesiek11/compactplusplus-discord-theme) +theme. diff --git a/src/plugins/ircColors/index.ts b/src/plugins/ircColors/index.ts new file mode 100644 index 000000000..d5cc5f3e4 --- /dev/null +++ b/src/plugins/ircColors/index.ts @@ -0,0 +1,94 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; + +// Compute a 64-bit FNV-1a hash of the passed data +function hash(id: bigint) { + const fnvPrime = 1099511628211n; + const offsetBasis = 14695981039346656037n; + + let result = offsetBasis; + for (let i = 7n; i >= 0n; i--) { + result ^= (id >> (8n * i)) & 0xffn; + result = (result * fnvPrime) % 2n ** 32n; + } + + return result; +} + +// Calculate a CSS color string based on the user ID +function calculateNameColorForUser(id: bigint) { + const idHash = hash(id); + + return `hsl(${idHash % 360n}, 100%, ${settings.store.lightness}%)`; +} + +const settings = definePluginSettings({ + lightness: { + description: "Lightness, in %. Change if the colors are too light or too dark. Reopen the chat to apply.", + type: OptionType.NUMBER, + default: 70, + }, + memberListColors: { + description: "Replace role colors in the member list", + restartNeeded: true, + type: OptionType.BOOLEAN, + default: true, + }, +}); + +export default definePlugin({ + name: "IrcColors", + description: "Makes username colors in chat unique, like in IRC clients", + authors: [Devs.Grzesiek11], + patches: [ + { + find: '="SYSTEM_TAG"', + replacement: { + match: /(?<=className:\i\.username,style:.{0,50}:void 0,)/, + replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])},", + }, + }, + { + find: ".NameWithRole,{roleName:", + replacement: { + match: /(?<=color:)null!=.{0,50}?(?=,)/, + replace: "$self.calculateNameColorForListContext(arguments[0])", + }, + predicate: () => settings.store.memberListColors, + }, + ], + settings, + calculateNameColorForMessageContext(context: any) { + const id = context?.message?.author?.id; + if (id == null) { + return null; + } + return calculateNameColorForUser(BigInt(id)); + }, + calculateNameColorForListContext(context: any) { + const id = context?.user?.id; + if (id == null) { + return null; + } + return calculateNameColorForUser(BigInt(id)); + }, +}); From f29662c5b315a3579e14bcfb062d96bf047b7d39 Mon Sep 17 00:00:00 2001 From: vishnyanetchereshnya <151846235+vishnyanetchereshnya@users.noreply.github.com> Date: Mon, 27 Jan 2025 06:12:26 +0300 Subject: [PATCH 12/28] feat(ViewRaw): add View Role option (#3083) Co-authored-by: v --- src/plugins/betterRoleContext/index.tsx | 30 ++++++++++++------------- src/plugins/viewRaw/index.tsx | 27 ++++++++++++++++++---- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx index 1029c07e2..afef63908 100644 --- a/src/plugins/betterRoleContext/index.tsx +++ b/src/plugins/betterRoleContext/index.tsx @@ -83,7 +83,7 @@ export default definePlugin({ if (!role) return; if (role.colorString) { - children.push( + children.unshift( { + await GuildSettingsActions.open(guild.id, "ROLES"); + GuildSettingsActions.selectRole(id); + }} + icon={PencilIcon} + /> + ); + } + if (role.icon) { children.push( { - await GuildSettingsActions.open(guild.id, "ROLES"); - GuildSettingsActions.selectRole(id); - }} - icon={PencilIcon} - /> - ); - } } } }); diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index b45919a21..ddcbd3b46 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -22,12 +22,12 @@ import { CodeBlock } from "@components/CodeBlock"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { Devs } from "@utils/constants"; -import { getIntlMessage } from "@utils/discord"; +import { getCurrentGuild, getIntlMessage } from "@utils/discord"; import { Margins } from "@utils/margins"; import { copyWithToast } from "@utils/misc"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common"; +import { Button, ChannelStore, Forms, GuildStore, Menu, Text } from "@webpack/common"; import { Message } from "discord-types/general"; @@ -118,7 +118,7 @@ const settings = definePluginSettings({ } }); -function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback { +function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel"): NavContextMenuPatchCallback { return (children, props) => { const value = props[name.toLowerCase()]; if (!value) return; @@ -144,6 +144,23 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenu }; } +const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id: string; }) => { + const guild = getCurrentGuild(); + if (!guild) return; + + const role = GuildStore.getRole(guild.id, id); + if (!role) return; + + children.push( + openViewRawModal(JSON.stringify(role, null, 4), "Role")} + icon={CopyIcon} + /> + ); +}; + export default definePlugin({ name: "ViewRaw", description: "Copy and view the raw content/data of any message, channel or guild", @@ -152,10 +169,12 @@ export default definePlugin({ contextMenus: { "guild-context": MakeContextCallback("Guild"), + "guild-settings-role-context": MakeContextCallback("Role"), "channel-context": MakeContextCallback("Channel"), "thread-context": MakeContextCallback("Channel"), "gdm-context": MakeContextCallback("Channel"), - "user-context": MakeContextCallback("User") + "user-context": MakeContextCallback("User"), + "dev-context": devContextCallback }, renderMessagePopoverButton(msg) { From 3350922c09ea1e9ad0f4f4f40320e7e80b7ce940 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Mon, 27 Jan 2025 04:25:37 +0100 Subject: [PATCH 13/28] LastFmRPC: Add option to hide if there is another presence closes #2866 Co-Authored-By: 54ac --- src/plugins/lastfm/index.tsx | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index 02fd694f8..77fa27841 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -86,7 +86,7 @@ const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f"; const logger = new Logger("LastFMRichPresence"); -const presenceStore = findByPropsLazy("getLocalPresence"); +const PresenceStore = findByPropsLazy("getLocalPresence"); async function getApplicationAsset(key: string): Promise { return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; @@ -124,6 +124,11 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true, }, + hideWithActivity: { + description: "Hide Last.fm presence if you have any other presence", + type: OptionType.BOOLEAN, + default: false, + }, statusName: { description: "custom status text", type: OptionType.STRING, @@ -274,12 +279,16 @@ export default definePlugin({ }, async getActivity(): Promise { + if (settings.store.hideWithActivity) { + if (PresenceStore.getActivities().some(a => a.application_id !== applicationId)) { + return null; + } + } + if (settings.store.hideWithSpotify) { - for (const activity of presenceStore.getActivities()) { - if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) { - // there is already music status because of Spotify or richerCider (probably more) - return null; - } + if (PresenceStore.getActivities().some(a => a.type === ActivityType.LISTENING && a.application_id !== applicationId)) { + // there is already music status because of Spotify or richerCider (probably more) + return null; } } From c4f8221f75157c6f23f4aaba22b17e06a785917d Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:30:11 -0300 Subject: [PATCH 14/28] IrcColors: Make lightness apply without restart --- src/plugins/ircColors/index.ts | 53 ++++++++++++++-------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/src/plugins/ircColors/index.ts b/src/plugins/ircColors/index.ts index d5cc5f3e4..208b327e9 100644 --- a/src/plugins/ircColors/index.ts +++ b/src/plugins/ircColors/index.ts @@ -17,33 +17,22 @@ */ import { definePluginSettings } from "@api/Settings"; +import { hash as h64 } from "@intrnl/xxhash64"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; - -// Compute a 64-bit FNV-1a hash of the passed data -function hash(id: bigint) { - const fnvPrime = 1099511628211n; - const offsetBasis = 14695981039346656037n; - - let result = offsetBasis; - for (let i = 7n; i >= 0n; i--) { - result ^= (id >> (8n * i)) & 0xffn; - result = (result * fnvPrime) % 2n ** 32n; - } - - return result; -} +import { useMemo } from "@webpack/common"; // Calculate a CSS color string based on the user ID -function calculateNameColorForUser(id: bigint) { - const idHash = hash(id); +function calculateNameColorForUser(id: string) { + const { lightness } = settings.use(["lightness"]); + const idHash = useMemo(() => h64(id), [id]); - return `hsl(${idHash % 360n}, 100%, ${settings.store.lightness}%)`; + return `hsl(${idHash % 360n}, 100%, ${lightness}%)`; } const settings = definePluginSettings({ lightness: { - description: "Lightness, in %. Change if the colors are too light or too dark. Reopen the chat to apply.", + description: "Lightness, in %. Change if the colors are too light or too dark", type: OptionType.NUMBER, default: 70, }, @@ -51,44 +40,46 @@ const settings = definePluginSettings({ description: "Replace role colors in the member list", restartNeeded: true, type: OptionType.BOOLEAN, - default: true, - }, + default: true + } }); export default definePlugin({ name: "IrcColors", description: "Makes username colors in chat unique, like in IRC clients", authors: [Devs.Grzesiek11], + settings, + patches: [ { find: '="SYSTEM_TAG"', replacement: { match: /(?<=className:\i\.username,style:.{0,50}:void 0,)/, - replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])},", - }, + replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])}," + } }, { - find: ".NameWithRole,{roleName:", + find: "#{intl::GUILD_OWNER}),children:", replacement: { - match: /(?<=color:)null!=.{0,50}?(?=,)/, - replace: "$self.calculateNameColorForListContext(arguments[0])", + match: /(?<=\.MEMBER_LIST}\),\[\]\),)(.+?color:)null!=.{0,50}?(?=,)/, + replace: (_, rest) => `ircColor=$self.calculateNameColorForListContext(arguments[0]),${rest}ircColor` }, - predicate: () => settings.store.memberListColors, - }, + predicate: () => settings.store.memberListColors + } ], - settings, + calculateNameColorForMessageContext(context: any) { const id = context?.message?.author?.id; if (id == null) { return null; } - return calculateNameColorForUser(BigInt(id)); + return calculateNameColorForUser(id); }, calculateNameColorForListContext(context: any) { const id = context?.user?.id; if (id == null) { return null; } - return calculateNameColorForUser(BigInt(id)); - }, + return calculateNameColorForUser(id); + } }); From ceba9776c4be12288dbf618b5bc36e0e41d3f70f Mon Sep 17 00:00:00 2001 From: Vendicated Date: Mon, 27 Jan 2025 20:44:54 +0100 Subject: [PATCH 15/28] Delete MoreUserTags for now because it's unstable This plugin is written in a way that makes it susceptible to crashes. This is not the first time it has caused crashes and will not be the last. A rewrite is necessary to make it more robust --- src/plugins/moreUserTags/index.tsx | 372 ----------------------------- 1 file changed, 372 deletions(-) delete mode 100644 src/plugins/moreUserTags/index.tsx diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx deleted file mode 100644 index 8029b4833..000000000 --- a/src/plugins/moreUserTags/index.tsx +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { definePluginSettings } from "@api/Settings"; -import { Flex } from "@components/Flex"; -import { Devs } from "@utils/constants"; -import { getIntlMessage } from "@utils/discord"; -import { Margins } from "@utils/margins"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByCodeLazy, findLazy } from "@webpack"; -import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip } from "@webpack/common"; -import type { Permissions, RC } from "@webpack/types"; -import type { Channel, Guild, Message, User } from "discord-types/general"; - -interface Tag { - // name used for identifying, must be alphanumeric + underscores - name: string; - // name shown on the tag itself, can be anything probably; automatically uppercase'd - displayName: string; - description: string; - permissions?: Permissions[]; - condition?(message: Message | null, user: User, channel: Channel): boolean; -} - -interface TagSetting { - text: string; - showInChat: boolean; - showInNotChat: boolean; -} -interface TagSettings { - WEBHOOK: TagSetting, - OWNER: TagSetting, - ADMINISTRATOR: TagSetting, - MODERATOR_STAFF: TagSetting, - MODERATOR: TagSetting, - VOICE_MODERATOR: TagSetting, - TRIAL_MODERATOR: TagSetting, - [k: string]: TagSetting; -} - -// PermissionStore.computePermissions will not work here since it only gets permissions for the current user -const computePermissions: (options: { - user?: { id: string; } | string | null; - context?: Guild | Channel | null; - overwrites?: Channel["permissionOverwrites"] | null; - checkElevated?: boolean /* = true */; - excludeGuildPermissions?: boolean /* = false */; -}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()"); - -const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number, className?: string, useRemSizes?: boolean; }> & { Types: Record; }; - -const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot(); - -const tags: Tag[] = [ - { - name: "WEBHOOK", - displayName: "Webhook", - description: "Messages sent by webhooks", - condition: isWebhook - }, { - name: "OWNER", - displayName: "Owner", - description: "Owns the server", - condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id - }, { - name: "ADMINISTRATOR", - displayName: "Admin", - description: "Has the administrator permission", - permissions: ["ADMINISTRATOR"] - }, { - name: "MODERATOR_STAFF", - displayName: "Staff", - description: "Can manage the server, channels or roles", - permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"] - }, { - name: "MODERATOR", - displayName: "Mod", - description: "Can manage messages or kick/ban people", - permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"] - }, { - name: "VOICE_MODERATOR", - displayName: "VC Mod", - description: "Can manage voice chats", - permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"] - }, { - name: "CHAT_MODERATOR", - displayName: "Chat Mod", - description: "Can timeout people", - permissions: ["MODERATE_MEMBERS"] - } -]; -const defaultSettings = Object.fromEntries( - tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }]) -) as TagSettings; - -function SettingsComponent() { - const tagSettings = settings.store.tagSettings ??= defaultSettings; - - return ( - - {tags.map(t => ( - - - - {({ onMouseEnter, onMouseLeave }) => ( -
- {t.displayName} Tag -
- )} -
-
- - tagSettings[t.name].text = v} - className={Margins.bottom16} - /> - - tagSettings[t.name].showInChat = v} - hideBorder - > - Show in messages - - - tagSettings[t.name].showInNotChat = v} - hideBorder - > - Show in member list and profiles - -
- ))} -
- ); -} - -const settings = definePluginSettings({ - dontShowForBots: { - description: "Don't show extra tags for bots (excluding webhooks)", - type: OptionType.BOOLEAN - }, - dontShowBotTag: { - description: "Only show extra tags for bots / Hide [BOT] text", - type: OptionType.BOOLEAN - }, - tagSettings: { - type: OptionType.COMPONENT, - component: SettingsComponent, - description: "fill me" - } -}); - -export default definePlugin({ - name: "MoreUserTags", - description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)", - authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN], - settings, - patches: [ - // add tags to the tag list - { - find: ".ORIGINAL_POSTER=", - replacement: { - match: /(?=(\i)\[\i\.BOT)/, - replace: "$self.genTagTypes($1);" - } - }, - { - find: "#{intl::DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP_OFFICIAL}", - replacement: [ - // make the tag show the right text - { - match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(.{0,40}#{intl::APP_TAG}\))/, - replace: (_, origSwitch, variant, tags, displayedText, originalText) => - `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}],${originalText})}` - }, - // show OP tags correctly - { - match: /(\i)=(\i)===\i(?:\.\i)?\.ORIGINAL_POSTER/, - replace: "$1=$self.isOPTag($2)" - }, - // add HTML data attributes (for easier theming) - { - match: /.botText,children:(\i)}\)]/, - replace: "$&,'data-tag':$1.toLowerCase()" - } - ], - }, - // in messages - { - find: ".Types.ORIGINAL_POSTER", - replacement: { - match: /;return\((\(null==\i\?void 0:\i\.isSystemDM\(\).+?.Types.ORIGINAL_POSTER\)),null==(\i)\)/, - replace: ";$1;$2=$self.getTag({...arguments[0],origType:$2,location:'chat'});return $2 == null" - } - }, - // in the member list - { - find: "#{intl::GUILD_OWNER}),children:", - replacement: { - match: /(?\i)=\(null==.{0,100}\.BOT;return null!=(?\i)&&\i\.bot/, - replace: "$ = $self.getTag({user: $, channel: arguments[0].channel, origType: $.bot ? 0 : null, location: 'not-chat' }); return typeof $ === 'number'" - } - }, - // pass channel id down props to be used in profiles - { - find: ".hasAvatarForGuild(null==", - replacement: { - match: /(?=usernameIcon:)/, - replace: "moreTags_channelId:arguments[0].channelId," - } - }, - { - find: "#{intl::USER_PROFILE_PRONOUNS}", - replacement: { - match: /(?=,hideBotTag:!0)/, - replace: ",moreTags_channelId:arguments[0].moreTags_channelId" - } - }, - // in profiles - { - find: ",overrideDiscriminator:", - group: true, - replacement: [ - { - // prevent channel id from getting ghosted - // it's either this or extremely long lookbehind - match: /user:\i,nick:\i,/, - replace: "$&moreTags_channelId," - }, { - match: /,botType:(\i),botVerified:(\i),(?!discriminatorClass:)(?<=user:(\i).+?)/g, - replace: ",botType:$self.getTag({user:$3,channelId:moreTags_channelId,origType:$1,location:'not-chat'}),botVerified:$2," - } - ] - }, - ], - - start() { - settings.store.tagSettings ??= defaultSettings; - - // newly added field might be missing from old users - settings.store.tagSettings.CHAT_MODERATOR ??= { - text: "Chat Mod", - showInChat: true, - showInNotChat: true - }; - }, - - getPermissions(user: User, channel: Channel): string[] { - const guild = GuildStore.getGuild(channel?.guild_id); - if (!guild) return []; - - const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites }); - return Object.entries(PermissionsBits) - .map(([perm, permInt]) => - permissions & permInt ? perm : "" - ) - .filter(Boolean); - }, - - genTagTypes(obj) { - let i = 100; - tags.forEach(({ name }) => { - obj[name] = ++i; - obj[i] = name; - obj[`${name}-BOT`] = ++i; - obj[i] = `${name}-BOT`; - obj[`${name}-OP`] = ++i; - obj[i] = `${name}-OP`; - }); - }, - - isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]), - - getTagText(passedTagName: string, originalText: string) { - try { - const [tagName, variant] = passedTagName.split("-"); - if (!passedTagName) return getIntlMessage("APP_TAG"); - const tag = tags.find(({ name }) => tagName === name); - if (!tag) return getIntlMessage("APP_TAG"); - if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return getIntlMessage("APP_TAG"); - - const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName; - switch (variant) { - case "OP": - return `${getIntlMessage("BOT_TAG_FORUM_ORIGINAL_POSTER")} • ${tagText}`; - case "BOT": - return `${getIntlMessage("APP_TAG")} • ${tagText}`; - default: - return tagText; - } - } catch { - return originalText; - } - }, - - getTag({ - message, user, channelId, origType, location, channel - }: { - message?: Message, - user: User & { isClyde(): boolean; }, - channel?: Channel & { isForumPost(): boolean; isMediaPost(): boolean; }, - channelId?: string; - origType?: number; - location: "chat" | "not-chat"; - }): number | null { - if (!user) - return null; - if (location === "chat" && user.id === "1") - return Tag.Types.OFFICIAL; - if (user.isClyde()) - return Tag.Types.AI; - - let type = typeof origType === "number" ? origType : null; - - channel ??= ChannelStore.getChannel(channelId!) as any; - if (!channel) return type; - - const settings = this.settings.store; - const perms = this.getPermissions(user, channel); - - for (const tag of tags) { - if (location === "chat" && !settings.tagSettings[tag.name].showInChat) continue; - if (location === "not-chat" && !settings.tagSettings[tag.name].showInNotChat) continue; - - // If the owner tag is disabled, and the user is the owner of the guild, - // avoid adding other tags because the owner will always match the condition for them - if ( - tag.name !== "OWNER" && - GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id && - (location === "chat" && !settings.tagSettings.OWNER.showInChat) || - (location === "not-chat" && !settings.tagSettings.OWNER.showInNotChat) - ) continue; - - if ( - tag.permissions?.some(perm => perms.includes(perm)) || - (tag.condition?.(message!, user, channel)) - ) { - if ((channel.isForumPost() || channel.isMediaPost()) && channel.ownerId === user.id) - type = Tag.Types[`${tag.name}-OP`]; - else if (user.bot && !isWebhook(message!, user) && !settings.dontShowBotTag) - type = Tag.Types[`${tag.name}-BOT`]; - else - type = Tag.Types[tag.name]; - break; - } - } - return type; - } -}); From ea1e96185b1f1a613a2100c1d7899603790dc862 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Mon, 27 Jan 2025 20:54:16 +0100 Subject: [PATCH 16/28] MessageLatency: ErrorBoundary should be noop --- src/plugins/messageLatency/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/messageLatency/index.tsx b/src/plugins/messageLatency/index.tsx index e4e5b8771..1bc18580e 100644 --- a/src/plugins/messageLatency/index.tsx +++ b/src/plugins/messageLatency/index.tsx @@ -162,7 +162,7 @@ export default definePlugin({ } ; - }); + }, { noop: true }); }, Icon({ delta, fill, props }: { From 21ded874a3e94415639ee14b3e8716d894e3456a Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:47:27 -0300 Subject: [PATCH 17/28] Settings API: Add utility to migrate a setting --- src/api/Settings.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index c99d030d0..8c05d9bb3 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -220,6 +220,19 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) { } } +export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) { + const { plugins } = SettingsStore.plain; + + if ( + plugins[pluginName][newSetting] != null || + plugins[pluginName][oldSetting] == null + ) return; + + plugins[pluginName][newSetting] = plugins[pluginName][oldSetting]; + delete plugins[pluginName][oldSetting]; + SettingsStore.markAsChanged(); +} + export function definePluginSettings< Def extends SettingsDefinition, Checks extends SettingsChecks, From f43baddc550dfd30c1a6b44f7e42544b03084a5a Mon Sep 17 00:00:00 2001 From: jamesbt365 Date: Tue, 28 Jan 2025 01:57:16 +0000 Subject: [PATCH 18/28] NoBlockedMessages: Add ignored messages (#3126) --- src/plugins/noBlockedMessages/index.ts | 59 +++++++++++++++++--------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/src/plugins/noBlockedMessages/index.ts b/src/plugins/noBlockedMessages/index.ts index 48ca63d18..95b53c6b3 100644 --- a/src/plugins/noBlockedMessages/index.ts +++ b/src/plugins/noBlockedMessages/index.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Settings } from "@api/Settings"; +import { definePluginSettings, migratePluginSetting } from "@api/Settings"; import { Devs } from "@utils/constants"; import { runtimeHashMessageKey } from "@utils/intlHash"; import { Logger } from "@utils/Logger"; @@ -32,10 +32,29 @@ interface MessageDeleteProps { collapsedReason: () => any; } +// Remove this migration once enough time has passed +migratePluginSetting("NoBlockedMessages", "ignoreBlockedMessages", "ignoreMessages"); +const settings = definePluginSettings({ + ignoreMessages: { + description: "Completely ignores incoming messages from blocked and ignored (if enabled) users", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + }, + applyToIgnoredUsers: { + description: "Additionally apply to 'ignored' users", + type: OptionType.BOOLEAN, + default: true, + restartNeeded: false + } +}); + export default definePlugin({ name: "NoBlockedMessages", - description: "Hides all blocked messages from chat completely.", - authors: [Devs.rushii, Devs.Samu], + description: "Hides all blocked/ignored messages from chat completely", + authors: [Devs.rushii, Devs.Samu, Devs.jamesbt365], + settings, + patches: [ { find: "#{intl::BLOCKED_MESSAGES_HIDE}", @@ -51,38 +70,40 @@ export default definePlugin({ '"ReadStateStore"' ].map(find => ({ find, - predicate: () => Settings.plugins.NoBlockedMessages.ignoreBlockedMessages === true, + predicate: () => settings.store.ignoreMessages, replacement: [ { match: /(?<=function (\i)\((\i)\){)(?=.*MESSAGE_CREATE:\1)/, - replace: (_, _funcName, props) => `if($self.isBlocked(${props}.message))return;` + replace: (_, _funcName, props) => `if($self.shouldIgnoreMessage(${props}.message))return;` } ] })) ], - options: { - ignoreBlockedMessages: { - description: "Completely ignores (recent) incoming messages from blocked users (locally).", - type: OptionType.BOOLEAN, - default: false, - restartNeeded: true, - }, - }, - isBlocked(message: Message) { + shouldIgnoreMessage(message: Message) { try { - return RelationshipStore.isBlocked(message.author.id); + if (RelationshipStore.isBlocked(message.author.id)) { + return true; + } + return settings.store.applyToIgnoredUsers && RelationshipStore.isIgnored(message.author.id); } catch (e) { - new Logger("NoBlockedMessages").error("Failed to check if user is blocked:", e); + new Logger("NoBlockedMessages").error("Failed to check if user is blocked or ignored:", e); + return false; } }, - shouldHide(props: MessageDeleteProps) { + shouldHide(props: MessageDeleteProps): boolean { try { - return props.collapsedReason() === i18n.t[runtimeHashMessageKey("BLOCKED_MESSAGE_COUNT")](); + const collapsedReason = props.collapsedReason(); + const blockedReason = i18n.t[runtimeHashMessageKey("BLOCKED_MESSAGE_COUNT")](); + const ignoredReason = settings.store.applyToIgnoredUsers + ? i18n.t[runtimeHashMessageKey("IGNORED_MESSAGE_COUNT")]() + : null; + + return collapsedReason === blockedReason || collapsedReason === ignoredReason; } catch (e) { console.error(e); + return false; } - return false; } }); From cdc756193e93914d3f644f52ecc4d65dbd02d5a9 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 Jan 2025 01:13:36 -0300 Subject: [PATCH 19/28] Settings API: Fix erroring if plugin settings don't exist --- src/api/Settings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 8c05d9bb3..262722b14 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -224,8 +224,8 @@ export function migratePluginSetting(pluginName: string, oldSetting: string, new const { plugins } = SettingsStore.plain; if ( - plugins[pluginName][newSetting] != null || - plugins[pluginName][oldSetting] == null + plugins?.[pluginName]?.[oldSetting] == null || + plugins[pluginName][newSetting] != null ) return; plugins[pluginName][newSetting] = plugins[pluginName][oldSetting]; From 33d4f13a242fb4b5124010546773a7bda3d9d962 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 29 Jan 2025 01:04:36 -0300 Subject: [PATCH 20/28] Fix everything broken by recent Discord update (#3177) Co-authored-by: sadan <117494111+sadan4@users.noreply.github.com> Co-authored-by: Vendicated --- src/api/ChatButtons.tsx | 4 +- src/api/ContextMenu.ts | 8 +- src/api/Settings.ts | 12 +- src/debug/runReporter.ts | 15 +- src/plugins/_api/badges/index.tsx | 18 +-- src/plugins/_api/chatButtons.ts | 16 ++- src/plugins/_api/contextMenu.ts | 20 ++- src/plugins/_api/menuItemDemangler.ts | 68 +++++++++ src/plugins/_core/settings.tsx | 2 +- src/plugins/betterFolders/index.tsx | 4 +- src/plugins/betterSessions/index.tsx | 4 +- src/plugins/betterSettings/index.tsx | 4 +- src/plugins/consoleJanitor/index.ts | 4 +- src/plugins/consoleShortcuts/index.ts | 37 ++++- src/plugins/ctrlEnterSend/index.ts | 6 +- src/plugins/fakeNitro/index.tsx | 8 +- src/plugins/fullSearchContext/index.tsx | 4 +- src/plugins/gameActivityToggle/index.tsx | 2 +- src/plugins/iLoveSpam/index.ts | 2 +- src/plugins/ignoreActivities/index.tsx | 2 +- src/plugins/implicitRelationships/index.ts | 2 +- src/plugins/mentionAvatars/index.tsx | 2 +- src/plugins/messageLatency/index.tsx | 4 +- src/plugins/messageLogger/index.tsx | 2 +- src/plugins/openInApp/index.ts | 4 +- src/plugins/permissionFreeWill/index.ts | 4 +- .../pinDms/components/CreateCategoryModal.tsx | 4 +- src/plugins/platformIndicators/index.tsx | 8 +- src/plugins/showHiddenChannels/index.tsx | 22 +-- src/plugins/showTimeoutDuration/index.tsx | 4 +- src/plugins/typingIndicator/index.tsx | 4 +- src/plugins/userVoiceShow/components.tsx | 2 +- src/plugins/vencordToolbox/index.tsx | 4 +- src/plugins/viewIcons/index.tsx | 8 +- src/utils/modal.tsx | 81 ++++++----- src/webpack/common/components.ts | 130 +++++++++--------- src/webpack/common/menu.ts | 20 ++- src/webpack/common/types/components.d.ts | 3 - src/webpack/common/types/iconNames.d.ts | 14 -- src/webpack/patchWebpack.ts | 40 ++++-- src/webpack/webpack.ts | 31 +++-- 41 files changed, 389 insertions(+), 244 deletions(-) create mode 100644 src/plugins/_api/menuItemDemangler.ts delete mode 100644 src/webpack/common/types/iconNames.d.ts diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx index c24e3886f..6f4285ff5 100644 --- a/src/api/ChatButtons.tsx +++ b/src/api/ChatButtons.tsx @@ -9,7 +9,7 @@ import "./ChatButton.css"; import ErrorBoundary from "@components/ErrorBoundary"; import { Logger } from "@utils/Logger"; import { waitFor } from "@webpack"; -import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; +import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common"; import { Channel } from "discord-types/general"; import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react"; @@ -110,7 +110,7 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => { + ) } }); diff --git a/src/plugins/reviewDB/settings.tsx b/src/plugins/reviewDB/settings.tsx index eeebd0aa1..2b58d080c 100644 --- a/src/plugins/reviewDB/settings.tsx +++ b/src/plugins/reviewDB/settings.tsx @@ -27,7 +27,6 @@ import { cl } from "./utils"; export const settings = definePluginSettings({ authorize: { type: OptionType.COMPONENT, - description: "Authorize with ReviewDB", component: () => ( diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index 3d1e891d1..4bec9b6f9 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -45,7 +45,6 @@ const makeEmptyRuleArray = () => [makeEmptyRule()]; const settings = definePluginSettings({ replace: { type: OptionType.COMPONENT, - description: "", component: () => { const { stringRules, regexRules } = settings.use(["stringRules", "regexRules"]); diff --git a/src/utils/types.ts b/src/utils/types.ts index 54de59e34..8f0ec3860 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -198,15 +198,16 @@ export type SettingsChecks = { (IsDisabled> & IsValid, DefinedSettings>); }; -export type PluginSettingDef = (PluginSettingCustomDef & Pick) | (( - | PluginSettingStringDef - | PluginSettingNumberDef - | PluginSettingBooleanDef - | PluginSettingSelectDef - | PluginSettingSliderDef - | PluginSettingComponentDef - | PluginSettingBigIntDef -) & PluginSettingCommon); +export type PluginSettingDef = + (PluginSettingCustomDef & Pick) | + (PluginSettingComponentDef & Omit) | (( + | PluginSettingStringDef + | PluginSettingNumberDef + | PluginSettingBooleanDef + | PluginSettingSelectDef + | PluginSettingSliderDef + | PluginSettingBigIntDef + ) & PluginSettingCommon); export interface PluginSettingCommon { description: string; @@ -226,12 +227,14 @@ export interface PluginSettingCommon { */ target?: "WEB" | "DESKTOP" | "BOTH"; } + interface IsDisabled { /** * Checks if this setting should be disabled */ disabled?(this: D): boolean; } + interface IsValid { /** * Prevents the user from saving settings if this is false or a string @@ -320,7 +323,7 @@ type PluginSettingType = O extends PluginSettingStri O extends PluginSettingBooleanDef ? boolean : O extends PluginSettingSelectDef ? O["options"][number]["value"] : O extends PluginSettingSliderDef ? number : - O extends PluginSettingComponentDef ? any : + O extends PluginSettingComponentDef ? O extends { default: infer Default; } ? Default : any : O extends PluginSettingCustomDef ? O extends { default: infer Default; } ? Default : any : never; @@ -382,7 +385,7 @@ export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDe export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid; -export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon; +export type PluginOptionComponent = PluginSettingComponentDef & Omit; export type PluginOptionCustom = PluginSettingCustomDef & Pick; export type PluginNative any>> = { From 240195f9bfb59294c11294282b140bcd55523432 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 29 Jan 2025 20:44:15 +0100 Subject: [PATCH 25/28] Bump to v1.11.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7dca5793..057175f9c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.11.2", + "version": "1.11.3", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 7d45862023510838206e76cc9b08f72da213dea0 Mon Sep 17 00:00:00 2001 From: sadan4 <117494111+sadan4@users.noreply.github.com> Date: Wed, 29 Jan 2025 20:28:11 -0500 Subject: [PATCH 26/28] Fix IgnoreActivities and AlwaysAnimate for canary (#3182) --- src/plugins/alwaysAnimate/index.ts | 4 ++-- src/plugins/ignoreActivities/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/alwaysAnimate/index.ts b/src/plugins/alwaysAnimate/index.ts index fc528466f..a5297445b 100644 --- a/src/plugins/alwaysAnimate/index.ts +++ b/src/plugins/alwaysAnimate/index.ts @@ -43,8 +43,8 @@ export default definePlugin({ // Status emojis find: "#{intl::GUILD_OWNER}),children:", replacement: { - match: /(?<=\.activityEmoji,.+?animate:)\i/, - replace: "!0" + match: /(\.CUSTOM_STATUS.+?animate:)\i/, + replace: (_, rest) => `${rest}!0` } }, { diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 0a88aa1f5..d21f05799 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -241,7 +241,7 @@ export default definePlugin({ find: '"LocalActivityStore"', replacement: [ { - match: /HANG_STATUS.+?(?=!?\i\(\)\(\i,\i\))(?<=(\i)\.push.+?)/, + match: /\.LISTENING.+?(?=!?\i\(\)\(\i,\i\))(?<=(\i)\.push.+?)/, replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);` } ] From 68662c96255f617189411d97a9f11cabe2f5584d Mon Sep 17 00:00:00 2001 From: jamesbt365 Date: Thu, 30 Jan 2025 01:36:56 +0000 Subject: [PATCH 27/28] QuickReply: Fix showing toggle mention in guilds (#3181) --- src/plugins/quickReply/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/quickReply/index.ts b/src/plugins/quickReply/index.ts index 4a7060c59..f6ca5b459 100644 --- a/src/plugins/quickReply/index.ts +++ b/src/plugins/quickReply/index.ts @@ -196,7 +196,7 @@ function nextReply(isUp: boolean) { channel, message, shouldMention: shouldMention(message), - showMentionToggle: channel.isPrivate() && message.author.id !== meId, + showMentionToggle: !channel.isPrivate() && message.author.id !== meId, _isQuickReply: true }); ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS"); From 8fccda4a249b843c094e944c2051e951a7a43235 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Thu, 30 Jan 2025 14:41:32 +1300 Subject: [PATCH 28/28] WhoReacted, TypingIndicator: Fix triggering other actions (#3161) Prevents typing in message user box from activating parent click handlers Fixes https://github.com/Vendicated/Vencord/issues/3128 --- src/plugins/typingIndicator/index.tsx | 28 +++++++++++++++++---------- src/plugins/whoReacted/index.tsx | 4 ++-- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index 642dcc29f..e6903bcde 100644 --- a/src/plugins/typingIndicator/index.tsx +++ b/src/plugins/typingIndicator/index.tsx @@ -100,16 +100,24 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s {props => (
{((settings.store.indicatorMode & IndicatorMode.Avatars) === IndicatorMode.Avatars) && ( - UserStore.getUser(id))} - guildId={guildId} - renderIcon={false} - max={3} - showDefaultAvatarsForNullUsers - showUserPopout - size={16} - className="vc-typing-indicator-avatars" - /> +
{ + e.stopPropagation(); + e.preventDefault(); + }} + onKeyPress={e => e.stopPropagation()} + > + UserStore.getUser(id))} + guildId={guildId} + renderIcon={false} + max={3} + showDefaultAvatarsForNullUsers + showUserPopout + size={16} + className="vc-typing-indicator-avatars" + /> +
)} {((settings.store.indicatorMode & IndicatorMode.Dots) === IndicatorMode.Dots) && (
diff --git a/src/plugins/whoReacted/index.tsx b/src/plugins/whoReacted/index.tsx index 803cc7156..aea57fef2 100644 --- a/src/plugins/whoReacted/index.tsx +++ b/src/plugins/whoReacted/index.tsx @@ -93,7 +93,7 @@ function makeRenderMoreUsers(users: User[]) { }; } -function handleClickAvatar(event: React.MouseEvent) { +function handleClickAvatar(event: React.UIEvent) { event.stopPropagation(); } @@ -165,7 +165,7 @@ export default definePlugin({
-
+