This commit is contained in:
NotII
2025-03-04 22:52:39 +00:00
parent c7a2ee0234
commit 75e0b65e11
4 changed files with 249 additions and 7 deletions

View File

@@ -41,6 +41,88 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const [previousMessageCount, setPreviousMessageCount] = useState<number>(0);
const audioRef = useRef<HTMLAudioElement | null>(null);
const markReadTimeoutRef = useRef<NodeJS.Timeout | null>(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 // Fetch chat data
const fetchChat = async () => { 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); setChat(response.data);
// Update the previous message count
if (response.data.messages) {
setPreviousMessageCount(response.data.messages.length);
}
setLoading(false); 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) { } catch (error) {
console.error("Error fetching chat:", error); console.error("Error fetching chat:", error);
toast.error("Failed to load conversation"); toast.error("Failed to load conversation");

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -30,8 +30,62 @@ export default function ChatList() {
const [chats, setChats] = useState<Chat[]>([]); const [chats, setChats] = useState<Chat[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [unreadCounts, setUnreadCounts] = useState<UnreadCounts>({ totalUnread: 0, chatCounts: {} }); const [unreadCounts, setUnreadCounts] = useState<UnreadCounts>({ totalUnread: 0, chatCounts: {} });
const [previousTotalUnread, setPreviousTotalUnread] = useState<number>(0);
const [selectedStore, setSelectedStore] = useState<string>(""); const [selectedStore, setSelectedStore] = useState<string>("");
const [vendorStores, setVendorStores] = useState<{ _id: string, name: string }[]>([]); const [vendorStores, setVendorStores] = useState<{ _id: string, name: string }[]>([]);
const audioRef = useRef<HTMLAudioElement | null>(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 // Fetch vendor ID and stores
useEffect(() => { useEffect(() => {
@@ -160,14 +214,22 @@ export default function ChatList() {
// Fetch unread counts // Fetch unread counts
const unreadResponse = await authAxios.get(`/chats/vendor/${vendorId}/unread`); const unreadResponse = await authAxios.get(`/chats/vendor/${vendorId}/unread`);
console.log("Unread counts:", unreadResponse.data); 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); setUnreadCounts(unreadResponse.data);
setPreviousTotalUnread(unreadResponse.data.totalUnread);
setLoading(false);
console.log("Chat loading complete"); console.log("Chat loading complete");
} catch (error) { } catch (error) {
console.error("Error fetching chats:", error); console.error("Error fetching chats:", error);
toast.error("Failed to load chats"); toast.error("Failed to load chats");
} finally { } finally {
setLoading(false);
console.log("Loading state set to false"); console.log("Loading state set to false");
} }
}; };
@@ -175,7 +237,7 @@ export default function ChatList() {
fetchChats(); fetchChats();
// Set up polling for updates every 30 seconds // Set up polling for updates every 30 seconds
const intervalId = setInterval(fetchChats, 5000); const intervalId = setInterval(fetchChats, 10000);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [selectedStore]); }, [selectedStore]);

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -22,8 +22,62 @@ interface UnreadCounts {
export default function ChatNotifications() { export default function ChatNotifications() {
const router = useRouter(); const router = useRouter();
const [unreadCounts, setUnreadCounts] = useState<UnreadCounts>({ totalUnread: 0, chatCounts: {} }); const [unreadCounts, setUnreadCounts] = useState<UnreadCounts>({ totalUnread: 0, chatCounts: {} });
const [previousUnreadTotal, setPreviousUnreadTotal] = useState<number>(0);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [chatMetadata, setChatMetadata] = useState<Record<string, { buyerId: string }>>({}); const [chatMetadata, setChatMetadata] = useState<Record<string, { buyerId: string }>>({});
const audioRef = useRef<HTMLAudioElement | null>(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 // Fetch unread counts
useEffect(() => { useEffect(() => {
@@ -52,7 +106,15 @@ export default function ChatNotifications() {
} }
const response = await authAxios.get(`/chats/vendor/${vendorId}/unread`); 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); setUnreadCounts(response.data);
setPreviousUnreadTotal(response.data.totalUnread);
if (response.data.totalUnread > 0) { if (response.data.totalUnread > 0) {
const chatIds = Object.keys(response.data.chatCounts); const chatIds = Object.keys(response.data.chatCounts);
@@ -65,7 +127,8 @@ export default function ChatNotifications() {
await Promise.all( await Promise.all(
chatIds.map(async (chatId) => { chatIds.map(async (chatId) => {
try { 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] = { metadata[chatId] = {
buyerId: chatResponse.data.buyerId, buyerId: chatResponse.data.buyerId,
}; };
@@ -91,7 +154,7 @@ export default function ChatNotifications() {
const intervalId = setInterval(fetchUnreadCounts, 30000); const intervalId = setInterval(fetchUnreadCounts, 30000);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, []); }, [loading, previousUnreadTotal]);
const handleChatClick = (chatId: string) => { const handleChatClick = (chatId: string) => {
router.push(`/dashboard/chats/${chatId}`); router.push(`/dashboard/chats/${chatId}`);

BIN
public/notification.mp3 Normal file

Binary file not shown.