veza/apps/web/src/features/chat/components/RoomMembersModal.tsx

198 lines
6.4 KiB
TypeScript
Raw Normal View History

2026-03-06 17:52:08 +00:00
import React, { useEffect, useState } from 'react';
import { Dialog } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog';
2026-03-06 17:58:37 +00:00
import { getRoomMembers, kickMember, leaveConversation } from '../services/conversationService';
2026-03-06 17:52:08 +00:00
import type { RoomMemberWithRole } from '../services/conversationService';
import { useChatStore } from '../store/chatStore';
import { useToast } from '@/hooks/useToast';
2026-03-06 17:58:37 +00:00
import { UserMinus, Loader2, Crown, Shield, User, LogOut } from 'lucide-react';
2026-03-06 17:52:08 +00:00
interface RoomMembersModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
conversationId: string;
}
function RoleBadge({ role }: { role: string }) {
const config =
role === 'owner'
? { icon: Crown, label: 'Propriétaire', className: 'text-amber-500' }
: role === 'admin'
? { icon: Shield, label: 'Admin', className: 'text-primary' }
: { icon: User, label: 'Membre', className: 'text-muted-foreground' };
const Icon = config.icon;
return (
<span
className={`inline-flex items-center gap-1 text-xs ${config.className}`}
title={config.label}
>
<Icon className="h-3 w-3" />
{config.label}
</span>
);
}
/**
* RoomMembersModal (v0.9.7): List members with roles, kick for owner/admin
*/
export function RoomMembersModal({
open,
onOpenChange,
conversationId,
}: RoomMembersModalProps) {
const { userId } = useChatStore();
const [members, setMembers] = useState<RoomMemberWithRole[]>([]);
const [myRole, setMyRole] = useState<string>('');
const [loading, setLoading] = useState(true);
const [kicking, setKicking] = useState<string | null>(null);
2026-03-06 17:58:37 +00:00
const [leaving, setLeaving] = useState(false);
2026-03-06 17:52:08 +00:00
const [pendingKick, setPendingKick] = useState<{ userId: string; username: string } | null>(null);
2026-03-06 17:58:37 +00:00
const [pendingLeave, setPendingLeave] = useState(false);
2026-03-06 17:52:08 +00:00
const { success: showSuccess, error: showError } = useToast();
const canKick = myRole === 'owner' || myRole === 'admin';
useEffect(() => {
if (!open || !conversationId) return;
setLoading(true);
getRoomMembers(conversationId)
.then((res) => {
setMembers(res.members);
setMyRole(res.my_role);
})
.catch(() => setMembers([]))
.finally(() => setLoading(false));
}, [open, conversationId]);
const handleKickClick = (m: RoomMemberWithRole) => {
setPendingKick({ userId: m.user_id, username: m.username });
};
const handleKickConfirm = async () => {
if (!pendingKick || !canKick || pendingKick.userId === userId) return;
setKicking(pendingKick.userId);
setPendingKick(null);
try {
await kickMember(conversationId, pendingKick.userId);
setMembers((prev) => prev.filter((m) => m.user_id !== pendingKick.userId));
showSuccess('Membre exclu');
} catch (err) {
showError(err instanceof Error ? err.message : 'Impossible d\'exclure');
} finally {
setKicking(null);
}
};
2026-03-06 17:58:37 +00:00
const handleLeaveClick = () => setPendingLeave(true);
const handleLeaveConfirm = async () => {
if (!pendingLeave) return;
setLeaving(true);
setPendingLeave(false);
try {
await leaveConversation(conversationId);
showSuccess('Vous avez quitté la conversation');
onOpenChange(false);
useChatStore.getState().setCurrentConversation(null);
} catch (err) {
showError(err instanceof Error ? err.message : 'Impossible de quitter');
} finally {
setLeaving(false);
}
};
2026-03-06 17:52:08 +00:00
return (
<Dialog
open={open}
onOpenChange={onOpenChange}
title="Membres"
size="md"
2026-03-06 17:58:37 +00:00
footer={
!loading && (
<Button
variant="outline"
onClick={handleLeaveClick}
disabled={leaving}
className="text-destructive border-destructive/30 hover:bg-destructive/10 hover:border-destructive/50"
aria-label="Quitter la conversation"
>
{leaving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<LogOut className="mr-2 h-4 w-4" />
Quitter la conversation
</>
)}
</Button>
)
}
2026-03-06 17:52:08 +00:00
showCancel={false}
>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<ul className="space-y-2">
{members.map((m) => (
<li
key={m.user_id}
className="flex items-center justify-between gap-4 py-2 px-3 rounded-lg hover:bg-muted/50"
>
<div className="flex flex-col min-w-0">
<span className="font-medium truncate">{m.username}</span>
<RoleBadge role={m.role} />
</div>
{canKick &&
m.user_id !== userId &&
m.role !== 'owner' && (
<Button
variant="ghost"
size="sm"
onClick={() => handleKickClick(m)}
disabled={!!kicking}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
aria-label={`Exclure ${m.username}`}
>
{kicking === m.user_id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<UserMinus className="h-4 w-4" />
)}
</Button>
)}
</li>
))}
</ul>
)}
<ConfirmationDialog
open={!!pendingKick}
onClose={() => setPendingKick(null)}
onConfirm={handleKickConfirm}
title="Exclure un membre"
description={
pendingKick
? `Êtes-vous sûr de vouloir exclure ${pendingKick.username} du canal ?`
: ''
}
confirmLabel="Exclure"
variant="destructive"
isLoading={!!kicking}
/>
2026-03-06 17:58:37 +00:00
<ConfirmationDialog
open={pendingLeave}
onClose={() => setPendingLeave(false)}
onConfirm={handleLeaveConfirm}
title="Quitter la conversation"
description="Vous ne recevrez plus de messages de ce canal. Vous pourrez le rejoindre à nouveau via une invitation."
confirmLabel="Quitter"
variant="destructive"
isLoading={leaving}
/>
2026-03-06 17:52:08 +00:00
</Dialog>
);
}