'use client'; import { MessageCircle, Search, Users, X, Send, UserPlus, Smile, Image as ImageIcon, Paperclip } from 'lucide-react'; import { useEffect, useState, useRef, useCallback } from 'react'; import { ChatMessage, Conversation, Friend, FriendRequest, WebSocketMessage } from '../lib/types'; import { getAuthInfoFromBrowserCookie } from '../lib/auth'; import { useWebSocket } from '../hooks/useWebSocket'; import { useToast } from './Toast'; interface ChatModalProps { isOpen: boolean; onClose: () => void; onMessageCountChange?: (count: number) => void; onChatCountReset?: (resetCount: number) => void; onFriendRequestCountReset?: (resetCount: number) => void; } export function ChatModal({ isOpen, onClose, onMessageCountChange, onChatCountReset, onFriendRequestCountReset }: ChatModalProps) { const [activeTab, setActiveTab] = useState<'chat' | 'friends'>('chat'); const [conversations, setConversations] = useState([]); const [friends, setFriends] = useState([]); const [selectedConversation, setSelectedConversation] = useState(null); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [friendSearchQuery, setFriendSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [friendRequests, setFriendRequests] = useState([]); const [onlineUsers, setOnlineUsers] = useState([]); const [unreadChatCount, setUnreadChatCount] = useState(0); const [unreadFriendRequestCount, setUnreadFriendRequestCount] = useState(0); const [conversationUnreadCounts, setConversationUnreadCounts] = useState<{ [key: string]: number }>({}); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [uploadingImage, setUploadingImage] = useState(false); const [userAvatars, setUserAvatars] = useState<{ [username: string]: string | null }>({}); const [isDragging, setIsDragging] = useState(false); const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }); const [dragStartPosition, setDragStartPosition] = useState({ x: 0, y: 0 }); const messagesEndRef = useRef(null); const fileInputRef = useRef(null); const currentUser = getAuthInfoFromBrowserCookie(); const { showError, showSuccess } = useToast(); // 拖动相关事件处理 const handleMouseDown = (e: React.MouseEvent) => { if (e.button !== 0) return; // 只处理左键 setIsDragging(true); setDragStartPosition({ x: e.clientX - dragPosition.x, y: e.clientY - dragPosition.y }); }; const handleMouseMove = useCallback((e: MouseEvent) => { if (!isDragging) return; const newX = e.clientX - dragStartPosition.x; const newY = e.clientY - dragStartPosition.y; // 限制拖动范围,确保模态框不会完全移出视口 const maxX = window.innerWidth - 400; // 模态框最小宽度 const maxY = window.innerHeight - 200; // 模态框最小高度 setDragPosition({ x: Math.max(-200, Math.min(maxX, newX)), y: Math.max(0, Math.min(maxY, newY)) }); }, [isDragging, dragStartPosition]); const handleMouseUp = useCallback(() => { setIsDragging(false); }, []); // 添加全局鼠标事件监听 useEffect(() => { if (isDragging) { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); document.body.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; } return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; }; }, [isDragging, handleMouseMove, handleMouseUp]); // 实时搜索功能 useEffect(() => { const timer = setTimeout(() => { if (friendSearchQuery.trim()) { searchUsers(); } else { setSearchResults([]); } }, 300); return () => clearTimeout(timer); }, [friendSearchQuery]); // 常用表情列表 const emojis = [ '😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '🤗', '🤔', '😐', '😑', '😶', '🙄', '😏', '😣', '😥', '😮', '🤐', '😯', '😴', '😫', '😪', '😵', '🤯', '🤠', '🥳', '😎', '👍', '👎', '👌', '✌️', '🤞', '🤟', '🤘', '👏', '🙌', '👐', '❤️', '💙', '💚', '💛', '💜', '🧡', '🖤', '🤍', '🤎', '💕', '💖', '💗', '💘', '💝', '💞', '💟', '❣️', '💔', '❤️‍🔥', '💯' ]; // 获取用户真实头像 const fetchUserAvatar = useCallback(async (username: string) => { // 如果已经缓存了该用户的头像(包括 null 值),直接返回 if (username in userAvatars) { return userAvatars[username]; } try { const response = await fetch(`/api/avatar?user=${encodeURIComponent(username)}`); if (response.ok) { const data = await response.json(); const avatar = data.avatar || null; // 缓存头像结果 setUserAvatars(prev => ({ ...prev, [username]: avatar })); return avatar; } } catch (error) { console.error('获取用户头像失败:', error); } // 获取失败时缓存 null setUserAvatars(prev => ({ ...prev, [username]: null })); return null; }, [userAvatars]); // 预加载用户头像 const preloadUserAvatars = useCallback(async (usernames: string[]) => { const promises = usernames .filter(username => !(username in userAvatars)) // 只加载未缓存的头像 .map(username => fetchUserAvatar(username)); await Promise.allSettled(promises); }, [userAvatars, fetchUserAvatar]); // 使用 useCallback 稳定 onMessage 函数引用 const handleWebSocketMessage = useCallback((message: WebSocketMessage) => { switch (message.type) { case 'message': const conversationId = message.data.conversation_id; // 预加载消息发送者的头像 if (message.data.sender_id) { preloadUserAvatars([message.data.sender_id]); } // 收到新消息的处理逻辑 if (selectedConversation && conversationId === selectedConversation.id && isOpen) { // 只有当模态框打开且用户正在查看这个对话时,才只刷新消息列表 loadMessages(selectedConversation.id); } else if (conversationId) { // 所有其他情况都增加未读消息计数(包括模态框关闭时的当前对话) setConversationUnreadCounts(prev => { const newCounts = { ...prev, [conversationId]: (prev[conversationId] || 0) + 1 }; return newCounts; }); // 如果用户正在查看这个对话且模态框是打开的,同时刷新消息列表 if (selectedConversation && conversationId === selectedConversation.id && isOpen) { loadMessages(selectedConversation.id); } } loadConversations(); break; case 'friend_request': // 收到好友申请 // 预加载好友申请发送者的头像 if (message.data.from_user) { preloadUserAvatars([message.data.from_user]); } setUnreadFriendRequestCount(prev => prev + 1); loadFriendRequests(); break; case 'friend_accepted': // 好友申请被接受 loadFriends(); break; case 'user_status': // 用户状态变化 setFriends(prevFriends => prevFriends.map(friend => friend.username === message.data.userId ? { ...friend, status: message.data.status } : friend ) ); break; case 'online_users': // 更新在线用户列表 setOnlineUsers(message.data.users || []); break; case 'connection_confirmed': // 连接确认,请求在线用户列表 break; default: break; } }, [selectedConversation, preloadUserAvatars]); // WebSocket 连接 - 始终保持连接以接收实时消息 const { isConnected, sendMessage: sendWebSocketMessage } = useWebSocket({ onMessage: handleWebSocketMessage, enabled: true, // 始终启用WebSocket以接收实时消息 }); useEffect(() => { if (isOpen) { loadConversations(); loadFriends(); loadFriendRequests(); // 预加载当前用户的头像 if (currentUser?.username) { preloadUserAvatars([currentUser.username]); } // 开发模式下创建一些测试数据 if (process.env.NODE_ENV === 'development') { createTestDataIfNeeded(); } } }, [isOpen, currentUser?.username, preloadUserAvatars]); // 创建测试数据(仅开发模式) const createTestDataIfNeeded = async () => { if (!currentUser) return; try { // 检查是否已有对话 const response = await fetch('/api/chat/conversations'); if (response.ok) { const existingConversations = await response.json(); if (existingConversations.length === 0) { // 创建一个测试对话 const testConversation = { name: '测试对话', participants: [currentUser.username, 'test-user'], type: 'private', created_at: Date.now(), updated_at: Date.now(), }; const createResponse = await fetch('/api/chat/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(testConversation), }); if (createResponse.ok) { loadConversations(); // 重新加载对话列表 } } } } catch (error) { console.error('创建测试数据失败:', error); } }; useEffect(() => { scrollToBottom(); }, [messages]); // 点击外部关闭表情选择器 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as HTMLElement; if (showEmojiPicker && !target.closest('.emoji-picker-container')) { setShowEmojiPicker(false); } }; if (showEmojiPicker) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [showEmojiPicker]); // 计算总的未读聊天消息数量 useEffect(() => { const totalChatCount = Object.values(conversationUnreadCounts).reduce((sum, count) => sum + count, 0); setUnreadChatCount(totalChatCount); }, [conversationUnreadCounts]); // 通知父组件消息数量变化 useEffect(() => { const totalCount = unreadChatCount + unreadFriendRequestCount; onMessageCountChange?.(totalCount); }, [unreadChatCount, unreadFriendRequestCount, onMessageCountChange]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; // 生成头像URL(优先使用真实头像,回退到默认头像) const getAvatarUrl = (username: string) => { const realAvatar = userAvatars[username]; if (realAvatar) { return realAvatar; // 返回Base64格式的真实头像 } // 使用Dicebear API生成默认头像 return `https://api.dicebear.com/7.x/initials/svg?seed=${username}&backgroundColor=3B82F6,8B5CF6,EC4899,10B981,F59E0B&textColor=ffffff`; }; // 获取用户显示名称 const getDisplayName = (username: string) => { if (username === currentUser?.username) return '我'; const friend = friends.find(f => f.username === username); return friend?.nickname || username; }; // 格式化消息时间显示 const formatMessageTime = (timestamp: number) => { const messageDate = new Date(timestamp); const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000); const messageDay = new Date(messageDate.getFullYear(), messageDate.getMonth(), messageDate.getDate()); const timeStr = messageDate.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); if (messageDay.getTime() === today.getTime()) { // 今天的消息:只显示时分秒 return timeStr; } else if (messageDay.getTime() === yesterday.getTime()) { // 昨天的消息:昨天-时分秒 return `昨天-${timeStr}`; } else { // 更早的消息:年月日-时分秒 const dateStr = messageDate.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }); return `${dateStr}-${timeStr}`; } }; // 处理表情选择 const handleEmojiSelect = (emoji: string) => { setNewMessage(prev => prev + emoji); setShowEmojiPicker(false); }; // 处理图片上传 const handleImageUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; // 验证文件类型 if (!file.type.startsWith('image/')) { showError('文件类型错误', '请选择图片文件'); return; } // 验证文件大小 (5MB) if (file.size > 5 * 1024 * 1024) { showError('文件过大', '图片大小不能超过5MB'); return; } setUploadingImage(true); try { // 转换为base64 const base64 = await fileToBase64(file); // 发送图片消息 if (selectedConversation && currentUser) { const message: Omit = { conversation_id: selectedConversation.id, sender_id: currentUser.username || '', sender_name: currentUser.username || '', content: base64, message_type: 'image', timestamp: Date.now(), is_read: false, }; const response = await fetch('/api/chat/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(message), }); if (response.ok) { const sentMessage = await response.json(); await loadMessages(selectedConversation.id); await loadConversations(); // 通过WebSocket通知其他参与者 if (isConnected) { sendWebSocketMessage({ type: 'message', data: { ...sentMessage, conversation_id: selectedConversation.id, participants: selectedConversation.participants, }, timestamp: Date.now(), }); } } else { showError('发送失败', '图片发送失败,请重试'); } } } catch (error) { console.error('Image upload failed:', error); showError('发送失败', '图片处理失败,请重试'); } finally { setUploadingImage(false); // 清除文件选择 if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; // 文件转base64 const fileToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result as string); reader.onerror = error => reject(error); }); }; const loadConversations = async () => { try { const response = await fetch('/api/chat/conversations'); if (response.ok) { const data = await response.json(); setConversations(data); // 预加载所有对话参与者的头像 const allParticipants = data.reduce((acc: string[], conv: Conversation) => { return [...acc, ...conv.participants]; }, []); const uniqueParticipants = Array.from(new Set(allParticipants)); preloadUserAvatars(uniqueParticipants); } } catch (error) { console.error('Failed to load conversations:', error); } }; const loadFriends = async () => { try { const response = await fetch('/api/chat/friends'); if (response.ok) { const data = await response.json(); setFriends(data); // 预加载所有好友的头像 const friendUsernames = data.map((friend: Friend) => friend.username); preloadUserAvatars(friendUsernames); } } catch (error) { console.error('Failed to load friends:', error); } }; const loadFriendRequests = async () => { try { const response = await fetch('/api/chat/friend-requests'); if (response.ok) { const data = await response.json(); setFriendRequests(data); // 预加载好友请求发送者的头像 const requestSenders = data.map((request: FriendRequest) => request.from_user); const uniqueSenders = Array.from(new Set(requestSenders)); preloadUserAvatars(uniqueSenders); } } catch (error) { console.error('Failed to load friend requests:', error); } }; const loadMessages = async (conversationId: string) => { try { const response = await fetch(`/api/chat/messages?conversationId=${conversationId}`); if (response.ok) { const data = await response.json(); setMessages(data); // 预加载所有发送者的头像 const senderIds = Array.from(new Set(data.map((msg: ChatMessage) => msg.sender_id))); preloadUserAvatars(senderIds); } else { // 处理非200状态码 const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); console.error('Failed to load messages - Status:', response.status, 'Error:', errorData); if (response.status === 401) { showError('未授权', '请重新登录'); } else if (response.status === 403) { showError('无权限', '您没有权限访问此对话'); } else if (response.status === 404) { showError('对话不存在', '该对话可能已被删除'); } else { showError('加载消息失败', errorData.error || '服务器错误'); } // 清空消息列表 setMessages([]); } } catch (error) { console.error('Failed to load messages:', error); showError('加载消息失败', '网络错误,请稍后重试'); setMessages([]); } }; const handleSendMessage = async () => { if (!newMessage.trim() || !selectedConversation || !currentUser) return; const message: Omit = { conversation_id: selectedConversation.id, sender_id: currentUser.username || '', sender_name: currentUser.username || '', content: newMessage.trim(), message_type: 'text', timestamp: Date.now(), is_read: false, }; try { const response = await fetch('/api/chat/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(message), }); if (response.ok) { const sentMessage = await response.json(); setNewMessage(''); await loadMessages(selectedConversation.id); await loadConversations(); // 通过WebSocket通知其他参与者 if (isConnected) { sendWebSocketMessage({ type: 'message', data: { ...sentMessage, conversation_id: selectedConversation.id, participants: selectedConversation.participants, }, timestamp: Date.now(), }); } } } catch (error) { console.error('Failed to send message:', error); } }; const searchUsers = async () => { if (!friendSearchQuery.trim()) { setSearchResults([]); return; } try { const response = await fetch(`/api/chat/search-users?q=${encodeURIComponent(friendSearchQuery)}`); if (response.ok) { const data = await response.json(); setSearchResults(data); // 预加载搜索结果用户的头像 const searchUsernames = data.map((user: Friend) => user.username); preloadUserAvatars(searchUsernames); } } catch (error) { console.error('Failed to search users:', error); } }; const sendFriendRequest = async (toUser: string) => { if (!currentUser) return; const request: Omit = { from_user: currentUser.username || '', to_user: toUser, message: '请求添加您为好友', status: 'pending', created_at: Date.now(), updated_at: Date.now(), }; try { const response = await fetch('/api/chat/friend-requests', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), }); if (response.ok) { const sentRequest = await response.json(); showSuccess('好友申请已发送', '等待对方确认'); // 清空搜索结果和搜索框 setFriendSearchQuery(''); setSearchResults([]); // 通过WebSocket通知目标用户 if (isConnected) { sendWebSocketMessage({ type: 'friend_request', data: sentRequest, timestamp: Date.now(), }); } } else { showError('发送失败', '请稍后重试'); } } catch (error) { console.error('Failed to send friend request:', error); showError('发送失败', '网络错误,请稍后重试'); } }; const handleFriendRequest = async (requestId: string, status: 'accepted' | 'rejected') => { try { const response = await fetch('/api/chat/friend-requests', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requestId, status }), }); if (response.ok) { await loadFriendRequests(); if (status === 'accepted') { await loadFriends(); } // 处理好友申请后,减少好友请求计数 onFriendRequestCountReset?.(1); } } catch (error) { console.error('Failed to handle friend request:', error); } }; const filteredConversations = conversations.filter(conv => conv.name.toLowerCase().includes(searchQuery.toLowerCase()) ); const isFriend = (username: string) => { return friends.some(friend => friend.username === username); }; const isUserOnline = (username: string) => { return onlineUsers.includes(username); }; // 创建或获取与好友的对话 const startConversationWithFriend = async (friendUsername: string) => { try { // 尝试查找现有对话 const existingConv = conversations.find(conv => conv.participants.includes(friendUsername) && conv.participants.includes(currentUser?.username || '') ); if (existingConv) { setSelectedConversation(existingConv); setActiveTab('chat'); loadMessages(existingConv.id); return; } // 创建新对话 const newConv = { name: friendUsername, participants: [currentUser?.username || '', friendUsername], type: 'private' as const, created_at: Date.now(), updated_at: Date.now(), }; const response = await fetch('/api/chat/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newConv), }); if (response.ok) { const createdConv = await response.json(); setSelectedConversation(createdConv); setActiveTab('chat'); await loadConversations(); loadMessages(createdConv.id); } } catch (error) { console.error('Failed to start conversation:', error); showError('创建对话失败', '请稍后重试'); } }; // 处理标签切换 const handleTabChange = (tab: 'chat' | 'friends') => { setActiveTab(tab); // 清除相应的未读计数 if (tab === 'friends') { const currentFriendRequestCount = unreadFriendRequestCount; setUnreadFriendRequestCount(0); // 通知父组件重置好友请求计数 onFriendRequestCountReset?.(currentFriendRequestCount); } }; // 处理对话选择 const handleConversationSelect = (conv: Conversation) => { setSelectedConversation(conv); loadMessages(conv.id); // 清除该对话的未读消息计数 const resetCount = conversationUnreadCounts[conv.id] || 0; if (resetCount > 0) { setConversationUnreadCounts(prev => ({ ...prev, [conv.id]: 0 })); // 通知父组件重置聊天计数 onChatCountReset?.(resetCount); } }; if (!isOpen) return null; return (
{/* 拖动头部 */}
{/* 左侧面板 */}
{/* 头部 */}

