feat(UserAreaAPI): ability to add buttons to user-area (#560)

* feat(UserAreaAPI): add UserArea button API

* fix supporthelper plugin card

* lint

* fix and revert

* fix horror
This commit is contained in:
prism 2025-12-14 19:30:56 +01:00 • committed by GitHub
parent 7202091567
commit 54718f88f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 238 additions and 125 deletions

View file

@ -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;

78
src/api/UserArea.tsx Normal file
View file

@ -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<UserAreaButtonProps>;
export interface UserAreaButtonProps {
icon: ReactNode;
tooltipText?: ReactNode;
onClick?: MouseEventHandler<HTMLDivElement>;
onContextMenu?: MouseEventHandler<HTMLDivElement>;
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<string, ButtonEntry>();
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 }]) => (
<ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render ${id}`, e.error)}>
<Button {...props} />
</ErrorBoundary>
))}
</>
);
}
export function _renderButtons(props: UserAreaRenderProps) {
return [<UserAreaButtons key="vc-user-area-buttons" props={props} />];
}

View file

@ -32,6 +32,7 @@ import * as $MessageUpdater from "./MessageUpdater";
import * as $NicknameIcons from "./NicknameIcons";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $UserArea from "./UserArea";
export * as PluginManager from "./PluginManager";
import * as $ServerList from "./ServerList";
import * as $Settings from "./Settings";
@ -160,6 +161,11 @@ export const NicknameIcons = $NicknameIcons;
*/
export const AudioPlayer = $AudioPlayer;
/**
* An API allowing you to add buttons to the user area panel.
*/
export const UserArea = $UserArea;
/**
* Just used to identify if user is on Equicord as Vencord doesn't have this.
*/

View file

@ -0,0 +1,36 @@
/*
* 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: "UserAreaAPI",
description: "API to add buttons to the user area panel.",
authors: [EquicordDevs.Prism],
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: {
match: /(className:(\i)\.buttons,.{0,50}?children:\[)/,
replace: "$1...$self.renderButtons(arguments[0],$2),"
}
}
],
renderButtons(props: { nameplate?: any; }, styles: { iconForeground?: string; }) {
return Vencord.Api.UserArea._renderButtons({
nameplate: props.nameplate,
iconForeground: props.nameplate != null ? styles.iconForeground : void 0,
hideTooltips: this.shouldHideTooltips()
});
},
shouldHideTooltips() {
return Vencord.Plugins.isPluginEnabled("BetterUserArea") && Vencord.Plugins.plugins.BetterUserArea.settings?.store?.removeButtonTooltips;
}
});

View file

@ -0,0 +1,17 @@
.vc-plugins-management-cards.vc-plugins-grid {
display: flex;
flex-wrap: wrap;
}
.vc-plugins-management-cards .vc-addon-card {
width: auto;
}
.vc-plugins-management-cards .vc-addon-title-container {
width: max-content;
}
.vc-plugins-management-cards .vc-addon-title {
position: static;
overflow: visible;
}

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./pluginCards.css";
import { isPluginEnabled, isPluginRequired } from "@api/PluginManager";
import { useSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";

View file

@ -4,19 +4,17 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { UserAreaButton, UserAreaRenderProps } from "@api/UserArea";
import { debounce } from "@shared/debounce";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { makeRange, OptionType } from "@utils/types";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
import { ChannelActions, ChannelRouter, ChannelStore, ContextMenuApi, GuildStore, MediaEngineStore, Menu, PermissionsBits, PermissionStore, React, SelectedChannelStore, Toasts, UserStore, VoiceActions, VoiceStateStore } from "@webpack/common";
import style from "./styles.css?managed";
const ChatVoiceIcon = findComponentByCodeLazy("0l1.8-1.8c.17");
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
const startStream = findByCodeLazy('type:"STREAM_START"');
const getDesktopSources = findByCodeLazy("desktop sources");
@ -160,21 +158,28 @@ interface VoiceState {
requestToSpeakTimestamp: string | null;
}
function RandomVoiceIcon({ className }: { className?: string; }) {
return (
<svg className={className} width="18" height="18" viewBox="0 0 24 24">
<g fill="currentColor">
<path d="M19,9H14a5.006,5.006,0,0,0-5,5v5a5.006,5.006,0,0,0,5,5h5a5.006,5.006,0,0,0,5-5V14A5.006,5.006,0,0,0,19,9Zm-5,6a1,1,0,1,1,1-1A1,1,0,0,1,14,15Zm5,5a1,1,0,1,1,1-1A1,1,0,0,1,19,20ZM15.6,5,12.069,1.462A5.006,5.006,0,0,0,5,1.462L1.462,5a5.006,5.006,0,0,0,0,7.071L5,15.6a4.961,4.961,0,0,0,2,1.223V14a7.008,7.008,0,0,1,7-7h2.827A4.961,4.961,0,0,0,15.6,5ZM5,10A1,1,0,1,1,6,9,1,1,0,0,1,5,10ZM9,6a1,1,0,1,1,1-1A1,1,0,0,1,9,6Z" />
</g>
</svg>
);
}
export default definePlugin({
name: "RandomVoice",
description: "Adds a Button near the Mute button to join a random voice call.",
authors: [EquicordDevs.xijexo, EquicordDevs.omaw, Devs.thororen],
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: {
match: /className:\i\.buttons,.{0,60}children:\[/,
replace: "$&$self.randomVoice(),"
}
}
],
userAreaButton: {
icon: RandomVoiceIcon,
render: RandomVoiceButton
},
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
VOICE_STATE_UPDATES() {
const currentUserId = UserStore.getCurrentUser().id;
const myChannelId = VoiceStateStore.getVoiceStateForUser(currentUserId)?.channelId;
if (!myChannelId || !settings.store.leaveEmpty) return;
@ -185,40 +190,24 @@ export default definePlugin({
);
if (othersInChannel.length === 0) {
randomVoice();
getChannels();
}
},
},
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
},
settings,
randomVoice: ErrorBoundary.wrap(randomVoice, { noop: true }),
});
function randomVoice() {
function RandomVoiceButton({ iconForeground, hideTooltips, nameplate }: UserAreaRenderProps) {
return (
<>
<Button
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <ContextMenu />)}
onClick={() => getChannels()}
role="switch"
tooltipText={"Random Voice"}
icon={<svg
width="18"
height="18"
id="test"
viewBox="0 0 24 24"
>
<g fill={"currentColor"}>
<path d="M19,9H14a5.006,5.006,0,0,0-5,5v5a5.006,5.006,0,0,0,5,5h5a5.006,5.006,0,0,0,5-5V14A5.006,5.006,0,0,0,19,9Zm-5,6a1,1,0,1,1,1-1A1,1,0,0,1,14,15Zm5,5a1,1,0,1,1,1-1A1,1,0,0,1,19,20ZM15.6,5,12.069,1.462A5.006,5.006,0,0,0,5,1.462L1.462,5a5.006,5.006,0,0,0,0,7.071L5,15.6a4.961,4.961,0,0,0,2,1.223V14a7.008,7.008,0,0,1,7-7h2.827A4.961,4.961,0,0,0,15.6,5ZM5,10A1,1,0,1,1,6,9,1,1,0,0,1,5,10ZM9,6a1,1,0,1,1,1-1A1,1,0,0,1,9,6Z" />
</g>
</svg>}
/>
</>
<UserAreaButton
onContextMenu={e => ContextMenuApi.openContextMenu(e, () => <ContextMenu />)}
onClick={() => getChannels()}
role="switch"
tooltipText={hideTooltips ? void 0 : "Random Voice"}
icon={<RandomVoiceIcon className={iconForeground} />}
plated={nameplate != null}
/>
);
}
@ -320,8 +309,7 @@ function ContextMenu() {
height={"22"}
viewBox={"0 0 26 26"}
>
<g fill={"#b5bac1"}
>
<g fill="currentColor">
<path d="M12,2a10.032,10.032,0,0,1,7.122,3H16a1,1,0,0,0-1,1h0a1,1,0,0,0,1,1h4.143A1.858,1.858,0,0,0,22,5.143V1a1,1,0,0,0-1-1h0a1,1,0,0,0-1,1V3.078A11.981,11.981,0,0,0,.05,10.9a1.007,1.007,0,0,0,1,1.1h0a.982.982,0,0,0,.989-.878A10.014,10.014,0,0,1,12,2Z" /><path d="M22.951,12a.982.982,0,0,0-.989.878A9.986,9.986,0,0,1,4.878,19H8a1,1,0,0,0,1-1H9a1,1,0,0,0-1-1H3.857A1.856,1.856,0,0,0,2,18.857V23a1,1,0,0,0,1,1H3a1,1,0,0,0,1-1V20.922A11.981,11.981,0,0,0,23.95,13.1a1.007,1.007,0,0,0-1-1.1Z" />
</g>
@ -589,7 +577,6 @@ function ContextMenu() {
>
<Menu.MenuItem id="voiceOptions" label="Voice Options" action={() => { }} >
<>
{ }
<Menu.MenuCheckboxItem
key="selfMute"
id="selfMute"

View file

@ -1,8 +1,8 @@
[class^="buttons__"] > [class^="buttons__"]:not([class^="expression-picker"]) {
[class^="buttons__"]>[class^="buttons__"]:not([class^="expression-picker"]) {
width: 150px;
margin-left: -15px;
}
[class^="micButtonParent__"] {
width: 16px;
}
}

View file

@ -6,24 +6,23 @@
import { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { UserAreaButton, UserAreaRenderProps } from "@api/UserArea";
import equicordToolbox from "@equicordplugins/equicordToolbox";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Constants, Menu, React, RestAPI, useEffect, useState } from "@webpack/common";
import { Constants, Menu, RestAPI, useEffect, useState } from "@webpack/common";
let spotifyId: string | null = null;
let showActivity: boolean = false;
let showActivity = false;
let isLoaded = false;
let forceUpdate: (() => void) | null = null;
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
function makeSpotifyIcon(enabled: boolean) {
function SpotifyIcon({ className, enabled }: { className?: string; enabled: boolean; }) {
const redLinePath = "M48.54 4.54a2.2 2.2 0 0 0-3.08-3.08l-44 44a2.2 2.2 0 1 0 3.08 3.08Z";
const maskBlackPath = "M50.376 8.248 41.752-.376-.376 41.752 8.248 50.376Z";
return (
<svg width="20" height="20" viewBox="0 0 50 50">
<svg className={className} width="20" height="20" viewBox="0 0 50 50">
<path
fill={!enabled ? "var(--status-danger)" : "currentColor"}
mask={!enabled ? "url(#spotifyActivityMask)" : void 0}
@ -40,34 +39,32 @@ function makeSpotifyIcon(enabled: boolean) {
);
}
function SpotifyActivityToggleButton(props: { nameplate?: any; }) {
function SpotifyActivityToggleButton({ iconForeground, hideTooltips, nameplate }: UserAreaRenderProps) {
const { location } = settings.use(["location"]);
const [showActivityState, setShowActivityState] = useState(showActivity);
const [, rerender] = useState(0);
useEffect(() => {
setShowActivityState(showActivity);
forceUpdate = () => rerender(n => n + 1);
return () => { forceUpdate = null; };
}, []);
if (showActivity === null) return null;
if (!spotifyId || location !== "PANEL" && isPluginEnabled(equicordToolbox.name)) return null;
if (!isLoaded || !spotifyId || location !== "PANEL" && isPluginEnabled(equicordToolbox.name)) return null;
return (
<Button
tooltipText={showActivityState ? "Turn off Spotify activity" : "Turn on Spotify activity"}
icon={makeSpotifyIcon(showActivityState)}
<UserAreaButton
tooltipText={hideTooltips ? void 0 : showActivity ? "Turn off Spotify activity" : "Turn on Spotify activity"}
icon={<SpotifyIcon className={iconForeground} enabled={showActivity} />}
role="switch"
aria-checked={showActivityState}
redGlow={!showActivityState}
plated={props?.nameplate != null}
aria-checked={showActivity}
redGlow={!showActivity}
plated={nameplate != null}
onClick={async () => {
const newValue = !showActivityState;
setShowActivityState(newValue);
showActivity = !showActivity;
forceUpdate?.();
await RestAPI.patch({
url: Constants.Endpoints.CONNECTION("spotify", spotifyId),
body: {
show_activity: newValue,
},
body: { show_activity: showActivity },
});
}}
/>
@ -77,7 +74,7 @@ function SpotifyActivityToggleButton(props: { nameplate?: any; }) {
const settings = definePluginSettings({
location: {
type: OptionType.SELECT,
description: "Where to show the spotify toggle button",
description: "Where to show the Spotify toggle button.",
options: [
{ label: "Next to Mute/Deafen", value: "PANEL", default: true },
{ label: "Equicord Toolbox", value: "TOOLBOX" }
@ -93,15 +90,12 @@ export default definePlugin({
description: "Adds a toggle button for Spotify activity visibility.",
authors: [Devs.thororen],
settings,
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: {
match: /className:\i\.buttons,.{0,50}children:\[/,
replace: "$&$self.SpotifyActivityToggleButton(arguments[0]),"
}
}
],
userAreaButton: {
icon: (props: { className?: string; }) => <SpotifyIcon {...props} enabled={showActivity} />,
render: SpotifyActivityToggleButton
},
toolboxActions() {
const { location } = settings.use(["location"]);
@ -112,28 +106,29 @@ export default definePlugin({
id="spotify-activity-toggle-toolbox"
label={showActivity ? "Disable Spotify Activity" : "Enable Spotify Activity"}
action={async () => {
const newValue = !showActivity;
showActivity = newValue;
showActivity = !showActivity;
await RestAPI.patch({
url: Constants.Endpoints.CONNECTION("spotify", spotifyId),
body: { show_activity: newValue },
body: { show_activity: showActivity },
});
}}
/>
);
},
async start() {
const { body } = await RestAPI.get({
url: Constants.Endpoints.CONNECTIONS,
});
if (!body) return;
const spotifyConn = body.find(c => c.type === "spotify");
if (spotifyConn) {
spotifyId = spotifyConn.id;
showActivity = spotifyConn.show_activity;
flux: {
async CONNECTION_OPEN() {
const { body } = await RestAPI.get({ url: Constants.Endpoints.CONNECTIONS });
if (!body) return;
const spotifyConn = body.find((c: { type: string; }) => c.type === "spotify");
if (spotifyConn) {
spotifyId = spotifyConn.id;
showActivity = spotifyConn.show_activity;
}
isLoaded = true;
forceUpdate?.();
}
},
SpotifyActivityToggleButton: ErrorBoundary.wrap(SpotifyActivityToggleButton, { noop: true }),
});

View file

@ -16,20 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings } from "@api/Settings";
import { UserAreaButton, UserAreaRenderProps } from "@api/UserArea";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
import equicordToolbox from "@equicordplugins/equicordToolbox";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Menu } from "@webpack/common";
import managedStyle from "./style.css?managed";
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
const settings = definePluginSettings({
@ -51,11 +48,10 @@ const settings = definePluginSettings({
}
});
function Icon() {
function Icon({ className }: { className?: string; }) {
const { oldIcon } = settings.use(["oldIcon"]);
const showCurrentGame = ShowCurrentGame.useSetting();
const redLinePath = !oldIcon
? "M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4Z"
: "M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z";
@ -65,7 +61,7 @@ function Icon() {
: "M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z";
return (
<svg width="20" height="20" viewBox="0 0 24 24">
<svg className={className} width="20" height="20" viewBox="0 0 24 24">
<path
fill={!showCurrentGame && !oldIcon ? "var(--status-danger)" : "currentColor"}
mask={!showCurrentGame ? "url(#gameActivityMask)" : void 0}
@ -82,21 +78,20 @@ function Icon() {
);
}
function GameActivityToggleButton(props: { nameplate?: any; }) {
function GameActivityToggleButton({ iconForeground, hideTooltips, nameplate }: UserAreaRenderProps) {
const { location } = settings.use(["location"]);
const showCurrentGame = ShowCurrentGame.useSetting();
if (location !== "PANEL" && isPluginEnabled(equicordToolbox.name)) return null;
return (
<Button
className="vc-game-activity"
tooltipText={showCurrentGame ? "Disable Game Activity" : "Enable Game Activity"}
icon={Icon}
<UserAreaButton
tooltipText={hideTooltips ? void 0 : showCurrentGame ? "Disable Game Activity" : "Enable Game Activity"}
icon={<Icon className={iconForeground} />}
role="switch"
aria-checked={!showCurrentGame}
redGlow={!showCurrentGame}
plated={props?.nameplate != null}
plated={nameplate != null}
onClick={() => ShowCurrentGame.updateSetting(old => !old)}
/>
);
@ -109,17 +104,10 @@ export default definePlugin({
dependencies: ["UserSettingsAPI"],
settings,
managedStyle,
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
replacement: {
match: /className:\i\.buttons,.{0,50}children:\[/,
replace: "$&$self.GameActivityToggleButton(arguments[0]),"
}
}
],
userAreaButton: {
icon: Icon,
render: GameActivityToggleButton
},
toolboxActions() {
const { location } = settings.use(["location"]);
@ -136,6 +124,4 @@ export default definePlugin({
/>
);
},
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
});

View file

@ -1,3 +1,3 @@
[class^="panels"] [class^="avatarWrapper_"] {
min-width: 0;
}
}

View file

@ -27,6 +27,7 @@ import type { MessageDecorationFactory } from "@api/MessageDecorations";
import type { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents";
import type { MessagePopoverButtonData, MessagePopoverButtonFactory } from "@api/MessagePopover";
import type { NicknameIconFactory } from "@api/NicknameIcons";
import type { UserAreaButtonData } from "@api/UserArea";
import type { Command, FluxEvents } from "@vencord/discord-types";
import type { ReactNode } from "react";
import type { LiteralUnion } from "type-fest";
@ -206,6 +207,7 @@ export interface PluginDef {
renderNicknameIcon?: NicknameIconFactory;
headerBarButton?: HeaderBarButtonData;
audioProcessor?: AudioProcessor;
userAreaButton?: UserAreaButtonData;
// TODO: Remove eventually
/**