From 81b1891b92a7c9bbfe7bfd960dc2a0af31ec0266 Mon Sep 17 00:00:00 2001
From: Jake <134332787+justjxke@users.noreply.github.com>
Date: Tue, 21 Apr 2026 17:14:42 +0100
Subject: [PATCH] feat: minichat (#983)
* feat: add MiniChat Equicord port
* feat: merge MiniChat into SidebarChat
* Update index.tsx
---------
Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
---
src/equicordplugins/sidebarChat/index.tsx | 328 ++++++++++++++++++---
src/equicordplugins/sidebarChat/store.ts | 66 ++++-
src/equicordplugins/sidebarChat/styles.css | 11 +-
3 files changed, 368 insertions(+), 37 deletions(-)
diff --git a/src/equicordplugins/sidebarChat/index.tsx b/src/equicordplugins/sidebarChat/index.tsx
index 2958acea9..9beaad8ae 100644
--- a/src/equicordplugins/sidebarChat/index.tsx
+++ b/src/equicordplugins/sidebarChat/index.tsx
@@ -9,7 +9,7 @@ import "./styles.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { HeaderBarButton } from "@api/HeaderBar";
import ErrorBoundary from "@components/ErrorBoundary";
-import { Devs } from "@utils/constants";
+import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { getCurrentChannel } from "@utils/discord";
import definePlugin from "@utils/types";
@@ -23,6 +23,7 @@ import {
findStoreLazy
} from "@webpack";
import {
+ ChannelActionCreators,
ChannelRouter,
ChannelStore,
FluxDispatcher,
@@ -33,6 +34,7 @@ import {
PermissionsBits,
PermissionStore,
PopoutActions,
+ PopoutWindowStore,
RelationshipStore,
SelectedChannelStore,
SelectedGuildStore,
@@ -44,7 +46,15 @@ import {
useStateFromStores
} from "@webpack/common";
-import { settings, SidebarStore } from "./store";
+import {
+ getOpenPopoutWindowKeys,
+ getPersistedPopoutChannelIds,
+ getPopoutWindowKey,
+ isPopoutWindowOpen,
+ settings,
+ SidebarStore,
+ syncPersistedPopoutWindows
+} from "./store";
const cl = classNameFactory("vc-sidebar-chat-");
@@ -80,17 +90,159 @@ const ChatClasses = findCssClassesLazy("threadSidebarOpen", "loader");
const ChannelSectionStore = findStoreLazy("ChannelSectionStore");
-const requireChannelContextMenu = extractAndLoadChunksLazy(
- ["&&this.handleActivitiesPopoutClose(),"],
- new RegExp(DefaultExtractAndLoadChunksRegex.source + ".{1,250}hasActiveThread")
-);
-
const requireForumView = extractAndLoadChunksLazy(
["Missing channel in Channel.renderHeaderToolbar"],
new RegExp(DefaultExtractAndLoadChunksRegex.source + '.{1,150}name:"ForumChannel"')
);
-const MakeContextMenu = (id: string, guildId: string | null) => {
+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,
+ () =>