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
/>