diff --git a/src/api/Settings.ts b/src/api/Settings.ts index ac116f547..ed38ad75e 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -220,6 +220,15 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) { } } +export function migrateSettingsToArrays(pluginName: string, settings: Array, stringSeparator: string = ",") { + for (const setting of settings) { + if (typeof SettingsStore.plain.plugins[pluginName][setting] !== "string") continue; + logger.info(`Migrating setting ${setting} from ${pluginName} to list`); + if (SettingsStore.plain.plugins[pluginName][setting] === "") SettingsStore.plain.plugins[pluginName][setting] = []; + else SettingsStore.plain.plugins[pluginName][setting] = SettingsStore.plain.plugins[pluginName][setting].split(stringSeparator); + } +} + export function definePluginSettings< Def extends SettingsDefinition, Checks extends SettingsChecks, diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 8b14283b8..2825022b7 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -43,7 +43,8 @@ import { SettingNumericComponent, SettingSelectComponent, SettingSliderComponent, - SettingTextComponent + SettingTextComponent, + SettingListComponent, } from "./components"; import { openContributorModal } from "./ContributorModal"; import { GithubButton, WebsiteButton } from "./LinkIconButton"; @@ -81,7 +82,11 @@ const Components: Record. +*/ + +import { Margins } from "@utils/margins"; +import { wordsFromCamel, wordsToTitle } from "@utils/text"; +import { PluginOptionSelect } from "@utils/types"; +import { Forms, useState, useEffect, ChannelStore, UserStore, GuildStore } from "@webpack/common"; + +import { ISettingElementProps } from "."; + +export function SettingListComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps) { + const [error, setError] = useState(null); + + const [items, setItems] = useState([]); + const [newItem, setNewItem] = useState(""); + + const addItem = () => { + if (newItem.trim() !== "") { + setItems([...items, newItem]); + setNewItem(""); + } + }; + + const removeItem = (index: number) => { + setItems(items.filter((_, i) => i !== index)); + }; + + + useEffect(() => { + onError(error !== null); + }, [error]); + + function handleChange(newValue) { + const isValid = option.isValid?.call(definedSettings, newValue) ?? true; + if (typeof isValid === "string") setError(isValid); + else if (!isValid) setError("Invalid input provided."); + else { + setError(null); + onChange(newValue); + } + } + + return ( + + {wordsToTitle(wordsFromCamel(id))} + {option.description} + + {error && {error}} + + ); +} diff --git a/src/components/PluginSettings/components/index.ts b/src/components/PluginSettings/components/index.ts index d307b4e68..d87f9d21a 100644 --- a/src/components/PluginSettings/components/index.ts +++ b/src/components/PluginSettings/components/index.ts @@ -33,6 +33,7 @@ export interface ISettingElementProps { export * from "../../Badge"; export * from "./SettingBooleanComponent"; export * from "./SettingCustomComponent"; +export * from "./SettingListComponent"; export * from "./SettingNumericComponent"; export * from "./SettingSelectComponent"; export * from "./SettingSliderComponent"; diff --git a/src/plugins/_api/settingLists.tsx b/src/plugins/_api/settingLists.tsx new file mode 100644 index 000000000..18b49062f --- /dev/null +++ b/src/plugins/_api/settingLists.tsx @@ -0,0 +1,121 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { Devs } from "@utils/constants"; +import { getIntlMessage } from "@utils/discord"; +import definePlugin, { OptionType } from "@utils/types"; +import { Menu, useState } from "@webpack/common"; + +function createContextMenu(name: "Guild" | "User" | "Channel", value: any) { + return ( + + {renderRegisteredPlugins(name, value)} + + ); +} + + +function renderRegisteredPlugins(name: "Guild" | "User" | "Channel", value: any) { + const type = name === "Guild" ? OptionType.GUILDS : name === "User" ? OptionType.USERS : OptionType.CHANNELS; + const plugins = registeredPlugins[type]; + + const [checkedItems, setCheckedItems] = useState>({}); + + const handleCheckboxClick = (plugin: string, setting: string) => { + const key = `${plugin}-${setting}`; + setCheckedItems(prevState => ({ + ...prevState, + [key]: !prevState[key] + })); + // @ts-ignore (cannot be undefined because settings have to exist for this to be called in the first place) + const s = Vencord.Plugins.plugins[plugin].settings.store[setting]; + Vencord.Plugins.plugins[plugin].settings.store[setting] = s.includes(value.id) + ? s.filter(id => id !== value.id) + : [...s, value.id]; + + }; + return Object.keys(plugins).map(plugin => ( + + {plugins[plugin].map(setting => ( + handleCheckboxClick(plugin, setting)} + checked={checkedItems[`${plugin}-${setting}`]} + /> + ))} + + )); +} + + +function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback { + return (children, props) => { + const value = props[name.toLowerCase()]; + if (!value) return; + if (props.label === getIntlMessage("CHANNEL_ACTIONS_MENU_LABEL")) return; // random shit like notification settings + + const lastChild = children.at(-1); + if (lastChild?.key === "developer-actions") { + const p = lastChild.props; + if (!Array.isArray(p.children)) + p.children = [p.children]; + + children = p.children; + } + children.splice(-1, 0, + createContextMenu(name, value) + ); + }; +} + + +// {type: {plugin: [setting, setting, setting]}} +const registeredPlugins: Record>> = { + [OptionType.USERS]: {}, + [OptionType.GUILDS]: {}, + [OptionType.CHANNELS]: {} +}; + + +// TODO find a better name + +export default definePlugin({ + name: "SettingListsAPI", + description: "API to automatically add context menus for settings", + authors: [Devs.Elvyra], + contextMenus: { + "channel-context": MakeContextCallback("Channel"), + "thread-context": MakeContextCallback("Channel"), + "guild-context": MakeContextCallback("Guild"), + "user-context": MakeContextCallback("User") + }, + + start() { + for (const plugin of Object.values(Vencord.Plugins.plugins)) { + if (!Vencord.Plugins.isPluginEnabled(plugin.name) || !plugin.settings) continue; + const settings = plugin.settings.def; + for (const settingKey of Object.keys(settings)) { + const setting = settings[settingKey]; + if (setting.type === OptionType.USERS || setting.type === OptionType.GUILDS || setting.type === OptionType.CHANNELS) { + if (!registeredPlugins[setting.type][plugin.name]) { + registeredPlugins[setting.type][plugin.name] = []; + } + registeredPlugins[setting.type][plugin.name].push(settingKey); + } + } + } + } +}); + diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 3d32d625e..ce05521e7 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -20,7 +20,7 @@ import "./messageLogger.css"; import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { updateMessage } from "@api/MessageUpdater"; -import { Settings } from "@api/Settings"; +import { definePluginSettings, migrateSettingsToArrays, Settings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; @@ -36,6 +36,66 @@ import overlayStyle from "./deleteStyleOverlay.css?managed"; import textStyle from "./deleteStyleText.css?managed"; import { openHistoryModal } from "./HistoryModal"; +migrateSettingsToArrays("MessageLogger", ["ignoreChannels", "ignoreGuilds", "ignoreUsers"]); + + +const settings = definePluginSettings({ + deleteStyle: { + type: OptionType.SELECT, + description: "The style of deleted messages", + options: [ + { label: "Red text", value: "text", default: true }, + { label: "Red overlay", value: "overlay" } + ], + onChange: () => addDeleteStyle() + }, + logDeletes: { + type: OptionType.BOOLEAN, + description: "Whether to log deleted messages", + default: true, + }, + collapseDeleted: { + type: OptionType.BOOLEAN, + description: "Whether to collapse deleted messages, similar to blocked messages", + default: false + }, + logEdits: { + type: OptionType.BOOLEAN, + description: "Whether to log edited messages", + default: true, + }, + inlineEdits: { + type: OptionType.BOOLEAN, + description: "Whether to display edit history as part of message content", + default: true + }, + ignoreBots: { + type: OptionType.BOOLEAN, + description: "Whether to ignore messages by bots", + default: false + }, + ignoreSelf: { + type: OptionType.BOOLEAN, + description: "Whether to ignore messages by yourself", + default: false + }, + ignoreUsers: { + type: OptionType.USERS, + description: "List of users to ignore", + popoutText: "Ignore deleted messages from this user", + }, + ignoreChannels: { + type: OptionType.CHANNELS, + description: "List of channel IDs to ignore", + popoutText: "Ignore deleted messages in this channel" + }, + ignoreGuilds: { + type: OptionType.GUILDS, + description: "List of guild IDs to ignore", + popoutText: "Ignore deleted messages in this guild", + }, +}); + interface MLMessage extends Message { deleted?: boolean; editHistory?: { timestamp: Date; content: string; }[]; @@ -145,7 +205,7 @@ export default definePlugin({ name: "MessageLogger", description: "Temporarily logs deleted and edited messages.", authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi], - dependencies: ["MessageUpdaterAPI"], + dependencies: ["MessageUpdaterAPI", "SettingListsAPI"], contextMenus: { "message": patchMessageContextMenu, @@ -192,63 +252,7 @@ export default definePlugin({ }; }, - options: { - deleteStyle: { - type: OptionType.SELECT, - description: "The style of deleted messages", - default: "text", - options: [ - { label: "Red text", value: "text", default: true }, - { label: "Red overlay", value: "overlay" } - ], - onChange: () => addDeleteStyle() - }, - logDeletes: { - type: OptionType.BOOLEAN, - description: "Whether to log deleted messages", - default: true, - }, - collapseDeleted: { - type: OptionType.BOOLEAN, - description: "Whether to collapse deleted messages, similar to blocked messages", - default: false - }, - logEdits: { - type: OptionType.BOOLEAN, - description: "Whether to log edited messages", - default: true, - }, - inlineEdits: { - type: OptionType.BOOLEAN, - description: "Whether to display edit history as part of message content", - default: true - }, - ignoreBots: { - type: OptionType.BOOLEAN, - description: "Whether to ignore messages by bots", - default: false - }, - ignoreSelf: { - type: OptionType.BOOLEAN, - description: "Whether to ignore messages by yourself", - default: false - }, - ignoreUsers: { - type: OptionType.STRING, - description: "Comma-separated list of user IDs to ignore", - default: "" - }, - ignoreChannels: { - type: OptionType.STRING, - description: "Comma-separated list of channel IDs to ignore", - default: "" - }, - ignoreGuilds: { - type: OptionType.STRING, - description: "Comma-separated list of guild IDs to ignore", - default: "" - }, - }, + settings: settings, handleDelete(cache: any, data: { ids: string[], id: string; mlDeleted?: boolean; }, isBulk: boolean) { try { diff --git a/src/utils/types.ts b/src/utils/types.ts index dd3c11576..3a0169b10 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -167,6 +167,10 @@ export const enum OptionType { SELECT, SLIDER, COMPONENT, + LIST, + USERS, // List of users + CHANNELS, // List of channels + GUILDS, // List of guilds } export type SettingsDefinition = Record; @@ -183,6 +187,7 @@ export type PluginSettingDef = ( | PluginSettingSliderDef | PluginSettingComponentDef | PluginSettingBigIntDef + | PluginSettingListDef ) & PluginSettingCommon; export interface PluginSettingCommon { @@ -259,6 +264,11 @@ export interface PluginSettingSliderDef { stickToMarkers?: boolean; } +export interface PluginSettingListDef{ + type: OptionType.LIST | OptionType.CHANNELS | OptionType.GUILDS | OptionType.USERS; + popoutText?: string; +} + export interface IPluginOptionComponentProps { /** * Run this when the value changes.