weewoo
This commit is contained in:
@@ -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 (
|
||||
<Dashboard>
|
||||
<ChatDetail chatId={params.id} />
|
||||
<ChatDetail chatId={chatId} />
|
||||
</Dashboard>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user