woohoo?!
This commit is contained in:
@@ -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");
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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
BIN
public/notification.mp3
Normal file
Binary file not shown.
Reference in New Issue
Block a user