diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx
index c38ba1a5c..76096ad84 100644
--- a/src/components/Icons.tsx
+++ b/src/components/Icons.tsx
@@ -128,6 +128,23 @@ export function InfoIcon(props: IconProps) {
);
}
+export function WarningIcon({ height = 32, width = 32, className }: IconProps) {
+ return (
+
+
+
+ );
+}
+
export function OwnerCrownIcon(props: IconProps) {
return (
{
+interface PluginCardProps {
plugin: Plugin;
- disabled: boolean;
+ disabled?: boolean;
onRestartNeeded(name: string, key: string): void;
isNew?: boolean;
+ onMouseEnter?: React.MouseEventHandler;
+ onMouseLeave?: React.MouseEventHandler;
}
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx
index deef932ab..20a747505 100644
--- a/src/components/settings/tabs/plugins/index.tsx
+++ b/src/components/settings/tabs/plugins/index.tsx
@@ -104,19 +104,19 @@ const enum SearchStatus {
NEW,
}
+export const ExcludedReasons: Record<"web" | "discordDesktop" | "vesktop" | "equibop" | "desktop" | "dev", string> = {
+ desktop: "Discord Desktop app or Vesktop",
+ discordDesktop: "Discord Desktop app",
+ vesktop: "Vesktop & Equibop apps",
+ equibop: "Vesktop & Equibop apps",
+ web: "Vesktop & Equibop apps as well as the Web version of Discord",
+ dev: "Developer version of Equicord"
+};
+
function ExcludedPluginsList({ search }: { search: string; }) {
const matchingExcludedPlugins = Object.entries(ExcludedPlugins)
.filter(([name]) => name.toLowerCase().includes(search));
- const ExcludedReasons: Record<"web" | "discordDesktop" | "vesktop" | "equibop" | "desktop" | "dev", string> = {
- desktop: "Discord Desktop app or Vesktop",
- discordDesktop: "Discord Desktop app",
- vesktop: "Vesktop & Equibop apps",
- equibop: "Vesktop & Equibop apps",
- web: "Vesktop & Equibop apps as well as the Web version of Discord",
- dev: "Developer version of Equicord"
- };
-
return (
{matchingExcludedPlugins.length
@@ -160,19 +160,7 @@ export default function PluginSettings() {
}));
}, []);
- const depMap = useMemo(() => {
- const o = {} as Record;
- for (const plugin in Plugins) {
- const deps = Plugins[plugin].dependencies;
- if (deps) {
- for (const dep of deps) {
- o[dep] ??= [];
- o[dep].push(plugin);
- }
- }
- }
- return o;
- }, []);
+ const depMap = Vencord.Plugins.calculatePluginDependencyMap();
const sortedPlugins = useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []);
@@ -241,7 +229,7 @@ export default function PluginSettings() {
if (isRequired) {
const tooltipText = p.required || !depMap[p.name]
? "This plugin is required for Equicord to function."
- : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
+ : settings.plugins[d].enabled)} />;
requiredPlugins.push(
@@ -419,7 +407,7 @@ export default function PluginSettings() {
);
}
-function makeDependencyList(deps: string[]) {
+export function PluginDependencyList({ deps }: { deps: string[]; }) {
return (
This plugin is required by:
diff --git a/src/equicordplugins/equicordHelper/README.md b/src/equicordplugins/equicordHelper/README.md
index 9c1dcab8a..d371ef512 100644
--- a/src/equicordplugins/equicordHelper/README.md
+++ b/src/equicordplugins/equicordHelper/README.md
@@ -1,3 +1,3 @@
# EquicordHelper
-Fixes various issues for crashing and adds other items that might help with support
+Fixes various issues for crashing and adds other items such as plugin cards and buttons for managing plugins quickly in support
diff --git a/src/equicordplugins/equicordHelper/index.tsx b/src/equicordplugins/equicordHelper/index.tsx
index 0d068f6b3..0f33cd1a6 100644
--- a/src/equicordplugins/equicordHelper/index.tsx
+++ b/src/equicordplugins/equicordHelper/index.tsx
@@ -8,13 +8,10 @@ import "@equicordplugins/_misc/styles.css";
import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
-import { Logger } from "@utils/Logger";
-import { isEquicordGuild, isEquicordSupport } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
-import { Button, Flex, showToast, Toasts } from "@webpack/common";
-import { JSX } from "react";
-import { toggleEnabled } from "./utils";
+import { PluginButtons } from "./pluginButtons";
+import { PluginCards } from "./pluginCards";
const settings = definePluginSettings({
disableCreateDMButton: {
@@ -70,59 +67,11 @@ export default definePlugin({
},
],
renderMessageAccessory(props) {
- return pluginToggleButtons(props);
- }
-});
-
-function pluginToggleButtons(props) {
- const buttons = [] as JSX.Element[];
- const msg = props.message.content?.toLowerCase() ?? "";
-
- const contentWords = (msg.match(/`\w+`/g) ?? []).map(e => e.slice(1, -1));
- const matchedPlugins = Object.keys(Vencord.Plugins.plugins).filter(name => contentWords.includes(name.toLowerCase()));
- const matchedPlugin = matchedPlugins.sort((a, b) => b.length - a.length)[0];
- const pluginData = matchedPlugin ? Vencord.Plugins.plugins[matchedPlugin] : null;
-
- const isEquicord = isEquicordGuild(props.channel.id) && isEquicordSupport(props.message.author.id);
- const startsWithEnabled = msg.startsWith("enable");
- const startsWithDisabled = msg.startsWith("disable");
-
- const shouldAddPluginButtons = pluginData && isEquicord && (startsWithEnabled || startsWithDisabled);
-
- if (shouldAddPluginButtons) {
- if (pluginData.required || pluginData.name.endsWith("API")) return;
- const isEnabled = Vencord.Plugins.isPluginEnabled(matchedPlugin);
-
- let label = `${matchedPlugin} is already ${isEnabled ? "enabled" : "disabled"}`;
- let disabled = true;
-
- if ((startsWithDisabled && isEnabled) || (startsWithEnabled && !isEnabled)) {
- label = `${isEnabled ? "Disable" : "Enable"} ${matchedPlugin}`;
- disabled = false;
- }
-
- buttons.push(
-
+ return (
+ <>
+
+
+ >
);
}
-
- return buttons.length
- ? {buttons}
- : null;
-}
+});
diff --git a/src/equicordplugins/equicordHelper/pluginButtons.tsx b/src/equicordplugins/equicordHelper/pluginButtons.tsx
new file mode 100644
index 000000000..a7c26e0d0
--- /dev/null
+++ b/src/equicordplugins/equicordHelper/pluginButtons.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { isEquicordGuild, isEquicordSupport } from "@utils/misc";
+import { Message } from "@vencord/discord-types";
+import { Button, showToast, Toasts } from "@webpack/common";
+import { JSX } from "react";
+
+import { toggleEnabled } from "./utils";
+
+export const PluginButtons = ErrorBoundary.wrap(function PluginCards({ message }: { message: Message; }) {
+ const pluginButtons = [] as JSX.Element[];
+ const msg = message.content?.toLowerCase() ?? "";
+
+ const contentWords = (msg.match(/`\w+`/g) ?? []).map(e => e.slice(1, -1));
+ const matchedPlugins = Object.keys(Vencord.Plugins.plugins).filter(name => contentWords.includes(name.toLowerCase()));
+ const matchedPlugin = matchedPlugins.sort((a, b) => b.length - a.length)[0];
+ const pluginData = matchedPlugin ? Vencord.Plugins.plugins[matchedPlugin] : null;
+
+ const isEquicord = isEquicordGuild(message.channel_id) && isEquicordSupport(message.author.id);
+ const startsWithEnabled = msg.startsWith("enable");
+ const startsWithDisabled = msg.startsWith("disable");
+
+ const shouldAddPluginButtons = pluginData && isEquicord && (startsWithEnabled || startsWithDisabled);
+
+ if (shouldAddPluginButtons) {
+ if (pluginData.required || pluginData.name.endsWith("API")) return;
+ const isEnabled = Vencord.Plugins.isPluginEnabled(matchedPlugin);
+
+ let label = `${matchedPlugin} is already ${isEnabled ? "enabled" : "disabled"}`;
+ let disabled = true;
+
+ if ((startsWithDisabled && isEnabled) || (startsWithEnabled && !isEnabled)) {
+ label = `${isEnabled ? "Disable" : "Enable"} ${matchedPlugin}`;
+ disabled = false;
+ }
+
+ pluginButtons.push(
+
+ );
+ }
+
+ if (pluginButtons.length === 0) return null;
+
+ return (
+
+ {pluginButtons}
+
+ );
+}, { noop: true });
diff --git a/src/equicordplugins/equicordHelper/pluginCards.tsx b/src/equicordplugins/equicordHelper/pluginCards.tsx
new file mode 100644
index 000000000..17af02327
--- /dev/null
+++ b/src/equicordplugins/equicordHelper/pluginCards.tsx
@@ -0,0 +1,155 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2025 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { useSettings } from "@api/Settings";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { WarningIcon } from "@components/Icons";
+import { AddonCard } from "@components/settings";
+import { ExcludedReasons, PluginDependencyList } from "@components/settings/tabs/plugins";
+import { PluginCard } from "@components/settings/tabs/plugins/PluginCard";
+import { EQUIBOT_USER_ID } from "@utils/constants";
+import { isEquicordGuild, isEquicordSupport } from "@utils/misc";
+import { Message } from "@vencord/discord-types";
+import { showToast, Tooltip, TooltipContainer } from "@webpack/common";
+import { JSX } from "react";
+
+import plugins, { ExcludedPlugins } from "~plugins";
+
+
+export function ChatPluginCard({ url, description }: { url: string, description: string; }) {
+ const pluginNameFromUrl = new URL(url).pathname.split("/")[2];
+
+ const actualPluginName = Object.keys(plugins).find(name =>
+ name.toLowerCase() === pluginNameFromUrl?.toLowerCase()
+ );
+
+ const pluginName = actualPluginName || pluginNameFromUrl;
+
+ useSettings([`plugins.${pluginName ?? ""}.enabled`]);
+
+ if (!pluginName) return null;
+
+ const p = plugins[pluginName];
+ const excludedPlugin = ExcludedPlugins[pluginName];
+
+ if (excludedPlugin || !p) {
+ const toolTipText = !p
+ ? `${pluginName} is only available on the ${ExcludedReasons[ExcludedPlugins[pluginName]]}`
+ : "This plugin is not on this version of Equicord. Try updating!";
+
+ const card = (
+ { }}
+ disabled={true}
+ infoButton={}
+ />
+ );
+
+ return description
+ ? {card}
+ : card;
+ }
+
+ const onRestartNeeded = () => showToast("A restart is required for the change to take effect!");
+
+ const required = Vencord.Plugins.isPluginRequired(pluginName);
+ const dependents = Vencord.Plugins.calculatePluginDependencyMap()[p.name]?.filter(d => Vencord.Plugins.isPluginEnabled(d));
+
+ if (required) {
+ const tooltipText = p.required || !dependents.length
+ ? "This plugin is required for Equicord to function."
+ : ;
+
+ return (
+
+ {({ onMouseLeave, onMouseEnter }) =>
+
+ }
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export const PluginCards = ErrorBoundary.wrap(function PluginCards({ message }: { message: Message; }) {
+ const seenPlugins = new Set();
+ const pluginCards: JSX.Element[] = [];
+
+ // Process embeds
+ message.embeds?.forEach(embed => {
+ if (!embed.url?.startsWith("https://equicord.org/plugins/") && !embed.url?.startsWith("https://vencord.dev/plugins/")) return;
+
+ const isEquicord = isEquicordGuild(message.channel_id) && isEquicordSupport(message.author.id);
+ if (!isEquicord) return;
+
+ const pluginNameFromUrl = new URL(embed.url).pathname.split("/")[2];
+ const actualPluginName = Object.keys(plugins).find(name =>
+ name.toLowerCase() === pluginNameFromUrl?.toLowerCase()
+ );
+ const pluginName = actualPluginName || pluginNameFromUrl;
+
+ if (!pluginName || seenPlugins.has(pluginName)) return;
+ seenPlugins.add(pluginName);
+
+ pluginCards.push(
+
+ );
+ });
+
+ // Process components
+ const components = (message.components?.[0] as any)?.components;
+ if (message.author.id === EQUIBOT_USER_ID && components?.length >= 4) {
+ const description = components[1]?.content;
+ const pluginUrl = components.find((c: any) => c?.components)?.components[0]?.url;
+ if (pluginUrl?.startsWith("https://equicord.org/plugins/") || pluginUrl?.startsWith("https://vencord.dev/plugins/")) {
+ const pluginNameFromUrl = new URL(pluginUrl).pathname.split("/")[2];
+ const actualPluginName = Object.keys(plugins).find(name =>
+ name.toLowerCase() === pluginNameFromUrl?.toLowerCase()
+ );
+ const pluginName = actualPluginName || pluginNameFromUrl;
+
+ if (pluginName && !seenPlugins.has(pluginName)) {
+ seenPlugins.add(pluginName);
+ pluginCards.push(
+
+ );
+ }
+ }
+ }
+
+ if (pluginCards.length === 0) return null;
+
+ return (
+
+ {pluginCards}
+
+ );
+}, { noop: true });
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index eb2ba73ce..7b111d630 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -28,6 +28,7 @@ import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/Messag
import { addNicknameIcon, removeNicknameIcon } from "@api/NicknameIcons";
import { Settings, SettingsStore } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
+import { makeLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
@@ -52,10 +53,36 @@ const subscribedFluxEventsPlugins = new Set();
const pluginsValues = Object.values(Plugins);
const settings = Settings.plugins;
+/**
+ * Whether a plugin is required (or dependency of another enabled plugin)
+ */
+export function isPluginRequired(name: string) {
+ const p = Plugins[name];
+ if (!p) return false;
+ return p.required || p.isDependency;
+}
+
+/**
+ * A map of plugin names to the plugins that depend on them
+ */
+export const calculatePluginDependencyMap = makeLazy(() => {
+ const dependencies: Record = {};
+ for (const plugin in Plugins) {
+ const deps = Plugins[plugin].dependencies;
+ if (deps) {
+ for (const dep of deps) {
+ dependencies[dep] ??= [];
+ dependencies[dep].push(plugin);
+ }
+ }
+ }
+
+ return dependencies;
+});
+
export function isPluginEnabled(p: string) {
return (
- Plugins[p]?.required ||
- Plugins[p]?.isDependency ||
+ isPluginRequired(p) ||
settings[p]?.enabled
) ?? false;
}
diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx
index 7805175b8..081337afc 100644
--- a/src/plugins/messageLogger/index.tsx
+++ b/src/plugins/messageLogger/index.tsx
@@ -14,7 +14,7 @@ import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
-import { Devs, VC_SUPPORT_CATEGORY_ID, VENBOT_USER_ID } from "@utils/constants";
+import { Devs, EQUIBOT_USER_ID, SUPPORT_CHANNEL_ID, VC_SUPPORT_CATEGORY_ID, VENBOT_USER_ID } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import { Logger } from "@utils/Logger";
import { classes } from "@utils/misc";
@@ -409,7 +409,8 @@ export default definePlugin({
(isEdit ? !logEdits : !logDeletes) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||
// Ignore Venbot in the support channels (love you venbot!!!)
- (message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === VC_SUPPORT_CATEGORY_ID));
+ (message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === VC_SUPPORT_CATEGORY_ID) ||
+ (message.author?.id === EQUIBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.id === SUPPORT_CHANNEL_ID));
},
EditMarker({ message, className, children, ...props }: any) {
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index b2e3e5544..629333e68 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -27,6 +27,7 @@ export const EQUICORD_TEAM = "1173520023239786538";
export const EQUICORD_HELPERS = "1326406112144265257";
export const EQUIBOP_CONTRIB_ROLE_ID = "1287079931645263968";
export const VENCORD_CONTRIB_ROLE_ID = "1173343399470964856";
+export const EQUIBOT_USER_ID = "1243063117852835941";
// Vencord
export const VC_SUPPORT_CHANNEL_ID = "1026515880080842772";