Add the option to toggle plugin chatbar & message hover buttons (#3693)

This commit is contained in:
V 2025-11-21 13:53:30 +01:00 • committed by GitHub
parent 52e76a9cfc
commit eb5a42dd87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 586 additions and 209 deletions

View file

@ -9,11 +9,15 @@ import "./ChatButton.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
import { IconComponent } from "@utils/types";
import { Channel } from "@vencord/discord-types";
import { waitFor } from "@webpack";
import { ButtonWrapperClasses, Clickable, Tooltip } from "@webpack/common";
import { ButtonWrapperClasses, Clickable, Menu, Tooltip } from "@webpack/common";
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
import { addContextMenuPatch, findGroupChildrenByChildId } from "./ContextMenu";
import { useSettings } from "./Settings";
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;
waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m);
@ -76,26 +80,51 @@ export interface ChatBarProps {
}
export type ChatBarButtonFactory = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null;
export type ChatBarButtonData = {
render: ChatBarButtonFactory;
/**
* This icon is used only for Settings UI. Your render function must still render an icon,
* and it can be different from this one.
*/
icon: IconComponent;
};
const buttonFactories = new Map<string, ChatBarButtonFactory>();
/**
* Don't use this directly, use {@link addChatBarButton} and {@link removeChatBarButton} instead.
*/
export const ChatBarButtonMap = new Map<string, ChatBarButtonData>();
const logger = new Logger("ChatButtons");
function VencordChatBarButtons(props: ChatBarProps) {
const { chatBarButtons } = useSettings(["uiElements.chatBarButtons.*"]).uiElements;
return (
<>
{Array.from(ChatBarButtonMap)
.filter(([key]) => chatBarButtons[key]?.enabled !== false)
.map(([key, { render: Button }]) => (
<ErrorBoundary noop key={key} onError={e => logger.error(`Failed to render ${key}`, e.error)}>
<Button {...props} isMainChat={props.type.analyticsName === "normal"} />
</ErrorBoundary>
))}
</>
);
}
export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) {
if (props.disabled) return;
for (const [key, Button] of buttonFactories) {
buttons.unshift(
<ErrorBoundary noop key={key} onError={e => logger.error(`Failed to render ${key}`, e.error)}>
<Button {...props} isMainChat={props.type.analyticsName === "normal"} />
</ErrorBoundary>
);
}
buttons.unshift(<VencordChatBarButtons key="vencord-chat-buttons" {...props} />);
return buttons;
}
export const addChatBarButton = (id: string, button: ChatBarButtonFactory) => buttonFactories.set(id, button);
export const removeChatBarButton = (id: string) => buttonFactories.delete(id);
/**
* The icon argument is used only for Settings UI. Your render function must still render an icon,
* and it can be different from this one.
*/
export const addChatBarButton = (id: string, render: ChatBarButtonFactory, icon: IconComponent) => ChatBarButtonMap.set(id, { render, icon });
export const removeChatBarButton = (id: string) => ChatBarButtonMap.delete(id);
export interface ChatBarButtonProps {
children: ReactNode;
@ -105,6 +134,7 @@ export interface ChatBarButtonProps {
onAuxClick?: MouseEventHandler;
buttonProps?: Omit<HTMLProps<HTMLDivElement>, "size" | "onClick" | "onContextMenu" | "onAuxClick">;
}
export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
return (
<Tooltip text={props.tooltip}>
@ -129,3 +159,35 @@ export const ChatBarButton = ErrorBoundary.wrap((props: ChatBarButtonProps) => {
</Tooltip>
);
}, { noop: true });
addContextMenuPatch("textarea-context", (children, args) => {
const { chatBarButtons } = useSettings(["uiElements.chatBarButtons.*"]).uiElements;
const buttons = Array.from(ChatBarButtonMap.entries());
if (!buttons.length) return;
const group = findGroupChildrenByChildId("submit-button", children);
if (!group) return;
const idx = group.findIndex(c => c?.props?.id === "submit-button");
if (idx === -1) return;
group.splice(idx, 0,
<Menu.MenuItem id="vc-chat-buttons" key="vencord-chat-buttons" label="Vencord Buttons">
{buttons.map(([id]) => (
<Menu.MenuCheckboxItem
label={id}
key={id}
id={`vc-chat-button-${id}`}
checked={chatBarButtons[id]?.enabled !== false}
action={() => {
const wasEnabled = chatBarButtons[id]?.enabled !== false;
chatBarButtons[id] ??= {} as any;
chatBarButtons[id].enabled = !wasEnabled;
}}
/>
))}
</Menu.MenuItem>
);
});

View file

@ -18,9 +18,12 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { IconComponent } from "@utils/types";
import { Channel, Message } from "@vencord/discord-types";
import type { ComponentType, MouseEventHandler } from "react";
import { useSettings } from "./Settings";
const logger = new Logger("MessagePopover");
export interface MessagePopoverButtonItem {
@ -34,41 +37,63 @@ export interface MessagePopoverButtonItem {
}
export type MessagePopoverButtonFactory = (message: Message) => MessagePopoverButtonItem | null;
export type MessagePopoverButtonData = {
render: MessagePopoverButtonFactory;
/**
* This icon is used only for Settings UI. Your render function must still return an icon,
* and it can be different from this one.
*/
icon: IconComponent;
};
export const buttons = new Map<string, MessagePopoverButtonFactory>();
export const MessagePopoverButtonMap = new Map<string, MessagePopoverButtonData>();
/**
* The icon argument is used only for Settings UI. Your render function must still return an icon,
* and it can be different from this one.
*/
export function addMessagePopoverButton(
identifier: string,
item: MessagePopoverButtonFactory,
render: MessagePopoverButtonFactory,
icon: IconComponent
) {
buttons.set(identifier, item);
MessagePopoverButtonMap.set(identifier, { render, icon });
}
export function removeMessagePopoverButton(identifier: string) {
buttons.delete(identifier);
MessagePopoverButtonMap.delete(identifier);
}
function VencordPopoverButtons(props: { Component: React.ComponentType<MessagePopoverButtonItem>, message: Message; }) {
const { Component, message } = props;
const { messagePopoverButtons } = useSettings(["uiElements.messagePopoverButtons.*"]).uiElements;
const elements = Array.from(MessagePopoverButtonMap.entries())
.filter(([key]) => messagePopoverButtons[key]?.enabled !== false)
.map(([key, { render }]) => {
try {
// FIXME: this should use proper React to ensure hooks work
const item = render(message);
if (!item) return null;
return (
<ErrorBoundary noop>
<Component key={key} {...item} />
</ErrorBoundary>
);
} catch (err) {
logger.error(`[${key}]`, err);
return null;
}
});
return <>{elements}</>;
}
export function _buildPopoverElements(
Component: React.ComponentType<MessagePopoverButtonItem>,
message: Message
) {
const items: React.ReactNode[] = [];
for (const [identifier, getItem] of buttons.entries()) {
try {
const item = getItem(message);
if (item) {
item.key ??= identifier;
items.push(
<ErrorBoundary noop>
<Component {...item} />
</ErrorBoundary>
);
}
} catch (err) {
logger.error(`[${identifier}]`, err);
}
}
return <>{items}</>;
return <VencordPopoverButtons Component={Component} message={message} />;
}

View file

@ -179,7 +179,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
const {
name, commands, contextMenus, managedStyle, userProfileBadge,
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton
} = p;
if (p.start) {
@ -229,11 +229,15 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
if (onBeforeMessageSend) addMessagePreSendListener(onBeforeMessageSend);
if (onMessageClick) addMessageClickListener(onMessageClick);
if (renderChatBarButton) addChatBarButton(name, renderChatBarButton);
if (chatBarButton) addChatBarButton(name, chatBarButton.render, chatBarButton.icon);
// @ts-expect-error: legacy code doesn't have icon
else 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);
if (messagePopoverButton) addMessagePopoverButton(name, messagePopoverButton.render, messagePopoverButton.icon);
// @ts-expect-error: legacy code doesn't have icon
else if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton);
return true;
}, p => `startPlugin ${p.name}`);
@ -242,7 +246,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
const {
name, commands, contextMenus, managedStyle, userProfileBadge,
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton
} = p;
if (p.stop) {
@ -290,11 +294,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
if (onBeforeMessageSend) removeMessagePreSendListener(onBeforeMessageSend);
if (onMessageClick) removeMessageClickListener(onMessageClick);
if (renderChatBarButton) removeChatBarButton(name);
if (chatBarButton || renderChatBarButton) removeChatBarButton(name);
if (renderMemberListDecorator) removeMemberListDecorator(name);
if (renderMessageDecoration) removeMessageDecoration(name);
if (renderMessageAccessory) removeMessageAccessory(name);
if (renderMessagePopoverButton) removeMessagePopoverButton(name);
if (messagePopoverButton || renderMessagePopoverButton) removeMessagePopoverButton(name);
return true;
}, p => `stopPlugin ${p.name}`);
@ -335,11 +339,11 @@ export const initPluginManager = onlyOnce(function init() {
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.chatBarButton || 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.messagePopoverButton || p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI");
if (p.userProfileBadge) neededApiPlugins.add("BadgeAPI");
for (const key of pluginKeysToBind) {

View file

@ -25,6 +25,18 @@ import { React, useEffect } from "@webpack/common";
import plugins from "~plugins";
const logger = new Logger("Settings");
export interface SettingsPluginUiElement {
enabled: boolean;
// TODO
/** not implemented for now */
order?: number;
}
export type SettingsPluginUiElements = {
/** id will be whatever id the element was registered with. Usually, but not always, the plugin name */
[id: string]: SettingsPluginUiElement;
};
export interface Settings {
autoUpdate: boolean;
autoUpdateNotification: boolean,
@ -59,6 +71,11 @@ export interface Settings {
};
};
uiElements: {
messagePopoverButtons: SettingsPluginUiElements;
chatBarButtons: SettingsPluginUiElements;
},
notifications: {
timeout: number;
position: "top-right" | "bottom-right";
@ -90,6 +107,11 @@ const DefaultSettings: Settings = {
winNativeTitleBar: false,
plugins: {},
uiElements: {
chatBarButtons: {},
messagePopoverButtons: {}
},
notifications: {
timeout: 5000,
position: "bottom-right",

View file

@ -430,3 +430,17 @@ export function WebsiteIcon(props: IconProps) {
</Icon>
);
}
/**
* A question mark inside a square, used as a placeholder icon when no other icon is available
*/
export function PlaceholderIcon(props: IconProps) {
return (
<Icon
{...props}
viewBox="0 0 24 24"
>
<path fill={props.fill || "currentColor"} fillRule="evenodd" d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" clipRule="evenodd" />
</Icon>
);
}

View file

@ -0,0 +1,42 @@
.vc-plugin-ui-elements-button {
display: flex;
align-items: center;
color: var(--interactive-normal);
margin-top: 0.5em;
cursor: pointer;
&:hover {
background-color: var(--background-modifier-hover);
color: var(--interactive-hover);
}
}
.vc-plugin-ui-elements-button-arrow {
margin-left: auto;
width: 24px;
height: 24px;
}
.vc-plugin-ui-elements-modal-content {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1.5em;
}
.vc-plugin-ui-elements-switches {
display: flex;
flex-direction: column;
gap: 1em;
}
.vc-plugin-ui-elements-switches-row {
display: flex;
gap: 1em;
width: 100%;
align-items: center;
:last-child {
margin-left: auto;
}
}

View file

@ -0,0 +1,107 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./UIElements.css";
import { ChatBarButtonMap } from "@api/ChatButtons";
import { MessagePopoverButtonMap } from "@api/MessagePopover";
import { SettingsPluginUiElements, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { BaseText } from "@components/BaseText";
import { Card } from "@components/Card";
import { PlaceholderIcon } from "@components/Icons";
import { Paragraph } from "@components/Paragraph";
import { Switch } from "@components/Switch";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { IconComponent } from "@utils/types";
import { Clickable } from "@webpack/common";
const cl = classNameFactory("vc-plugin-ui-elements-");
export function UIElementsButton() {
return (
<Clickable onClick={() => openModal(modalProps => <UIElementsModal {...modalProps} />)}>
<Card className={cl("button")} defaultPadding>
<div className={cl("button-description")}>
<Paragraph size="md" weight="semibold">
Manage plugin UI elements
</Paragraph>
<Paragraph size="xs">
Allows you to hide buttons you don't like
</Paragraph>
</div>
<svg
className={cl("button-arrow")}
aria-hidden="true"
viewBox="0 0 24 24"
>
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z" />
</svg>
</Card>
</Clickable >
);
}
function Section(props: {
title: string;
description: string;
settings: SettingsPluginUiElements;
buttonMap: Map<string, { icon: IconComponent; }>;
}) {
const { buttonMap, description, title, settings } = props;
return (
<section>
<BaseText tag="h3" size="xl" weight="bold">{title}</BaseText>
<Paragraph size="sm" className={classes(Margins.top8, Margins.bottom20)}>{description}</Paragraph>
<div className={cl("switches")}>
{Array.from(buttonMap, ([name, { icon }]) => {
const Icon = icon ?? PlaceholderIcon;
return (
<Paragraph size="md" weight="semibold" key={name} className={cl("switches-row")}>
<Icon height={20} width={20} />
{name}
<Switch
checked={settings[name]?.enabled ?? true}
onChange={v => {
settings[name] ??= {} as any;
settings[name].enabled = v;
}}
/>
</Paragraph>
);
})}
</div>
</section>
);
}
function UIElementsModal(props: ModalProps) {
const { uiElements } = useSettings(["uiElements.*"]);
return (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalContent className={cl("modal-content")}>
<Section
title="Chatbar Buttons"
description="These are the buttons on the right side of the chat input bar"
buttonMap={ChatBarButtonMap}
settings={uiElements.chatBarButtons}
/>
<Section
title="Message Popover Buttons"
description="These are the floating buttons on the right when you hover over a message"
buttonMap={MessagePopoverButtonMap}
settings={uiElements.messagePopoverButtons}
/>
</ModalContent>
</ModalRoot>
);
}

View file

@ -40,6 +40,7 @@ import { JSX } from "react";
import Plugins, { ExcludedPlugins, PluginMeta } from "~plugins";
import { PluginCard } from "./PluginCard";
import { UIElementsButton } from "./UIElements";
export const cl = classNameFactory("vc-plugins-");
export const logger = new Logger("PluginSettings", "#a6d189");
@ -260,6 +261,8 @@ function PluginSettings() {
<SettingsTab title="Plugins">
<ReloadRequiredCard required={changes.hasChanges} />
<UIElementsButton />
<HeadingTertiary className={classes(Margins.top20, Margins.bottom8)}>
Filters
</HeadingTertiary>

View file

@ -43,6 +43,15 @@ migratePluginSettings("HideMedia", "HideAttachments");
const hasMedia = (msg: Message) => msg.attachments.length > 0 || msg.embeds.length > 0 || msg.stickerItems.length > 0;
async function toggleHide(channelId: string, messageId: string) {
const ids = await getHiddenMessages();
if (!ids.delete(messageId))
ids.add(messageId);
await saveHiddenMessages(ids);
updateMessage(channelId, messageId);
}
export default definePlugin({
name: "HideMedia",
description: "Hide attachments and embeds for individual messages via hover button",
@ -57,18 +66,21 @@ export default definePlugin({
}
}],
renderMessagePopoverButton(msg) {
if (!hasMedia(msg) && !msg.messageSnapshots.some(s => hasMedia(s.message))) return null;
messagePopoverButton: {
icon: ImageInvisible,
render(msg) {
if (!hasMedia(msg) && !msg.messageSnapshots.some(s => hasMedia(s.message))) return null;
const isHidden = hiddenMessages.has(msg.id);
const isHidden = hiddenMessages.has(msg.id);
return {
label: isHidden ? "Show Media" : "Hide Media",
icon: isHidden ? ImageVisible : ImageInvisible,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => this.toggleHide(msg.channel_id, msg.id)
};
return {
label: isHidden ? "Show Media" : "Hide Media",
icon: isHidden ? ImageVisible : ImageInvisible,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => toggleHide(msg.channel_id, msg.id)
};
},
},
renderMessageAccessory({ message }) {
@ -92,13 +104,4 @@ export default definePlugin({
shouldHide(messageId: string) {
return hiddenMessages.has(messageId);
},
async toggleHide(channelId: string, messageId: string) {
const ids = await getHiddenMessages();
if (!ids.delete(messageId))
ids.add(messageId);
await saveHiddenMessages(ids);
updateMessage(channelId, messageId);
}
});

View file

@ -19,7 +19,7 @@
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { generateId, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin, { StartAt } from "@utils/types";
import definePlugin, { IconComponent, StartAt } from "@utils/types";
import { CloudUpload, MessageAttachment } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack";
import { DraftStore, DraftType, SelectedChannelStore, UserStore, useStateFromStores } from "@webpack/common";
@ -73,6 +73,22 @@ const getAttachments = async (channelId: string) =>
);
const PreviewIcon: IconComponent = ({ height = 20, width = 20, className }) => {
return (
<svg
fill="currentColor"
fillRule="evenodd"
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
style={{ scale: "1.096", translate: "0 -1px" }}
>
<path d="M22.89 11.7c.07.2.07.4 0 .6C22.27 13.9 19.1 21 12 21c-7.11 0-10.27-7.11-10.89-8.7a.83.83 0 0 1 0-.6C1.73 10.1 4.9 3 12 3c7.11 0 10.27 7.11 10.89 8.7Zm-4.5-3.62A15.11 15.11 0 0 1 20.85 12c-.38.88-1.18 2.47-2.46 3.92C16.87 17.62 14.8 19 12 19c-2.8 0-4.87-1.38-6.39-3.08A15.11 15.11 0 0 1 3.15 12c.38-.88 1.18-2.47 2.46-3.92C7.13 6.38 9.2 5 12 5c2.8 0 4.87 1.38 6.39 3.08ZM15.56 11.77c.2-.1.44.02.44.23a4 4 0 1 1-4-4c.21 0 .33.25.23.44a2.5 2.5 0 0 0 3.32 3.32Z" />
</svg>
);
};
const PreviewButton: ChatBarButtonFactory = ({ isMainChat, isEmpty, type: { attachments } }) => {
const channelId = SelectedChannelStore.getChannelId();
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
@ -102,16 +118,7 @@ const PreviewButton: ChatBarButtonFactory = ({ isMainChat, isEmpty, type: { atta
}
}}
>
<svg
fill="currentColor"
fillRule="evenodd"
width="20"
height="20"
viewBox="0 0 24 24"
style={{ scale: "1.096", translate: "0 -1px" }}
>
<path d="M22.89 11.7c.07.2.07.4 0 .6C22.27 13.9 19.1 21 12 21c-7.11 0-10.27-7.11-10.89-8.7a.83.83 0 0 1 0-.6C1.73 10.1 4.9 3 12 3c7.11 0 10.27 7.11 10.89 8.7Zm-4.5-3.62A15.11 15.11 0 0 1 20.85 12c-.38.88-1.18 2.47-2.46 3.92C16.87 17.62 14.8 19 12 19c-2.8 0-4.87-1.38-6.39-3.08A15.11 15.11 0 0 1 3.15 12c.38-.88 1.18-2.47 2.46-3.92C7.13 6.38 9.2 5 12 5c2.8 0 4.87 1.38 6.39 3.08ZM15.56 11.77c.2-.1.44.02.44.23a4 4 0 1 1-4-4c.21 0 .33.25.23.44a2.5 2.5 0 0 0 3.32 3.32Z" />
</svg>
<PreviewIcon />
</ChatBarButton>
);
@ -125,5 +132,8 @@ export default definePlugin({
// This makes the popping in less awkward
startAt: StartAt.Init,
renderChatBarButton: PreviewButton,
chatBarButton: {
icon: PreviewIcon,
render: PreviewButton
}
});

View file

@ -21,29 +21,12 @@ import { insertTextIntoChatInputBox } from "@utils/discord";
import definePlugin from "@utils/types";
import { ChannelStore, PermissionsBits, PermissionStore } from "@webpack/common";
export default definePlugin({
name: "QuickMention",
authors: [Devs.kemo],
description: "Adds a quick mention button to the message actions bar",
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}> `)
};
},
Icon: () => (
function Icon({ height = 24, width = 24, className = "icon" }: { height?: number | string; width?: number | string; className?: string; }) {
return (
<svg
className="icon"
height="24"
width="24"
className={className}
height={height}
width={width}
viewBox="0 0 24 24"
fill="currentColor"
>
@ -51,5 +34,27 @@ export default definePlugin({
d="M12 2C6.486 2 2 6.486 2 12C2 17.515 6.486 22 12 22C14.039 22 15.993 21.398 17.652 20.259L16.521 18.611C15.195 19.519 13.633 20 12 20C7.589 20 4 16.411 4 12C4 7.589 7.589 4 12 4C16.411 4 20 7.589 20 12V12.782C20 14.17 19.402 15 18.4 15L18.398 15.018C18.338 15.005 18.273 15 18.209 15H18C17.437 15 16.6 14.182 16.6 13.631V12C16.6 9.464 14.537 7.4 12 7.4C9.463 7.4 7.4 9.463 7.4 12C7.4 14.537 9.463 16.6 12 16.6C13.234 16.6 14.35 16.106 15.177 15.313C15.826 16.269 16.93 17 18 17L18.002 16.981C18.064 16.994 18.129 17 18.195 17H18.4C20.552 17 22 15.306 22 12.782V12C22 6.486 17.514 2 12 2ZM12 14.599C10.566 14.599 9.4 13.433 9.4 11.999C9.4 10.565 10.566 9.399 12 9.399C13.434 9.399 14.6 10.565 14.6 11.999C14.6 13.433 13.434 14.599 12 14.599Z"
/>
</svg>
),
);
}
export default definePlugin({
name: "QuickMention",
authors: [Devs.kemo],
description: "Adds a quick mention button to the message actions bar",
messagePopoverButton: {
icon: Icon,
render(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: Icon,
message: msg,
channel,
onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `)
};
}
}
});

View file

@ -25,7 +25,7 @@ import { Devs } from "@utils/constants";
import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord";
import { Margins } from "@utils/margins";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin, { IconComponent, OptionType } from "@utils/types";
import { Button, Forms, Parser, Select, useMemo, useState } from "@webpack/common";
const settings = definePluginSettings({
@ -125,7 +125,26 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi
);
}
const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
const SendTimestampIcon: IconComponent = ({ height = 20, width = 20, className }) => {
return (
<svg
aria-hidden="true"
role="img"
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
style={{ scale: "1.2" }}
>
<g fill="none" fillRule="evenodd">
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
<rect width="24" height="24" />
</g>
</svg>
);
};
const SendTimestampButton: ChatBarButtonFactory = ({ isMainChat }) => {
if (!isMainChat) return null;
return (
@ -141,19 +160,7 @@ const ChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
}}
buttonProps={{ "aria-haspopup": "dialog" }}
>
<svg
aria-hidden="true"
role="img"
width="20"
height="20"
viewBox="0 0 24 24"
style={{ scale: "1.2" }}
>
<g fill="none" fillRule="evenodd">
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
<rect width="24" height="24" />
</g>
</svg>
<SendTimestampIcon />
</ChatBarButton>
);
};
@ -164,7 +171,10 @@ export default definePlugin({
authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11],
settings,
renderChatBarButton: ChatBarIcon,
chatBarButton: {
icon: SendTimestampIcon,
render: SendTimestampButton
},
onBeforeMessageSend(_, msg) {
if (settings.store.replaceMessageContents) {

View file

@ -20,7 +20,7 @@ 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";
import definePlugin, { IconComponent, OptionType } from "@utils/types";
import { React, useEffect, useState } from "@webpack/common";
let lastState = false;
@ -41,6 +41,33 @@ const settings = definePluginSettings({
}
});
function SilentMessageDisabledIcon() {
return (
<SilentMessageIcon>
<mask id="vc-silent-msg-mask">
<path fill="#fff" d="M0 0h24v24H0Z" />
<path stroke="#000" strokeWidth="5.99068" d="M0 24 24 0" />
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</SilentMessageIcon>
);
}
const SilentMessageIcon: IconComponent = ({ height = 20, width = 20, className, children }) => {
return (
<svg
width={width}
height={height}
viewBox="0 0 24 24"
className={className}
style={{ scale: "1.2" }}
>
<path fill="currentColor" mask="url(#vc-silent-msg-mask)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" />
{children}
</svg>
);
};
const SilentMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => {
const [enabled, setEnabled] = useState(lastState);
@ -68,21 +95,7 @@ const SilentMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => {
tooltip={enabled ? "Disable Silent Message" : "Enable Silent Message"}
onClick={() => setEnabledValue(!enabled)}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
style={{ scale: "1.2" }}
>
<path fill="currentColor" mask="url(#vc-silent-msg-mask)" d="M18 10.7101C15.1085 9.84957 13 7.17102 13 4c0-.30736.0198-.6101.0582-.907C12.7147 3.03189 12.3611 3 12 3 8.686 3 6 5.686 6 9v5c0 1.657-1.344 3-3 3v1h18v-1c-1.656 0-3-1.343-3-3v-3.2899ZM8.55493 19c.693 1.19 1.96897 2 3.44497 2s2.752-.81 3.445-2H8.55493ZM18.2624 5.50209 21 2.5V1h-4.9651v1.49791h2.4411L16 5.61088V7h5V5.50209h-2.7376Z" />
{!enabled && <>
<mask id="vc-silent-msg-mask">
<path fill="#fff" d="M0 0h24v24H0Z" />
<path stroke="#000" strokeWidth="5.99068" d="M0 24 24 0" />
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</>}
</svg>
{enabled ? <SilentMessageIcon /> : <SilentMessageDisabledIcon />}
</ChatBarButton>
);
};
@ -93,5 +106,8 @@ export default definePlugin({
description: "Adds a button to the chat bar to toggle sending a silent message.",
settings,
renderChatBarButton: SilentMessageToggle,
chatBarButton: {
icon: SilentMessageIcon,
render: SilentMessageToggle
}
});

View file

@ -21,7 +21,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption,
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin, { IconComponent, OptionType } from "@utils/types";
import { FluxDispatcher, Menu, React } from "@webpack/common";
const settings = definePluginSettings({
@ -43,6 +43,33 @@ const settings = definePluginSettings({
}
});
function SilentTypingEnabledIcon() {
return (
<SilentTypingIcon>
<mask id="silent-typing-msg-mask">
<path fill="#fff" d="M0 0h24v24H0Z"></path>
<path stroke="#000" strokeWidth="5.99068" d="M0 24 24 0" transform="translate(-2, -3)"></path>
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</SilentTypingIcon>
);
}
const SilentTypingIcon: IconComponent = ({ height = 20, width = 20, className, children }) => {
return (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
style={{ scale: "1.2" }}
>
<path fill="currentColor" mask="url(#silent-typing-msg-mask)" d="M18.333 15.556H1.667a1.667 1.667 0 0 1 -1.667 -1.667v-10a1.667 1.667 0 0 1 1.667 -1.667h16.667a1.667 1.667 0 0 1 1.667 1.667v10a1.667 1.667 0 0 1 -1.667 1.667M4.444 6.25V4.861a0.417 0.417 0 0 0 -0.417 -0.417H2.639a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417H5.973a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m-11.667 3.333V8.194a0.417 0.417 0 0 0 -0.417 -0.417H4.306a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417H7.639a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m-11.667 3.333v-1.389a0.417 0.417 0 0 0 -0.417 -0.417H2.639a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m10 0v-1.389a0.417 0.417 0 0 0 -0.417 -0.417H5.973a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h8.056a0.417 0.417 0 0 0 0.417 -0.417m3.333 0v-1.389a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417" transform="translate(2, 3)" />
{children}
</svg>
);
};
const SilentTypingToggle: ChatBarButtonFactory = ({ isMainChat }) => {
const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]);
const toggle = () => settings.store.isEnabled = !settings.store.isEnabled;
@ -54,18 +81,7 @@ const SilentTypingToggle: ChatBarButtonFactory = ({ isMainChat }) => {
tooltip={isEnabled ? "Disable Silent Typing" : "Enable Silent Typing"}
onClick={toggle}
>
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style={{ scale: "1.2" }}>
<path fill="currentColor" mask="url(#silent-typing-msg-mask)" d="M18.333 15.556H1.667a1.667 1.667 0 0 1 -1.667 -1.667v-10a1.667 1.667 0 0 1 1.667 -1.667h16.667a1.667 1.667 0 0 1 1.667 1.667v10a1.667 1.667 0 0 1 -1.667 1.667M4.444 6.25V4.861a0.417 0.417 0 0 0 -0.417 -0.417H2.639a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417H5.973a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V4.861a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V6.25a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m-11.667 3.333V8.194a0.417 0.417 0 0 0 -0.417 -0.417H4.306a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417H7.639a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m3.333 0V8.194a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V9.583a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m-11.667 3.333v-1.389a0.417 0.417 0 0 0 -0.417 -0.417H2.639a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417m10 0v-1.389a0.417 0.417 0 0 0 -0.417 -0.417H5.973a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h8.056a0.417 0.417 0 0 0 0.417 -0.417m3.333 0v-1.389a0.417 0.417 0 0 0 -0.417 -0.417h-1.389a0.417 0.417 0 0 0 -0.417 0.417V12.917a0.417 0.417 0 0 0 0.417 0.417h1.389a0.417 0.417 0 0 0 0.417 -0.417" transform="translate(2, 3)" />
{isEnabled && (
<>
<mask id="silent-typing-msg-mask">
<path fill="#fff" d="M0 0h24v24H0Z"></path>
<path stroke="#000" strokeWidth="5.99068" d="M0 24 24 0" transform="translate(-2, -3)"></path>
</mask>
<path fill="var(--status-danger)" d="m21.178 1.70703 1.414 1.414L4.12103 21.593l-1.414-1.415L21.178 1.70703Z" />
</>
)}
</svg>
{isEnabled ? <SilentTypingEnabledIcon /> : <SilentTypingIcon />}
</ChatBarButton>
);
};
@ -137,5 +153,8 @@ export default definePlugin({
FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId });
},
renderChatBarButton: SilentTypingToggle,
chatBarButton: {
icon: SilentTypingIcon,
render: SilentTypingToggle
}
});

View file

@ -19,13 +19,14 @@
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { IconComponent } from "@utils/types";
import { Alerts, Forms, Tooltip, useEffect, useState } from "@webpack/common";
import { settings } from "./settings";
import { TranslateModal } from "./TranslateModal";
import { cl } from "./utils";
export function TranslateIcon({ height = 20, width = 20, className }: { height?: number; width?: number; className?: string; }) {
export const TranslateIcon: IconComponent = ({ height = 20, width = 20, className }) => {
return (
<svg
viewBox="0 96 960 960"
@ -36,7 +37,7 @@ export function TranslateIcon({ height = 20, width = 20, className }: { height?:
<path fill="currentColor" d="m475 976 181-480h82l186 480h-87l-41-126H604l-47 126h-82Zm151-196h142l-70-194h-2l-70 194Zm-466 76-55-55 204-204q-38-44-67.5-88.5T190 416h87q17 33 37.5 62.5T361 539q45-47 75-97.5T487 336H40v-80h280v-80h80v80h280v80H567q-22 69-58.5 135.5T419 598l98 99-30 81-127-122-200 200Z" />
</svg>
);
}
};
export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);

View file

@ -74,22 +74,28 @@ export default definePlugin({
renderMessageAccessory: props => <TranslationAccessory message={props.message} />,
renderChatBarButton: TranslateChatBarIcon,
chatBarButton: {
icon: TranslateIcon,
render: TranslateChatBarIcon
},
renderMessagePopoverButton(message: Message) {
const content = getMessageContent(message);
if (!content) return null;
messagePopoverButton: {
icon: TranslateIcon,
render(message: Message) {
const content = getMessageContent(message);
if (!content) return null;
return {
label: "Translate",
icon: TranslateIcon,
message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: async () => {
const trans = await translate("received", content);
handleTranslate(message.id, trans);
}
};
return {
label: "Translate",
icon: TranslateIcon,
message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: async () => {
const trans = await translate("received", content);
handleTranslate(message.id, trans);
}
};
}
},
async onBeforeMessageSend(_, message) {

View file

@ -26,16 +26,25 @@ import { Devs } from "@utils/constants";
import { copyWithToast, getCurrentGuild, getIntlMessage } from "@utils/discord";
import { Margins } from "@utils/margins";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin, { IconComponent, OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types";
import { Button, ChannelStore, Forms, GuildRoleStore, Menu, Text } from "@webpack/common";
const CopyIcon = () => {
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="20" height="20">
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
</svg>;
const CopyIcon: IconComponent = ({ height = 20, width = 20, className }) => {
return (
<svg
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
width={width}
height={height}
className={className}
>
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
</svg>
);
};
function sortObject<T extends object>(obj: T): T {
@ -133,9 +142,11 @@ function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel"): NavCo
children = p.children;
}
// typescript parser goes crazy if this is inline
const id = `vc-view-${name.toLowerCase()}-raw`;
children.splice(-1, 0,
<Menu.MenuItem
id={`vc-view-${name.toLowerCase()}-raw`}
id={id}
label="View Raw"
action={() => openViewRawModal(JSON.stringify(value, null, 4), name)}
icon={CopyIcon}
@ -177,38 +188,41 @@ export default definePlugin({
"dev-context": devContextCallback
},
renderMessagePopoverButton(msg) {
const handleClick = () => {
if (settings.store.clickMethod === "Right") {
copyWithToast(msg.content);
} else {
openViewRawModalMessage(msg);
}
};
messagePopoverButton: {
icon: CopyIcon,
render(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
};
return {
label,
icon: CopyIcon,
message: msg,
channel: ChannelStore.getChannel(msg.channel_id),
onClick: handleClick,
onContextMenu: handleContextMenu
};
}
}
});

View file

@ -16,17 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { ProfileBadge } from "@api/Badges";
import type { ChatBarButtonFactory } from "@api/ChatButtons";
import type { NavContextMenuPatchCallback } from "@api/ContextMenu";
import type { MemberListDecoratorFactory } from "@api/MemberListDecorators";
import type { MessageAccessoryFactory } from "@api/MessageAccessories";
import type { MessageDecorationFactory } from "@api/MessageDecorations";
import type { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents";
import type { MessagePopoverButtonFactory } from "@api/MessagePopover";
import type { Command, FluxEvents } from "@vencord/discord-types";
import type { ReactNode } from "react";
import type { LiteralUnion } from "type-fest";
import { ProfileBadge } from "@api/Badges";
import { ChatBarButtonData } from "@api/ChatButtons";
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 { MessagePopoverButtonData } from "@api/MessagePopover";
import { Command, FluxEvents } from "@vencord/discord-types";
import { ReactNode } from "react";
import { LiteralUnion } from "type-fest";
// exists to export default definePlugin({...})
export default function definePlugin<P extends PluginDef>(p: P & Record<PropertyKey, any>) {
@ -95,6 +95,8 @@ export interface Plugin extends PluginDef {
isDependency?: boolean;
}
export type IconComponent = (props: IconProps & Record<string, any>) => ReactNode;
export type IconProps = { height?: number | string; width?: number | string; className?: string; };
export interface PluginDef {
name: string;
description: string;
@ -180,17 +182,27 @@ export interface PluginDef {
userProfileBadge?: ProfileBadge;
messagePopoverButton?: MessagePopoverButtonData;
chatBarButton?: ChatBarButtonData;
onMessageClick?: MessageClickListener;
onBeforeMessageSend?: MessageSendListener;
onBeforeMessageEdit?: MessageEditListener;
renderMessagePopoverButton?: MessagePopoverButtonFactory;
renderMessageAccessory?: MessageAccessoryFactory;
renderMessageDecoration?: MessageDecorationFactory;
renderMemberListDecorator?: MemberListDecoratorFactory;
renderChatBarButton?: ChatBarButtonFactory;
// TODO: Remove eventually
/**
* @deprecated Use {@link chatBarButton} instead
*/
renderChatBarButton?: never;
/**
* @deprecated Use {@link messagePopoverButton} instead
*/
renderMessagePopoverButton?: never;
}
export const enum StartAt {
@ -420,3 +432,5 @@ export type PluginNative<PluginExports extends Record<string, (event: Electron.I
? (...args: Args) => Return extends Promise<any> ? Return : Promise<Return>
: never;
};
export type AllOrNothing<T> = T | { [K in keyof T]?: never; };