PinDMs: add category support & fix bugs (#2203)

Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
Syncx 2024-03-22 10:55:37 +11:00 committed by GitHub
parent 55901ba35e
commit 2ce3487477
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 822 additions and 247 deletions

View file

@ -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<ColorPickerProps>(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>(".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<Category | null>(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<HTMLFormElement> | React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
if (!categoryId)
await createCategory(category);
else
await updateCategory(category);
forceUpdate();
modalProps.onClose();
};
return (
<ModalRoot {...modalProps}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{categoryId ? "Edit" : "New"} Category</Text>
</ModalHeader>
{/* form is here so when you press enter while in the text input it submits */}
<form onSubmit={onSave}>
<ModalContent className={cl("content")}>
<Forms.FormSection>
<Forms.FormTitle>Name</Forms.FormTitle>
<TextInput
value={category.name}
onChange={e => setCategory({ ...category, name: e })}
/>
</Forms.FormSection>
<Forms.FormDivider />
<Forms.FormSection>
<Forms.FormTitle>Color</Forms.FormTitle>
<ColorPickerWithSwatches
key={category.name}
defaultColor={DEFAULT_COLOR}
colors={SWATCHES}
onChange={c => setCategory({ ...category, color: c! })}
value={category.color}
renderDefaultButton={() => null}
renderCustomButton={() => (
<ColorPicker
color={category.color}
onChange={c => setCategory({ ...category, color: c! })}
key={category.name}
showEyeDropper={false}
/>
)}
/>
</Forms.FormSection>
</ModalContent>
<ModalFooter>
<Button type="submit" onClick={onSave} disabled={!category.name}>{categoryId ? "Save" : "Create"}</Button>
</ModalFooter>
</form>
</ModalRoot>
);
}
export const openCategoryModal = (categoryId: string | null, channelId: string | null) =>
openModalLazy(async () => {
await requireSettingsMenu();
return modalProps => <NewCategoryModal categoryId={categoryId} modalProps={modalProps} initalChannelId={channelId} />;
});

View file

@ -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 (
<Menu.MenuItem
id="pin-dm"
label="Pin DMs"
>
{!pinned && (
<>
<Menu.MenuItem
id="vc-add-category"
label="Add Category"
color="brand"
action={() => openCategoryModal(null, channelId)}
/>
<Menu.MenuSeparator />
{
categories.map(category => (
<Menu.MenuItem
id={`pin-category-${category.name}`}
label={category.name}
action={() => addChannelToCategory(channelId, category.id).then(forceUpdate)}
/>
))
}
</>
)}
{pinned && (
<>
<Menu.MenuItem
id="unpin-dm"
label="Unpin DM"
color="danger"
action={() => removeChannelFromCategory(channelId).then(forceUpdate)}
/>
{
!settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, -1) && (
<Menu.MenuItem
id="move-up"
label="Move Up"
action={() => moveChannel(channelId, -1).then(forceUpdate)}
/>
)
}
{
!settings.store.sortDmsByNewestMessage && canMoveChannelInDirection(channelId, 1) && (
<Menu.MenuItem
id="move-down"
label="Move Down"
action={() => moveChannel(channelId, 1).then(forceUpdate)}
/>
)
}
</>
)}
</Menu.MenuItem>
);
}
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
};

View file

