PinDMs: add category support & fix bugs (#2203)
Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
parent
55901ba35e
commit
2ce3487477
8 changed files with 822 additions and 247 deletions
134
src/plugins/pinDms/components/CreateCategoryModal.tsx
Normal file
134
src/plugins/pinDms/components/CreateCategoryModal.tsx
Normal 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} />;
|
||||||
|
});
|
||||||
|
|
96
src/plugins/pinDms/components/contextMenu.tsx
Normal file
96
src/plugins/pinDms/components/contextMenu.tsx
Normal 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
|
||||||
|
};
|
32
src/plugins/pinDms/constants.ts
Normal file
32
src/plugins/pinDms/constants.ts
Normal 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
|
||||||
|
];
|
|
@ -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
214
src/plugins/pinDms/data.ts
Normal 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);
|
||||||
|
}
|
|
@ -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 ?? [];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
37
src/plugins/pinDms/styles.css
Normal file
37
src/plugins/pinDms/styles.css
Normal 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);
|
||||||
|
}
|
Loading…
Reference in a new issue