SidebarChat: kill off until someone can have a look

This commit is contained in:
thororen1234 2026-04-28 19:56:27 -04:00
parent 25502c6590
commit 5e81dbcbda
No known key found for this signature in database
3 changed files with 0 additions and 768 deletions

View file

@ -1,594 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { HeaderBarButton } from "@api/HeaderBar";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { getCurrentChannel } from "@utils/discord";
import definePlugin from "@utils/types";
import { Channel, Guild, User } from "@vencord/discord-types";
import { ChannelType } from "@vencord/discord-types/enums";
import {
DefaultExtractAndLoadChunksRegex,
extractAndLoadChunksLazy,
findByPropsLazy,
findComponentByCodeLazy,
findCssClassesLazy,
findStoreLazy,
} from "@webpack";
import {
ChannelActionCreators,
ChannelRouter,
ChannelStore,
FluxDispatcher,
GuildStore,
Menu,
MessageActions,
MessageStore,
PermissionsBits,
PermissionStore,
PopoutActions,
PopoutWindowStore,
RelationshipStore,
SelectedChannelStore,
SelectedGuildStore,
useCallback,
useEffect,
useLayoutEffect,
UserStore,
useState,
useStateFromStores,
} from "@webpack/common";
import { getOpenPopoutWindowKeys, getPersistedPopoutChannelIds, getPopoutWindowKey, isPopoutWindowOpen, settings, SidebarStore, syncPersistedPopoutWindows } from "./store";
const cl = classNameFactory("vc-sidebar-chat-");
const HeaderBar = findComponentByCodeLazy("toolbarClassName:", "}),onDoubleClick:");
const ForumView = findComponentByCodeLazy("sidebarState");
const ArrowsLeftRightIcon = ({ color, ...rest }) => {
return (
<svg
aria-hidden="true"
role="img"
xmlns="http://www.w3.org/2000/svg"
fill={color}
viewBox="0 0 24 24"
{...rest}>
<path d="M2.3 7.7a1 1 0 0 1 0-1.4l4-4a1 1 0 0 1 1.4 1.4L5.42 6H21a1 1 0 1 1 0 2H5.41l2.3 2.3a1 1 0 1 1-1.42 1.4l-4-4ZM17.7 21.7l4-4a1 1 0 0 0 0-1.4l-4-4a1 1 0 0 0-1.4 1.4l2.29 2.3H3a1 1 0 1 0 0 2h15.59l-2.3 2.3a1 1 0 0 0 1.42 1.4Z" />
</svg>
);
};
const WindowLaunchIcon = findComponentByCodeLazy("1-1h6a1 1 0 1 0 0-2H5Z");
const XSmallIcon = findComponentByCodeLazy("1.4L12 13.42l5.3 5.3Z");
const Chat = findComponentByCodeLazy("filterAfterTimestamp:", "chatInputType");
const Resize = findComponentByCodeLazy("sidebarType:", "RESIZE_HANDLE_WIDTH)");
const ChannelHeader = findComponentByCodeLazy("`channel-${");
const PopoutWindow = findComponentByCodeLazy("Missing guestWindow reference");
const FullChannelView = findComponentByCodeLazy("showFollowButton:");
const WanderingCubesLoading = findComponentByCodeLazy('="wanderingCubes"');
const ChatInputTypes = findByPropsLazy("FORM", "NORMAL");
const Sidebars = findByPropsLazy("ThreadSidebar", "MessageRequestSidebar");
const ChatClasses = findCssClassesLazy("threadSidebarOpen", "loader");
const ChannelSectionStore = findStoreLazy("ChannelSectionStore");
const requireForumView = extractAndLoadChunksLazy(
["Missing channel in Channel.renderHeaderToolbar"],
new RegExp(DefaultExtractAndLoadChunksRegex.source + '.{1,150}name:"ForumChannel"')
);
function getChannelTitle(channel: Channel | null | undefined) {
if (!channel) return "Chat";
if (channel.isPrivate()) {
const recipientId = channel.getRecipientId?.();
if (!channel.name && recipientId) {
const user = UserStore.getUser(recipientId);
if (user) {
return RelationshipStore.getNickname(recipientId) || user.globalName || user.username || "DM";
}
}
return channel.name || "DM";
}
return channel.name || "Chat";
}
function canOpenPopout(channel: Channel) {
if (channel.isPrivate()) return true;
return !channel.isCategory() && !channel.isDirectory() && !channel.isVocal();
}
function getPopoutMenuLabel(channelId: string) {
return isPopoutWindowOpen(channelId) ? "Close popout chat" : "Popout chat";
}
let restorePersistedPopoutsInterval: number | null = null;
let restoringPersistedPopouts = false;
function clearPersistedPopoutRestoreLoop() {
restoringPersistedPopouts = false;
if (restorePersistedPopoutsInterval !== null) {
window.clearInterval(restorePersistedPopoutsInterval);
restorePersistedPopoutsInterval = null;
}
}
async function waitForChannel(channelId: string, timeoutMs = 2500) {
const startedAt = Date.now();
while (Date.now() - startedAt <= timeoutMs) {
const channel = ChannelStore.getChannel(channelId);
if (channel) return channel;
await new Promise(resolve => setTimeout(resolve, 80));
}
return null;
}
async function waitForDmChannel(userId: string, timeoutMs = 2500) {
const startedAt = Date.now();
while (Date.now() - startedAt <= timeoutMs) {
const channelId = ChannelStore.getDMFromUserId?.(userId);
if (channelId) return channelId;
await new Promise(resolve => setTimeout(resolve, 80));
}
return null;
}
async function openPopoutFromUserMenu(userId: string) {
try {
const channelId = await Promise.resolve(ChannelActionCreators.getOrEnsurePrivateChannel(userId));
if (!channelId) return;
const channel = await waitForChannel(channelId);
if (channel) openPopout(channel.id);
return;
} catch {
const fallbackChannelId = await waitForDmChannel(userId);
if (!fallbackChannelId) return;
const channel = await waitForChannel(fallbackChannelId);
if (channel) openPopout(channel.id);
}
}
function closePopout(channelId: string, syncPersistence = true) {
const windowKey = getPopoutWindowKey(channelId);
PopoutActions.close(windowKey);
if (syncPersistence && !restoringPersistedPopouts) {
syncPersistedPopoutWindows();
}
}
function openPopout(channelId: string, syncPersistence = true) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || !canOpenPopout(channel)) return;
const windowKey = getPopoutWindowKey(channelId);
if (isPopoutWindowOpen(channelId)) {
closePopout(channelId, syncPersistence);
return;
}
const title = getChannelTitle(channel);
PopoutActions.open(
windowKey,
() => <RenderPopout channel={channel} name={title} windowKey={windowKey} />,
{
defaultWidth: 854,
defaultHeight: 480,
}
);
PopoutActions.setAlwaysOnTop(windowKey, settings.store.popoutAlwaysOnTop);
if (syncPersistence && !restoringPersistedPopouts) {
syncPersistedPopoutWindows();
}
}
function restorePersistedPopouts() {
if (!settings.store.persistPopoutWindows) return;
clearPersistedPopoutRestoreLoop();
const pendingRestoreIds = new Set(getPersistedPopoutChannelIds());
if (pendingRestoreIds.size === 0) return;
restoringPersistedPopouts = true;
const attemptRestore = () => {
for (const channelId of pendingRestoreIds) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || !canOpenPopout(channel)) continue;
pendingRestoreIds.delete(channelId);
openPopout(channelId, false);
}
if (pendingRestoreIds.size === 0) {
clearPersistedPopoutRestoreLoop();
syncPersistedPopoutWindows();
}
};
attemptRestore();
if (pendingRestoreIds.size > 0) {
restorePersistedPopoutsInterval = window.setInterval(attemptRestore, 250);
}
}
const createSidebarChatContextMenuItem = (id: string, guildId: string | null) => {
return (
<Menu.MenuItem
id={`vc-sidebar-chat-${id}`}
label={"Open Sidebar Chat"}
action={() => {
FluxDispatcher.dispatch({
// @ts-ignore
type: "VC_SIDEBAR_CHAT_NEW",
guildId,
id,
});
}}
/>
);
};
const createPopoutChatContextMenuItem = (id: string, label: string, action: () => void | Promise<void>) => {
return (
<Menu.MenuItem
id={`vc-sidebar-chat-popout-${id}`}
label={label}
action={() => {
void action();
}}
/>
);
};
const UserContextPatch: NavContextMenuPatchCallback = (children, args: { user: User; }) => {
const checks = [
args.user,
args.user.id !== UserStore.getCurrentUser().id,
];
if (checks.some(check => !check)) return;
const channelId = ChannelStore.getDMFromUserId?.(args.user.id) ?? null;
const isOpen = channelId ? isPopoutWindowOpen(channelId) : false;
children.push(createSidebarChatContextMenuItem(args.user.id, null));
children.push(createPopoutChatContextMenuItem(
args.user.id,
isOpen ? "Close popout chat" : "Popout chat",
() => {
if (channelId && isOpen) {
closePopout(channelId);
return;
}
return openPopoutFromUserMenu(args.user.id);
}
));
};
const ChannelContextPatch: NavContextMenuPatchCallback = (children, args: { channel: Channel; }) => {
const checks = [
args.channel,
args.channel.type !== ChannelType.GUILD_CATEGORY,
PermissionStore.can(PermissionsBits.VIEW_CHANNEL, args.channel) || args.channel.type === ChannelType.GROUP_DM,
];
if (checks.some(check => !check)) return;
children.push(createSidebarChatContextMenuItem(args.channel.id, args.channel.guild_id));
children.push(createPopoutChatContextMenuItem(
args.channel.id,
getPopoutMenuLabel(args.channel.id),
() => openPopout(args.channel.id)
));
};
export default definePlugin({
name: "SidebarChat",
authors: [Devs.Joona, EquicordDevs.justjxke],
description: "Open a channel or DM as a sidebar or a popout.",
tags: ["Appearance", "Chat", "Servers"],
dependencies: ["HeaderBarAPI"],
patches: [
{
find: 'case"pendingFriends":',
group: true,
replacement: [
{
match: /ChannelRenderer"\),/,
replace: "$&vc_SidebarChat=$self.renderSidebar(),"
},
{
match: /return(\(0,\i\.jsxs?\)\(\i,{}\))}/,
replace: "return [$1,vc_SidebarChat]}"
},
{
match: /(?<=guild_products.{0,1600})(case \i\.\i.{0,50}return)(.+?\}\));(?=.+?params\.messageId)/g,
replace: "$1[$2,vc_SidebarChat];",
predicate: () => settings.store.patchCommunity,
},
{
match: /(case \i\.\i\.GAME_SERVERS:.{0,50}\.CHANNEL.{0,25}return)(.*?);/,
replace: "$1[$2,vc_SidebarChat];",
predicate: () => settings.store.patchCommunity,
},
],
},
],
headerBarButton: {
icon: WindowLaunchIcon,
render: () => (
<>
<PopoutPersistenceSync />
<WrappedPopoutHeaderButton />
</>
)
},
stop() {
clearPersistedPopoutRestoreLoop();
syncPersistedPopoutWindows();
for (const windowKey of getOpenPopoutWindowKeys()) {
PopoutActions.close(windowKey);
}
},
async start() {
restorePersistedPopouts();
},
settings,
contextMenus: {
"user-context": UserContextPatch,
"channel-context": ChannelContextPatch,
"thread-context": ChannelContextPatch,
"gdm-context": ChannelContextPatch,
},
toolboxActions: {
"Open Previous Chat"() {
FluxDispatcher.dispatch({
// @ts-ignore
type: "VC_SIDEBAR_CHAT_PREVIOUS",
});
}
},
renderSidebar() {
const { guild, channel /* width*/ } = useStateFromStores(
[SidebarStore, GuildStore, ChannelStore], () => {
const { channelId, guildId } = SidebarStore.getState();
return {
guild: GuildStore.getGuild(guildId),
channel: ChannelStore.getChannel(channelId)
};
}, []
);
const [channelSidebar, guildSidebar] = useStateFromStores(
[ChannelSectionStore, SelectedChannelStore, ChannelStore], () => {
const currentChannelId = SelectedChannelStore.getChannelId();
const currentGuildId = SelectedGuildStore.getGuildId();
return [
ChannelSectionStore.getSidebarState(currentChannelId),
ChannelSectionStore.getGuildSidebarState(currentGuildId),
];
}, []
);
useEffect(() => {
if (!channel?.id || MessageStore.getLastMessage(channel.id)) return;
MessageActions.fetchMessages({
channelId: channel.id,
limit: 50,
});
}, [channel?.id]);
const [width, setWidth] = useState(window.innerWidth);
useLayoutEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const [View, setViewComponent] = useState<React.ReactNode>(null);
useEffect(() => {
if (!channel) return;
if (channel.isForumLikeChannel()) {
requireForumView().then(() => {
setViewComponent(
<ForumView
channel={channel}
guild={guild}
sidebarState={null}
/>
);
});
setViewComponent(
<div className={ChatClasses.loader}>
<WanderingCubesLoading />
</div>
);
} else {
setViewComponent(
<Chat
channel={channel}
guild={guild}
chatInputType={ChatInputTypes.SIDEBAR}
/>
);
}
}, [channel]);
if (!channel || channelSidebar || guildSidebar) return null;
return (
<ErrorBoundary noop>
<Resize
sidebarType={Sidebars.MessageRequestSidebar}
maxWidth={~~(width * 0.31)/* width - 690*/}
>
<Header channel={channel} guild={guild} />
{View}
</Resize>
</ErrorBoundary>
);
},
});
const Header = ({ guild, channel }: { guild: Guild; channel: Channel; }) => {
const recipientId = channel.isPrivate() ? channel.getRecipientId() as string : null;
const name = useStateFromStores([UserStore, RelationshipStore], () => getChannelTitle(channel), [channel.id, channel.name]);
const parentChannel = useStateFromStores(
[ChannelStore], () => ChannelStore.getChannel(channel?.parent_id),
[channel?.parent_id]
);
const closeSidebar = () => FluxDispatcher.dispatch({ type: "VC_SIDEBAR_CHAT_CLOSE", });
const isPopoutOpen = useStateFromStores(
[PopoutWindowStore], () => isPopoutWindowOpen(channel.id),
[channel.id]
);
const openPopoutClick = useCallback(() => openPopout(channel.id), [channel.id]);
const switchChannels = useCallback(() => {
const mainChannel = getCurrentChannel()!;
FluxDispatcher.dispatch({
// @ts-ignore
type: "VC_SIDEBAR_CHAT_NEW",
guildId: mainChannel.guild_id,
id: mainChannel.id,
});
ChannelRouter.transitionToChannel(channel.id);
}, [channel.id]);
return (
<HeaderBar
toolbar={
<>
<HeaderBarButton icon={ArrowsLeftRightIcon} tooltip="Switch channels" onClick={switchChannels} />
<HeaderBarButton
key={`${channel.id}-${isPopoutOpen ? "open" : "closed"}`}
icon={isPopoutOpen ? XSmallIcon : WindowLaunchIcon}
tooltip={isPopoutOpen ? "Close popout chat" : "Popout chat"}
selected={isPopoutOpen}
onClick={openPopoutClick}
/>
<HeaderBarButton icon={XSmallIcon} tooltip="Close Sidebar Chat" onClick={closeSidebar} />
</>
}
>
<ChannelHeader
channel={channel}
channelName={name}
guild={guild}
parentChannel={parentChannel}
/>
</HeaderBar>
);
};
const RenderPopout = ErrorBoundary.wrap(({ channel, name, windowKey }: { channel: Channel; name: string; windowKey: string; }) => {
// Copy from an unexported function of the one they use in the experiment
// right click a channel and search withTitleBar:!0,windowKey
useEffect(() => {
if (!channel?.id || MessageStore.getLastMessage(channel.id)) return;
MessageActions.fetchMessages({
channelId: channel.id,
limit: 50,
});
}, [channel?.id]);
return (
<PopoutWindow
withTitleBar
windowKey={windowKey}
title={name || "Equicord"}
channelId={channel.id}
>
<div className={cl("window")}>
<FullChannelView providedChannel={channel} />
</div>
</PopoutWindow>
);
});
function PopoutHeaderButton() {
const channelState = useStateFromStores(
[SelectedChannelStore, ChannelStore, PopoutWindowStore],
() => {
const channelId = SelectedChannelStore.getChannelId();
const channel = channelId ? ChannelStore.getChannel(channelId) : null;
return {
channel,
isOpen: channel ? isPopoutWindowOpen(channel.id) : false,
label: getChannelTitle(channel)
};
},
[]
);
if (!channelState.channel || !canOpenPopout(channelState.channel)) return null;
const { channel, isOpen, label } = channelState;
return (
<HeaderBarButton
key={`${channel.id}-${isOpen ? "open" : "closed"}`}
icon={isOpen ? XSmallIcon : WindowLaunchIcon}
tooltip={isOpen ? "Close popout chat" : `Popout chat for ${label}`}
aria-label="Popout chat"
selected={isOpen}
onClick={() => openPopout(channel.id)}
/>
);
}
const WrappedPopoutHeaderButton = ErrorBoundary.wrap(PopoutHeaderButton, { noop: true });
function PopoutPersistenceSync() {
const openWindowKeySignature = useStateFromStores(
[PopoutWindowStore],
() => getOpenPopoutWindowKeys().join("|"),
[]
);
useEffect(() => {
if (restoringPersistedPopouts) return;
syncPersistedPopoutWindows();
}, [openWindowKeySignature]);
return null;
}