@ -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
];

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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 (
<>
<Menu.MenuItem
id="pin-dm"
label={pinned ? "Unpin DM" : "Pin DM"}
action={() => togglePin(channelId)}
/>
{canMove && snapshotArray[0] !== channelId && (
<Menu.MenuItem
id="move-pin-up"
label="Move Pin Up"
action={() => movePin(channelId, -1)}
/>
)}
{canMove && 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 const contextMenus = {
"gdm-context": GroupDMContext,
"user-context": UserContext
};

214
src/plugins/pinDms/data.ts Normal file
View file

@ -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[]>(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<Category[]>(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);
}

View file

@ -1,116 +1,131 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors * Copyright (c) 2024 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* 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 "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; 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 { Channel } from "discord-types/general";
import { contextMenus } from "./contextMenus"; import { contextMenus } from "./components/contextMenu";
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings"; 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({ export default definePlugin({
name: "PinDMs", 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", 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, settings,
contextMenus, contextMenus,
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: 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: [ patches: [
// Patch DM list
{ {
find: ".privateChannelsHeaderContainer,", find: ".privateChannelsHeaderContainer,",
replacement: [ replacement: [
// Init
{ {
// filter Discord's privateChannelIds list to remove pins, and pass match: /(?<=componentDidMount\(\){).{1,100}scrollToChannel/,
// pinCount as prop. This needs to be here so that the entire DM list receives replace: "$self._instance = this;$&"
// updates on pin/unpin
match: /(?<=\i,{channels:\i,)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 // Filter out pinned channels from the private channel list
// the number is the amount of rows. Add our pinCount in second place match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/,
// - Section 1: buttons for pages like Friends & Library replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))"
// - 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??[],"
}, },
{ {
// Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages" // Insert the pinned channels to sections
// lookbehind is used to lookup parameter name. We could use arguments[0], but match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/,
// if children ever is wrapped in an iife, it will break replace: "...$self.makeProps(this,{$&})"
match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=(\i)=>{.+?)/, },
replace: "children:$2.section===1?'Pinned DMs':$1"
// 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 match: /this\.renderSection=(\i)=>{/,
// channel=channels[channelIds[row]]; replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);"
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;"
}, },
{ {
// Fix getRowHeight's check for whether this is the DMs section match: /(?<=span",{)className:\i\.headerText,/,
// DMS (inlined) === section replace: "...$self.makeSpanProps(),$&"
match: /(?<=getRowHeight=\(.{2,50}?)1===\i/,
// DMS (inlined) === section - 1
replace: "$&-1"
}, },
// 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 // Override scrollToChannel to properly account for pinned channels
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/, match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)" 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 // Fix Alt Up/Down navigation
{ {
find: ".Routes.APPLICATION_STORE&&", find: ".Routes.APPLICATION_STORE&&",
@ -118,16 +133,227 @@ export default definePlugin({
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)] // channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/, match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c))) // ....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 // fix alt+shift+up/down
{ {
find: ".getFlattenedGuildIds()],", find: ".getFlattenedGuildIds()],",
replacement: { replacement: {
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/, 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 (
<h2
className={classes(headerClasses.privateChannelsHeaderContainer, "vc-pindms-section-container", category.collapsed ? "vc-pindms-collapsed" : "")}
style={{ color: `#${category.color.toString(16).padStart(6, "0")}` }}
onClick={async () => {
await collapseCategory(category.id, !category.collapsed);
forceUpdate();
}}
onContextMenu={e => {
ContextMenuApi.openContextMenu(e, () => (
<Menu.Menu
navId="vc-pindms-header-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
color="danger"
aria-label="Pin DMs Category Menu"
>
<Menu.MenuItem
id="vc-pindms-edit-category"
label="Edit Category"
action={() => openCategoryModal(category.id, null)}
/>
{
canMoveCategory(category.id) && (
<>
{
canMoveCategoryInDirection(category.id, -1) && <Menu.MenuItem
id="vc-pindms-move-category-up"
label="Move Up"
action={() => moveCategory(category.id, -1).then(() => forceUpdate())}
/>
}
{
canMoveCategoryInDirection(category.id, 1) && <Menu.MenuItem
id="vc-pindms-move-category-down"
label="Move Down"
action={() => moveCategory(category.id, 1).then(() => forceUpdate())}
/>
}
</>
)
}
<Menu.MenuSeparator />
<Menu.MenuItem
id="vc-pindms-delete-category"
color="danger"
label="Delete Category"
action={() => removeCategory(category.id).then(() => forceUpdate())}
/>
</Menu.Menu>
));
}}
>
<span className={headerClasses.headerText}>
{category?.name ?? "uh oh"}
</span>
<svg className="vc-pindms-collapse-icon" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M9.3 5.3a1 1 0 0 0 0 1.4l5.29 5.3-5.3 5.3a1 1 0 1 0 1.42 1.4l6-6a1 1 0 0 0 0-1.4l-6-6a1 1 0 0 0-1.42 0Z"></path>
</svg>
</h2>
);
}),
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
if (!channel || !category) return null;
if (this.isChannelHidden(sectionIndex, index)) return null;
return (
<ChannelComponent
channel={channel}
selected={this.instance.props.selectedChannelId === channel.id}
>
{channel.id}
</ChannelComponent>
);
},
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
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 ?? [];
}
}); });

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<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 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);
}

View file

@ -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);
}