import { useEffect, useState, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { Avatar } from '@/components/channel/Avatar'; import { useRoom } from '@/contexts/room'; import { projectMembersGrouped, projectInfo, projectPresence } from '@/client/api'; import type { MemberGroup } from '@/client/model'; import type { PresenceStatus } from '@/client/model'; const STATUS_COLORS: Record = { online: 'var(--status-online)', idle: 'var(--status-idle)', dnd: 'var(--status-dnd)', offline: 'var(--status-offline)', }; const ROLE_COLORS: Record = { owner: 'var(--role-red)', admin: 'var(--role-orange)', member: 'var(--role-blue)', }; function useRoomSafe() { try { return useRoom(); } catch { return null; } } export function MemberList() { const { projectName } = useParams<{ projectName: string }>(); const room = useRoomSafe(); const [groups, setGroups] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [apiPresence, setApiPresence] = useState>(new Map()); // Fetch project presence from API useEffect(() => { if (!projectName) { setApiPresence(new Map()); return; } let cancelled = false; // First get project info to get project_id, then fetch presence projectInfo(projectName) .then((infoRes) => { if (cancelled) return; const projectId = infoRes.data?.data?.uid; if (!projectId) return; return projectPresence(projectId); }) .then((presenceRes) => { if (cancelled || !presenceRes) return; const presenceMap = new Map(); for (const p of presenceRes.data?.data || []) { presenceMap.set(p.user_id, p.status); } setApiPresence(presenceMap); }) .catch((err) => { if (cancelled) return; console.error('[MemberList] failed to load project presence:', err); setApiPresence(new Map()); }); return () => { cancelled = true; }; }, [projectName]); // Build a map of uid -> presence from room context + API presence const presenceMap = useMemo(() => { const map = new Map(); // First, add presence from room context (from WebSocket real-time updates) if (room) { for (const m of room.members) { map.set(m.uid, m.presence); } } // Then, override with API-fetched presence (authoritative data) for (const [userId, status] of apiPresence) { map.set(userId, status); } return map; }, [room, apiPresence]); useEffect(() => { if (!projectName) { setGroups([]); setTotal(0); setLoading(false); return; } let cancelled = false; setLoading(true); projectMembersGrouped(projectName) .then((res) => { if (cancelled) return; const data = res.data.data; setGroups(data?.groups ?? []); setTotal(data?.total ?? 0); }) .catch((err) => { if (cancelled) return; console.error('[MemberList] failed to load project members:', err); setGroups([]); setTotal(0); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [projectName]); // Loading state if (loading) { return (
); } // No project selected or no members if (groups.length === 0) { return (

{projectName ? 'No members' : 'Select a project'}

); } // Real project members return (
Members — {total}
{groups.map((g) => { const color = ROLE_COLORS[g.role.toLowerCase()] || 'var(--role-gray)'; return (
{g.role} — {g.members.length}
{g.members.map((m) => { const presence = presenceMap.get(m.user_id) || 'offline'; const isOffline = presence === 'offline'; return ( ); })}
); })}
); }