Equicord/src/equicordplugins/chatButtonsPlus/index.tsx
2025-09-24 13:04:55 -04:00

284 lines
13 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { ChatBarButton } from "@api/ChatButtons";
import { DataStore } from "@api/index";
import { definePluginSettings, migratePluginSettings, Settings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { getCurrentChannel, sendMessage } from "@utils/discord";
import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Forms, React, Switch, TextInput } from "@webpack/common";
type ButtonEntry = {
id: string;
label: string;
message: string;
svg: string;
enabled: boolean;
};
let buttonEntries: ButtonEntry[] = [];
const BUTTON_ENTRIES_KEY = "ChatButtonsPlus_buttonEntries";
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
async function handleButtonClick(context: string) {
const currentChannel = getCurrentChannel();
if (!currentChannel) return;
sendMessage(currentChannel.id, { content: context });
}
async function addButtonEntry(forceUpdate: () => void) {
try {
buttonEntries.push({
id: generateId(),
label: "New Button",
message: "Hello!",
svg: '<path fill="currentColor" d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>',
enabled: true
});
await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries);
forceUpdate();
} catch (error) {
console.error("ChatButtonsPlus: Failed to add button entry:", error);
}
}
async function removeButtonEntry(id: string, forceUpdate: () => void) {
try {
const index = buttonEntries.findIndex(entry => entry.id === id);
if (index !== -1) {
buttonEntries.splice(index, 1);
await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries);
forceUpdate();
}
} catch (error) {
console.error("ChatButtonsPlus: Failed to remove button entry:", error);
}
}
function ButtonEntries() {
const update = useForceUpdater();
async function setLabel(id: string, value: string) {
try {
const index = buttonEntries.findIndex(entry => entry.id === id);
if (index !== -1) {
buttonEntries[index].label = value.trim() || "Button";
await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries);
update();
}
} catch (error) {
console.error("ChatButtonsPlus: Failed to update label:", error);
}
}
async function setMessage(id: string, value: string) {
try {
const index = buttonEntries.findIndex(entry => entry.id === id);
if (index !== -1) {
buttonEntries[index].message = value.trim() || "Hello!";
await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries);
update();
}
} catch (error) {
console.error("ChatButtonsPlus: Failed to update message:", error);
}
}
async function setSvg(id: string, value: string) {
try {
const index = buttonEntries.findIndex(entry => entry.id === id);
if (index !== -1 && isValidSvg(value.trim())) {
buttonEntries[index].svg = value.trim();
await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries);
update();
}
} catch (error) {
console.error("ChatButtonsPlus: Failed to update SVG:", error);
}
}
async function setEnabled(id: string, value: boolean) {
try {
const index = buttonEntries.findIndex(entry => entry.id === id);
if (index !== -1) {
buttonEntries[index].enabled = value;
await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries);
update();
}
} catch (error) {
console.error("ChatButtonsPlus: Failed to update enabled state:", error);
}
}
function isValidSvg(svg: string): boolean {
if (!svg) return true;
const dangerousPatterns = [
/<script/i,
/javascript:/i,
/on\w+=/i,
/<iframe/i,
/<object/i,
/<embed/i,
/<link/i,
/<meta/i
];
if (dangerousPatterns.some(pattern => pattern.test(svg))) {
return false;
}
return svg.includes("<") && (
svg.includes("path") ||
svg.includes("svg") ||
svg.includes("circle") ||
svg.includes("rect") ||
svg.includes("polygon") ||
svg.includes("g")
);
}
const elements = buttonEntries.map((entry, i) => {
return (
<div key={entry.id} className="chatButtonsPlus-card">
<div className="chatButtonsPlus-header">
<Forms.FormTitle tag="h5" className="chatButtonsPlus-title">Button {i + 1}</Forms.FormTitle>
<div className="chatButtonsPlus-controls">
<Switch
value={entry.enabled ?? true}
onChange={value => setEnabled(entry.id, value)}
className="chatButtonsPlus-toggle"
>
Enabled
</Switch>
<Button
onClick={() => removeButtonEntry(entry.id, update)}
look={Button.Looks.OUTLINED}
color={Button.Colors.RED}
size={Button.Sizes.SMALL}
>
Delete
</Button>
</div>
</div>
<div className="chatButtonsPlus-field">
<Forms.FormText className="chatButtonsPlus-label">Button Label (Tooltip)</Forms.FormText>
<TextInput
placeholder="Button label/tooltip"
value={entry.label}
onChange={e => setLabel(entry.id, e)}
/>
</div>
<div className="chatButtonsPlus-field">
<Forms.FormText className="chatButtonsPlus-label">Message to Send</Forms.FormText>
<TextInput
placeholder="Message to send when clicked"
value={entry.message}
onChange={e => setMessage(entry.id, e)}
/>
</div>
<div className="chatButtonsPlus-field">
<Forms.FormText className="chatButtonsPlus-label">Custom SVG Path (24x24 viewBox)</Forms.FormText>
<TextInput
placeholder='<path fill="currentColor" d="..."/>'
value={entry.svg}
onChange={e => setSvg(entry.id, e)}
/>
<Forms.FormText className="chatButtonsPlus-description">
Enter SVG path elements for a 24x24 viewBox. Use "currentColor" for the fill to match Discord's theme.
</Forms.FormText>
</div>
</div>
);
});
return (
<>
{elements}
<Button onClick={() => addButtonEntry(update)}>Add Button</Button>
</>
);
}
const settings = definePluginSettings({
buttons: {
type: OptionType.COMPONENT,
description: "Manage your custom chat buttons",
component: () => <ButtonEntries />
}
});
migratePluginSettings("ChatButtonsPlus", "Meow", "Woof");
export default definePlugin({
name: "ChatButtonsPlus",
description: "Add custom chat buttons with personalized + messages and SVG icons",
authors: [EquicordDevs.creations],
settings,
renderChatBarButton: ({ isMainChat }) => {
if (!isMainChat) return null;
return (
<>
{buttonEntries.filter(entry => entry.enabled !== false).map(entry => (
<ChatBarButton
key={entry.id}
tooltip={entry.label}
onClick={() => handleButtonClick(entry.message)}
>
<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 24 24">
<g dangerouslySetInnerHTML={{ __html: entry.svg }} />
</svg>
</ChatBarButton>
))}
</>
);
},
async start() {
const storedEntries = await DataStore.get(BUTTON_ENTRIES_KEY) ?? [];
if (Settings.plugins?.Woof.enabled) {
storedEntries.push({
id: generateId(),
label: "Woof",
message: "Woof",
svg: '<path fill="currentColor" d="M 12.898438 7.9375 L 13.863281 2.160156 C 13.941406 1.683594 14.355469 1.332031 14.835938 1.332031 C 15.148438 1.332031 15.441406 1.480469 15.628906 1.730469 L 16.332031 2.667969 L 18.503906 2.667969 C 19.035156 2.667969 19.542969 2.878906 19.917969 3.253906 L 20.667969 4 L 23 4 C 23.554688 4 24 4.445312 24 5 L 24 6 C 24 7.839844 22.507812 9.332031 20.667969 9.332031 L 17.777344 9.332031 L 17.566406 10.605469 Z M 17.332031 12.003906 L 17.332031 21.332031 C 17.332031 22.070312 16.738281 22.667969 16 22.667969 L 14.667969 22.667969 C 13.929688 22.667969 13.332031 22.070312 13.332031 21.332031 L 13.332031 16.535156 C 12.332031 17.046875 11.199219 17.332031 10 17.332031 C 8.800781 17.332031 7.667969 17.046875 6.667969 16.535156 L 6.667969 21.332031 C 6.667969 22.070312 6.070312 22.667969 5.332031 22.667969 L 4 22.667969 C 3.261719 22.667969 2.667969 22.070312 2.667969 21.332031 L 2.667969 11.742188 C 1.464844 11.289062 0.523438 10.269531 0.199219 8.972656 L 0.0429688 8.324219 C -0.136719 7.613281 0.296875 6.886719 1.011719 6.707031 C 1.730469 6.527344 2.449219 6.960938 2.628906 7.679688 L 2.792969 8.324219 C 2.9375 8.917969 3.472656 9.332031 4.082031 9.332031 L 12.660156 9.332031 Z M 19.332031 4.667969 C 19.332031 4.296875 19.035156 4 18.667969 4 C 18.296875 4 18 4.296875 18 4.667969 C 18 5.035156 18.296875 5.332031 18.667969 5.332031 C 19.035156 5.332031 19.332031 5.035156 19.332031 4.667969 "/>',
enabled: true
});
Settings.plugins.Woof.enabled = false;
}
if (Settings.plugins?.Meow.enabled) {
storedEntries.push({
id: generateId(),
label: "Meow",
message: "Meow",
svg: '<path fill="currentColor" d="M 13.332031 9 L 14.046875 9 C 14.964844 10.796875 16.691406 12 18.667969 12 C 19.125 12 19.574219 11.933594 20 11.8125 L 20 22.5 C 20 23.328125 19.402344 24 18.667969 24 C 17.929688 24 17.332031 23.328125 17.332031 22.5 L 17.332031 15.898438 L 11.667969 21 L 14 21 C 14.738281 21 15.332031 21.671875 15.332031 22.5 C 15.332031 23.328125 14.738281 24 14 24 L 8 24 C 5.792969 24 4 21.984375 4 19.5 L 4 9.023438 C 4 8.269531 3.5 7.625 2.832031 7.53125 L 2.503906 7.484375 C 1.773438 7.382812 1.253906 6.632812 1.347656 5.8125 C 1.4375 4.992188 2.105469 4.40625 2.832031 4.507812 L 3.164062 4.554688 C 5.164062 4.835938 6.667969 6.75 6.667969 9.023438 L 6.667969 13.023438 C 8.101562 10.597656 10.550781 9 13.332031 9 M 20 10.242188 C 19.582031 10.40625 19.132812 10.5 18.667969 10.5 C 17.484375 10.5 16.417969 9.917969 15.683594 9 C 15.527344 8.808594 15.390625 8.601562 15.269531 8.382812 C 14.886719 7.6875 14.667969 6.871094 14.667969 6 L 14.667969 0.5 C 14.667969 0.226562 14.863281 0.00390625 15.109375 0 L 15.117188 0 C 15.253906 0 15.382812 0.0742188 15.464844 0.195312 L 15.464844 0.203125 L 16 1 L 17.132812 2.699219 L 17.332031 3 L 20 3 L 20.199219 2.699219 L 21.332031 1 L 21.867188 0.203125 L 21.867188 0.195312 C 21.949219 0.0742188 22.078125 0 22.214844 0 L 22.226562 0 C 22.472656 0.00390625 22.667969 0.226562 22.667969 0.5 L 22.667969 6 C 22.667969 6.8125 22.476562 7.574219 22.140625 8.230469 C 21.671875 9.160156 20.910156 9.882812 20 10.242188 M 18 6 C 18 5.585938 17.703125 5.25 17.332031 5.25 C 16.964844 5.25 16.667969 5.585938 16.667969 6 C 16.667969 6.414062 16.964844 6.75 17.332031 6.75 C 17.703125 6.75 18 6.414062 18 6 M 20 6.75 C 20.367188 6.75 20.667969 6.414062 20.667969 6 C 20.667969 5.585938 20.367188 5.25 20 5.25 C 19.632812 5.25 19.332031 5.585938 19.332031 6 C 19.332031 6.414062 19.632812 6.75 20 6.75 "/>',
enabled: true
});
Settings.plugins.Meow.enabled = false;
}
await DataStore.set(BUTTON_ENTRIES_KEY, storedEntries);
buttonEntries = storedEntries;
}
});