From 1a1156e1ed73f71a79cbb8444990928731dbfb44 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:09:55 -0300 Subject: [PATCH 01/70] Add more settings to IgnoreActivities (#2153) --- src/plugins/ignoreActivities/index.tsx | 148 +++++++++++++++++++++++-- 1 file changed, 138 insertions(+), 10 deletions(-) diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 4e747f363..c04ce1c56 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -5,12 +5,14 @@ */ import * as DataStore from "@api/DataStore"; -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; +import { Flex } from "@components/Flex"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import { Margins } from "@utils/margins"; +import definePlugin, { OptionType } from "@utils/types"; import { findStoreLazy } from "@webpack"; -import { StatusSettingsStores, Tooltip } from "webpack/common"; +import { Button, Forms, showToast, StatusSettingsStores, TextInput, Toasts, Tooltip, useEffect, useState } from "webpack/common"; const enum ActivitiesTypes { Game, @@ -69,7 +71,113 @@ function handleActivityToggle(e: React.MouseEvent StatusSettingsStores.ShowCurrentGame.updateSetting(old => old); } -const settings = definePluginSettings({}).withPrivateSettings<{ +function ImportCustomRPCComponent() { + return ( + + Import the application id of the CustomRPC plugin to the allowed list +
+ +
+
+ ); +} + +let allowedIdsPushID: ((id: string) => boolean) | null = null; + +function AllowedIdsComponent(props: { setValue: (value: string) => void; }) { + const [allowedIds, setAllowedIds] = useState(settings.store.allowedIds ?? ""); + + allowedIdsPushID = (id: string) => { + const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean)); + + const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false); + + const ids = Array.from(currentIds).join(", "); + setAllowedIds(ids); + props.setValue(ids); + + return isAlreadyAdded; + }; + + useEffect(() => () => { + allowedIdsPushID = null; + }, []); + + function handleChange(newValue: string) { + setAllowedIds(newValue); + props.setValue(newValue); + } + + return ( + + Allowed List + Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC) + + + ); +} + +const settings = definePluginSettings({ + importCustomRPC: { + type: OptionType.COMPONENT, + description: "", + component: () => + }, + allowedIds: { + type: OptionType.COMPONENT, + description: "", + default: "", + onChange(newValue: string) { + const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean)); + settings.store.allowedIds = Array.from(ids).join(", "); + }, + component: props => + }, + ignorePlaying: { + type: OptionType.BOOLEAN, + description: "Ignore all playing activities (These are usually game and RPC activities)", + default: false + }, + ignoreStreaming: { + type: OptionType.BOOLEAN, + description: "Ignore all streaming activities", + default: false + }, + ignoreListening: { + type: OptionType.BOOLEAN, + description: "Ignore all listening activities (These are usually spotify activities)", + default: false + }, + ignoreWatching: { + type: OptionType.BOOLEAN, + description: "Ignore all watching activities", + default: false + }, + ignoreCompeting: { + type: OptionType.BOOLEAN, + description: "Ignore all competing activities (These are normally special game activities)", + default: false + } +}).withPrivateSettings<{ ignoredActivities: IgnoredActivity[]; }>(); @@ -77,10 +185,26 @@ function getIgnoredActivities() { return settings.store.ignoredActivities ??= []; } +function isActivityTypeIgnored(type: number, id?: string) { + if (id && settings.store.allowedIds.includes(id)) { + return false; + } + + switch (type) { + case 0: return settings.store.ignorePlaying; + case 1: return settings.store.ignoreStreaming; + case 2: return settings.store.ignoreListening; + case 3: return settings.store.ignoreWatching; + case 5: return settings.store.ignoreCompeting; + } + + return false; +} + export default definePlugin({ name: "IgnoreActivities", authors: [Devs.Nuckyz], - description: "Ignore activities from showing up on your status ONLY. You can configure which ones are ignored from the Registered Games and Activities tabs.", + description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.", settings, @@ -141,13 +265,17 @@ export default definePlugin({ }, isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) { - if (props.type === 0 || props.type === 3) { - if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id); - else { - const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; - if (exePath) return !getIgnoredActivities().some(activity => activity.id === exePath); + if (isActivityTypeIgnored(props.type, props.application_id)) return false; + + if (props.application_id != null) { + return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id); + } else { + const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; + if (exePath) { + return !getIgnoredActivities().some(activity => activity.id === exePath); } } + return true; }, From 806960f1c640822e190e2c7f355bda38d596601b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:15:19 -0300 Subject: [PATCH 02/70] ClientTheme: do not use lodash on start method --- src/plugins/clientTheme/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index 5d8cd4dc0..4e07daf42 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -12,7 +12,7 @@ import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import definePlugin, { OptionType, StartAt } from "@utils/types"; import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common"; +import { Button, Forms, useStateFromStores } from "@webpack/common"; const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); @@ -200,8 +200,8 @@ function captureOne(str, regex) { return (result === null) ? null : result[1]; } -function mapReject(arr, mapFunc, rejectFunc = _.isNull) { - return _.reject(arr.map(mapFunc), rejectFunc); +function mapReject(arr, mapFunc) { + return arr.map(mapFunc).filter(Boolean); } function updateColorVars(color: string) { From 553a48b6ce50a107e36e16f9914e723717b82c96 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:20:12 -0300 Subject: [PATCH 03/70] FakeNitro: Fix hyperlink text setting for stickers --- src/plugins/fakeNitro/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 1cf1e536f..5d662feed 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -162,7 +162,7 @@ const settings = definePluginSettings({ default: true }, hyperLinkText: { - description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji name.", + description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.", type: OptionType.STRING, default: "{{NAME}}" } @@ -864,7 +864,9 @@ export default definePlugin({ const url = new URL(link); url.searchParams.set("name", sticker.name); - messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${sticker.name}](${url})` : url}`; + const linkText = s.hyperLinkText.replaceAll("{{NAME}}", sticker.name); + + messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`; extra.stickers!.length = 0; } } From 23ff82fa627807fcbcdea03e88b3f21f65d007ca Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 1 Mar 2024 05:33:20 -0300 Subject: [PATCH 04/70] FakeNitro: Remove extra space in modal --- src/plugins/fakeNitro/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 5d662feed..8cf2492a8 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -791,8 +791,8 @@ export default definePlugin({ title: "Hold on!", body:
- You are trying to send/edit a message that contains a FakeNitro emoji or sticker - , however you do not have permissions to embed links in the current channel. + You are trying to send/edit a message that contains a FakeNitro emoji or sticker, + however you do not have permissions to embed links in the current channel. Are you sure you want to send this message? Your FakeNitro items will appear as a link only. From 4f0c0a12dc1832fbff3a71e05b0eb503e6b9c9c2 Mon Sep 17 00:00:00 2001 From: dolfies Date: Tue, 5 Mar 2024 17:38:49 -0500 Subject: [PATCH 05/70] feat(plugin): ResurrectHome (#2232) --- src/plugins/resurrectHome/README.md | 5 ++ src/plugins/resurrectHome/index.tsx | 125 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/plugins/resurrectHome/README.md create mode 100644 src/plugins/resurrectHome/index.tsx diff --git a/src/plugins/resurrectHome/README.md b/src/plugins/resurrectHome/README.md new file mode 100644 index 000000000..215ef816c --- /dev/null +++ b/src/plugins/resurrectHome/README.md @@ -0,0 +1,5 @@ +# ResurrectHome + +Brings back the phased out [Server Home](https://support.discord.com/hc/en-us/articles/6156116949911-Server-Home-Beta) feature! + +![](https://private-user-images.githubusercontent.com/47677887/309572891-a9ee7354-9e5e-4b81-8faf-304d9c44f512.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MDk0OTE5MTIsIm5iZiI6MTcwOTQ5MTYxMiwicGF0aCI6Ii80NzY3Nzg4Ny8zMDk1NzI4OTEtYTllZTczNTQtOWU1ZS00YjgxLThmYWYtMzA0ZDljNDRmNTEyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDAzMDMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwMzAzVDE4NDY1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBhYzUxMWY1MzQxNTA4NDE1MWU0YjAxNzM1NzI1YWJkMTNiZmNkNjRmYTRkZDg1ZDE5NzdkMjM0MGVjMDA0OWQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.TPYWPRWHTJstfviT9HOaBWFkbBhokyxiDC-gOVL2dqs) diff --git a/src/plugins/resurrectHome/index.tsx b/src/plugins/resurrectHome/index.tsx new file mode 100644 index 000000000..91ed87a02 --- /dev/null +++ b/src/plugins/resurrectHome/index.tsx @@ -0,0 +1,125 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ContextMenuApi, Menu } from "@webpack/common"; + +const settings = definePluginSettings({ + forceServerHome: { + type: OptionType.BOOLEAN, + description: "Force the Server Guide to be the Server Home tab when it is enabled.", + default: false + } +}); + +const contextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (!props?.guild) return; + + const group = findGroupChildrenByChildId("hide-muted-channels", children); + + group?.unshift( + { + settings.store.forceServerHome = !settings.store.forceServerHome; + ContextMenuApi.closeContextMenu(); + }} + /> + + ); +}; + +export default definePlugin({ + name: "ResurrectHome", + description: "Re-enables the Server Home tab when there isn't a Server Guide. Also has an option to force the Server Home over the Server Guide, which is accessible through right-clicking the Server Guide.", + authors: [Devs.Dolfies, Devs.Nuckyz], + settings, + + patches: [ + // Force home deprecation override + { + find: "GuildFeatures.GUILD_HOME_DEPRECATION_OVERRIDE", + all: true, + replacement: [ + { + match: /\i\.hasFeature\(\i\.GuildFeatures\.GUILD_HOME_DEPRECATION_OVERRIDE\)/g, + replace: "true" + } + ], + }, + // Disable feedback prompts + { + find: "GuildHomeFeedbackExperiment.definition.id", + replacement: [ + { + match: /return{showFeedback:\i,setOnDismissedFeedback:(\i)}/, + replace: "return{showFeedback:false,setOnDismissedFeedback:$1}" + } + ] + }, + // This feature was never finished, so the patch is disabled + + // Enable guild feed render mode selector + // { + // find: "2022-01_home_feed_toggle", + // replacement: [ + // { + // match: /showSelector:!1/, + // replace: "showSelector:true" + // } + // ] + // }, + + // Fix focusMessage clearing previously cached messages and causing a loop when fetching messages around home messages + { + find: '"MessageActionCreators"', + replacement: { + match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/, + replace: "before:$1," + } + }, + // Force Server Home instead of Server Guide + { + find: "61eef9_2", + replacement: { + match: /(?<=getMutableGuildChannelsForGuild\(\i\)\);)(?=if\(null==\i\|\|)/, + replace: "if($self.useForceServerHome())return false;" + } + } + ], + + start() { + addContextMenuPatch("guild-context", contextMenuPatch); + }, + + stop() { + removeContextMenuPatch("guild-context", contextMenuPatch); + }, + + useForceServerHome() { + const { forceServerHome } = settings.use(["forceServerHome"]); + + return forceServerHome; + } +}); From 612fdf8952c8b3ad80e16552a146088deadecb53 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 6 Mar 2024 07:11:14 -0300 Subject: [PATCH 06/70] FakeNitro: Fix trimming wrong content --- src/plugins/fakeNitro/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 8cf2492a8..68560817c 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -585,13 +585,15 @@ export default definePlugin({ for (const [index, child] of children.entries()) children[index] = modifyChild(child); children = this.clearEmptyArrayItems(children); - this.trimContent(children); return children; }; try { - return modifyChildren(lodash.cloneDeep(content)); + const newContent = modifyChildren(lodash.cloneDeep(content)); + this.trimContent(newContent); + + return newContent; } catch (err) { new Logger("FakeNitro").error(err); return content; From 42a9fa2d47d2e0d0e4233436086e93f0b3dcb1af Mon Sep 17 00:00:00 2001 From: Kyuuhachi <1547062+Kyuuhachi@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:06:24 +0100 Subject: [PATCH 07/70] Refactor ContextMenuAPI (#2236) --- src/api/ContextMenu.ts | 53 ++++++++++++++++------- src/plugins/_api/contextMenu.ts | 6 +-- src/plugins/_core/settings.tsx | 12 ++--- src/plugins/biggerStreamPreview/index.tsx | 14 +++--- src/plugins/copyUserURLs/index.tsx | 15 +++---- src/plugins/emoteCloner/index.tsx | 18 +++----- src/plugins/imageZoom/index.tsx | 23 +++++----- src/plugins/index.ts | 17 +++++++- src/plugins/messageLogger/index.tsx | 13 +++--- src/plugins/permissionsViewer/index.tsx | 27 ++++-------- src/plugins/pinDms/contextMenus.tsx | 19 +++----- src/plugins/pinDms/index.tsx | 6 +-- src/plugins/resurrectHome/index.tsx | 52 ++++++++++------------ src/plugins/reverseImageSearch/index.tsx | 18 +++----- src/plugins/reviewDB/index.tsx | 13 +++--- src/plugins/searchReply/index.tsx | 19 ++++---- src/plugins/serverProfile/index.tsx | 14 +++--- src/plugins/translate/index.tsx | 9 ++-- src/plugins/unsuppressEmbeds/index.tsx | 15 +++---- src/plugins/vencordToolbox/index.tsx | 9 ++-- src/plugins/viewIcons/index.tsx | 17 +++----- src/plugins/viewRaw/index.tsx | 22 ++++------ src/plugins/voiceMessages/index.tsx | 45 +++++++++---------- src/utils/constants.ts | 4 ++ src/utils/types.ts | 5 +++ 25 files changed, 220 insertions(+), 245 deletions(-) diff --git a/src/api/ContextMenu.ts b/src/api/ContextMenu.ts index d66d98c4f..fdd4facf4 100644 --- a/src/api/ContextMenu.ts +++ b/src/api/ContextMenu.ts @@ -17,22 +17,20 @@ */ import { Logger } from "@utils/Logger"; +import { Menu, React } from "@webpack/common"; import type { ReactElement } from "react"; -type ContextMenuPatchCallbackReturn = (() => void) | void; /** * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example - * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; +export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => void; /** * @param navId The navId of the context menu being patched * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example - * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; +export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => void; const ContextMenuLogger = new Logger("ContextMenu"); @@ -93,14 +91,19 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba * @param id The id of the child. If an array is specified, all ids will be tried * @param children The context menu children */ -export function findGroupChildrenByChildId(id: string | string[], children: Array, _itemsArray?: Array): Array | null { +export function findGroupChildrenByChildId(id: string | string[], children: Array): Array | null { for (const child of children) { if (child == null) continue; + if (Array.isArray(child)) { + const found = findGroupChildrenByChildId(id, child); + if (found !== null) return found; + } + if ( (Array.isArray(id) && id.some(id => child.props?.id === id)) || child.props?.id === id - ) return _itemsArray ?? null; + ) return children; let nextChildren = child.props?.children; if (nextChildren) { @@ -109,7 +112,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra child.props.children = nextChildren; } - const found = findGroupChildrenByChildId(id, nextChildren, nextChildren); + const found = findGroupChildrenByChildId(id, nextChildren); if (found !== null) return found; } } @@ -126,9 +129,12 @@ interface ContextMenuProps { onClose: (callback: (...args: Array) => any) => void; } -const patchedMenus = new WeakSet(); +export function _usePatchContextMenu(props: ContextMenuProps) { + props = { + ...props, + children: cloneMenuChildren(props.children), + }; -export function _patchContextMenu(props: ContextMenuProps) { props.contextMenuApiArguments ??= []; const contextMenuPatches = navPatches.get(props.navId); @@ -137,8 +143,7 @@ export function _patchContextMenu(props: ContextMenuProps) { if (contextMenuPatches) { for (const patch of contextMenuPatches) { try { - const callback = patch(props.children, ...props.contextMenuApiArguments); - if (!patchedMenus.has(props)) callback?.(); + patch(props.children, ...props.contextMenuApiArguments); } catch (err) { ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); } @@ -147,12 +152,30 @@ export function _patchContextMenu(props: ContextMenuProps) { for (const patch of globalPatches) { try { - const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments); - if (!patchedMenus.has(props)) callback?.(); + patch(props.navId, props.children, ...props.contextMenuApiArguments); } catch (err) { ContextMenuLogger.error("Global patch errored,", err); } } - patchedMenus.add(props); + return props; +} + +function cloneMenuChildren(obj: ReactElement | Array | null) { + if (Array.isArray(obj)) { + return obj.map(cloneMenuChildren); + } + + if (React.isValidElement(obj)) { + obj = React.cloneElement(obj); + + if ( + obj?.props?.children && + (obj.type !== Menu.MenuControlItem || obj.type === Menu.MenuControlItem && obj.props.control != null) + ) { + obj.props.children = cloneMenuChildren(obj.props.children); + } + } + + return obj; } diff --git a/src/plugins/_api/contextMenu.ts b/src/plugins/_api/contextMenu.ts index 55fdf3eae..01619546d 100644 --- a/src/plugins/_api/contextMenu.ts +++ b/src/plugins/_api/contextMenu.ts @@ -22,15 +22,15 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "ContextMenuAPI", description: "API for adding/removing items to/from context menus.", - authors: [Devs.Nuckyz, Devs.Ven], + authors: [Devs.Nuckyz, Devs.Ven, Devs.Kyuuhachi], required: true, patches: [ { find: "♫ (つ。◕‿‿◕。)つ ♪", replacement: { - match: /let{navId:/, - replace: "Vencord.Api.ContextMenu._patchContextMenu(arguments[0]);$&" + match: /(?=let{navId:)(?<=function \i\((\i)\).+?)/, + replace: "$1=Vencord.Api.ContextMenu._usePatchContextMenu($1);" } }, { diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 6f43b76a8..01220eb4e 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -30,21 +30,21 @@ export default definePlugin({ authors: [Devs.Ven, Devs.Megu], required: true, - start() { + contextMenus: { // The settings shortcuts in the user settings cog context menu // read the elements from a hardcoded map which for obvious reason // doesn't contain our sections. This patches the actions of our // sections to manually use SettingsRouter (which only works on desktop // but the context menu is usually not available on mobile anyway) - addContextMenuPatch("user-settings-cog", children => () => { - const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any; + "user-settings-cog"(children) { + const section = findGroupChildrenByChildId("VencordSettings", children); section?.forEach(c => { const id = c?.props?.id; if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) { - c.props.action = () => SettingsRouter.open(id); + c!.props.action = () => SettingsRouter.open(id); } }); - }); + } }, patches: [{ diff --git a/src/plugins/biggerStreamPreview/index.tsx b/src/plugins/biggerStreamPreview/index.tsx index 40bbe30a8..8cca912bc 100644 --- a/src/plugins/biggerStreamPreview/index.tsx +++ b/src/plugins/biggerStreamPreview/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { ScreenshareIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import { openImageModal } from "@utils/discord"; @@ -60,7 +60,7 @@ export const handleViewPreview = async ({ guildId, channelId, ownerId }: Applica openImageModal(previewUrl); }; -export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => () => { +export const addViewStreamContext: NavContextMenuPatchCallback = (children, { userId }: { userId: string | bigint; }) => { const stream = ApplicationStreamingStore.getAnyStreamForUser(userId); if (!stream) return; @@ -89,12 +89,8 @@ export default definePlugin({ name: "BiggerStreamPreview", description: "This plugin allows you to enlarge stream previews", authors: [Devs.phil], - start: () => { - addContextMenuPatch("user-context", userContextPatch); - addContextMenuPatch("stream-context", streamContextPatch); - }, - stop: () => { - removeContextMenuPatch("user-context", userContextPatch); - removeContextMenuPatch("stream-context", streamContextPatch); + contextMenus: { + "user-context": userContextPatch, + "stream-context": streamContextPatch } }); diff --git a/src/plugins/copyUserURLs/index.tsx b/src/plugins/copyUserURLs/index.tsx index 9f69674cf..7af8502db 100644 --- a/src/plugins/copyUserURLs/index.tsx +++ b/src/plugins/copyUserURLs/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { LinkIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -29,7 +29,7 @@ interface UserContextProps { user: User; } -const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => { +const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => { if (!user) return; children.push( @@ -46,12 +46,7 @@ export default definePlugin({ name: "CopyUserURLs", authors: [Devs.castdrian], description: "Adds a 'Copy User URL' option to the user context menu.", - - start() { - addContextMenuPatch("user-context", UserContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("user-context", UserContextMenuPatch); - }, + contextMenus: { + "user-context": UserContextMenuPatch + } }); diff --git a/src/plugins/emoteCloner/index.tsx b/src/plugins/emoteCloner/index.tsx index 219ce435f..b25e1be27 100644 --- a/src/plugins/emoteCloner/index.tsx +++ b/src/plugins/emoteCloner/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { CheckedTextInput } from "@components/CheckedTextInput"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; @@ -312,7 +312,7 @@ function isGifUrl(url: string) { return new URL(url).pathname.endsWith(".gif"); } -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {}; if (!favoriteableId) return; @@ -341,7 +341,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) = findGroupChildrenByChildId("copy-link", children)?.push(menuItem); }; -const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => { +const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => { const { id, name, type } = props?.target?.dataset ?? {}; if (!id) return; @@ -363,14 +363,8 @@ export default definePlugin({ description: "Allows you to clone Emotes & Stickers to your own server (right click them)", tags: ["StickerCloner"], authors: [Devs.Ven, Devs.Nuckyz], - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - addContextMenuPatch("expression-picker", expressionPickerPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - removeContextMenuPatch("expression-picker", expressionPickerPatch); + contextMenus: { + "message": messageContextMenuPatch, + "expression-picker": expressionPickerPatch } }); diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 8d8b6726d..048c0ed5b 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -16,14 +16,14 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; import { debounce } from "@utils/debounce"; import definePlugin, { OptionType } from "@utils/types"; -import { ContextMenuApi, Menu, React, ReactDOM } from "@webpack/common"; +import { Menu, React, ReactDOM } from "@webpack/common"; import type { Root } from "react-dom/client"; import { Magnifier, MagnifierProps } from "./components/Magnifier"; @@ -80,25 +80,25 @@ export const settings = definePluginSettings({ }); -const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => { +const imageContextMenuPatch: NavContextMenuPatchCallback = children => { + const { square, nearestNeighbour } = settings.use(["square", "nearestNeighbour"]); + children.push( { - settings.store.square = !settings.store.square; - ContextMenuApi.closeContextMenu(); + settings.store.square = !square; }} /> { - settings.store.nearestNeighbour = !settings.store.nearestNeighbour; - ContextMenuApi.closeContextMenu(); + settings.store.nearestNeighbour = !nearestNeighbour; }} /> | null, @@ -245,7 +248,6 @@ export default definePlugin({ start() { enableStyle(styles); - addContextMenuPatch("image-context", imageContextMenuPatch); this.element = document.createElement("div"); this.element.classList.add("MagnifierContainer"); document.body.appendChild(this.element); @@ -256,6 +258,5 @@ export default definePlugin({ // so componenetWillUnMount gets called if Magnifier component is still alive this.root && this.root.unmount(); this.element?.remove(); - removeContextMenuPatch("image-context", imageContextMenuPatch); } }); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 234838606..7092001ee 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -17,6 +17,7 @@ */ import { registerCommand, unregisterCommand } from "@api/Commands"; +import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; import { Patch, Plugin, StartAt } from "@utils/types"; @@ -119,7 +120,7 @@ export function startDependenciesRecursive(p: Plugin) { } export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { - const { name, commands, flux } = p; + const { name, commands, flux, contextMenus } = p; if (p.start) { logger.info("Starting plugin", name); @@ -154,11 +155,17 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } } + if (contextMenus) { + for (const navId in contextMenus) { + addContextMenuPatch(navId, contextMenus[navId]); + } + } + return true; }, p => `startPlugin ${p.name}`); export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) { - const { name, commands, flux } = p; + const { name, commands, flux, contextMenus } = p; if (p.stop) { logger.info("Stopping plugin", name); if (!p.started) { @@ -192,5 +199,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } } + if (contextMenus) { + for (const navId in contextMenus) { + removeContextMenuPatch(navId, contextMenus[navId]); + } + } + return true; }, p => `stopPlugin ${p.name}`); diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index ef986bf87..8bc563b19 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -18,7 +18,7 @@ import "./messageLogger.css"; -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -45,7 +45,7 @@ function addDeleteStyle() { const REMOVE_HISTORY_ID = "ml-remove-history"; const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style"; -const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => { +const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => { const { message } = props; const { deleted, editHistory, id, channel_id } = message; @@ -94,13 +94,12 @@ export default definePlugin({ description: "Temporarily logs deleted and edited messages.", authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN], - start() { - addDeleteStyle(); - addContextMenuPatch("message", patchMessageContextMenu); + contextMenus: { + "message": patchMessageContextMenu }, - stop() { - removeContextMenuPatch("message", patchMessageContextMenu); + start() { + addDeleteStyle(); }, renderEdit(edit: { timestamp: any, content: string; }) { diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 9e0131e64..07f073d63 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -18,7 +18,7 @@ import "./styles.css"; -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -125,10 +125,10 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { } function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback { - return (children, props) => () => { + return (children, props) => { if (!props) return; if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild))) - return children; + return; const group = findGroupChildrenByChildId(childId, children); @@ -173,19 +173,10 @@ export default definePlugin({ UserPermissions: (guild: Guild, guildMember: GuildMember | undefined, showBoder: boolean) => !!guildMember && , - userContextMenuPatch: makeContextMenuPatch("roles", MenuItemParentType.User), - channelContextMenuPatch: makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel), - guildContextMenuPatch: makeContextMenuPatch("privacy", MenuItemParentType.Guild), - - start() { - addContextMenuPatch("user-context", this.userContextMenuPatch); - addContextMenuPatch("channel-context", this.channelContextMenuPatch); - addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("user-context", this.userContextMenuPatch); - removeContextMenuPatch("channel-context", this.channelContextMenuPatch); - removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch); - }, + contextMenus: { + "user-context": makeContextMenuPatch("roles", MenuItemParentType.User), + "channel-context": makeContextMenuPatch(["mute-channel", "unmute-channel"], MenuItemParentType.Channel), + "guild-context": makeContextMenuPatch("privacy", MenuItemParentType.Guild), + "guild-header-popout": makeContextMenuPatch("privacy", MenuItemParentType.Guild) + } }); diff --git a/src/plugins/pinDms/contextMenus.tsx b/src/plugins/pinDms/contextMenus.tsx index 7d89ec123..1db8b25a9 100644 --- a/src/plugins/pinDms/contextMenus.tsx +++ b/src/plugins/pinDms/contextMenus.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Menu } from "@webpack/common"; import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings"; @@ -50,13 +50,13 @@ function PinMenuItem(channelId: string) { ); } -const GroupDMContext: NavContextMenuPatchCallback = (children, props) => () => { +const GroupDMContext: NavContextMenuPatchCallback = (children, props) => { const container = findGroupChildrenByChildId("leave-channel", children); if (container) container.unshift(PinMenuItem(props.channel.id)); }; -const UserContext: NavContextMenuPatchCallback = (children, props) => () => { +const UserContext: NavContextMenuPatchCallback = (children, props) => { const container = findGroupChildrenByChildId("close-dm", children); if (container) { const idx = container.findIndex(c => c?.props?.id === "close-dm"); @@ -64,12 +64,7 @@ const UserContext: NavContextMenuPatchCallback = (children, props) => () => { } }; -export function addContextMenus() { - addContextMenuPatch("gdm-context", GroupDMContext); - addContextMenuPatch("user-context", UserContext); -} - -export function removeContextMenus() { - removeContextMenuPatch("gdm-context", GroupDMContext); - removeContextMenuPatch("user-context", UserContext); -} +export const contextMenus = { + "gdm-context": GroupDMContext, + "user-context": UserContext +}; diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 792bdab6a..943f0f1b1 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -20,7 +20,7 @@ import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { Channel } from "discord-types/general"; -import { addContextMenus, removeContextMenus } from "./contextMenus"; +import { contextMenus } from "./contextMenus"; import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings"; export default definePlugin({ @@ -29,9 +29,7 @@ export default definePlugin({ authors: [Devs.Ven, Devs.Strencher], settings, - - start: addContextMenus, - stop: removeContextMenus, + contextMenus, usePinCount(channelIds: string[]) { const pinnedDms = usePinnedDms(); diff --git a/src/plugins/resurrectHome/index.tsx b/src/plugins/resurrectHome/index.tsx index 91ed87a02..6b0069a7f 100644 --- a/src/plugins/resurrectHome/index.tsx +++ b/src/plugins/resurrectHome/index.tsx @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { ContextMenuApi, Menu } from "@webpack/common"; +import { Menu } from "@webpack/common"; const settings = definePluginSettings({ forceServerHome: { @@ -30,25 +30,11 @@ const settings = definePluginSettings({ } }); -const contextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (!props?.guild) return; +function useForceServerHome() { + const { forceServerHome } = settings.use(["forceServerHome"]); - const group = findGroupChildrenByChildId("hide-muted-channels", children); - - group?.unshift( - { - settings.store.forceServerHome = !settings.store.forceServerHome; - ContextMenuApi.closeContextMenu(); - }} - /> - - ); -}; + return forceServerHome; +} export default definePlugin({ name: "ResurrectHome", @@ -109,17 +95,25 @@ export default definePlugin({ } ], - start() { - addContextMenuPatch("guild-context", contextMenuPatch); - }, + useForceServerHome, - stop() { - removeContextMenuPatch("guild-context", contextMenuPatch); - }, + contextMenus: { + "guild-context"(children, props) { + const forceServerHome = useForceServerHome(); - useForceServerHome() { - const { forceServerHome } = settings.use(["forceServerHome"]); + if (!props?.guild) return; - return forceServerHome; + const group = findGroupChildrenByChildId("hide-muted-channels", children); + + group?.unshift( + settings.store.forceServerHome = !forceServerHome} + /> + ); + } } }); diff --git a/src/plugins/reverseImageSearch/index.tsx b/src/plugins/reverseImageSearch/index.tsx index 6c5f3e729..415dc13d8 100644 --- a/src/plugins/reverseImageSearch/index.tsx +++ b/src/plugins/reverseImageSearch/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Flex } from "@components/Flex"; import { OpenExternalIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; @@ -84,7 +84,7 @@ function makeSearchItem(src: string) { ); } -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { if (props?.reverseImageSearchType !== "img") return; const src = props.itemHref ?? props.itemSrc; @@ -93,7 +93,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) = group?.push(makeSearchItem(src)); }; -const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { +const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { if (!props?.src) return; const group = findGroupChildrenByChildId("copy-native-link", children) ?? children; @@ -115,14 +115,8 @@ export default definePlugin({ } } ], - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - addContextMenuPatch("image-context", imageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - removeContextMenuPatch("image-context", imageContextMenuPatch); + contextMenus: { + "message": messageContextMenuPatch, + "image-context": imageContextMenuPatch } }); diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index 50bb62184..ad24e9696 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -18,7 +18,7 @@ import "./style.css"; -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import ErrorBoundary from "@components/ErrorBoundary"; import ExpandableHeader from "@components/ExpandableHeader"; import { OpenExternalIcon } from "@components/Icons"; @@ -36,7 +36,7 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { settings } from "./settings"; import { showToast } from "./utils"; -const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => { +const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => { children.push( { const [reviewCount, setReviewCount] = useState(); diff --git a/src/plugins/searchReply/index.tsx b/src/plugins/searchReply/index.tsx index b151712af..35b197874 100644 --- a/src/plugins/searchReply/index.tsx +++ b/src/plugins/searchReply/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { ReplyIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -27,7 +27,7 @@ import { Message } from "discord-types/general"; const messageUtils = findByPropsLazy("replyToMessage"); -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => { // make sure the message is in the selected channel if (SelectedChannelStore.getChannelId() !== message.channel_id) return; const channel = ChannelStore.getChannel(message?.channel_id); @@ -38,7 +38,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag const dmGroup = findGroupChildrenByChildId("pin", children); if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) { const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin"); - return dmGroup.splice(pinIndex + 1, 0, ( + dmGroup.splice(pinIndex + 1, 0, ( messageUtils.replyToMessage(channel, message, e)} /> )); + return; } // servers const serverGroup = findGroupChildrenByChildId("mark-unread", children); if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) { - return serverGroup.unshift(( + serverGroup.unshift(( messageUtils.replyToMessage(channel, message, e)} /> )); + return; } }; @@ -67,12 +69,7 @@ export default definePlugin({ name: "SearchReply", description: "Adds a reply button to search results", authors: [Devs.Aria], - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); + contextMenus: { + "message": messageContextMenuPatch } }); diff --git a/src/plugins/serverProfile/index.tsx b/src/plugins/serverProfile/index.tsx index 68f6193cc..9d495c9d3 100644 --- a/src/plugins/serverProfile/index.tsx +++ b/src/plugins/serverProfile/index.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { Menu } from "@webpack/common"; @@ -12,7 +12,7 @@ import { Guild } from "discord-types/general"; import { openGuildProfileModal } from "./GuildProfileModal"; -const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => { +const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => { const group = findGroupChildrenByChildId("privacy", children); group?.push( @@ -29,12 +29,8 @@ export default definePlugin({ description: "Allows you to view info about a server by right clicking it in the server list", authors: [Devs.Ven, Devs.Nuckyz], tags: ["guild", "info"], - - start() { - addContextMenuPatch(["guild-context", "guild-header-popout"], Patch); - }, - - stop() { - removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch); + contextMenus: { + "guild-context": Patch, + "guild-header-popout": Patch } }); diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index 702e60cf7..f602d1255 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -19,7 +19,7 @@ import "./styles.css"; import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { addButton, removeButton } from "@api/MessagePopover"; @@ -32,7 +32,7 @@ import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon"; import { handleTranslate, TranslationAccessory } from "./TranslationAccessory"; import { translate } from "./utils"; -const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => () => { +const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => { if (!message.content) return; const group = findGroupChildrenByChildId("copy-text", children); @@ -57,13 +57,15 @@ export default definePlugin({ authors: [Devs.Ven], dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], settings, + contextMenus: { + "message": messageCtxPatch + }, // not used, just here in case some other plugin wants it or w/e translate, start() { addAccessory("vc-translation", props => ); - addContextMenuPatch("message", messageCtxPatch); addChatBarButton("vc-translate", TranslateChatBarIcon); addButton("vc-translate", message => { @@ -91,7 +93,6 @@ export default definePlugin({ stop() { removePreSendListener(this.preSend); - removeContextMenuPatch("message", messageCtxPatch); removeChatBarButton("vc-translate"); removeButton("vc-translate"); removeAccessory("vc-translation"); diff --git a/src/plugins/unsuppressEmbeds/index.tsx b/src/plugins/unsuppressEmbeds/index.tsx index a21960774..0e87201c6 100644 --- a/src/plugins/unsuppressEmbeds/index.tsx +++ b/src/plugins/unsuppressEmbeds/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; import { ImageInvisible, ImageVisible } from "@components/Icons"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -24,7 +24,7 @@ import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@web const EMBED_SUPPRESSED = 1 << 2; -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => () => { +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, message: { author, embeds, flags, id: messageId } }) => { const isEmbedSuppressed = (flags & EMBED_SUPPRESSED) !== 0; if (!isEmbedSuppressed && !embeds.length) return; @@ -56,12 +56,7 @@ export default definePlugin({ name: "UnsuppressEmbeds", authors: [Devs.rad, Devs.HypedDomi], description: "Allows you to unsuppress embeds in messages", - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - }, + contextMenus: { + "message": messageContextMenuPatch + } }); diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index 0a805a0d2..ba295fa72 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -19,7 +19,7 @@ import "./index.css"; import { openNotificationLogModal } from "@api/Notifications/notificationLog"; -import { Settings } from "@api/Settings"; +import { Settings, useSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; @@ -30,6 +30,8 @@ import type { ReactNode } from "react"; const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider"); function VencordPopout(onClose: () => void) { + const { useQuickCss } = useSettings(); + const pluginEntries = [] as ReactNode[]; for (const plugin of Object.values(Vencord.Plugins.plugins)) { @@ -68,11 +70,10 @@ function VencordPopout(onClose: () => void) { /> { - Settings.useQuickCss = !Settings.useQuickCss; - onClose(); + Settings.useQuickCss = !useQuickCss; }} /> . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { definePluginSettings } from "@api/Settings"; import { ImageIcon } from "@components/Icons"; import { Devs } from "@utils/constants"; @@ -80,7 +80,7 @@ function openImage(url: string) { }); } -const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => { +const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => { if (!user) return; const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null; @@ -109,7 +109,7 @@ const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: U )); }; -const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => () => { +const GuildContext: NavContextMenuPatchCallback = (children, { guild }: GuildContextProps) => { if (!guild) return; const { id, icon, banner } = guild; @@ -155,14 +155,9 @@ export default definePlugin({ openImage, - start() { - addContextMenuPatch("user-context", UserContext); - addContextMenuPatch("guild-context", GuildContext); - }, - - stop() { - removeContextMenuPatch("user-context", UserContext); - removeContextMenuPatch("guild-context", GuildContext); + contextMenus: { + "user-context": UserContext, + "guild-context": GuildContext }, patches: [ diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 08acdc4c5..68b33eed0 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import { CodeBlock } from "@components/CodeBlock"; @@ -117,8 +117,8 @@ const settings = definePluginSettings({ } }); -function MakeContextCallback(name: "Guild" | "User" | "Channel") { - const callback: NavContextMenuPatchCallback = (children, props) => () => { +function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback { + return (children, props) => { const value = props[name.toLowerCase()]; if (!value) return; if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings @@ -141,16 +141,19 @@ function MakeContextCallback(name: "Guild" | "User" | "Channel") { /> ); }; - return callback; } - export default definePlugin({ name: "ViewRaw", description: "Copy and view the raw content/data of any message, channel or guild", authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna], dependencies: ["MessagePopoverAPI"], settings, + contextMenus: { + "guild-context": MakeContextCallback("Guild"), + "channel-context": MakeContextCallback("Channel"), + "user-context": MakeContextCallback("User") + }, start() { addButton("ViewRaw", msg => { @@ -187,16 +190,9 @@ export default definePlugin({ onContextMenu: handleContextMenu }; }); - - addContextMenuPatch("guild-context", MakeContextCallback("Guild")); - addContextMenuPatch("channel-context", MakeContextCallback("Channel")); - addContextMenuPatch("user-context", MakeContextCallback("User")); }, stop() { - removeButton("CopyRawMessage"); - removeContextMenuPatch("guild-context", MakeContextCallback("Guild")); - removeContextMenuPatch("channel-context", MakeContextCallback("Channel")); - removeContextMenuPatch("user-context", MakeContextCallback("User")); + removeButton("ViewRaw"); } }); diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index 2393ef2b6..2f232f341 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -18,7 +18,7 @@ import "./styles.css"; -import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { Microphone } from "@components/Icons"; import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; @@ -48,18 +48,30 @@ export type VoiceRecorder = ComponentType<{ const VoiceRecorder = IS_DISCORD_DESKTOP ? VoiceRecorderDesktop : VoiceRecorderWeb; +const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => { + if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return; + + children.push( + + +
Send voice message
+
+ } + action={() => openModal(modalProps => )} + /> + ); +}; + export default definePlugin({ name: "VoiceMessages", description: "Allows you to send voice messages like on mobile. To do so, right click the upload button and click Send Voice Message", authors: [Devs.Ven, Devs.Vap, Devs.Nickyux], settings, - - start() { - addContextMenuPatch("channel-attach", ctxMenuPatch); - }, - - stop() { - removeContextMenuPatch("channel-attach", ctxMenuPatch); + contextMenus: { + "channel-attach": ctxMenuPatch } }); @@ -234,20 +246,3 @@ function Modal({ modalProps }: { modalProps: ModalProps; }) { ); } - -const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (props.channel.guild_id && !(PermissionStore.can(PermissionsBits.SEND_VOICE_MESSAGES, props.channel) && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel))) return; - - children.push( - - -
Send voice message
- - } - action={() => openModal(modalProps => )} - /> - ); -}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d66bdc826..d213ce272 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -418,6 +418,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ Av32000: { name: "Av32000", id: 593436735380127770n, + }, + Kyuuhachi: { + name: "Kyuuhachi", + id: 236588665420251137n, } } satisfies Record); diff --git a/src/utils/types.ts b/src/utils/types.ts index 16867a43c..bec7cb0b3 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -17,6 +17,7 @@ */ import { Command } from "@api/Commands"; +import { NavContextMenuPatchCallback } from "@api/ContextMenu"; import { FluxEvents } from "@webpack/types"; import { Promisable } from "type-fest"; @@ -115,6 +116,10 @@ export interface PluginDef { flux?: { [E in FluxEvents]?: (event: any) => void; }; + /** + * Allows you to manipulate context menus + */ + contextMenus?: Record; /** * Allows you to add custom actions to the Vencord Toolbox. * The key will be used as text for the button From 980206d31568e479f0f0c7a32ab24035374f06d5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 7 Mar 2024 09:36:59 -0300 Subject: [PATCH 08/70] Fix waitFor initial finds traces getting logged to the console even though they always fail --- src/webpack/webpack.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index a68890a83..0f7d8b73c 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -475,8 +475,10 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback else if (typeof filter !== "function") throw new Error("filter must be a string, string[] or function, got " + typeof filter); - const [existing, id] = find(filter!, { isIndirect: true, isWaitFor: true }); - if (existing) return void callback(existing, id); + if (cache != null) { + const [existing, id] = find(filter, { isIndirect: true, isWaitFor: true }); + if (existing) return void callback(existing, id); + } subscriptions.set(filter, callback); } From a59c14f9aa8034fcb3df45af855a7a57c2882b82 Mon Sep 17 00:00:00 2001 From: AutumnVN Date: Thu, 7 Mar 2024 19:51:14 +0700 Subject: [PATCH 09/70] CustomRPC: Change timestamp to milisecond (#2231) --- src/plugins/customRPC/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/customRPC/index.tsx b/src/plugins/customRPC/index.tsx index 3653a0776..e70f8c908 100644 --- a/src/plugins/customRPC/index.tsx +++ b/src/plugins/customRPC/index.tsx @@ -175,7 +175,7 @@ const settings = definePluginSettings({ }, startTime: { type: OptionType.NUMBER, - description: "Start timestamp (only for custom timestamp mode)", + description: "Start timestamp in milisecond (only for custom timestamp mode)", onChange: onChange, disabled: isTimestampDisabled, isValid: (value: number) => { @@ -185,7 +185,7 @@ const settings = definePluginSettings({ }, endTime: { type: OptionType.NUMBER, - description: "End timestamp (only for custom timestamp mode)", + description: "End timestamp in milisecond (only for custom timestamp mode)", onChange: onChange, disabled: isTimestampDisabled, isValid: (value: number) => { @@ -313,12 +313,12 @@ async function createActivity(): Promise { switch (settings.store.timestampMode) { case TimestampMode.NOW: activity.timestamps = { - start: Math.floor(Date.now() / 1000) + start: Date.now() }; break; case TimestampMode.TIME: activity.timestamps = { - start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds() + start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000 }; break; case TimestampMode.CUSTOM: From f70114238c1307e086db537640926a0db78adb10 Mon Sep 17 00:00:00 2001 From: Sam <149597648+cheesesamwich@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:55:51 +0000 Subject: [PATCH 10/70] MemberCount: Add options to choose where the member count will be displayed (#2224) --- src/plugins/memberCount/index.tsx | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index eb4ce372c..92e9a2057 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -18,10 +18,11 @@ import "./style.css"; +import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { findStoreLazy } from "@webpack"; import { FluxStore } from "@webpack/types"; @@ -32,6 +33,21 @@ export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxSto getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; }; }; +const settings = definePluginSettings({ + toolTip: { + type: OptionType.BOOLEAN, + description: "If the member count should be displayed on the server tooltip", + default: true, + restartNeeded: true + }, + memberList: { + type: OptionType.BOOLEAN, + description: "If the member count should be displayed on the member list", + default: true, + restartNeeded: true + } +}); + const sharedIntlNumberFormat = new Intl.NumberFormat(); export const numberFormat = (value: number) => sharedIntlNumberFormat.format(value); export const cl = classNameFactory("vc-membercount-"); @@ -40,6 +56,7 @@ export default definePlugin({ name: "MemberCount", description: "Shows the amount of online & total members in the server member list and tooltip", authors: [Devs.Ven, Devs.Commandtechno], + settings, patches: [ { @@ -47,17 +64,18 @@ export default definePlugin({ replacement: { match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, replace: ":[$1?.startsWith('members')?$self.render():null,$2" - } + }, + predicate: () => settings.store.memberList }, { find: ".invitesDisabledTooltip", replacement: { match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/, replace: ",$self.renderTooltip(arguments[0].guild)]" - } + }, + predicate: () => settings.store.toolTip } ], - render: ErrorBoundary.wrap(MemberCount, { noop: true }), renderTooltip: ErrorBoundary.wrap(guild => , { noop: true }) }); From 19799767adef192a8451adf0f8c1710933b21a41 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:09:04 -0300 Subject: [PATCH 11/70] Fix trying to check for updates if origin doesn't have same branch --- src/main/updater/git.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts index 2ff3ba512..20a5d7003 100644 --- a/src/main/updater/git.ts +++ b/src/main/updater/git.ts @@ -49,9 +49,12 @@ async function getRepo() { async function calculateGitChanges() { await git("fetch"); - const branch = await git("branch", "--show-current"); + const branch = (await git("branch", "--show-current")).stdout.trim(); - const res = await git("log", `HEAD...origin/${branch.stdout.trim()}`, "--pretty=format:%an/%h/%s"); + const existsOnOrigin = (await git("ls-remote", "origin", branch)).stdout.length > 0; + if (!existsOnOrigin) return []; + + const res = await git("log", `HEAD...origin/${branch}`, "--pretty=format:%an/%h/%s"); const commits = res.stdout.trim(); return commits ? commits.split("\n").map(line => { From 102842d5288fd22821e2a6eb295255fd88603964 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 7 Mar 2024 11:33:00 -0300 Subject: [PATCH 12/70] Close Ipc FS watchers if window is closed --- src/main/ipcMain.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 47d400eb6..3ac8a14c5 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -23,7 +23,7 @@ import { debounce } from "@utils/debounce"; import { IpcEvents } from "@utils/IpcEvents"; import { Queue } from "@utils/Queue"; import { BrowserWindow, ipcMain, shell, systemPreferences } from "electron"; -import { mkdirSync, readFileSync, watch } from "fs"; +import { FSWatcher, mkdirSync, readFileSync, watch } from "fs"; import { open, readdir, readFile, writeFile } from "fs/promises"; import { join, normalize } from "path"; @@ -126,16 +126,23 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => { export function initIpc(mainWindow: BrowserWindow) { + let quickCssWatcher: FSWatcher | undefined; + open(QUICKCSS_PATH, "a+").then(fd => { fd.close(); - watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { + quickCssWatcher = watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); - }); + }).catch(() => { }); - watch(THEMES_DIR, { persistent: false }, debounce(() => { + const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => { mainWindow.webContents.postMessage(IpcEvents.THEME_UPDATE, void 0); })); + + mainWindow.once("closed", () => { + quickCssWatcher?.close(); + themesWatcher.close(); + }); } ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { From 1c1d82f9a820ce4b7611032488624d98624a7ecb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:06:08 -0300 Subject: [PATCH 13/70] VencordToolbox: don't subscribe to all settings Also remove one indirection from useSettings --- src/api/Settings.ts | 14 ++++++++------ src/plugins/vencordToolbox/index.tsx | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 004a8988b..c1ff6915b 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -223,13 +223,13 @@ export const Settings = makeProxy(settings); export function useSettings(paths?: UseSettings[]) { const [, forceUpdate] = React.useReducer(() => ({}), {}); - const onUpdate: SubscriptionCallback = paths - ? (value, path) => paths.includes(path as UseSettings) && forceUpdate() - : forceUpdate; + if (paths) { + (forceUpdate as SubscriptionCallback)._paths = paths; + } React.useEffect(() => { - subscriptions.add(onUpdate); - return () => void subscriptions.delete(onUpdate); + subscriptions.add(forceUpdate); + return () => void subscriptions.delete(forceUpdate); }, []); return Settings; @@ -253,8 +253,10 @@ type ResolvePropDeep = P extends "" ? T : export function addSettingsListener(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void; export function addSettingsListener(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep, path: Path extends "" ? string : Path) => void): void; export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) { - if (path) + if (path) { ((onUpdate as SubscriptionCallback)._paths ??= []).push(path); + } + subscriptions.add(onUpdate); } diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index ba295fa72..00805fbd3 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -30,7 +30,7 @@ import type { ReactNode } from "react"; const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider"); function VencordPopout(onClose: () => void) { - const { useQuickCss } = useSettings(); + const { useQuickCss } = useSettings(["useQuickCss"]); const pluginEntries = [] as ReactNode[]; From 2e90d4c03d98b9e5ea1dd8505cb0740d90b051d0 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 7 Mar 2024 19:53:26 +0100 Subject: [PATCH 14/70] New plugin: BetterRoleContext ~ edit/copy colour shortcuts in profile --- src/plugins/betterRoleContext/README.md | 6 ++ src/plugins/betterRoleContext/index.tsx | 79 ++++++++++++++++++++++ src/webpack/common/settingsStores.ts | 7 +- src/webpack/common/types/index.d.ts | 4 +- src/webpack/common/types/settingsStores.ts | 11 +++ 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/plugins/betterRoleContext/README.md create mode 100644 src/plugins/betterRoleContext/index.tsx create mode 100644 src/webpack/common/types/settingsStores.ts diff --git a/src/plugins/betterRoleContext/README.md b/src/plugins/betterRoleContext/README.md new file mode 100644 index 000000000..3f3086bdb --- /dev/null +++ b/src/plugins/betterRoleContext/README.md @@ -0,0 +1,6 @@ +# BetterRoleContext + +Adds options to copy role color and edit role when right clicking roles in the user profile + +![](https://github.com/Vendicated/Vencord/assets/45497981/d1765e9e-7db2-4a3c-b110-139c59235326) + diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx new file mode 100644 index 000000000..7a914293a --- /dev/null +++ b/src/plugins/betterRoleContext/index.tsx @@ -0,0 +1,79 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import { getCurrentGuild } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; + +const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild"); + +function PencilIcon() { + return ( + + + + ); +} + +function AppearanceIcon() { + return ( + + + + ); +} + +export default definePlugin({ + name: "BetterRoleContext", + description: "Adds options to copy role color / edit role when right clicking roles in the user profile", + authors: [Devs.Ven], + + start() { + // DeveloperMode needs to be enabled for the context menu to be shown + TextAndImagesSettingsStores.DeveloperMode.updateSetting(true); + }, + + contextMenus: { + "dev-context"(children, { id }: { id: string; }) { + const guild = getCurrentGuild(); + const role = guild?.roles[id]; + if (!role) return; + + if (role.colorString) { + children.push( + Clipboard.copy(role.colorString!)} + icon={AppearanceIcon} + /> + ); + } + + if (PermissionStore.getGuildPermissionProps(guild).canManageRoles) { + children.push( + { + await GuildSettingsActions.open(guild.id, "ROLES"); + GuildSettingsActions.selectRole(id); + }} + icon={PencilIcon} + /> + ); + } + } + } +}); diff --git a/src/webpack/common/settingsStores.ts b/src/webpack/common/settingsStores.ts index 6db21949a..4a48efda6 100644 --- a/src/webpack/common/settingsStores.ts +++ b/src/webpack/common/settingsStores.ts @@ -6,7 +6,10 @@ import { findByPropsLazy } from "@webpack"; -export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact"); -export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame"); +import * as t from "./types/settingsStores"; + + +export const TextAndImagesSettingsStores = findByPropsLazy("MessageDisplayCompact") as Record; +export const StatusSettingsStores = findByPropsLazy("ShowCurrentGame") as Record; export const UserSettingsActionCreators = findByPropsLazy("PreloadedUserSettingsActionCreators"); diff --git a/src/webpack/common/types/index.d.ts b/src/webpack/common/types/index.d.ts index af4b5e1fb..01c968553 100644 --- a/src/webpack/common/types/index.d.ts +++ b/src/webpack/common/types/index.d.ts @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +export * from "./classes"; export * from "./components"; export * from "./fluxEvents"; +export * from "./i18nMessages"; export * from "./menu"; +export * from "./settingsStores"; export * from "./stores"; export * from "./utils"; - diff --git a/src/webpack/common/types/settingsStores.ts b/src/webpack/common/types/settingsStores.ts new file mode 100644 index 000000000..5453ca352 --- /dev/null +++ b/src/webpack/common/types/settingsStores.ts @@ -0,0 +1,11 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface SettingsStore { + getSetting(): T; + updateSetting(value: T): void; + useSetting(): T; +} From 688ff255d27de8b5bd611c7e678fac69c7f47032 Mon Sep 17 00:00:00 2001 From: Amia <9750071+aamiaa@users.noreply.github.com> Date: Fri, 8 Mar 2024 04:18:18 +0100 Subject: [PATCH 15/70] fix(MutualGroupDMs): update regex (#2242) --- src/plugins/mutualGroupDMs/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 40d5201cb..f5e4b6149 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -47,8 +47,8 @@ export default definePlugin({ { find: ".Messages.USER_PROFILE_MODAL", // Note: the module is lazy-loaded replacement: { - match: /(?<=\.MUTUAL_GUILDS\}\),)(?=(\i\.bot).{0,20}(\(0,\i\.jsx\)\(.{0,100}id:))/, - replace: '($1||arguments[0].isCurrentUser)?null:$2"MUTUAL_GDMS",children:"Mutual Groups"}),' + match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/, + replace: '(arguments[0].user.bot||arguments[0].isCurrentUser)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),' } }, { From 10f33b3dec4f2d197f117f780a6c8283f6f1046d Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 8 Mar 2024 00:23:35 -0300 Subject: [PATCH 16/70] Make more finds use filters.componentByCode --- src/plugins/reviewDB/components/ReviewsView.tsx | 6 +++--- src/webpack/common/components.ts | 2 +- src/webpack/webpack.ts | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index eea92bb81..46bd7fb84 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react"; -import { find, findByPropsLazy } from "@webpack"; +import { useAwaiter, useForceUpdater } from "@utils/react"; +import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common"; import { Auth, authorize } from "../auth"; @@ -31,7 +31,7 @@ import ReviewComponent from "./ReviewComponent"; const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms"); const { ChatInputTypes } = findByPropsLazy("ChatInputTypes"); -const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); +const InputComponent = findComponentByCodeLazy("default.CHANNEL_TEXT_AREA"); const { createChannelRecordFromServer } = findByPropsLazy("createChannelRecordFromServer"); interface UserProps { diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index d7bb5d759..048e65d68 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -51,7 +51,7 @@ export let Avatar: t.Avatar; /** css colour resolver stuff, no clue what exactly this does, just copied usage from Discord */ export let useToken: t.useToken; -export const MaskedLink = waitForComponent("MaskedLink", m => m?.type?.toString().includes("MASKED_LINK)")); +export const MaskedLink = waitForComponent("MaskedLink", filters.componentByCode("MASKED_LINK)")); export const Timestamp = waitForComponent("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const Flex = waitForComponent("Flex", ["Justify", "Align", "Wrap"]); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 0f7d8b73c..992bf38f3 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -60,6 +60,7 @@ export const filters = { return m => { if (filter(m)) return true; if (!m.$$typeof) return false; + if (m.type && m.type.render) return filter(m.type.render); // memo + forwardRef if (m.type) return filter(m.type); // memos if (m.render) return filter(m.render); // forwardRefs return false; From cf7830e747a5f2ff0b1bace93a9fab13f6936477 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 8 Mar 2024 00:24:04 -0300 Subject: [PATCH 17/70] ShowHiddenChannels: Fix patches --- src/plugins/showHiddenChannels/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 919f3f3c5..2d091c24a 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -305,27 +305,27 @@ export default definePlugin({ ] }, { - find: ".avatars),children", + find: '+1]})},"overflow"))', replacement: [ { // Create a variable for the channel prop - match: /maxUsers:\i,users:\i.+?=(\i).+?;/, + match: /maxUsers:\i,users:\i.+?}=(\i).*?;/, replace: (m, props) => `${m}let{shcChannel}=${props};` }, { // Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen match: /\i>0(?=&&.{0,60}renderPopout)/, - replace: m => `($self.isHiddenChannel(shcChannel,true)?true:${m})` + replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})` }, { // Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/, - replace: (_, amount) => `($self.isHiddenChannel(shcChannel,true)&&${amount}<=0?0:1)` + replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?0:1)` }, { // Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen match: /(?<="\+",)(\i)\+1/, - replace: (m, amount) => `$self.isHiddenChannel(shcChannel,true)&&${amount}<=0?"":${m}` + replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${m}` } ] }, From b0d37c981e6bbb47b4b25f28d3e87488f9038bac Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 8 Mar 2024 00:29:06 -0300 Subject: [PATCH 18/70] Bump to 1.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dde55d311..dbf8aaf34 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.7.0", + "version": "1.7.1", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 992533245bc08ccaac258aaca803d362942dde74 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 8 Mar 2024 19:05:30 -0300 Subject: [PATCH 19/70] RoleColorEverywhere: Wrap roleGroupColor in ErrorBoundary --- src/plugins/roleColorEverywhere/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index 968027163..14d38e8ee 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -17,6 +17,7 @@ */ import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common"; @@ -112,7 +113,7 @@ export default definePlugin({ return colorString && parseInt(colorString.slice(1), 16); }, - roleGroupColor({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) { + roleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => { const guild = GuildStore.getGuild(guildId); const role = guild?.roles[id]; @@ -125,7 +126,7 @@ export default definePlugin({ {title ?? label} — {count} ); - }, + }, { noop: true }), getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) { return { From 497f0de9a1dc7f228a67e73b25f12c0d6fa93240 Mon Sep 17 00:00:00 2001 From: Jack <30497388+FieryFlames@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:52:11 -0400 Subject: [PATCH 20/70] chore(Decor): Change URL formula for cost savings (#2247) --- src/plugins/decor/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/decor/index.tsx b/src/plugins/decor/index.tsx index 8cfd8c036..713b17d76 100644 --- a/src/plugins/decor/index.tsx +++ b/src/plugins/decor/index.tsx @@ -131,9 +131,10 @@ export default definePlugin({ getDecorAvatarDecorationURL({ avatarDecoration, canAnimate }: { avatarDecoration: AvatarDecoration | null; canAnimate?: boolean; }) { // Only Decor avatar decorations have this SKU ID if (avatarDecoration?.skuId === SKU_ID) { - const url = new URL(`${CDN_URL}/${avatarDecoration.asset}.png`); - url.searchParams.set("animate", (!!canAnimate && isAnimatedAvatarDecoration(avatarDecoration.asset)).toString()); - return url.toString(); + const parts = avatarDecoration.asset.split("_"); + // Remove a_ prefix if it's animated and animation is disabled + if (isAnimatedAvatarDecoration(avatarDecoration.asset) && !canAnimate) parts.shift(); + return `${CDN_URL}/${parts.join("_")}.png`; } else if (avatarDecoration?.skuId === RAW_SKU_ID) { return avatarDecoration.asset; } From 34390e03652f523fd92e3974feb93395bb825bec Mon Sep 17 00:00:00 2001 From: Vendicated Date: Mon, 11 Mar 2024 16:13:07 +0100 Subject: [PATCH 21/70] Add workaround for guild role api changes on canary/ptb fixes RoleColorEverywhere, ServerInfo, PermissionViewer, BetterRoleContext --- src/plugins/betterRoleContext/index.tsx | 6 ++++-- .../components/RolesAndUsersPermissions.tsx | 8 +++++--- src/plugins/permissionsViewer/index.tsx | 3 ++- src/plugins/permissionsViewer/utils.ts | 13 ++++++++----- src/plugins/roleColorEverywhere/index.tsx | 6 +++--- src/plugins/serverProfile/GuildProfileModal.tsx | 4 ++-- src/plugins/xsOverlay.desktop/index.ts | 3 ++- src/utils/discord.tsx | 10 +++++++++- 8 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/plugins/betterRoleContext/index.tsx b/src/plugins/betterRoleContext/index.tsx index 7a914293a..e73779adf 100644 --- a/src/plugins/betterRoleContext/index.tsx +++ b/src/plugins/betterRoleContext/index.tsx @@ -5,7 +5,7 @@ */ import { Devs } from "@utils/constants"; -import { getCurrentGuild } from "@utils/discord"; +import { getCurrentGuild, getGuildRoles } from "@utils/discord"; import definePlugin from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { Clipboard, Menu, PermissionStore, TextAndImagesSettingsStores } from "@webpack/common"; @@ -47,7 +47,9 @@ export default definePlugin({ contextMenus: { "dev-context"(children, { id }: { id: string; }) { const guild = getCurrentGuild(); - const role = guild?.roles[id]; + if (!guild) return; + + const role = getGuildRoles(guild.id)[id]; if (!role) return; if (role.colorString) { diff --git a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx index e0d25c7ab..adadc90b5 100644 --- a/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx +++ b/src/plugins/permissionsViewer/components/RolesAndUsersPermissions.tsx @@ -19,7 +19,7 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; -import { getUniqueUsername } from "@utils/discord"; +import { getGuildRoles, getUniqueUsername } from "@utils/discord"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ContextMenuApi, FluxDispatcher, GuildMemberStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; import type { Guild } from "discord-types/general"; @@ -78,6 +78,8 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea const [selectedItemIndex, selectItem] = useState(0); const selectedItem = permissions[selectedItemIndex]; + const roles = getGuildRoles(guild.id); + return ( {permissions.map((permission, index) => { const user = UserStore.getUser(permission.id ?? ""); - const role = guild.roles[permission.id ?? ""]; + const role = roles[permission.id ?? ""]; return ( + + + + ); +} + +export const openCategoryModal = (categoryId: string | null, channelId: string | null) => + openModalLazy(async () => { + await requireSettingsMenu(); + return modalProps => ; + }); + diff --git a/src/plugins/pinDms/components/contextMenu.tsx b/src/plugins/pinDms/components/contextMenu.tsx new file mode 100644 index 000000000..0f5a198ba --- /dev/null +++ b/src/plugins/pinDms/components/contextMenu.tsx @@ -0,0 +1,96 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { Menu } from "@webpack/common"; + +import { addChannelToCategory, canMoveChannelInDirection, categories, isPinned, moveChannel, removeChannelFromCategory } from "../data"; +import { forceUpdate, settings } from "../index"; +import { openCategoryModal } from "./CreateCategoryModal"; + +function createPinMenuItem(channelId: string) { + const pinned = isPinned(channelId); + + return ( + + + {!pinned && ( + <> + openCategoryModal(null, channelId)} + /> + + + { + categories.map(category => ( + addChannelToCategory(channelId, category.id).then(forceUpdate)} + /> + )) + } + + )} + + {pinned && ( + <> + removeChannelFromCategory(channelId).then(forceUpdate)} + /> + + { + !settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, -1) && ( + moveChannel(channelId, -1).then(forceUpdate)} + /> + ) + } + + { + !settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, 1) && ( + moveChannel(channelId, 1).then(forceUpdate)} + /> + ) + } + + )} + + + ); +} + +const GroupDMContext: NavContextMenuPatchCallback = (children, props) => { + const container = findGroupChildrenByChildId("leave-channel", children); + container?.unshift(createPinMenuItem(props.channel.id)); +}; + +const UserContext: NavContextMenuPatchCallback = (children, props) => { + const container = findGroupChildrenByChildId("close-dm", children); + if (container) { + const idx = container.findIndex(c => c?.props?.id === "close-dm"); + container.splice(idx, 0, createPinMenuItem(props.channel.id)); + } +}; + +export const contextMenus = { + "gdm-context": GroupDMContext, + "user-context": UserContext +}; diff --git a/src/plugins/pinDms/constants.ts b/src/plugins/pinDms/constants.ts new file mode 100644 index 000000000..fbd995e14 --- /dev/null +++ b/src/plugins/pinDms/constants.ts @@ -0,0 +1,32 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export const DEFAULT_CHUNK_SIZE = 256; +export const DEFAULT_COLOR = 10070709; + +export const SWATCHES = [ + 1752220, + 3066993, + 3447003, + 10181046, + 15277667, + 15844367, + 15105570, + 15158332, + 9807270, + 6323595, + + 1146986, + 2067276, + 2123412, + 7419530, + 11342935, + 12745742, + 11027200, + 10038562, + 9936031, + 5533306 +]; diff --git a/src/plugins/pinDms/contextMenus.tsx b/src/plugins/pinDms/contextMenus.tsx deleted file mode 100644 index 1db8b25a9..000000000 --- a/src/plugins/pinDms/contextMenus.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { Menu } from "@webpack/common"; - -import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings"; - -function PinMenuItem(channelId: string) { - const pinned = isPinned(channelId); - const canMove = pinned && settings.store.pinOrder === PinOrder.Custom; - - return ( - <> - togglePin(channelId)} - /> - {canMove && snapshotArray[0] !== channelId && ( - movePin(channelId, -1)} - /> - )} - {canMove && snapshotArray[snapshotArray.length - 1] !== channelId && ( - movePin(channelId, +1)} - /> - )} - - ); -} - -const GroupDMContext: NavContextMenuPatchCallback = (children, props) => { - const container = findGroupChildrenByChildId("leave-channel", children); - if (container) - container.unshift(PinMenuItem(props.channel.id)); -}; - -const UserContext: NavContextMenuPatchCallback = (children, props) => { - const container = findGroupChildrenByChildId("close-dm", children); - if (container) { - const idx = container.findIndex(c => c?.props?.id === "close-dm"); - container.splice(idx, 0, PinMenuItem(props.channel.id)); - } -}; - -export const contextMenus = { - "gdm-context": GroupDMContext, - "user-context": UserContext -}; diff --git a/src/plugins/pinDms/data.ts b/src/plugins/pinDms/data.ts new file mode 100644 index 000000000..afb6f9795 --- /dev/null +++ b/src/plugins/pinDms/data.ts @@ -0,0 +1,214 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import * as DataStore from "@api/DataStore"; +import { Settings } from "@api/Settings"; +import { UserStore } from "@webpack/common"; + +import { DEFAULT_COLOR } from "./constants"; +import { forceUpdate } from "./index"; + +export interface Category { + id: string; + name: string; + color: number; + channels: string[]; + collapsed?: boolean; +} + +const CATEGORY_BASE_KEY = "PinDMsCategories-"; +const CATEGORY_MIGRATED_PINDMS_KEY = "PinDMsMigratedPinDMs"; +const CATEGORY_MIGRATED_KEY = "PinDMsMigratedOldCategories"; +const OLD_CATEGORY_KEY = "BetterPinDMsCategories-"; + + +export let categories: Category[] = []; + +export async function saveCats(cats: Category[]) { + const { id } = UserStore.getCurrentUser(); + await DataStore.set(CATEGORY_BASE_KEY + id, cats); +} + +export async function init() { + const id = UserStore.getCurrentUser()?.id; + await initCategories(id); + await migrateData(id); + forceUpdate(); +} + +export async function initCategories(userId: string) { + categories = await DataStore.get(CATEGORY_BASE_KEY + userId) ?? []; +} + +export function getCategory(id: string) { + return categories.find(c => c.id === id); +} + +export async function createCategory(category: Category) { + categories.push(category); + await saveCats(categories); +} + +export async function updateCategory(category: Category) { + const index = categories.findIndex(c => c.id === category.id); + if (index === -1) return; + + categories[index] = category; + await saveCats(categories); +} + +export async function addChannelToCategory(channelId: string, categoryId: string) { + const category = categories.find(c => c.id === categoryId); + if (!category) return; + + if (category.channels.includes(channelId)) return; + + category.channels.push(channelId); + await saveCats(categories); + +} + +export async function removeChannelFromCategory(channelId: string) { + const category = categories.find(c => c.channels.includes(channelId)); + if (!category) return; + + category.channels = category.channels.filter(c => c !== channelId); + await saveCats(categories); +} + +export async function removeCategory(categoryId: string) { + const catagory = categories.find(c => c.id === categoryId); + if (!catagory) return; + + // catagory?.channels.forEach(c => removeChannelFromCategory(c)); + categories = categories.filter(c => c.id !== categoryId); + await saveCats(categories); +} + +export async function collapseCategory(id: string, value = true) { + const category = categories.find(c => c.id === id); + if (!category) return; + + category.collapsed = value; + await saveCats(categories); +} + +// utils +export function isPinned(id: string) { + return categories.some(c => c.channels.includes(id)); +} + +export function categoryLen() { + return categories.length; +} + +export function getAllUncollapsedChannels() { + return categories.filter(c => !c.collapsed).map(c => c.channels).flat(); +} + +export function getSections() { + return categories.reduce((acc, category) => { + acc.push(category.channels.length === 0 ? 1 : category.channels.length); + return acc; + }, [] as number[]); +} + +// move categories +export const canMoveArrayInDirection = (array: any[], index: number, direction: -1 | 1) => { + const a = array[index]; + const b = array[index + direction]; + + return a && b; +}; + +export const canMoveCategoryInDirection = (id: string, direction: -1 | 1) => { + const index = categories.findIndex(m => m.id === id); + return canMoveArrayInDirection(categories, index, direction); +}; + +export const canMoveCategory = (id: string) => canMoveCategoryInDirection(id, -1) || canMoveCategoryInDirection(id, 1); + +export const canMoveChannelInDirection = (channelId: string, direction: -1 | 1) => { + const category = categories.find(c => c.channels.includes(channelId)); + if (!category) return false; + + const index = category.channels.indexOf(channelId); + return canMoveArrayInDirection(category.channels, index, direction); +}; + + +function swapElementsInArray(array: any[], index1: number, index2: number) { + if (!array[index1] || !array[index2]) return; + [array[index1], array[index2]] = [array[index2], array[index1]]; +} + +// stolen from PinDMs +export async function moveCategory(id: string, direction: -1 | 1) { + const a = categories.findIndex(m => m.id === id); + const b = a + direction; + + swapElementsInArray(categories, a, b); + + await saveCats(categories); +} + +export async function moveChannel(channelId: string, direction: -1 | 1) { + const category = categories.find(c => c.channels.includes(channelId)); + if (!category) return; + + const a = category.channels.indexOf(channelId); + const b = a + direction; + + swapElementsInArray(category.channels, a, b); + + await saveCats(categories); +} + + + +// migrate data +const getPinDMsPins = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined; + +async function migratePinDMs() { + if (categories.some(m => m.id === "oldPins")) { + return await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true); + } + + const pindmspins = getPinDMsPins(); + + // we dont want duplicate pins + const difference = [...new Set(pindmspins)]?.filter(m => !categories.some(c => c.channels.includes(m))); + if (difference?.length) { + categories.push({ + id: "oldPins", + name: "Pins", + color: DEFAULT_COLOR, + channels: difference + }); + } + + await DataStore.set(CATEGORY_MIGRATED_PINDMS_KEY, true); +} + +async function migrateOldCategories(userId: string) { + const oldCats = await DataStore.get(OLD_CATEGORY_KEY + userId); + // dont want to migrate if the user has already has categories. + if (categories.length === 0 && oldCats?.length) { + categories.push(...(oldCats.filter(m => m.id !== "oldPins"))); + } + await DataStore.set(CATEGORY_MIGRATED_KEY, true); +} + +export async function migrateData(userId: string) { + const m1 = await DataStore.get(CATEGORY_MIGRATED_KEY), m2 = await DataStore.get(CATEGORY_MIGRATED_PINDMS_KEY); + 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(); + + await saveCats(categories); +} diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 45172328e..ee726bfab 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -1,116 +1,131 @@ /* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +import "./styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import { classes } from "@utils/misc"; +import definePlugin, { OptionType, StartAt } from "@utils/types"; +import { findByPropsLazy, findStoreLazy } from "@webpack"; +import { ContextMenuApi, FluxDispatcher, Menu, React } from "@webpack/common"; import { Channel } from "discord-types/general"; -import { contextMenus } from "./contextMenus"; -import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings"; +import { contextMenus } from "./components/contextMenu"; +import { openCategoryModal, requireSettingsMenu } from "./components/CreateCategoryModal"; +import { DEFAULT_CHUNK_SIZE } from "./constants"; +import { canMoveCategory, canMoveCategoryInDirection, categories, Category, categoryLen, collapseCategory, getAllUncollapsedChannels, getSections, init, isPinned, moveCategory, removeCategory } from "./data"; + +interface ChannelComponentProps { + children: React.ReactNode, + channel: Channel, + selected: boolean; +} + + +const headerClasses = findByPropsLazy("privateChannelsHeaderContainer"); + +const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; }; + +export let instance: any; +export const forceUpdate = () => instance?.props?._forceUpdate?.(); + +export const settings = definePluginSettings({ + sortDmsByNewestMessage: { + type: OptionType.BOOLEAN, + description: "Sort DMs by newest message", + default: false, + onChange: () => forceUpdate() + }, + + dmSectioncollapsed: { + type: OptionType.BOOLEAN, + description: "Collapse DM sections", + default: false, + onChange: () => forceUpdate() + } +}); export default definePlugin({ name: "PinDMs", description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs", - authors: [Devs.Ven], - + authors: [Devs.Ven, Devs.Aria], settings, contextMenus, - usePinCount(channelIds: string[]) { - const pinnedDms = usePinnedDms(); - // See comment on 2nd patch for reasoning - return channelIds.length ? [pinnedDms.size] : []; - }, - - getChannel(channels: Record, idx: number) { - return channels[getPinAt(idx)]; - }, - - isPinned, - getSnapshot: sortedSnapshot, - - getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) { - if (!isPinned(channelId)) - return ( - (rowHeight + padding) * 2 // header - + rowHeight * snapshotArray.length // pins - + originalOffset // original pin offset minus pins - ); - - return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding; - }, - patches: [ - // Patch DM list { find: ".privateChannelsHeaderContainer,", replacement: [ + // Init { - // filter Discord's privateChannelIds list to remove pins, and pass - // pinCount as prop. This needs to be here so that the entire DM list receives - // updates on pin/unpin - match: /(?<=\i,{channels:\i,)privateChannelIds:(\i),/, - replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1)," + match: /(?<=componentDidMount\(\){).{1,100}scrollToChannel/, + replace: "$self._instance = this;$&" }, { - // sections is an array of numbers, where each element is a section and - // the number is the amount of rows. Add our pinCount in second place - // - Section 1: buttons for pages like Friends & Library - // - Section 2: our pinned dms - // - Section 3: the normal dm list - match: /(?<=renderRow:this\.renderRow,)sections:\[\i,/, - // For some reason, adding our sections when no private channels are ready yet - // makes DMs infinitely load. Thus usePinCount returns either a single element - // array with the count, or an empty array. Due to spreading, only in the former - // case will an element be added to the outer array - // Thanks for the fix, Strencher! - replace: "$&...this.props.pinCount??[]," + // Filter out pinned channels from the private channel list + match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/, + replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))" }, { - // Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages" - // lookbehind is used to lookup parameter name. We could use arguments[0], but - // if children ever is wrapped in an iife, it will break - match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=(\i)=>{.+?)/, - replace: "children:$2.section===1?'Pinned DMs':$1" + // Insert the pinned channels to sections + match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/, + replace: "...$self.makeProps(this,{$&})" + }, + + // Rendering + { + match: /this\.renderDM=\(.+?(\i\.default),{channel.+?this.renderRow=(\i)=>{/, + replace: "$&if($self.isChannelIndex($2.section, $2.row))return $self.renderChannel($2.section,$2.row,$1);" }, { - // Patch channel lookup inside renderDM - // channel=channels[channelIds[row]]; - match: /(?<=renderDM=\((\i),(\i)\)=>{.*?this.state,\i=\i\[\i\],\i=)((\i)\[\i\]);/, - // section 1 is us, manually get our own channel - // section === 1 ? getChannel(channels, row) : channels[channelIds[row]]; - replace: "$1===1?$self.getChannel($4,$2):$3;" + match: /this\.renderSection=(\i)=>{/, + replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);" }, { - // Fix getRowHeight's check for whether this is the DMs section - // DMS (inlined) === section - match: /(?<=getRowHeight=\(.{2,50}?)1===\i/, - // DMS (inlined) === section - 1 - replace: "$&-1" + match: /(?<=span",{)className:\i\.headerText,/, + replace: "...$self.makeSpanProps(),$&" }, + + // Fix Row Height + { + match: /(?<=this\.getRowHeight=.{1,100}return 1===)\i/, + replace: "($&-$self.categoryLen())" + }, + { + match: /this.getRowHeight=\((\i),(\i)\)=>{/, + replace: "$&if($self.isChannelHidden($1,$2))return 0;" + }, + + // Fix ScrollTo { // Override scrollToChannel to properly account for pinned channels match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/, replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)" - } + }, + { + match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/, + replace: "[...$&,...$self.getAllUncollapsedChannels()]" + }, + ] }, + + // forceUpdate moment + // https://regex101.com/r/kDN9fO/1 + { + find: ".FRIENDS},\"friends\"", + replacement: { + match: /(?<=\i=\i=>{).{1,100}premiumTabSelected.{1,800}showDMHeader:.+?,/, + replace: "let forceUpdate = Vencord.Util.useForceUpdater();$&_forceUpdate:forceUpdate," + } + }, + // Fix Alt Up/Down navigation { find: ".Routes.APPLICATION_STORE&&", @@ -118,16 +133,227 @@ export default definePlugin({ // channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)] match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/, // ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c))) - replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))" + replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))" } }, + // fix alt+shift+up/down { find: ".getFlattenedGuildIds()],", replacement: { match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/, - replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))" + replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))" } }, - ] + ], + sections: null as number[] | null, + + set _instance(i: any) { + this.instance = i; + instance = i; + }, + + startAt: StartAt.WebpackReady, + start: init, + flux: { + CONNECTION_OPEN: init, + }, + + isPinned, + categoryLen, + getSections, + getAllUncollapsedChannels, + requireSettingsMenu, + makeProps(instance, { sections }: { sections: number[]; }) { + this.sections = sections; + + this.sections.splice(1, 0, ...this.getPinCount(instance.props.privateChannelIds || [])); + + if (this.instance?.props?.privateChannelIds?.length === 0) { + this.sections[this.sections.length - 1] = 0; + } + + return { + sections: this.sections, + chunkSize: this.getChunkSize(), + }; + }, + + makeSpanProps() { + return { + onClick: () => this.collapseDMList(), + role: "button", + style: { cursor: "pointer" } + }; + }, + + getChunkSize() { + // the chunk size is the amount of rows (measured in pixels) that are rendered at once (probably) + // the higher the chunk size, the more rows are rendered at once + // also if the chunk size is 0 it will render everything at once + + const sections = this.getSections(); + const sectionHeaderSizePx = sections.length * 40; + // (header heights + DM heights + DEFAULT_CHUNK_SIZE) * 1.5 + // we multiply everything by 1.5 so it only gets unmounted after the entire list is off screen + return (sectionHeaderSizePx + sections.reduce((acc, v) => acc += v + 44, 0) + DEFAULT_CHUNK_SIZE) * 1.5; + }, + + getPinCount(channelIds: string[]) { + return channelIds.length ? this.getSections() : []; + }, + + isCategoryIndex(sectionIndex: number) { + return this.sections && sectionIndex > 0 && sectionIndex < this.sections.length - 1; + }, + + isChannelIndex(sectionIndex: number, channelIndex: number) { + if (settings.store.dmSectioncollapsed && sectionIndex !== 0) + return true; + const cat = categories[sectionIndex - 1]; + return this.isCategoryIndex(sectionIndex) && (cat.channels.length === 0 || cat?.channels[channelIndex]); + }, + + isDMSectioncollapsed() { + return settings.store.dmSectioncollapsed; + }, + + collapseDMList() { + // console.log("HI"); + settings.store.dmSectioncollapsed = !settings.store.dmSectioncollapsed; + forceUpdate(); + }, + + isChannelHidden(categoryIndex: number, channelIndex: number) { + if (categoryIndex === 0) return false; + + if (settings.store.dmSectioncollapsed && this.getSections().length + 1 === categoryIndex) + return true; + + if (!this.instance || !this.isChannelIndex(categoryIndex, channelIndex)) return false; + + const category = categories[categoryIndex - 1]; + if (!category) return false; + + return category.collapsed && this.instance.props.selectedChannelId !== category.channels[channelIndex]; + }, + + getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) { + if (!isPinned(channelId)) + return ( + (rowHeight + padding) * 2 // header + + rowHeight * this.getAllUncollapsedChannels().length // pins + + originalOffset // original pin offset minus pins + ); + + return rowHeight * (this.getAllUncollapsedChannels().indexOf(channelId) + preRenderedChildren) + padding; + }, + + renderCategory: ErrorBoundary.wrap(({ section }: { section: number; }) => { + const category = categories[section - 1]; + + if (!category) return null; + + return ( +

{ + await collapseCategory(category.id, !category.collapsed); + forceUpdate(); + }} + onContextMenu={e => { + ContextMenuApi.openContextMenu(e, () => ( + FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} + color="danger" + aria-label="Pin DMs Category Menu" + > + openCategoryModal(category.id, null)} + /> + + { + canMoveCategory(category.id) && ( + <> + { + canMoveCategoryInDirection(category.id, -1) && moveCategory(category.id, -1).then(() => forceUpdate())} + /> + } + { + canMoveCategoryInDirection(category.id, 1) && moveCategory(category.id, 1).then(() => forceUpdate())} + /> + } + + + ) + } + + + removeCategory(category.id).then(() => forceUpdate())} + /> + + + + )); + }} + > + + {category?.name ?? "uh oh"} + + +

