import React, { useEffect, useState, useRef } from 'react'; import { io, Socket } from 'socket.io-client'; import { Compass, Users, Image as ImageIcon, MessageSquare, LogOut, Search, Check, X, Trash2, Send, UserPlus, Clock, ChevronRight, ChevronLeft, Shield, Calendar, MapPin, Download, Loader2, Bell, BellOff, ShieldAlert } from 'lucide-react'; import { useTourStore } from '@/store/useTourStore'; import { useNotification } from '@/hooks/useNotification'; import { useConfirm } from '@/hooks/useConfirm'; import { MyPhotosPage } from './MyPhotosPage'; import { useTranslation } from '../hooks/useTranslation'; import { useTheme } from '../hooks/useTheme'; interface MemberDashboardProps { user: any; onLogout: () => void; onExploreTours: () => void; onViewTour: (tourId: string, fromPage?: 'explore' | 'dashboard') => void; onOpenMyPhotos: () => void; } export const MemberDashboard: React.FC = ({ user, onLogout, onExploreTours, onViewTour }) => { const notify = useNotification(); const confirm = useConfirm(); const { t, lang, changeLanguage } = useTranslation(); const { theme, changeTheme } = useTheme(); const publicTours = useTourStore(state => state.publicTours); const fetchPublicTours = useTourStore(state => state.fetchPublicTours); const [activeTab, setActiveTab] = useState<'tours' | 'connections' | 'photos' | 'chats'>('tours'); const [connectionSubTab, setConnectionSubTab] = useState<'list' | 'search' | 'pending'>('list'); // Connection states const [connections, setConnections] = useState([]); const [receivedRequests, setReceivedRequests] = useState([]); const [sentRequests, setSentRequests] = useState([]); // User search states const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [searchingUsers, setSearchingUsers] = useState(false); // Photos states const [photos, setPhotos] = useState([]); // Chat states const [activeChatUser, setActiveChatUser] = useState(null); const [chatMessages, setChatMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const messagesEndRef = useRef(null); const socketRef = useRef(null); const [unreadChatSenders, setUnreadChatSenders] = useState([]); const [unreadTourSenders, setUnreadTourSenders] = useState([]); const [unreadTourChats, setUnreadTourChats] = useState([]); const hasNotifications = unreadChatSenders.length > 0 || unreadTourChats.length > 0 || unreadTourSenders.length > 0 || receivedRequests.length > 0; const [selectedImage, setSelectedImage] = useState(null); const [imagePreview, setImagePreview] = useState(null); const [attachedLocation, setAttachedLocation] = useState<{ latitude: number; longitude: number } | null>(null); const [isUploading, setIsUploading] = useState(false); const [isLocating, setIsLocating] = useState(false); // Emergency share states const [sharingTour, setSharingTour] = useState(null); const [shareStatus, setShareStatus] = useState(null); const [loadingShare, setLoadingShare] = useState(false); const handleOpenShareModal = async (tour: any) => { setSharingTour(tour); setShareStatus(null); setLoadingShare(true); try { const token = localStorage.getItem('token'); const res = await fetch(`/api/v1/tours/${tour.id}/share`, { headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const data = await res.json(); setShareStatus(data); } } catch (e) { console.error('Error fetching share status:', e); } finally { setLoadingShare(false); } }; const handleToggleShare = async (isEnabled: boolean) => { if (!sharingTour) return; try { const token = localStorage.getItem('token'); const res = await fetch(`/api/v1/tours/${sharingTour.id}/share`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ isEnabled }) }); if (res.ok) { const data = await res.json(); setShareStatus(data); notify({ title: 'Thành công', message: isEnabled ? 'Đã bật chia sẻ hành trình cứu hộ.' : 'Đã tắt chia sẻ.', type: 'success' }); } } catch (e) { console.error(e); } }; const fileInputRef = useRef(null); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const [muteNotifications, setMuteNotifications] = useState(() => { return localStorage.getItem('muteNotifications') === 'true'; }); const muteNotificationsRef = useRef(muteNotifications); const [mobileShowDetail, setMobileShowDetail] = useState(false); useEffect(() => { const handleResize = () => { setIsMobile(window.innerWidth < 768); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Request browser Notification permission useEffect(() => { if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } }, []); const showSystemNotification = (title: string, body: string) => { if (muteNotificationsRef.current) return; if ('Notification' in window && Notification.permission === 'granted') { try { new Notification(title, { body, icon: '/favicon.ico' }); } catch (e) { console.error('System Notification error:', e); } } }; const isMemberOfMyTours = (senderId: string) => { if (!publicTours) return false; const userTours = publicTours.filter(tour => tour.participants?.some((p: any) => p.userId === user?.id) ); return userTours.some(tour => tour.participants?.some((p: any) => p.userId === senderId && p.userId !== user?.id) ); }; // Compress image to 2K (max 2048px longest side) const compressImageTo2K = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = (event) => { const img = new Image(); img.src = event.target?.result as string; img.onload = () => { const MAX_DIM = 2048; let width = img.width; let height = img.height; if (width > MAX_DIM || height > MAX_DIM) { if (width > height) { height = Math.round((height * MAX_DIM) / width); width = MAX_DIM; } else { width = Math.round((width * MAX_DIM) / height); height = MAX_DIM; } } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) { resolve(file); return; } ctx.drawImage(img, 0, 0, width, height); canvas.toBlob( (blob) => { if (blob) { resolve(blob); } else { resolve(file); } }, 'image/jpeg', 0.85 ); }; img.onerror = (err) => reject(err); }; reader.onerror = (err) => reject(err); }); }; // Upload image to backend const uploadImage = async (file: File): Promise => { try { setIsUploading(true); const compressedBlob = await compressImageTo2K(file); const formData = new FormData(); formData.append('image', compressedBlob, 'compressed.jpg'); const res = await fetch('/api/v1/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: formData }); if (res.ok) { const data = await res.json(); return data.url; } return null; } catch (err) { console.error('Lỗi upload ảnh:', err); return null; } finally { setIsUploading(false); } }; // Handle Location Sharing const handleGetLocation = () => { if (!navigator.geolocation) { notify({ title: 'Không hỗ trợ', message: 'Trình duyệt của bạn không hỗ trợ định vị GPS.', type: 'error' }); return; } setIsLocating(true); navigator.geolocation.getCurrentPosition( (position) => { setAttachedLocation({ latitude: position.coords.latitude, longitude: position.coords.longitude }); notify({ title: 'Gắn vị trí thành công', message: 'Vị trí hiện tại đã được đính kèm vào tin nhắn.', type: 'success' }); setIsLocating(false); }, (error) => { console.error('Lỗi định vị:', error); notify({ title: 'Lỗi GPS', message: 'Không thể lấy vị trí hiện tại của bạn. Hãy kiểm tra quyền truy cập.', type: 'error' }); setIsLocating(false); }, { enableHighAccuracy: true, timeout: 10000 } ); }; // Download image file helper const handleDownloadImage = async (url: string, id: string) => { try { const response = await fetch(url); const blob = await response.blob(); const blobUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = blobUrl; link.download = `chat-photo-${id}.jpg`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(blobUrl); } catch (error) { console.error('Lỗi tải ảnh:', error); window.open(url, '_blank'); } }; const handleSelectTab = (tab: 'tours' | 'connections' | 'photos' | 'chats') => { setActiveTab(tab); if (tab === 'tours') { setUnreadTourSenders([]); setUnreadTourChats([]); } if (window.innerWidth < 768) { setMobileShowDetail(true); } }; // Fetch connections, tours, and photos on mount useEffect(() => { fetchPublicTours(); fetchConnections(); fetchPhotos(); }, []); const fetchConnectionsRef = useRef<() => Promise>(null as any); const fetchPublicToursRef = useRef<() => Promise>(null as any); const isMemberOfMyToursRef = useRef<(senderId: string) => boolean>(null as any); useEffect(() => { fetchConnectionsRef.current = fetchConnections; fetchPublicToursRef.current = fetchPublicTours; isMemberOfMyToursRef.current = isMemberOfMyTours; muteNotificationsRef.current = muteNotifications; }); // Socket connection for realtime messaging useEffect(() => { if (!user?.id) return; // Connect to WebSocket using same origin/proxy const socket = io(); socketRef.current = socket; socket.on('connect', () => { console.log('[WS] MemberDashboard connected:', socket.id); socket.emit('joinUser', user.id); }); socket.on('messageReceived', (message: any) => { // If we are actively chatting with the sender of this message if (activeChatUser && (message.senderId === activeChatUser.id || message.receiverId === activeChatUser.id)) { setChatMessages(prev => [...prev, message]); } else { if (!muteNotificationsRef.current) { // Show notification toast for new message notify({ title: 'Tin nhắn mới', message: `${message.sender?.name || 'Ai đó'} gửi: "${message.content.substring(0, 30)}${message.content.length > 30 ? '...' : ''}"`, type: 'success' }); // Show push notification showSystemNotification( `Tin nhắn mới từ ${message.sender?.name || 'Thành viên'}`, message.content ); } // Add to unread states setUnreadChatSenders(prev => prev.includes(message.senderId) ? prev : [...prev, message.senderId]); if (isMemberOfMyToursRef.current(message.senderId)) { setUnreadTourSenders(prev => prev.includes(message.senderId) ? prev : [...prev, message.senderId]); } } }); socket.on('connectionAccepted', (data: any) => { if (!muteNotificationsRef.current) { notify({ title: 'Kết nối mới', message: `${data.acceptedByName} đã chấp nhận yêu cầu kết nối của bạn.`, type: 'success' }); } fetchConnectionsRef.current(); }); socket.on('tourMessageNotification', (data: any) => { if (!muteNotificationsRef.current) { notify({ title: `Tin nhắn mới trong tour "${data.tourTitle}"`, message: `${data.senderName}: "${data.content.substring(0, 30)}${data.content.length > 30 ? '...' : ''}"`, type: 'success' }); showSystemNotification( `Tin nhắn mới trong tour "${data.tourTitle}"`, `${data.senderName}: ${data.content}` ); } setUnreadTourChats(prev => prev.includes(data.tourId) ? prev : [...prev, data.tourId]); }); socket.on('joinRequestAccepted', (data: any) => { if (!muteNotificationsRef.current) { notify({ title: 'Yêu cầu tham gia được duyệt', message: `Yêu cầu tham gia hành trình "${data.tourTitle}" của bạn đã được chấp nhận!`, type: 'success' }); } fetchPublicToursRef.current(); }); return () => { socket.disconnect(); }; }, [user?.id, activeChatUser]); // Autoscroll chat to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [chatMessages]); const getHeaders = () => ({ 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json' }); const fetchConnections = async () => { try { const res = await fetch('/api/v1/connections', { headers: getHeaders() }); if (res.ok) { const data = await res.json(); setConnections(data.connections || []); setReceivedRequests(data.receivedRequests || []); setSentRequests(data.sentRequests || []); } } catch (err) { console.error('Lỗi khi tải danh sách kết nối:', err); } }; const fetchPhotos = async () => { try { const res = await fetch('/api/v1/users/me/photos', { headers: getHeaders() }); if (res.ok) { const data = await res.json(); setPhotos(data || []); } } catch (err) { console.error('Lỗi tải ảnh cá nhân:', err); } }; // Search system users useEffect(() => { const delayDebounce = setTimeout(() => { if (searchQuery.trim().length >= 2) { performUserSearch(); } else { setSearchResults([]); } }, 400); return () => clearTimeout(delayDebounce); }, [searchQuery]); const performUserSearch = async () => { setSearchingUsers(true); try { const res = await fetch(`/api/v1/users?q=${encodeURIComponent(searchQuery)}`, { headers: getHeaders() }); if (res.ok) { const data = await res.json(); setSearchResults(data || []); } } catch (err) { console.error('Lỗi tìm kiếm thành viên:', err); } finally { setSearchingUsers(false); } }; // Send a connection request const handleSendConnectionRequest = async (receiverId: string) => { try { const res = await fetch('/api/v1/connections', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ receiverId }) }); const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Gửi lời mời thất bại.'); notify({ title: 'Thành công', message: 'Đã gửi lời mời kết nối thành công.', type: 'success' }); fetchConnections(); // Refresh search results to show pending status performUserSearch(); } catch (err: any) { notify({ title: 'Lỗi', message: err.message, type: 'error' }); } }; // Accept or Reject connection request const handleUpdateConnectionStatus = async (connId: string, status: 'ACCEPTED' | 'REJECTED') => { try { const res = await fetch(`/api/v1/connections/${connId}`, { method: 'PATCH', headers: getHeaders(), body: JSON.stringify({ status }) }); if (res.ok) { notify({ title: 'Thành công', message: status === 'ACCEPTED' ? 'Đã chấp nhận kết nối.' : 'Đã từ chối kết nối.', type: 'success' }); fetchConnections(); } } catch (err) { console.error('Lỗi cập nhật kết nối:', err); } }; // Change classification: FRIEND <-> FAMILY const handleChangeConnectionType = async (connId: string, type: 'FRIEND' | 'FAMILY') => { try { const res = await fetch(`/api/v1/connections/${connId}`, { method: 'PATCH', headers: getHeaders(), body: JSON.stringify({ type }) }); if (res.ok) { notify({ title: 'Đã cập nhật', message: `Mối quan hệ đã được chuyển sang nhóm: ${type === 'FAMILY' ? 'Gia đình' : 'Bạn bè'}.`, type: 'success' }); fetchConnections(); } } catch (err) { console.error('Lỗi chuyển nhóm kết nối:', err); } }; // Remove connection const handleRemoveConnection = async (connId: string, targetName: string) => { const isConfirmed = await confirm({ title: 'Hủy kết nối', message: `Bạn có chắc chắn muốn hủy kết nối với ${targetName}?` }); if (isConfirmed) { try { const res = await fetch(`/api/v1/connections/${connId}`, { method: 'DELETE', headers: getHeaders() }); if (res.ok) { notify({ title: 'Thành công', message: 'Đã hủy kết nối thành công.', type: 'success' }); fetchConnections(); if (activeChatUser && connections.find(c => c.id === connId)?.targetUser?.id === activeChatUser.id) { setActiveChatUser(null); } } } catch (err) { console.error('Lỗi hủy kết nối:', err); } } }; // Fetch chat messages const handleSelectChatUser = async (targetUser: any) => { setActiveChatUser(targetUser); setUnreadChatSenders(prev => prev.filter(id => id !== targetUser.id)); setUnreadTourSenders(prev => prev.filter(id => id !== targetUser.id)); setChatMessages([]); try { const res = await fetch(`/api/v1/messages/${targetUser.id}`, { headers: getHeaders() }); if (res.ok) { const data = await res.json(); setChatMessages(data || []); } } catch (err) { console.error('Lỗi tải tin nhắn:', err); } }; // Send chat message const handleSendMessage = async (e?: React.FormEvent) => { if (e) e.preventDefault(); if (!newMessage.trim() && !selectedImage && !attachedLocation) return; if (!activeChatUser) return; let attachmentUrl = undefined; if (selectedImage) { attachmentUrl = await uploadImage(selectedImage); if (!attachmentUrl) { notify({ title: 'Lỗi', message: 'Không thể tải ảnh đính kèm lên server.', type: 'error' }); return; } } const payload = { receiverId: activeChatUser.id, content: newMessage, attachmentUrl, latitude: attachedLocation?.latitude, longitude: attachedLocation?.longitude }; setNewMessage(''); setSelectedImage(null); setImagePreview(null); setAttachedLocation(null); try { const res = await fetch('/api/v1/messages', { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); if (res.ok) { const data = await res.json(); setChatMessages(prev => [...prev, data]); } else { notify({ title: 'Lỗi', message: 'Không thể gửi tin nhắn.', type: 'error' }); } } catch (err) { console.error('Lỗi gửi tin nhắn:', err); } }; // Filter tours where the current logged-in user is a participant const myTours = React.useMemo(() => { if (!publicTours) return []; return publicTours.filter(tour => tour.participants?.some((p: any) => p.userId === user?.id) ); }, [publicTours, user?.id]); // Check relationship status for query results const getConnectionStatusText = (targetId: string) => { const isConnected = connections.find(c => c.targetUser?.id === targetId); if (isConnected) { return isConnected.type === 'FAMILY' ? 'Gia đình' : 'Bạn bè'; } const isPendingReceived = receivedRequests.find(r => r.requester?.id === targetId); if (isPendingReceived) return 'Chờ bạn duyệt'; const isPendingSent = sentRequests.find(s => s.receiver?.id === targetId); if (isPendingSent) return 'Đã gửi yêu cầu'; return null; }; // Helper function to render a user's initials avatar const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') => { const initials = name ? name.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase() : 'U'; return (
{initials}
); }; // Define mobile back button handler const handleMobileBack = () => { if (activeTab === 'chats' && activeChatUser) { setActiveChatUser(null); } else { setMobileShowDetail(false); } }; const showMobileHeader = isMobile && mobileShowDetail && activeTab !== 'photos' && !(activeTab === 'chats' && activeChatUser); if (isMobile && !mobileShowDetail) { return (
{/* User Card */}
{user?.avatar ? ( {user.name} ) : ( renderInitialsAvatar(user?.name || 'User', 'w-20 h-20 text-2xl') )}

