From 1f045a0557e646da920d9e10a1c74110dce4fbcd Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sun, 26 Apr 2026 20:54:33 +0200 Subject: [PATCH 01/27] Settings: add support for searching acronyms like BF & SHC --- src/components/settings/tabs/plugins/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index 1ce0d862d..18be40817 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -168,14 +168,13 @@ function PluginSettings() { const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => { const { status, tags } = searchValue; - const enabled = isPluginEnabled(plugin.name); switch (status) { case SearchStatus.DISABLED: - if (enabled) return false; + if (isPluginEnabled(plugin.name)) return false; break; case SearchStatus.ENABLED: - if (!enabled) return false; + if (!isPluginEnabled(plugin.name)) return false; break; case SearchStatus.NEW: if (!newPlugins?.includes(plugin.name)) return false; @@ -194,6 +193,7 @@ function PluginSettings() { return ( plugin.name.toLowerCase().includes(search) || + plugin.name.match(/[A-Z]/g)?.join("").toLowerCase().includes(search) || // acronyms like BF for BetterFolders plugin.description.toLowerCase().includes(search) || plugin.searchTerms?.some(t => t.toLowerCase().includes(search)) ); From 10339e2cb15f6e0e1d9cd6878e9cd5ab6daef7ee Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:39:30 -0400 Subject: [PATCH 02/27] Fix Reporter --- src/equicordplugins/findReply/ReplyNavigator.tsx | 9 +++------ src/plugins/reviewDB/components/ReviewModal.tsx | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/equicordplugins/findReply/ReplyNavigator.tsx b/src/equicordplugins/findReply/ReplyNavigator.tsx index 7875d79f5..1c656d1ce 100644 --- a/src/equicordplugins/findReply/ReplyNavigator.tsx +++ b/src/equicordplugins/findReply/ReplyNavigator.tsx @@ -7,18 +7,15 @@ import "./styles.css"; import ErrorBoundary from "@components/ErrorBoundary"; +import { Paginator, requirePaginator } from "@plugins/reviewDB/components/ReviewModal"; import { ModalCloseButton } from "@utils/modal"; -import * as t from "@vencord/discord-types"; import { Message } from "@vencord/discord-types"; -import { DefaultExtractAndLoadChunksRegex, extractAndLoadChunksLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack"; +import { findCssClassesLazy } from "@webpack"; import { React, useRef, useState } from "@webpack/common"; -import { ComponentProps, MutableRefObject } from "react"; +import { MutableRefObject } from "react"; import { jumper } from "./index"; -const Paginator = findComponentByCodeLazy>('rel:"prev",children:'); -const requirePaginator = extractAndLoadChunksLazy(['name:"SearchResults"'], new RegExp(`${DefaultExtractAndLoadChunksRegex.source}.{0,30}?name:"SearchResults"`)); - const containerStyles = findCssClassesLazy("containerBottom", "containerTop"); export default function ReplyNavigator({ replies }: { replies: Message[]; }) { diff --git a/src/plugins/reviewDB/components/ReviewModal.tsx b/src/plugins/reviewDB/components/ReviewModal.tsx index 8690c85ce..89fc0740f 100644 --- a/src/plugins/reviewDB/components/ReviewModal.tsx +++ b/src/plugins/reviewDB/components/ReviewModal.tsx @@ -32,9 +32,9 @@ import { ComponentProps } from "react"; import ReviewComponent from "./ReviewComponent"; import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; -const Paginator = findComponentByCodeLazy>('rel:"prev",children:'); +export const Paginator = findComponentByCodeLazy>('rel:"prev",children:'); // This matches a massive module with ~230k chars so we need an anchor before to prevent REDOS -const requirePaginator = extractAndLoadChunksLazy(['name:"SearchResults"'], new RegExp(`name:"StageChannelCall",renderLoader:.+?(?:${DefaultExtractAndLoadChunksRegex.source}).{0,30}?name:"SearchResults"`)); +export const requirePaginator = extractAndLoadChunksLazy(['name:"SearchResults"'], new RegExp(`name:"StageChannelCall",renderLoader:.+?(?:${DefaultExtractAndLoadChunksRegex.source}).{0,30}?name:"SearchResults"`)); function Modal({ modalProps, modalKey, discordId, name, type }: { modalProps: any; modalKey: string, discordId: string; name: string; type: ReviewType; }) { const [data, setData] = useState(); From 393eff5a17d64d2f6e55055925067c4eff6f743a Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:49:04 -0400 Subject: [PATCH 03/27] ClientSideBlock: Shorten Match --- src/equicordplugins/clientSideBlock/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/clientSideBlock/index.tsx b/src/equicordplugins/clientSideBlock/index.tsx index 440531276..68cb61d44 100644 --- a/src/equicordplugins/clientSideBlock/index.tsx +++ b/src/equicordplugins/clientSideBlock/index.tsx @@ -225,7 +225,7 @@ export default definePlugin({ { find: "ACTIVE_NOW_COLUMN)", replacement: { - match: /(?<=__invalid_consentCard.{0,80})(\i),\{(?=\}\)\])/, + match: /(?<=__invalid_consentCard.{0,40})(\i),\{/, replace: '"div",{children:$self.activeNowView($1())' } }, From 7b6934fde7fa445d26fe43d040b33773710572ed Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:54:01 +0100 Subject: [PATCH 04/27] Fix betterfolders nestedFolders navigation bug (#1014) * Fix betterfolders nestedFolders navigation bug * Fix BetterFolders nested navigation * tidy up patches * setting --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/plugins/betterFolders/FolderSideBar.tsx | 36 ++++-- src/plugins/betterFolders/index.tsx | 136 ++++++++++++++++---- 2 files changed, 142 insertions(+), 30 deletions(-) diff --git a/src/plugins/betterFolders/FolderSideBar.tsx b/src/plugins/betterFolders/FolderSideBar.tsx index fb33d9317..ca9be04b2 100644 --- a/src/plugins/betterFolders/FolderSideBar.tsx +++ b/src/plugins/betterFolders/FolderSideBar.tsx @@ -21,19 +21,35 @@ import { findComponentByCodeLazy } from "@webpack"; import { Animations, ChannelRTCStore, useStateFromStores } from "@webpack/common"; import type { CSSProperties } from "react"; -import { ExpandedGuildFolderStore, settings, SortedGuildStore } from "."; +import { ExpandedGuildFolderStore, getExpandedFolderIdSet, getParentFolderId, settings, SortedGuildStore } from "."; const GuildsBar = findComponentByCodeLazy('("guildsnav")'); +type GuildTreeNode = { + id?: string | number; + type?: string; + children?: GuildTreeNode[]; +}; + function getExpandedFolderIds() { - const expandedFolders = ExpandedGuildFolderStore.getExpandedFolders(); - const folders = SortedGuildStore.getGuildFolders(); + const expandedFolders = getExpandedFolderIdSet(); + const tree = SortedGuildStore.getGuildsTree(); + const expandedFolderIds = new Set(); + const stack = [...(tree?.root?.children ?? [])]; - const expandedFolderIds = new Set(); + while (stack.length) { + const node = stack.pop(); + if (!node) continue; - for (const folder of folders) { - if (expandedFolders.has(folder.folderId) && folder.guildIds?.length) { - expandedFolderIds.add(folder.folderId); + if (node.type === "folder") { + const folderId = node.id; + if (folderId != null && expandedFolders.has(Number(folderId))) { + expandedFolderIds.add(Number(folderId)); + } + } + + if (node.children?.length) { + stack.push(...node.children); } } @@ -52,7 +68,11 @@ export default ErrorBoundary.wrap(guildsBarProps => { /> ); - const visible = !!expandedFolderIds.size; + const visible = SortedGuildStore.getGuildFolders().some(folder => + folder.guildIds?.length && + expandedFolderIds.has(Number(folder.folderId)) && + getParentFolderId(folder.folderId) == null + ); const guilds = document.querySelector(guildsBarProps.className.split(" ").map(c => `.${c}`).join("")); // We need to display none if we are in fullscreen. Yes this seems horrible doing with css, but it's literally how Discord does it. diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index 26fc688cf..3b3038af8 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -19,7 +19,7 @@ import "./style.css"; import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; +import { Devs, EquicordDevs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; import type { GuildFolder } from "@vencord/discord-types"; @@ -64,32 +64,86 @@ type FolderMentionProps = { export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore"); export const SortedGuildStore = findStoreLazy("SortedGuildStore"); const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand"); -const FolderItem = findComponentByCodeLazy("FolderItem", "onExpandCollapse", "folderButtonSize"); +const FolderItem = findComponentByCodeLazy("folderButtonContent", "onHoverChange", "treeItemProps"); const MAX_TREE_FILTER_DEPTH = 1000; let dispatchingFoldersClose = false; +let pendingNavigationGuildId: string | null | undefined; +let pendingNavigationCloseServerFolder = false; +let navigationExpandTimeout: ReturnType | undefined; function getGuildFolder(id: string) { return SortedGuildStore.getGuildFolders().find((folder: GuildFolder) => folder.guildIds.includes(id)); } -function expandGuildNavigationPath(guildId: string | null | undefined) { - if (!guildId || guildId === "@me") return; +function getGuildNavigationPathTargets(guildId: string | null | undefined): string[] { + if (!guildId || guildId === "@me") return []; - for (const folderId of getGuildNavigationExpandTargets( + return getGuildNavigationExpandTargets( getNestedFolderMap(), SortedGuildStore.getGuildFolders(), guildId, ExpandedGuildFolderStore.getExpandedFolders() - )) { - if (!ExpandedGuildFolderStore.isFolderExpanded(folderId)) { - FolderUtils.toggleGuildFolderExpand(folderId); + ); +} + +export function getExpandedFolderIdSet() { + return new Set(Array.from(ExpandedGuildFolderStore.getExpandedFolders(), id => Number(id))); +} + +function isFolderExpanded(folderId: string | number) { + return getExpandedFolderIdSet().has(Number(folderId)); +} + +function expandGuildNavigationPathNow(guildId: string | null | undefined) { + for (const folderId of getGuildNavigationPathTargets(guildId)) { + const numericFolderId = Number(folderId); + if (!isFolderExpanded(numericFolderId)) { + FolderUtils.toggleGuildFolderExpand(numericFolderId); } } } +function flushGuildNavigationPath() { + navigationExpandTimeout = undefined; + + const guildId = pendingNavigationGuildId; + const shouldCloseServerFolder = pendingNavigationCloseServerFolder; + pendingNavigationGuildId = undefined; + pendingNavigationCloseServerFolder = false; + + if (!guildId || guildId === "@me") return; + + const folderIds = getGuildNavigationPathTargets(guildId); + const guildFolder = getGuildFolder(guildId); + const folderId = guildFolder?.folderId; + const shouldCloseTargetFolder = shouldCloseServerFolder + && folderId != null + && folderIds.length === 0 + && isFolderExpanded(folderId); + + expandGuildNavigationPathNow(guildId); + + if (shouldCloseTargetFolder) { + FolderUtils.toggleGuildFolderExpand(folderId); + } +} + +function scheduleGuildNavigationPath(guildId: string | null | undefined, closeServerFolder = false) { + const isSamePendingGuild = pendingNavigationGuildId === guildId; + + pendingNavigationGuildId = guildId; + pendingNavigationCloseServerFolder = isSamePendingGuild + ? pendingNavigationCloseServerFolder || closeServerFolder + : closeServerFolder; + + if (navigationExpandTimeout != null) return; + + navigationExpandTimeout = setTimeout(flushGuildNavigationPath, 0); +} + function closeFolders() { - for (const id of ExpandedGuildFolderStore.getExpandedFolders()) + for (const id of getExpandedFolderIdSet()) FolderUtils.toggleGuildFolderExpand(id); } @@ -131,6 +185,8 @@ function filterTreeWithTargetNode(children: any, predicate: (node: any) => boole } function getNestedFolderMap(): Record { + if (!settings.store.enableNestedFolders) return {}; + const nestedFolders = settings.store.nestedFolders ?? {}; const validFolderIds = new Set( SortedGuildStore.getGuildFolders() @@ -227,6 +283,11 @@ export const settings = definePluginSettings({ description: "Force a folder to open when switching to a server of that folder", default: false }, + enableNestedFolders: { + type: OptionType.BOOLEAN, + description: "Allow nesting folders inside other folders by dragging.", + default: true + }, keepIcons: { type: OptionType.BOOLEAN, description: "Keep showing guild icons in the primary guild bar folder when it's open in the BetterFolders sidebar", @@ -255,12 +316,20 @@ export default definePlugin({ name: "BetterFolders", description: "Shows server folders on dedicated sidebar and adds folder related improvements", tags: ["Organisation", "Servers", "Appearance"], - authors: [Devs.juby, Devs.AutumnVN, Devs.Nuckyz], + authors: [Devs.juby, Devs.AutumnVN, Devs.Nuckyz, EquicordDevs.justjxke], isModified: true, settings, start() { settings.store.nestedFolders = getNestedFolderMap(); }, + stop() { + if (navigationExpandTimeout != null) { + clearTimeout(navigationExpandTimeout); + navigationExpandTimeout = undefined; + } + pendingNavigationGuildId = undefined; + pendingNavigationCloseServerFolder = false; + }, patches: [ { @@ -446,26 +515,35 @@ export default definePlugin({ match: /return \i\.type!==\i\.\i\.GUILD/, replace: "return $self.renderFolderChild(arguments[0],arguments[1],arguments[2])" } + }, + { + find: "`transitionToGuild - Transitioning to", + replacement: { + match: /(`transitionToGuild - Transitioning to .{0,80}?guildId:\i,channelId:\i,messageId:\i.{0,40}?`\),)/, + replace: "$&$self.handleGuildNavigation(arguments[0])," + } + }, + { + find: "`transitionTo - Transitioning to", + replacement: { + match: /`transitionTo - Transitioning to \$\{\i\}`\);/, + replace: "$&$self.handleRouteNavigation(arguments[0]);" + } } ], flux: { GUILD_SELECT({ guildId }: { guildId: string | null; }) { if (guildId == null) return; - expandGuildNavigationPath(guildId); + scheduleGuildNavigationPath(guildId); }, CHANNEL_SELECT(data) { const guildFolder = getGuildFolder(data.guildId); if (guildFolder?.folderId) { - expandGuildNavigationPath(data.guildId); - - if (settings.store.forceOpen && !ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) { - FolderUtils.toggleGuildFolderExpand(guildFolder.folderId); - } - if (settings.store.closeServerFolder && ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) { - FolderUtils.toggleGuildFolderExpand(guildFolder.folderId); + if (settings.store.forceOpen || settings.store.closeServerFolder) { + scheduleGuildNavigationPath(data.guildId, settings.store.closeServerFolder); } } else if (settings.store.closeAllFolders) { closeFolders(); @@ -500,6 +578,19 @@ export default definePlugin({ FolderSideBar, closeFolders, + handleGuildNavigation(guildId: string | null | undefined) { + expandGuildNavigationPathNow(guildId); + }, + handleRouteNavigation(route: unknown) { + if (typeof route !== "string") return; + + const guildId = route.match(/^\/channels\/([^/]+)/)?.[1]; + if (!guildId) return; + + if (!settings.store.forceOpen && !settings.store.closeServerFolder) return; + + expandGuildNavigationPathNow(guildId); + }, getGuildMentionCount(guildId: string): number { const mentionChannelIds = ReadStateStore.getMentionChannelIds() ?? []; let count = 0; @@ -619,8 +710,6 @@ export default definePlugin({ props: FolderRenderProps | undefined, baseClassName: string ): string { - if (props?.isBetterFolders) return baseClassName; - const folderNode = props?.folderNode; if (folderNode == null) return baseClassName; @@ -651,6 +740,7 @@ export default definePlugin({ handleFolderDrop(dragItem: FolderDragItem, targetNode: GuildTreeNode, _moveToBelow: boolean, isCombine: boolean): boolean { if (dragItem.type !== "folder") return false; + if (!settings.store.enableNestedFolders) return false; if (targetNode.type === "folder") { const childId = dragItem.nodeId?.toString(); @@ -678,13 +768,14 @@ export default definePlugin({ shouldShowCombineTarget(noCombine: boolean, targetNode: GuildTreeNode): boolean { if (noCombine) return false; + if (!settings.store.enableNestedFolders) return targetNode.parentId == null; return targetNode.parentId == null || targetNode.type === "folder"; }, getFolderAcceptTypes(targetNode: GuildTreeNode): string[] { const GUILD = "guild"; const FOLDER = "folder"; - if (targetNode?.type === FOLDER) return [GUILD, FOLDER]; + if (settings.store.enableNestedFolders && targetNode?.type === FOLDER) return [GUILD, FOLDER]; return [GUILD]; }, @@ -692,6 +783,7 @@ export default definePlugin({ if (dragItem.nodeId === targetNode.id) return false; if (dragItem.type === "folder" && targetNode.type === "folder") { + if (!settings.store.enableNestedFolders) return false; return !hasParentInChain(targetNode.id?.toString(), dragItem.nodeId?.toString()); } @@ -715,7 +807,7 @@ export default definePlugin({ if ( !isBetterFolders || - node.type === "folder" && expandedFolderIds?.has(node.id) || + node.type === "folder" && (node.isBetterFoldersNested === true || expandedFolderIds?.has(node.id)) || node.type === "guild" && node.parentId != null && expandedFolderIds?.has(node.parentId) ) { return originalComponent(); From 26dfa1308c3238e5d79f1ac706481fa361eb836d Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:58:20 -0400 Subject: [PATCH 05/27] ClientsideBlock: Attempt to make patch faster --- src/equicordplugins/clientSideBlock/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/equicordplugins/clientSideBlock/index.tsx b/src/equicordplugins/clientSideBlock/index.tsx index 68cb61d44..fa04cc14b 100644 --- a/src/equicordplugins/clientSideBlock/index.tsx +++ b/src/equicordplugins/clientSideBlock/index.tsx @@ -225,8 +225,8 @@ export default definePlugin({ { find: "ACTIVE_NOW_COLUMN)", replacement: { - match: /(?<=__invalid_consentCard.{0,40})(\i),\{/, - replace: '"div",{children:$self.activeNowView($1())' + match: /(__invalid_consentCard.{0,40}\()(\i),\{/, + replace: '$1"div",{children:$self.activeNowView($2())' } }, // mutual friends list in user profile From 24711e611a71cd60adda052b37a968022a922cef Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:10:32 -0400 Subject: [PATCH 06/27] BetterFolders: Fix FolderItem --- src/plugins/betterFolders/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index 3b3038af8..36ab0e5f4 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -64,7 +64,7 @@ type FolderMentionProps = { export const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore"); export const SortedGuildStore = findStoreLazy("SortedGuildStore"); const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand"); -const FolderItem = findComponentByCodeLazy("folderButtonContent", "onHoverChange", "treeItemProps"); +const FolderItem = findComponentByCodeLazy("FolderItem", "onExpandCollapse", "folderButtonSize"); const MAX_TREE_FILTER_DEPTH = 1000; let dispatchingFoldersClose = false; @@ -529,6 +529,13 @@ export default definePlugin({ match: /`transitionTo - Transitioning to \$\{\i\}`\);/, replace: "$&$self.handleRouteNavigation(arguments[0]);" } + }, + { + find: ".getCompatibleGuildFolders())", + replacement: { + match: /\i:\(\)=>\i(?=.*?\.FOLDER_ITEM_ANIMATION_DURATION.{0,10}\.animated\)\(\i\.\i\),(\i))/, + replace: "$&,FolderItem:()=>$1" + } } ], From 6e1d17b06ff470daa8fa927c391389cf608708b1 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:15:18 -0400 Subject: [PATCH 07/27] Reporter: Ignore 2 errors --- scripts/generateReport.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index d6373e99f..2dcc54876 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -81,7 +81,9 @@ const IGNORED_DISCORD_ERRORS = [ "Downloading the full bad domains file", /\[GatewaySocket\].{0,110}Cannot access '/, "search for 'name' in undefined", - "Attempting to set fast connect zstd when unsupported" + "Attempting to set fast connect zstd when unsupported", + "Cannot read properties of undefined (reading 'call')", + "Could not complete Remote Auth login, trying to restart with a new Remote Auth session." ] as Array; function toCodeBlock(s: string, indentation = 0, isDiscord = false) { From 752b413c7e034ef8627ca5f33a053f5bf920cb14 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:35:08 -0400 Subject: [PATCH 08/27] Reporter: Fix Matching (reading 'call') --- scripts/generateReport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 2dcc54876..22fc571b1 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -82,7 +82,7 @@ const IGNORED_DISCORD_ERRORS = [ /\[GatewaySocket\].{0,110}Cannot access '/, "search for 'name' in undefined", "Attempting to set fast connect zstd when unsupported", - "Cannot read properties of undefined (reading 'call')", + /Cannot read properties of undefined \(reading 'call'\)/, "Could not complete Remote Auth login, trying to restart with a new Remote Auth session." ] as Array; From fbcb7254283e65e1933d7727bd8f87666765ddfe Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:35:22 +0100 Subject: [PATCH 09/27] fix(channelTabs): honor new tab button behaviour (#1022) --- .../components/ChannelTabsContainer.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx b/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx index d878f1c49..674e514db 100644 --- a/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx +++ b/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx @@ -288,6 +288,16 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) { if (isFullscreen) return null; + const shouldFollowNewTabButton = newTabButtonBehavior && !tabsOverflow; + const newTabButton = ( + + ); + return (
{ scrollerRef.current = node; }} - className={cl("tab-scroller", newTabButtonBehavior && !tabsOverflow && "tab-scroller-following")} + className={cl("tab-scroller", shouldFollowNewTabButton && "tab-scroller-following")} > {openedTabs.filter(tab => tab != null).map((tab, i) => )} {GhostTabs} + {shouldFollowNewTabButton && newTabButton} - + {!shouldFollowNewTabButton && newTabButton}
From e53f1221eb1563ae12156b2d60426543beb2a7c6 Mon Sep 17 00:00:00 2001 From: Key Date: Tue, 28 Apr 2026 12:44:05 +0100 Subject: [PATCH 10/27] FileUpload: add upload on paste (#1025) * FileUpload: add upload on paste - also change the wording of some settings * FileUpload: bypass discord uploads on by default --- src/equicordplugins/fileUpload/index.tsx | 23 +++++++++++++- src/equicordplugins/fileUpload/settings.tsx | 34 ++++++++++++++------- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/equicordplugins/fileUpload/index.tsx b/src/equicordplugins/fileUpload/index.tsx index 959b56949..4a59451aa 100644 --- a/src/equicordplugins/fileUpload/index.tsx +++ b/src/equicordplugins/fileUpload/index.tsx @@ -22,6 +22,7 @@ import { cancelCurrentUpload, getUploadState, isConfigured, subscribeUploadState const cl = classNameFactory("vc-file-upload-"); const { getUserMaxFileSize } = findByPropsLazy("getUserMaxFileSize"); let uploadAddFilesInterceptor: ((event: unknown) => void) | null = null; +let pasteEventListener: ((event: ClipboardEvent) => void) | null = null; type UploadAddFilesEvent = { type: string; @@ -37,7 +38,7 @@ type UploadAddFilesEvent = { }; function shouldInterceptUploadFiles(files: readonly File[], payload: UploadAddFilesEvent): boolean { - if (!settings.store.interceptDiscordUploadOnlyOverLimit) return true; + if (!settings.store.bypassDiscordUploadOnlyOverLimit) return true; const directLimit = [payload.maxFileSize, payload.fileSizeLimit, payload.limits?.fileSize].find(limit => Number.isFinite(limit)) as number | undefined; const fallbackLimit = getUserMaxFileSize(UserStore.getCurrentUser()); @@ -91,6 +92,18 @@ function interceptUploadAddFiles(event: unknown): void { void uploadProvidedFiles(uniqueFiles); } +function handlePaste(event: ClipboardEvent) { + const files = Array.from(event.clipboardData?.files || []); + if (files.length === 0) return; + + if (!settings.store.autoUploadPastedFiles || !isConfigured()) return; + + event.preventDefault(); + event.stopPropagation(); + + void uploadProvidedFiles(files); +} + const ProgressBarInner = () => { const [state, setState] = useState(getUploadState); @@ -248,6 +261,9 @@ export default definePlugin({ uploadAddFilesInterceptor = event => interceptUploadAddFiles(event); FluxDispatcher.addInterceptor(uploadAddFilesInterceptor); + + pasteEventListener = event => handlePaste(event); + document.addEventListener("paste", pasteEventListener, true); }, stop() { if (!uploadAddFilesInterceptor) { @@ -260,6 +276,11 @@ export default definePlugin({ } uploadAddFilesInterceptor = null; + + if (pasteEventListener) { + document.removeEventListener("paste", pasteEventListener, true); + pasteEventListener = null; + } }, shouldBypassDiscordUploadSizeCheck(): boolean { return Boolean((settings.store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload) && isConfigured(); diff --git a/src/equicordplugins/fileUpload/settings.tsx b/src/equicordplugins/fileUpload/settings.tsx index 14980ea55..9f2951b16 100644 --- a/src/equicordplugins/fileUpload/settings.tsx +++ b/src/equicordplugins/fileUpload/settings.tsx @@ -172,15 +172,15 @@ export const settings = definePluginSettings({ default: false, hidden: true }, - interceptDiscordUpload: { + bypassDiscordUpload: { type: OptionType.BOOLEAN, - description: "Intercept Discord uploads and use FileUpload instead.", - default: false, + description: "Bypass Discord uploads and use FileUpload instead.", + default: true, hidden: true }, - interceptDiscordUploadOnlyOverLimit: { + bypassDiscordUploadOnlyOverLimit: { type: OptionType.BOOLEAN, - description: "Only intercept uploads that exceed Discord file size limit.", + description: "Only use FileUpload if the file(s) exceed the file size limit.", default: true, hidden: true }, @@ -239,6 +239,11 @@ export const settings = definePluginSettings({ default: true, hidden: true }, + autoUploadPastedFiles: { + type: OptionType.BOOLEAN, + description: "Automatically upload files from clipboard to image host when pasting in chat input.", + default: false + }, settingsComponent: { type: OptionType.COMPONENT, description: "Settings", @@ -592,17 +597,24 @@ export function SettingsComponent() { /> - + (store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload = v} + checked={Boolean((store as { bypassDiscordUpload?: boolean; }).bypassDiscordUpload)} + onChange={v => (store as { bypassDiscordUpload?: boolean; }).bypassDiscordUpload = v} /> - + store.interceptDiscordUploadOnlyOverLimit = v} + checked={store.autoUploadPastedFiles} + onChange={v => store.autoUploadPastedFiles = v} + /> + + + + store.bypassDiscordUploadOnlyOverLimit = v} /> From d6fdefacc4dfa94d64a31241bc9a739cff16b896 Mon Sep 17 00:00:00 2001 From: Jakub Bednarski <45859721+Leon135@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:44:46 +0200 Subject: [PATCH 11/27] Revert declutter and disable avatar decoration removal if decor is enabled (#1020) * revert(declutter): revert option to hide avatar decoration * fix(declutter): disable avatar decoration removal if Decor plugin is enabled Co-authored-by: Copilot * fix(declutter): address code review --------- Co-authored-by: Leon135 Co-authored-by: Copilot --- src/equicordplugins/declutter/index.tsx | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/equicordplugins/declutter/index.tsx b/src/equicordplugins/declutter/index.tsx index c92aa7ae7..29f43a149 100644 --- a/src/equicordplugins/declutter/index.tsx +++ b/src/equicordplugins/declutter/index.tsx @@ -6,6 +6,7 @@ import "./style.css"; +import { isPluginEnabled } from "@api/PluginManager"; import { definePluginSettings, migrateOldSettingToNewPlugin, migratePluginSetting, migratePluginSettings } from "@api/Settings"; import { Divider } from "@components/Divider"; import { HeadingSecondary } from "@components/Heading"; @@ -13,6 +14,7 @@ import { Notice } from "@components/Notice"; import { classNameFactory } from "@utils/css"; import { Devs, EquicordDevs } from "@utils/index"; import definePlugin, { OptionType } from "@utils/types"; +import { Alerts } from "@webpack/common"; migratePluginSettings("Declutter", "BetterUserArea", "Anammox"); migrateOldSettingToNewPlugin("Declutter", "removeClanTag", "GuildTagSettings", "hideTags"); @@ -37,6 +39,13 @@ export const settings = definePluginSettings({ type: OptionType.COMPONENT, component: () => SectionSeparator("User Profile"), }, + removeAvatarDecoration: { + type: OptionType.BOOLEAN, + description: "Remove avatar decorations.", + default: false, + disabled: () => isPluginEnabled("Decor"), + restartNeeded: true, + }, removeNameplate: { type: OptionType.BOOLEAN, description: "Remove nameplates.", @@ -143,8 +152,29 @@ export default definePlugin({ description: "Cleans up Discord by removing non-essential UI elements like profile effects, shop tabs, boosts, and more.", tags: ["Appearance", "Customisation"], authors: [EquicordDevs.Leon135, Devs.prism, Devs.Kyuuhachi], + start() { + if (isPluginEnabled("Decor") && settings.store.removeAvatarDecoration) { + settings.store.removeAvatarDecoration = false; + Alerts.show({ + title: "Declutter", + body: "Avatar decoration removal has been disabled to prevent conflicts with Decor plugin.", + confirmText: "OK", + // @ts-expect-error not typed + confirmVariant: "critical-primary" + }); + } + }, settings, patches: [ + { + // Avatar decoration + find: "getAvatarDecorationURL:", + replacement: { + match: /(?<=function \i\(\i\){)(?=.{0,150}let{avatarDecoration)/, + replace: "$&return null;" + }, + predicate: () => settings.store.removeAvatarDecoration && !isPluginEnabled("Decor"), + }, { // Nameplate find: ".MINI_PREVIEW,[", From ae72ab6a515533ec2b27f4dfc3b90593918428d1 Mon Sep 17 00:00:00 2001 From: Yonn Date: Tue, 28 Apr 2026 13:45:22 +0200 Subject: [PATCH 12/27] RoleColorEverywhere: Fix issue that role icon doesn't show up (#1018) * RoleColorEverywhere: Fix issue that role icon doesn't show up * Cleanup authors in RoleColorEverywhere * Remove unused import --- src/plugins/roleColorEverywhere/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index 6ebbdf1c2..efcfe6770 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -108,8 +108,8 @@ export default definePlugin({ find: 'tutorialId:"whos-online', replacement: [ { - match: /,"aria-hidden":!0,children:\[.{0,200}— ",\i\]\}\)\]/, - replace: ',"aria-hidden":!0,children:[$self.RoleGroupColor(arguments[0])]' + match: /(#{intl::CHANNEL_MEMBERS_A11Y_LABEL}.+}\):null,).{0,100}?— ",\i\]\}\)\]/, + replace: (_, rest) => `${rest}$self.RoleGroupColor(arguments[0])]` }, ], predicate: () => settings.store.memberList From c2e966e50deeac116e0fe65cb56012d133ec8f24 Mon Sep 17 00:00:00 2001 From: Yonn Date: Tue, 28 Apr 2026 13:51:34 +0200 Subject: [PATCH 13/27] BetterBanReasons: Add per-reason message delete duration option (#1024) * Add per-reason message delete duration option * Update index.tsx * Update index.tsx --------- Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> --- .../betterBanReasons/index.tsx | 128 +++++++++++++----- .../betterBanReasons/styles.css | 7 + src/utils/constants.ts | 4 + 3 files changed, 105 insertions(+), 34 deletions(-) diff --git a/src/equicordplugins/betterBanReasons/index.tsx b/src/equicordplugins/betterBanReasons/index.tsx index 86d0b6dfe..179ab8004 100644 --- a/src/equicordplugins/betterBanReasons/index.tsx +++ b/src/equicordplugins/betterBanReasons/index.tsx @@ -9,48 +9,82 @@ import "./styles.css"; import { definePluginSettings } from "@api/Settings"; import { Heading } from "@components/Heading"; import { DeleteIcon, PlusIcon } from "@components/Icons"; -import { Devs } from "@utils/constants"; +import { Devs, EquicordDevs } from "@utils/constants"; import { classNameFactory } from "@utils/css"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, TextInput } from "@webpack/common"; +import { Button, Select, TextInput, useState } from "@webpack/common"; const cl = classNameFactory("vc-bbr-"); +interface BanReason { + text: string; + deleteSeconds?: number; +} + +const DELETE_DURATION_OPTIONS = [ + { label: "Discord default", value: null }, + { label: "Don't delete any", value: 0 }, + { label: "Previous hour", value: 3600 }, + { label: "Previous 6 hours", value: 21600 }, + { label: "Previous 12 hours", value: 43200 }, + { label: "Previous 24 hours", value: 86400 }, + { label: "Previous 3 days", value: 259200 }, + { label: "Previous 7 days", value: 604800 }, +]; + +function normalizeReason(r: BanReason | string): BanReason { + return typeof r === "string" ? { text: r } : r; +} + +function getStoredReasons(): BanReason[] { + return (settings.store.reasons as (BanReason | string)[]).map(normalizeReason); +} + function ReasonsComponent() { - const { reasons } = settings.store; + const [reasons, setReasons] = useState(getStoredReasons); + + const update = (next: BanReason[]) => { + setReasons(next); + settings.store.reasons = next; + }; return (
Reasons {reasons.map((r, i) => ( -
+
{ - reasons[i] = v; - settings.store.reasons = reasons; - }} + value={r.text} + onChange={v => update(reasons.map((x, j) => j === i ? { ...x, text: v } : x))} placeholder="Reason" /> +
+ Message Auto-delete duration +