diff --git a/components/dashboard/ChatDetail.tsx b/components/dashboard/ChatDetail.tsx index 1eb2637..34d3452 100644 --- a/components/dashboard/ChatDetail.tsx +++ b/components/dashboard/ChatDetail.tsx @@ -41,6 +41,88 @@ export default function ChatDetail({ chatId }: { chatId: string }) { const [message, setMessage] = useState(""); const [sending, setSending] = useState(false); const messagesEndRef = useRef(null); + const [previousMessageCount, setPreviousMessageCount] = useState(0); + const audioRef = useRef(null); + const markReadTimeoutRef = useRef(null); + + // Initialize audio element + useEffect(() => { + // Create audio element for notification sound + audioRef.current = new Audio('/notification.mp3'); + + // Fallback if notification.mp3 doesn't exist - use browser API for a simple beep + audioRef.current.addEventListener('error', () => { + audioRef.current = null; + }); + + return () => { + if (audioRef.current) { + audioRef.current = null; + } + + // Clear any pending timeouts when component unmounts + if (markReadTimeoutRef.current) { + clearTimeout(markReadTimeoutRef.current); + } + }; + }, []); + + // Function to play notification sound + const playNotificationSound = () => { + if (audioRef.current) { + audioRef.current.currentTime = 0; + audioRef.current.play().catch(err => { + console.log('Error playing sound:', err); + // Fallback to simple beep if audio file fails + try { + const context = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, context.currentTime); + oscillator.connect(context.destination); + oscillator.start(); + oscillator.stop(context.currentTime + 0.2); + } catch (e) { + console.error('Could not play fallback audio', e); + } + }); + } else { + // Fallback to simple beep if audio element is not available + try { + const context = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, context.currentTime); + oscillator.connect(context.destination); + oscillator.start(); + oscillator.stop(context.currentTime + 0.2); + } catch (e) { + console.error('Could not play fallback audio', e); + } + } + }; + + // 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 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); + } + }; // Fetch chat data const fetchChat = async () => { @@ -62,9 +144,44 @@ export default function ChatDetail({ chatId }: { chatId: string }) { } }); - const response = await authAxios.get(`/chats/${chatId}`); + // Always fetch messages without marking as read + const response = await authAxios.get(`/chats/${chatId}?markAsRead=false`); + + // Check if there are new messages + const hasNewMessages = chat && response.data.messages.length > chat.messages.length; + const unreadBuyerMessages = response.data.messages.some( + (msg: Message) => msg.sender === 'buyer' && !msg.read + ); + + if (hasNewMessages) { + // Don't play sound for messages we sent (vendor) + const lastMessage = response.data.messages[response.data.messages.length - 1]; + if (lastMessage.sender === 'buyer') { + playNotificationSound(); + } + } + setChat(response.data); + + // Update the previous message count + if (response.data.messages) { + setPreviousMessageCount(response.data.messages.length); + } + setLoading(false); + + // Clear any existing timeout + if (markReadTimeoutRef.current) { + clearTimeout(markReadTimeoutRef.current); + } + + // Only mark as read with a delay if there are unread buyer messages + if (unreadBuyerMessages) { + // Add a 3-second delay before marking messages as read to allow notification to appear + markReadTimeoutRef.current = setTimeout(() => { + markMessagesAsRead(); + }, 3000); + } } catch (error) { console.error("Error fetching chat:", error); toast.error("Failed to load conversation"); diff --git a/components/dashboard/ChatList.tsx b/components/dashboard/ChatList.tsx index 20deca0..051b27f 100644 --- a/components/dashboard/ChatList.tsx +++ b/components/dashboard/ChatList.tsx @@ -1,6 +1,6 @@ "use client" -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -30,8 +30,62 @@ export default function ChatList() { const [chats, setChats] = useState([]); const [loading, setLoading] = useState(true); const [unreadCounts, setUnreadCounts] = useState({ totalUnread: 0, chatCounts: {} }); + const [previousTotalUnread, setPreviousTotalUnread] = useState(0); const [selectedStore, setSelectedStore] = useState(""); const [vendorStores, setVendorStores] = useState<{ _id: string, name: string }[]>([]); + const audioRef = useRef(null); + + // Initialize audio element + useEffect(() => { + // Create audio element for notification sound + audioRef.current = new Audio('/notification.mp3'); + + // Fallback if notification.mp3 doesn't exist - use browser API for a simple beep + audioRef.current.addEventListener('error', () => { + audioRef.current = null; + }); + + return () => { + if (audioRef.current) { + audioRef.current = null; + } + }; + }, []); + + // Function to play notification sound + const playNotificationSound = () => { + if (audioRef.current) { + audioRef.current.currentTime = 0; + audioRef.current.play().catch(err => { + console.log('Error playing sound:', err); + // Fallback to simple beep if audio file fails + try { + const context = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, context.currentTime); + oscillator.connect(context.destination); + oscillator.start(); + oscillator.stop(context.currentTime + 0.2); + } catch (e) { + console.error('Could not play fallback audio', e); + } + }); + } else { + // Fallback to simple beep if audio element is not available + try { + const context = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, context.currentTime); + oscillator.connect(context.destination); + oscillator.start(); + oscillator.stop(context.currentTime + 0.2); + } catch (e) { + console.error('Could not play fallback audio', e); + } + } + }; // Fetch vendor ID and stores useEffect(() => { @@ -160,14 +214,22 @@ export default function ChatList() { // Fetch unread counts const unreadResponse = await authAxios.get(`/chats/vendor/${vendorId}/unread`); console.log("Unread counts:", unreadResponse.data); + + // Check if there are new unread messages and play sound + if (!loading && unreadResponse.data.totalUnread > previousTotalUnread) { + playNotificationSound(); + } + + // Update states setUnreadCounts(unreadResponse.data); + setPreviousTotalUnread(unreadResponse.data.totalUnread); + setLoading(false); console.log("Chat loading complete"); } catch (error) { console.error("Error fetching chats:", error); toast.error("Failed to load chats"); } finally { - setLoading(false); console.log("Loading state set to false"); } }; @@ -175,7 +237,7 @@ export default function ChatList() { fetchChats(); // Set up polling for updates every 30 seconds - const intervalId = setInterval(fetchChats, 5000); + const intervalId = setInterval(fetchChats, 10000); return () => clearInterval(intervalId); }, [selectedStore]); diff --git a/components/dashboard/ChatNotifications.tsx b/components/dashboard/ChatNotifications.tsx index 753b48c..9dacb16 100644 --- a/components/dashboard/ChatNotifications.tsx +++ b/components/dashboard/ChatNotifications.tsx @@ -1,6 +1,6 @@ "use client" -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -22,9 +22,63 @@ interface UnreadCounts { export default function ChatNotifications() { const router = useRouter(); const [unreadCounts, setUnreadCounts] = useState({ totalUnread: 0, chatCounts: {} }); + const [previousUnreadTotal, setPreviousUnreadTotal] = useState(0); const [loading, setLoading] = useState(true); const [chatMetadata, setChatMetadata] = useState>({}); + const audioRef = useRef(null); + // Initialize audio element + useEffect(() => { + // Create audio element for notification sound + audioRef.current = new Audio('/notification.mp3'); + + // Fallback if notification.mp3 doesn't exist - use browser API for a simple beep + audioRef.current.addEventListener('error', () => { + audioRef.current = null; + }); + + return () => { + if (audioRef.current) { + audioRef.current = null; + } + }; + }, []); + + // Function to play notification sound + const playNotificationSound = () => { + if (audioRef.current) { + audioRef.current.currentTime = 0; + audioRef.current.play().catch(err => { + console.log('Error playing sound:', err); + // Fallback to simple beep if audio file fails + try { + const context = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, context.currentTime); + oscillator.connect(context.destination); + oscillator.start(); + oscillator.stop(context.currentTime + 0.2); + } catch (e) { + console.error('Could not play fallback audio', e); + } + }); + } else { + // Fallback to simple beep if audio element is not available + try { + const context = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, context.currentTime); + oscillator.connect(context.destination); + oscillator.start(); + oscillator.stop(context.currentTime + 0.2); + } catch (e) { + console.error('Could not play fallback audio', e); + } + } + }; + // Fetch unread counts useEffect(() => { const fetchUnreadCounts = async () => { @@ -52,7 +106,15 @@ export default function ChatNotifications() { } const response = await authAxios.get(`/chats/vendor/${vendorId}/unread`); + + // Check if there are new notifications and play sound if needed + if (!loading && response.data.totalUnread > previousUnreadTotal) { + playNotificationSound(); + } + + // Update state setUnreadCounts(response.data); + setPreviousUnreadTotal(response.data.totalUnread); if (response.data.totalUnread > 0) { const chatIds = Object.keys(response.data.chatCounts); @@ -65,7 +127,8 @@ export default function ChatNotifications() { await Promise.all( chatIds.map(async (chatId) => { try { - const chatResponse = await authAxios.get(`/chats/${chatId}`); + // Use markAsRead=false to ensure we don't mark messages as read + const chatResponse = await authAxios.get(`/chats/${chatId}?markAsRead=false`); metadata[chatId] = { buyerId: chatResponse.data.buyerId, }; @@ -91,7 +154,7 @@ export default function ChatNotifications() { const intervalId = setInterval(fetchUnreadCounts, 30000); return () => clearInterval(intervalId); - }, []); + }, [loading, previousUnreadTotal]); const handleChatClick = (chatId: string) => { router.push(`/dashboard/chats/${chatId}`); diff --git a/public/notification.mp3 b/public/notification.mp3 new file mode 100644 index 0000000..de51d68 Binary files /dev/null and b/public/notification.mp3 differ