聊天

{/* 标签页 */}
{/* 搜索栏 */}
{activeTab === 'chat' ? (
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
) : (
{ setFriendSearchQuery(e.target.value); }} className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
{/* 搜索结果显示在搜索框下方 */} {searchResults.length > 0 && (

搜索结果

{searchResults.map((user) => (
{/* 用户头像 */} {user.nickname { const target = e.target as HTMLImageElement; target.style.display = 'none'; target.nextElementSibling?.classList.remove('hidden'); }} />
{(user.nickname || user.username).charAt(0).toUpperCase()}
{user.nickname || user.username}
{isFriend(user.username) ? '已是好友' : '陌生人'}
{!isFriend(user.username) && ( )}
))}
)}
)}
{/* 列表内容 */}
{activeTab === 'chat' ? (
{filteredConversations.map((conv) => { // 获取对话头像 - 私人对话显示对方头像,群聊显示群组图标 const getConversationAvatar = () => { if (conv.participants.length === 2) { // 私人对话:显示对方用户的头像 const otherUser = conv.participants.find(p => p !== currentUser?.username); return otherUser ? (
{getDisplayName(otherUser)} { const target = e.target as HTMLImageElement; target.style.display = 'none'; target.nextElementSibling?.classList.remove('hidden'); }} />
{getDisplayName(otherUser).charAt(0).toUpperCase()}
{/* 在线状态指示器 */}
) : null; } else { // 群聊:显示群组图标和参与者头像叠加 const firstThreeParticipants = conv.participants.slice(0, 3); return (
{/* 群聊成员数量指示 */}
{conv.participants.length}
); } }; return ( ); })}
) : (
{/* 好友申请 */} {friendRequests.filter(req => req.to_user === currentUser?.username && req.status === 'pending').length > 0 && (

好友申请

{friendRequests .filter(req => req.to_user === currentUser?.username && req.status === 'pending') .map((request) => (
{/* 申请者头像 */}
{request.from_user} { const target = e.target as HTMLImageElement; target.style.display = 'none'; target.nextElementSibling?.classList.remove('hidden'); }} />
{request.from_user.charAt(0).toUpperCase()}
{/* 申请者信息 */}
{request.from_user}
{new Date(request.created_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
{request.message}
))}
)} {/* 好友列表 */}

我的好友

{friends.map((friend) => ( ))}
)}
{/* 右侧聊天区域 */}
{selectedConversation ? ( <> {/* 聊天头部 */}
{/* 对话头像(显示对方用户的头像,如果是群聊则显示群组图标) */}
{selectedConversation.participants.length === 2 ? ( // 私人对话:显示对方的头像 (() => { const otherUser = selectedConversation.participants.find(p => p !== currentUser?.username); return otherUser ? (
{getDisplayName(otherUser)} { const target = e.target as HTMLImageElement; target.style.display = 'none'; target.nextElementSibling?.classList.remove('hidden'); }} />
{getDisplayName(otherUser).charAt(0).toUpperCase()}
{/* 在线状态 */}
) : null; })() ) : ( // 群聊:显示群组图标
)}

