From 1c5236162a61d31358858ec5a91a16692cbe0695 Mon Sep 17 00:00:00 2001 From: Benjamin <7@toru.ca> Date: Fri, 24 Apr 2026 06:46:53 -0400 Subject: [PATCH] feat(plugin): DMsAsServers --- src/plugins/dmsAsServers/README.md | 5 ++ src/plugins/dmsAsServers/index.tsx | 111 +++++++++++++++++++++++++++++ src/utils/constants.ts | 4 ++ 3 files changed, 120 insertions(+) create mode 100644 src/plugins/dmsAsServers/README.md create mode 100644 src/plugins/dmsAsServers/index.tsx diff --git a/src/plugins/dmsAsServers/README.md b/src/plugins/dmsAsServers/README.md new file mode 100644 index 0000000000..d7d6b5e0bf --- /dev/null +++ b/src/plugins/dmsAsServers/README.md @@ -0,0 +1,5 @@ +# DMsAsServers + +Promote DMs as permanent icons in your server list as if they're servers. + +![](https://bin.t7ru.link/fol/dms.gif) diff --git a/src/plugins/dmsAsServers/index.tsx b/src/plugins/dmsAsServers/index.tsx new file mode 100644 index 0000000000..c85203b4c8 --- /dev/null +++ b/src/plugins/dmsAsServers/index.tsx @@ -0,0 +1,111 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { + findGroupChildrenByChildId, + NavContextMenuPatchCallback, +} from "@api/ContextMenu"; +import * as DataStore from "@api/DataStore"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Channel } from "@vencord/discord-types"; +import { ChannelType } from "@vencord/discord-types/enums"; +import { Menu } from "@webpack/common"; + +const STORE_KEY = "DMsAsServers_promotedDmChannelIds"; +let promotedDmChannelIds: string[] = []; + +const PromotedDmsStore = { + _listeners: new Set<() => void>(), + addReactChangeListener(fn: () => void) { + this._listeners.add(fn); + }, + removeReactChangeListener(fn: () => void) { + this._listeners.delete(fn); + }, + emitChange() { + this._listeners.forEach((fn) => fn()); + }, +}; + +function getPromotedDmChannelIds(): string[] { + return promotedDmChannelIds; +} + +function setPromotedDmChannelIds(ids: string[]) { + promotedDmChannelIds = ids; + void DataStore.set(STORE_KEY, ids); + PromotedDmsStore.emitChange(); +} + +function togglePromoted(channelId: string) { + const promoted = getPromotedDmChannelIds(); + setPromotedDmChannelIds( + promoted.includes(channelId) + ? promoted.filter((id) => id !== channelId) + : [...promoted, channelId], + ); +} + +const userContextPatch: NavContextMenuPatchCallback = ( + children, + { channel }: { channel?: Channel }, +) => { + if (!channel || channel.type !== ChannelType.DM) return; + + const group = findGroupChildrenByChildId("close-dm", children); + if (!group) return; + + const isPromoted = getPromotedDmChannelIds().includes(channel.id); + const closeDmIndex = group.findIndex((c) => c?.props?.id === "close-dm"); + group.splice( + closeDmIndex >= 0 ? closeDmIndex : group.length, + 0, + togglePromoted(channel.id)} + />, + ); +}; + +export default definePlugin({ + name: "DMsAsServers", + description: + "Promote DMs as permanent icons in your server list as if they're servers.", + authors: [Devs.t7ru], + tags: ["Friends", "Organisation"], + + patches: [ + { + // force promoted dms into guild-list-unread-dms to always shown regardless of read state + // the whole thing is bit of a hack but i think it's pretty clever and 'stable' + find: '"guild-list-unread-dms"', + replacement: { + match: /\(0,(\i\.\i)\)\(\[(\i\.\i)\],\(\)=>\2\.getUnreadPrivateChannelIds\(\)\)/, + replace: + "(0,$1)([$self.store,$2],()=>[...$self.getPromotedIds(),...$2.getUnreadPrivateChannelIds()].filter((v,i,a)=>a.indexOf(v)===i))", + }, + }, + ], + + store: PromotedDmsStore, + getPromotedIds: getPromotedDmChannelIds, + + contextMenus: { + "user-context": userContextPatch, + }, + + async start() { + const stored = await DataStore.get(STORE_KEY); + promotedDmChannelIds = Array.isArray(stored) ? stored : []; + PromotedDmsStore.emitChange(); + }, +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c3b784716d..c7cf574748 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -629,6 +629,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "prism", id: 390884143749136386n, }, + t7ru: { + name: "t7ru", + id: 380694434980954114n, + }, } satisfies Record); // iife so #__PURE__ works correctly