diff --git a/src/components/CheckedTextInput.tsx b/src/components/CheckedTextInput.tsx
index cf4aa119b..de2d7ae04 100644
--- a/src/components/CheckedTextInput.tsx
+++ b/src/components/CheckedTextInput.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { React, TextInput } from "@webpack/common";
+import { React, TextInput, useEffect } from "@webpack/common";
// TODO: Refactor settings to use this as well
interface TextInputProps {
@@ -55,6 +55,10 @@ export function CheckedTextInput({ value: initialValue, onChange, validate }: Te
}
}
+ useEffect(() => {
+ handleChange(initialValue);
+ }, [initialValue]);
+
return (
<>
("");
+ const [fullPatchError, setFullPatchError] = React.useState("");
+
+ function update() {
+ if (fullPatch === "") {
+ setFullPatchError("");
+
+ setFind("");
+ setMatch("");
+ setReplacement("");
+ return;
+ }
+
+ try {
+ const parsed = (0, eval)(`(${fullPatch})`) as Patch;
+
+ if (!parsed.find) throw new Error("No 'find' field");
+ if (!parsed.replacement) throw new Error("No 'replacement' field");
+
+ if (parsed.replacement instanceof Array) {
+ if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
+
+ parsed.replacement = {
+ match: parsed.replacement[0].match,
+ replace: parsed.replacement[0].replace
+ };
+ }
+
+ if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
+ if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
+
+ setFind(parsed.find);
+ setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
+ setReplacement(parsed.replacement.replace);
+ setFullPatchError("");
+ } catch (e) {
+ setFullPatchError((e as Error).message);
+ }
+ }
+
+ return <>
+ Paste your full JSON patch here to fill out the fields
+
+ {fullPatchError !== "" && {fullPatchError}}
+ >;
+}
+
function PatchHelper() {
const [find, setFind] = React.useState("");
const [match, setMatch] = React.useState("");
@@ -260,6 +314,13 @@ function PatchHelper() {
return (
+ full patch
+
+
find
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 */}
+
+
+ );
+}
+
+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 000000000..0f5a198ba
--- /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 000000000..fbd995e14
--- /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 1db8b25a9..000000000
--- 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 000000000..afb6f9795
--- /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 45172328e..ee726bfab 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";
+
+interface ChannelComponentProps {
+ children: React.ReactNode,
+ channel: Channel,
+ selected: boolean;
+}
+
+
+const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
+
+const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
+
+export let instance: any;
+export const forceUpdate = () => instance?.props?._forceUpdate?.();
+
+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],
-
+ authors: [Devs.Ven, Devs.Aria],
settings,
contextMenus,
- usePinCount(channelIds: string[]) {
- const pinnedDms = usePinnedDms();
- // See comment on 2nd patch for reasoning
- return channelIds.length ? [pinnedDms.size] : [];
- },
-
- getChannel(channels: Record, idx: number) {
- return channels[getPinAt(idx)];
- },
-
- isPinned,
- getSnapshot: sortedSnapshot,
-
- 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: [
+ // Init
{
- // 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),"
+ match: /(?<=componentDidMount\(\){).{1,100}scrollToChannel/,
+ replace: "$self._instance = this;$&"
},
{
- // 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??[],"
+ // Filter out pinned channels from the private channel list
+ match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/,
+ replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))"
},
{
- // 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"
+ // 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 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: /this\.renderSection=(\i)=>{/,
+ replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);"
},
{
- // 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: /(?<=span",{)className:\i\.headerText,/,
+ replace: "...$self.makeSpanProps(),$&"
},
+
+ // Fix Row Height
+ {
+ 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 1e0244ed7..000000000
--- 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 000000000..173f0f76b
--- /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);
+}