diff --git a/src/plugins/pinDms/components/CreateCategoryModal.tsx b/src/plugins/pinDms/components/CreateCategoryModal.tsx new file mode 100644 index 0000000..f38cc88 --- /dev/null +++ b/src/plugins/pinDms/components/CreateCategoryModal.tsx @@ -0,0 +1,134 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal"; +import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack"; +import { Button, Forms, Text, TextInput, Toasts, useEffect, useState } from "@webpack/common"; + +import { DEFAULT_COLOR, SWATCHES } from "../constants"; +import { categories, Category, createCategory, getCategory, updateCategory } from "../data"; +import { forceUpdate } from "../index"; + +interface ColorPickerProps { + color: number | null; + showEyeDropper?: boolean; + suggestedColors?: string[]; + onChange(value: number | null): void; +} + +interface ColorPickerWithSwatchesProps { + defaultColor: number; + colors: number[]; + value: number; + disabled?: boolean; + onChange(value: number | null): void; + renderDefaultButton?: () => React.ReactNode; + renderCustomButton?: () => React.ReactNode; +} + +const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); +const ColorPickerWithSwatches = findComponentByCodeLazy(".presets,", "customColor:"); + +export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}el\("(.+?)"\).{0,50}"UserSettings"/); + +const cl = classNameFactory("vc-pindms-modal-"); + +interface Props { + categoryId: string | null; + initalChannelId: string | null; + modalProps: ModalProps; +} + +function useCategory(categoryId: string | null, initalChannelId: string | null) { + const [category, setCategory] = useState(null); + + useEffect(() => { + if (categoryId) + setCategory(getCategory(categoryId)!); + else if (initalChannelId) + setCategory({ + id: Toasts.genId(), + name: `Pin Category ${categories.length + 1}`, + color: 10070709, + collapsed: false, + channels: [initalChannelId] + }); + }, [categoryId, initalChannelId]); + + return { + category, + setCategory + }; +} + +export function NewCategoryModal({ categoryId, modalProps, initalChannelId }: Props) { + const { category, setCategory } = useCategory(categoryId, initalChannelId); + + if (!category) return null; + + const onSave = async (e: React.FormEvent | React.MouseEvent) => { + e.preventDefault(); + if (!categoryId) + await createCategory(category); + else + await updateCategory(category); + + forceUpdate(); + modalProps.onClose(); + }; + + return ( + + + {categoryId ? "Edit" : "New"} Category + + + {/* form is here so when you press enter while in the text input it submits */} +
+ + + Name + setCategory({ ...category, name: e })} + /> + + + + Color + setCategory({ ...category, color: c! })} + value={category.color} + renderDefaultButton={() => null} + renderCustomButton={() => ( + setCategory({ ...category, color: c! })} + key={category.name} + showEyeDropper={false} + /> + )} + /> + + + + + +
+
+ ); +} + +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 0000000..0f5a198 --- /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 0000000..fbd995e --- /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 1db8b25..0000000 --- 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 0000000..afb6f97 --- /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 4517232..ee726bf 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"; -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], +interface ChannelComponentProps { + children: React.ReactNode, + channel: Channel, + selected: boolean; +} - settings, - contextMenus, - usePinCount(channelIds: string[]) { - const pinnedDms = usePinnedDms(); - // See comment on 2nd patch for reasoning - return channelIds.length ? [pinnedDms.size] : []; - }, +const headerClasses = findByPropsLazy("privateChannelsHeaderContainer"); - getChannel(channels: Record, idx: number) { - return channels[getPinAt(idx)]; - }, - - isPinned, - getSnapshot: sortedSnapshot, +const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; }; - 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 - ); +export let instance: any; +export const forceUpdate = () => instance?.props?._forceUpdate?.(); - return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding; +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, Devs.Aria], + settings, + contextMenus, + patches: [ - // Patch DM list { find: ".privateChannelsHeaderContainer,", replacement: [ + // Init + { + match: /(?<=componentDidMount\(\){).{1,100}scrollToChannel/, + replace: "$self._instance = this;$&" + }, { - // 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)," + // Filter out pinned channels from the private channel list + match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/, + replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))" }, { - // 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??[]," + // 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 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" + match: /this\.renderSection=(\i)=>{/, + replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($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: /(?<=span",{)className:\i\.headerText,/, + replace: "...$self.makeSpanProps(),$&" }, + + // Fix Row Height { - // 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: /(?<=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 1e0244e..0000000 --- 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 0000000..173f0f7 --- /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); +}