import React, { useEffect, useState } from 'react'; import { useChatStore } from '../store/chatStore'; import { useUser } from '@/features/auth/hooks/useUser'; import { apiClient } from '@/services/api/client'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { cn } from '@/lib/utils'; import { Loader2, Plus, Trash2, LogOut, MessageSquare, Hash, User, MoreVertical, } from 'lucide-react'; import { CreateRoomDialog } from './CreateRoomDialog'; import { useToast } from '@/hooks/useToast'; import { Button } from '@/components/ui/button'; import { ErrorDisplay } from '@/components/ui/ErrorDisplay'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'; interface ConversationItemProps { conversation: { id: string; name: string; type: string; unread_count?: number; }; onSelect: (id: string) => void; isSelected: boolean; } const ConversationItem: React.FC = ({ conversation, onSelect, isSelected, }) => { const { data: user } = useUser(); const queryClient = useQueryClient(); const toast = useToast(); const { setCurrentConversation } = useChatStore(); const [showLeaveDialog, setShowLeaveDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [mutationError, setMutationError] = useState(null); const [retryCount, setRetryCount] = useState(0); const [lastMutationType, setLastMutationType] = useState< 'leave' | 'delete' | null >(null); const [lastRoomId, setLastRoomId] = useState(null); // Action 4.4.1.5: Add optimistic update const leaveRoomMutation = useMutation({ mutationFn: async (roomId: string) => { await apiClient.delete( `/conversations/${roomId}/participants/${user?.id}`, ); }, onMutate: async (roomId: string) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ['chatConversations', user?.id], }); // Snapshot previous value const previousConversations = queryClient.getQueryData( ['chatConversations', user?.id], ); // Optimistically remove conversation from list if (previousConversations) { queryClient.setQueryData( ['chatConversations', user?.id], previousConversations.filter((c) => c.id !== roomId), ); } return { previousConversations }; }, onError: (error: any, _roomId, context) => { // Rollback on error if (context?.previousConversations) { queryClient.setQueryData( ['chatConversations', user?.id], context.previousConversations, ); } const errorMessage = error.response?.data?.error || 'Failed to leave room'; setMutationError(new Error(errorMessage)); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['chatConversations', user?.id], }); toast.success('Left room successfully'); setCurrentConversation(null); setShowLeaveDialog(false); setMutationError(null); }, }); // Action 4.4.1.5: Add optimistic update const deleteRoomMutation = useMutation({ mutationFn: async (roomId: string) => { await apiClient.delete(`/conversations/${roomId}`); }, onMutate: async (roomId: string) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ['chatConversations', user?.id], }); // Snapshot previous value const previousConversations = queryClient.getQueryData( ['chatConversations', user?.id], ); // Optimistically remove conversation from list if (previousConversations) { queryClient.setQueryData( ['chatConversations', user?.id], previousConversations.filter((c) => c.id !== roomId), ); } return { previousConversations }; }, onError: (error: any, _roomId, context) => { // Rollback on error if (context?.previousConversations) { queryClient.setQueryData( ['chatConversations', user?.id], context.previousConversations, ); } const errorMessage = error.response?.data?.error || 'Failed to delete room'; setMutationError(new Error(errorMessage)); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['chatConversations', user?.id], }); toast.success('Room deleted successfully'); setCurrentConversation(null); setShowDeleteDialog(false); setMutationError(null); setRetryCount(0); setLastMutationType(null); setLastRoomId(null); }, }); const handleLeave = (e: React.MouseEvent) => { e.stopPropagation(); setShowLeaveDialog(true); }; const handleDelete = (e: React.MouseEvent) => { e.stopPropagation(); setShowDeleteDialog(true); }; const confirmLeave = () => { setLastMutationType('leave'); setLastRoomId(conversation.id); leaveRoomMutation.mutate(conversation.id); }; const confirmDelete = () => { setLastMutationType('delete'); setLastRoomId(conversation.id); deleteRoomMutation.mutate(conversation.id); }; // Action 3.4.1.3: Retry handler for failed mutations const handleRetry = async () => { if (!lastMutationType || !lastRoomId || retryCount >= 3) return; setRetryCount((prev) => prev + 1); try { if (lastMutationType === 'leave') { await leaveRoomMutation.mutateAsync(lastRoomId); } else { await deleteRoomMutation.mutateAsync(lastRoomId); } } catch (error) { // Error will be handled by mutation's onError } }; return ( <> {mutationError && ( { setMutationError(null); setRetryCount(0); setLastMutationType(null); setLastRoomId(null); }} /> )}
onSelect(conversation.id)} className={cn( 'group relative flex items-center justify-between p-3 rounded-xl cursor-pointer transition-all duration-300 border border-transparent', isSelected ? 'bg-kodo-cyan/10 border-kodo-cyan/30 shadow-[0_0_15px_rgba(102,252,241,0.1)]' : 'hover:bg-white/5 hover:border-white/5', )} >
{conversation.type === 'direct' ? ( ) : ( )}
{conversation.name || `Channel ${conversation.id.substring(0, 4)}`} {conversation.type !== 'direct' && ( {conversation.type} )}
{conversation.unread_count && conversation.unread_count > 0 ? ( {conversation.unread_count} ) : null} e.stopPropagation()}> Leave Channel {conversation.type !== 'direct' && ( Delete Channel )} {/* Active Indicator Line */} {isSelected && (
)}
setShowLeaveDialog(false)} onConfirm={confirmLeave} title="Leave Channel" description="Disconnect from this secure frequency? Incoming transmission will cease." confirmLabel="Disconnect" cancelLabel="Cancel" variant="default" isLoading={leaveRoomMutation.isPending} /> setShowDeleteDialog(false)} onConfirm={confirmDelete} title="Delete Channel" description="Permanently purge this channel from the network? This action is irreversible." confirmLabel="Purge" cancelLabel="Cancel" variant="destructive" isLoading={deleteRoomMutation.isPending} /> ); }; export const ChatSidebar: React.FC = () => { const { data: user } = useUser(); const userId = user?.id; const queryClient = useQueryClient(); const { conversations, currentConversationId, setCurrentConversation, addConversation, } = useChatStore(); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); 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) => { if (!conversations.some((c) => c.id === conv.id)) { addConversation({ id: conv.id, name: conv.name, type: conv.type, participants: conv.participants, unread_count: 0, }); } }); } }, [data, conversations, addConversation]); if (isLoading) { return (
); } if (error) { return (
queryClient.invalidateQueries({ queryKey: ['chatConversations', userId], }) } />
); } return (

Active Channels

{conversations.length}
{conversations.length === 0 ? (
No active frequencies detected.
Initialize a new channel.
) : ( conversations.map((conv) => ( )) )}
setIsCreateDialogOpen(false)} />
); };