This commit is contained in:
NotII
2025-03-24 00:44:19 +00:00
parent 37d4d0930c
commit be0f9aa3af
2 changed files with 158 additions and 102 deletions

View File

@@ -1,17 +1,17 @@
"use client";
import React from "react"; import React from "react";
import { Metadata } from "next"; import { useParams } from "next/navigation";
import ChatDetail from "@/components/dashboard/ChatDetail"; import ChatDetail from "@/components/dashboard/ChatDetail";
import Dashboard from "@/components/dashboard/dashboard"; import Dashboard from "@/components/dashboard/dashboard";
export const metadata: Metadata = { export default function ChatDetailPage() {
title: "Chat Conversation", const params = useParams();
description: "View and respond to customer messages", const chatId = params.id as string;
};
export default function ChatDetailPage({ params }: { params: { id: string } }) {
return ( return (
<Dashboard> <Dashboard>
<ChatDetail chatId={params.id} /> <ChatDetail chatId={chatId} />
</Dashboard> </Dashboard>
); );
} }

View File

@@ -174,19 +174,10 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
// Function to mark messages as read // Function to mark messages as read
const markMessagesAsRead = async () => { const markMessagesAsRead = async () => {
try { try {
const authToken = getCookie("Authorization"); // Use clientFetch instead of direct axios
await clientFetch(`/chats/${chatId}/mark-read`, {
if (!authToken) return; method: 'POST'
const authAxios = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
Authorization: `Bearer ${authToken}`
}
}); });
// Use dedicated endpoint to mark messages as read
await authAxios.post(`/chats/${chatId}/mark-read`);
console.log("Marked messages as read"); console.log("Marked messages as read");
} catch (error) { } catch (error) {
console.error("Error marking messages as read:", error); console.error("Error marking messages as read:", error);
@@ -226,7 +217,25 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
setChatData(response); setChatData(response);
setChat(response); // Set chat data to maintain compatibility setChat(response); // Set chat data to maintain compatibility
// 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 : []); setMessages(Array.isArray(response.messages) ? response.messages : []);
}
} else {
// Initial load
setMessages(Array.isArray(response.messages) ? response.messages : []);
}
// Scroll to bottom on initial load // Scroll to bottom on initial load
setTimeout(() => { setTimeout(() => {
@@ -240,61 +249,76 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
} }
}; };
// Fetch new messages periodically // Setup polling for new messages
useEffect(() => { useEffect(() => {
if (!chatId || !chatData) return; // Set up a polling interval to check for new messages
const pollInterval = setInterval(() => {
if (chatId && !isPollingRef.current) {
pollNewMessages();
}
}, 3000); // Poll every 3 seconds
const checkForNewMessages = async () => { return () => {
if (isPollingRef.current) return; clearInterval(pollInterval);
};
}, [chatId]);
// Poll for new messages without replacing existing ones
const pollNewMessages = async () => {
if (!chatId || isPollingRef.current) return;
isPollingRef.current = true; isPollingRef.current = true;
try { try {
// Use clientFetch instead of direct axios calls const response = await clientFetch(`/chats/${chatId}`);
const response = await clientFetch(`/chats/${chatId}?markAsRead=true`);
// Update chat metadata
setChatData(response);
setChat(response);
if (response && Array.isArray(response.messages)) {
// Check if there are new messages // Check if there are new messages
const hasNewMessages = response.messages.length > messages.length; 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));
// Always update messages to ensure we have the latest state if (newMessages.length > 0) {
setMessages(response.messages); // Add only new messages to avoid re-rendering all messages
setMessages(prev => [...prev, ...newMessages]);
// If there are new messages and we're near the bottom, scroll down // Play notification sound for new buyer messages
if (hasNewMessages && isNearBottom()) { const hasBuyerMessage = newMessages.some((msg: Message) => msg.sender === 'buyer');
setTimeout(scrollToBottom, 100); if (hasBuyerMessage) {
}
// 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(); 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) { } catch (error) {
console.error("Error checking for new messages:", error); console.error("Error polling new messages:", error);
} finally { } finally {
isPollingRef.current = false; isPollingRef.current = false;
} }
}; };
// Check for new messages every 3 seconds
const intervalId = setInterval(checkForNewMessages, 3000);
return () => {
clearInterval(intervalId);
};
}, [chatId, chatData, messages.length]);
// Handle form submit for sending messages // Handle form submit for sending messages
const handleSendMessage = (e: React.FormEvent) => { const handleSendMessage = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!message.trim()) return; if (!message.trim()) return;
sendMessage(); sendMessage(message);
}; };
// Handle keyboard shortcuts for sending message // Handle keyboard shortcuts for sending message
@@ -302,65 +326,97 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
if (message.trim()) { if (message.trim()) {
sendMessage(); sendMessage(message);
} }
} }
}; };
// Send a message // Function to send a new message
const sendMessage = async () => { const sendMessage = async (newMessage: string, file?: File | null) => {
if (!message.trim() || !chatId) return; // Don't send empty messages
if (!newMessage.trim() && !file) return;
// Save the message text and clear input immediately if (!chatId || !chatData) return;
const messageText = message.trim();
setMessage("");
// Create temporary message to show immediately // Create a temporary message with a unique temporary ID
const tempId = `temp-${Date.now()}`; const tempId = `temp-${Date.now()}`;
const tempMessage: Message = { const tempMessage: Message = {
_id: tempId, _id: tempId,
sender: 'vendor', sender: 'vendor',
content: messageText, content: newMessage,
attachments: [], attachments: file ? [URL.createObjectURL(file)] : [],
read: true, read: true,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
buyerId: chat?.buyerId || '', buyerId: chatData.buyerId || '',
vendorId: chat?.vendorId || '', vendorId: chatData.vendorId || ''
}; };
// Update messages array with temp message // Optimistically add the temp message to the UI
setMessages(prev => [...prev, tempMessage]); setMessages(prev => [...prev, tempMessage]);
// Scroll to bottom // Scroll to bottom to show the new message
setTimeout(scrollToBottom, 50); setTimeout(scrollToBottom, 50);
try { try {
// Use clientFetch to send message to server setSending(true);
const response = await clientFetch(`/chats/${chatId}/message`, {
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', 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: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, }
body: JSON.stringify({
content: messageText,
attachments: []
}),
}); });
}
// Replace temp message with server response // Replace the temporary message with the real one from the server
setMessages(prevMessages => { setMessages(prev => prev.map(msg =>
const updatedMessages = prevMessages.filter(m => m._id !== tempId); msg._id === tempId ? response : msg
return [...updatedMessages, response]; ));
// 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) { } catch (error) {
console.error("Error sending message:", error); console.error('Error sending message:', error);
toast.error("Failed to send message"); toast.error('Failed to send message');
// Remove temp message on error // Remove the temporary message if sending failed
setMessages(prev => prev.filter(m => m._id !== tempId)); setMessages(prev => prev.filter(msg => msg._id !== tempId));
} finally {
setSending(false);
} }
}; };