PinDMs (#879)
This commit is contained in:
		
							parent
							
								
									c6fd8cae16
								
							
						
					
					
						commit
						6c719f5ee9
					
				
					 5 changed files with 284 additions and 2 deletions
				
			
		|  | @ -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 a new issue