{user?.name || 'Thành viên'}

{user?.email}

{user?.isAdmin && ( Quản trị viên )}
{/* Mobile Language & Theme Selectors */}
{t('languageSelect') || 'Ngôn ngữ'}
{t('themeSelect') || 'Giao diện'}
{/* Explore Map Quick Button */}
{/* Menu Items List */}
{/* Bottom Menu Items */}
{/* Horizontal Divider ---- */}
{/* Logout Section */}
); } return (
{/* Background Glow */}
{/* Mobile Detail Header: Only on Mobile detail mode */} {showMobileHeader && (

{activeTab === 'tours' && 'Hành trình của tôi'} {activeTab === 'connections' && 'Danh sách bạn bè'} {activeTab === 'chats' && 'Trò chuyện trực tiếp'}

)} {/* Left Sidebar Menu (Desktop only) */} {!isMobile && ( )} {/* Main Content Area */}
{/* Background Glow */} {!isMobile && ( <>
)} {/* Tab content renders here */}
{/* TAB 1: MY TOURS */} {activeTab === 'tours' && (

Hành trình của tôi

Danh sách các chuyến đi bạn tham gia (với vai trò chủ sở hữu, quản lý hoặc thành viên).

{myTours.length === 0 ? (

Chưa có hành trình nào

Bạn chưa tham gia bất kỳ hành trình nào. Hãy bắt đầu bằng cách tìm các hành trình công khai hoặc tạo chuyến đi mới.

) : (
{myTours.map((tour) => { const status = (() => { const now = new Date(); const start = tour.startDate ? new Date(tour.startDate) : null; const end = tour.endDate ? new Date(tour.endDate) : null; if (end && end < now) return { color: 'gray', label: 'Đã hoàn thành', bg: 'bg-slate-800/80 border-slate-700 text-slate-400' }; if (start && end && start <= now && end >= now) return { color: 'red', label: 'Đang diễn ra', bg: 'bg-rose-900/30 border-rose-800/50 text-rose-300' }; return { color: 'green', label: 'Mới tạo / Sắp tới', bg: 'bg-emerald-900/30 border-emerald-800/50 text-emerald-300' }; })(); const participant = tour.participants?.find((p: any) => p.userId === user?.id); const roleLabel = participant?.role === 'OWNER' ? 'Chủ tour' : participant?.role === 'MANAGER' ? 'Quản lý' : 'Thành viên'; return (
{status.label}
{unreadTourChats.includes(tour.id) && ( Tin nhắn mới )} {roleLabel}

{tour.title}

{tour.description && (

{tour.description}

)} {/* Tags */} {tour.tags && tour.tags.length > 0 && (
{tour.tags.map((tag: string) => ( {tag} ))}
)}
{tour.startDate ? new Date(tour.startDate).toLocaleDateString('vi-VN') : 'N/A'} {' - '} {tour.endDate ? new Date(tour.endDate).toLocaleDateString('vi-VN') : 'N/A'}
); })}
)}
)} {/* TAB 2: CONNECTIONS */} {activeTab === 'connections' && (

Danh sách bạn bè

Quản lý các mối quan hệ bạn bè, gia đình, duyệt các yêu cầu kết nối từ thành viên khác.

{/* Sub-tabs for connections */}
{/* SUB TAB CONTENT 2.1: LIST CONNECTIONS */} {connectionSubTab === 'list' && (
{connections.length === 0 ? (

Chưa có kết nối nào

Bạn chưa kết nối với ai. Hãy chuyển sang tìm kiếm để gửi lời mời.

) : (
{connections.map((conn) => { const connUser = conn.targetUser; return (
{connUser.avatar ? ( {connUser.name} ) : ( renderInitialsAvatar(connUser.name, 'w-12 h-12 text-base') )}

{connUser.name}

{connUser.email}

{conn.type === 'FAMILY' ? 'Gia đình' : 'Bạn bè'}
{/* Classification selector */}
); })}
)}
)} {/* SUB TAB CONTENT 2.2: SEARCH USERS */} {connectionSubTab === 'search' && (
setSearchQuery(e.target.value)} className="w-full bg-slate-950 border border-slate-800/80 rounded-2xl py-3 pl-11 pr-4 text-sm text-white placeholder-slate-500 outline-none focus:border-indigo-500/80 transition-all duration-150" />
{searchingUsers ? (
Đang tìm kiếm...
) : searchQuery.trim().length < 2 ? (
Vui lòng nhập tối thiểu 2 ký tự để tìm kiếm thành viên.
) : searchResults.length === 0 ? (
Không tìm thấy thành viên phù hợp.
) : (
{searchResults.map((usr) => { const statusText = getConnectionStatusText(usr.id); return (
{usr.avatar ? ( {usr.name} ) : ( renderInitialsAvatar(usr.name, 'w-10 h-10 text-xs') )}

{usr.name}

{usr.email}

{statusText ? ( {statusText} ) : ( )}
); })}
)}
)} {/* SUB TAB CONTENT 2.3: PENDING REQUESTS */} {connectionSubTab === 'pending' && (
{/* Received Requests */}

Lời mời nhận được ({receivedRequests.length})

{receivedRequests.length === 0 ? (
Không có lời mời kết nối nào.
) : (
{receivedRequests.map((req) => (
{req.requester?.avatar ? ( {req.requester.name} ) : ( renderInitialsAvatar(req.requester?.name || 'User', 'w-10 h-10 text-xs') )}

{req.requester?.name}

{req.requester?.email}

))}
)}
{/* Sent Requests */}

Yêu cầu đã gửi ({sentRequests.length})

{sentRequests.length === 0 ? (
Không có yêu cầu đang chờ duyệt.
) : (
{sentRequests.map((req) => (
{req.receiver?.avatar ? ( {req.receiver.name} ) : ( renderInitialsAvatar(req.receiver?.name || 'User', 'w-10 h-10 text-xs') )}

{req.receiver?.name}

{req.receiver?.email}

))}
)}
)}
)} {/* TAB 3: PERSONAL PHOTOS */} {activeTab === 'photos' && (
setMobileShowDetail(false) : () => setActiveTab('tours')} />
)} {/* TAB 4: REALTIME CHAT */} {activeTab === 'chats' && (
{/* Chats List sidebar: show if not mobile OR if mobile and no active chat user */} {(!isMobile || !activeChatUser) && (

Chọn người hội thoại

{connections.length === 0 ? (
Bạn cần kết bạn trước khi có thể nhắn tin.
) : ( connections.map((conn) => { const connUser = conn.targetUser; const isActive = activeChatUser && activeChatUser.id === connUser.id; return ( ); }) )}
)} {/* Chat Window Panel: show if not mobile OR if mobile and active chat user */} {(!isMobile || activeChatUser) && (
{activeChatUser ? ( <> {/* Chat Header */}
{isMobile && ( )} {activeChatUser.avatar ? ( {activeChatUser.name} ) : ( renderInitialsAvatar(activeChatUser.name, 'w-10 h-10 text-xs') )}

{activeChatUser.name}

Trực tuyến (Realtime)
{/* Chat Messages Panel */}
{chatMessages.length === 0 ? (
Chưa có tin nhắn nào. Gửi tin nhắn để bắt đầu cuộc hội thoại!
) : ( chatMessages.map((msg) => { const isMe = msg.senderId === user.id; return (
{msg.attachmentUrl && (
Đính kèm
)} {msg.latitude !== undefined && msg.latitude !== null && (
Vị trí hiện tại {msg.latitude.toFixed(6)}, {msg.longitude.toFixed(6)}
)} {msg.content &&

{msg.content}

}
{new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
); }) )}
{/* Previews (Image & GPS Location) */} {(imagePreview || attachedLocation) && (
{imagePreview && (
Preview
)} {attachedLocation && (
Đã đính kèm GPS
)}
)} {/* Chat Input form */}
{ const file = e.target.files?.[0]; if (file) { setSelectedImage(file); setImagePreview(URL.createObjectURL(file)); } }} className="hidden" /> setNewMessage(e.target.value)} placeholder={isUploading ? "Đang tải ảnh lên..." : `Nhắn cho ${activeChatUser.name.split(' ').pop()}...`} disabled={isUploading} className="flex-1 bg-slate-950 border border-slate-800 rounded-2xl py-3 px-4 text-xs text-white placeholder-slate-500 outline-none focus:border-indigo-500/80 transition-all disabled:bg-slate-900 disabled:cursor-not-allowed" />
) : (
Chọn một kết nối bên trái để bắt đầu cuộc trò chuyện trực tiếp!
)}
)}
)} {/* Emergency Share Configuration Modal */} {sharingTour && (
{/* Modal Header */}

{t('emergencyShare')}

{/* Modal Body */}

{sharingTour.title}

{t('emergencyShareTooltip')}

{loadingShare ? (
{t('loading')}
) : ( <> {/* Share Activation Toggle */}
Kích hoạt đường dẫn cứu hộ
{shareStatus && ( )}
{shareStatus?.isEnabled && ( <> {/* Configuration: Language and Theme selectors */}
{/* Shareable Link Input with Copy button */}
Đường dẫn khẩn cấp:
)} )}
{/* Modal Footer */}
)}
); };