From 63eb6358fbd9218dde7dd0ce2ebea500ac7e0ac9 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 01:25:40 +0100 Subject: [PATCH 01/78] bleh --- src/api/Settings.ts | 9 ++ src/components/PluginSettings/PluginModal.tsx | 9 +- .../components/SettingListComponent.tsx | 66 ++++++++++ .../PluginSettings/components/index.ts | 1 + src/plugins/_api/settingLists.tsx | 121 +++++++++++++++++ src/plugins/messageLogger/index.tsx | 122 +++++++++--------- src/utils/types.ts | 10 ++ 7 files changed, 277 insertions(+), 61 deletions(-) create mode 100644 src/components/PluginSettings/components/SettingListComponent.tsx create mode 100644 src/plugins/_api/settingLists.tsx diff --git a/src/api/Settings.ts b/src/api/Settings.ts index ac116f547e8..ed38ad75e7c 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 8b14283b84a..2825022b76b 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 d307b4e6855..d87f9d21a2c 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 00000000000..18b49062fef --- /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 3d32d625e5b..ce05521e77e 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 dd3c115760f..3a0169b1032 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. From 826162952c959a5c1b87c2ec716138e82e96cdb4 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 01:50:32 +0100 Subject: [PATCH 02/78] make textreplace work --- src/plugins/_api/settingLists.tsx | 2 +- src/plugins/textReplace/index.tsx | 44 ++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/plugins/_api/settingLists.tsx b/src/plugins/_api/settingLists.tsx index 18b49062fef..0af3558c942 100644 --- a/src/plugins/_api/settingLists.tsx +++ b/src/plugins/_api/settingLists.tsx @@ -34,7 +34,7 @@ function renderRegisteredPlugins(name: "Guild" | "User" | "Channel", value: any) ...prevState, [key]: !prevState[key] })); - // @ts-ignore (cannot be undefined because settings have to exist for this to be called in the first place) + // @ts-ignore (can't 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) diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index 615477d079d..a35765744de 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -46,9 +46,6 @@ const makeEmptyRule: () => Rule = () => ({ }); const makeEmptyRuleArray = () => [makeEmptyRule()]; -let stringRules = makeEmptyRuleArray(); -let regexRules = makeEmptyRuleArray(); - const settings = definePluginSettings({ replace: { type: OptionType.COMPONENT, @@ -59,13 +56,13 @@ const settings = definePluginSettings({ <> @@ -74,6 +71,19 @@ const settings = definePluginSettings({ ); } }, + stringRules: { + type: OptionType.LIST, + hidden: true, + }, + regexRules: { + type: OptionType.LIST, + hidden: true, + }, + migrated: { + type: OptionType.BOOLEAN, + hidden: true, + default: false, + } }); function stringToRegex(str: string) { @@ -127,7 +137,8 @@ function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) if (index === rulesArray.length - 1) return; rulesArray.splice(index, 1); - await DataStore.set(rulesKey, rulesArray); + if (rulesKey === STRING_RULES_KEY) settings.store.stringRules = [...rulesArray]; + else settings.store.regexRules = [...rulesArray]; update(); } @@ -140,7 +151,8 @@ function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) rulesArray.splice(index, 1); - await DataStore.set(rulesKey, rulesArray); + if (rulesKey === STRING_RULES_KEY) settings.store.stringRules = [...rulesArray]; + else settings.store.regexRules = [...rulesArray]; update(); } @@ -211,8 +223,8 @@ function applyRules(content: string): string { if (content.length === 0) return content; - if (stringRules) { - for (const rule of stringRules) { + if (settings.store.stringRules) { + for (const rule of settings.store.stringRules) { if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; @@ -220,8 +232,8 @@ function applyRules(content: string): string { } } - if (regexRules) { - for (const rule of regexRules) { + if (settings.store.stringRulesregexRules) { + for (const rule of settings.store.stringRulesregexRules) { if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; @@ -249,8 +261,14 @@ export default definePlugin({ settings, async start() { - stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray(); - regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray(); + if (!settings.store.migrated) { + const stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray(); + const regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray(); + + settings.store.stringRules = stringRules; + settings.store.regexRules = regexRules; + settings.store.migrated = true; + } this.preSend = addPreSendListener((channelId, msg) => { // Channel used for sharing rules, applying rules here would be messy From 4f2a87a61042713a746749184478c9c1d88d0dc5 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:27:29 +0100 Subject: [PATCH 03/78] add lists to plugin modals (super shitty) also fixes some bugs lol --- src/components/PluginSettings/PluginModal.tsx | 6 +- .../components/SettingListComponent.tsx | 95 ++++++++++++++++--- src/plugins/_api/settingLists.tsx | 27 ++++-- src/plugins/messageLogger/index.tsx | 4 +- src/plugins/textReplace/index.tsx | 7 +- src/utils/types.ts | 3 + 6 files changed, 112 insertions(+), 30 deletions(-) diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 2825022b76b..b8d01276ae3 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -40,11 +40,11 @@ import { ISettingElementProps, SettingBooleanComponent, SettingCustomComponent, + SettingListComponent, SettingNumericComponent, SettingSelectComponent, SettingSliderComponent, SettingTextComponent, - SettingListComponent, } from "./components"; import { openContributorModal } from "./ContributorModal"; import { GithubButton, WebsiteButton } from "./LinkIconButton"; @@ -83,10 +83,10 @@ const Components: Record. */ +import ErrorBoundary from "@components/ErrorBoundary"; +import { Flex } from "@components/Flex"; 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 { OptionType, PluginOptionList } from "@utils/types"; +import { findComponentByCodeLazy } from "@webpack"; +import { Button,ChannelStore, Forms, GuildStore, React, useState } from "@webpack/common"; import { ISettingElementProps } from "."; +import { Channel } from "discord-types/general"; -export function SettingListComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps) { +const UserMentionComponent = findComponentByCodeLazy(".USER_MENTION)"); + +const CloseIcon = () => { + return ; +}; + +interface UserMentionComponentProps { + id: string; + channelId: string; + guildId: string; +} + +export function SettingListComponent({ + option, + pluginSettings, + definedSettings, + onChange, + onError, + id +}: ISettingElementProps) { const [error, setError] = useState(null); const [items, setItems] = useState([]); @@ -34,31 +59,75 @@ export function SettingListComponent({ option, pluginSettings, definedSettings, setItems([...items, newItem]); setNewItem(""); } + pluginSettings[id] = items; }; + if (items.length === 0 && pluginSettings[id].length !== 0) { + setItems(pluginSettings[id]); + } + const removeItem = (index: number) => { setItems(items.filter((_, i) => i !== index)); + pluginSettings[id] = items; }; - 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); + onChange(newValue); + } + + function wrapChannel(id: string) { + const channel = ChannelStore.getChannel(id) as Channel; + if (!channel) { + return "Unknown Channel"; } + return (GuildStore.getGuild(channel.guild_id)?.name ?? "Unknown Guild") + " - " + channel.name; } + // FIXME make channels and guilds nicer! return ( {wordsToTitle(wordsFromCamel(id))} {option.description} + + {items.map((item, index) => ( + + + {option.type === OptionType.USERS ? ( + + ) : option.type === OptionType.CHANNELS ? ( + {wrapChannel(item)} + ) : option.type === OptionType.GUILDS ? ( + + {GuildStore.getGuild(item)?.name || "Unknown Guild"} + + // TODO add logo to guild and channel? + ) : ( + {item} + )} + + + + ))} + + + {error && {error}} diff --git a/src/plugins/_api/settingLists.tsx b/src/plugins/_api/settingLists.tsx index 0af3558c942..fbf616adc44 100644 --- a/src/plugins/_api/settingLists.tsx +++ b/src/plugins/_api/settingLists.tsx @@ -8,7 +8,7 @@ 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"; +import { Menu, React } from "@webpack/common"; function createContextMenu(name: "Guild" | "User" | "Channel", value: any) { return ( @@ -26,16 +26,24 @@ 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 [checkedItems, setCheckedItems] = React.useState>( + Object.fromEntries( + Object.keys(plugins).flatMap(plugin => + plugins[plugin].map(setting => [`${plugin}-${setting}-${value.id}`, Vencord.Plugins.plugins[plugin].settings?.store[setting].includes(value.id)]) + ) + ) + ); const handleCheckboxClick = (plugin: string, setting: string) => { - const key = `${plugin}-${setting}`; + const key = `${plugin}-${setting}-${value.id}`; setCheckedItems(prevState => ({ ...prevState, [key]: !prevState[key] })); - // @ts-ignore (can't be undefined because settings have to exist for this to be called in the first place) + // @ts-ignore settings must be defined otherwise we wouldn't be here const s = Vencord.Plugins.plugins[plugin].settings.store[setting]; + // @ts-ignore Vencord.Plugins.plugins[plugin].settings.store[setting] = s.includes(value.id) ? s.filter(id => id !== value.id) : [...s, value.id]; @@ -49,10 +57,9 @@ function renderRegisteredPlugins(name: "Guild" | "User" | "Channel", value: any) {plugins[plugin].map(setting => ( handleCheckboxClick(plugin, setting)} - checked={checkedItems[`${plugin}-${setting}`]} + checked={checkedItems[`${plugin}-${setting}-${value.id}`]} /> ))} @@ -93,7 +100,7 @@ const registeredPlugins: Record = O extends PluginSettingStri O extends PluginSettingSelectDef ? O["options"][number]["value"] : O extends PluginSettingSliderDef ? number : O extends PluginSettingComponentDef ? any : + O extends PluginSettingListDef ? any[] : never; type PluginSettingDefaultType = O extends PluginSettingSelectDef ? ( O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined @@ -361,6 +363,7 @@ export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionComponent = PluginSettingComponentDef & PluginSettingCommon; +export type PluginOptionList = PluginSettingListDef & PluginSettingCommon; export type PluginNative any>> = { [key in keyof PluginExports]: From 83c0290549e43b9ef140f3aea5fa46348675930b Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:12:46 +0100 Subject: [PATCH 04/78] stuff --- src/components/PluginSettings/PluginModal.tsx | 2 +- .../components/SettingListComponent.tsx | 134 +++++++++++------- src/plugins/loadingQuotes/index.ts | 19 +-- src/plugins/textReplace/index.tsx | 4 +- src/utils/types.ts | 6 +- 5 files changed, 98 insertions(+), 67 deletions(-) diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index b8d01276ae3..64dad2a70e8 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -83,7 +83,7 @@ const Components: Record { ; }; +const CheckMarkIcon = () => { + return + + ; +}; + + interface UserMentionComponentProps { id: string; channelId: string; @@ -52,30 +59,21 @@ export function SettingListComponent({ const [error, setError] = useState(null); const [items, setItems] = useState([]); - const [newItem, setNewItem] = useState(""); - - const addItem = () => { - if (newItem.trim() !== "") { - setItems([...items, newItem]); - setNewItem(""); - } - pluginSettings[id] = items; - }; if (items.length === 0 && pluginSettings[id].length !== 0) { setItems(pluginSettings[id]); } const removeItem = (index: number) => { + if (items.length === 1) { + setItems([]); + pluginSettings[id] = []; + return; + } setItems(items.filter((_, i) => i !== index)); pluginSettings[id] = items; }; - - function handleChange(newValue) { - onChange(newValue); - } - function wrapChannel(id: string) { const channel = ChannelStore.getChannel(id) as Channel; if (!channel) { @@ -84,48 +82,86 @@ export function SettingListComponent({ return (GuildStore.getGuild(channel.guild_id)?.name ?? "Unknown Guild") + " - " + channel.name; } + /* Pseudocode for handling submit */ + function handleSubmit() { + // Handle the submit action for the specific item + // This could involve updating the state, making an API call, etc. + // Clear the input field after submission + const inputElement = document.getElementById(`vc-plugin-modal-input-${option.type === OptionType.CHANNELS ? "channel" : option.type === OptionType.GUILDS ? "guild" : option.type === OptionType.USERS ? "user" : "string"}`); + if (!inputElement || inputElement.value === "") { + return; + } + // TODO add searching for users, channels, and guilds lol + setItems([...items, inputElement.value]); + pluginSettings[id] = items; + inputElement.value = ""; + } + + // FIXME make channels and guilds nicer! + // TODO make string type work return ( {wordsToTitle(wordsFromCamel(id))} {option.description} - {items.map((item, index) => ( - - - {option.type === OptionType.USERS ? ( - - ) : option.type === OptionType.CHANNELS ? ( - {wrapChannel(item)} - ) : option.type === OptionType.GUILDS ? ( - - {GuildStore.getGuild(item)?.name || "Unknown Guild"} - - // TODO add logo to guild and channel? - ) : ( - {item} - )} - + {option.type === OptionType.USERS ? ( + + ) : option.type === OptionType.CHANNELS ? ( + {wrapChannel(item)} + ) : option.type === OptionType.GUILDS ? ( + + {GuildStore.getGuild(item)?.name || "Unknown Guild"} + + // TODO add logo to guild and channel? + ) : ( + {item} + )} + - - ))} - + ))} + + {/* Add a single input field */} + + {/* Add a submit button */} + + + diff --git a/src/plugins/loadingQuotes/index.ts b/src/plugins/loadingQuotes/index.ts index 9bfc88a73c9..395f1d06ea3 100644 --- a/src/plugins/loadingQuotes/index.ts +++ b/src/plugins/loadingQuotes/index.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, migrateSettingsToArrays } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; @@ -25,6 +25,8 @@ import presetQuotesText from "file://quotes.txt"; const presetQuotes = presetQuotesText.split("\n").map(quote => /^\s*[^#\s]/.test(quote) && quote.trim()).filter(Boolean) as string[]; const noQuotesQuote = "Did you really disable all loading quotes? What a buffoon you are..."; +migrateSettingsToArrays("LoadingQuotes", ["additionalQuotes"], "|"); + const settings = definePluginSettings({ replaceEvents: { description: "Should this plugin also apply during events with special event themed quotes? (e.g. Halloween)", @@ -42,14 +44,8 @@ const settings = definePluginSettings({ default: false }, additionalQuotes: { - description: "Additional custom quotes to possibly appear, separated by the below delimiter", - type: OptionType.STRING, - default: "", - }, - additionalQuotesDelimiter: { - description: "Delimiter for additional quotes", - type: OptionType.STRING, - default: "|", + description: "Additional custom quotes to possibly appear", + type: OptionType.ARRAY, }, }); @@ -79,16 +75,15 @@ export default definePlugin({ mutateQuotes(quotes: string[]) { try { - const { enableDiscordPresetQuotes, additionalQuotes, additionalQuotesDelimiter, enablePluginPresetQuotes } = settings.store; + const { enableDiscordPresetQuotes, additionalQuotes, enablePluginPresetQuotes } = settings.store; if (!enableDiscordPresetQuotes) quotes.length = 0; - if (enablePluginPresetQuotes) quotes.push(...presetQuotes); - quotes.push(...additionalQuotes.split(additionalQuotesDelimiter).filter(Boolean)); + quotes.push(...additionalQuotes); if (!quotes.length) quotes.push(noQuotesQuote); diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index 4d98d4bb23b..04500b7467e 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -72,12 +72,12 @@ const settings = definePluginSettings({ } }, stringRules: { - type: OptionType.LIST, + type: OptionType.ARRAY, hidden: true, description: "" }, regexRules: { - type: OptionType.LIST, + type: OptionType.ARRAY, hidden: true, description: "" }, diff --git a/src/utils/types.ts b/src/utils/types.ts index 1254f04cae3..b4aff0fa541 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -167,7 +167,7 @@ export const enum OptionType { SELECT, SLIDER, COMPONENT, - LIST, + ARRAY, USERS, // List of users CHANNELS, // List of channels GUILDS, // List of guilds @@ -265,7 +265,7 @@ export interface PluginSettingSliderDef { } export interface PluginSettingListDef{ - type: OptionType.LIST | OptionType.CHANNELS | OptionType.GUILDS | OptionType.USERS; + type: OptionType.ARRAY | OptionType.CHANNELS | OptionType.GUILDS | OptionType.USERS; popoutText?: string; hidePopout?: boolean; } @@ -305,7 +305,7 @@ type PluginSettingType = O extends PluginSettingStri O extends PluginSettingComponentDef ? any : O extends PluginSettingListDef ? any[] : never; -type PluginSettingDefaultType = O extends PluginSettingSelectDef ? ( +type PluginSettingDefaultType = O extends PluginSettingListDef ? any[] : O extends PluginSettingSelectDef ? ( O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined ) : O extends { default: infer T; } ? T : undefined; From 740780f2aea23c4cc3783f90ce98d55c81c21d07 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:14:50 +0100 Subject: [PATCH 05/78] renames --- src/plugins/_api/{settingLists.tsx => settingArrays.tsx} | 4 +--- src/plugins/messageLogger/index.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) rename src/plugins/_api/{settingLists.tsx => settingArrays.tsx} (98%) diff --git a/src/plugins/_api/settingLists.tsx b/src/plugins/_api/settingArrays.tsx similarity index 98% rename from src/plugins/_api/settingLists.tsx rename to src/plugins/_api/settingArrays.tsx index fbf616adc44..2720cf82cc9 100644 --- a/src/plugins/_api/settingLists.tsx +++ b/src/plugins/_api/settingArrays.tsx @@ -96,10 +96,8 @@ const registeredPlugins: Record Date: Fri, 3 Jan 2025 16:15:45 +0100 Subject: [PATCH 06/78] lint --- .../PluginSettings/components/SettingListComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PluginSettings/components/SettingListComponent.tsx b/src/components/PluginSettings/components/SettingListComponent.tsx index 26d7af6bd73..4abdc1f37ed 100644 --- a/src/components/PluginSettings/components/SettingListComponent.tsx +++ b/src/components/PluginSettings/components/SettingListComponent.tsx @@ -23,9 +23,9 @@ import { wordsFromCamel, wordsToTitle } from "@utils/text"; import { OptionType, PluginOptionList } from "@utils/types"; import { findComponentByCodeLazy } from "@webpack"; import { Button, ChannelStore, Forms, GuildStore, React, TextInput, useState } from "@webpack/common"; +import { Channel } from "discord-types/general"; import { ISettingElementProps } from "."; -import { Channel } from "discord-types/general"; const UserMentionComponent = findComponentByCodeLazy(".USER_MENTION)"); From 81979662470ce817f573969c606db6370179bba6 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:19:14 +0100 Subject: [PATCH 07/78] lint --- .../PluginSettings/components/SettingListComponent.tsx | 2 +- src/plugins/_api/settingArrays.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/PluginSettings/components/SettingListComponent.tsx b/src/components/PluginSettings/components/SettingListComponent.tsx index 4abdc1f37ed..4b7c289c70f 100644 --- a/src/components/PluginSettings/components/SettingListComponent.tsx +++ b/src/components/PluginSettings/components/SettingListComponent.tsx @@ -87,7 +87,7 @@ export function SettingListComponent({ // Handle the submit action for the specific item // This could involve updating the state, making an API call, etc. // Clear the input field after submission - const inputElement = document.getElementById(`vc-plugin-modal-input-${option.type === OptionType.CHANNELS ? "channel" : option.type === OptionType.GUILDS ? "guild" : option.type === OptionType.USERS ? "user" : "string"}`); + const inputElement = document.getElementById(`vc-plugin-modal-input-${option.type === OptionType.CHANNELS ? "channel" : option.type === OptionType.GUILDS ? "guild" : option.type === OptionType.USERS ? "user" : "string"}`) as HTMLInputElement; if (!inputElement || inputElement.value === "") { return; } diff --git a/src/plugins/_api/settingArrays.tsx b/src/plugins/_api/settingArrays.tsx index 2720cf82cc9..0c98022ddcd 100644 --- a/src/plugins/_api/settingArrays.tsx +++ b/src/plugins/_api/settingArrays.tsx @@ -57,6 +57,7 @@ function renderRegisteredPlugins(name: "Guild" | "User" | "Channel", value: any) {plugins[plugin].map(setting => ( handleCheckboxClick(plugin, setting)} checked={checkedItems[`${plugin}-${setting}-${value.id}`]} From abdfea15db073648b0f702c8a3ce284a723bb677 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:54:51 +0100 Subject: [PATCH 08/78] rename --- src/components/PluginSettings/PluginModal.tsx | 10 +- ...omponent.tsx => SettingArrayComponent.tsx} | 104 +++++++++--------- .../PluginSettings/components/index.ts | 2 +- src/plugins/_api/settingArrays.tsx | 6 +- 4 files changed, 61 insertions(+), 61 deletions(-) rename src/components/PluginSettings/components/{SettingListComponent.tsx => SettingArrayComponent.tsx} (61%) diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 64dad2a70e8..abf0ada8d3f 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -38,9 +38,9 @@ import { PluginMeta } from "~plugins"; import { ISettingElementProps, + SettingArrayComponent, SettingBooleanComponent, SettingCustomComponent, - SettingListComponent, SettingNumericComponent, SettingSelectComponent, SettingSliderComponent, @@ -83,10 +83,10 @@ const Components: Record {items.map((item, index) => ( - + {option.type === OptionType.USERS ? ( + + ) : option.type === OptionType.CHANNELS ? ( + {wrapChannel(item)} + ) : option.type === OptionType.GUILDS ? ( + + {GuildStore.getGuild(item)?.name || "Unknown Guild"} + + // TODO add logo to guild and channel? + ) : ( + {item} + )} + + + ))} - - {/* Add a single input field */} - - {/* Add a submit button */} - - + + {/* Add a single input field */} + + {/* Add a submit button */} + + diff --git a/src/components/PluginSettings/components/index.ts b/src/components/PluginSettings/components/index.ts index d87f9d21a2c..ac3f8297531 100644 --- a/src/components/PluginSettings/components/index.ts +++ b/src/components/PluginSettings/components/index.ts @@ -33,7 +33,7 @@ export interface ISettingElementProps { export * from "../../Badge"; export * from "./SettingBooleanComponent"; export * from "./SettingCustomComponent"; -export * from "./SettingListComponent"; +export * from "./SettingArrayComponent"; export * from "./SettingNumericComponent"; export * from "./SettingSelectComponent"; export * from "./SettingSliderComponent"; diff --git a/src/plugins/_api/settingArrays.tsx b/src/plugins/_api/settingArrays.tsx index 0c98022ddcd..bd990d13b82 100644 --- a/src/plugins/_api/settingArrays.tsx +++ b/src/plugins/_api/settingArrays.tsx @@ -10,7 +10,7 @@ import { getIntlMessage } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; import { Menu, React } from "@webpack/common"; -function createContextMenu(name: "Guild" | "User" | "Channel", value: any) { +function createContextMenu(name: string, value: any) { return ( Date: Fri, 3 Jan 2025 19:56:26 +0100 Subject: [PATCH 09/78] idek fixes or sum --- src/api/Settings.ts | 4 ++-- .../PluginSettings/components/SettingArrayComponent.tsx | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index ed38ad75e7c..0e2fcf89c99 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -220,9 +220,9 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) { } } -export function migrateSettingsToArrays(pluginName: string, settings: Array, stringSeparator: string = ",") { +export function migrateSettingsToArrays(pluginName: string, settings: string[], stringSeparator: string = ",") { for (const setting of settings) { - if (typeof SettingsStore.plain.plugins[pluginName][setting] !== "string") continue; + if (SettingsStore.plain.plugins[pluginName] === undefined || 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); diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 9718e872344..dbf47847037 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -91,7 +91,12 @@ export function SettingArrayComponent({ if (!inputElement || inputElement.value === "") { return; } - // TODO add searching for users, channels, and guilds lol + // TODO add picker for users? + if (option.type !== OptionType.ARRAY && !(inputElement.value.length >= 18 && inputElement.value.length <= 19 && !isNaN(Number(inputElement.value)))) { + setError("Value is not a valid snowflake ID"); + inputElement.value = ""; + return; + } setItems([...items, inputElement.value]); pluginSettings[id] = items; inputElement.value = ""; @@ -149,7 +154,7 @@ export function SettingArrayComponent({ {/* Add a single input field */} {/* Add a submit button */} From 2a5d84de7be525a76d24ce0f6d7fdd59aa5cb5b8 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 19:57:39 +0100 Subject: [PATCH 10/78] guhhh --- src/components/PluginSettings/components/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PluginSettings/components/index.ts b/src/components/PluginSettings/components/index.ts index ac3f8297531..419c222b29e 100644 --- a/src/components/PluginSettings/components/index.ts +++ b/src/components/PluginSettings/components/index.ts @@ -31,9 +31,9 @@ export interface ISettingElementProps { } export * from "../../Badge"; +export * from "./SettingArrayComponent"; export * from "./SettingBooleanComponent"; export * from "./SettingCustomComponent"; -export * from "./SettingArrayComponent"; export * from "./SettingNumericComponent"; export * from "./SettingSelectComponent"; export * from "./SettingSliderComponent"; From 716b75995b413ead0d83605900071fd8a41d9b55 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:35:05 +0100 Subject: [PATCH 11/78] update plugins --- src/api/Settings.ts | 2 +- .../components/SettingArrayComponent.tsx | 5 +- src/plugins/invisibleChat.desktop/index.tsx | 13 +++-- src/plugins/messageTags/index.ts | 53 ++++++++++++------- 4 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 0e2fcf89c99..0227c4e7f16 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -224,7 +224,7 @@ export function migrateSettingsToArrays(pluginName: string, settings: string[], for (const setting of settings) { if (SettingsStore.plain.plugins[pluginName] === undefined || 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] = []; + if (SettingsStore.plain.plugins[pluginName][setting] === "") SettingsStore.plain.plugins[pluginName][setting] = SettingsStore.plain.plugins[pluginName][setting].default ?? []; else SettingsStore.plain.plugins[pluginName][setting] = SettingsStore.plain.plugins[pluginName][setting].split(stringSeparator); } } diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index dbf47847037..c208e84dbae 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -91,7 +91,7 @@ export function SettingArrayComponent({ if (!inputElement || inputElement.value === "") { return; } - // TODO add picker for users? + // TODO add picker for users etc? if (option.type !== OptionType.ARRAY && !(inputElement.value.length >= 18 && inputElement.value.length <= 19 && !isNaN(Number(inputElement.value)))) { setError("Value is not a valid snowflake ID"); inputElement.value = ""; @@ -104,7 +104,6 @@ export function SettingArrayComponent({ // FIXME make channels and guilds nicer! - // TODO make string type work return ( {wordsToTitle(wordsFromCamel(id))} @@ -151,13 +150,11 @@ export function SettingArrayComponent({ marginTop: "10px", }} > - {/* Add a single input field */} - {/* Add a submit button */} + {removeButton(index)} - ))} - + + - - + + + - - {error && {error}} ); diff --git a/src/plugins/_api/settingArrays.tsx b/src/plugins/_api/settingArrays.tsx index 35b31c0b14c..7f2ae091620 100644 --- a/src/plugins/_api/settingArrays.tsx +++ b/src/plugins/_api/settingArrays.tsx @@ -53,10 +53,12 @@ function renderRegisteredPlugins(name: string, value: any) { {plugins[plugin].map(setting => ( handleCheckboxClick(plugin, setting)} @@ -104,9 +106,11 @@ export default definePlugin({ contextMenus: { "channel-context": MakeContextCallback("Channel"), "thread-context": MakeContextCallback("Channel"), + "gdm-context": MakeContextCallback("Channel"), // TODO make this work "guild-context": MakeContextCallback("Guild"), "user-context": MakeContextCallback("User") }, + required: true, start() { for (const plugin of Object.values(Vencord.Plugins.plugins)) { diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index f192f6677a0..cd57294e7fe 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -205,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", "SettingArraysAPI"], + dependencies: ["MessageUpdaterAPI"], contextMenus: { "message": patchMessageContextMenu, From 202cda9f99bad427ded1b5ec9f9e275d60b02051 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Sat, 4 Jan 2025 18:28:38 +0100 Subject: [PATCH 18/78] remove comment --- .../PluginSettings/components/SettingArrayComponent.tsx | 7 +++---- src/plugins/_api/settingArrays.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 60083d51ba3..e1e21c3c1e9 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -24,8 +24,6 @@ const getDMChannelIcon = findByCodeLazy(".getChannelIconURL({"); const GroupDMAvatars = findComponentByCodeLazy(".AvatarSizeSpecs[", "getAvatarURL"); -// FIXME saving is broken, so are indexes apparently? - const CloseIcon = () => { return + {channel.recipients.length >= 2 && channel.icon == null ? ( + + ) : ( + + )} + {channel.name} + ; + }; + if (dmChannels.length > 0) { elements.push(
@@ -187,19 +205,12 @@ export function SettingArrayComponent({ marginBottom: "8px", }} > - - {channel.recipients.length >= 2 && channel.icon == null ? ( - - ) : ( - - )} - {channel.name} - {removeButton(index)} - + {channel.recipients.length === 1 ? userMention(channel) : gdmComponent(channel)} + {removeButton(index)} ))} -
+ ); } Object.keys(channels).forEach(guildId => { From 334678d0de1c2ccfef05d475ce015f2be2dcb6ca Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Sat, 4 Jan 2025 18:56:23 +0100 Subject: [PATCH 23/78] improve buttons --- .../PluginSettings/components/SettingArrayComponent.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index f55bf99d030..e924f7bb445 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -65,10 +65,11 @@ export function SettingArrayComponent({ const removeButton = (index: number) => { return ( + {error && {error}} From 7c729de800d3dfebb93465adbd7f519b6c421583 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Sun, 5 Jan 2025 02:25:04 +0100 Subject: [PATCH 25/78] make searching from modal work --- .../components/SettingArrayComponent.tsx | 104 ++++++++++++++++-- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 0c574dd6443..81aba533cd4 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -7,13 +7,14 @@ import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; +import { debounce } from "@shared/debounce"; import { Margins } from "@utils/margins"; +import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { wordsFromCamel, wordsToTitle } from "@utils/text"; import { OptionType, PluginOptionList } from "@utils/types"; -import { findByCodeLazy, findComponentByCodeLazy } from "@webpack"; -import { Avatar, Button, ChannelStore, Forms, GuildStore, IconUtils, React, Text, TextInput, useEffect, useState } from "@webpack/common"; +import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { Avatar, Button, ChannelStore, Forms, GuildStore, IconUtils, React, Text, TextInput, useCallback, useEffect, useRef, useState } from "@webpack/common"; import { Channel, Guild } from "discord-types/general"; -import { JSX } from "react"; import { ISettingElementProps } from "."; @@ -22,9 +23,9 @@ const cl = classNameFactory("vc-plugin-modal-"); const UserMentionComponent = findComponentByCodeLazy(".USER_MENTION)"); const getDMChannelIcon = findByCodeLazy(".getChannelIconURL({"); const GroupDMAvatars = findComponentByCodeLazy(".AvatarSizeSpecs[", "getAvatarURL"); - -const SearchBar = findComponentByCodeLazy("focus(){let{current:"); - +const SearchBarModule = findByPropsLazy("SearchBar", "Checkbox"); +const SearchBarWrapper = findByPropsLazy("SearchBar", "Item"); +const SearchHandler = findByCodeLazy("createSearchContext", "setLimit"); const CloseIcon = () => { return
+
+ Search for { + option.type === OptionType.USERS ? "Users" : option.type === OptionType.CHANNELS ? "Channels" : "Guilds" + } + All selected items will be added to {wordsToTitle(wordsFromCamel(id))} +
+ +
+ + + - - + + - + ); } @@ -342,11 +376,16 @@ export function SettingArrayComponent({ if (text === "") { return; } - if (option.type !== OptionType.ARRAY && !(text.length >= 18 && text.length <= 19 && !isNaN(Number(text)))) { - openSearchModal(); - setText(""); - // FIXME - return; + if (option.type !== OptionType.ARRAY) { + if (isNaN(Number(text))) { + openSearchModal(text); + setText(""); + return; + } + if (!(text.length >= 18 && text.length <= 19)) { + setError("Invalid ID"); + return; + } } if (items.includes(text)) { @@ -398,7 +437,7 @@ export function SettingArrayComponent({ > setText(v)} value={text} @@ -414,7 +453,7 @@ export function SettingArrayComponent({ - + {text === "" ? null : + !isNaN(Number(text)) ? + : + < Button + id={cl("search-button")} + size={Button.Sizes.MIN} + onClick={() => openSearchModal(text)} + style={ + { background: "none" } + } + > + + + } {error && {error}} From 3db9a0039c74657e5949a3859176beb803320a55 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:26:26 +0100 Subject: [PATCH 32/78] change a bit more --- .../components/SettingArrayComponent.tsx | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 35ee43cb039..ea6b2912aef 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -437,32 +437,31 @@ export function SettingArrayComponent({ > setText(v)} value={text} /> - {text === "" ? null : - !isNaN(Number(text)) ? - : - < Button - id={cl("search-button")} - size={Button.Sizes.MIN} - onClick={() => openSearchModal(text)} - style={ - { background: "none" } - } - > - - + {!isNaN(Number(text)) || text === "" ? + : + < Button + id={cl("search-button")} + size={Button.Sizes.MIN} + onClick={() => openSearchModal(text)} + style={ + { background: "none" } + } + > + + } From 8af0bd52ad9fba534df4772416fa369c6ab60f06 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:47:08 +0100 Subject: [PATCH 33/78] flip buttons around --- .../PluginSettings/components/SettingArrayComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index ea6b2912aef..d2ef77cabf4 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -442,7 +442,7 @@ export function SettingArrayComponent({ onChange={v => setText(v)} value={text} /> - {!isNaN(Number(text)) || text === "" ? + {!isNaN(Number(text)) && text !== "" ? - - - - - ); - } - function openSearchModal(val?: string) { const key = openModal(modalProps => ( closeModal(key)} - val={val} + input={val} + subText={"All selected items will be added to " + wordsToTitle(wordsFromCamel(id))} + searchType={option.type === OptionType.USERS ? "USERS" : option.type === OptionType.CHANNELS ? "CHANNELS" : "GUILDS"} + onSubmit={v => console.log(v)} /> )); } @@ -262,37 +180,36 @@ export function SettingArrayComponent({ switch (type) { case 2: return ; case 5: return ; case 13: return ; case 15: return ; default: // Text channel icon return ; } }; - // collapsible guild list with channels in it const channels: Record = {}; const dmChannels: Channel[] = []; const elements: React.JSX.Element[] = []; @@ -311,14 +228,14 @@ export function SettingArrayComponent({ channels[channel.guild_id].push(channel); } - const userMention = channel => { + const userMention = (channel: Channel) => { return ; }; - const gdmComponent = channel => { + const gdmComponent = (channel: Channel) => { return {channel.recipients.length >= 2 && channel.icon == null ? ( diff --git a/src/plugins/consoleJanitor/index.ts b/src/plugins/consoleJanitor/index.ts index d251ff740b5..7c6fef467f9 100644 --- a/src/plugins/consoleJanitor/index.ts +++ b/src/plugins/consoleJanitor/index.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, migrateSettingsToArrays } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType, StartAt } from "@utils/types"; @@ -24,6 +24,8 @@ const NoopLogger = { const logAllow = new Set(); +migrateSettingsToArrays("consoleJanitor", ["whitelistedLoggers"], s => s.split(";").map(x => x.trim())); + const settings = definePluginSettings({ disableLoggers: { type: OptionType.BOOLEAN, @@ -38,12 +40,12 @@ const settings = definePluginSettings({ restartNeeded: true }, whitelistedLoggers: { - type: OptionType.STRING, - description: "Semi colon separated list of loggers to allow even if others are hidden", - default: "GatewaySocket; Routing/Utils", - onChange(newVal: string) { + type: OptionType.ARRAY, + description: "List of loggers to allow even if others are hidden", + default: ["GatewaySocket", "Routing/Utils"], + onChange(newVal: string[]) { logAllow.clear(); - newVal.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow)); + newVal.forEach(logAllow.add.bind(logAllow)); } } }); @@ -57,7 +59,7 @@ export default definePlugin({ startAt: StartAt.Init, start() { logAllow.clear(); - this.settings.store.whitelistedLoggers?.split(";").map(x => x.trim()).forEach(logAllow.add.bind(logAllow)); + this.settings.store.whitelistedLoggers.forEach(logAllow.add.bind(logAllow)); }, NoopLogger: () => NoopLogger, diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx index e8a8c65d982..e799ad4f29c 100644 --- a/src/plugins/invisibleChat.desktop/index.tsx +++ b/src/plugins/invisibleChat.desktop/index.tsx @@ -93,7 +93,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { }; -migrateSettingsToArrays("InvisibleChat", ["savedPasswords"]); +migrateSettingsToArrays("InvisibleChat", ["savedPasswords"], s => s.split(",").map(s => s.trim())); const settings = definePluginSettings({ savedPasswords: { @@ -209,7 +209,7 @@ export function isCorrectPassword(result: string): boolean { } export async function iteratePasswords(message: Message): Promise { - const passwords = settings.store.savedPasswords.map(s => s.trim()); + const passwords = settings.store.savedPasswords; if (!message?.content || !passwords?.length) return false; diff --git a/src/utils/types.ts b/src/utils/types.ts index 679085818c7..8b35e732175 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -279,6 +279,8 @@ export interface PluginSettingArrayDef { popoutText?: string; hidePopout?: boolean; default?: any[]; + + onChange?(newValue: any[]): void; } export interface IPluginOptionComponentProps { From b1666685223ef8507165dcc015ce2f56d9f1c263 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:48:32 +0100 Subject: [PATCH 35/78] blep --- src/api/Settings.ts | 8 ++++---- .../PluginSettings/components/SettingArrayComponent.tsx | 7 +++---- src/plugins/consoleJanitor/index.ts | 6 +++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 64a71b8af6b..1c3b634aec4 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -226,12 +226,12 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) { export function migrateSettingsToArrays(pluginName: string, settings: string[], stringSeparator: string | ((input: string) => string[]) = ",") { const { plugins } = SettingsStore.plain; - + if (plugins[pluginName] === undefined) + return logger.error(`Plugin '${pluginName}' does not exist and cannot be migrated! Did you spell it correctly?`); for (const setting of settings) { - if (plugins[pluginName] === undefined || typeof plugins[pluginName][setting] !== "string") continue; + if (typeof plugins[pluginName][setting] !== "string") continue; logger.info(`Migrating setting ${setting} from ${pluginName} to list`); - // @ts-ignore - if (plugins[pluginName][setting] === "") plugins[pluginName][setting] = plugins[pluginName][setting].default ?? []; + if (plugins[pluginName][setting] === "") plugins[pluginName][setting] = []; else if (typeof stringSeparator === "string") plugins[pluginName][setting] = plugins[pluginName][setting].split(stringSeparator); else plugins[pluginName][setting] = stringSeparator(plugins[pluginName][setting]); } diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index d739914e284..874888e56e7 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -8,7 +8,7 @@ import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import SearchModal from "@components/SearchModal"; import { Margins } from "@utils/margins"; -import { closeModal, openModal } from "@utils/modal"; +import { openModal } from "@utils/modal"; import { wordsFromCamel, wordsToTitle } from "@utils/text"; import { OptionType, PluginOptionArray } from "@utils/types"; import { findByCodeLazy, findComponentByCodeLazy } from "@webpack"; @@ -104,14 +104,13 @@ export function SettingArrayComponent({ } function openSearchModal(val?: string) { - const key = openModal(modalProps => ( + return openModal(modalProps => ( closeModal(key)} input={val} subText={"All selected items will be added to " + wordsToTitle(wordsFromCamel(id))} searchType={option.type === OptionType.USERS ? "USERS" : option.type === OptionType.CHANNELS ? "CHANNELS" : "GUILDS"} - onSubmit={v => console.log(v)} + onSubmit={values => setItems([...items, ...values.map(v => v.id)])} /> )); } diff --git a/src/plugins/consoleJanitor/index.ts b/src/plugins/consoleJanitor/index.ts index 7c6fef467f9..29b225cabd9 100644 --- a/src/plugins/consoleJanitor/index.ts +++ b/src/plugins/consoleJanitor/index.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { definePluginSettings, migrateSettingsToArrays } from "@api/Settings"; +import { definePluginSettings, migratePluginSettings, migrateSettingsToArrays } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType, StartAt } from "@utils/types"; @@ -24,7 +24,7 @@ const NoopLogger = { const logAllow = new Set(); -migrateSettingsToArrays("consoleJanitor", ["whitelistedLoggers"], s => s.split(";").map(x => x.trim())); +migrateSettingsToArrays("ConsoleJanitor", ["whitelistedLoggers"], s => s.split(";").map(x => x.trim())); const settings = definePluginSettings({ disableLoggers: { @@ -59,7 +59,7 @@ export default definePlugin({ startAt: StartAt.Init, start() { logAllow.clear(); - this.settings.store.whitelistedLoggers.forEach(logAllow.add.bind(logAllow)); + settings.store.whitelistedLoggers.forEach(logAllow.add.bind(logAllow)); }, NoopLogger: () => NoopLogger, From 900cd46e5467593f4b9de5077a4fbab112fa3e32 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:34:25 +0100 Subject: [PATCH 36/78] it works omg --- src/components/SearchModal.css | 52 ++ src/components/SearchModal.tsx | 731 ++++++++++++++++++++++++++++ src/plugins/consoleJanitor/index.ts | 2 +- 3 files changed, 784 insertions(+), 1 deletion(-) create mode 100644 src/components/SearchModal.css create mode 100644 src/components/SearchModal.tsx diff --git a/src/components/SearchModal.css b/src/components/SearchModal.css new file mode 100644 index 00000000000..1d4e2ef7129 --- /dev/null +++ b/src/components/SearchModal.css @@ -0,0 +1,52 @@ +.vc-search-modal-destination-row { + display: flex; + min-height: 48px; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0 8px; + margin-left: 16px; + margin-right: 8px; + border-radius: 4px; + cursor: pointer +} + +.vc-search-modal-identity { + display: flex; + flex-shrink: 1; + flex-direction: row; + align-items: center; + gap: 12px; + overflow: hidden +} + +.vc-search-modal-labels { + display: flex; + flex-direction: column; + overflow: hidden; + margin: 4px 0 +} + +.vc-search-modal-checkbox { + flex: 0; + margin-left: 16px +} + +.vc-search-modal-label { + color: var(--header-primary) +} + +.vc-search-modal-sub-label { + color: var(--header-muted) +} + +.vc-search-modal-sub-label-icon { + width: 12px; + height: 12px; + margin-right: 2px +} + +.vc-search-modal-thread-sub-label { + display: flex; + align-items: center +} diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx new file mode 100644 index 00000000000..c87a0a0678e --- /dev/null +++ b/src/components/SearchModal.tsx @@ -0,0 +1,731 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./SearchModal.css"; + +import { classNameFactory } from "@api/Styles"; +import { + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalProps, + ModalRoot, + ModalSize +} from "@utils/modal"; +import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { + Button, ChannelStore, + Flex, GuildStore, + Heading, + PresenceStore, + React, + RelationshipStore, + Text, + useCallback, + useMemo, + useRef, + UsernameUtils, UserStore, + useState +} from "@webpack/common"; +import { Channel, User } from "discord-types/general"; + +const cl = classNameFactory("vc-search-modal-"); + +// TODO make guilds work +// FIXME fix the no results display + +// TODO add all channel types + +// TODO filter for input type +// TODO setting for max amount of selected items + +// FIXME remove scrolling up onclick +// FIXME move selected items to the top of the list. + +const SearchBarModule = findByPropsLazy("SearchBar", "Checkbox", "AvatarSizes"); +const SearchBarWrapper = findByPropsLazy("SearchBar", "Item"); +const TextTypes = findByPropsLazy("APPLICATION", "GROUP_DM", "GUILD"); +const FrequencyModule = findByPropsLazy("getFrequentlyWithoutFetchingLatest"); +const ConnectionModule = findByPropsLazy("isConnected", "getSocket"); +const FrequentsModule = findByPropsLazy("getChannelHistory", "getFrequentGuilds"); + +const wrapperFn = findByCodeLazy("prevDeps:void 0,"); +const convertItem = findByCodeLazy("GROUP_DM:return{", "GUILD_VOICE:case"); +const loadFunction = findByCodeLazy(".frecencyWithoutFetchingLatest)"); +const SearchHandler = findByCodeLazy("createSearchContext", "setLimit"); +const navigatorWrapper = findByCodeLazy("useMemo(()=>({onKeyDown:"); +const createNavigator = findByCodeLazy(".keyboardModeEnabled)", "useCallback(()=>new Promise(", "Number.MAX_SAFE_INTEGER"); +const getChannelLabel = findByCodeLazy("recipients.map(", "getNickname("); +const ChannelIcon = findByCodeLazy("channelGuildIcon,"); + +const GroupDMAvatars = findComponentByCodeLazy("facepileSizeOverride", "recipients.length"); + +interface DestinationItemProps { + type: string; + id: string; +} + +interface UnspecificRowProps { + key: string + destination: DestinationItemProps, + rowMode: string + disabled: boolean, + isSelected: boolean, + onPressDestination: (destination: DestinationItemProps) => void, + "aria-posinset": number, + "aria-setsize": number +} +interface SpecificRowProps extends UnspecificRowProps { + icon: React.JSX.Element, + label: string, + subLabel: string | React.JSX.Element +} + +interface UserIconProps { + user: User; + size?: number; + animate?: boolean; + "aria-hidden"?: boolean; + [key: string]: any; // To allow any additional props +} + +const searchTypesToResultTypes = (type: string | string[]) => { + if (type === "ALL") return ["USER", "TEXT_CHANNEL", "VOICE_CHANNEL", "GROUP_DM", "GUILD"]; + if (typeof type === "string") { + if (type === "USERS") return ["USER"]; + else if (type === "CHANNELS") return ["TEXT_CHANNEL", "VOICE_CHANNEL", "GROUP_DM"]; + else if (type === "GUILDS") return ["GUILD"]; + } else { + return type.flatMap(searchTypesToResultTypes); + } +}; + +function searchTypeToText(type: string | string[]) { + if (type === undefined || type === "ALL") return "Users, Channels, and Servers"; + if (typeof type === "string") { + if (type === "GUILD") return "Servers"; + else return type.charAt(0) + type.slice(1).toLowerCase(); + } else { + if (type.length === 1) { + return searchTypeToText(type[0]); + } else if (type.length === 2) { + return `${searchTypeToText(type[0])} and ${searchTypeToText(type[1])}`; + } else { + return "Users, Channels, and Servers"; + } + } +} + +export default function SearchModal({ modalProps, onSubmit, input, searchType = "ALL", subText }: { + modalProps: ModalProps; + onSubmit(selected: DestinationItemProps[]): void; + input?: string; + searchType?: ("USERS" | "CHANNELS" | "GUILDS")[] | "USERS" | "CHANNELS" | "GUILDS" | "ALL"; + subText?: string +}) { + + const callbacks = new Map(); + + function registerCallback(key, callback) { + let currentCallbacks = callbacks.get(key); + if (!currentCallbacks) { + currentCallbacks = new Set(); + callbacks.set(key, currentCallbacks); + } + + currentCallbacks.add(callback); + + return () => { + currentCallbacks.delete(callback); + if (currentCallbacks.size === 0) { + callbacks.delete(key); + } + }; + + } + + const UserIcon = React.memo(function ({ + user, + size = SearchBarModule.AvatarSizes.SIZE_32, + animate = false, + "aria-hidden": ariaHidden = false, + ...rest + }: UserIconProps) { + + const avatarSrc = user.getAvatarURL(void 0, SearchBarModule.getAvatarSize(size), animate); + + return ( + + ); + }); + + const resultTypes = searchTypesToResultTypes(searchType); + + const [selected, setSelected] = useState([]); + + const refCounter = useRef(0); + + const rowContext = React.createContext({ + id: "NO_LIST", + setFocus(id: string) { + } + }); + + const Row = (props: SpecificRowProps) => { + const { + destination, + rowMode, + icon, + label, + subLabel, + isSelected, + disabled, + onPressDestination, + ...rest + } = props; + + const interactionProps = generateRowData(destination.id); + + const handlePress = useCallback(() => { + onPressDestination?.(destination); + }, [onPressDestination, destination]); + + return ( + +
+
{icon}
+
+ {label} + {subLabel} +
+
+ +
+ ); + }; + + function generateUserItem(user: User, otherProps: UnspecificRowProps) { + const username = UsernameUtils.getName(user); + const userTag = UsernameUtils.getUserTag(user, { decoration: "never" }); + const nickname = RelationshipStore.getNickname(user.id); + const userStatus = PresenceStore.getStatus(user.id); + + return ( + } + label={nickname ?? username} + subLabel={userTag} + /> + ); + } + + function generateChannelLabel(channel: Channel) { + return getChannelLabel(channel, UserStore, RelationshipStore, false); + } + + function generateChannelItem(channel: Channel, otherProps: UnspecificRowProps) { + const guild = GuildStore.getGuild(channel?.guild_id); + + const channelLabel = generateChannelLabel(channel); + + const parentChannelLabel = () => { + const parentChannel = ChannelStore.getChannel(channel.parent_id); + return parentChannel ? getChannelLabel(parentChannel, UserStore, RelationshipStore, false) : null; + }; + + let subLabel: string | React.JSX.Element = guild?.name; + + // @ts-ignore isForumPost is not in the types but exists + if (channel.isThread() || channel.isForumPost()) { + // @ts-ignore + const IconComponent = channel.isForumPost() ? SearchBarModule.ForumIcon : SearchBarModule.TextIcon; + + subLabel = ( +
+ + + {parentChannelLabel()} + +
+ ); + } + + return ( + + } + label={channelLabel} + subLabel={subLabel} + /> + ); + } + + function generateGdmItem(channel: Channel, otherProps: UnspecificRowProps) { + function getParticipants(channel: Channel) { + const userNames = channel.recipients + .map(recipient => UserStore.getUser(recipient)) + .filter(user => user != null) + .map(user => UsernameUtils.getName(user)); + + if (!userNames || userNames.length === 0 || channel.name === "") + return null; + if (userNames.length <= 3) + return userNames.join(", "); + const amount = userNames.length - 3; + return userNames?.slice(0, 3).join(", ") + " and " + (amount === 1 ? "1 other" : amount + " others"); + } + + const label = getChannelLabel(channel, UserStore, RelationshipStore, false); + const subLabelValue = getParticipants(channel); + + return ( + } + label={label} + subLabel={subLabelValue ?? ""} + /> + ); + } + + const navigatorContext = React.createContext({ + id: "NO_LIST", + onKeyDown() { + }, + orientation: "vertical", + ref: React.createRef(), + tabIndex: -1 + }); + + function generateNavigatorData() { + const { id: id, onKeyDown, ref, tabIndex } = React.useContext(navigatorContext); + return { + role: "list", + tabIndex, + "data-list-id": id, + onKeyDown: onKeyDown, + ref: ref, + }; + } + + function navigatorData(e) { + const { children } = e; + return children(generateNavigatorData()); + } + + + function generateRowData(rowId: string) { + const [tabIndex, setTabIndex] = useState(-1); + + const { id, setFocus } = React.useContext(rowContext); + + const handleFocus = useCallback(() => setFocus(rowId), [rowId, setFocus]); + + React.useLayoutEffect(() => { + return registerCallback(id, (tabIndex, id) => { + setTabIndex(id && tabIndex === rowId ? 0 : -1); + }); + }, [rowId, id]); + + return { + role: "listitem", + "data-list-item-id": `${id}___${rowId}`, + tabIndex, + onFocus: handleFocus, + }; + } + + const [searchText, setSearchText] = useState(input || ""); + const ref = {}; + + function getSearchHandler(e) { + const { searchOptions } = e; + const [results, setResults] = useState({ + results: [], + query: "" + }); + + function getRef(e) { // FIXME probably should use a proper type for this + const ref_ = useRef(ref); + if (ref_.current === ref) + ref_.current = e(); + return ref_.current; + } + + const searchHandler: typeof SearchHandler = getRef(() => { + const searchHandler = new SearchHandler((r, q) => { + setResults({ + results: r, + query: q + }); + } + ); + searchHandler.setLimit(20); + searchHandler.search(""); + return searchHandler; + } + ); + React.useEffect(() => () => searchHandler.destroy(), [searchHandler]); + React.useEffect(() => { + searchOptions != null && searchOptions !== searchHandler.options && searchHandler.setOptions(searchOptions); + }, [searchHandler, searchOptions] + ); + return { + search: useCallback(e => { + const { query, resultTypes } = e; + if (searchHandler.resultTypes == null || !(resultTypes.length === searchHandler.resultTypes.size && resultTypes.every(e => searchHandler.resultTypes.has(e)))) { + searchHandler.setResultTypes(resultTypes); + searchHandler.setLimit(resultTypes.length === 1 ? 50 : 20); + } + searchHandler.search(query.trim() === "" ? "" : query); + } + , [searchHandler]), + ...results + }; + } + + function generateResults({ selectedDestinations }) { + const { search, query, results } = getSearchHandler({ + blacklist: null, + frecencyBoosters: !0, + userFilters: null + }); + + const [queryData, setQueryData] = useState(""); + + const updateSearch = useCallback((e: string) => setQueryData(e), [setQueryData]); + + if (queryData === "" && searchText !== "") { + updateSearch(searchText); + } + + const [pinned, setPinned] = useState(selectedDestinations != null ? selectedDestinations : []); + React.useLayoutEffect(() => { + search({ + query: queryData, + resultTypes: resultTypes, + }); + setPinned(selectedDestinations != null ? selectedDestinations : []); + } + , [search, queryData]); + + loadFunction(); + + const frequentChannels = wrapperFn([FrequencyModule], () => FrequencyModule.getFrequentlyWithoutFetchingLatest()); + const isConnected = wrapperFn([ConnectionModule], () => ConnectionModule.isConnected()); + const hasQuery = query !== ""; + + function getItem(e) { + if (e.type !== "user") + return convertItem(e.id); + { + const user = UserStore.getUser(e.id); + return { + type: TextTypes.USER, + record: user, + score: 0, + // @ts-ignore globalName is not in the types but exists + comparator: user.globalName, + }; + } + } + + function processItems(items, existingItems?) { + let temp: null; + const set = new Set(existingItems || []); + + const array: any[] = []; + + items.forEach(item => { + if (item != null) { + if (item.type === TextTypes.HEADER) temp = item; + else { + const { id } = item.record; + if (!set.has(id)) { + set.add(item); + if (temp != null) { + array.push(temp); + temp = null; + } + array.push(item); + } + } + } + }); + return array; + } + + const filterItems = (items: any[]) => { + return items.filter( + item => item != null && (item.type === TextTypes.HEADER || resultTypes.includes(item.type)) + ); + }; + + function filterResults(e) { + const removeDuplicates = (arr: any[]) => { + const clean: any[] = []; + const seenIds = new Set(); + arr.forEach(item => { + if (item == null || item.record == null) return; + const id = item.type === "user" ? item.id : item.record.id; + if (!seenIds.has(id)) { + seenIds.add(id); + clean.push(item); + } + }); + return clean; + }; + + const { results, hasQuery, frequentChannels, pinnedDestinations } = e; + if (hasQuery) return processItems(filterItems(results)); + + const channelHistory = FrequentsModule.getChannelHistory(); + + const recentDestinations = filterItems([ + ...(channelHistory.length > 0 ? channelHistory.map(e => convertItem(e)) : []), + ...(frequentChannels.length > 0 ? frequentChannels.map(e => convertItem(e.id)) : []) + ]); + + const destinations = removeDuplicates( + [...(pinnedDestinations.length > 0 ? pinnedDestinations.map(e => getItem(e)) : []), + ...recentDestinations + ]); + + return processItems(destinations).slice(0, 15); + } + + return { + results: useMemo(() => filterResults({ + results: results, + hasQuery: hasQuery, + frequentChannels: frequentChannels, + pinnedDestinations: pinned, + isConnected: isConnected + }), [results, hasQuery, frequentChannels, pinned, isConnected]), + updateSearchText: updateSearch + }; + } + + const { results, updateSearchText } = generateResults({ + selectedDestinations: selected, + }); + + const selectedDestinationKeys = useMemo(() => { + return selected?.map(destination => `${destination.type}-${destination.id}`) || []; + }, [selected]); + + const rowHeight = useCallback(() => 48, []); + + function ModalScroller(e) { + + const { rowData: t, handleToggleDestination, ...extraProps } = e; + const sectionCount = useMemo(() => [t.length], [t.length]); + + const callback = useCallback(e => { + const { section, row } = e; + if (section > 0) + return; + const { type, record } = results[row]; + if (type === TextTypes.HEADER) + return; + + const destination = { + type: type === TextTypes.USER ? "user" : "channel", + id: record.id + }; + + const key = `${destination.type}-${destination.id}`; + + + const rowProps: UnspecificRowProps = { + key, + destination, + rowMode: "toggle", + disabled: false, + isSelected: selectedDestinationKeys.includes(key), + onPressDestination: handleToggleDestination, + "aria-posinset": row + 1, + "aria-setsize": results.length + }; + + if (type === TextTypes.USER) + return generateUserItem(record, rowProps); + if (type === TextTypes.GROUP_DM) + return generateGdmItem(record, rowProps); + if (type === TextTypes.TEXT_CHANNEL || type === TextTypes.VOICE_CHANNEL) { + return generateChannelItem(record, rowProps); + } else throw new Error("Unknown type " + type); + }, [results, selectedDestinationKeys, handleToggleDestination]); + const navRef = useRef(null); + const nav = createNavigator(cl("search-modal"), navRef); + + return navigatorWrapper({ + navigator: nav, + children: navigatorData({ + children: e => { + const { ref, ...data } = e; + return { + navRef.current = elem; + ref.current = elem?.getScrollerNode() ?? null; + } + } + {...data} + {...extraProps} + sections={sectionCount} + sectionHeight={0} + renderRow={callback} + rowHeight={rowHeight}/>; + } + }) + }); + } + + + const setSelectedCallback = useCallback(e => { + setSelected(currentSelected => { + const index = currentSelected.findIndex(item => { + const { type, id } = item; + return type === e.type && id === e.id; + }); + + if (index === -1) { + /* if (currentSelected.length >= 5) { TODO add this later + $(""); // Handle the case when max selection is reached + return currentSelected; + } */ + + refCounter.current += 1; + return [e, ...currentSelected]; + } + + refCounter.current += 1; + currentSelected.splice(index, 1); + return [...currentSelected]; + }); + }, [selected]); + + + return ( + + +
+
+ {"Search for " + searchTypeToText(searchType)} + {subText !== undefined && {subText}} +
+ +
+ { + setSearchText(v); + updateSearchText(v); + }} + onClear={() => { + setSearchText(""); + updateSearchText(""); + }} + setFocus={true} + /> +
+ { + results.length > 0 ? : + No results found + + } + + + + + + +
+ ); +} diff --git a/src/plugins/consoleJanitor/index.ts b/src/plugins/consoleJanitor/index.ts index 29b225cabd9..e7bbecedd74 100644 --- a/src/plugins/consoleJanitor/index.ts +++ b/src/plugins/consoleJanitor/index.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { definePluginSettings, migratePluginSettings, migrateSettingsToArrays } from "@api/Settings"; +import { definePluginSettings, migrateSettingsToArrays } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType, StartAt } from "@utils/types"; From ebd6a2b27b443bbb6d186ff2dcd573c592c04089 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Fri, 10 Jan 2025 22:47:06 +0100 Subject: [PATCH 37/78] remove comments and fix scrolling --- src/components/SearchModal.tsx | 35 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index c87a0a0678e..4f03c19bbd5 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -38,14 +38,6 @@ const cl = classNameFactory("vc-search-modal-"); // TODO make guilds work // FIXME fix the no results display -// TODO add all channel types - -// TODO filter for input type -// TODO setting for max amount of selected items - -// FIXME remove scrolling up onclick -// FIXME move selected items to the top of the list. - const SearchBarModule = findByPropsLazy("SearchBar", "Checkbox", "AvatarSizes"); const SearchBarWrapper = findByPropsLazy("SearchBar", "Item"); const TextTypes = findByPropsLazy("APPLICATION", "GROUP_DM", "GUILD"); @@ -79,6 +71,7 @@ interface UnspecificRowProps { "aria-posinset": number, "aria-setsize": number } + interface SpecificRowProps extends UnspecificRowProps { icon: React.JSX.Element, label: string, @@ -90,7 +83,7 @@ interface UserIconProps { size?: number; animate?: boolean; "aria-hidden"?: boolean; - [key: string]: any; // To allow any additional props + [key: string]: any; } const searchTypesToResultTypes = (type: string | string[]) => { @@ -203,7 +196,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = return ( { handlePress(); e.preventDefault(); e.stopPropagation(); }} aria-selected={isSelected} {...interactionProps} {...rest} @@ -329,9 +322,10 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = return ( } - label={label} - subLabel={subLabelValue ?? ""} + icon={} + label={label} + subLabel={subLabelValue ?? ""} /> ); } @@ -536,10 +530,10 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const destinations = removeDuplicates( [...(pinnedDestinations.length > 0 ? pinnedDestinations.map(e => getItem(e)) : []), - ...recentDestinations + ...recentDestinations ]); - return processItems(destinations).slice(0, 15); + return processItems(destinations); } return { @@ -639,11 +633,6 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = }); if (index === -1) { - /* if (currentSelected.length >= 5) { TODO add this later - $(""); // Handle the case when max selection is reached - return currentSelected; - } */ - refCounter.current += 1; return [e, ...currentSelected]; } @@ -682,7 +671,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = size={SearchBarModule.SearchBar.Sizes.MEDIUM} placeholder="Search" query={searchText} - onChange={v => { + onChange={(v: string) => { setSearchText(v); updateSearchText(v); }} @@ -690,7 +679,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = setSearchText(""); updateSearchText(""); }} - setFocus={true} + autoFocus={true} /> { @@ -715,7 +704,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = modalProps.onClose(); }} > - Confirm + {"Add" + (selected.length > 1 ? " (" + selected.length + ")" : "")} @@ -762,7 +766,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = look={Button.Looks.LINK} onClick={modalProps.onClose} > - Cancel + Cancel From 567127ff45d316a5fa39cd5ac7c956414ff3c907 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Sat, 11 Jan 2025 02:28:16 +0100 Subject: [PATCH 41/78] remove comments guh --- .../PluginSettings/components/SettingArrayComponent.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 874888e56e7..44b35a005be 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -32,10 +32,6 @@ import { ISettingElementProps } from "."; const cl = classNameFactory("vc-plugin-modal-"); - -// TODO add interfaces for the stuff from the modal instead of using any -// TODO also add types to all the vars :husk: :husk: - const UserMentionComponent = findComponentByCodeLazy(".USER_MENTION)"); const getDMChannelIcon = findByCodeLazy(".getChannelIconURL({"); const GroupDMAvatars = findComponentByCodeLazy(".AvatarSizeSpecs[", "getAvatarURL"); From c141b8e88493a1cdb4f470ce221d88060aa9d45e Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Sat, 11 Jan 2025 20:17:11 +0100 Subject: [PATCH 42/78] fixes --- .../components/SettingArrayComponent.tsx | 38 +++++++++---------- src/plugins/_api/settingArrays.tsx | 2 + 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 44b35a005be..5ccd2902c15 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -40,20 +40,20 @@ const GroupDMAvatars = findComponentByCodeLazy(".AvatarSizeSpecs[", "getAvatarUR const CloseIcon = () => { return ; }; const CheckMarkIcon = () => { return + d="M21.7 5.3a1 1 0 0 1 0 1.4l-12 12a1 1 0 0 1-1.4 0l-6-6a1 1 0 1 1 1.4-1.4L9 16.58l11.3-11.3a1 1 0 0 1 1.4 0Z" /> ; }; const SearchIcon = () => { return + strokeLinecap="round" strokeLinejoin="round"> ; @@ -72,15 +72,13 @@ export function SettingArrayComponent({ const [text, setText] = useState(""); useEffect(() => { - if (text === "") { - setError(null); - } else if (!isNaN(Number(text))) { + if (!isNaN(Number(text)) && text !== "") { if (text.length >= 18 && text.length <= 19) { setError(null); } else { setError("Invalid ID"); } - } else { + } else if (text !== "") { setError(null); } }, [text]); @@ -175,32 +173,32 @@ export function SettingArrayComponent({ switch (type) { case 2: return ; case 5: return ; case 13: return ; case 15: return ; default: // Text channel icon return ; } }; @@ -349,18 +347,18 @@ export function SettingArrayComponent({ > setText(v)} value={text} /> - {!isNaN(Number(text)) && text !== "" ? + {option.type === OptionType.ARRAY || (!isNaN(Number(text)) && text !== "") ? : diff --git a/src/plugins/_api/settingArrays.tsx b/src/plugins/_api/settingArrays.tsx index 8083a71a71e..956385afadf 100644 --- a/src/plugins/_api/settingArrays.tsx +++ b/src/plugins/_api/settingArrays.tsx @@ -72,6 +72,8 @@ function renderRegisteredPlugins(name: string, value: any) { function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback { return (children, props) => { + if (!registeredPlugins[OptionType.CHANNELS] && !registeredPlugins[OptionType.USERS] && !registeredPlugins[OptionType.GUILDS]) + return; const value = props[name.toLowerCase()]; if (!value) return; if (props.label === getIntlMessage("CHANNEL_ACTIONS_MENU_LABEL")) return; // random shit like notification settings From 14fe735fc43b7f090953979101741a76a4627f30 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Sat, 11 Jan 2025 20:42:30 +0100 Subject: [PATCH 43/78] Ensure settings get written for message tags --- src/plugins/messageTags/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/messageTags/index.ts b/src/plugins/messageTags/index.ts index a4112612111..7d503f0316a 100644 --- a/src/plugins/messageTags/index.ts +++ b/src/plugins/messageTags/index.ts @@ -34,10 +34,10 @@ interface Tag { const getTag = (name: string) => settings.store.data.find((tt: Tag) => tt.name === name); const addTag = (tag: Tag) => { - settings.store.data.push(tag); + settings.store.data = [...settings.store.data, tag]; }; const removeTag = (name: string) => { - settings.store.data.filter((t: Tag) => t.name !== name); + settings.store.data = settings.store.data.filter((t: Tag) => t.name !== name); }; function createTagCommand(tag: Tag) { From 6166a8ef174b1d988cce7a98854c864c2133a3be Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Mon, 13 Jan 2025 04:10:07 +0100 Subject: [PATCH 44/78] fix me being silly with messagetags --- src/plugins/messageTags/index.ts | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/plugins/messageTags/index.ts b/src/plugins/messageTags/index.ts index a4112612111..a218e94f560 100644 --- a/src/plugins/messageTags/index.ts +++ b/src/plugins/messageTags/index.ts @@ -64,17 +64,16 @@ function createTagCommand(tag: Tag) { const settings = definePluginSettings({ + clyde: { + description: "If enabled, clyde will send you an ephemeral message when a tag was used.", + type: OptionType.BOOLEAN, + default: true + }, data: { type: OptionType.ARRAY, hidden: true, description: "" }, - migrated: { - type: OptionType.BOOLEAN, - description: "", - default: false, - hidden: true - } }); @@ -82,22 +81,16 @@ export default definePlugin({ name: "MessageTags", description: "Allows you to save messages and to use them with a simple command.", authors: [Devs.Luna], - options: { - clyde: { - name: "Clyde message on send", - description: "If enabled, clyde will send you an ephemeral message when a tag was used.", - type: OptionType.BOOLEAN, - default: true - } - }, settings, async start() { - if (!settings.store.migrated) { - const data = await DataStore.get(DATA_KEY); - if (data !== undefined) settings.store.data = data; - settings.store.migrated = true; + const data = await DataStore.get(DATA_KEY); + + if (data != null) { + settings.store.data = data; + await DataStore.del(DATA_KEY); } + for (const tag of settings.store.data) createTagCommand(tag); }, From f5e455e041594f5b465cd8dd51d682eff0cca752 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Mon, 13 Jan 2025 04:33:22 +0100 Subject: [PATCH 45/78] implement a better way to convert string to array --- src/api/Settings.ts | 13 ------------- src/plugins/consoleJanitor/index.ts | 5 ++--- src/plugins/index.ts | 18 ++++++++++++++++-- src/plugins/invisibleChat.desktop/index.tsx | 5 ++--- src/plugins/loadingQuotes/index.ts | 4 ++-- src/plugins/messageLogger/index.tsx | 5 +---- src/utils/types.ts | 5 +++++ 7 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 1c3b634aec4..1be12a01fdc 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -224,19 +224,6 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) { } } -export function migrateSettingsToArrays(pluginName: string, settings: string[], stringSeparator: string | ((input: string) => string[]) = ",") { - const { plugins } = SettingsStore.plain; - if (plugins[pluginName] === undefined) - return logger.error(`Plugin '${pluginName}' does not exist and cannot be migrated! Did you spell it correctly?`); - for (const setting of settings) { - if (typeof plugins[pluginName][setting] !== "string") continue; - logger.info(`Migrating setting ${setting} from ${pluginName} to list`); - if (plugins[pluginName][setting] === "") plugins[pluginName][setting] = []; - else if (typeof stringSeparator === "string") plugins[pluginName][setting] = plugins[pluginName][setting].split(stringSeparator); - else plugins[pluginName][setting] = stringSeparator(plugins[pluginName][setting]); - } -} - export function definePluginSettings< Def extends SettingsDefinition, Checks extends SettingsChecks, diff --git a/src/plugins/consoleJanitor/index.ts b/src/plugins/consoleJanitor/index.ts index e7bbecedd74..d2b6c3573fe 100644 --- a/src/plugins/consoleJanitor/index.ts +++ b/src/plugins/consoleJanitor/index.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { definePluginSettings, migrateSettingsToArrays } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType, StartAt } from "@utils/types"; @@ -24,8 +24,6 @@ const NoopLogger = { const logAllow = new Set(); -migrateSettingsToArrays("ConsoleJanitor", ["whitelistedLoggers"], s => s.split(";").map(x => x.trim())); - const settings = definePluginSettings({ disableLoggers: { type: OptionType.BOOLEAN, @@ -43,6 +41,7 @@ const settings = definePluginSettings({ type: OptionType.ARRAY, description: "List of loggers to allow even if others are hidden", default: ["GatewaySocket", "Routing/Utils"], + oldStringSeparator: s => s.split(";").map(x => x.trim()), onChange(newVal: string[]) { logAllow.clear(); newVal.forEach(logAllow.add.bind(logAllow)); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 129e42a0d19..9ba11caa3d7 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -21,7 +21,7 @@ import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; import { canonicalizeFind } from "@utils/patches"; -import { Patch, Plugin, ReporterTestable, StartAt } from "@utils/types"; +import { OptionType, Patch, Plugin, ReporterTestable, StartAt } from "@utils/types"; import { FluxDispatcher } from "@webpack/common"; import { FluxEvents } from "@webpack/types"; @@ -215,7 +215,21 @@ export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatc } export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { - const { name, commands, contextMenus } = p; + const { name, commands, contextMenus, settings } = p; + + if (settings != null) + for (const setting of Object.keys(settings.def)) { + const { type } = settings.def[setting]; + if (type === OptionType.ARRAY || type === OptionType.USERS || type === OptionType.GUILDS || type === OptionType.CHANNELS) { + if (typeof settings.store[setting] === "string") { + logger.info(`Converting string values of setting ${setting} of plugin ${name} to array`); + + const sep = settings.def[setting].oldStringSeparator ?? ","; + if (typeof sep === "string") settings.store[setting] = settings.store[setting].split(sep); + else settings.store[setting] = sep(settings.store[setting]); + } + } + } if (p.start) { logger.info("Starting plugin", name); diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx index e799ad4f29c..c88a1c08961 100644 --- a/src/plugins/invisibleChat.desktop/index.tsx +++ b/src/plugins/invisibleChat.desktop/index.tsx @@ -19,7 +19,7 @@ import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { addButton, removeButton } from "@api/MessagePopover"; import { updateMessage } from "@api/MessageUpdater"; -import { definePluginSettings, migrateSettingsToArrays } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getStegCloak } from "@utils/dependencies"; @@ -93,13 +93,12 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { }; -migrateSettingsToArrays("InvisibleChat", ["savedPasswords"], s => s.split(",").map(s => s.trim())); - const settings = definePluginSettings({ savedPasswords: { type: OptionType.ARRAY, default: ["password", "Password"], description: "Saved Passwords", + oldStringSeparator: s => s.split(",").map(s => s.trim()), } }); diff --git a/src/plugins/loadingQuotes/index.ts b/src/plugins/loadingQuotes/index.ts index 395f1d06ea3..7f7a2508792 100644 --- a/src/plugins/loadingQuotes/index.ts +++ b/src/plugins/loadingQuotes/index.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { definePluginSettings, migrateSettingsToArrays } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; @@ -25,7 +25,6 @@ import presetQuotesText from "file://quotes.txt"; const presetQuotes = presetQuotesText.split("\n").map(quote => /^\s*[^#\s]/.test(quote) && quote.trim()).filter(Boolean) as string[]; const noQuotesQuote = "Did you really disable all loading quotes? What a buffoon you are..."; -migrateSettingsToArrays("LoadingQuotes", ["additionalQuotes"], "|"); const settings = definePluginSettings({ replaceEvents: { @@ -46,6 +45,7 @@ const settings = definePluginSettings({ additionalQuotes: { description: "Additional custom quotes to possibly appear", type: OptionType.ARRAY, + oldStringSeparator: "|", }, }); diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index cd57294e7fe..c9a19e09201 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 { definePluginSettings, migrateSettingsToArrays, Settings } from "@api/Settings"; +import { definePluginSettings, Settings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; @@ -36,9 +36,6 @@ 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, diff --git a/src/utils/types.ts b/src/utils/types.ts index 8b35e732175..d304f6aa158 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -279,6 +279,11 @@ export interface PluginSettingArrayDef { popoutText?: string; hidePopout?: boolean; default?: any[]; + /** + * If the setting used to be a string with a custom delimiter, you can specify the delimiter or a function to split the string + * @default "," + */ + oldStringSeparator?: string | ((value: string) => string[]); onChange?(newValue: any[]): void; } From 8d9e9c49fa7de094f6d63b35e178796ff5f03558 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:43:31 +0100 Subject: [PATCH 46/78] dispatch onchange when checkbox is clicked --- src/plugins/_api/settingArrays.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/plugins/_api/settingArrays.tsx b/src/plugins/_api/settingArrays.tsx index 956385afadf..28a884f90a7 100644 --- a/src/plugins/_api/settingArrays.tsx +++ b/src/plugins/_api/settingArrays.tsx @@ -41,12 +41,14 @@ function renderRegisteredPlugins(name: string, value: any) { ...prevState, [key]: !prevState[key] })); - // @ts-ignore settings must be defined otherwise we wouldn't be here - const s = Vencord.Plugins.plugins[plugin].settings.store[setting]; - // @ts-ignore - Vencord.Plugins.plugins[plugin].settings.store[setting] = s.includes(value.id) - ? s.filter(id => id !== value.id) - : [...s, value.id]; + + // settings must be defined otherwise the checkbox wouldn't exist in the first place + const s = Vencord.Plugins.plugins[plugin].settings!; + s.store[setting] = s.store[setting].includes(value.id) + ? s.store[setting].filter((id: string) => id !== value.id) + : [...s.store[setting], value.id]; + + s.def[setting].onChange?.(s.store[setting]); }; return Object.keys(plugins).map(plugin => ( From e6188b7cc5c4c9feb301f6cae877e2514c471875 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Tue, 14 Jan 2025 02:19:40 +0100 Subject: [PATCH 47/78] fix textreplace migration --- src/plugins/textReplace/index.tsx | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index 04500b7467e..f5426dc4c51 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -81,12 +81,6 @@ const settings = definePluginSettings({ hidden: true, description: "" }, - migrated: { - type: OptionType.BOOLEAN, - hidden: true, - default: false, - description: "" - } }); function stringToRegex(str: string) { @@ -264,13 +258,20 @@ export default definePlugin({ settings, async start() { - if (!settings.store.migrated) { - const stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray(); - const regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray(); - settings.store.stringRules = stringRules; - settings.store.regexRules = regexRules; - settings.store.migrated = true; + if (settings.store.stringRules.length === 0 || settings.store.regexRules.length === 0) { + const stringRules = await DataStore.get(STRING_RULES_KEY); + const regexRules = await DataStore.get(REGEX_RULES_KEY); + + if (stringRules != null) { + settings.store.stringRules = stringRules; + await DataStore.del(STRING_RULES_KEY); + } else if (settings.store.stringRules.length === 0) settings.store.stringRules = makeEmptyRuleArray(); + + if (regexRules != null) { + settings.store.regexRules = regexRules; + await DataStore.del(REGEX_RULES_KEY); + } else if (settings.store.regexRules.length === 0) settings.store.regexRules = makeEmptyRuleArray(); } this.preSend = addPreSendListener((channelId, msg) => { From 9205b5d90084be31d54f63a39551a94f36d03780 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:30:07 +0100 Subject: [PATCH 48/78] improve styles --- src/components/SearchModal.css | 15 +++++++++++++++ src/components/SearchModal.tsx | 16 +++++----------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/SearchModal.css b/src/components/SearchModal.css index 1d4e2ef7129..e322a788a98 100644 --- a/src/components/SearchModal.css +++ b/src/components/SearchModal.css @@ -50,3 +50,18 @@ display: flex; align-items: center } + +.vc-search-modal-header-text { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + margin-bottom: 8px; +} + +.vc-search-modal-no-results-container { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 65e6baaa6f8..3264f8812c9 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -637,7 +637,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = else throw new Error("Unknown type " + type); }, [results, selectedDestinationKeys, handleToggleDestination]); const navRef = useRef(null); - const nav = createNavigator(cl("search-modal"), navRef); + const nav = createNavigator(cl("nav"), navRef); return navigatorWrapper({ navigator: nav, @@ -684,20 +684,14 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = return ( - + -
+
{"Search for " + searchTypeToText(searchType)} @@ -728,7 +722,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = rowData={results} handleToggleDestination={setSelectedCallback} /> : -
+
Date: Thu, 16 Jan 2025 14:20:24 +0100 Subject: [PATCH 49/78] im so tupid sometimes --- src/components/SearchModal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 3264f8812c9..24584b1b80b 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -32,7 +32,8 @@ import { useRef, UsernameUtils, UserStore, - useState + useState, + useStateFromStores } from "@webpack/common"; import { Channel, Guild, User } from "discord-types/general"; @@ -44,7 +45,6 @@ const TextTypes = findByPropsLazy("APPLICATION", "GROUP_DM", "GUILD"); const FrequencyModule = findByPropsLazy("getFrequentlyWithoutFetchingLatest"); const FrequentsModule = findByPropsLazy("getChannelHistory", "getFrequentGuilds"); -const wrapperFn = findByCodeLazy("prevDeps:void 0,"); const convertItem = findByCodeLazy("GROUP_DM:return{", "GUILD_VOICE:case"); const loadFunction = findByCodeLazy(".frecencyWithoutFetchingLatest)"); const SearchHandler = findByCodeLazy("createSearchContext", "setLimit"); @@ -494,7 +494,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = loadFunction(); - const frequentChannels: Channel[] = wrapperFn([FrequencyModule], () => FrequencyModule.getFrequentlyWithoutFetchingLatest()); + const frequentChannels: Channel[] = useStateFromStores([FrequencyModule], () => FrequencyModule.getFrequentlyWithoutFetchingLatest()); const hasQuery = query !== ""; function getItem(e: DestinationItem): Result { From fcdfd0795aa8ad49a39874daa80949132f83af53 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:35:11 +0100 Subject: [PATCH 50/78] convert noreplymention setting --- src/plugins/noReplyMention/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/noReplyMention/index.tsx b/src/plugins/noReplyMention/index.tsx index 16b3a3e00ca..ca4bec4e741 100644 --- a/src/plugins/noReplyMention/index.tsx +++ b/src/plugins/noReplyMention/index.tsx @@ -24,9 +24,9 @@ import type { Message } from "discord-types/general"; const settings = definePluginSettings({ userList: { description: - "List of users to allow or exempt pings for (separated by commas or spaces)", - type: OptionType.STRING, - default: "1234567890123445,1234567890123445", + "List of users to allow or exempt pings for", + type: OptionType.USERS, + oldStringSeparator: s => s.split(/[\s,]+/).filter(v => v !== "1234567890123445") }, shouldPingListed: { description: "Behaviour", From 25dd64dd11e985a36fab9b959f95bcb250c60266 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:31:29 +0100 Subject: [PATCH 51/78] more slight improvements --- src/components/SearchModal.tsx | 59 +++++++++++++++++----------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 24584b1b80b..1c2e322b425 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -16,7 +16,7 @@ import { ModalRoot, ModalSize } from "@utils/modal"; -import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { Button, ChannelStore, @@ -33,7 +33,7 @@ import { UsernameUtils, UserStore, useState, - useStateFromStores + useStateFromStores, } from "@webpack/common"; import { Channel, Guild, User } from "discord-types/general"; @@ -42,19 +42,21 @@ const cl = classNameFactory("vc-search-modal-"); const SearchBarModule = findByPropsLazy("SearchBar", "Checkbox", "AvatarSizes"); const SearchBarWrapper = findByPropsLazy("SearchBar", "Item"); const TextTypes = findByPropsLazy("APPLICATION", "GROUP_DM", "GUILD"); -const FrequencyModule = findByPropsLazy("getFrequentlyWithoutFetchingLatest"); -const FrequentsModule = findByPropsLazy("getChannelHistory", "getFrequentGuilds"); +const SearchHandler = findByCodeLazy("createSearchContext", "setLimit"); const convertItem = findByCodeLazy("GROUP_DM:return{", "GUILD_VOICE:case"); -const loadFunction = findByCodeLazy(".frecencyWithoutFetchingLatest)"); -const SearchHandler = findByCodeLazy("createSearchContext", "setLimit"); +const loadFrecency = findByCodeLazy(".frecencyWithoutFetchingLatest)"); const navigatorWrapper = findByCodeLazy("useMemo(()=>({onKeyDown:"); const createNavigator = findByCodeLazy(".keyboardModeEnabled)", "useCallback(()=>new Promise(", "Number.MAX_SAFE_INTEGER"); const getChannelLabel = findByCodeLazy("recipients.map(", "getNickname("); -const ChannelIcon = findByCodeLazy("channelGuildIcon,"); +const ChannelIcon = findComponentByCodeLazy("channelGuildIcon,"); const GroupDMAvatars = findComponentByCodeLazy("facepileSizeOverride", "recipients.length"); +const FrecencyStore = findStoreLazy("FrecencyStore"); +const QuickSwitcherStore = findStoreLazy("QuickSwitcherStore"); + + interface DestinationItem { type: "channel" | "user" | "guild"; id: string; @@ -278,14 +280,10 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = ); } - function generateChannelLabel(channel: Channel): string { - return getChannelLabel(channel, UserStore, RelationshipStore, false); - } - function generateChannelItem(channel: Channel, otherProps: UnspecificRowProps) { const guild = GuildStore.getGuild(channel?.guild_id); - const channelLabel = generateChannelLabel(channel); + const channelLabel = getChannelLabel(channel, UserStore, RelationshipStore, false); const parentChannelLabel = (): string => { const parentChannel = ChannelStore.getChannel(channel.parent_id); @@ -492,9 +490,21 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = } , [search, queryData]); - loadFunction(); + loadFrecency(); + + const frequentChannels: Channel[] = useStateFromStores([FrecencyStore], () => FrecencyStore.getFrequentlyWithoutFetchingLatest()); + const channelHistory: string[] = useStateFromStores([QuickSwitcherStore], () => QuickSwitcherStore.getChannelHistory()); + const guilds: GuildResult[] = useStateFromStores([GuildStore], () => Object.values(GuildStore.getGuilds()).map( + guild => { + return { + type: TextTypes.GUILD, + record: guild, + score: 0, + comparator: guild.name + }; + } + )); - const frequentChannels: Channel[] = useStateFromStores([FrequencyModule], () => FrequencyModule.getFrequentlyWithoutFetchingLatest()); const hasQuery = query !== ""; function getItem(e: DestinationItem): Result { @@ -532,6 +542,8 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = hasQuery: boolean; frequentChannels: Channel[]; pinnedDestinations: DestinationItem[]; + channelHistory: string[]; + guilds: GuildResult[] }): Result[] { const removeDuplicates = (arr: Result[]): Result[] => { const clean: any[] = []; @@ -546,22 +558,9 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = return clean; }; - const { results, hasQuery, frequentChannels, pinnedDestinations } = props; + const { results, hasQuery, frequentChannels, pinnedDestinations, channelHistory, guilds } = props; if (hasQuery) return filterItems(results); - const channelHistory: string[] = FrequentsModule.getChannelHistory(); - const guilds = Object.values(GuildStore.getGuilds()).map( - guild => { - if (guild == null) return; - return { - type: TextTypes.GUILD, - record: guild, - score: 0, - comparator: guild.name - }; - } - ); - const recentDestinations = filterItems([ ...(channelHistory.length > 0 ? channelHistory.map(e => convertItem(e)) : []), ...(frequentChannels.length > 0 ? frequentChannels.map(e => convertItem(e.id)) : []), @@ -580,7 +579,9 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = hasQuery: hasQuery, frequentChannels: frequentChannels, pinnedDestinations: pinned, - }), [results, hasQuery, frequentChannels, pinned]), + channelHistory: channelHistory, + guilds: guilds + }), [results, hasQuery, frequentChannels, pinned, channelHistory, guilds]), updateSearchText: updateSearch }; } From 0a96e1a508a1a1ca2a6a1c2adaf27f802ebd116e Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:52:56 +0100 Subject: [PATCH 52/78] remove useless code --- src/components/SearchModal.tsx | 275 +++++++++++---------------------- 1 file changed, 94 insertions(+), 181 deletions(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 1c2e322b425..ea9c7ce0a84 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -46,8 +46,6 @@ const SearchHandler = findByCodeLazy("createSearchContext", "setLimit"); const convertItem = findByCodeLazy("GROUP_DM:return{", "GUILD_VOICE:case"); const loadFrecency = findByCodeLazy(".frecencyWithoutFetchingLatest)"); -const navigatorWrapper = findByCodeLazy("useMemo(()=>({onKeyDown:"); -const createNavigator = findByCodeLazy(".keyboardModeEnabled)", "useCallback(()=>new Promise(", "Number.MAX_SAFE_INTEGER"); const getChannelLabel = findByCodeLazy("recipients.map(", "getNickname("); const ChannelIcon = findComponentByCodeLazy("channelGuildIcon,"); @@ -158,27 +156,6 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = searchType?: ("USERS" | "CHANNELS" | "GUILDS")[] | "USERS" | "CHANNELS" | "GUILDS" | "ALL"; subText?: string }) { - - const callbacks = new Map(); - - function registerCallback(key: string, callback: (...args: any[]) => void): () => void { - let currentCallbacks = callbacks.get(key); - if (!currentCallbacks) { - currentCallbacks = new Set(); - callbacks.set(key, currentCallbacks); - } - - currentCallbacks.add(callback); - - return () => { - currentCallbacks.delete(callback); - if (currentCallbacks.size === 0) { - callbacks.delete(key); - } - }; - - } - const UserIcon = React.memo(function ({ user, size = SearchBarModule.AvatarSizes.SIZE_32, @@ -204,8 +181,6 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const [selected, setSelected] = useState([]); - const refCounter = useRef(0); - const Row = (props: SpecificRowProps) => { const { destination, @@ -219,8 +194,11 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = ...rest } = props; - const interactionProps = generateRowData(destination.id); - + const interactionProps = { + role: "listitem", + "data-list-item-id": `NO_LIST___${destination.id}`, + tabIndex: -1, + }; const handlePress = useCallback(() => { onPressDestination?.(destination); }, [onPressDestination, destination]); @@ -228,7 +206,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = return ( { e.stopPropagation(); e.preventDefault(); handlePress(); }} + onClick={handlePress} aria-selected={isSelected} {...interactionProps} {...rest} @@ -250,6 +228,8 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
console.log(e)} + onChange={e => console.log(e)} type={SearchBarModule.Checkbox.Types.INVERTED} displayOnly={true} size={24} @@ -375,50 +355,80 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = ); } - const navigatorContext = React.createContext({ - id: "NO_LIST", - onKeyDown() { - }, - orientation: "vertical", - ref: React.createRef(), - tabIndex: -1 - }); - - function generateNavigatorData() { - const { id: id, onKeyDown, ref, tabIndex } = React.useContext(navigatorContext); - return { - role: "list", - tabIndex, - "data-list-id": id, - onKeyDown: onKeyDown, - ref: ref, - }; - } + const [searchText, setSearchText] = useState(input || ""); + const ref = {}; - function navigatorData(e: { children: (data: ReturnType) => React.ReactNode }): React.ReactNode { - const { children } = e; - return children(generateNavigatorData()); + function getItem(e: DestinationItem): Result { + if (e.type === "guild") { + const guild = GuildStore.getGuild(e.id); + return { + type: TextTypes.GUILD, + record: guild, + score: 0, + comparator: guild.name, + }; + } + if (e.type !== "user") + return convertItem(e.id); + { + const user = UserStore.getUser(e.id); + return { + type: TextTypes.USER, + record: user, + score: 0, + // @ts-ignore globalName is not in the types but exists + comparator: user.globalName, + }; + } } + const filterItems = (items: any[]) => { + return items.filter( + item => item != null && resultTypes.includes(item.type) + ); + }; - function generateRowData(rowId: string) { - const [tabIndex, setTabIndex] = useState(-1); - const id = "NO_LIST"; - React.useLayoutEffect(() => { - return registerCallback(id, (tabIndex: string, id: string) => { - setTabIndex(id && tabIndex === rowId ? 0 : -1); + function filterResults(props: { + results: Result[]; + hasQuery: boolean; + frequentChannels: Channel[]; + channelHistory: string[]; + guilds: GuildResult[] + }): Result[] { + const removeDuplicates = (arr: Result[]): Result[] => { + const clean: any[] = []; + const seenIds = new Set(); + arr.forEach(item => { + if (item == null || item.record == null) return; + if (!seenIds.has(item.record.id)) { + seenIds.add(item.record.id); + clean.push(item); + } }); - }, [rowId, id]); - - return { - role: "listitem", - "data-list-item-id": `${id}___${rowId}`, - tabIndex, + return clean; }; + + const { results, hasQuery, frequentChannels, channelHistory, guilds } = props; + if (hasQuery) return filterItems(results); + + const recentDestinations = filterItems([ + ...(channelHistory.length > 0 ? channelHistory.map(e => convertItem(e)) : []), + ...(frequentChannels.length > 0 ? frequentChannels.map(e => convertItem(e.id)) : []), + ...guilds + ]); + + return removeDuplicates( + [...(selected.length > 0 ? selected.map(e => getItem(e)) : []), + ...recentDestinations + ]); } - const [searchText, setSearchText] = useState(input || ""); - const ref = {}; + function getRef(e: () => T): T { + const ref_ = useRef(ref as T); + if (ref_.current === ref) + ref_.current = e(); + return ref_.current; + } function getSearchHandler(searchOptions: Record): { search: (e: { query: string, resultTypes: string[] }) => void, results: Result[], query: string } { const [results, setResults] = useState<{ results: Result[], query: string }>({ @@ -426,13 +436,6 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = query: "" }); - function getRef(e: () => T): T { - const ref_ = useRef(ref as T); - if (ref_.current === ref) - ref_.current = e(); - return ref_.current; - } - const searchHandler: InstanceType = getRef(() => { const searchHandler = new SearchHandler((r: Result[], q: string) => { setResults({ @@ -465,7 +468,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = }; } - function generateResults({ selectedDestinations }: { selectedDestinations: DestinationItem[] }) { + function generateResults() { const { search, query, results } = getSearchHandler({ blacklist: null, frecencyBoosters: !0, @@ -480,15 +483,12 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = updateSearch(searchText); } - const [pinned, setPinned] = useState(selectedDestinations != null ? selectedDestinations : []); React.useLayoutEffect(() => { - search({ - query: queryData, - resultTypes: resultTypes, - }); - setPinned(selectedDestinations != null ? selectedDestinations : []); - } - , [search, queryData]); + search({ + query: queryData, + resultTypes: resultTypes, + }); + }, [search, queryData]); loadFrecency(); @@ -507,88 +507,19 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const hasQuery = query !== ""; - function getItem(e: DestinationItem): Result { - if (e.type === "guild") { - const guild = GuildStore.getGuild(e.id); - return { - type: TextTypes.GUILD, - record: guild, - score: 0, - comparator: guild.name, - }; - } - if (e.type !== "user") - return convertItem(e.id); - { - const user = UserStore.getUser(e.id); - return { - type: TextTypes.USER, - record: user, - score: 0, - // @ts-ignore globalName is not in the types but exists - comparator: user.globalName, - }; - } - } - - const filterItems = (items: any[]) => { - return items.filter( - item => item != null && resultTypes.includes(item.type) - ); - }; - - function filterResults(props: { - results: Result[]; - hasQuery: boolean; - frequentChannels: Channel[]; - pinnedDestinations: DestinationItem[]; - channelHistory: string[]; - guilds: GuildResult[] - }): Result[] { - const removeDuplicates = (arr: Result[]): Result[] => { - const clean: any[] = []; - const seenIds = new Set(); - arr.forEach(item => { - if (item == null || item.record == null) return; - if (!seenIds.has(item.record.id)) { - seenIds.add(item.record.id); - clean.push(item); - } - }); - return clean; - }; - - const { results, hasQuery, frequentChannels, pinnedDestinations, channelHistory, guilds } = props; - if (hasQuery) return filterItems(results); - - const recentDestinations = filterItems([ - ...(channelHistory.length > 0 ? channelHistory.map(e => convertItem(e)) : []), - ...(frequentChannels.length > 0 ? frequentChannels.map(e => convertItem(e.id)) : []), - ...guilds - ]); - - return removeDuplicates( - [...(pinnedDestinations.length > 0 ? pinnedDestinations.map(e => getItem(e)) : []), - ...recentDestinations - ]); - } - return { results: useMemo(() => filterResults({ results: results, hasQuery: hasQuery, frequentChannels: frequentChannels, - pinnedDestinations: pinned, channelHistory: channelHistory, guilds: guilds - }), [results, hasQuery, frequentChannels, pinned, channelHistory, guilds]), + }), [results, hasQuery, frequentChannels, channelHistory, guilds]), updateSearchText: updateSearch }; } - const { results, updateSearchText } = generateResults({ - selectedDestinations: selected, - }); + const { results, updateSearchText } = generateResults(); const selectedDestinationKeys = useMemo(() => { return selected?.map(destination => `${destination.type}-${destination.id}`) || []; @@ -615,7 +546,6 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const key = `${destination.type}-${destination.id}`; - const rowProps: UnspecificRowProps = { key, destination, @@ -636,32 +566,18 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = if (type === "GUILD") return generateGuildItem(record, rowProps); else throw new Error("Unknown type " + type); - }, [results, selectedDestinationKeys, handleToggleDestination]); - const navRef = useRef(null); - const nav = createNavigator(cl("nav"), navRef); - - return navigatorWrapper({ - navigator: nav, - children: navigatorData({ - children: e => { - const { ref, ...data } = e; - return { - navRef.current = elem; - ref.current = elem?.getScrollerNode() ?? null; - } - } - {...data} - paddingBottom={paddingBottom} - paddingTop={paddingTop} - sections={sectionCount} - sectionHeight={0} - renderRow={callback} - rowHeight={rowHeight}/>; - } - }) - }); + }, [results]); + + return ; } @@ -673,11 +589,9 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = }); if (index === -1) { - refCounter.current += 1; return [e, ...currentSelected]; } - refCounter.current += 1; currentSelected.splice(index, 1); return [...currentSelected]; }); @@ -713,7 +627,6 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = setSearchText(""); updateSearchText(""); }} - autoFocus={true} /> { From 6ddf20360909b80610c82ef139bcdd7f9a1cf33a Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:40:53 +0100 Subject: [PATCH 53/78] revert the changes that migrate datastore to array settings --- src/plugins/messageTags/index.ts | 33 ++++++++++++--------- src/plugins/pinDms/data.ts | 20 ++++--------- src/plugins/pinDms/index.tsx | 7 +---- src/plugins/textReplace/index.tsx | 48 +++++++++---------------------- 4 files changed, 40 insertions(+), 68 deletions(-) diff --git a/src/plugins/messageTags/index.ts b/src/plugins/messageTags/index.ts index 65699541d75..50bb62665d1 100644 --- a/src/plugins/messageTags/index.ts +++ b/src/plugins/messageTags/index.ts @@ -34,10 +34,10 @@ interface Tag { const getTag = (name: string) => settings.store.data.find((tt: Tag) => tt.name === name); const addTag = (tag: Tag) => { - settings.store.data = [...settings.store.data, tag]; + settings.store.data.push(tag); }; const removeTag = (name: string) => { - settings.store.data = settings.store.data.filter((t: Tag) => t.name !== name); + settings.store.data.filter((t: Tag) => t.name !== name); }; function createTagCommand(tag: Tag) { @@ -64,16 +64,17 @@ function createTagCommand(tag: Tag) { const settings = definePluginSettings({ - clyde: { - description: "If enabled, clyde will send you an ephemeral message when a tag was used.", - type: OptionType.BOOLEAN, - default: true - }, data: { type: OptionType.ARRAY, hidden: true, description: "" }, + migrated: { + type: OptionType.BOOLEAN, + description: "", + default: false, + hidden: true + } }); @@ -81,16 +82,22 @@ export default definePlugin({ name: "MessageTags", description: "Allows you to save messages and to use them with a simple command.", authors: [Devs.Luna], + options: { + clyde: { + name: "Clyde message on send", + description: "If enabled, clyde will send you an ephemeral message when a tag was used.", + type: OptionType.BOOLEAN, + default: true + } + }, settings, async start() { - const data = await DataStore.get(DATA_KEY); - - if (data != null) { - settings.store.data = data; - await DataStore.del(DATA_KEY); + if (!settings.store.migrated) { + const data = await DataStore.get(DATA_KEY); + if (!!data) settings.store.data = data; + settings.store.migrated = true; } - for (const tag of settings.store.data) createTagCommand(tag); }, diff --git a/src/plugins/pinDms/data.ts b/src/plugins/pinDms/data.ts index 8adc2dbfcba..a4e40dde01c 100644 --- a/src/plugins/pinDms/data.ts +++ b/src/plugins/pinDms/data.ts @@ -28,18 +28,19 @@ const OLD_CATEGORY_KEY = "BetterPinDMsCategories-"; export let categories: Category[] = []; export async function saveCats(cats: Category[]) { - settings.store.data = cats; + const { id } = UserStore.getCurrentUser(); + await DataStore.set(CATEGORY_BASE_KEY + id, cats); } export async function init() { const id = UserStore.getCurrentUser()?.id; - await initCategories(); + await initCategories(id); await migrateData(id); forceUpdate(); } -export async function initCategories() { - categories = settings.store.data ?? []; +export async function initCategories(userId: string) { + categories = await DataStore.get(CATEGORY_BASE_KEY + userId) ?? []; } export function getCategory(id: string) { @@ -206,22 +207,13 @@ async function migrateOldCategories(userId: string) { await DataStore.set(CATEGORY_MIGRATED_KEY, true); } -async function migrateFromDatastore(userId: string) { - const cats = await DataStore.get(CATEGORY_BASE_KEY + userId); - if (cats !== undefined) settings.store.data = cats; -} - export async function migrateData(userId: string) { - if (settings.store.data.length > 0) return; const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY); - if (m1 && m2) { - return await migrateFromDatastore(userId); - } + if (m1 && m2) return; // want to migrate the old categories first and then slove any conflicts with the PinDMs pins if (!m1) await migrateOldCategories(userId); if (!m2) await migratePinDMs(); - if (settings.store.data.length === 0) await migrateFromDatastore(userId); await saveCats(categories); } diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index a7abcb8d664..fce725cabd7 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -54,12 +54,7 @@ export const settings = definePluginSettings({ description: "Collapse DM sections", default: false, onChange: () => forceUpdate() - }, - data: { - type: OptionType.ARRAY, - hidden: true, - description: "", - }, + } }); export default definePlugin({ diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index f5426dc4c51..615477d079d 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -46,6 +46,9 @@ const makeEmptyRule: () => Rule = () => ({ }); const makeEmptyRuleArray = () => [makeEmptyRule()]; +let stringRules = makeEmptyRuleArray(); +let regexRules = makeEmptyRuleArray(); + const settings = definePluginSettings({ replace: { type: OptionType.COMPONENT, @@ -56,13 +59,13 @@ const settings = definePluginSettings({ <> @@ -71,16 +74,6 @@ const settings = definePluginSettings({ ); } }, - stringRules: { - type: OptionType.ARRAY, - hidden: true, - description: "" - }, - regexRules: { - type: OptionType.ARRAY, - hidden: true, - description: "" - }, }); function stringToRegex(str: string) { @@ -134,8 +127,7 @@ function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) if (index === rulesArray.length - 1) return; rulesArray.splice(index, 1); - if (rulesKey === STRING_RULES_KEY) settings.store.stringRules = [...rulesArray]; - else settings.store.regexRules = [...rulesArray]; + await DataStore.set(rulesKey, rulesArray); update(); } @@ -148,8 +140,7 @@ function TextReplace({ title, rulesArray, rulesKey, update }: TextReplaceProps) if (rulesArray[index].find === "" && rulesArray[index].replace === "" && rulesArray[index].onlyIfIncludes === "" && index !== rulesArray.length - 1) rulesArray.splice(index, 1); - if (rulesKey === STRING_RULES_KEY) settings.store.stringRules = [...rulesArray]; - else settings.store.regexRules = [...rulesArray]; + await DataStore.set(rulesKey, rulesArray); update(); } @@ -220,8 +211,8 @@ function applyRules(content: string): string { if (content.length === 0) return content; - if (settings.store.stringRules) { - for (const rule of settings.store.stringRules) { + if (stringRules) { + for (const rule of stringRules) { if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; @@ -229,8 +220,8 @@ function applyRules(content: string): string { } } - if (settings.store.regexRules) { - for (const rule of settings.store.regexRules) { + if (regexRules) { + for (const rule of regexRules) { if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; @@ -258,21 +249,8 @@ export default definePlugin({ settings, async start() { - - if (settings.store.stringRules.length === 0 || settings.store.regexRules.length === 0) { - const stringRules = await DataStore.get(STRING_RULES_KEY); - const regexRules = await DataStore.get(REGEX_RULES_KEY); - - if (stringRules != null) { - settings.store.stringRules = stringRules; - await DataStore.del(STRING_RULES_KEY); - } else if (settings.store.stringRules.length === 0) settings.store.stringRules = makeEmptyRuleArray(); - - if (regexRules != null) { - settings.store.regexRules = regexRules; - await DataStore.del(REGEX_RULES_KEY); - } else if (settings.store.regexRules.length === 0) settings.store.regexRules = makeEmptyRuleArray(); - } + stringRules = await DataStore.get(STRING_RULES_KEY) ?? makeEmptyRuleArray(); + regexRules = await DataStore.get(REGEX_RULES_KEY) ?? makeEmptyRuleArray(); this.preSend = addPreSendListener((channelId, msg) => { // Channel used for sharing rules, applying rules here would be messy From bc843c362546c9c2559f46798a1b831050376de7 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:43:33 +0100 Subject: [PATCH 54/78] revert the changes that migrate datastore to array settings v2 --- src/plugins/messageTags/index.ts | 53 ++++++++++++-------------------- src/plugins/pinDms/index.tsx | 1 + 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/plugins/messageTags/index.ts b/src/plugins/messageTags/index.ts index 50bb62665d1..5ba4ab94a2b 100644 --- a/src/plugins/messageTags/index.ts +++ b/src/plugins/messageTags/index.ts @@ -18,7 +18,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, registerCommand, sendBotMessage, unregisterCommand } from "@api/Commands"; import * as DataStore from "@api/DataStore"; -import { definePluginSettings, Settings } from "@api/Settings"; +import { Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -32,12 +32,19 @@ interface Tag { enabled: boolean; } -const getTag = (name: string) => settings.store.data.find((tt: Tag) => tt.name === name); -const addTag = (tag: Tag) => { - settings.store.data.push(tag); +const getTags = () => DataStore.get(DATA_KEY).then(t => t ?? []); +const getTag = (name: string) => DataStore.get(DATA_KEY).then((t: Tag[]) => (t ?? []).find((tt: Tag) => tt.name === name) ?? null); +const addTag = async (tag: Tag) => { + const tags = await getTags(); + tags.push(tag); + DataStore.set(DATA_KEY, tags); + return tags; }; -const removeTag = (name: string) => { - settings.store.data.filter((t: Tag) => t.name !== name); +const removeTag = async (name: string) => { + let tags = await getTags(); + tags = await tags.filter((t: Tag) => t.name !== name); + DataStore.set(DATA_KEY, tags); + return tags; }; function createTagCommand(tag: Tag) { @@ -46,7 +53,7 @@ function createTagCommand(tag: Tag) { description: tag.name, inputType: ApplicationCommandInputType.BUILT_IN_TEXT, execute: async (_, ctx) => { - if (!getTag(tag.name)) { + if (!await getTag(tag.name)) { sendBotMessage(ctx.channel.id, { content: `${EMOTE} The tag **${tag.name}** does not exist anymore! Please reload ur Discord to fix :)` }); @@ -63,21 +70,6 @@ function createTagCommand(tag: Tag) { } -const settings = definePluginSettings({ - data: { - type: OptionType.ARRAY, - hidden: true, - description: "" - }, - migrated: { - type: OptionType.BOOLEAN, - description: "", - default: false, - hidden: true - } -}); - - export default definePlugin({ name: "MessageTags", description: "Allows you to save messages and to use them with a simple command.", @@ -90,16 +82,9 @@ export default definePlugin({ default: true } }, - settings, async start() { - if (!settings.store.migrated) { - const data = await DataStore.get(DATA_KEY); - if (!!data) settings.store.data = data; - settings.store.migrated = true; - } - for (const tag of settings.store.data) createTagCommand(tag); - + for (const tag of await getTags()) createTagCommand(tag); }, commands: [ @@ -180,7 +165,7 @@ export default definePlugin({ }; createTagCommand(tag); - addTag(tag); + await addTag(tag); sendBotMessage(ctx.channel.id, { content: `${EMOTE} Successfully created the tag **${name}**!` @@ -196,7 +181,7 @@ export default definePlugin({ }); unregisterCommand(name); - removeTag(name); + await removeTag(name); sendBotMessage(ctx.channel.id, { content: `${EMOTE} Successfully deleted the tag **${name}**!` @@ -210,7 +195,7 @@ export default definePlugin({ // @ts-ignore title: "All Tags:", // @ts-ignore - description: settings.store.data + description: (await getTags()) .map(tag => `\`${tag.name}\`: ${tag.message.slice(0, 72).replaceAll("\\n", " ")}${tag.message.length > 72 ? "..." : ""}`) .join("\n") || `${EMOTE} Woops! There are no tags yet, use \`/tags create\` to create one!`, // @ts-ignore @@ -223,7 +208,7 @@ export default definePlugin({ } case "preview": { const name: string = findOption(args[0].options, "tag-name", ""); - const tag = getTag(name); + const tag = await getTag(name); if (!tag) return sendBotMessage(ctx.channel.id, { diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index fce725cabd7..8cbb03bfcad 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -49,6 +49,7 @@ export const settings = definePluginSettings({ ], onChange: () => forceUpdate() }, + dmSectioncollapsed: { type: OptionType.BOOLEAN, description: "Collapse DM sections", From 9cee56e379a9520dd8c30f34b10b0a02ca105ca9 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:06:16 +0100 Subject: [PATCH 55/78] minor fixes - switch from indices to IDs - make indexes for the channel view unique - Fix guilds shuffling around in the channel view by sorting them alphabetically - Fix possibility to add elements twice from search modal by allowing to hide already added items from the search modal - Remove invalid channels instead of just not rendering them. --- .../components/SettingArrayComponent.tsx | 85 ++++++++++++------- src/components/SearchModal.tsx | 6 +- 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 5ccd2902c15..bdaa6c4e01a 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -68,7 +68,7 @@ export function SettingArrayComponent({ id }: ISettingElementProps) { const [error, setError] = useState(null); - const [items, setItems] = useState([]); + const [items, setItems] = useState(pluginSettings[id] || []); const [text, setText] = useState(""); useEffect(() => { @@ -87,16 +87,12 @@ export function SettingArrayComponent({ useEffect(() => { pluginSettings[id] = items; onChange(items); - }, [items, pluginSettings, id]); + }, [items]); useEffect(() => { onError(error !== null); }, [error]); - if (items.length === 0 && pluginSettings[id].length !== 0) { - setItems(pluginSettings[id]); - } - function openSearchModal(val?: string) { return openModal(modalProps => ( setItems([...items, ...values.map(v => v.id)])} + excludeIds={items} /> )); } - const removeButton = (index: number) => { + const removeButton = (id: string) => { return ( From 75bb8079e7d636456e2e66da1ef1af29819d8983 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:27:10 +0100 Subject: [PATCH 63/78] oop --- src/components/SearchModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 19aa571974d..5e6d979eadc 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -71,7 +71,7 @@ interface UnspecificRowProps { isSelected: boolean, onPressDestination: (destination: DestinationItem) => void, "aria-posinset": number, - "aria-setsize": number; + "aria-setsize": number, } interface SpecificRowProps extends UnspecificRowProps { From 8deb3cdd9348721bc0c075f52db24d21b7c544a6 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:27:21 +0100 Subject: [PATCH 64/78] oop x2 --- src/components/SearchModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 5e6d979eadc..647e1725eab 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -66,7 +66,7 @@ interface DestinationItem { interface UnspecificRowProps { key: string; destination: DestinationItem, - rowMode: string; + rowMode: string, disabled: boolean, isSelected: boolean, onPressDestination: (destination: DestinationItem) => void, From d34793a5975160dcc219d76951ec476845d97b6e Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:30:21 +0100 Subject: [PATCH 65/78] make it fancier --- src/components/SearchModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 647e1725eab..d7207590688 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -156,7 +156,7 @@ function searchTypeToText(type: SearchType) { * * @param {SearchModalProps} props - The props for the SearchModal component. * @param {ModalProps} props.modalProps - The modal props. You get these from the `openModal` function. - * @param {function} props.onSubmit - The function to call when the user submits their selection. + * @param {function} props.onSubmit - Callback function invoked when the user submits their selection. * @param {string} [props.input] - The initial input value for the search bar. * @param {SearchType} [props.searchType="ALL"] - The type of items to search for. * @param {string} [props.subText] - Additional text to display below the heading. From 5b52243c18707538d15fe5fdbbeb035d8f8b3bf6 Mon Sep 17 00:00:00 2001 From: Elvy <88881326+EepyElvyra@users.noreply.github.com> Date: Mon, 3 Feb 2025 21:25:14 +0100 Subject: [PATCH 66/78] fix breakages by latest discord update --- src/components/SearchModal.tsx | 146 ++++++++++++----------- src/utils/modal.tsx | 10 +- src/webpack/common/components.ts | 2 + src/webpack/common/types/components.d.ts | 38 ++++++ 4 files changed, 128 insertions(+), 68 deletions(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index d7207590688..97f52e4460f 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -9,23 +9,25 @@ import "./SearchModal.css"; import { classNameFactory } from "@api/Styles"; import { ModalCloseButton, - ModalContent, - ModalFooter, + ModalContent, ModalFooter, ModalHeader, +ModalListContent, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { + Avatar, Button, - ChannelStore, + ChannelStore, Checkbox, Clickable, Flex, GuildStore, Heading, PresenceStore, React, RelationshipStore, + SearchBar, Text, useCallback, useEffect, @@ -42,8 +44,8 @@ import { JSX } from "react"; const cl = classNameFactory("vc-search-modal-"); -const SearchBarModule = findByPropsLazy("SearchBar", "Checkbox", "AvatarSizes"); -const SearchBarWrapper = findByPropsLazy("SearchBar", "Item"); +const ColorModule = findByPropsLazy("colors", "modules", "themes"); +const SearchBarWrapper = findByPropsLazy("SearchBar"); const TextTypes = findByPropsLazy("APPLICATION", "GROUP_DM", "GUILD"); const SearchHandler = findByCodeLazy("createSearchContext", "setLimit"); @@ -64,7 +66,7 @@ interface DestinationItem { } interface UnspecificRowProps { - key: string; + key: string, destination: DestinationItem, rowMode: string, disabled: boolean, @@ -82,7 +84,6 @@ interface SpecificRowProps extends UnspecificRowProps { interface UserIconProps { user: User; - size?: number; animate?: boolean; "aria-hidden"?: boolean; [key: string]: any; @@ -167,18 +168,18 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const UserIcon = React.memo(function ({ user, - size = SearchBarModule.AvatarSizes.SIZE_32, animate = false, "aria-hidden": ariaHidden = false, ...rest }: UserIconProps) { - const avatarSrc = user.getAvatarURL(void 0, SearchBarModule.getAvatarSize(size), animate); + // FIXME + const avatarSrc = user.getAvatarURL(void 0, 32, animate); return ( - { - onPressDestination?.(destination); - }, [onPressDestination, destination]); - return ( - { onPressDestination?.(destination); }} aria-selected={isSelected} {...interactionProps} {...rest} @@ -236,16 +233,14 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = >{subLabel}
- console.log(e)} - onChange={e => console.log(e)} - type={SearchBarModule.Checkbox.Types.INVERTED} - displayOnly={true} + - + ); }; @@ -259,7 +254,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = } @@ -272,6 +267,36 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = function generateChannelItem(channel: Channel, otherProps: UnspecificRowProps) { const guild = GuildStore.getGuild(channel?.guild_id); + const svgProps = { + "aria-hidden": true, + className: cl("sub-label-icon"), + role: "img", + xmlns: "http://www.w3.org/2000/svg", + width: 24, + height: 24, + fill: "none", + viewBox: "0 0 24 24" + }; + const ForumChannelSvg = () => + + + ; + + const TextIcon = () => + + ; + const channelLabel = getChannelLabel(channel, UserStore, RelationshipStore, false); const parentChannelLabel = (): string => { @@ -283,15 +308,12 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = // @ts-ignore isForumPost is not in the types but exists if (channel.isThread() || channel.isForumPost()) { - // @ts-ignore - const IconComponent = channel.isForumPost() ? SearchBarModule.ForumIcon : SearchBarModule.TextIcon; - subLabel = (
- + { + // @ts-ignore isForumPost is not in the types but exists + channel.isForumPost() ? : + } } label={guildName} @@ -356,7 +378,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = return ( } label={label} subLabel={subLabelValue} @@ -400,7 +422,6 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = hasQuery: boolean; frequentChannels: Channel[]; channelHistory: string[]; - guilds: GuildResult[]; }): Result[] { const removeDuplicates = (arr: Result[]): Result[] => { const clean: any[] = []; @@ -415,7 +436,18 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = return clean; }; - const { results, hasQuery, frequentChannels, channelHistory, guilds } = props; + const guilds: GuildResult[] = useStateFromStores([GuildStore], () => Object.values(GuildStore.getGuilds()).map( + guild => { + return { + type: TextTypes.GUILD, + record: guild, + score: 0, + comparator: guild.name + }; + } + )); + + const { results, hasQuery, frequentChannels, channelHistory } = props; if (hasQuery) return filterItems(results); const recentDestinations = filterItems([ @@ -424,8 +456,9 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = ...guilds ]); + return removeDuplicates( - [...(selected.length > 0 ? selected.map(e => getItem(e)) : []), + [...selected.map(e => getItem(e)), ...recentDestinations ]); } @@ -451,16 +484,13 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = }); } ); + searchHandler.setOptions(searchOptions); searchHandler.setLimit(20); searchHandler.search(""); return searchHandler; } ); useEffect(() => () => searchHandler.destroy(), [searchHandler]); - useEffect(() => { - searchOptions != null && searchOptions !== searchHandler.options && searchHandler.setOptions(searchOptions); - }, [searchHandler, searchOptions] - ); return { search: useCallback(e => { const { query, resultTypes } = e; @@ -501,16 +531,6 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const frequentChannels: Channel[] = useStateFromStores([FrecencyStore], () => FrecencyStore.getFrequentlyWithoutFetchingLatest()); const channelHistory: string[] = useStateFromStores([QuickSwitcherStore], () => QuickSwitcherStore.getChannelHistory()); - const guilds: GuildResult[] = useStateFromStores([GuildStore], () => Object.values(GuildStore.getGuilds()).map( - guild => { - return { - type: TextTypes.GUILD, - record: guild, - score: 0, - comparator: guild.name - }; - } - )); const hasQuery = query !== ""; @@ -520,20 +540,13 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = hasQuery: hasQuery, frequentChannels: frequentChannels, channelHistory: channelHistory, - guilds: guilds - }), [results, hasQuery, frequentChannels, channelHistory, guilds]), + }), [results, hasQuery, frequentChannels, channelHistory]), updateSearchText: updateSearch }; } const { results, updateSearchText } = generateResults(); - const selectedDestinationKeys = useMemo(() => { - return selected.map(destination => `${destination.type}-${destination.id}`) || []; - }, [selected]); - - const rowHeight = useCallback(() => 48, []); - function ModalScroller({ rowData, handleToggleDestination, paddingBottom, paddingTop }: { rowData: Result[], handleToggleDestination: (destination: DestinationItem) => void, paddingBottom?: number, paddingTop?: number; }) { const sectionCount: number[] = useMemo(() => [rowData.length], [rowData.length]); @@ -558,7 +571,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = destination, rowMode: "toggle", disabled: false, - isSelected: selectedDestinationKeys.includes(key), + isSelected: selected.some(e => e.type === destination.type && e.id === destination.id), onPressDestination: handleToggleDestination, "aria-posinset": row + 1, "aria-setsize": results.length @@ -573,9 +586,9 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = if (type === "GUILD") return generateGuildItem(record, rowProps); else throw new Error("Unknown type " + type); - }, [results]); + }, []); - return ; + rowHeight={48} />; } @@ -602,8 +615,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = currentSelected.splice(index, 1); return [...currentSelected]; }); - }, [selected]); - + }, []); return ( @@ -623,7 +635,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType =
{ diff --git a/src/utils/modal.tsx b/src/utils/modal.tsx index d06e58036e7..0edb6ebbfd1 100644 --- a/src/utils/modal.tsx +++ b/src/utils/modal.tsx @@ -75,6 +75,12 @@ interface Modals { }>>; /** This also accepts Scroller props but good luck with that */ ModalContent: ComponentType; + scrollbarType?: unknown; + [prop: string]: any; + }>>; + ModalListContent: ComponentType; [prop: string]: any; @@ -106,7 +112,8 @@ export const Modals: Modals = mapMangledModuleLazy(':"thin")', { ModalHeader: filters.componentByCode(",id:"), ModalContent: filters.componentByCode(".content,"), ModalFooter: filters.componentByCode(".footer,"), - ModalCloseButton: filters.componentByCode(".close]:") + ModalCloseButton: filters.componentByCode(".close]:"), + ModalListContent: filters.componentByCode(",scrollerRef:") }); export const ModalRoot = LazyComponent(() => Modals.ModalRoot); @@ -114,6 +121,7 @@ export const ModalHeader = LazyComponent(() => Modals.ModalHeader); export const ModalContent = LazyComponent(() => Modals.ModalContent); export const ModalFooter = LazyComponent(() => Modals.ModalFooter); export const ModalCloseButton = LazyComponent(() => Modals.ModalCloseButton); +export const ModalListContent = LazyComponent(() => Modals.ModalListContent); export type MediaModalItem = { url: string; diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index dfe00e3378a..66adcbe4204 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -63,6 +63,8 @@ export const TabBar = waitForComponent("TabBar", filters.componentByCode("ref:th export const Paginator = waitForComponent("Paginator", filters.componentByCode('rel:"prev",children:')); export const Clickable = waitForComponent("Clickable", filters.componentByCode("this.context?this.renderNonInteractive():")); export const Avatar = waitForComponent("Avatar", filters.componentByCode(".size-1.375*")); +export const Checkbox = waitForComponent("Checkbox", filters.componentByCode(".checkboxWrapper")); +export const SearchBar = waitForComponent("SearchBar", filters.componentByCode(".containerRef", ".inputRef", ".handleOnChange")); export let createScroller: (scrollbarClassName: string, fadeClassName: string, customThemeClassName: string) => t.ScrollerThin; export let scrollerClasses: Record; diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index d1e117a8ef8..e658d994121 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -17,6 +17,7 @@ */ import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; +import Input = Electron.Input; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; @@ -502,3 +503,40 @@ export type Icon = ComponentType>; +type Checkbox = ComponentType> & Record; + + +type SearchBar = ComponentType void; + className?: string; + placeholder?: string; + iconClassName?: string; + onKeyDown?: (event: KeyboardEvent) => void; + onKeyUp?: (event: KeyboardEvent) => void; + onKeyPress?: (event: KeyboardEvent) => void; + isLoading?: boolean; + size?: string; + disabled?: boolean; + onChange?: (value: string) => void; + onBlur?: () => void; + onFocus?: () => void; + autoComplete?: string; + inputProps?: HTMLProps; + hideSearchIcon?: boolean; + "aria-label"?: string; +}>> & Record; + From a3ee0784e29a78f7a1c124628e6964d0c13c3b43 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:00:13 +0100 Subject: [PATCH 67/78] guh --- src/webpack/common/types/components.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webpack/common/types/components.d.ts b/src/webpack/common/types/components.d.ts index e658d994121..07b4bef8121 100644 --- a/src/webpack/common/types/components.d.ts +++ b/src/webpack/common/types/components.d.ts @@ -17,7 +17,6 @@ */ import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; -import Input = Electron.Input; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; From f0bf8ab2e9a962270014700c85ba7db270057bb9 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:24:07 +0100 Subject: [PATCH 68/78] minor improvements and fixes for plugin migration Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> --- src/plugins/index.ts | 46 +++++++++++++++++++----------- src/plugins/loadingQuotes/index.ts | 9 +++++- src/utils/types.ts | 4 ++- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 27a25659d4c..06ed6a00172 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -23,7 +23,14 @@ import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators"; import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories"; import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; -import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents"; +import { + addMessageClickListener, + addMessagePreEditListener, + addMessagePreSendListener, + removeMessageClickListener, + removeMessagePreEditListener, + removeMessagePreSendListener +} from "@api/MessageEvents"; import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover"; import { Settings, SettingsStore } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; @@ -149,6 +156,27 @@ for (const p of pluginsValues) { const def = p.settings.def[name]; const checks = p.settings.checks?.[name]; p.options[name] = { ...def, ...checks }; + + if ( + (def.type === OptionType.ARRAY || def.type === OptionType.USERS || def.type === OptionType.GUILDS || def.type === OptionType.CHANNELS) + && typeof p.settings.store[name] === "string" + ) { + if (p.settings.store[name] === "") + p.settings.store[name] = def.default ?? []; + else { + logger.info(`Converting string values of setting ${name} of plugin ${p.name} to array`); + + const sep = def.oldStringSeparator ?? ","; + let newVal: string[]; + if (typeof sep === "string" || sep instanceof RegExp) newVal = p.settings.store[name].split(sep); + else newVal = sep(p.settings.store[name]); + + // additional safeguard to prevent the new array to be an empty string, looks weird in the UI. + if (newVal.length > 1 || newVal[0] !== "") p.settings.store[name] = newVal; + else p.settings.store[name] = []; + + } + } } } @@ -255,25 +283,11 @@ export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatc export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { const { - name, commands, contextMenus, settings, managedStyle, userProfileBadge, + name, commands, contextMenus, managedStyle, userProfileBadge, onBeforeMessageEdit, onBeforeMessageSend, onMessageClick, renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton } = p; - if (settings != null) - for (const setting of Object.keys(settings.def)) { - const { type } = settings.def[setting]; - if (type === OptionType.ARRAY || type === OptionType.USERS || type === OptionType.GUILDS || type === OptionType.CHANNELS) { - if (typeof settings.store[setting] === "string") { - logger.info(`Converting string values of setting ${setting} of plugin ${name} to array`); - - const sep = settings.def[setting].oldStringSeparator ?? ","; - if (typeof sep === "string") settings.store[setting] = settings.store[setting].split(sep); - else settings.store[setting] = sep(settings.store[setting]); - } - } - } - if (p.start) { logger.info("Starting plugin", name); if (p.started) { diff --git a/src/plugins/loadingQuotes/index.ts b/src/plugins/loadingQuotes/index.ts index 7f7a2508792..84043817762 100644 --- a/src/plugins/loadingQuotes/index.ts +++ b/src/plugins/loadingQuotes/index.ts @@ -45,7 +45,14 @@ const settings = definePluginSettings({ additionalQuotes: { description: "Additional custom quotes to possibly appear", type: OptionType.ARRAY, - oldStringSeparator: "|", + oldStringSeparator: s => { + if ("additionalQuotesDelimiter" in settings.store) { + const deli = settings.store.additionalQuotesDelimiter ?? "|"; + delete settings.store.additionalQuotesDelimiter; + return s.split(deli); + } + return s.split("|"); + }, }, }); diff --git a/src/utils/types.ts b/src/utils/types.ts index 112f146a561..9e94e32d7ce 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -315,10 +315,12 @@ export interface PluginSettingArrayDef { /** * The text to show in the context-menu. * If not specified, the setting name will be used. + * Only applies to User, Channel, and Guild arrays. */ popoutText?: string; /** * If the context-menu entry should be hidden. + * Only applies to User, Channel, and Guild arrays. */ hidePopout?: boolean; default?: any[]; @@ -326,7 +328,7 @@ export interface PluginSettingArrayDef { * If the setting used to be a string with a custom delimiter, you can specify the delimiter or a function to split the string * @default "," */ - oldStringSeparator?: string | ((value: string) => string[]); + oldStringSeparator?: string | ((value: string) => string[]) | RegExp; onChange?(newValue: any[]): void; } From fa4783511aa5b5d8e684fbb693effbe2114570de Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:26:00 +0100 Subject: [PATCH 69/78] add todo comment for removal of migration code --- src/plugins/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 06ed6a00172..0aedefa1855 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -157,6 +157,7 @@ for (const p of pluginsValues) { const checks = p.settings.checks?.[name]; p.options[name] = { ...def, ...checks }; + // TODO remove this in a few months when everyone has updated. if ( (def.type === OptionType.ARRAY || def.type === OptionType.USERS || def.type === OptionType.GUILDS || def.type === OptionType.CHANNELS) && typeof p.settings.store[name] === "string" From b64523cbb666e05df977063404a02e49ba3d03b1 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:42:48 +0100 Subject: [PATCH 70/78] imporve comments --- src/utils/types.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utils/types.ts b/src/utils/types.ts index 9e94e32d7ce..9b177c5f331 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -197,15 +197,18 @@ export const enum OptionType { CUSTOM, ARRAY, /** - * Array of users + * Array of users. + * A user context menu will be automatically added for this setting. */ USERS, /** - * Array of channels + * Array of channels. + * A channel context menu will be automatically added for this setting. */ CHANNELS, /** - * Array of guilds + * Array of guilds. + * A guild context menu will be automatically added for this setting. */ GUILDS } From 7b50083d205cd4e54e9bfc881c2ae6ec449c1e16 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:23:12 +0100 Subject: [PATCH 71/78] add isValid to arrays and fix types --- .../components/SettingArrayComponent.tsx | 19 +++++++++++++++++-- src/utils/types.ts | 6 +++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index e0ab5f0bd88..6d52824990a 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -72,6 +72,18 @@ export function SettingArrayComponent({ const [text, setText] = useState(""); useEffect(() => { + if (text === "") { + setError(null); + return; + } + + if (option.type === OptionType.ARRAY) { + const isValid = option.isValid?.call(definedSettings, text) ?? true; + if (typeof isValid === "string") setError(isValid); + else if (!isValid) setError("Invalid input provided."); + else setError(null); + return; + } if (!isNaN(Number(text)) && text !== "") { if (text.length >= 18 && text.length <= 19) { setError(null); @@ -79,7 +91,10 @@ export function SettingArrayComponent({ setError("Invalid ID"); } } else if (text !== "") { - setError(null); + const isValid = option.isValid?.call(definedSettings, text) ?? true; + if (typeof isValid === "string") setError(isValid); + else if (!isValid) setError("Invalid input provided."); + else setError(null); } }, [text]); @@ -377,7 +392,7 @@ export function SettingArrayComponent({ id={cl("add-button")} onClick={handleSubmit} style={{ background: "none" }} - disabled={((text.length < 18 || text.length > 19) && option.type !== OptionType.ARRAY) || text === ""} + disabled={((text.length < 18 || text.length > 19) && option.type !== OptionType.ARRAY) || text === "" || error != null} > : diff --git a/src/utils/types.ts b/src/utils/types.ts index 9b177c5f331..09e28904679 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -326,14 +326,14 @@ export interface PluginSettingArrayDef { * Only applies to User, Channel, and Guild arrays. */ hidePopout?: boolean; - default?: any[]; + default?: string[]; /** * If the setting used to be a string with a custom delimiter, you can specify the delimiter or a function to split the string * @default "," */ oldStringSeparator?: string | ((value: string) => string[]) | RegExp; - onChange?(newValue: any[]): void; + onChange?(newValue: string[]): void; } export interface IPluginOptionComponentProps { @@ -434,7 +434,7 @@ export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginOptionComponent = PluginSettingComponentDef & Omit; export type PluginOptionCustom = PluginSettingCustomDef & Pick; -export type PluginOptionArray = PluginSettingArrayDef & PluginSettingCommon; +export type PluginOptionArray = PluginSettingArrayDef & PluginSettingCommon & IsDisabled & IsValid; export type PluginNative any>> = { [key in keyof PluginExports]: From 64a657b936378dd2c9db3096b16ebb09c1111d96 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:29:55 +0100 Subject: [PATCH 72/78] simplify conditionals --- .../components/SettingArrayComponent.tsx | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 6d52824990a..058d6d7313e 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -77,20 +77,18 @@ export function SettingArrayComponent({ return; } - if (option.type === OptionType.ARRAY) { - const isValid = option.isValid?.call(definedSettings, text) ?? true; - if (typeof isValid === "string") setError(isValid); - else if (!isValid) setError("Invalid input provided."); - else setError(null); + if (items.includes(text)) { + setError("This item is already added"); return; } - if (!isNaN(Number(text)) && text !== "") { + + if (option.type !== OptionType.ARRAY && !isNaN(Number(text)) && text !== "") { if (text.length >= 18 && text.length <= 19) { setError(null); } else { setError("Invalid ID"); } - } else if (text !== "") { + } else { const isValid = option.isValid?.call(definedSettings, text) ?? true; if (typeof isValid === "string") setError(isValid); else if (!isValid) setError("Invalid input provided."); @@ -331,19 +329,6 @@ export function SettingArrayComponent({ return elements; } - function handleSubmit() { - if (items.includes(text)) { - setError("This item is already added"); - setText(""); - return; - } - - setItems([...items, text]); - - setText(""); - setError(null); - } - return ( @@ -390,9 +375,9 @@ export function SettingArrayComponent({ : From 3766cc829ea23728f58cfdece409d4b096933565 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:39:13 +0100 Subject: [PATCH 73/78] guh --- src/components/SearchModal.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 97f52e4460f..5aa66c1ae4a 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -9,9 +9,10 @@ import "./SearchModal.css"; import { classNameFactory } from "@api/Styles"; import { ModalCloseButton, - ModalContent, ModalFooter, + ModalContent, + ModalFooter, ModalHeader, -ModalListContent, + ModalListContent, ModalProps, ModalRoot, ModalSize @@ -20,7 +21,9 @@ import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy import { Avatar, Button, - ChannelStore, Checkbox, Clickable, + ChannelStore, + Checkbox, + Clickable, Flex, GuildStore, Heading, From 36c81714ffba4e61cf411e43e43581e053f3bb17 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:12:16 +0100 Subject: [PATCH 74/78] catch manual edits of settings without a reload when opening settings Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> --- .../components/SettingArrayComponent.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 058d6d7313e..6690ed78125 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -68,9 +68,26 @@ export function SettingArrayComponent({ id }: ISettingElementProps) { const [error, setError] = useState(null); - const [items, setItems] = useState(pluginSettings[id] || []); + const [items, setItems] = useState(ensureSettingsMigrated() || []); const [text, setText] = useState(""); + function ensureSettingsMigrated(): string[] | undefined { + // in case the settings get manually overridden without a restart of Vencord itself this will prevent crashing + if (pluginSettings[id] == null || Array.isArray(pluginSettings[id])) { + return pluginSettings[id]; + } + let migrated: string[]; + if (typeof option.oldStringSeparator === "string" || option.oldStringSeparator instanceof RegExp) { + migrated = pluginSettings[id]?.split(option.oldStringSeparator); + } else if (typeof option.oldStringSeparator === "function") { + migrated = option.oldStringSeparator(pluginSettings[id]); + } else { + throw new Error(`Invalid oldStringSeparator for in setting ${id} for plugin ${definedSettings?.pluginName || "Unknown plugin"}`); + } + onChange(migrated); + return migrated; + } + useEffect(() => { if (text === "") { setError(null); From 25a1fd13760f9b9cc6084c982eaaf9f188e5ed27 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:54:01 +0100 Subject: [PATCH 75/78] Fix return type Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> --- src/components/SearchModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 5aa66c1ae4a..28bcb91e8ca 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -302,7 +302,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = const channelLabel = getChannelLabel(channel, UserStore, RelationshipStore, false); - const parentChannelLabel = (): string => { + const parentChannelLabel = (): string | null => { const parentChannel = ChannelStore.getChannel(channel.parent_id); return parentChannel ? getChannelLabel(parentChannel, UserStore, RelationshipStore, false) : null; }; From 6156828d40c13418e586c4b20103715c2c7815e5 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Thu, 6 Feb 2025 01:53:07 +0100 Subject: [PATCH 76/78] wrap functions in errorboundary Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> --- .../PluginSettings/components/SettingArrayComponent.tsx | 6 ++---- src/components/SearchModal.tsx | 8 ++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/PluginSettings/components/SettingArrayComponent.tsx b/src/components/PluginSettings/components/SettingArrayComponent.tsx index 6690ed78125..9b8f9d1f659 100644 --- a/src/components/PluginSettings/components/SettingArrayComponent.tsx +++ b/src/components/PluginSettings/components/SettingArrayComponent.tsx @@ -59,7 +59,7 @@ const SearchIcon = () => { ; }; -export function SettingArrayComponent({ +export const SettingArrayComponent = ErrorBoundary.wrap(function SettingArrayComponent({ option, pluginSettings, definedSettings, @@ -351,7 +351,6 @@ export function SettingArrayComponent({ {wordsToTitle(wordsFromCamel(id))} {option.description} - {option.type === OptionType.ARRAY || option.type === OptionType.USERS ? items.map((item, index) => ( } - {error && {error}} ); -} +},); diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index 28bcb91e8ca..ad58e897425 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -7,6 +7,7 @@ import "./SearchModal.css"; import { classNameFactory } from "@api/Styles"; +import { ErrorBoundary } from "@components/index"; import { ModalCloseButton, ModalContent, @@ -167,7 +168,7 @@ function searchTypeToText(type: SearchType) { * @param {string[]} [props.excludeIds] - An array of IDs to exclude from the search results. * @returns The rendered SearchModal component. */ -export default function SearchModal({ modalProps, onSubmit, input, searchType = "ALL", subText, excludeIds }: SearchModalProps) { +export default ErrorBoundary.wrap(function SearchModal({ modalProps, onSubmit, input, searchType = "ALL", subText, excludeIds }: SearchModalProps) { const UserIcon = React.memo(function ({ user, @@ -702,4 +703,7 @@ export default function SearchModal({ modalProps, onSubmit, input, searchType = ); -} +}, { + noop: true, + onError: ({ props }) => props.modalProps.onClose() +}); From 20183377815e454916aee8434b684a035f687ced Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Thu, 6 Feb 2025 01:59:45 +0100 Subject: [PATCH 77/78] fix silly crash Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com> --- src/components/SearchModal.tsx | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index ad58e897425..278abaaf1c1 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -440,16 +440,19 @@ export default ErrorBoundary.wrap(function SearchModal({ modalProps, onSubmit, i return clean; }; - const guilds: GuildResult[] = useStateFromStores([GuildStore], () => Object.values(GuildStore.getGuilds()).map( - guild => { - return { - type: TextTypes.GUILD, - record: guild, - score: 0, - comparator: guild.name - }; - } - )); + let guilds: GuildResult[] = []; + + if (resultTypes.includes("GUILD")) + guilds = Object.values(GuildStore.getGuilds()).map( + guild => { + return { + type: TextTypes.GUILD, + record: guild, + score: 0, + comparator: guild.name + }; + } + ); const { results, hasQuery, frequentChannels, channelHistory } = props; if (hasQuery) return filterItems(results); From 874422557c3c3dab98a664bddc3b8a422328c754 Mon Sep 17 00:00:00 2001 From: Elvyra <88881326+EepyElvyra@users.noreply.github.com> Date: Sun, 9 Feb 2025 13:19:33 +0100 Subject: [PATCH 78/78] Guh? --- src/plugins/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/index.ts b/src/plugins/index.ts index a3d05d2fe2c..44d8d73d652 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -35,10 +35,8 @@ import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/Messag import { Settings, SettingsStore } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { Logger } from "@utils/Logger"; - import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches"; import { OptionType, Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types"; - import { FluxDispatcher } from "@webpack/common"; import { FluxEvents } from "@webpack/types";