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;