feat(ChannelTabs): Improves tab management/behaviour (#285)

* basically instead of changing the current tab:
  -if there is a tab for that channel: swap to the channel tab
  -if there isnt a tab for that channel it creates a new one
  -there is a 3 second (you can change it in settings) grace period for when you are quickly switching tabs so your tabs dont get filled

also added background (and border to the active tab) to the tabs so it works better with custom themes that have image backgrounds

* made the improvements settings

* better regex (ty thororen)

* Update style.css

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
Wes 2025-06-11 14:47:52 -06:00 committed by GitHub
parent dd6920d20a
commit 6096013c65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 10 deletions

View file

@ -52,16 +52,25 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
}, []);
useEffect(() => {
(Vencord.Plugins.plugins.ChannelTabs as any).containerHeight = ref.current?.clientHeight;
if (ref.current) {
try {
(Vencord.Plugins.plugins.ChannelTabs as any).containerHeight = ref.current.clientHeight;
} catch { }
}
}, [userId, showBookmarkBar]);
useEffect(() => {
_update();
}, [widerTabsAndBookmarks]);
useEffect(() => {
if (userId) {
handleChannelSwitch(props);
saveTabs(userId);
}
}, [userId, props.channelId, props.guildId]);
if (!userId) return null;
handleChannelSwitch(props);
saveTabs(userId);
return (
<div
@ -121,4 +130,4 @@ export function ChannelTabsPreview(p) {
</Flex>
</>
);
}
}

View file

@ -15,7 +15,7 @@ import { Channel, Message } from "discord-types/general";
import { JSX } from "react";
import ChannelsTabsContainer from "./components/ChannelTabsContainer";
import { BasicChannelTabsProps, createTab, settings } from "./util";
import { BasicChannelTabsProps, createTab, handleChannelSwitch, settings } from "./util";
import * as ChannelTabsUtils from "./util";
const contextMenuPatch: NavContextMenuPatchCallback = (children, props: { channel: Channel, messageId?: string; }) =>
@ -52,6 +52,14 @@ export default definePlugin({
replace: "$1$4$self.render,{currentChannel:$2,children:$3})$5"
}
},
// intercept channel navigation to switch/create tabs
{
find: "transitionToGuild",
replacement: {
match: /transitionToGuild\(([^,]+),([^)]+)\)/,
replace: "$&;$self.handleNavigation($1,$2)"
}
},
// ctrl click to open in new tab in inbox unread
{
find: ".messageContainer,onKeyDown",
@ -113,5 +121,14 @@ export default definePlugin({
createTab(tab, false, message.id);
},
handleNavigation(guildId: string, channelId: string) {
if (!guildId || !channelId) return;
// wait for discord to update channel data
requestAnimationFrame(() => {
handleChannelSwitch({ guildId, channelId });
});
},
util: ChannelTabsUtils,
});
});

View file

@ -145,6 +145,9 @@
width: 12rem;
min-width: 0;
margin-right: 0.25rem;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-accent);
transition: background-color 0.15s ease;
}
.vc-channeltabs-tab.vc-channeltabs-wider:not(.vc-channeltabs-tab-compact) {
@ -159,6 +162,7 @@
.vc-channeltabs-tab-selected {
background: var(--bg-overlay-selected);
background-color: var(--background-modifier-selected);
border-color: var(--brand-500);
}
/* channel type container */

View file

@ -71,7 +71,32 @@ export const settings = definePluginSettings({
type: OptionType.BOOLEAN,
default: false,
restartNeeded: false
},
switchToExistingTab: {
type: OptionType.BOOLEAN,
description: "Switch to tab if it already exists for the channel you're navigating to",
default: false,
restartNeeded: false
},
createNewTabIfNotExists: {
type: OptionType.BOOLEAN,
description: "Create a new tab if one doesn't exist for the channel you're navigating to",
default: false,
restartNeeded: false
},
enableRapidNavigation: {
type: OptionType.BOOLEAN,
description: "Enable rapid navigation behavior - quickly navigating between channels will replace the current tab instead of creating new ones",
default: false,
restartNeeded: false
},
rapidNavigationThreshold: {
type: OptionType.SLIDER,
description: "Time window (in milliseconds) for rapid navigation. Within this time, new channels replace the current tab instead of creating new ones.",
markers: [500, 1000, 1500, 2000, 3000, 5000, 10000],
default: 3000,
stickToMarkers: false,
}
});
export const CircleQuestionIcon = findComponentByCodeLazy("10.58l-3.3-3.3a1");
export const CircleQuestionIcon = findComponentByCodeLazy("10.58l-3.3-3.3a1");

View file

@ -128,11 +128,46 @@ export function closeTabsToTheLeft(id: number) {
else update();
}
let lastNavigationTime = 0;
export function handleChannelSwitch(ch: BasicChannelTabsProps) {
const tab = openTabs.find(c => c.id === currentlyOpenTab);
if (tab === undefined) return logger.error("Couldn't find the currently open channel " + currentlyOpenTab, openTabs);
const existingTab = openTabs.find(tab => tab.channelId === ch.channelId && tab.guildId === ch.guildId);
if (tab.channelId !== ch.channelId) openTabs[openTabs.indexOf(tab)] = { id: tab.id, compact: tab.compact, ...ch };
// First check: switch to existing tab if setting enabled and tab exists
if (settings.store.switchToExistingTab && existingTab) {
moveToTab(existingTab.id);
return;
}
// Second check: create new tab if setting enabled
if (settings.store.createNewTabIfNotExists) {
// Apply rapid navigation logic when creating new tabs
const now = Date.now();
const isRapidNavigation = now - lastNavigationTime < settings.store.rapidNavigationThreshold;
lastNavigationTime = now;
if (isRapidNavigation && settings.store.enableRapidNavigation) {
// Replace current tab content instead of creating new one
const currentTab = openTabs.find(t => t.id === currentlyOpenTab);
if (currentTab) {
currentTab.channelId = ch.channelId;
currentTab.guildId = ch.guildId;
update();
return;
}
}
// Create new tab (normal behavior)
createTab(ch, true);
return;
}
// Default behavior: replace current tab content
if (tab && tab.channelId !== ch.channelId) {
openTabs[openTabs.indexOf(tab)] = { id: tab.id, compact: tab.compact, ...ch };
update();
}
}
export function hasClosedTabs() {
@ -271,4 +306,4 @@ export function useGhostTabs() {
setCount(0);
};
return new Array<JSX.Element>(count).fill(<div className={cl("tab", "ghost-tab")} />);
}
}