spam list + socket events

This commit is contained in:
hamid zarghami
2025-07-19 16:36:51 +03:30
parent f8af0af065
commit 27fc6b765c
20 changed files with 777 additions and 30 deletions
+159
View File
@@ -0,0 +1,159 @@
import React, { useState, useCallback, useEffect } from 'react';
import { useEmailWebSocket } from '@/hooks/useEmailWebSocket';
import { useEmailEvents, EmailPayload, UnreadCountPayload } from '@/hooks/useEmailEvents';
import { useNotificationSound } from '@/hooks/useNotificationSound';
import { Notification as NotificationIcon, VolumeHigh } from 'iconsax-react';
interface EmailNotificationsProps {
userToken: string;
onEmailClick?: (email: EmailPayload) => void;
}
export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
userToken,
onEmailClick
}) => {
const [notifications, setNotifications] = useState<EmailPayload[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const { socket, isConnected } = useEmailWebSocket({
token: userToken,
onConnect: () => console.log('نوتیفیکیشن‌های ایمیل متصل شد'),
onError: (error) => console.error('خطای WebSocket:', error),
});
// Sound settings state
const [soundEnabled, setSoundEnabled] = useState(true);
const [soundVolume, setSoundVolume] = useState(0.7);
// Load sound settings from localStorage
useEffect(() => {
const savedEnabled = localStorage.getItem('notificationSoundEnabled');
const savedVolume = localStorage.getItem('notificationSoundVolume');
if (savedEnabled !== null) {
setSoundEnabled(savedEnabled === 'true');
}
if (savedVolume !== null) {
setSoundVolume(parseFloat(savedVolume));
}
}, []);
// Notification sound hook
const { playSound } = useNotificationSound({
enabled: soundEnabled,
volume: soundVolume,
});
const handleNewEmail = useCallback((email: EmailPayload) => {
console.log('📧 ایمیل جدید دریافت شد:', email);
// Add to notifications list
setNotifications(prev => [email, ...prev.slice(0, 9)]); // Keep only 10 latest
// Show browser notification if permission granted
if (Notification.permission === 'granted') {
new Notification(`📧 ایمیل جدید: ${email.subject}`, {
body: `از: ${email.from.name || email.from.address}\n${email.preview}`,
tag: `email-${email.messageId}`, // Prevent duplicates
});
}
// Play notification sound
playSound();
}, [playSound]);
const handleUnreadCountUpdate = useCallback((data: UnreadCountPayload) => {
setUnreadCount(data.count);
}, []);
const handleEmailRead = useCallback((data: { messageId: number }) => {
// Remove from notifications if it was there
setNotifications(prev =>
prev.filter(notification => notification.messageId !== data.messageId)
);
}, []);
useEmailEvents(socket, {
onNewEmail: handleNewEmail,
onUnreadCountUpdated: handleUnreadCountUpdate,
onEmailRead: handleEmailRead,
});
// Request notification permission on mount
React.useEffect(() => {
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}, []);
return (
<div className="relative">
{/* Connection Status */}
<div className={`absolute -top-2 -right-2 w-3 h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'
}`}></div>
{/* Test Sound Button */}
<button
onClick={() => {
console.log("کلیک روی دکمه تست صدا");
playSound();
}}
className="p-2 rounded-full hover:bg-gray-100 transition-colors"
title="تست صدا"
>
<VolumeHigh size={20} color="black" />
</button>
{/* Notification Bell */}
<div
className="relative cursor-pointer p-2 rounded-full hover:bg-gray-100 transition-colors"
onClick={() => setIsVisible(!isVisible)}
>
<NotificationIcon size={24} color="black" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center font-bold">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</div>
{/* Notifications Dropdown */}
{isVisible && (
<div className="absolute top-full left-0 w-80 max-h-96 bg-white border border-gray-200 rounded-lg shadow-lg z-50 overflow-hidden">
<div className="p-3 bg-gray-50 border-b border-gray-200 flex justify-between items-center">
<h3 className="font-semibold">📧 نوتیفیکیشنهای ایمیل</h3>
<span className="text-sm text-gray-600">{unreadCount} خوانده نشده</span>
</div>
<div className="max-h-72 overflow-y-auto">
{notifications.length === 0 ? (
<div className="p-4 text-center text-gray-500">ایمیل جدیدی وجود ندارد</div>
) : (
notifications.map((email) => (
<div
key={email.messageId}
className="p-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 transition-colors"
onClick={() => onEmailClick?.(email)}
>
<div className="flex items-center justify-between mb-1">
<strong className="text-sm truncate">
{email.from.name || email.from.address}
</strong>
{email.hasAttachments && <span>📎</span>}
</div>
<div className="font-medium text-sm mb-1 truncate">{email.subject}</div>
<div className="text-xs text-gray-600 mb-1 line-clamp-2">{email.preview}</div>
<div className="text-xs text-gray-400">
{new Date(email.timestamp).toLocaleString('fa-IR')}
</div>
</div>
))
)}
</div>
</div>
)}
</div>
);
};
+91
View File
@@ -0,0 +1,91 @@
import React, { useState, useEffect } from 'react';
import { VolumeHigh, VolumeLow, VolumeSlash } from 'iconsax-react';
import { useNotificationSound } from '@/hooks/useNotificationSound';
interface SoundSettingsProps {
className?: string;
}
export const SoundSettings: React.FC<SoundSettingsProps> = ({ className = '' }) => {
const [isEnabled, setIsEnabled] = useState(true);
const [volume, setVolume] = useState(0.7);
const { playSound } = useNotificationSound({
enabled: isEnabled,
volume: volume,
});
// Load settings from localStorage on mount
useEffect(() => {
const savedEnabled = localStorage.getItem('notificationSoundEnabled');
const savedVolume = localStorage.getItem('notificationSoundVolume');
if (savedEnabled !== null) {
setIsEnabled(savedEnabled === 'true');
}
if (savedVolume !== null) {
setVolume(parseFloat(savedVolume));
}
}, []);
// Save settings to localStorage when changed
useEffect(() => {
localStorage.setItem('notificationSoundEnabled', isEnabled.toString());
localStorage.setItem('notificationSoundVolume', volume.toString());
}, [isEnabled, volume]);
const handleVolumeChange = (newVolume: number) => {
setVolume(newVolume);
if (isEnabled) {
// Test sound with new volume
setTimeout(() => playSound(), 100);
}
};
const toggleSound = () => {
const newEnabled = !isEnabled;
setIsEnabled(newEnabled);
if (newEnabled) {
// Test sound when enabling
setTimeout(() => playSound(), 100);
}
};
const getVolumeIcon = () => {
if (!isEnabled) return <VolumeSlash size={18} color="gray" />;
if (volume < 0.3) return <VolumeLow size={18} color="black" />;
return <VolumeHigh size={18} color="black" />;
};
return (
<div className={`flex items-center gap-3 ${className}`}>
{/* Sound Toggle */}
<button
onClick={toggleSound}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
title={isEnabled ? 'غیرفعال کردن صدا' : 'فعال کردن صدا'}
>
{getVolumeIcon()}
</button>
{/* Volume Slider */}
{isEnabled && (
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
className="w-20 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
title={`حجم صدا: ${Math.round(volume * 100)}%`}
/>
<span className="text-xs text-gray-500 min-w-[30px]">
{Math.round(volume * 100)}%
</span>
</div>
)}
</div>
);
};