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
|
|
|
|
2026-01-15 18:48:47 +00:00
|
|
|
// Action 4.4.1.5: Add optimistic update
|
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
|
|
|
},
|
2026-01-15 18:48:47 +00:00
|
|
|
onMutate: async (roomId: string) => {
|
|
|
|
|
// Cancel outgoing refetches
|
|
|
|
|
await queryClient.cancelQueries({
|
|
|
|
|
queryKey: ['chatConversations', user?.id],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Snapshot previous value
|
|
|
|
|
const previousConversations = queryClient.getQueryData<any[]>(
|
|
|
|
|
['chatConversations', user?.id],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Optimistically remove conversation from list
|
|
|
|
|
if (previousConversations) {
|
|
|
|
|
queryClient.setQueryData<any[]>(
|
|
|
|
|
['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));
|
|
|
|
|
},
|
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
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-15 18:48:47 +00:00
|
|
|
// Action 4.4.1.5: Add optimistic update
|
2025-12-24 11:51:40 +00:00
|
|
|
const deleteRoomMutation = useMutation({
|
|
|
|
|
mutationFn: async (roomId: string) => {
|
|
|
|
|
await apiClient.delete(`/conversations/${roomId}`);
|
|
|
|
|
},
|
2026-01-15 18:48:47 +00:00
|
|
|
onMutate: async (roomId: string) => {
|
|
|
|
|
// Cancel outgoing refetches
|
|
|
|
|
await queryClient.cancelQueries({
|
|
|
|
|
queryKey: ['chatConversations', user?.id],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Snapshot previous value
|
|
|
|
|
const previousConversations = queryClient.getQueryData<any[]>(
|
|
|
|
|
['chatConversations', user?.id],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Optimistically remove conversation from list
|
|
|
|
|
if (previousConversations) {
|
|
|
|
|
queryClient.setQueryData<any[]>(
|
|
|
|
|
['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));
|
|
|
|
|
},
|
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('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
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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">
|
aesthetic-improvements: reduce decorative cyan in chat, auth, player, streaming, and dashboard (80/20 rule, batch 13)
- Chat: ChatSidebar loading spinner and decorative icon, VirtualizedChatMessages decorative attachment badge, ChatPage decorative icon and loading spinner border/text, ChatMessage decorative username indicator and icon (7 instances)
- Auth: TwoFactorVerify decorative icon (1 instance)
- Player: PlayerLoading decorative spinner (1 instance)
- Streaming: PlaybackSummary decorative icon (1 instance)
- Dashboard: DashboardPage decorative chart color and gradient and icon (3 instances)
- Total: ~13 files, ~13 instances replaced
- Preserved: Active/selected states (ChatSidebar selected conversation, ChatMessage isMe message bubble and highlighted message, DashboardPage selected button 30J, ChatInput drag active overlay and emoji picker active, TrackFilters active filter badge, TrackHistory current track, TrackGridDensitySelector selected density, PlaybackSpeedControl selected speed, ViewToggle selected view mode, TrackList selected tracks, TrackListRow selected state, PlaylistList selected view mode, QualitySelector selected quality, SettingsPage selected tab and theme, LoginForm checkbox accent - focus/interaction, RegisterPage checkbox accent - focus/interaction), functional links (ForgotPasswordPage link, TwoFactorVerify links, RegisterPage links, AuthLayout link, ProfileForm links, LoginPage link, RegisterPage link), design system variants, semantic status indicators, interactive states, functional loading indicators, informational alerts/toasts
- Action 11.3.1.3 in progress (thirteenth batch: chat, auth, player, streaming, and dashboard components)
2026-01-16 10:32:55 +00:00
|
|
|
<Loader2 className="animate-spin text-kodo-steel" 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">
|
aesthetic-improvements: reduce decorative cyan in chat, auth, player, streaming, and dashboard (80/20 rule, batch 13)
- Chat: ChatSidebar loading spinner and decorative icon, VirtualizedChatMessages decorative attachment badge, ChatPage decorative icon and loading spinner border/text, ChatMessage decorative username indicator and icon (7 instances)
- Auth: TwoFactorVerify decorative icon (1 instance)
- Player: PlayerLoading decorative spinner (1 instance)
- Streaming: PlaybackSummary decorative icon (1 instance)
- Dashboard: DashboardPage decorative chart color and gradient and icon (3 instances)
- Total: ~13 files, ~13 instances replaced
- Preserved: Active/selected states (ChatSidebar selected conversation, ChatMessage isMe message bubble and highlighted message, DashboardPage selected button 30J, ChatInput drag active overlay and emoji picker active, TrackFilters active filter badge, TrackHistory current track, TrackGridDensitySelector selected density, PlaybackSpeedControl selected speed, ViewToggle selected view mode, TrackList selected tracks, TrackListRow selected state, PlaylistList selected view mode, QualitySelector selected quality, SettingsPage selected tab and theme, LoginForm checkbox accent - focus/interaction, RegisterPage checkbox accent - focus/interaction), functional links (ForgotPasswordPage link, TwoFactorVerify links, RegisterPage links, AuthLayout link, ProfileForm links, LoginPage link, RegisterPage link), design system variants, semantic status indicators, interactive states, functional loading indicators, informational alerts/toasts
- Action 11.3.1.3 in progress (thirteenth batch: chat, auth, player, streaming, and dashboard components)
2026-01-16 10:32:55 +00:00
|
|
|
<MessageSquare className="w-4 h-4 text-kodo-steel" />
|
2026-01-11 02:20:52 +00:00
|
|
|
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-16 10:40:13 +00:00
|
|
|
className="w-full shadow-lg shadow-kodo-steel/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
|
|
|
};
|