{selectedConversation.name}

{selectedConversation.participants.length === 2 ? ( // 私人对话:显示在线状态 (() => { const otherUser = selectedConversation.participants.find(p => p !== currentUser?.username); return otherUser ? ( {isUserOnline(otherUser) ? '在线' : '离线'} {selectedConversation.participants.length} 人 ) : `${selectedConversation.participants.length} 人`; })() ) : ( // 群聊:显示参与者数量 `${selectedConversation.participants.length} 人` )}
{/* 消息列表 */}
{messages.map((message, index) => { const isOwnMessage = message.sender_id === currentUser?.username; const prevMessage = index > 0 ? messages[index - 1] : null; const nextMessage = index < messages.length - 1 ? messages[index + 1] : null; // 每条消息都显示头像 const showName = !prevMessage || prevMessage.sender_id !== message.sender_id; const isSequential = prevMessage && prevMessage.sender_id === message.sender_id; return (
{/* 头像 - 每条消息都显示 */}
{getDisplayName(message.sender_id)} { // 头像加载失败时显示文字头像 const target = e.target as HTMLImageElement; target.style.display = 'none'; target.nextElementSibling?.classList.remove('hidden'); }} />
{getDisplayName(message.sender_id).charAt(0).toUpperCase()}
{/* 在线状态指示器 */}
{/* 消息内容 */}
{/* 发送者名称(仅在非连续消息时显示) */} {!isOwnMessage && showName && (
{getDisplayName(message.sender_id)}
)} {/* 消息气泡 */}
{message.message_type === 'image' ? (
图片消息 { // 点击图片放大查看 const img = new Image(); img.src = message.content; const newWindow = window.open(''); if (newWindow) { newWindow.document.write(` 图片查看 `); } }} /> {/* 图片遮罩 */}
) : (
{message.content}
)} {/* 消息气泡装饰尾巴 */}
{/* 时间戳显示在消息气泡下方 */}
{formatMessageTime(message.timestamp)}
); })}
{/* 消息列表底部装饰 */}
{/* 消息输入区域 */}
{/* 表情选择器 */} {showEmojiPicker && (

选择表情

{emojis.map((emoji, index) => ( ))}
)} {/* 主输入区域 */}
{/* 顶部工具栏 */}
{/* 左侧功能按钮组 */}
{/* 表情按钮 */} {/* 图片上传按钮 */} {/* 隐藏的文件输入 */} {/* 附件按钮(预留) */}
{/* 右侧状态指示 */}
{/* 字符计数 */} {newMessage.length > 0 && ( 500 ? 'text-red-500' : ''}> {newMessage.length}/1000 )} {/* 连接状态 */}
{isConnected ? '在线' : '离线'}
{/* 消息输入区域 */}