Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
forbidals
/
student_panel
/
src
/
components
/
chat
:
ChatPage.tsx
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
'use client'; // app/components/chat/ChatPage.tsx import React, { useState, useEffect, useRef } from 'react'; import { ChatSidebar } from './ChatSidebar'; import { ChatWindow } from './ChatWindow'; import Breadcrumb from '../ui/pages/dashboard/Breadcrumb'; import { BiHomeSmile } from 'react-icons/bi'; import { useGetClassSectionTeachers } from '@/lib/api/student/queryHooks'; import { useSelector } from 'react-redux'; import type { RootState } from '../store'; import { useTranslate } from '@/components/hooks/useTranslate'; import { useNotification } from '@/lib/firebase/NotificationContext'; import { useQueryClient } from '@tanstack/react-query'; // Define the contact type type Contact = { id: number; name: string; lastMessage: string; time: string; avatar: string; unread: number; active: boolean; }; // Props for ChatPage type ChatPageProps = { initialTeacherId?: number; // Optional teacher ID to auto-open chat }; const ChatPage = ({ initialTeacherId }: ChatPageProps) => { const translate = useTranslate(); const queryClient = useQueryClient(); // State for managing selected contact and mobile view const [selectedContact, setSelectedContact] = useState<Contact | null>(null); const [showChatWindow, setShowChatWindow] = useState(false); const { lastNotification } = useNotification(); // Get the logged-in student's class section ID from Redux store // This is needed to fetch the teacher's data if initialTeacherId is provided const user = useSelector((state: RootState) => state.studentAuth.user); const classSectionId = user?.class_section?.id?.toString() || null; // Fetch teachers for the student's class section // This is only needed if initialTeacherId is provided const { data: teachersResponse, isLoading: isLoadingTeachers } = useGetClassSectionTeachers(classSectionId); // Effect to auto-select teacher chat when initialTeacherId is provided // This runs when the component mounts or when initialTeacherId changes useEffect(() => { // Only proceed if we have an initialTeacherId and teachers data is loaded if (initialTeacherId && teachersResponse?.data && !isLoadingTeachers) { const teachers = teachersResponse.data; // Find the teacher with the matching ID const selectedTeacher = teachers.find((t) => t.id === initialTeacherId); if (selectedTeacher) { // Create a contact object for the selected teacher const teacherContact: Contact = { id: selectedTeacher.id, name: selectedTeacher.full_name, lastMessage: translate('startConversation'), time: 'now', // Use teacher image if available, otherwise use initials avatar: selectedTeacher.image || selectedTeacher.full_name .split(' ') .map((n) => n[0]) .join('') .toUpperCase(), unread: 0, active: true, }; // Set the selected contact to open the chat automatically setSelectedContact(teacherContact); setShowChatWindow(true); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialTeacherId, teachersResponse, isLoadingTeachers]); // Effect to handle incoming chat notifications useEffect(() => { if (lastNotification?.data) { const data = lastNotification.data; // Check if it's a chat notification and has a sender_id // Field names might vary, checking common ones: sender_id, user_id const rawSenderId = data.sender_id || data.user_id; const senderId = rawSenderId ? parseInt(rawSenderId) : null; if (senderId) { // If we're already chatting with this person, don't do anything // (ChatWindow already handles refetching) if (selectedContact?.id === senderId) return; // Try to find the sender in the loaded teachers list first let senderContact: Contact | null = null; if (teachersResponse?.data) { const teacher = teachersResponse.data.find((t) => t.id === senderId); if (teacher) { senderContact = { id: teacher.id, name: teacher.full_name, lastMessage: data.message || translate('newMessage'), time: 'now', avatar: teacher.image || '', unread: 0, active: true, }; } } // If not found in teachers or list not loaded, try using data from notification if (!senderContact && data.sender_name) { senderContact = { id: senderId, name: data.sender_name, lastMessage: data.message || translate('newMessage'), time: 'now', avatar: data.sender_image || '', unread: 0, active: true, }; } // If we managed to build a contact object, select it if (senderContact) { setSelectedContact(senderContact); setShowChatWindow(true); } } } }, [lastNotification, teachersResponse, selectedContact?.id, translate]); // Effect to handle Reverb WebSocket connection const wsRef = useRef<WebSocket | null>(null); const reconnectAttemptsRef = useRef(0); const maxReconnectAttempts = 10; useEffect(() => { const userId = user?.id; if (!userId) return; let isSubscribed = true; const connect = () => { if (!isSubscribed) return; try { const ws = new WebSocket(process.env.NEXT_PUBLIC_REVERB_URL || ''); wsRef.current = ws; ws.onopen = () => { console.log( '[Reverb] WebSocket connected, waiting for connection_established...', ); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); const eventName = data.event; if (eventName === 'pusher:error') { console.log('[Reverb] ⚠️ ERROR from server:', data.data); return; } if (eventName === 'pusher:connection_established') { console.log('[Reverb] ✅ Connection established!'); reconnectAttemptsRef.current = 0; const subscribeMsg = JSON.stringify({ event: 'pusher:subscribe', data: { channel: `user.${userId}` }, }); ws.send(subscribeMsg); return; } if (eventName === 'pusher_internal:subscription_succeeded') { console.log(`[Reverb] ✅ Subscribed to channel: user.${userId}`); return; } if (eventName === 'pusher:ping') { ws.send(JSON.stringify({ event: 'pusher:pong', data: {} })); return; } if ( eventName === 'NewMessage' || eventName === 'App\\Events\\NewMessage' ) { const payload = typeof data.data === 'string' ? JSON.parse(data.data) : data.data; const messageData = payload.message; if (messageData) { const senderId = messageData.sender_id || messageData.user_id; // Invalidate queries to refresh chat window and sidebar if (senderId) { queryClient.invalidateQueries({ queryKey: ['chat', 'messages', 'infinite', senderId], }); queryClient.invalidateQueries({ queryKey: ['chat', 'history', 'infinite'], }); queryClient.invalidateQueries({ queryKey: ['chat', 'users', 'infinite'], }); } } } } catch (e) { console.error('[Reverb] Error parsing event:', e); } }; ws.onclose = () => { if (!isSubscribed) return; console.log('[Reverb] Connection closed, reconnecting...'); reconnect(); }; ws.onerror = (error) => { if (!isSubscribed) return; console.log('[Reverb] Connection error:', error, ', reconnecting...'); }; } catch (e) { console.log('[Reverb] Connection exception:', e); if (isSubscribed) reconnect(); } }; const reconnect = () => { if (wsRef.current) { wsRef.current.onclose = null; // Prevent multiple reconnect calls wsRef.current.close(); wsRef.current = null; } reconnectAttemptsRef.current++; if (reconnectAttemptsRef.current > maxReconnectAttempts) { console.log( `[Reverb] Max reconnection attempts (${maxReconnectAttempts}) reached. Giving up.`, ); return; } // Exponential backoff: 3s, 6s, 12s, 24s ... capped at 60s const baseDelay = 3000; const delay = Math.min( baseDelay * (1 << (reconnectAttemptsRef.current - 1)), 60000, ); console.log( `[Reverb] Reconnecting in ${delay / 1000}s (attempt ${reconnectAttemptsRef.current}/${maxReconnectAttempts})...`, ); setTimeout(() => { if (isSubscribed) { connect(); } }, delay); }; connect(); return () => { isSubscribed = false; if (wsRef.current) { wsRef.current.onclose = null; wsRef.current.close(); wsRef.current = null; } }; }, [user?.id, queryClient]); const breadcrumbItems = [ { label: translate('home'), href: '/student/dashboard', icon: <BiHomeSmile className="w-5 h-5" />, }, { label: translate('chats'), }, ]; // Handle contact selection const handleContactSelect = (contact: Contact) => { setSelectedContact(contact); setShowChatWindow(true); }; // Handle back to contact list on mobile const handleBackToContacts = () => { setShowChatWindow(false); }; return ( <> <Breadcrumb title={translate('chats')} items={breadcrumbItems} /> {/* Desktop Layout - Always show both sidebar and chat */} <div className="hidden lg:flex h-[calc(100vh-140px)] font-sans text-gray-800 bg-white border border-gray-200 rounded-[12px] overflow-hidden"> <ChatSidebar onContactSelect={handleContactSelect} selectedContact={selectedContact} /> <ChatWindow selectedContact={selectedContact} /> </div> {/* Mobile Layout - Toggle between sidebar and chat */} <div className="lg:hidden flex h-[calc(100vh-120px)] font-sans text-gray-800 bg-white border border-gray-200 rounded-[12px] overflow-hidden"> {!showChatWindow ? ( <ChatSidebar onContactSelect={handleContactSelect} selectedContact={selectedContact} /> ) : ( <ChatWindow selectedContact={selectedContact} onBackToContacts={handleBackToContacts} isMobile={true} /> )} </div> </> ); }; export default ChatPage;