aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/pinDms
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/pinDms')
-rw-r--r--src/plugins/pinDms/contextMenus.tsx74
-rw-r--r--src/plugins/pinDms/index.tsx127
-rw-r--r--src/plugins/pinDms/settings.ts67
3 files changed, 268 insertions, 0 deletions
diff --git a/src/plugins/pinDms/contextMenus.tsx b/src/plugins/pinDms/contextMenus.tsx
new file mode 100644
index 0000000..b04ba8c
--- /dev/null
+++ b/src/plugins/pinDms/contextMenus.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+*/
+
+import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
+import { Menu } from "@webpack/common";
+
+import { isPinned, movePin, snapshotArray, togglePin } from "./settings";
+
+function PinMenuItem(channelId: string) {
+ const pinned = isPinned(channelId);
+
+ return (
+ <>
+ <Menu.MenuItem
+ id="pin-dm"
+ label={pinned ? "Unpin DM" : "Pin DM"}
+ action={() => togglePin(channelId)}
+ />
+ {pinned && snapshotArray[0] !== channelId && (
+ <Menu.MenuItem
+ id="move-pin-up"
+ label="Move Pin Up"
+ action={() => movePin(channelId, -1)}
+ />
+ )}
+ {pinned && snapshotArray[snapshotArray.length - 1] !== channelId && (
+ <Menu.MenuItem
+ id="move-pin-down"
+ label="Move Pin Down"
+ action={() => 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 function addContextMenus() {
+ addContextMenuPatch("gdm-context", GroupDMContext);
+ addContextMenuPatch("user-context", UserContext);
+}
+
+export function removeContextMenus() {
+ removeContextMenuPatch("gdm-context", GroupDMContext);
+ removeContextMenuPatch("user-context", UserContext);
+}
diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx
new file mode 100644
index 0000000..1cc6289
--- /dev/null
+++ b/src/plugins/pinDms/index.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+*/
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { Channel } from "discord-types/general";
+
+import { addContextMenus, removeContextMenus } from "./contextMenus";
+import { getPinAt, isPinned, snapshotArray, usePinnedDms } from "./settings";
+
+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, Devs.Strencher],
+
+ dependencies: ["ContextMenuAPI"],
+
+ start: addContextMenus,
+ stop: removeContextMenus,
+
+ usePinCount(channelIds: string[]) {
+ const pinnedDms = usePinnedDms();
+ // See comment on 2nd patch for reasoning
+ return channelIds.length ? [pinnedDms.size] : [];
+ },
+
+ getChannel(channels: Record<string, Channel>, idx: number) {
+ return channels[getPinAt(idx)];
+ },
+
+ isPinned,
+ getSnapshot: () => snapshotArray,
+
+ 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: [
+ {
+ // 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: /privateChannelIds:(\i),/,
+ replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1),"
+ },
+ {
+ // 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:(\i)\.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: "$&...$1.props.pinCount,"
+ },
+ {
+ // 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=function\((\i)\).+?)/,
+ replace: "children:$2.section===1?'Pinned DMs':$1"
+ },
+ {
+ // Patch channel lookup inside renderDM
+ // channel=channels[channelIds[row]];
+ match: /(?<=preRenderedChildren,(\i)=)((\i)\[\i\[\i\]\]);/,
+ // section 1 is us, manually get our own channel
+ // section === 1 ? getChannel(channels, row) : channels[channelIds[row]];
+ replace: "arguments[0]===1?$self.getChannel($3,arguments[1]):$2;"
+ },
+ {
+ // Fix getRowHeight's check for whether this is the DMs section
+ // section === DMS
+ match: /===\i.DMS&&0/,
+ // section -1 === DMS
+ replace: "-1$&"
+ },
+ {
+ // Override scrollToChannel to properly account for pinned channels
+ match: /(?<=else\{\i\+=)(\i)\*\(.+?(?=;)/,
+ replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
+ }
+ ]
+ },
+
+ // Fix Alt Up/Down navigation
+ {
+ find: '"mod+alt+right"',
+ replacement: {
+ // channelIds = __OVERLAY__ ? stuff : toArray(getStaticPaths()).concat(toArray(channelIds))
+ match: /(?<=(\i)=__OVERLAY__\?\i:.{0,10})\.concat\((.{0,10})\)/,
+ // ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
+ replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
+ }
+ }
+ ]
+});
diff --git a/src/plugins/pinDms/settings.ts b/src/plugins/pinDms/settings.ts
new file mode 100644
index 0000000..2526c69
--- /dev/null
+++ b/src/plugins/pinDms/settings.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 <https://www.gnu.org/licenses/>.
+*/
+
+import { Settings, useSettings } from "@api/settings";
+
+export let snapshotArray: string[];
+let snapshot: Set<string> | 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<string>(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 getPinAt(idx: number) {
+ requireSnapshot();
+ return snapshotArray[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);
+}