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:
@@ -40,6 +40,7 @@ export default function ChatPage() {
|
|||||||
const { session, logout, ensureFreshToken, refreshGuilds, updateName } = useAuth()
|
const { session, logout, ensureFreshToken, refreshGuilds, updateName } = useAuth()
|
||||||
const [selectedGuildId, setSelectedGuildId] = useState('')
|
const [selectedGuildId, setSelectedGuildId] = useState('')
|
||||||
const [selectedChannelId, setSelectedChannelId] = useState('')
|
const [selectedChannelId, setSelectedChannelId] = useState('')
|
||||||
|
const [channelMemberIds, setChannelMemberIds] = useState<string[]>([])
|
||||||
const [channels, setChannels] = useState<GuildChannel[]>([])
|
const [channels, setChannels] = useState<GuildChannel[]>([])
|
||||||
const [guildDbId, setGuildDbId] = useState('')
|
const [guildDbId, setGuildDbId] = useState('')
|
||||||
const [members, setMembers] = useState<MemberItem[]>([])
|
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() {
|
async function pullMessages() {
|
||||||
if (!selectedChannelId || !guild || !guildToken) return
|
if (!selectedChannelId || !guild || !guildToken) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -242,6 +257,7 @@ export default function ChatPage() {
|
|||||||
await guildApi().post(`/channels/${c.id}/join`)
|
await guildApi().post(`/channels/${c.id}/join`)
|
||||||
await loadChannels()
|
await loadChannels()
|
||||||
await loadMembers()
|
await loadMembers()
|
||||||
|
await loadChannelMembers()
|
||||||
} catch {
|
} catch {
|
||||||
setError('Failed to join channel')
|
setError('Failed to join channel')
|
||||||
}
|
}
|
||||||
@@ -257,6 +273,7 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
await loadChannels()
|
await loadChannels()
|
||||||
await loadMembers()
|
await loadMembers()
|
||||||
|
await loadChannelMembers()
|
||||||
} catch {
|
} catch {
|
||||||
setError('Failed to leave channel')
|
setError('Failed to leave channel')
|
||||||
}
|
}
|
||||||
@@ -290,6 +307,7 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void pullMessages()
|
void pullMessages()
|
||||||
|
void loadChannelMembers()
|
||||||
}, [selectedChannelId])
|
}, [selectedChannelId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -318,6 +336,23 @@ export default function ChatPage() {
|
|||||||
const authorLabel = (uid?: string) =>
|
const authorLabel = (uid?: string) =>
|
||||||
uid ? (uid === session?.user.id ? session?.user.name || 'You' : nameById.get(uid) || uid.slice(0, 8)) : 'unknown'
|
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 (
|
return (
|
||||||
<div className="dc-shell">
|
<div className="dc-shell">
|
||||||
<nav className="dc-rail">
|
<nav className="dc-rail">
|
||||||
@@ -479,18 +514,25 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
{showMembers ? (
|
{showMembers ? (
|
||||||
<aside className="dc-members">
|
<aside className="dc-members">
|
||||||
<div className="dc-section-label">
|
{currentChannel ? (
|
||||||
<span>Members — {members.length}</span>
|
<>
|
||||||
</div>
|
<div className="dc-section-label">
|
||||||
{members.map((m) => (
|
<span>In channel — {channelMembers.length}</span>
|
||||||
<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}
|
|
||||||
</div>
|
</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>
|
</aside>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user