import React, { useState } from 'react'; import { Avatar } from '../../ui/Avatar'; import { ConfirmDialog } from '../../ui/ConfirmDialog'; import { useSpaceStore, getApiForOrigin } from '../../../stores/spaceStore'; import { useAuthStore } from '../../../stores/authStore'; import { parseFederatedUsername, isFederationGlobeApplicable } from '../../../utils/identity'; import { useCanonicalUserView } from '../../../utils/userViewLookup'; import { hasPermissionBit, PermissionBits } from '../../../utils/permissions'; import type { MemberWithUser, Role } from '@backspace/shared'; function MembersPanelRow({ member, spaceId, ownerId, isExpanded, expandable, canKick, canBan, currentUserId, assignableRoles, memberRoleIds, hasPendingChanges, onToggleExpand, onRoleToggle, onSaveRoles, onCancelRoleChange, onPendingAction, }: { member: MemberWithUser; spaceId: string; ownerId: string | undefined; isExpanded: boolean; expandable: boolean; canKick: boolean; canBan: boolean; currentUserId: string | undefined; assignableRoles: Role[]; memberRoleIds: Set; hasPendingChanges: boolean; onToggleExpand: (userId: string) => void; onRoleToggle: (userId: string, roleId: string, currentRoleIds: Set) => void; onSaveRoles: (userId: string) => void; onCancelRoleChange: (userId: string) => void; onPendingAction: (action: { type: 'kick' | 'ban'; userId: string; displayName: string }) => void; }) { const canonical = useCanonicalUserView(member.user); const isOwner = member.userId === ownerId; const displayName = canonical.displayName ?? canonical.username; return (
{ if (expandable) onToggleExpand(member.userId); }} >
{displayName} {isFederationGlobeApplicable(canonical) && ( @{parseFederatedUsername(canonical.username).domain} )}
{isOwner && ( Owner )} {member.roles?.filter((r) => r.id !== spaceId).map((r) => ( {r.name} ))} {!isOwner && (!member.roles || member.roles.filter((r) => r.id !== spaceId).length === 0) && ( No roles )}
{canBan && member.userId !== currentUserId && !isOwner && ( )} {canKick && member.userId !== currentUserId && !isOwner && ( )} {expandable && ( )}
{/* Role checkboxes — only shown when expanded */} {isExpanded && expandable && (
{assignableRoles.map((role) => ( ))} {hasPendingChanges && (
)}
)}
); } interface MembersPanelProps { spaceId: string; } export function MembersPanel({ spaceId }: MembersPanelProps) { const spaces = useSpaceStore((s) => s.spaces); const members = useSpaceStore((s) => s.members); const roles = useSpaceStore((s) => s.roles); const loadSpaceDetail = useSpaceStore((s) => s.loadSpaceDetail); const currentUser = useAuthStore((s) => s.user); const spacePermissions = useSpaceStore((s) => s.spacePermissions); const space = spaces.find((s) => s.id === spaceId); const spaceApi = getApiForOrigin(space?._instanceOrigin ?? ''); const myPerms = spacePermissions.get(spaceId); const canManageRoles = hasPermissionBit(myPerms, PermissionBits.MANAGE_ROLES); const canKick = hasPermissionBit(myPerms, PermissionBits.KICK_MEMBERS); const canBan = hasPermissionBit(myPerms, PermissionBits.BAN_MEMBERS); const [pendingRoleChanges, setPendingRoleChanges] = useState>>(new Map()); const [expandedMemberId, setExpandedMemberId] = useState(null); const [error, setError] = useState(''); const [pendingAction, setPendingAction] = useState<{ type: 'kick' | 'ban'; userId: string; displayName: string } | null>(null); // Assignable roles: exclude @everyone (where role.id === spaceId) const assignableRoles = roles.filter((r) => r.id !== spaceId); if (!space) return null; const getMemberRoleIds = (member: MemberWithUser): Set => { const pending = pendingRoleChanges.get(member.userId); if (pending) return pending; return new Set(member.roles?.map((r) => r.id) ?? []); }; const handleRoleToggle = (userId: string, roleId: string, currentRoleIds: Set) => { const updated = new Set(currentRoleIds); if (updated.has(roleId)) { updated.delete(roleId); } else { updated.add(roleId); } setPendingRoleChanges((prev) => new Map(prev).set(userId, updated)); }; const handleSaveRoles = async (userId: string) => { const roleIds = pendingRoleChanges.get(userId); if (!roleIds) return; try { await spaceApi.spaces.updateMember(spaceId, userId, { roleIds: Array.from(roleIds) }); setPendingRoleChanges((prev) => { const next = new Map(prev); next.delete(userId); return next; }); setExpandedMemberId(null); await loadSpaceDetail(spaceId); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update roles'); } }; const handleCancelRoleChange = (userId: string) => { setPendingRoleChanges((prev) => { const next = new Map(prev); next.delete(userId); return next; }); }; const handleKick = async (userId: string) => { try { await spaceApi.spaces.removeMember(spaceId, userId); await loadSpaceDetail(spaceId); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to kick member'); } }; const handleBan = async (userId: string) => { try { await spaceApi.spaces.ban(spaceId, userId); await loadSpaceDetail(spaceId); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to ban member'); } }; const canExpandMember = (member: MemberWithUser) => canManageRoles && member.userId !== currentUser?.id && member.userId !== space.ownerId && assignableRoles.length > 0; return (

Members

{error && (
{error}
)}

Manage members of this space. Click a member to edit their roles.

Members ({members.length})
{members.map((member) => ( setExpandedMemberId(expandedMemberId === uid ? null : uid)} onRoleToggle={handleRoleToggle} onSaveRoles={handleSaveRoles} onCancelRoleChange={handleCancelRoleChange} onPendingAction={setPendingAction} /> ))}
setPendingAction(null)} onConfirm={async () => { if (!pendingAction) return; if (pendingAction.type === 'kick') { await handleKick(pendingAction.userId); } else { await handleBan(pendingAction.userId); } setPendingAction(null); }} title={pendingAction?.type === 'ban' ? `Ban ${pendingAction.displayName}` : `Kick ${pendingAction?.displayName ?? ''}`} description={ pendingAction?.type === 'ban' ? 'They will be permanently banned from this space until unbanned.' : 'They can rejoin with an invite link.' } variant={pendingAction?.type === 'ban' ? 'danger' : 'warning'} confirmLabel={pendingAction?.type === 'ban' ? 'Ban' : 'Kick'} />
); }