Equicord/src/equicordplugins/channelTabs/index.tsx
2026-02-04 16:30:41 -05:00

262 lines
9.3 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Channel, Message } from "@vencord/discord-types";
import { ChannelStore, Menu } from "@webpack/common";
import { JSX } from "react";
import ChannelsTabsContainer from "./components/ChannelTabsContainer";
import { BasicChannelTabsProps, createTab, handleChannelSwitch, settings } from "./util";
import * as ChannelTabsUtils from "./util";
const contextMenuPatch: NavContextMenuPatchCallback = (children, props: { channel: Channel, messageId?: string; }) => {
const { channel, messageId } = props;
const menuItem = (
<Menu.MenuItem
label="Open in New Tab"
id="open-link-in-tab"
action={() => createTab({
guildId: channel.guild_id || "@me", // Normalize for DMs/Group Chats
channelId: channel.id
}, settings.store.openInNewTabAutoSwitch, messageId, true, true)} // The true values are important for bypassing tab limits
/>
);
const group = findGroupChildrenByChildId("channel-copy-link", children);
if (group) {
group.push(menuItem);
} else {
children.splice(-1, 0, (
<Menu.MenuGroup>
{menuItem}
</Menu.MenuGroup>
));
}
};
export default definePlugin({
name: "ChannelTabs",
description: "Group your commonly visited channels in tabs, like a browser",
authors: [Devs.TheSun, Devs.TheKodeToad, EquicordDevs.keifufu, Devs.Nickyux, EquicordDevs.DiabeloDEV, EquicordDevs.justjxke],
dependencies: ["ContextMenuAPI"],
contextMenus: {
"channel-mention-context": contextMenuPatch,
"channel-context": contextMenuPatch,
"user-context": contextMenuPatch,
"gdm-context": contextMenuPatch
},
patches: [
// add the channel tab container at the top
{
find: '"AppView"',
replacement: [
{
match: /((\i\?.params)\?\.channelId.{0,600})"div",{(?=className:\i\.\i)/,
replace: "$1$self.render,{currentChannel:$2,",
predicate: () => settings.store.tabBarPosition === "top"
},
{
match: /((\i\?.params)\?.channelId.{0,300})"div",{/,
replace: "$1$self.render,{currentChannel:$2,",
predicate: () => settings.store.tabBarPosition === "bottom"
}
]
},
// intercept channel navigation to switch/create tabs
{
find: "`transitionToGuild - Transitioning to",
replacement: {
match: /(\i\((\i),(\i),\i,\i\)\{)(.{0,25}`transitionToGuild)/,
replace: "$1$self.handleNavigation($2,$3);$4"
}
},
// ctrl click to open in new tab in inbox unread
{
find: '[data-recents-channel="',
replacement: {
match: /(?<=className:\i.\i,onJump:)\i=>(\i)\(\i,(\i)\.id\)/,
replace: "event => { if (event.ctrlKey) $self.open($2); else $1(event, $2.id) }"
}
},
// ctrl click to open in new tab in inbox mentions
{
find: ".deleteRecentMention(",
replacement: {
match: /(?<=className:\i.\i,onJump:)(\i)(?=.{0,20}message:(\i))/,
replace: "event => { if (event.ctrlKey) $self.open($2); else $1(event) }"
}
},
// ctrl click to open in new tab in search results
{
find: "__invalid_searchResultFocusRing",
replacement: {
match: /(\i)\.stopPropagation.{0,50}(?=null!=(\i))/,
replace: "$&if ($1.ctrlKey) return $self.open($2);"
}
},
// prevent issues with the pins/inbox popouts being too tall
{
find: "#{intl::JUMP}),onClick:",
replacement: {
match: /\i&&\((\i).maxHeight.{0,5}\)/,
replace: "$&;$1.maxHeight-=$self.containerHeight"
}
}
],
settings,
start() {
// migrate old settings to new granular keybind settings
const store = settings.store as any;
if (store.enableHotkeys !== undefined) {
const oldValue = store.enableHotkeys;
settings.store.enableNumberKeySwitching = oldValue;
settings.store.enableCloseTabShortcut = oldValue;
settings.store.enableNewTabShortcut = oldValue;
settings.store.enableTabCycleShortcut = oldValue;
delete store.enableHotkeys;
}
if (store.hotkeyCount !== undefined) {
settings.store.numberKeySwitchCount = store.hotkeyCount;
delete store.hotkeyCount;
}
},
containerHeight: 0,
flux: {
CHANNEL_SELECT(data: { channelId: string | null, guildId: string | null; }) {
// Skip if this navigation was triggered by us (clicking a tab)
if (ChannelTabsUtils.isNavigatingViaTab()) {
ChannelTabsUtils.clearNavigationFlag();
return;
}
const isViewingViaBookmark = ChannelTabsUtils.isViewingViaBookmarkMode();
let { channelId } = data;
let { guildId } = data;
// Detect special pages by pathname when no channelId
if (!channelId) {
const path = window.location.pathname;
if (path === "/quest-home" || path.includes("quest-home")) {
channelId = "__quests__";
guildId = "@me";
} else if (path.includes("/message-requests")) {
channelId = "__message-requests__";
guildId = "@me";
} else if (path === "/channels/@me") {
channelId = "__friends__";
guildId = "@me";
} else if (path === "/channels/@me/activity") {
channelId = "__activity__";
guildId = "@me";
} else if (path.includes("/shop")) {
channelId = "__shop__";
guildId = "@me";
} else if (path.includes("/library")) {
channelId = "__library__";
guildId = "@me";
} else if (path.includes("/discovery")) {
channelId = "__discovery__";
guildId = "@me";
} else if (path.includes("/store")) {
channelId = "__nitro__";
guildId = "@me";
} else if (path.includes("/icymi")) {
channelId = "__icymi__";
guildId = "@me";
} else {
// Unknown page without channelId - ignore
return;
}
}
// if bookmark independent mode setting is on, navigate
if (isViewingViaBookmark) {
// dont modify tabs, just let that bookmark action happen
return;
}
// clear bookmark viewing mode only when this is NOT a bookmark navigation
// this happens when someone manually navigates to a channel (channel list)
ChannelTabsUtils.clearBookmarkViewingMode();
// At this point, channelId is guaranteed to be non-null
if (channelId && guildId) {
handleChannelSwitch({ guildId, channelId });
}
}
},
render({ currentChannel, children }: {
currentChannel: BasicChannelTabsProps,
children: JSX.Element;
}) {
const tabsContainer = (
<ErrorBoundary>
<ChannelsTabsContainer {...currentChannel} />
</ErrorBoundary>
);
if (settings.store.tabBarPosition === "top") {
return (
<>
{tabsContainer}
{children}
</>
);
}
return (
<>
{children}
{tabsContainer}
</>
);
},
open(message: Message) {
const tab = {
channelId: message.channel_id,
guildId: ChannelStore.getChannel(message.channel_id)?.guild_id,
compact: false
};
createTab(tab, false, message.id);
},
handleNavigation(guildId: string, channelId: string) {
// Skip if we initiated this navigation
if (ChannelTabsUtils.isNavigatingViaTab()) {
return;
}
// Skip if we're viewing via bookmarks in independent mode
if (ChannelTabsUtils.isViewingViaBookmarkMode()) {
return;
}
// Clear bookmark viewing mode when navigating to a regular channel
ChannelTabsUtils.clearBookmarkViewingMode();
// wait for discord to update channel data
requestAnimationFrame(() => {
handleChannelSwitch({ guildId, channelId });
});
},
util: ChannelTabsUtils,
});