View file

@ -1,155 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { proxyLazy } from "@utils/lazy";
import { OptionType } from "@utils/types";
import { Flux as TFlux } from "@vencord/discord-types";
import { ChannelActionCreators, Flux as FluxWP, FluxDispatcher, PopoutActions, PopoutWindowStore } from "@webpack/common";
interface IFlux extends TFlux {
PersistedStore: TFlux["Store"];
}
export const settings = definePluginSettings({
persistSidebar: {
type: OptionType.BOOLEAN,
description: "Keep the sidebar chat open across Discord restarts",
default: true,
},
persistPopoutWindows: {
type: OptionType.BOOLEAN,
description: "Restore open popout chats after Discord restarts.",
default: true,
onChange: value => {
if (!value) {
settings.store.persistedPopoutWindowIds = [];
return;
}
syncPersistedPopoutWindows();
}
},
persistedPopoutWindowIds: {
type: OptionType.CUSTOM,
description: "Persisted popout chat channel IDs.",
default: [] as string[],
hidden: true
},
popoutAlwaysOnTop: {
type: OptionType.BOOLEAN,
description: "Keep popout chat windows above all others.",
default: true,
onChange: value => {
setAlwaysOnTopForOpenPopouts(value);
}
},
patchCommunity: {
type: OptionType.BOOLEAN,
description: "Patch things like the Channel Browser or Members tab that community servers have.",
default: true,
restartNeeded: true,
}
});
export const SidebarStore = proxyLazy(() => {
const current = {
guildId: "",
channelId: "",
width: 0
};
let previous = { ...current };
class SidebarStore extends (FluxWP as IFlux).PersistedStore {
static persistKey = "SidebarStore";
// @ts-ignore
initialize(previousState: { guildId?: string; channelId?: string; width?: number; } | undefined) {
if (!settings.store.persistSidebar || !previousState) return;
const { guildId, channelId, width } = previousState;
current.guildId = guildId || "";
current.channelId = channelId || "";
current.width = width || 0;
}
getState() {
return current;
}
}
const store = new SidebarStore(FluxDispatcher, {
// @ts-ignore
async VC_SIDEBAR_CHAT_NEW({ guildId: newGId, id }: { guildId: string | null; id: string; }) {
previous = { ...current };
current.guildId = newGId || "";
if (current.guildId) {
current.channelId = id;
store.emitChange();
return;
}
current.channelId = await ChannelActionCreators.getOrEnsurePrivateChannel(id);
store.emitChange();
},
VC_SIDEBAR_CHAT_PREVIOUS() {
if (previous.channelId) {
current.guildId = previous.guildId;
current.channelId = previous.channelId;
}
store.emitChange();
},
VC_SIDEBAR_CHAT_CLOSE() {
previous = { ...current };
current.guildId = "";
current.channelId = "";
store.emitChange();
},
});
return store;
});
const WINDOW_PREFIX = "DISCORD_VC_SC-";
export function getPopoutWindowKey(channelId: string) {
return `${WINDOW_PREFIX}${channelId}`;
}
export function getOpenPopoutWindowKeys() {
return PopoutWindowStore.getWindowKeys().filter(key => key.startsWith(WINDOW_PREFIX));
}
export function isPopoutWindowOpen(channelId: string) {
return PopoutWindowStore.getWindowOpen(getPopoutWindowKey(channelId));
}
export function getPersistedPopoutChannelIds() {
return settings.store.persistedPopoutWindowIds ?? [];
}
export function getOpenPopoutChannelIds() {
return getOpenPopoutWindowKeys().map(key => key.slice(WINDOW_PREFIX.length));
}
export function syncPersistedPopoutWindows() {
if (!settings.store.persistPopoutWindows) {
settings.store.persistedPopoutWindowIds = [];
return;
}
settings.store.persistedPopoutWindowIds = getOpenPopoutChannelIds();
}
export function setAlwaysOnTopForOpenPopouts(value: boolean) {
for (const windowKey of getOpenPopoutWindowKeys()) {
PopoutActions.setAlwaysOnTop(windowKey, value);
}
}

View file

@ -1,19 +0,0 @@
[class*="content"] > [class*="page"] > * {
min-width: 0;
}
.vc-sidebar-chat-popout {
align-items: stretch;
background-color: var(--background-primary);
}
.vc-sidebar-chat-window {
display: flex;
flex: 1;
min-height: 0;
min-width: 0;
overflow: hidden;
width: 100%;
background-color: var(--background-primary);
color: var(--text-default);
}