feat: relocate MemberList into unified sidebar as collapsible section

Mount the MemberList component below the channel list in channel mode,
with admin callbacks (kick, ban, role change) and visibility toggled
via the existing ChatHeader member list button / uiStore subscription.
Tracked as channelModeExtras for proper cleanup on mode switch.
This commit is contained in:
jevb
2026-03-27 12:29:44 +01:00
parent 0aeaa50856
commit f4df18d5be
2 changed files with 86 additions and 0 deletions
@@ -12,6 +12,7 @@ import type { ApiClient } from "@lib/api";
import type { RateLimiterSet } from "@lib/rate-limiter";
import type { ToastContainer } from "@components/Toast";
import { createChannelSidebar } from "@components/ChannelSidebar";
import { createMemberList } from "@components/MemberList";
import { createDmSidebar } from "@components/DmSidebar";
import { createCreateChannelModal } from "@components/CreateChannelModal";
import { createEditChannelModal } from "@components/EditChannelModal";
@@ -74,6 +75,10 @@ export function createSidebarArea(opts: SidebarAreaOptions): SidebarAreaResult {
// Track invite controller cleanup (recreated on each channels mount)
let inviteCleanup: (() => void) | null = null;
// Track extra channel-mode components (member list) for cleanup on mode switch
let channelModeExtras: MountableComponent[] = [];
let channelModeUnsubs: Array<() => void> = [];
// Profile manager for quick-switch overlay
let profileManager: ProfileManager | null = null;
@@ -259,6 +264,16 @@ export function createSidebarArea(opts: SidebarAreaOptions): SidebarAreaResult {
inviteCleanup();
inviteCleanup = null;
}
// Clean up channel-mode extras (member list, subscriptions)
for (const comp of channelModeExtras) {
comp.destroy?.();
}
channelModeExtras = [];
for (const unsub of channelModeUnsubs) {
unsub();
}
channelModeUnsubs = [];
clearChildren(contentSlot);
const innerSlot = createElement("div", { style: "flex:1;overflow:hidden;display:flex;flex-direction:column;" });
@@ -292,6 +307,61 @@ export function createSidebarArea(opts: SidebarAreaOptions): SidebarAreaResult {
sidebarHeader.appendChild(inviteBtn);
inviteCleanup = () => { inviteCtrl.cleanup(); };
}
// --- Member list (below channels) ---
const memberListContainer = createElement("div", {
class: "sidebar-members-section",
"data-testid": "sidebar-members",
});
// Respect initial visibility state
const initialMemberVis = uiStore.getState().memberListVisible;
memberListContainer.style.display = initialMemberVis ? "" : "none";
const memberList = createMemberList({
currentUserRole: authStore.getState().user?.role ?? "member",
onKick: async (userId, username) => {
try {
await api.adminKickMember(userId);
getToast()?.show(`Kicked ${username}`, "success");
} catch (err) {
const msg = err instanceof Error ? err.message : "Failed to kick member";
getToast()?.show(msg, "error");
}
},
onBan: async (userId, username) => {
try {
await api.adminBanMember(userId);
getToast()?.show(`Banned ${username}`, "success");
} catch (err) {
const msg = err instanceof Error ? err.message : "Failed to ban member";
getToast()?.show(msg, "error");
}
},
onChangeRole: async (userId, username, newRole) => {
const roleNameToId: Record<string, number> = { owner: 1, admin: 2, moderator: 3, member: 4 };
const roleId = roleNameToId[newRole];
if (roleId === undefined) return;
try {
await api.adminChangeRole(userId, roleId);
getToast()?.show(`Changed ${username}'s role to ${newRole}`, "success");
} catch (err) {
const msg = err instanceof Error ? err.message : "Failed to change role";
getToast()?.show(msg, "error");
}
},
});
memberList.mount(memberListContainer);
contentSlot.appendChild(memberListContainer);
channelModeExtras.push(memberList);
// Toggle member list visibility from ChatHeader button
const unsubMemberVis = uiStore.subscribeSelector(
(s) => s.memberListVisible,
(visible) => {
memberListContainer.style.display = visible ? "" : "none";
},
);
channelModeUnsubs.push(unsubMemberVis);
} else {
const dmSidebar = buildDmSidebar();
dmSidebar.mount(innerSlot);
@@ -409,6 +479,14 @@ export function createSidebarArea(opts: SidebarAreaOptions): SidebarAreaResult {
inviteCleanup();
inviteCleanup = null;
}
for (const comp of channelModeExtras) {
comp.destroy?.();
}
channelModeExtras = [];
for (const unsub of channelModeUnsubs) {
unsub();
}
channelModeUnsubs = [];
});
unsubscribers.push(() => {
+8
View File
@@ -834,6 +834,14 @@
transition: width .2s;
}
.member-list.hidden { width: 0; padding: 0; overflow: hidden; }
.sidebar-members-section {
border-top: 1px solid var(--border-glow, var(--border));
overflow-y: auto;
max-height: 200px;
}
.sidebar-members-section .member-list {
width: 100%;
}
.member-role-group {
padding: 16px 16px 4px;
font-size: 11px; font-weight: 700;