diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 325e458..2e9e0b9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,7 +11,116 @@ import { AdminDashboard } from './pages/AdminDashboard'; import { ShareJourneyPage } from './pages/ShareJourneyPage'; import { TourNavigationPage } from './pages/TourNavigationPage'; import { ConfirmProvider } from './hooks/useConfirm'; -import { NotificationProvider } from './hooks/useNotification'; +import { NotificationProvider, useNotification } from './hooks/useNotification'; +import { io } from 'socket.io-client'; + +interface GlobalNotificationListenerProps { + user: any; + currentPage: string; + currentTourId: string | null; +} + +const GlobalNotificationListener: React.FC = ({ user, currentPage, currentTourId }) => { + const notify = useNotification(); + + // Request browser Notification permission once on mount + useEffect(() => { + if ('Notification' in window && Notification.permission === 'default') { + Notification.requestPermission(); + } + }, []); + + useEffect(() => { + if (!user?.id) return; + + const socketInstance = io(); + + socketInstance.on('connect', () => { + console.log('[WS] Global notification socket connected:', socketInstance.id); + socketInstance.emit('joinUser', user.id); + }); + + socketInstance.on('tourMessageNotification', (data: any) => { + // Dispatch custom window event + window.dispatchEvent(new CustomEvent('app:tourMessageNotification', { detail: data })); + + // Check if user is actively viewing this specific tour chat + const isViewingThisTourChat = currentPage === 'tourDetail' && currentTourId === data.tourId && (window as any).activeTourChatTab; + + if (!isViewingThisTourChat) { + 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' + }); + + // Show push notification on desktop browser + if ('Notification' in window && Notification.permission === 'granted') { + try { + new Notification(`Tin nhắn mới trong tour "${data.tourTitle}"`, { + body: `${data.senderName}: ${data.content}`, + icon: '/favicon.ico' + }); + } catch (e) { + console.error('System Notification error:', e); + } + } + } + }); + + socketInstance.on('messageReceived', (message: any) => { + // Dispatch custom window event + window.dispatchEvent(new CustomEvent('app:messageReceived', { detail: message })); + + // Check if user is actively chatting with the sender + const isChattingWithSender = currentPage === 'dashboard' && (window as any).activeChatUserId === message.senderId; + + if (!isChattingWithSender) { + 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 on desktop browser + if ('Notification' in window && Notification.permission === 'granted') { + try { + new Notification(`Tin nhắn mới từ ${message.sender?.name || 'Thành viên'}`, { + body: message.content, + icon: '/favicon.ico' + }); + } catch (e) { + console.error('System Notification error:', e); + } + } + } + }); + + socketInstance.on('connectionAccepted', (data: any) => { + window.dispatchEvent(new CustomEvent('app:connectionAccepted', { detail: data })); + 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' + }); + }); + + socketInstance.on('joinRequestAccepted', (data: any) => { + window.dispatchEvent(new CustomEvent('app:joinRequestAccepted', { detail: data })); + 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' + }); + }); + + return () => { + socketInstance.disconnect(); + }; + }, [user?.id, currentPage, currentTourId]); + + return null; +}; function App() { const params = new URLSearchParams(window.location.search); @@ -223,7 +332,14 @@ function App() { return ( - {(() => { + {user && ( + + )} + {(() => { if (currentPage === 'admin') { return ( = ({ tourId, embedded = false }) const mentionRef = useRef(null); const getHeaders = () => ({ - 'Authorization': `Bearer ${localStorage.getItem('token')}`, + 'Authorization': `Bearer ${localStorage.getItem('token') || localStorage.getItem('guest_token')}`, 'Content-Type': 'application/json' }); const currentUserId = (() => { try { - const token = localStorage.getItem('token'); + const token = localStorage.getItem('token') || localStorage.getItem('guest_token'); if (!token) return null; const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); @@ -311,7 +311,7 @@ export const TourChat: React.FC = ({ tourId, embedded = false }) const res = await fetch('/api/v1/upload', { method: 'POST', headers: { - 'Authorization': `Bearer ${localStorage.getItem('token')}` + 'Authorization': `Bearer ${localStorage.getItem('token') || localStorage.getItem('guest_token')}` }, body: formData }); @@ -562,7 +562,7 @@ export const TourChat: React.FC = ({ tourId, embedded = false }) )} {/* Locked Chat Input Footer */} -
+
{/* Mention list dropdown */} {showMentionList && filteredParticipants.length > 0 && (
= ({ tourId, embedded = false })