File "ChatPage.tsx"
Full Path: /home/trinadezambia/public_html/student_panel/src/components/ui/pages/ChatPage.tsx
File size: 10.93 KB
MIME-type: text/x-java
Charset: utf-8
'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;