/** * ChatListView Component * ADR-024: Telegram-like Chat List * * Displays list of conversations in Telegram style: * - Avatar, name, last message preview, time * - Search bar at top * - New chat FAB button */ import { useState, useMemo } from 'react'; import { Search, Plus, Bot, User, MessageSquare, Check, CheckCheck, Link2, MoreVertical, Trash2, Edit3, Pin, Volume2, VolumeX } from 'lucide-react'; import { cn } from '@/shared/utils/cn'; export interface ChatPreview { id: number; title: string; type: 'agent' | 'direct' | 'group' | 'task'; avatar?: string; icon?: string; lastMessage?: { content: string; sender: string; time: Date; isRead: boolean; isOwn: boolean; }; unreadCount?: number; isPinned?: boolean; isMuted?: boolean; // Linked rows bindings?: Array<{ tableId: number; tableName: string; rowId: number; rowTitle: string; }>; // Participants count for groups participantsCount?: number; } export interface ChatListViewProps { chats: ChatPreview[]; selectedChatId?: number | null; onSelectChat: (chatId: number) => void; onCreateChat: () => void; onDeleteChat?: (chatId: number) => void; onPinChat?: (chatId: number) => void; onMuteChat?: (chatId: number) => void; isLoading?: boolean; className?: string; } export function ChatListView({ chats, selectedChatId, onSelectChat, onCreateChat, onDeleteChat, onPinChat, onMuteChat, isLoading = false, className }: ChatListViewProps) { const [searchQuery, setSearchQuery] = useState(''); const [contextMenuChatId, setContextMenuChatId] = useState(null); // Filter chats by search const filteredChats = useMemo(() => { if (!searchQuery.trim()) return chats; const query = searchQuery.toLowerCase(); return chats.filter(chat => chat.title.toLowerCase().includes(query) || chat.lastMessage?.content.toLowerCase().includes(query) ); }, [chats, searchQuery]); // Sort: pinned first, then by last message time const sortedChats = useMemo(() => { return [...filteredChats].sort((a, b) => { // Pinned first if (a.isPinned && !b.isPinned) return -1; if (!a.isPinned && b.isPinned) return 1; // Then by time const timeA = a.lastMessage?.time?.getTime() || 0; const timeB = b.lastMessage?.time?.getTime() || 0; return timeB - timeA; }); }, [filteredChats]); // Format time for display const formatTime = (date?: Date) => { if (!date) return ''; const now = new Date(); const diff = now.getTime() - date.getTime(); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) { return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); } else if (days === 1) { return 'Вчера'; } else if (days < 7) { return date.toLocaleDateString('ru-RU', { weekday: 'short' }); } else { return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit' }); } }; // Truncate last message const truncateMessage = (text: string, maxLength: number = 40) => { if (text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; }; // Get chat avatar const renderAvatar = (chat: ChatPreview) => { if (chat.avatar) { return ( {chat.title} ); } // Default avatars based on type const bgColors: Record = { agent: 'bg-gradient-to-br from-purple-500 to-purple-600', direct: 'bg-gradient-to-br from-blue-500 to-blue-600', group: 'bg-gradient-to-br from-green-500 to-green-600', task: 'bg-gradient-to-br from-orange-500 to-orange-600' }; return (
{chat.icon ? ( {chat.icon} ) : chat.type === 'agent' ? ( ) : chat.type === 'group' ? ( ) : chat.type === 'task' ? ( ) : ( )}
); }; return (
{/* Search Header - Telegram style */}
setSearchQuery(e.target.value)} placeholder="Поиск..." className="w-full pl-10 pr-4 py-2 text-sm rounded-full bg-[var(--bg-tertiary)] text-[var(--text-primary)] placeholder:text-[var(--text-tertiary)] border-none focus:outline-none focus:ring-2 focus:ring-[var(--color-primary-500)]/30" />
{/* Chat List */}
{isLoading ? (
) : sortedChats.length === 0 ? (

{searchQuery ? 'Чаты не найдены' : 'Нет чатов'}

) : (
{sortedChats.map(chat => (
onSelectChat(chat.id)} onContextMenu={(e) => { e.preventDefault(); setContextMenuChatId(contextMenuChatId === chat.id ? null : chat.id); }} > {/* Avatar */}
{renderAvatar(chat)} {/* Online indicator for direct chats */} {chat.type === 'direct' && ( )}
{/* Content */}
{/* Top row: Name + Time */}
{chat.isPinned && ( )} {chat.title} {chat.type === 'agent' && ( AI )}
{formatTime(chat.lastMessage?.time)}
{/* Bottom row: Last message + Unread badge */}
{/* Read status for own messages */} {chat.lastMessage?.isOwn && ( chat.lastMessage.isRead ? ( ) : ( ) )} {chat.lastMessage ? ( <> {chat.lastMessage.isOwn && Вы: } {truncateMessage(chat.lastMessage.content)} ) : ( Нет сообщений )}
{/* Unread badge OR muted icon */}
{chat.isMuted && ( )} {(chat.unreadCount ?? 0) > 0 && ( {(chat.unreadCount ?? 0) > 99 ? '99+' : chat.unreadCount} )}
{/* Bindings indicator */} {chat.bindings && chat.bindings.length > 0 && (
{chat.bindings.map(b => b.rowTitle).join(', ')}
)}
{/* Context Menu */} {contextMenuChatId === chat.id && (
e.stopPropagation()} > {onPinChat && ( )} {onMuteChat && ( )} {onDeleteChat && ( )}
)}
))}
)}
{/* New Chat FAB - Telegram style */}
); }