From 45b9cc479be9173e43062d43bb546027bf8321c8 Mon Sep 17 00:00:00 2001 From: MasterPepe <56143265+Accentaro@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:33:23 +0100 Subject: [PATCH] Feat: CollapsibleUI plugin (#970) * Add CollapsibleUI plugin * Fix Conflicts * fix lint * lint * use node 22 --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- .github/workflows/build.yml | 4 +- .github/workflows/buildBranches.yml | 4 +- .github/workflows/nixosBuild.yml | 4 +- .github/workflows/publish.yml | 4 +- .github/workflows/reportBrokenPlugins.yml | 4 +- .github/workflows/test.yml | 4 +- src/equicordplugins/_api/userArea.ts | 4 +- src/equicordplugins/collapsibleUi/index.tsx | 148 ++++++++++++ src/equicordplugins/collapsibleUi/settings.ts | 70 ++++++ src/equicordplugins/collapsibleUi/style.css | 213 ++++++++++++++++++ src/equicordplugins/hideChatButtons/index.tsx | 2 +- .../messageLoggerEnhanced/db.ts | 6 +- src/plugins/ircColors/index.ts | 5 +- 13 files changed, 452 insertions(+), 20 deletions(-) create mode 100644 src/equicordplugins/collapsibleUi/index.tsx create mode 100644 src/equicordplugins/collapsibleUi/settings.ts create mode 100644 src/equicordplugins/collapsibleUi/style.css diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd39145b0..13af317c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,10 +23,10 @@ jobs: - uses: pnpm/action-setup@v3 - - name: Use Node.js 20 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/buildBranches.yml b/.github/workflows/buildBranches.yml index 568c8a379..e79258ea6 100644 --- a/.github/workflows/buildBranches.yml +++ b/.github/workflows/buildBranches.yml @@ -38,11 +38,11 @@ jobs: - uses: pnpm/action-setup@v3 if: github.ref_name != 'bun' - - name: Use Node.js 20 + - name: Use Node.js 22 if: github.ref_name != 'bun' uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/nixosBuild.yml b/.github/workflows/nixosBuild.yml index 827e4d9eb..4c70f85ac 100644 --- a/.github/workflows/nixosBuild.yml +++ b/.github/workflows/nixosBuild.yml @@ -20,10 +20,10 @@ jobs: - uses: pnpm/action-setup@v3 - - name: Use Node.js 20 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2452e5fc8..9c2523eab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -62,11 +62,11 @@ jobs: if: steps.check.outputs.release == 'true' uses: pnpm/action-setup@v3 - - name: Use Node.js 20 + - name: Use Node.js 22 if: steps.check.outputs.release == 'true' uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 117a9a70c..9eec2e800 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -33,10 +33,10 @@ jobs: - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json - - name: Use Node.js 20 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 708974e68..b6ae47688 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,10 +18,10 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json - - name: Use Node.js 20 + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: "pnpm" - name: Install dependencies diff --git a/src/equicordplugins/_api/userArea.ts b/src/equicordplugins/_api/userArea.ts index 112ac9bce..6fbefa328 100644 --- a/src/equicordplugins/_api/userArea.ts +++ b/src/equicordplugins/_api/userArea.ts @@ -22,8 +22,8 @@ export default definePlugin({ find: ".DISPLAY_NAME_STYLES_COACHMARK)", replacement: [ { - match: /(?<=className:(\i)\.\i,style:\i,)children:\[/, - replace: "children:[...$self.renderButtons(arguments[0],$1)," + match: /children:\[(?=.{0,50}accountContainerRef:\i)/, + replace: "children:[...$self.renderButtons(arguments[0])," }, // fix discord weird shrink with extra buttons { diff --git a/src/equicordplugins/collapsibleUi/index.tsx b/src/equicordplugins/collapsibleUi/index.tsx new file mode 100644 index 000000000..d72033893 --- /dev/null +++ b/src/equicordplugins/collapsibleUi/index.tsx @@ -0,0 +1,148 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ChannelToolbarButton } from "@api/HeaderBar"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import { classes } from "@utils/misc"; +import definePlugin from "@utils/types"; +import { ContextMenuApi, Menu } from "@webpack/common"; +import type { ReactNode, SVGProps } from "react"; + +import { PanelId, panelRegistry, settings, toolbarPanelOrder } from "./settings"; +import managedStyle from "./style.css?managed"; + +const cl = classNameFactory("vc-collapsible-ui-"); + +type StateStore = ReturnType; + +function PanelsIcon(props: SVGProps) { + return ( + + + + ); +} + +function getStateClasses(store: StateStore) { + const classNames = [cl("state"), cl(`transition-${store.transitionMs}`)]; + + for (const panelId of toolbarPanelOrder) { + const panel = panelRegistry[panelId]; + if (store[panel.collapsedKey]) classNames.push(cl(`${panel.classId}-collapsed`)); + } + + return classes(...classNames); +} + +function togglePanel(panelId: PanelId) { + const key = panelRegistry[panelId].collapsedKey; + settings.store[key] = !settings.store[key]; +} + +const ToolbarMenu = ErrorBoundary.wrap(({ onClose }: { onClose(): void; }) => { + const store = settings.use(); + + return ( + + {toolbarPanelOrder.map(panelId => { + const panel = panelRegistry[panelId]; + const collapsed = store[panel.collapsedKey]; + + return ( + togglePanel(panelId)} + /> + ); + })} + + ); +}, { noop: true }); + +const ToolbarButtons = ErrorBoundary.wrap(() => { + const store = settings.use(); + const anyCollapsed = toolbarPanelOrder.some(panelId => store[panelRegistry[panelId].collapsedKey]); + + return ( + <> + + ContextMenuApi.openContextMenu(event, () => )} + onContextMenu={event => ContextMenuApi.openContextMenu(event, () => )} + /> + + ); +}, { noop: true }); + +const ChatButtonsRow = ErrorBoundary.wrap(({ buttons }: { buttons: ReactNode[]; }) => { + const { chatButtonsCollapsed } = settings.use(); + + if (buttons.length === 0) return <>{buttons}; + + return ( +
+
+ {buttons} +
+
+ ); +}, { noop: true }); + +export default definePlugin({ + name: "CollapsibleUI", + description: "Native collapsible channel, member, chat button, and user area surfaces.", + authors: [EquicordDevs.benjii], + tags: ["ui", "sidebar", "layout"], + managedStyle, + settings, + + headerBarButton: { + icon: PanelsIcon, + location: "channeltoolbar", + priority: 25, + render: () => , + }, + + patches: [ + { + find: ".DISPLAY_NAME_STYLES_COACHMARK)", + replacement: { + match: /className:(\i\.\i),(?=style:(\i),children:\[)/, + replace: "className:$self.userAreaControlsClass($1)," + } + }, + { + find: '"sticker")', + replacement: { + match: /(?<="div",\{.{0,15}children:)(.+?)\}/, + replace: "$self.wrapChatButtons($1)}" + } + } + ], + + wrapChatButtons(buttons: ReactNode[]) { + if (!Array.isArray(buttons) || buttons.length === 0) return buttons; + return ; + }, + + userAreaControlsClass(className: string) { + return classes(className, cl("user-area-controls")); + }, +}); diff --git a/src/equicordplugins/collapsibleUi/settings.ts b/src/equicordplugins/collapsibleUi/settings.ts new file mode 100644 index 000000000..dce2485b2 --- /dev/null +++ b/src/equicordplugins/collapsibleUi/settings.ts @@ -0,0 +1,70 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; + +export const transitionMarkers = [0, 80, 120, 160, 200, 260, 320]; + +export const panelRegistry = { + channelList: { + id: "channelList", + classId: "channel-list", + label: "Channel List", + collapsedKey: "channelListCollapsed", + }, + membersList: { + id: "membersList", + classId: "members-list", + label: "Members List", + collapsedKey: "membersListCollapsed", + }, + chatButtons: { + id: "chatButtons", + classId: "chat-buttons", + label: "Message Buttons", + collapsedKey: "chatButtonsCollapsed", + }, + userArea: { + id: "userArea", + classId: "user-area", + label: "User Area", + collapsedKey: "userAreaCollapsed", + }, +} as const; + +export type PanelId = keyof typeof panelRegistry; + +export const toolbarPanelOrder = ["channelList", "membersList", "chatButtons", "userArea"] as const satisfies readonly PanelId[]; + +export const settings = definePluginSettings({ + transitionMs: { + type: OptionType.SLIDER, + description: "Panel transition speed in milliseconds.", + default: 160, + markers: transitionMarkers, + }, + channelListCollapsed: { + type: OptionType.BOOLEAN, + description: "Persist the channel list as collapsed.", + default: false, + }, + membersListCollapsed: { + type: OptionType.BOOLEAN, + description: "Persist the members list as collapsed.", + default: false, + }, + chatButtonsCollapsed: { + type: OptionType.BOOLEAN, + description: "Persist the message button row as collapsed.", + default: false, + }, + userAreaCollapsed: { + type: OptionType.BOOLEAN, + description: "Persist the user area as collapsed.", + default: false, + }, +}); diff --git a/src/equicordplugins/collapsibleUi/style.css b/src/equicordplugins/collapsibleUi/style.css new file mode 100644 index 000000000..0ff9d8cc3 --- /dev/null +++ b/src/equicordplugins/collapsibleUi/style.css @@ -0,0 +1,213 @@ +body:has(.vc-collapsible-ui-state) { + --vc-cui-chat-buttons-width: min(320px, 36vw); + --vc-cui-collapsed-size: 12px; + --vc-cui-hover-peek: 18px; + --vc-cui-hover-delay: 120ms; + --vc-cui-user-area-collapsed-width: 52px; + --vc-cui-transition: 160ms; + --vc-cui-ease: cubic-bezier(.2, 0, 0, 1); +} + +body:has(.vc-collapsible-ui-transition-0) { + --vc-cui-transition: 0ms; +} + +body:has(.vc-collapsible-ui-transition-80) { + --vc-cui-transition: 80ms; +} + +body:has(.vc-collapsible-ui-transition-120) { + --vc-cui-transition: 120ms; +} + +body:has(.vc-collapsible-ui-transition-160) { + --vc-cui-transition: 160ms; +} + +body:has(.vc-collapsible-ui-transition-200) { + --vc-cui-transition: 200ms; +} + +body:has(.vc-collapsible-ui-transition-260) { + --vc-cui-transition: 260ms; +} + +body:has(.vc-collapsible-ui-transition-320) { + --vc-cui-transition: 320ms; +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebarList_"]:hover) [class*="sidebarList_"] { + transition-delay: var(--vc-cui-hover-delay); +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebarList_"]:hover) [class*="sidebarList_"] > * { + opacity: 1; + pointer-events: auto; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); + transition-delay: var(--vc-cui-hover-delay); +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebarList_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebarList_"] { + width: var(--vc-cui-collapsed-size) !important; + inline-size: var(--vc-cui-collapsed-size) !important; + min-width: var(--vc-cui-collapsed-size) !important; + min-inline-size: var(--vc-cui-collapsed-size) !important; + max-width: var(--vc-cui-collapsed-size) !important; + max-inline-size: var(--vc-cui-collapsed-size) !important; + flex: 0 0 var(--vc-cui-collapsed-size) !important; + overflow: hidden !important; + transition: + width var(--vc-cui-transition) var(--vc-cui-ease), + inline-size var(--vc-cui-transition) var(--vc-cui-ease), + min-width var(--vc-cui-transition) var(--vc-cui-ease), + min-inline-size var(--vc-cui-transition) var(--vc-cui-ease), + max-width var(--vc-cui-transition) var(--vc-cui-ease), + max-inline-size var(--vc-cui-transition) var(--vc-cui-ease), + flex-basis var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebarList_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebarList_"] > * { + opacity: 0; + pointer-events: none; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover)) [class*="sidebar_"] > [class*="panels_"] { + width: var(--vc-cui-user-area-collapsed-width) !important; + inline-size: var(--vc-cui-user-area-collapsed-width) !important; + min-width: var(--vc-cui-user-area-collapsed-width) !important; + min-inline-size: var(--vc-cui-user-area-collapsed-width) !important; + max-width: var(--vc-cui-user-area-collapsed-width) !important; + max-inline-size: var(--vc-cui-user-area-collapsed-width) !important; + overflow: hidden !important; + transition: + width var(--vc-cui-transition) var(--vc-cui-ease), + inline-size var(--vc-cui-transition) var(--vc-cui-ease), + min-width var(--vc-cui-transition) var(--vc-cui-ease), + min-inline-size var(--vc-cui-transition) var(--vc-cui-ease), + max-width var(--vc-cui-transition) var(--vc-cui-ease), + max-inline-size var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover)) .vc-collapsible-ui-user-area-controls { + max-width: 0 !important; + min-width: 0 !important; + overflow: hidden !important; + opacity: 0; + pointer-events: none; + transition: + max-width var(--vc-cui-transition) var(--vc-cui-ease), + opacity var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-channel-list-collapsed):has([class*="sidebar_"] > [class*="panels_"]:hover) .vc-collapsible-ui-user-area-controls { + opacity: 1; + pointer-events: auto; +} + +body:has(.vc-collapsible-ui-members-list-collapsed) [class*="membersWrap"] { + overflow: hidden !important; + transition: + width var(--vc-cui-transition) var(--vc-cui-ease), + min-width var(--vc-cui-transition) var(--vc-cui-ease), + max-width var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:hover) [class*="membersWrap"], +body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:focus-within) [class*="membersWrap"] { + transition-delay: var(--vc-cui-hover-delay); +} + +body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:hover) [class*="membersWrap"] > :where(*), +body:has(.vc-collapsible-ui-members-list-collapsed):has([class*="membersWrap"]:focus-within) [class*="membersWrap"] > :where(*) { + opacity: 1; + pointer-events: auto; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); + transition-delay: var(--vc-cui-hover-delay); +} + +body:has(.vc-collapsible-ui-members-list-collapsed):not(:has([class*="membersWrap"]:hover), :has([class*="membersWrap"]:focus-within)) [class*="membersWrap"] { + width: var(--vc-cui-collapsed-size) !important; + min-width: var(--vc-cui-collapsed-size) !important; + max-width: var(--vc-cui-collapsed-size) !important; +} + +body:has(.vc-collapsible-ui-members-list-collapsed):not(:has([class*="membersWrap"]:hover), :has([class*="membersWrap"]:focus-within)) [class*="membersWrap"] > :where(*) { + opacity: 0; + pointer-events: none; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); +} + +body:has(.vc-collapsible-ui-user-area-collapsed):has([class*="sidebar_"] > [class*="panels_"]:hover) [class*="sidebar_"] > [class*="panels_"] > :where(*), +body:has(.vc-collapsible-ui-user-area-collapsed):has([class*="sidebar_"] > [class*="panels_"]:focus-within) [class*="sidebar_"] > [class*="panels_"] > :where(*) { + opacity: 1; + pointer-events: auto; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); +} + +body:has(.vc-collapsible-ui-user-area-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:focus-within)) [class*="sidebar_"] > [class*="panels_"] { + height: var(--vc-cui-hover-peek) !important; + block-size: var(--vc-cui-hover-peek) !important; + min-height: var(--vc-cui-hover-peek) !important; + min-block-size: var(--vc-cui-hover-peek) !important; + max-height: var(--vc-cui-hover-peek) !important; + max-block-size: var(--vc-cui-hover-peek) !important; + overflow: hidden !important; + border-top-width: 0 !important; + padding-block: 0 !important; + transition: + height var(--vc-cui-transition) var(--vc-cui-ease), + block-size var(--vc-cui-transition) var(--vc-cui-ease), + min-height var(--vc-cui-transition) var(--vc-cui-ease), + min-block-size var(--vc-cui-transition) var(--vc-cui-ease), + max-height var(--vc-cui-transition) var(--vc-cui-ease), + max-block-size var(--vc-cui-transition) var(--vc-cui-ease), + padding var(--vc-cui-transition) var(--vc-cui-ease) !important; +} + +body:has(.vc-collapsible-ui-user-area-collapsed):not(:has([class*="sidebar_"] > [class*="panels_"]:hover), :has([class*="sidebar_"] > [class*="panels_"]:focus-within)) [class*="sidebar_"] > [class*="panels_"] > :where(*) { + opacity: 0; + pointer-events: none; + transition: opacity var(--vc-cui-transition) var(--vc-cui-ease); +} + +.vc-collapsible-ui-chat-buttons { + display: flex; + align-items: center; + min-inline-size: 0; +} + +.vc-collapsible-ui-chat-buttons-collapsed { + min-inline-size: var(--vc-cui-hover-peek); +} + +.vc-collapsible-ui-chat-buttons-items { + display: flex; + align-items: center; + min-inline-size: 0; + max-inline-size: var(--vc-cui-chat-buttons-width); + overflow: hidden; + transition: + max-inline-size var(--vc-cui-transition) var(--vc-cui-ease), + opacity var(--vc-cui-transition) var(--vc-cui-ease), + margin var(--vc-cui-transition) var(--vc-cui-ease); +} + +.vc-collapsible-ui-chat-buttons-collapsed .vc-collapsible-ui-chat-buttons-items { + max-inline-size: 0; + opacity: 0; + pointer-events: none; +} + +.vc-collapsible-ui-chat-buttons-collapsed:hover .vc-collapsible-ui-chat-buttons-items { + max-inline-size: var(--vc-cui-chat-buttons-width); + opacity: 1; + pointer-events: auto; + transition-delay: var(--vc-cui-hover-delay); +} + +@media (prefers-reduced-motion: reduce) { + body:has(.vc-collapsible-ui-state) { + --vc-cui-transition: 0ms; + } +} diff --git a/src/equicordplugins/hideChatButtons/index.tsx b/src/equicordplugins/hideChatButtons/index.tsx index 9bdbfc98e..5f19fe715 100644 --- a/src/equicordplugins/hideChatButtons/index.tsx +++ b/src/equicordplugins/hideChatButtons/index.tsx @@ -74,7 +74,7 @@ function ButtonsInnerComponent({ buttons }: { buttons: ReactNode[]; }) { export default definePlugin({ name: "HideChatButtons", - description: "able to hide the chat buttons", + description: "Able to hide the chat buttons", settings: settings, authors: [EquicordDevs.iamme], patches: [ diff --git a/src/equicordplugins/messageLoggerEnhanced/db.ts b/src/equicordplugins/messageLoggerEnhanced/db.ts index 7afb56fa9..a9824e3dc 100644 --- a/src/equicordplugins/messageLoggerEnhanced/db.ts +++ b/src/equicordplugins/messageLoggerEnhanced/db.ts @@ -256,9 +256,11 @@ export async function deleteMessagesBulkIDB(message_ids: string[]) { message_ids.forEach(id => cachedMessages.delete(id)); } -export async function clearMessagesIDB() { - await db.clear("messages"); +export async function clearMessagesIDB(showToast = true) { cachedMessages.clear(); + await db.clear("messages"); + if (!showToast) return; + Toasts.show({ type: Toasts.Type.MESSAGE, message: "Cleared message log database and cache.", diff --git a/src/plugins/ircColors/index.ts b/src/plugins/ircColors/index.ts index 66bc716ac..c1d41fbfe 100644 --- a/src/plugins/ircColors/index.ts +++ b/src/plugins/ircColors/index.ts @@ -21,8 +21,7 @@ import { getCustomColorString } from "@equicordplugins/customUserColors"; import { hash as h64 } from "@intrnl/xxhash64"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { useMemo } from "@webpack/common"; -import { UserStore } from "@webpack/common"; +import { useMemo, UserStore } from "@webpack/common"; // Calculate a CSS color string based on the user ID function calculateNameColorForUser(id?: string) { @@ -139,7 +138,7 @@ export default definePlugin({ } } } - + return dmColor; },