From 0c0498e531f75b16ddb8c5ec9af608fb9633d566 Mon Sep 17 00:00:00 2001 From: prism Date: Thu, 27 Nov 2025 13:47:59 -0800 Subject: [PATCH] feat(HeaderBarAPI): Add API to register header bar buttons (#530) * feat(HeaderBarAPI): Add API to register header bar buttons * bleh * t * t * a * t --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/api/HeaderBar.tsx | 62 +++++++++++++++ src/api/PluginManager.ts | 18 +++-- src/api/index.ts | 6 ++ src/equicordplugins/baseDecoder/index.tsx | 1 - src/equicordplugins/customSounds/index.tsx | 5 +- src/equicordplugins/dontFilterMe/index.tsx | 46 +++++------- src/equicordplugins/equicordToolbox/index.tsx | 35 +++------ src/equicordplugins/findReply/index.tsx | 2 - src/equicordplugins/holyNotes/index.tsx | 48 ++---------- src/equicordplugins/husk/index.tsx | 1 - .../components/LogsButton.tsx | 25 +------ .../messageLoggerEnhanced/index.tsx | 30 +------- src/equicordplugins/moreUserTags/index.tsx | 1 - src/equicordplugins/noBulletPoints/index.tsx | 13 +--- src/equicordplugins/polishWording/index.ts | 6 +- .../questCompleter.discordDesktop/index.tsx | 75 +++---------------- src/equicordplugins/sekaiStickers/index.tsx | 1 - .../showBadgesInChat/index.tsx | 9 +-- src/equicordplugins/signature/index.tsx | 21 ++---- src/equicordplugins/soggy/index.tsx | 28 +------ .../soundBoardLogger/index.tsx | 33 +------- .../soundBoardLogger/settings.tsx | 9 --- .../splitLargeMessages/index.ts | 9 +-- src/equicordplugins/talkInReverse/index.tsx | 2 +- src/equicordplugins/translatePlus/index.tsx | 1 - src/plugins/_api/headerBar.ts | 24 ++++++ src/plugins/platformIndicators/index.tsx | 52 +++---------- src/plugins/silentTyping/index.tsx | 1 - src/plugins/userVoiceShow/index.tsx | 36 ++++----- src/utils/types.ts | 4 +- 30 files changed, 204 insertions(+), 400 deletions(-) create mode 100644 src/api/HeaderBar.tsx create mode 100644 src/plugins/_api/headerBar.ts diff --git a/src/api/HeaderBar.tsx b/src/api/HeaderBar.tsx new file mode 100644 index 000000000..04d740f8c --- /dev/null +++ b/src/api/HeaderBar.tsx @@ -0,0 +1,62 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { useEffect, useState } from "@webpack/common"; +import type { JSX } from "react"; + +export type HeaderBarButtonFactory = () => JSX.Element | null; + +interface HeaderBarButton { + render: HeaderBarButtonFactory; + priority: number; +} + +export const buttons = new Map(); + +const listeners = new Set<() => void>(); + +/** + * Add a button to the header bar. + * @param id Unique identifier for the button. + * @param render Function that renders the button component. + * @param priority Higher priority = more to the right. Default is 0. + */ +export function addHeaderBarButton(id: string, render: HeaderBarButtonFactory, priority = 0) { + buttons.set(id, { render, priority }); + listeners.forEach(l => l()); +} + +/** + * Remove a button from the header bar. + * @param id The identifier of the button to remove. + */ +export function removeHeaderBarButton(id: string) { + buttons.delete(id); + listeners.forEach(l => l()); +} + +function HeaderBarButtons() { + const [, forceUpdate] = useState(0); + + useEffect(() => { + const listener = () => forceUpdate(n => n + 1); + listeners.add(listener); + return () => { listeners.delete(listener); }; + }, []); + + const sorted = Array.from(buttons).sort(([, a], [, b]) => a.priority - b.priority); + + return sorted.map(([id, { render: Button }]) => ( + + - )} - - - ); - }, - renderNavBar(e) { - if (Array.isArray(e.toolbar)) - return e.toolbar.unshift( - - - - ); - - e.toolbar = [ - - - , - e.toolbar, - ]; - } + renderHeaderBarButton: ToolBarHeader }); diff --git a/src/equicordplugins/sekaiStickers/index.tsx b/src/equicordplugins/sekaiStickers/index.tsx index 08cc3058f..af1d6c1af 100644 --- a/src/equicordplugins/sekaiStickers/index.tsx +++ b/src/equicordplugins/sekaiStickers/index.tsx @@ -34,7 +34,6 @@ export default definePlugin({ name: "SekaiStickers", description: "Sekai Stickers built in discord originally from github.com/TheOriginalAyaka", authors: [Devs.MaiKokain], - dependencies: ["ChatInputButtonAPI"], settings, chatBarButton: { icon: kanadeSvg, diff --git a/src/equicordplugins/showBadgesInChat/index.tsx b/src/equicordplugins/showBadgesInChat/index.tsx index ec23fde67..4d672e144 100644 --- a/src/equicordplugins/showBadgesInChat/index.tsx +++ b/src/equicordplugins/showBadgesInChat/index.tsx @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; import { Devs, EquicordDevs } from "@utils/constants"; import { isEquicordPluginDev, isPluginDev } from "@utils/misc"; import definePlugin from "@utils/types"; @@ -142,12 +141,8 @@ export default definePlugin({ name: "ShowBadgesInChat", authors: [Devs.Inbestigator, EquicordDevs.KrystalSkull], description: "Shows the message author's badges beside their name in chat.", - dependencies: ["MessageDecorationsAPI"], settings, - start: () => { - addMessageDecoration("vc-show-badges-in-chat", props => props.message?.author ? : null); - }, - stop: () => { - removeMessageDecoration("vc-show-badges-in-chat"); + renderMessageDecoration(props) { + return props.message?.author ? : null; } }); diff --git a/src/equicordplugins/signature/index.tsx b/src/equicordplugins/signature/index.tsx index 5eb1d6b22..7c5630650 100644 --- a/src/equicordplugins/signature/index.tsx +++ b/src/equicordplugins/signature/index.tsx @@ -7,7 +7,6 @@ import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -78,14 +77,6 @@ function SignatureIcon() { ); } -const handleMessage = ((channelId, msg) => { - if (!settings.store.isEnabled) { - msg.content = msg.content; - } else { - msg.content = textProcessing(msg.content); - } -}); - const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => { const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]); if (!contextMenu) return; @@ -110,16 +101,18 @@ export default definePlugin({ name: "Signature", description: "Automated fingerprint/end text", authors: [Devs.Ven, Devs.Rini, Devs.ImBanana, EquicordDevs.KrystalSkull], - dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], - + onBeforeMessageSend(channelId, msg) { + if (!settings.store.isEnabled) { + msg.content = msg.content; + } else { + msg.content = textProcessing(msg.content); + } + }, start: () => { if (settings.store.isEnabled) true; - addMessagePreSendListener(handleMessage); }, stop: () => { if (settings.store.isEnabled) false; - removeMessagePreSendListener(handleMessage); - }, chatBarButton: { diff --git a/src/equicordplugins/soggy/index.tsx b/src/equicordplugins/soggy/index.tsx index edd0361e2..caf1b4659 100644 --- a/src/equicordplugins/soggy/index.tsx +++ b/src/equicordplugins/soggy/index.tsx @@ -6,7 +6,6 @@ import { AudioPlayerInterface, createAudioPlayer } from "@api/AudioPlayer"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { EquicordDevs } from "@utils/constants"; import { ModalProps, ModalRoot, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; @@ -143,15 +142,7 @@ export default definePlugin({ authors: [EquicordDevs.sliwka], settings, dependencies: ["AudioPlayerAPI"], - patches: [ - { - find: ".controlButtonWrapper,", - replacement: { - match: /(function \i\(\i\){)(.{1,200}toolbar.{1,450}mobileToolbar)/, - replace: "$1$self.addIconToToolBar(arguments[0]);$2" - } - } - ], + renderHeaderBarButton: SoggyButton, start() { assignBoop(settings.store.boopLink, settings.store.boopVolume * 100); @@ -162,21 +153,4 @@ export default definePlugin({ boopSound?.delete(); song?.delete(); }, - - // taken from message logger lol - addIconToToolBar(e: { toolbar: React.ReactNode[] | React.ReactNode; }) { - if (Array.isArray(e.toolbar)) - return e.toolbar.unshift( - - - - ); - - e.toolbar = [ - - - , - e.toolbar, - ]; - }, }); diff --git a/src/equicordplugins/soundBoardLogger/index.tsx b/src/equicordplugins/soundBoardLogger/index.tsx index 41aa76180..5d95a2f2d 100644 --- a/src/equicordplugins/soundBoardLogger/index.tsx +++ b/src/equicordplugins/soundBoardLogger/index.tsx @@ -4,14 +4,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { disableStyle, enableStyle } from "@api/Styles"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin from "@utils/types"; import { FluxDispatcher } from "@webpack/common"; -import { CharBarButton, ChatBarIcon } from "./components/Icons"; import { OpenSBLogsButton } from "./components/SoundBoardLog"; import settings from "./settings"; import { updateLoggedSounds } from "./store"; @@ -22,33 +19,9 @@ export default definePlugin({ name: "SoundBoardLogger", authors: [Devs.Moxxie, EquicordDevs.Fres, Devs.amy, Devs.thororen], description: "Logs all soundboards that are played in a voice chat and allows you to download them", - dependencies: ["AudioPlayerAPI", "ChatInputButtonAPI"], - patches: [ - { - predicate: () => settings.store.IconLocation === "toolbar", - find: ".controlButtonWrapper,", - replacement: { - match: /(function \i\(\i\){)(.{1,500}toolbar.{1,500}mobileToolbar)/, - replace: "$1$self.addSBIconToToolBar(arguments[0]);$2" - } - } - ], + dependencies: ["AudioPlayerAPI"], settings, - addSBIconToToolBar(e: { toolbar: React.ReactNode[] | React.ReactNode; }) { - if (Array.isArray(e.toolbar)) - return e.toolbar.unshift( - - - - ); - - e.toolbar = [ - - - , - e.toolbar, - ]; - }, + renderHeaderBarButton: OpenSBLogsButton, start() { enableStyle(styles); FluxDispatcher.subscribe("VOICE_CHANNEL_EFFECT_SEND", async sound => { @@ -56,10 +29,8 @@ export default definePlugin({ await updateLoggedSounds(sound); getListeners().forEach(cb => cb()); }); - if (settings.store.IconLocation === "chat") addChatBarButton("vc-soundlog-button", CharBarButton, ChatBarIcon); }, stop() { disableStyle(styles); - if (settings.store.IconLocation === "chat") removeChatBarButton("vc-soundlog-button"); } }); diff --git a/src/equicordplugins/soundBoardLogger/settings.tsx b/src/equicordplugins/soundBoardLogger/settings.tsx index 6c9453077..75d211c5d 100644 --- a/src/equicordplugins/soundBoardLogger/settings.tsx +++ b/src/equicordplugins/soundBoardLogger/settings.tsx @@ -85,15 +85,6 @@ const settings = definePluginSettings({ default: 0.5, markers: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] }, - IconLocation: { - description: "choose where to show the SoundBoard Log icon (requires restart)", - type: OptionType.SELECT, - options: [ - { label: "Toolbar", value: "toolbar", default: true }, - { label: "Chat input", value: "chat" } - ], - restartNeeded: true - }, }); export default settings; diff --git a/src/equicordplugins/splitLargeMessages/index.ts b/src/equicordplugins/splitLargeMessages/index.ts index 50dadff56..0e52af7f4 100644 --- a/src/equicordplugins/splitLargeMessages/index.ts +++ b/src/equicordplugins/splitLargeMessages/index.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; +import { MessageSendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; import { getCurrentChannel, sendMessage } from "@utils/discord"; @@ -117,16 +117,11 @@ export default definePlugin({ name: "SplitLargeMessages", description: "Splits large messages into multiple to fit Discord's message limit.", authors: [EquicordDevs.Reycko], - dependencies: ["MessageEventsAPI"], settings, + onBeforeMessageSend: listener, start() { if (settings.store.maxLength === 0) maxLength = autoMaxLength(); - addMessagePreSendListener(listener); - }, - - stop() { - removeMessagePreSendListener(listener); }, patches: [ diff --git a/src/equicordplugins/talkInReverse/index.tsx b/src/equicordplugins/talkInReverse/index.tsx index de5bc0477..c469c9b31 100644 --- a/src/equicordplugins/talkInReverse/index.tsx +++ b/src/equicordplugins/talkInReverse/index.tsx @@ -66,7 +66,7 @@ export default definePlugin({ name: "TalkInReverse", authors: [EquicordDevs.Tolgchu], description: "Reverses the message content before sending it.", - dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], + dependencies: ["MessageEventsAPI"], chatBarButton: { icon: ReverseMessageIcon, render: ReverseMessageToggle diff --git a/src/equicordplugins/translatePlus/index.tsx b/src/equicordplugins/translatePlus/index.tsx index 2457d0877..78e48b120 100644 --- a/src/equicordplugins/translatePlus/index.tsx +++ b/src/equicordplugins/translatePlus/index.tsx @@ -46,7 +46,6 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => export default definePlugin({ name: "Translate+", description: "Vencord's translate plugin but with support for artistic languages!", - dependencies: ["MessageAccessoriesAPI"], authors: [Devs.Ven, EquicordDevs.Prince527], settings, contextMenus: { diff --git a/src/plugins/_api/headerBar.ts b/src/plugins/_api/headerBar.ts new file mode 100644 index 000000000..20af4ecb4 --- /dev/null +++ b/src/plugins/_api/headerBar.ts @@ -0,0 +1,24 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "HeaderBarAPI", + description: "API to add buttons to the header bar.", + authors: [EquicordDevs.Prism], + + patches: [ + { + find: '?"BACK_FORWARD_NAVIGATION":', + replacement: { + match: /(?<=\i\.Fragment,\{children:\[.{1,500}className:\i\}\))(?=\])/, + replace: ",...Vencord.Api.HeaderBar._addButtons()" + } + } + ] +}); diff --git a/src/plugins/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx index 9d1541dbc..17d44cd45 100644 --- a/src/plugins/platformIndicators/index.tsx +++ b/src/plugins/platformIndicators/index.tsx @@ -18,9 +18,6 @@ import "./style.css"; -import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators"; -import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; -import { addNicknameIcon, removeNicknameIcon } from "@api/NicknameIcons"; import { definePluginSettings, migratePluginSetting, Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { classes } from "@utils/misc"; @@ -192,49 +189,22 @@ const PlatformIndicator = ({ user, isProfile, isMessage, isMemberList }: Platfor ); }; -function toggleMemberListDecorators(enabled: boolean) { - if (enabled) { - addMemberListDecorator("PlatformIndicators", props => ); - } else { - removeMemberListDecorator("PlatformIndicators"); - } -} - -function toggleNicknameIcons(enabled: boolean) { - if (enabled) { - addNicknameIcon("PlatformIndicators", props => , 1); - } else { - removeNicknameIcon("PlatformIndicators"); - } -} - -function toggleMessageDecorators(enabled: boolean) { - if (enabled) { - addMessageDecoration("PlatformIndicators", props => ); - } else { - removeMessageDecoration("PlatformIndicators"); - } -} - migratePluginSetting("PlatformIndicators", "profiles", "badges"); const settings = definePluginSettings({ list: { type: OptionType.BOOLEAN, description: "Show indicators in the member list", default: true, - onChange: toggleMemberListDecorators }, profiles: { type: OptionType.BOOLEAN, description: "Show indicators in user profiles", default: true, - onChange: toggleNicknameIcons }, messages: { type: OptionType.BOOLEAN, description: "Show indicators inside messages", default: true, - onChange: toggleMessageDecorators }, colorMobileIndicator: { type: OptionType.BOOLEAN, @@ -278,19 +248,21 @@ export default definePlugin({ name: "PlatformIndicators", description: "Adds platform indicators (Desktop, Mobile, Web...) to users", authors: [Devs.kemo, Devs.TheSun, Devs.Nuckyz, Devs.Ven], - dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"], settings, - - start() { - if (settings.store.list) toggleMemberListDecorators(true); - if (settings.store.profiles) toggleNicknameIcons(true); - if (settings.store.messages) toggleMessageDecorators(true); + renderNicknameIcon(props) { + if (!settings.store.profiles) return null; + return ( + + ); }, + renderMemberListDecorator(props) { + if (!settings.store.list) return null; + return ; - stop() { - if (settings.store.list) toggleMemberListDecorators(false); - if (settings.store.profiles) toggleNicknameIcons; - if (settings.store.messages) toggleMessageDecorators(false); + }, + renderMessageDecoration(props) { + if (!settings.store.messages) return null; + return ; }, patches: [ diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index 4b0c41a8d..8637889ac 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -296,7 +296,6 @@ export default definePlugin({ name: "SilentTyping", authors: [Devs.Ven, Devs.Rini, Devs.ImBanana, EquicordDevs.Etorix], description: "Hide your typing indicator from chat.", - dependencies: ["ChatInputButtonAPI"], isModified: true, settings, diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx index 5f1a05e54..70c22cf59 100644 --- a/src/plugins/userVoiceShow/index.tsx +++ b/src/plugins/userVoiceShow/index.tsx @@ -18,9 +18,6 @@ import "./style.css"; -import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators"; -import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; -import { addNicknameIcon, removeNicknameIcon } from "@api/NicknameIcons"; import { definePluginSettings } from "@api/Settings"; import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -52,9 +49,22 @@ export default definePlugin({ name: "UserVoiceShow", description: "Shows an indicator when a user is in a Voice Channel", authors: [Devs.Nuckyz, Devs.LordElias, EquicordDevs.omaw], - dependencies: ["NicknameIconsAPI", "MemberListDecoratorsAPI", "MessageDecorationsAPI"], settings, + renderNicknameIcon({ userId }) { + if (!settings.store.showInUserProfileModal) return null; + return ( + + ); + }, + renderMemberListDecorator({ user }) { + if (!settings.store.showInMemberList) return null; + return user == null ? null : ; + }, + renderMessageDecoration({ message }) { + if (!settings.store.showInMessages) return null; + return message?.author == null ? null : ; + }, patches: [ // Friends List { @@ -67,23 +77,5 @@ export default definePlugin({ } ], - start() { - if (settings.store.showInUserProfileModal) { - addNicknameIcon("UserVoiceShow", ({ userId }) => ); - } - if (settings.store.showInMemberList) { - addMemberListDecorator("UserVoiceShow", ({ user }) => user == null ? null : ); - } - if (settings.store.showInMessages) { - addMessageDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : ); - } - }, - - stop() { - removeNicknameIcon("UserVoiceShow"); - removeMemberListDecorator("UserVoiceShow"); - removeMessageDecoration("UserVoiceShow"); - }, - VoiceChannelIndicator }); diff --git a/src/utils/types.ts b/src/utils/types.ts index a03ab10b5..14b6f8327 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -20,6 +20,7 @@ import { AudioProcessor } from "@api/AudioPlayer"; import type { ProfileBadge } from "@api/Badges"; import type { ChatBarButtonData, ChatBarButtonFactory } from "@api/ChatButtons"; import type { NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { HeaderBarButtonFactory } from "@api/HeaderBar"; import type { MemberListDecoratorFactory } from "@api/MemberListDecorators"; import type { MessageAccessoryFactory } from "@api/MessageAccessories"; import type { MessageDecorationFactory } from "@api/MessageDecorations"; @@ -199,7 +200,8 @@ export interface PluginDef { // Custom renderNicknameIcon?: NicknameIconFactory; - audioProcessor?: AudioProcessor; + renderHeaderBarButton?: HeaderBarButtonFactory; + onAudioProcessor?: AudioProcessor; // TODO: Remove eventually /**