PinDMs (#879)
This commit is contained in:
parent
c6fd8cae16
commit
6c719f5ee9
@ -21,7 +21,8 @@ import { relaunch } from "@utils/native";
|
||||
import definePlugin from "@utils/types";
|
||||
import * as Webpack from "@webpack";
|
||||
import { extract, filters, findAll, search } from "@webpack";
|
||||
import { React } from "@webpack/common";
|
||||
import { React, ReactDOM } from "@webpack/common";
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
const WEB_ONLY = (f: string) => () => {
|
||||
throw new Error(`'${f}' is Discord Desktop only.`);
|
||||
@ -59,6 +60,7 @@ export default definePlugin({
|
||||
};
|
||||
}
|
||||
|
||||
let fakeRenderWin: WeakRef<Window> | undefined;
|
||||
return {
|
||||
wp: Vencord.Webpack,
|
||||
wpc: Webpack.wreq.c,
|
||||
@ -79,7 +81,15 @@ export default definePlugin({
|
||||
Settings: Vencord.Settings,
|
||||
Api: Vencord.Api,
|
||||
reload: () => location.reload(),
|
||||
restart: IS_WEB ? WEB_ONLY("restart") : relaunch
|
||||
restart: IS_WEB ? WEB_ONLY("restart") : relaunch,
|
||||
fakeRender: (component: ComponentType, props: any) => {
|
||||
const prevWin = fakeRenderWin?.deref();
|
||||
const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
|
||||
fakeRenderWin = new WeakRef(win);
|
||||
win.focus();
|
||||
|
||||
ReactDOM.render(React.createElement(component, props), win.document.body);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
74
src/plugins/pinDms/contextMenus.tsx
Normal file
74
src/plugins/pinDms/contextMenus.tsx
Normal file
@ -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);
|
||||
}
|
127
src/plugins/pinDms/index.tsx
Normal file
127
src/plugins/pinDms/index.tsx
Normal file
@ -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)))"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
67
src/plugins/pinDms/settings.ts
Normal file
67
src/plugins/pinDms/settings.ts
Normal file
@ -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);
|
||||
}
|
@ -257,5 +257,9 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
pylix: {
|
||||
name: "pylix",
|
||||
id: 492949202121261067n
|
||||
},
|
||||
Strencher: {
|
||||
name: "Strencher",
|
||||
id: 415849376598982656n
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user