spam list + socket events
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user