diff --git a/src/api/PluginManager.ts b/src/api/PluginManager.ts index 4b42aae11..c3332f856 100644 --- a/src/api/PluginManager.ts +++ b/src/api/PluginManager.ts @@ -42,6 +42,7 @@ export { Plugins as plugins }; import { traceFunction } from "../debug/Tracer"; import { addAudioProcessor, removeAudioProcessor } from "./AudioPlayer"; import { addChannelToolbarButton, addHeaderBarButton, removeChannelToolbarButton, removeHeaderBarButton } from "./HeaderBar"; +import { addUserAreaButton, removeUserAreaButton } from "./UserArea"; const logger = new Logger("PluginManager", "#a6d189"); @@ -195,7 +196,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: onBeforeMessageEdit, onBeforeMessageSend, onMessageClick, renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton, // Custom - renderNicknameIcon, headerBarButton, audioProcessor + renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton } = p; if (p.start) { @@ -265,6 +266,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } } if (audioProcessor) addAudioProcessor(name, audioProcessor); + if (userAreaButton) addUserAreaButton(name, userAreaButton.render, userAreaButton.priority); return true; }, p => `startPlugin ${p.name}`); @@ -275,7 +277,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu onBeforeMessageEdit, onBeforeMessageSend, onMessageClick, renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton, // Custom - renderNicknameIcon, headerBarButton, audioProcessor + renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton } = p; if (p.stop) { @@ -339,6 +341,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } } if (audioProcessor) removeAudioProcessor(name); + if (userAreaButton) removeUserAreaButton(name); return true; }, p => `stopPlugin ${p.name}`); @@ -392,6 +395,7 @@ export const initPluginManager = onlyOnce(function init() { if (p.renderNicknameIcon) neededApiPlugins.add("NicknameIconsAPI"); if (p.headerBarButton) neededApiPlugins.add("HeaderBarAPI"); if (p.audioProcessor) neededApiPlugins.add("AudioPlayerAPI"); + if (p.userAreaButton) neededApiPlugins.add("UserAreaAPI"); for (const key of pluginKeysToBind) { p[key] &&= p[key].bind(p) as any; diff --git a/src/api/UserArea.tsx b/src/api/UserArea.tsx new file mode 100644 index 000000000..4701d8de8 --- /dev/null +++ b/src/api/UserArea.tsx @@ -0,0 +1,78 @@ +/* + * 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 { Logger } from "@utils/Logger"; +import { findComponentByCodeLazy } from "@webpack"; +import type { ComponentType, MouseEventHandler, ReactNode } from "react"; + +const PanelButton = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON") as ComponentType; + +export interface UserAreaButtonProps { + icon: ReactNode; + tooltipText?: ReactNode; + onClick?: MouseEventHandler; + onContextMenu?: MouseEventHandler; + className?: string; + role?: string; + "aria-label"?: string; + "aria-checked"?: boolean; + disabled?: boolean; + plated?: boolean; + redGlow?: boolean; + orangeGlow?: boolean; +} + +export interface UserAreaRenderProps { + nameplate?: any; + iconForeground?: string; + hideTooltips?: boolean; +} + +export type UserAreaButtonFactory = (props: UserAreaRenderProps) => ReactNode; + +export interface UserAreaButtonData { + render: UserAreaButtonFactory; + icon: ComponentType<{ className?: string; }>; + priority?: number; +} + +interface ButtonEntry { + render: UserAreaButtonFactory; + priority: number; +} + +export const UserAreaButton = PanelButton; + +const logger = new Logger("UserArea"); + +export const buttons = new Map(); + +export function addUserAreaButton(id: string, render: UserAreaButtonFactory, priority = 0) { + buttons.set(id, { render, priority }); +} + +export function removeUserAreaButton(id: string) { + buttons.delete(id); +} + +function UserAreaButtons({ props }: { props: UserAreaRenderProps; }) { + return ( + <> + {Array.from(buttons) + .sort(([, a], [, b]) => a.priority - b.priority) + .map(([id, { render: Button }]) => ( + logger.error(`Failed to render ${id}`, e.error)}> +