240 lines
7.5 KiB
TypeScript
240 lines
7.5 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useChatStore } from '../store/chatStore';
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
|
import { apiClient } from '@/services/api/client';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { cn } from '@/lib/utils';
|
|
import { Loader2, Plus, Trash2, LogOut } from 'lucide-react';
|
|
import { CreateRoomDialog } from './CreateRoomDialog';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { MoreVertical } from 'lucide-react';
|
|
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog';
|
|
|
|
// FE-PAGE-005: Complete Chat page implementation - Room Management
|
|
|
|
interface ConversationItemProps {
|
|
conversation: { id: string; name: string; type: string };
|
|
onSelect: (id: string) => void;
|
|
isSelected: boolean;
|
|
}
|
|
|
|
const ConversationItem: React.FC<ConversationItemProps> = ({
|
|
conversation,
|
|
onSelect,
|
|
isSelected,
|
|
}) => {
|
|
const { user } = useAuthStore();
|
|
const queryClient = useQueryClient();
|
|
const toast = useToast();
|
|
const { setCurrentConversation } = useChatStore();
|
|
const [showLeaveDialog, setShowLeaveDialog] = useState(false);
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
|
|
const leaveRoomMutation = useMutation({
|
|
mutationFn: async (roomId: string) => {
|
|
await apiClient.delete(`/conversations/${roomId}/participants/${user?.id}`);
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['chatConversations', user?.id] });
|
|
toast.success('Left room successfully');
|
|
setCurrentConversation(null);
|
|
setShowLeaveDialog(false);
|
|
},
|
|
onError: (error: any) => {
|
|
toast.error(error.response?.data?.error || 'Failed to leave room');
|
|
},
|
|
});
|
|
|
|
const deleteRoomMutation = useMutation({
|
|
mutationFn: async (roomId: string) => {
|
|
await apiClient.delete(`/conversations/${roomId}`);
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['chatConversations', user?.id] });
|
|
toast.success('Room deleted successfully');
|
|
setCurrentConversation(null);
|
|
setShowDeleteDialog(false);
|
|
},
|
|
onError: (error: any) => {
|
|
toast.error(error.response?.data?.error || 'Failed to delete room');
|
|
},
|
|
});
|
|
|
|
const handleLeave = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
setShowLeaveDialog(true);
|
|
};
|
|
|
|
const handleDelete = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
setShowDeleteDialog(true);
|
|
};
|
|
|
|
const confirmLeave = () => {
|
|
leaveRoomMutation.mutate(conversation.id);
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
deleteRoomMutation.mutate(conversation.id);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
onClick={() => onSelect(conversation.id)}
|
|
className={cn(
|
|
'flex items-center justify-between p-3 rounded-lg cursor-pointer transition-colors group',
|
|
isSelected ? 'bg-blue-100 text-blue-800' : 'hover:bg-gray-100',
|
|
)}
|
|
>
|
|
<span className="font-medium flex-1 truncate">
|
|
{conversation.name || `Conversation ${conversation.id.substring(0, 8)}`}
|
|
</span>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
|
|
>
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={handleLeave}>
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
Leave Room
|
|
</DropdownMenuItem>
|
|
{conversation.type !== 'direct' && (
|
|
<DropdownMenuItem onClick={handleDelete} className="text-destructive">
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Delete Room
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
<ConfirmationDialog
|
|
open={showLeaveDialog}
|
|
onClose={() => setShowLeaveDialog(false)}
|
|
onConfirm={confirmLeave}
|
|
title="Leave Room"
|
|
description="Are you sure you want to leave this room? You will no longer receive messages from this conversation."
|
|
confirmLabel="Leave"
|
|
cancelLabel="Cancel"
|
|
variant="default"
|
|
isLoading={leaveRoomMutation.isPending}
|
|
/>
|
|
<ConfirmationDialog
|
|
open={showDeleteDialog}
|
|
onClose={() => setShowDeleteDialog(false)}
|
|
onConfirm={confirmDelete}
|
|
title="Delete Room"
|
|
description="Are you sure you want to delete this room? This action cannot be undone. All messages and participants will be removed."
|
|
confirmLabel="Delete"
|
|
cancelLabel="Cancel"
|
|
variant="destructive"
|
|
isLoading={deleteRoomMutation.isPending}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const ChatSidebar: React.FC = () => {
|
|
const { user } = useAuthStore();
|
|
const userId = user?.id;
|
|
const {
|
|
conversations,
|
|
currentConversationId,
|
|
setCurrentConversation,
|
|
addConversation,
|
|
} = useChatStore();
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
|
|
// Fetch conversations from backend
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ['chatConversations', userId],
|
|
queryFn: async () => {
|
|
if (!userId) return [];
|
|
const response = await apiClient.get('/conversations');
|
|
return response.data.conversations;
|
|
},
|
|
enabled: !!userId,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (data) {
|
|
data.forEach((conv: any) =>
|
|
addConversation({
|
|
id: conv.id,
|
|
name: conv.name,
|
|
type: conv.type,
|
|
participants: conv.participants,
|
|
unread_count: 0, // Default for now
|
|
}),
|
|
);
|
|
}
|
|
}, [data, addConversation]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="w-64 border-r bg-gray-50 flex items-center justify-center">
|
|
<Loader2 className="animate-spin text-blue-500" size={24} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="w-64 border-r bg-gray-50 flex items-center justify-center text-red-500 p-4">
|
|
Erreur:{' '}
|
|
{(error as any).message || 'Impossible de charger les conversations'}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-64 border-r bg-gray-50 flex flex-col">
|
|
<div className="p-4 border-b">
|
|
<h2 className="text-xl font-bold">Conversations</h2>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto p-2">
|
|
{conversations.length === 0 ? (
|
|
<div className="text-gray-500 text-sm p-2">
|
|
Aucune conversation. Créez-en une !
|
|
</div>
|
|
) : (
|
|
conversations.map((conv) => (
|
|
<ConversationItem
|
|
key={conv.id}
|
|
conversation={conv}
|
|
onSelect={setCurrentConversation}
|
|
isSelected={conv.id === currentConversationId}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
<div className="p-4 border-t">
|
|
<Button
|
|
onClick={() => setIsCreateDialogOpen(true)}
|
|
className="w-full"
|
|
variant="default"
|
|
>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Nouvelle Conversation
|
|
</Button>
|
|
</div>
|
|
<CreateRoomDialog
|
|
open={isCreateDialogOpen}
|
|
onClose={() => setIsCreateDialogOpen(false)}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|