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 fe5d9bf..325634b 100644 --- a/components/dashboard/ChatDetail.tsx +++ b/components/dashboard/ChatDetail.tsx @@ -97,6 +97,7 @@ export default function ChatDetail({ chatId }: { chatId: string }) { const [selectedImage, setSelectedImage] = useState(null); const [selectedMessageIndex, setSelectedMessageIndex] = useState(null); const [selectedAttachmentIndex, setSelectedAttachmentIndex] = useState(null); + const seenMessageIdsRef = useRef>(new Set()); // Scroll to bottom utility functions const scrollToBottom = () => { @@ -174,19 +175,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 +218,43 @@ 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]); + + // Mark all these messages as seen to avoid notification sounds + newMessages.forEach((msg: Message) => { + seenMessageIdsRef.current.add(msg._id); + }); + } else { + // If we need to replace all messages (e.g., first load or refresh) + setMessages(Array.isArray(response.messages) ? response.messages : []); + + // Mark all messages as seen + if (Array.isArray(response.messages)) { + response.messages.forEach((msg: Message) => { + seenMessageIdsRef.current.add(msg._id); + }); + } + } + } else { + // Initial load + const initialMessages = Array.isArray(response.messages) ? response.messages : []; + setMessages(initialMessages); + + // Mark all initial messages as seen + initialMessages.forEach((msg: Message) => { + seenMessageIdsRef.current.add(msg._id); + }); + } // Scroll to bottom on initial load setTimeout(() => { @@ -240,103 +268,194 @@ 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) && - response.messages.length !== messages.length - ) { - setMessages(response.messages); - // Only auto-scroll if we're already near the bottom - if (isNearBottom()) { - setTimeout(() => { - scrollToBottom(); - }, 100); - } - } - } 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]); - - // Send a message - const sendMessage = async () => { - if (!message.trim()) return; + }, [chatId]); + + // Poll for new messages without replacing existing ones + const pollNewMessages = async () => { + if (!chatId || isPollingRef.current) return; - // Create temporary message to show immediately + 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 only for new buyer messages we haven't seen before + const unseenBuyerMessages = newMessages.filter((msg: Message) => + msg.sender === 'buyer' && !seenMessageIdsRef.current.has(msg._id) + ); + + // If we have unseen buyer messages, play sound and mark them as seen + if (unseenBuyerMessages.length > 0) { + playNotificationSound(); + + // Add these messages to our seen set + unseenBuyerMessages.forEach((msg: Message) => { + seenMessageIdsRef.current.add(msg._id); + }); + } + + // 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(message); + }; + + // Handle keyboard shortcuts for sending message + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (message.trim()) { + sendMessage(message); + } + } + }; + + // Function to send a new message + const sendMessage = async (newMessage: string, file?: File | null) => { + // Don't send empty messages + if (!newMessage.trim() && !file) return; + + if (!chatId || !chatData) return; + + // Create a temporary message with a unique temporary ID const tempId = `temp-${Date.now()}`; const tempMessage: Message = { _id: tempId, sender: 'vendor', - content: message.trim(), - 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 || '' }; - setMessages(prev => [...prev, tempMessage]); - scrollToBottom(); + // Add the temp message ID to seen messages + seenMessageIdsRef.current.add(tempId); - // Clear input - setMessage(""); + // Optimistically add the temp message to the UI + setMessages(prev => [...prev, tempMessage]); + + // Scroll to bottom to show the new message + setTimeout(scrollToBottom, 50); try { - // Use clientFetch instead of direct axios calls - const response = await clientFetch(`/chats/${chatId}/message`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - content: message.trim(), - attachments: [] - }), - }); + setSending(true); + + 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 + )); + + // Add the real message ID to seen messages + if (response && response._id) { + seenMessageIdsRef.current.add(response._id); + } + + // 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() + }); + } - // Replace temp message with real one from server - setMessages(prev => - prev.filter(m => m._id !== tempId) - .concat(response) - ); } 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); } }; - // Handle form submit for sending messages - const handleSendMessage = (e: React.FormEvent) => { - e.preventDefault(); - sendMessage(); - }; - const handleBackClick = () => { router.push("/dashboard/chats"); }; @@ -563,6 +682,8 @@ export default function ChatDetail({ chatId }: { chatId: string }) { placeholder="Type your message..." disabled={sending} className="flex-1" + onKeyDown={handleKeyDown} + autoFocus />