+ ); + }), + + renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType) { + const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels); + + if (!channel || !category) return null; + if (this.isChannelHidden(sectionIndex, index)) return null; + + return ( + + {channel.id} + + ); + }, + + + getChannel(sectionIndex: number, index: number, channels: Record) { + const category = categories[sectionIndex - 1]; + if (!category) return { channel: null, category: null }; + + const channelId = this.getCategoryChannels(category)[index]; + + return { channel: channels[channelId], category }; + }, + + getCategoryChannels(category: Category) { + if (category.channels.length === 0) return []; + + if (settings.store.sortDmsByNewestMessage) { + return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category.channels.includes(c)); + } + + return category?.channels ?? []; + } }); diff --git a/src/plugins/pinDms/settings.ts b/src/plugins/pinDms/settings.ts deleted file mode 100644 index 1e0244ed7..000000000 --- a/src/plugins/pinDms/settings.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { definePluginSettings, Settings, useSettings } from "@api/Settings"; -import { OptionType } from "@utils/types"; -import { findStoreLazy } from "@webpack"; - -export const enum PinOrder { - LastMessage, - Custom -} - -export const settings = definePluginSettings({ - pinOrder: { - type: OptionType.SELECT, - description: "Which order should pinned DMs be displayed in?", - options: [ - { label: "Most recent message", value: PinOrder.LastMessage, default: true }, - { label: "Custom (right click channels to reorder)", value: PinOrder.Custom } - ] - } -}); - -const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore"); - -export let snapshotArray: string[]; -let snapshot: Set | undefined; - -const getArray = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined; -const save = (pins: string[]) => { - snapshot = void 0; - Settings.plugins.PinDMs.pinnedDMs = pins.join(","); -}; -const takeSnapshot = () => { - snapshotArray = getArray() ?? []; - return snapshot = new Set(snapshotArray); -}; -const requireSnapshot = () => snapshot ?? takeSnapshot(); - -export function usePinnedDms() { - useSettings(["plugins.PinDMs.pinnedDMs"]); - - return requireSnapshot(); -} - -export function isPinned(id: string) { - return requireSnapshot().has(id); -} - -export function togglePin(id: string) { - const snapshot = requireSnapshot(); - if (!snapshot.delete(id)) { - snapshot.add(id); - } - - save([...snapshot]); -} - -export function sortedSnapshot() { - requireSnapshot(); - if (settings.store.pinOrder === PinOrder.LastMessage) - return PrivateChannelSortStore.getPrivateChannelIds().filter(isPinned); - - return snapshotArray; -} - -export function getPinAt(idx: number) { - return sortedSnapshot()[idx]; -} - -export function movePin(id: string, direction: -1 | 1) { - const pins = getArray()!; - const a = pins.indexOf(id); - const b = a + direction; - - [pins[a], pins[b]] = [pins[b], pins[a]]; - - save(pins); -} diff --git a/src/plugins/pinDms/styles.css b/src/plugins/pinDms/styles.css new file mode 100644 index 000000000..173f0f76b --- /dev/null +++ b/src/plugins/pinDms/styles.css @@ -0,0 +1,37 @@ +.vc-pindms-section-container { + box-sizing: border-box; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-transform: uppercase; + font-size: 12px; + line-height: 16px; + letter-spacing: .02em; + font-family: var(--font-display); + font-weight: 600; + flex: 1 1 auto; + color: var(--channels-default); + cursor: pointer; +} + +.vc-pindms-modal-content { + display: grid; + justify-content: center; + padding: 1rem; + gap: 1.5rem; +} + +.vc-pindms-modal-content [class^="defaultContainer"] { + display: none; +} + +.vc-pindms-collapse-icon { + width: 16px; + height: 16px; + color: var(--interactive-normal); + transform: rotate(90deg) +} + +.vc-pindms-collapsed .vc-pindms-collapse-icon { + transform: rotate(0deg); +} From 5646fe402ac0da45e9a6a9f25d2594cb721527cf Mon Sep 17 00:00:00 2001 From: MrDiamondDog <84212701+MrDiamondDog@users.noreply.github.com> Date: Thu, 21 Mar 2024 17:59:25 -0600 Subject: [PATCH 43/70] feat(PatchHelper): Paste Full Patch (#1982) --- src/components/CheckedTextInput.tsx | 6 +- .../VencordSettings/PatchHelperTab.tsx | 65 ++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/components/CheckedTextInput.tsx b/src/components/CheckedTextInput.tsx index cf4aa119b..de2d7ae04 100644 --- a/src/components/CheckedTextInput.tsx +++ b/src/components/CheckedTextInput.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { React, TextInput } from "@webpack/common"; +import { React, TextInput, useEffect } from "@webpack/common"; // TODO: Refactor settings to use this as well interface TextInputProps { @@ -55,6 +55,10 @@ export function CheckedTextInput({ value: initialValue, onChange, validate }: Te } } + useEffect(() => { + handleChange(initialValue); + }, [initialValue]); + return ( <> (""); + const [fullPatchError, setFullPatchError] = React.useState(""); + + function update() { + if (fullPatch === "") { + setFullPatchError(""); + + setFind(""); + setMatch(""); + setReplacement(""); + return; + } + + try { + const parsed = (0, eval)(`(${fullPatch})`) as Patch; + + if (!parsed.find) throw new Error("No 'find' field"); + if (!parsed.replacement) throw new Error("No 'replacement' field"); + + if (parsed.replacement instanceof Array) { + if (parsed.replacement.length === 0) throw new Error("Invalid replacement"); + + parsed.replacement = { + match: parsed.replacement[0].match, + replace: parsed.replacement[0].replace + }; + } + + if (!parsed.replacement.match) throw new Error("No 'replacement.match' field"); + if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field"); + + setFind(parsed.find); + setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match); + setReplacement(parsed.replacement.replace); + setFullPatchError(""); + } catch (e) { + setFullPatchError((e as Error).message); + } + } + + return <> + Paste your full JSON patch here to fill out the fields +