diff --git a/app/dashboard/chats/[id]/page.tsx b/app/dashboard/chats/[id]/page.tsx index a4f82af..405e362 100644 --- a/app/dashboard/chats/[id]/page.tsx +++ b/app/dashboard/chats/[id]/page.tsx @@ -1,17 +1,17 @@ +"use client"; + import React from "react"; -import { Metadata } from "next"; +import { useParams } from "next/navigation"; import ChatDetail from "@/components/dashboard/ChatDetail"; import Dashboard from "@/components/dashboard/dashboard"; -export const metadata: Metadata = { - title: "Chat Conversation", - description: "View and respond to customer messages", -}; - -export default function ChatDetailPage({ params }: { params: { id: string } }) { +export default function ChatDetailPage() { + const params = useParams(); + const chatId = params.id as string; + return ( - + ); } \ No newline at end of file diff --git a/components/dashboard/ChatDetail.tsx b/components/dashboard/ChatDetail.tsx index e7d0ef0..967be86 100644 --- a/components/dashboard/ChatDetail.tsx +++ b/components/dashboard/ChatDetail.tsx @@ -174,19 +174,10 @@ export default function ChatDetail({ chatId }: { chatId: string }) { // Function to mark messages as read const markMessagesAsRead = async () => { try { - const authToken = getCookie("Authorization"); - - if (!authToken) return; - - const authAxios = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL, - headers: { - Authorization: `Bearer ${authToken}` - } + // Use clientFetch instead of direct axios + await clientFetch(`/chats/${chatId}/mark-read`, { + method: 'POST' }); - - // Use dedicated endpoint to mark messages as read - await authAxios.post(`/chats/${chatId}/mark-read`); console.log("Marked messages as read"); } catch (error) { console.error("Error marking messages as read:", error); @@ -226,7 +217,25 @@ export default function ChatDetail({ chatId }: { chatId: string }) { setChatData(response); setChat(response); // Set chat data to maintain compatibility - setMessages(Array.isArray(response.messages) ? response.messages : []); + + // Set messages with a transition effect + // If we already have messages, append new ones to avoid jumpiness + if (messages.length > 0) { + const existingMessageIds = new Set(messages.map(m => m._id)); + const newMessages = response.messages.filter( + (msg: Message) => !existingMessageIds.has(msg._id) + ); + + if (newMessages.length > 0) { + setMessages(prev => [...prev, ...newMessages]); + } else { + // If we need to replace all messages (e.g., first load or refresh) + setMessages(Array.isArray(response.messages) ? response.messages : []); + } + } else { + // Initial load + setMessages(Array.isArray(response.messages) ? response.messages : []); + } // Scroll to bottom on initial load setTimeout(() => { @@ -240,61 +249,76 @@ export default function ChatDetail({ chatId }: { chatId: string }) { } }; - // Fetch new messages periodically + // Setup polling for new messages useEffect(() => { - if (!chatId || !chatData) return; - - const checkForNewMessages = async () => { - if (isPollingRef.current) return; - - isPollingRef.current = true; - - try { - // Use clientFetch instead of direct axios calls - const response = await clientFetch(`/chats/${chatId}?markAsRead=true`); - - if (response && Array.isArray(response.messages)) { - // Check if there are new messages - const hasNewMessages = response.messages.length > messages.length; - - // Always update messages to ensure we have the latest state - setMessages(response.messages); - - // If there are new messages and we're near the bottom, scroll down - if (hasNewMessages && isNearBottom()) { - setTimeout(scrollToBottom, 100); - } - - // Play notification sound if there are new buyer messages - if (hasNewMessages) { - const newMessages = response.messages.slice(messages.length); - const hasNewBuyerMessages = newMessages.some((msg: Message) => msg.sender === 'buyer'); - - if (hasNewBuyerMessages) { - playNotificationSound(); - } - } - } - } catch (error) { - console.error("Error checking for new messages:", error); - } finally { - isPollingRef.current = false; + // Set up a polling interval to check for new messages + const pollInterval = setInterval(() => { + if (chatId && !isPollingRef.current) { + pollNewMessages(); } - }; - - // Check for new messages every 3 seconds - const intervalId = setInterval(checkForNewMessages, 3000); + }, 3000); // Poll every 3 seconds return () => { - clearInterval(intervalId); + clearInterval(pollInterval); }; - }, [chatId, chatData, messages.length]); + }, [chatId]); + + // Poll for new messages without replacing existing ones + const pollNewMessages = async () => { + if (!chatId || isPollingRef.current) return; + + isPollingRef.current = true; + + try { + const response = await clientFetch(`/chats/${chatId}`); + + // Update chat metadata + setChatData(response); + setChat(response); + + // Check if there are new messages + if (Array.isArray(response.messages) && response.messages.length > 0) { + // Get existing message IDs to avoid duplicates + const existingIds = new Set(messages.map(m => m._id)); + const newMessages = response.messages.filter((msg: Message) => !existingIds.has(msg._id)); + + if (newMessages.length > 0) { + // Add only new messages to avoid re-rendering all messages + setMessages(prev => [...prev, ...newMessages]); + + // Play notification sound for new buyer messages + const hasBuyerMessage = newMessages.some((msg: Message) => msg.sender === 'buyer'); + if (hasBuyerMessage) { + playNotificationSound(); + } + + // If near bottom, scroll to new messages + if (isNearBottom()) { + setTimeout(scrollToBottom, 50); + } + + // Set timeout to mark new messages as read + if (markReadTimeoutRef.current) { + clearTimeout(markReadTimeoutRef.current); + } + + markReadTimeoutRef.current = setTimeout(() => { + markMessagesAsRead(); + }, 1000); + } + } + } catch (error) { + console.error("Error polling new messages:", error); + } finally { + isPollingRef.current = false; + } + }; // Handle form submit for sending messages const handleSendMessage = (e: React.FormEvent) => { e.preventDefault(); if (!message.trim()) return; - sendMessage(); + sendMessage(message); }; // Handle keyboard shortcuts for sending message @@ -302,65 +326,97 @@ export default function ChatDetail({ chatId }: { chatId: string }) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (message.trim()) { - sendMessage(); + sendMessage(message); } } }; - // Send a message - const sendMessage = async () => { - if (!message.trim() || !chatId) return; + // Function to send a new message + const sendMessage = async (newMessage: string, file?: File | null) => { + // Don't send empty messages + if (!newMessage.trim() && !file) return; - // Save the message text and clear input immediately - const messageText = message.trim(); - setMessage(""); + if (!chatId || !chatData) return; - // Create temporary message to show immediately + // Create a temporary message with a unique temporary ID const tempId = `temp-${Date.now()}`; const tempMessage: Message = { _id: tempId, sender: 'vendor', - content: messageText, - attachments: [], + content: newMessage, + attachments: file ? [URL.createObjectURL(file)] : [], read: true, createdAt: new Date().toISOString(), - buyerId: chat?.buyerId || '', - vendorId: chat?.vendorId || '', + buyerId: chatData.buyerId || '', + vendorId: chatData.vendorId || '' }; - // Update messages array with temp message + // Optimistically add the temp message to the UI setMessages(prev => [...prev, tempMessage]); - // Scroll to bottom + // Scroll to bottom to show the new message setTimeout(scrollToBottom, 50); try { - // Use clientFetch to send message to server - const response = await clientFetch(`/chats/${chatId}/message`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - content: messageText, - attachments: [] - }), - }); + setSending(true); - // Replace temp message with server response - setMessages(prevMessages => { - const updatedMessages = prevMessages.filter(m => m._id !== tempId); - return [...updatedMessages, response]; - }); + let response; + + if (file) { + // Use FormData for file uploads + const formData = new FormData(); + formData.append('content', newMessage); + formData.append('attachment', file); + + response = await clientFetch(`/chats/${chatId}/message`, { + method: 'POST', + body: formData, + // Don't set Content-Type with FormData - browser will set it with boundary + }); + } else { + // Use JSON for text-only messages + response = await clientFetch(`/chats/${chatId}/message`, { + method: 'POST', + body: JSON.stringify({ + content: newMessage + }), + headers: { + 'Content-Type': 'application/json' + } + }); + } + + // Replace the temporary message with the real one from the server + setMessages(prev => prev.map(msg => + msg._id === tempId ? response : msg + )); + + // Update the textarea value to empty + setMessage(''); + + // Clear the file if there was one + if (file) { + setSelectedImage(null); + setSelectedMessageIndex(null); + setSelectedAttachmentIndex(null); + } + + // Update the chat's last message + if (chatData) { + setChatData({ + ...chatData, + lastUpdated: new Date().toISOString() + }); + } - // Force a refresh of the entire chat to ensure consistency - fetchChatData(); } catch (error) { - console.error("Error sending message:", error); - toast.error("Failed to send message"); + console.error('Error sending message:', error); + toast.error('Failed to send message'); - // Remove temp message on error - setMessages(prev => prev.filter(m => m._id !== tempId)); + // Remove the temporary message if sending failed + setMessages(prev => prev.filter(msg => msg._id !== tempId)); + } finally { + setSending(false); } };