feat(frontend): split members sidebar into channel + guild sections

When a channel is selected the Members panel shows 'In channel — N'
(that channel's members) above 'Guild — N' (all guild members).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-15 15:00:24 +01:00
parent b04b099754
commit ab31afa13d

View File

@@ -40,6 +40,7 @@ export default function ChatPage() {
const { session, logout, ensureFreshToken, refreshGuilds, updateName } = useAuth()
const [selectedGuildId, setSelectedGuildId] = useState('')
const [selectedChannelId, setSelectedChannelId] = useState('')
const [channelMemberIds, setChannelMemberIds] = useState<string[]>([])
const [channels, setChannels] = useState<GuildChannel[]>([])
const [guildDbId, setGuildDbId] = useState('')
const [members, setMembers] = useState<MemberItem[]>([])
@@ -134,6 +135,20 @@ export default function ChatPage() {
}
}
async function loadChannelMembers() {
if (!guild || !guildToken || !selectedChannelId) {
setChannelMemberIds([])
return
}
try {
const res = await guildApi().get(`/channels/${selectedChannelId}/members`)
const list = Array.isArray(res.data) ? (res.data as Array<{ userId: string }>) : []
setChannelMemberIds(list.map((x) => x.userId))
} catch {
setChannelMemberIds([])
}
}
async function pullMessages() {
if (!selectedChannelId || !guild || !guildToken) return
setLoading(true)
@@ -242,6 +257,7 @@ export default function ChatPage() {
await guildApi().post(`/channels/${c.id}/join`)
await loadChannels()
await loadMembers()
await loadChannelMembers()
} catch {
setError('Failed to join channel')
}
@@ -257,6 +273,7 @@ export default function ChatPage() {
}
await loadChannels()
await loadMembers()
await loadChannelMembers()
} catch {
setError('Failed to leave channel')
}
@@ -290,6 +307,7 @@ export default function ChatPage() {
useEffect(() => {
void pullMessages()
void loadChannelMembers()
}, [selectedChannelId])
useEffect(() => {
@@ -318,6 +336,23 @@ export default function ChatPage() {
const authorLabel = (uid?: string) =>
uid ? (uid === session?.user.id ? session?.user.name || 'You' : nameById.get(uid) || uid.slice(0, 8)) : 'unknown'
const guildById = new Map(members.map((m) => [m.userId, m]))
const channelMembers = channelMemberIds.map(
(id) => guildById.get(id) ?? { userId: id, email: '', name: '', status: 'active' },
)
const renderMember = (m: { userId: string; name?: string; email?: string }) => {
const label = m.name || m.email || m.userId.slice(0, 8)
return (
<div key={m.userId} className="member-row">
<div className="avatar dot">{initials(label)}</div>
<div style={{ minWidth: 0 }}>
<span className="nm">{label}</span>
{m.userId === session?.user.id ? <span className="you">you</span> : null}
</div>
</div>
)
}
return (
<div className="dc-shell">
<nav className="dc-rail">
@@ -479,18 +514,25 @@ export default function ChatPage() {
{showMembers ? (
<aside className="dc-members">
<div className="dc-section-label">
<span>Members {members.length}</span>
</div>
{members.map((m) => (
<div key={m.userId} className="member-row">
<div className="avatar dot">{initials(m.name || m.email)}</div>
<div style={{ minWidth: 0 }}>
<span className="nm">{m.name || m.email}</span>
{m.userId === session?.user.id ? <span className="you">you</span> : null}
{currentChannel ? (
<>
<div className="dc-section-label">
<span>In channel {channelMembers.length}</span>
</div>
</div>
))}
{channelMembers.map(renderMember)}
<div className="dc-section-label" style={{ marginTop: 14 }}>
<span>Guild {members.length}</span>
</div>
{members.map(renderMember)}
</>
) : (
<>
<div className="dc-section-label">
<span>Members {members.length}</span>
</div>
{members.map(renderMember)}
</>
)}
</aside>
) : null}