2025-12-24 11:51:40 +00:00
|
|
|
import React, { useEffect, useState } from 'react';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { useChatStore } from '../store/chatStore';
|
state-ownership: replace all useAuthStore().user with useUser() hook
- Migrated all hooks: useAuth, useChat, useLogin
- Migrated all components: Header, ProfileForm, FollowButton, LikeButton, PlaylistFollowButton, ChatMessage, ChatMessages, CommentThread, CommentSection, PlaylistList, ChatSidebar, SettingsPage, DashboardPage
- Updated storeSelectors.ts useAuthUser() to use React Query
- All production code now uses useUser() hook instead of Zustand store
- Action 4.1.1.3 and 4.1.1.4 complete
2026-01-14 00:45:42 +00:00
|
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
2025-12-17 14:15:45 +00:00
|
|
|
import { apiClient } from '@/services/api/client';
|
2025-12-24 11:51:40 +00:00
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { cn } from '@/lib/utils';
|
2026-01-13 18:47:57 +00:00
|
|
|
import {
|
|
|
|
|
Loader2,
|
|
|
|
|
Plus,
|
|
|
|
|
Trash2,
|
|
|
|
|
LogOut,
|
|
|
|
|
MessageSquare,
|
|
|
|
|
Hash,
|
|
|
|
|
User,
|
|
|
|
|
MoreVertical,
|
|
|
|
|
} from 'lucide-react';
|
2025-12-24 11:51:40 +00:00
|
|
|
import { CreateRoomDialog } from './CreateRoomDialog';
|
|
|
|
|
import { useToast } from '@/hooks/useToast';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2026-01-11 16:10:27 +00:00
|
|
|
import { ErrorDisplay } from '@/components/ui/ErrorDisplay';
|
2025-12-24 11:51:40 +00:00
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from '@/components/ui/dropdown-menu';
|
2025-12-24 13:38:55 +00:00
|
|
|
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog';
|
2025-12-24 11:51:40 +00:00
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
interface ConversationItemProps {
|
2026-01-13 18:47:57 +00:00
|
|
|
conversation: {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
type: string;
|
|
|
|
|
unread_count?: number;
|
|
|
|
|
};
|
2025-12-03 21:56:50 +00:00
|
|
|
onSelect: (id: string) => void;
|
|
|
|
|
isSelected: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
const ConversationItem: React.FC<ConversationItemProps> = ({
|
|
|
|
|
conversation,
|
|
|
|
|
onSelect,
|
|
|
|
|
isSelected,
|
|
|
|
|
}) => {
|
state-ownership: replace all useAuthStore().user with useUser() hook
- Migrated all hooks: useAuth, useChat, useLogin
- Migrated all components: Header, ProfileForm, FollowButton, LikeButton, PlaylistFollowButton, ChatMessage, ChatMessages, CommentThread, CommentSection, PlaylistList, ChatSidebar, SettingsPage, DashboardPage
- Updated storeSelectors.ts useAuthUser() to use React Query
- All production code now uses useUser() hook instead of Zustand store
- Action 4.1.1.3 and 4.1.1.4 complete
2026-01-14 00:45:42 +00:00
|
|
|
const { data: user } = useUser();
|
2025-12-24 11:51:40 +00:00
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const toast = useToast();
|
|
|
|
|
const { setCurrentConversation } = useChatStore();
|
2025-12-24 13:38:55 +00:00
|
|
|
const [showLeaveDialog, setShowLeaveDialog] = useState(false);
|
|
|
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
2026-01-11 16:10:27 +00:00
|
|
|
const [mutationError, setMutationError] = useState<Error | null>(null);
|
2026-01-11 16:39:51 +00:00
|
|
|
const [retryCount, setRetryCount] = useState(0);
|
2026-01-13 18:47:57 +00:00
|
|
|
const [lastMutationType, setLastMutationType] = useState<
|
|
|
|
|
'leave' | 'delete' | null
|
|
|
|
|
>(null);
|
2026-01-11 16:39:51 +00:00
|
|
|
const [lastRoomId, setLastRoomId] = useState<string | null>(null);
|
2025-12-24 11:51:40 +00:00
|
|
|
|
|
|
|
|
const leaveRoomMutation = useMutation({
|
|
|
|
|
mutationFn: async (roomId: string) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
await apiClient.delete(
|
|
|
|
|
`/conversations/${roomId}/participants/${user?.id}`,
|
|
|
|
|
);
|
2025-12-24 11:51:40 +00:00
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ['chatConversations', user?.id],
|
|
|
|
|
});
|
2025-12-24 11:51:40 +00:00
|
|
|
toast.success('Left room successfully');
|
|
|
|
|
setCurrentConversation(null);
|
2025-12-24 13:38:55 +00:00
|
|
|
setShowLeaveDialog(false);
|
2026-01-11 16:10:27 +00:00
|
|
|
setMutationError(null);
|
2025-12-24 11:51:40 +00:00
|
|
|
},
|
|
|
|
|
onError: (error: any) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.error || 'Failed to leave room';
|
2026-01-11 16:10:27 +00:00
|
|
|
setMutationError(new Error(errorMessage));
|
2025-12-24 11:51:40 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const deleteRoomMutation = useMutation({
|
|
|
|
|
mutationFn: async (roomId: string) => {
|
|
|
|
|
await apiClient.delete(`/conversations/${roomId}`);
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ['chatConversations', user?.id],
|
|
|
|
|
});
|
2025-12-24 11:51:40 +00:00
|
|
|
toast.success('Room deleted successfully');
|
|
|
|
|
setCurrentConversation(null);
|
2025-12-24 13:38:55 +00:00
|
|
|
setShowDeleteDialog(false);
|
2026-01-11 16:10:27 +00:00
|
|
|
setMutationError(null);
|
2026-01-11 16:39:51 +00:00
|
|
|
setRetryCount(0);
|
|
|
|
|
setLastMutationType(null);
|
|
|
|
|
setLastRoomId(null);
|
2025-12-24 11:51:40 +00:00
|
|
|
},
|
|
|
|
|
onError: (error: any) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.error || 'Failed to delete room';
|
2026-01-11 16:10:27 +00:00
|
|
|
setMutationError(new Error(errorMessage));
|
2025-12-24 11:51:40 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleLeave = (e: React.MouseEvent) => {
|
|
|
|
|
e.stopPropagation();
|
2025-12-24 13:38:55 +00:00
|
|
|
setShowLeaveDialog(true);
|
2025-12-24 11:51:40 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = (e: React.MouseEvent) => {
|
|
|
|
|
e.stopPropagation();
|
2025-12-24 13:38:55 +00:00
|
|
|
setShowDeleteDialog(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const confirmLeave = () => {
|
2026-01-11 16:39:51 +00:00
|
|
|
setLastMutationType('leave');
|
|
|
|
|
setLastRoomId(conversation.id);
|
2025-12-24 13:38:55 +00:00
|
|
|
leaveRoomMutation.mutate(conversation.id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const confirmDelete = () => {
|
2026-01-11 16:39:51 +00:00
|
|
|
setLastMutationType('delete');
|
|
|
|
|
setLastRoomId(conversation.id);
|
2025-12-24 13:38:55 +00:00
|
|
|
deleteRoomMutation.mutate(conversation.id);
|
2025-12-24 11:51:40 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-11 16:39:51 +00:00
|
|
|
// Action 3.4.1.3: Retry handler for failed mutations
|
|
|
|
|
const handleRetry = async () => {
|
|
|
|
|
if (!lastMutationType || !lastRoomId || retryCount >= 3) return;
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 16:39:51 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
return (
|
2025-12-24 13:38:55 +00:00
|
|
|
<>
|
2026-01-11 16:10:27 +00:00
|
|
|
{mutationError && (
|
|
|
|
|
<ErrorDisplay
|
|
|
|
|
error={mutationError}
|
|
|
|
|
variant="banner"
|
|
|
|
|
severity="error"
|
|
|
|
|
context={{
|
|
|
|
|
action: 'managing room',
|
|
|
|
|
resource: 'conversation',
|
|
|
|
|
resourceId: conversation.id,
|
|
|
|
|
}}
|
2026-01-13 18:47:57 +00:00
|
|
|
onRetry={retryCount < 3 ? handleRetry : undefined}
|
|
|
|
|
onDismiss={() => {
|
|
|
|
|
setMutationError(null);
|
|
|
|
|
setRetryCount(0);
|
|
|
|
|
setLastMutationType(null);
|
|
|
|
|
setLastRoomId(null);
|
|
|
|
|
}}
|
2026-01-11 16:10:27 +00:00
|
|
|
/>
|
|
|
|
|
)}
|
2025-12-24 13:38:55 +00:00
|
|
|
<div
|
|
|
|
|
onClick={() => onSelect(conversation.id)}
|
|
|
|
|
className={cn(
|
2026-01-11 02:20:52 +00:00
|
|
|
'group relative flex items-center justify-between p-3 rounded-xl cursor-pointer transition-all duration-300 border border-transparent',
|
2026-01-13 18:47:57 +00:00
|
|
|
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',
|
2025-12-24 13:38:55 +00:00
|
|
|
)}
|
|
|
|
|
>
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="flex items-center gap-3 min-w-0">
|
2026-01-13 18:47:57 +00:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'w-8 h-8 rounded-lg flex items-center justify-center transition-colors',
|
|
|
|
|
isSelected
|
|
|
|
|
? 'bg-kodo-cyan text-kodo-void'
|
|
|
|
|
: 'bg-white/5 text-kodo-secondary group-hover:text-white',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{conversation.type === 'direct' ? (
|
|
|
|
|
<User size={14} />
|
|
|
|
|
) : (
|
|
|
|
|
<Hash size={14} />
|
|
|
|
|
)}
|
2026-01-11 02:20:52 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="flex flex-col min-w-0">
|
2026-01-13 18:47:57 +00:00
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
'text-sm font-medium truncate transition-colors',
|
|
|
|
|
isSelected
|
|
|
|
|
? 'text-white'
|
|
|
|
|
: 'text-kodo-secondary group-hover:text-white',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{conversation.name ||
|
|
|
|
|
`Channel ${conversation.id.substring(0, 4)}`}
|
2026-01-11 02:20:52 +00:00
|
|
|
</span>
|
|
|
|
|
{conversation.type !== 'direct' && (
|
|
|
|
|
<span className="text-[10px] text-kodo-secondary/50 uppercase tracking-wider">
|
|
|
|
|
{conversation.type}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{conversation.unread_count && conversation.unread_count > 0 ? (
|
|
|
|
|
<span className="bg-kodo-magenta text-white text-[10px] px-1.5 py-0.5 rounded-full font-bold shadow-lg shadow-kodo-magenta/20">
|
|
|
|
|
{conversation.unread_count}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
|
2025-12-24 13:38:55 +00:00
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
2026-01-11 02:20:52 +00:00
|
|
|
className={cn(
|
2026-01-13 18:47:57 +00:00
|
|
|
'h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity',
|
|
|
|
|
isSelected
|
|
|
|
|
? 'text-kodo-cyan hover:bg-kodo-cyan/20'
|
|
|
|
|
: 'text-kodo-secondary hover:text-white',
|
2026-01-11 02:20:52 +00:00
|
|
|
)}
|
2025-12-24 13:38:55 +00:00
|
|
|
>
|
|
|
|
|
<MoreVertical className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
2026-01-13 18:47:57 +00:00
|
|
|
<DropdownMenuContent
|
|
|
|
|
align="end"
|
|
|
|
|
className="bg-kodo-void border-white/10 text-white"
|
|
|
|
|
>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={handleLeave}
|
|
|
|
|
className="focus:bg-white/10 cursor-pointer"
|
|
|
|
|
>
|
2025-12-24 13:38:55 +00:00
|
|
|
<LogOut className="mr-2 h-4 w-4" />
|
2026-01-11 02:20:52 +00:00
|
|
|
Leave Channel
|
2025-12-24 11:51:40 +00:00
|
|
|
</DropdownMenuItem>
|
2025-12-24 13:38:55 +00:00
|
|
|
{conversation.type !== 'direct' && (
|
2026-01-13 18:47:57 +00:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
className="text-kodo-red focus:bg-kodo-red/10 cursor-pointer"
|
|
|
|
|
>
|
2025-12-24 13:38:55 +00:00
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
2026-01-11 02:20:52 +00:00
|
|
|
Delete Channel
|
2025-12-24 13:38:55 +00:00
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
2026-01-11 02:20:52 +00:00
|
|
|
|
|
|
|
|
{/* Active Indicator Line */}
|
|
|
|
|
{isSelected && (
|
|
|
|
|
<div className="absolute left-0 top-3 bottom-3 w-0.5 bg-kodo-cyan rounded-r-full shadow-[0_0_8px_rgba(102,252,241,0.8)]" />
|
|
|
|
|
)}
|
2025-12-24 13:38:55 +00:00
|
|
|
</div>
|
2026-01-11 02:20:52 +00:00
|
|
|
|
2025-12-24 13:38:55 +00:00
|
|
|
<ConfirmationDialog
|
|
|
|
|
open={showLeaveDialog}
|
|
|
|
|
onClose={() => setShowLeaveDialog(false)}
|
|
|
|
|
onConfirm={confirmLeave}
|
2026-01-11 02:20:52 +00:00
|
|
|
title="Leave Channel"
|
|
|
|
|
description="Disconnect from this secure frequency? Incoming transmission will cease."
|
|
|
|
|
confirmLabel="Disconnect"
|
2025-12-24 13:38:55 +00:00
|
|
|
cancelLabel="Cancel"
|
|
|
|
|
variant="default"
|
|
|
|
|
isLoading={leaveRoomMutation.isPending}
|
|
|
|
|
/>
|
|
|
|
|
<ConfirmationDialog
|
|
|
|
|
open={showDeleteDialog}
|
|
|
|
|
onClose={() => setShowDeleteDialog(false)}
|
|
|
|
|
onConfirm={confirmDelete}
|
2026-01-11 02:20:52 +00:00
|
|
|
title="Delete Channel"
|
|
|
|
|
description="Permanently purge this channel from the network? This action is irreversible."
|
|
|
|
|
confirmLabel="Purge"
|
2025-12-24 13:38:55 +00:00
|
|
|
cancelLabel="Cancel"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
isLoading={deleteRoomMutation.isPending}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const ChatSidebar: React.FC = () => {
|
state-ownership: replace all useAuthStore().user with useUser() hook
- Migrated all hooks: useAuth, useChat, useLogin
- Migrated all components: Header, ProfileForm, FollowButton, LikeButton, PlaylistFollowButton, ChatMessage, ChatMessages, CommentThread, CommentSection, PlaylistList, ChatSidebar, SettingsPage, DashboardPage
- Updated storeSelectors.ts useAuthUser() to use React Query
- All production code now uses useUser() hook instead of Zustand store
- Action 4.1.1.3 and 4.1.1.4 complete
2026-01-14 00:45:42 +00:00
|
|
|
const { data: user } = useUser();
|
2025-12-13 02:34:34 +00:00
|
|
|
const userId = user?.id;
|
2026-01-11 16:10:27 +00:00
|
|
|
const queryClient = useQueryClient();
|
2025-12-13 02:34:34 +00:00
|
|
|
const {
|
|
|
|
|
conversations,
|
|
|
|
|
currentConversationId,
|
|
|
|
|
setCurrentConversation,
|
|
|
|
|
addConversation,
|
|
|
|
|
} = useChatStore();
|
2025-12-24 11:51:40 +00:00
|
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (data) {
|
2026-01-03 17:48:45 +00:00
|
|
|
data.forEach((conv: any) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
if (!conversations.some((c) => c.id === conv.id)) {
|
2026-01-03 17:48:45 +00:00
|
|
|
addConversation({
|
|
|
|
|
id: conv.id,
|
|
|
|
|
name: conv.name,
|
|
|
|
|
type: conv.type,
|
|
|
|
|
participants: conv.participants,
|
|
|
|
|
unread_count: 0,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-13 02:34:34 +00:00
|
|
|
}
|
2026-01-03 17:48:45 +00:00
|
|
|
}, [data, conversations, addConversation]);
|
2025-12-13 02:34:34 +00:00
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="flex-1 flex items-center justify-center">
|
|
|
|
|
<Loader2 className="animate-spin text-kodo-cyan" size={24} />
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
2026-01-11 16:10:27 +00:00
|
|
|
<div className="flex-1 flex items-center justify-center p-4">
|
|
|
|
|
<ErrorDisplay
|
|
|
|
|
error={error instanceof Error ? error : new Error('Signal Lost')}
|
|
|
|
|
variant="card"
|
|
|
|
|
severity="error"
|
|
|
|
|
context={{
|
|
|
|
|
action: 'fetching conversations',
|
|
|
|
|
resource: 'conversations',
|
|
|
|
|
}}
|
2026-01-13 18:47:57 +00:00
|
|
|
onRetry={() =>
|
|
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ['chatConversations', userId],
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-01-11 16:10:27 +00:00
|
|
|
/>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="flex flex-col h-full">
|
|
|
|
|
<div className="p-4 border-b border-white/5 bg-white/2 backdrop-blur-sm">
|
|
|
|
|
<div className="flex items-center justify-between mb-1">
|
|
|
|
|
<h2 className="text-sm font-bold text-white tracking-wide uppercase flex items-center gap-2">
|
|
|
|
|
<MessageSquare className="w-4 h-4 text-kodo-cyan" />
|
|
|
|
|
Active Channels
|
|
|
|
|
</h2>
|
|
|
|
|
<span className="text-[10px] font-mono text-kodo-secondary bg-white/5 px-1.5 py-0.5 rounded">
|
|
|
|
|
{conversations.length}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-3 space-y-1">
|
2025-12-03 21:56:50 +00:00
|
|
|
{conversations.length === 0 ? (
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="text-kodo-secondary/50 text-sm p-4 text-center italic border border-dashed border-white/5 rounded-xl m-2">
|
|
|
|
|
No active frequencies detected.
|
|
|
|
|
<br />
|
|
|
|
|
Initialize a new channel.
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
conversations.map((conv) => (
|
|
|
|
|
<ConversationItem
|
|
|
|
|
key={conv.id}
|
|
|
|
|
conversation={conv}
|
|
|
|
|
onSelect={setCurrentConversation}
|
|
|
|
|
isSelected={conv.id === currentConversationId}
|
|
|
|
|
/>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="p-4 border-t border-white/5 bg-white/2 backdrop-blur-sm">
|
2025-12-24 11:51:40 +00:00
|
|
|
<Button
|
|
|
|
|
onClick={() => setIsCreateDialogOpen(true)}
|
2026-01-11 02:20:52 +00:00
|
|
|
className="w-full shadow-lg shadow-kodo-cyan/10"
|
2025-12-24 11:51:40 +00:00
|
|
|
variant="default"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
2026-01-11 02:20:52 +00:00
|
|
|
New Channel
|
2025-12-24 11:51:40 +00:00
|
|
|
</Button>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2025-12-24 11:51:40 +00:00
|
|
|
<CreateRoomDialog
|
|
|
|
|
open={isCreateDialogOpen}
|
|
|
|
|
onClose={() => setIsCreateDialogOpen(false)}
|
|
|
|
|
/>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
};
|