2023-04-14 20:26:46 -04:00
|
|
|
/*
|
2024-03-21 19:55:37 -04:00
|
|
|
* Vencord, a Discord client mod
|
|
|
|
* Copyright (c) 2024 Vendicated and contributors
|
|
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
*/
|
2023-04-14 20:26:46 -04:00
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
import "./styles.css";
|
|
|
|
|
|
|
|
import { definePluginSettings } from "@api/Settings";
|
|
|
|
import ErrorBoundary from "@components/ErrorBoundary";
|
2023-04-14 20:26:46 -04:00
|
|
|
import { Devs } from "@utils/constants";
|
2024-03-21 19:55:37 -04:00
|
|
|
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";
|
2023-04-14 20:26:46 -04:00
|
|
|
import { Channel } from "discord-types/general";
|
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
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";
|
2023-04-14 20:26:46 -04:00
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
interface ChannelComponentProps {
|
|
|
|
children: React.ReactNode,
|
|
|
|
channel: Channel,
|
|
|
|
selected: boolean;
|
|
|
|
}
|
2023-04-14 20:26:46 -04:00
|
|
|
|
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
const headerClasses = findByPropsLazy("privateChannelsHeaderContainer");
|
2023-04-14 20:26:46 -04:00
|
|
|
|
2024-04-05 15:29:08 -04:00
|
|
|
export const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
2023-04-14 20:26:46 -04:00
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
export let instance: any;
|
|
|
|
export const forceUpdate = () => instance?.props?._forceUpdate?.();
|
2023-04-14 20:26:46 -04:00
|
|
|
|
2024-03-28 16:40:47 -04:00
|
|
|
export const enum PinOrder {
|
|
|
|
LastMessage,
|
|
|
|
Custom
|
|
|
|
}
|
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
export const settings = definePluginSettings({
|
2024-03-28 16:40:47 -04:00
|
|
|
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 }
|
|
|
|
],
|
2024-03-21 19:55:37 -04:00
|
|
|
onChange: () => forceUpdate()
|
2023-04-14 20:26:46 -04:00
|
|
|
},
|
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
dmSectioncollapsed: {
|
|
|
|
type: OptionType.BOOLEAN,
|
|
|
|
description: "Collapse DM sections",
|
|
|
|
default: false,
|
|
|
|
onChange: () => forceUpdate()
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
export default definePlugin({
|
|
|
|
name: "PinDMs",
|
|
|
|
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
|
|
|
|
authors: [Devs.Ven, Devs.Aria],
|
|
|
|
settings,
|
|
|
|
contextMenus,
|
|
|
|
|
2023-04-14 20:26:46 -04:00
|
|
|
patches: [
|
|
|
|
{
|
|
|
|
find: ".privateChannelsHeaderContainer,",
|
|
|
|
replacement: [
|
|
|
|
{
|
2024-03-21 19:55:37 -04:00
|
|
|
// Filter out pinned channels from the private channel list
|
|
|
|
match: /(?<=\i,{channels:\i,)privateChannelIds:(\i)/,
|
|
|
|
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c))"
|
2023-04-14 20:26:46 -04:00
|
|
|
},
|
|
|
|
{
|
2024-03-21 19:55:37 -04:00
|
|
|
// Insert the pinned channels to sections
|
|
|
|
match: /(?<=renderRow:this\.renderRow,)sections:\[.+?1\)]/,
|
|
|
|
replace: "...$self.makeProps(this,{$&})"
|
|
|
|
},
|
|
|
|
|
|
|
|
// Rendering
|
|
|
|
{
|
2024-03-27 22:58:11 -04:00
|
|
|
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
|
|
|
|
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);"
|
2023-04-14 20:26:46 -04:00
|
|
|
},
|
|
|
|
{
|
2024-03-27 22:58:11 -04:00
|
|
|
match: /"renderSection",(\i)=>{/,
|
2024-03-21 19:55:37 -04:00
|
|
|
replace: "$&if($self.isCategoryIndex($1.section))return $self.renderCategory($1);"
|
2023-04-14 20:26:46 -04:00
|
|
|
},
|
|
|
|
{
|
2024-03-21 19:55:37 -04:00
|
|
|
match: /(?<=span",{)className:\i\.headerText,/,
|
|
|
|
replace: "...$self.makeSpanProps(),$&"
|
2023-04-14 20:26:46 -04:00
|
|
|
},
|
2024-03-21 19:55:37 -04:00
|
|
|
|
|
|
|
// Fix Row Height
|
2023-04-14 20:26:46 -04:00
|
|
|
{
|
2024-03-27 22:58:11 -04:00
|
|
|
match: /(?<="getRowHeight",.{1,100}return 1===)\i/,
|
2024-03-21 19:55:37 -04:00
|
|
|
replace: "($&-$self.categoryLen())"
|
2023-04-14 20:26:46 -04:00
|
|
|
},
|
2024-03-21 19:55:37 -04:00
|
|
|
{
|
2024-03-27 22:58:11 -04:00
|
|
|
match: /"getRowHeight",\((\i),(\i)\)=>{/,
|
2024-03-21 19:55:37 -04:00
|
|
|
replace: "$&if($self.isChannelHidden($1,$2))return 0;"
|
|
|
|
},
|
|
|
|
|
|
|
|
// Fix ScrollTo
|
2023-04-14 20:26:46 -04:00
|
|
|
{
|
|
|
|
// Override scrollToChannel to properly account for pinned channels
|
2023-10-25 11:34:23 -04:00
|
|
|
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
|
2023-04-14 20:26:46 -04:00
|
|
|
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
|
2024-03-21 19:55:37 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/,
|
|
|
|
replace: "[...$&,...$self.getAllUncollapsedChannels()]"
|
|
|
|
},
|
|
|
|
|
2023-04-14 20:26:46 -04:00
|
|
|
]
|
|
|
|
},
|
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
|
|
|
|
// 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,"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2023-04-14 20:26:46 -04:00
|
|
|
// Fix Alt Up/Down navigation
|
|
|
|
{
|
2023-10-25 11:34:23 -04:00
|
|
|
find: ".Routes.APPLICATION_STORE&&",
|
2023-04-14 20:26:46 -04:00
|
|
|
replacement: {
|
2023-10-25 11:34:23 -04:00
|
|
|
// channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
|
|
|
|
match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
|
2023-04-14 20:26:46 -04:00
|
|
|
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
|
2024-03-21 19:55:37 -04:00
|
|
|
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
|
2023-04-14 20:26:46 -04:00
|
|
|
}
|
2023-09-05 14:51:22 -04:00
|
|
|
},
|
2024-03-21 19:55:37 -04:00
|
|
|
|
2023-09-05 14:51:22 -04:00
|
|
|
// fix alt+shift+up/down
|
|
|
|
{
|
2023-10-25 11:34:23 -04:00
|
|
|
find: ".getFlattenedGuildIds()],",
|
2023-09-05 14:51:22 -04:00
|
|
|
replacement: {
|
2023-10-25 11:34:23 -04:00
|
|
|
match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
|
2024-03-21 19:55:37 -04:00
|
|
|
replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))"
|
2023-09-05 14:51:22 -04:00
|
|
|
}
|
|
|
|
},
|
2024-03-21 19:55:37 -04:00
|
|
|
],
|
|
|
|
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,
|
2024-03-28 16:40:47 -04:00
|
|
|
|
2024-03-21 19:55:37 -04:00
|
|
|
makeProps(instance, { sections }: { sections: number[]; }) {
|
2024-03-28 16:40:47 -04:00
|
|
|
this._instance = instance;
|
2024-03-21 19:55:37 -04:00
|
|
|
this.sections = sections;
|
|
|
|
|
2024-03-28 16:40:47 -04:00
|
|
|
this.sections.splice(1, 0, ...this.getSections());
|
2024-03-21 19:55:37 -04:00
|
|
|
|
|
|
|
if (this.instance?.props?.privateChannelIds?.length === 0) {
|
2024-03-28 16:40:47 -04:00
|
|
|
// dont render direct messages header
|
2024-03-21 19:55:37 -04:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
|
|
|
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];
|
2024-03-28 16:40:47 -04:00
|
|
|
return this.isCategoryIndex(sectionIndex) && (cat?.channels?.length === 0 || cat?.channels[channelIndex]);
|
2024-03-21 19:55:37 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
isDMSectioncollapsed() {
|
|
|
|
return settings.store.dmSectioncollapsed;
|
|
|
|
},
|
|
|
|
|
|
|
|
collapseDMList() {
|
|
|
|
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;
|
|
|
|
|
2024-04-05 15:29:08 -04:00
|
|
|
return category.collapsed && this.instance.props.selectedChannelId !== this.getCategoryChannels(category)[channelIndex];
|
2024-03-21 19:55:37 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
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 [];
|
|
|
|
|
2024-03-28 16:40:47 -04:00
|
|
|
if (settings.store.pinOrder === PinOrder.LastMessage) {
|
2024-03-21 19:55:37 -04:00
|
|
|
return PrivateChannelSortStore.getPrivateChannelIds().filter(c => category.channels.includes(c));
|
|
|
|
}
|
|
|
|
|
|
|
|
return category?.channels ?? [];
|
|
|
|
}
|
2023-04-14 20:26:46 -04:00
|
|
|
});
|