spam list + socket events
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
VITE_TOKEN_NAME = 'dmail_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'dmail_refresh_token'
|
||||
VITE_DANAK_BASE_URL ='https://api.danakcorp.com'
|
||||
# VITE_BASE_URL = 'http://192.168.1.113:4000'
|
||||
# VITE_BASE_URL = 'http://192.168.1.118:4000'
|
||||
VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
|
||||
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"react-quill-new": "^3.4.6",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"react-spinners": "^0.17.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"swiper": "^11.2.6",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
|
||||
Generated
+88
@@ -101,6 +101,9 @@ importers:
|
||||
react-spinners:
|
||||
specifier: ^0.17.0
|
||||
version: 0.17.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
socket.io-client:
|
||||
specifier: ^4.8.1
|
||||
version: 4.8.1
|
||||
swiper:
|
||||
specifier: ^11.2.6
|
||||
version: 11.2.8
|
||||
@@ -887,6 +890,9 @@ packages:
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
'@socket.io/component-emitter@3.1.2':
|
||||
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
@@ -1424,6 +1430,15 @@ packages:
|
||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
debug@4.3.7:
|
||||
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -1484,6 +1499,13 @@ packages:
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
engine.io-client@6.6.3:
|
||||
resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==}
|
||||
|
||||
engine.io-parser@5.2.3:
|
||||
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -2745,6 +2767,14 @@ packages:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
socket.io-client@4.8.1:
|
||||
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
socket.io-parser@4.2.4:
|
||||
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3094,6 +3124,22 @@ packages:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ws@8.17.1:
|
||||
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
xmlhttprequest-ssl@2.1.2:
|
||||
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3874,6 +3920,8 @@ snapshots:
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@socket.io/component-emitter@3.1.2': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@stoplight/better-ajv-errors@1.0.3(ajv@8.17.1)':
|
||||
@@ -4521,6 +4569,10 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
debug@4.3.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -4576,6 +4628,20 @@ snapshots:
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
engine.io-client@6.6.3:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.3.7
|
||||
engine.io-parser: 5.2.3
|
||||
ws: 8.17.1
|
||||
xmlhttprequest-ssl: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
engine.io-parser@5.2.3: {}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -5937,6 +6003,24 @@ snapshots:
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
socket.io-client@4.8.1:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.3.7
|
||||
engine.io-client: 6.6.3
|
||||
socket.io-parser: 4.2.4
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
socket.io-parser@4.2.4:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stats-gl@2.4.2(@types/three@0.177.0)(three@0.177.0):
|
||||
@@ -6295,6 +6379,10 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
ws@8.17.1: {}
|
||||
|
||||
xmlhttprequest-ssl@2.1.2: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
+11
-4
@@ -16,6 +16,7 @@ import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshT
|
||||
import { refreshToken } from './pages/auth/service/AuthService';
|
||||
import { Paths } from './utils/Paths';
|
||||
import Login from './pages/auth/Login';
|
||||
import { EmailWebSocketProvider } from './contexts/EmailWebSocketContext';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -107,11 +108,14 @@ const queryClient = new QueryClient({
|
||||
const App: FC = () => {
|
||||
|
||||
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
|
||||
const [userToken, setUserToken] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
const checkLogin = async () => {
|
||||
const token = await getToken();
|
||||
if (token) {
|
||||
setIsLogin('isLogin')
|
||||
setUserToken(token)
|
||||
} else {
|
||||
setIsLogin('isNotLogin')
|
||||
if (window.location.href.split('auth').length === 1) {
|
||||
@@ -126,10 +130,13 @@ const App: FC = () => {
|
||||
<BrowserRouter>
|
||||
<I18nextProvider i18n={i18next}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
{
|
||||
isLogin === 'isLogin' ? <Main /> : isLogin === 'isNotLogin' ? <Login /> : null
|
||||
}
|
||||
{isLogin === 'isLogin' && userToken ? (
|
||||
<EmailWebSocketProvider token={userToken}>
|
||||
<Main />
|
||||
</EmailWebSocketProvider>
|
||||
) : isLogin === 'isNotLogin' ? (
|
||||
<Login />
|
||||
) : null}
|
||||
<NewMessage />
|
||||
<ToastContainer />
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+43
-4
@@ -81,6 +81,30 @@ export const formatDate = (dateString: string) => {
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
export const detectTextDirection = (text: string): "ltr" | "rtl" => {
|
||||
// حذف تگهای HTML برای تحلیل فقط متن
|
||||
const plainText = text.replace(/<[^>]*>/g, " ").trim();
|
||||
|
||||
// اگر متن خالی است، از RTL استفاده کن
|
||||
if (!plainText) return "rtl";
|
||||
|
||||
// الگوی کاراکترهای انگلیسی و اعداد
|
||||
const englishPattern = /[a-zA-Z0-9\s.,!?;:'"()\-@#$%^&*+=<>{}[\]\\|`~_]/g;
|
||||
const englishMatches = plainText.match(englishPattern) || [];
|
||||
|
||||
// الگوی کاراکترهای فارسی و عربی (برای آینده ممکن است نیاز باشد)
|
||||
// const persianArabicPattern = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/g;
|
||||
|
||||
// محاسبه درصد کاراکترهای انگلیسی
|
||||
const totalChars = plainText.replace(/\s/g, "").length;
|
||||
const englishChars = englishMatches.join("").replace(/\s/g, "").length;
|
||||
const englishPercentage =
|
||||
totalChars > 0 ? (englishChars / totalChars) * 100 : 0;
|
||||
|
||||
// اگر بیش از 70% متن انگلیسی است، LTR استفاده کن
|
||||
return englishPercentage > 70 ? "ltr" : "rtl";
|
||||
};
|
||||
|
||||
export const sanitizeEmailHTML = (html: string): string => {
|
||||
try {
|
||||
// Remove style tags and their content
|
||||
@@ -95,15 +119,30 @@ export const sanitizeEmailHTML = (html: string): string => {
|
||||
// Remove potentially harmful attributes
|
||||
sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, "");
|
||||
|
||||
// فقط font-family و font-size رو از inline styles حذف کن، بقیه رو حفظ کن
|
||||
// تشخیص جهت متن اصلی
|
||||
const detectedDirection = detectTextDirection(sanitized);
|
||||
|
||||
// فقط font-family، font-size و font-weight رو از inline styles حذف کن
|
||||
// text-align و direction رو فقط اگر با جهت تشخیص داده شده مطابقت نداشته باشد حذف کن
|
||||
sanitized = sanitized.replace(
|
||||
/style\s*=\s*["']([^"']*?)["']/gi,
|
||||
(_, styles) => {
|
||||
// فقط font-family و font-size رو حذف کن
|
||||
const cleanedStyles = styles
|
||||
let cleanedStyles = styles
|
||||
.replace(/font-family\s*:\s*[^;]*;?/gi, "")
|
||||
.replace(/font-size\s*:\s*[^;]*;?/gi, "")
|
||||
.replace(/font-weight\s*:\s*[^;]*;?/gi, "")
|
||||
.replace(/font-weight\s*:\s*[^;]*;?/gi, "");
|
||||
|
||||
// اگر جهت تشخیص داده شده LTR است، direction و text-align را حفظ کن
|
||||
if (detectedDirection === "ltr") {
|
||||
// direction و text-align را حفظ کن
|
||||
} else {
|
||||
// اگر RTL است، این استایلها را حذف کن تا استایل سایت اعمال شود
|
||||
cleanedStyles = cleanedStyles
|
||||
.replace(/text-align\s*:\s*[^;]*;?/gi, "")
|
||||
.replace(/direction\s*:\s*[^;]*;?/gi, "");
|
||||
}
|
||||
|
||||
cleanedStyles = cleanedStyles
|
||||
.replace(/;\s*;/g, ";") // حذف سمیکالنهای اضافی
|
||||
.replace(/^\s*;\s*|\s*;\s*$/g, ""); // حذف سمیکالنهای ابتدا و انتها
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { useEmailWebSocket } from '@/hooks/useEmailWebSocket';
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
interface EmailWebSocketContextValue {
|
||||
socket: Socket | null;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
const EmailWebSocketContext = createContext<EmailWebSocketContextValue | null>(null);
|
||||
|
||||
interface EmailWebSocketProviderProps {
|
||||
children: ReactNode;
|
||||
token: string;
|
||||
serverUrl?: string;
|
||||
}
|
||||
|
||||
export const EmailWebSocketProvider: React.FC<EmailWebSocketProviderProps> = ({
|
||||
children,
|
||||
token,
|
||||
serverUrl,
|
||||
}) => {
|
||||
const { socket, isConnected } = useEmailWebSocket({ token, serverUrl });
|
||||
|
||||
return (
|
||||
<EmailWebSocketContext.Provider value={{ socket, isConnected }}>
|
||||
{children}
|
||||
</EmailWebSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useEmailWebSocketContext = () => {
|
||||
const context = useContext(EmailWebSocketContext);
|
||||
if (!context) {
|
||||
throw new Error('useEmailWebSocketContext must be used within EmailWebSocketProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useEffect } from "react";
|
||||
import { Socket } from "socket.io-client";
|
||||
|
||||
export interface EmailPayload {
|
||||
messageId: number;
|
||||
userId: string;
|
||||
from: {
|
||||
name?: string;
|
||||
address: string;
|
||||
};
|
||||
to: Array<{
|
||||
name?: string;
|
||||
address: string;
|
||||
}>;
|
||||
subject: string;
|
||||
preview?: string;
|
||||
hasAttachments: boolean;
|
||||
timestamp: string;
|
||||
mailboxId: string;
|
||||
mailboxName: string;
|
||||
isRead: boolean;
|
||||
isFlagged: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface EmailStatusPayload {
|
||||
messageId: number;
|
||||
userId: string;
|
||||
mailboxId: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface UnreadCountPayload {
|
||||
userId: string;
|
||||
totalUnread: number;
|
||||
mailboxCounts: Record<string, number>;
|
||||
timestamp: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface EmailEventHandlers {
|
||||
onNewEmail?: (email: EmailPayload) => void;
|
||||
onEmailRead?: (data: EmailStatusPayload) => void;
|
||||
onEmailDeleted?: (data: EmailStatusPayload) => void;
|
||||
onEmailSent?: (data: EmailStatusPayload) => void;
|
||||
onUnreadCountUpdated?: (data: UnreadCountPayload) => void;
|
||||
onMailboxUpdated?: (data: EmailStatusPayload) => void;
|
||||
}
|
||||
|
||||
export const useEmailEvents = (
|
||||
socket: Socket | null,
|
||||
handlers: EmailEventHandlers
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
// Register event handlers
|
||||
if (handlers.onNewEmail) {
|
||||
socket.on("new_email", handlers.onNewEmail);
|
||||
}
|
||||
|
||||
if (handlers.onEmailRead) {
|
||||
socket.on("email_read", handlers.onEmailRead);
|
||||
}
|
||||
|
||||
if (handlers.onEmailDeleted) {
|
||||
socket.on("email_deleted", handlers.onEmailDeleted);
|
||||
}
|
||||
|
||||
if (handlers.onEmailSent) {
|
||||
socket.on("email_sent", handlers.onEmailSent);
|
||||
}
|
||||
|
||||
if (handlers.onUnreadCountUpdated) {
|
||||
socket.on("unread_count_updated", handlers.onUnreadCountUpdated);
|
||||
}
|
||||
|
||||
if (handlers.onMailboxUpdated) {
|
||||
socket.on("mailbox_updated", handlers.onMailboxUpdated);
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
socket.off("new_email");
|
||||
socket.off("email_read");
|
||||
socket.off("email_deleted");
|
||||
socket.off("email_sent");
|
||||
socket.off("unread_count_updated");
|
||||
socket.off("mailbox_updated");
|
||||
};
|
||||
}, [socket, handlers]);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import io, { Socket } from "socket.io-client";
|
||||
|
||||
interface UseEmailWebSocketOptions {
|
||||
token: string;
|
||||
serverUrl?: string;
|
||||
onConnect?: () => void;
|
||||
onDisconnect?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
export const useEmailWebSocket = ({
|
||||
token,
|
||||
serverUrl = `ws://${import.meta.env.VITE_BASE_URL}/email`,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onError,
|
||||
}: UseEmailWebSocketOptions) => {
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
|
||||
const newSocket = io(serverUrl, {
|
||||
auth: { token },
|
||||
transports: ["websocket", "polling"],
|
||||
});
|
||||
|
||||
// Connection events
|
||||
newSocket.on("connect", () => {
|
||||
console.log("✅ اتصال به WebSocket ایمیل برقرار شد");
|
||||
setIsConnected(true);
|
||||
onConnect?.();
|
||||
});
|
||||
|
||||
newSocket.on("disconnect", () => {
|
||||
console.log("❌ اتصال به WebSocket ایمیل قطع شد");
|
||||
setIsConnected(false);
|
||||
onDisconnect?.();
|
||||
});
|
||||
|
||||
newSocket.on("connection_established", (data) => {
|
||||
console.log("🎉 اتصال WebSocket ایمیل تأیید شد:", data);
|
||||
});
|
||||
|
||||
newSocket.on("connection_error", (error) => {
|
||||
console.error("💥 خطای اتصال WebSocket ایمیل:", error);
|
||||
onError?.(error.error);
|
||||
});
|
||||
|
||||
// Health monitoring
|
||||
newSocket.on("ping", () => {
|
||||
newSocket.emit("pong", { timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
setSocket(newSocket);
|
||||
|
||||
return () => {
|
||||
newSocket.close();
|
||||
setSocket(null);
|
||||
};
|
||||
}, [token, serverUrl]);
|
||||
|
||||
return { socket, isConnected };
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
interface UseNotificationSoundOptions {
|
||||
enabled?: boolean;
|
||||
volume?: number;
|
||||
soundUrl?: string;
|
||||
}
|
||||
|
||||
export const useNotificationSound = ({
|
||||
enabled = true,
|
||||
volume = 0.5,
|
||||
soundUrl = "/notification.mp3",
|
||||
}: UseNotificationSoundOptions = {}) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const playSound = useCallback(async () => {
|
||||
if (!enabled || isPlaying) {
|
||||
console.log("صدا غیرفعال است یا در حال پخش");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsPlaying(true);
|
||||
console.log("شروع پخش صدا...", { soundUrl, volume, enabled });
|
||||
|
||||
const audio = new Audio();
|
||||
audio.preload = "auto";
|
||||
audio.src = soundUrl;
|
||||
audio.volume = Math.max(0, Math.min(1, volume));
|
||||
|
||||
// Add event listeners
|
||||
audio.addEventListener("ended", () => {
|
||||
console.log("پخش صدا تمام شد");
|
||||
setIsPlaying(false);
|
||||
});
|
||||
|
||||
audio.addEventListener("error", (e) => {
|
||||
console.error("خطا در پخش صدا:", e);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
|
||||
// Wait for audio to be ready
|
||||
if (audio.readyState >= 2) {
|
||||
await audio.play();
|
||||
console.log("🔊 صدای نوتیفیکیشن پخش شد");
|
||||
} else {
|
||||
audio.addEventListener("canplay", async () => {
|
||||
try {
|
||||
await audio.play();
|
||||
console.log("🔊 صدای نوتیفیکیشن پخش شد");
|
||||
} catch (playError) {
|
||||
console.error("خطا در پخش بعد از آماده شدن:", playError);
|
||||
setIsPlaying(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("خطا در ایجاد audio element:", error);
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}, [enabled, isPlaying, soundUrl, volume]);
|
||||
|
||||
const stopSound = useCallback(() => {
|
||||
setIsPlaying(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
playSound,
|
||||
stopSound,
|
||||
isPlaying,
|
||||
};
|
||||
};
|
||||
@@ -48,6 +48,37 @@ body,
|
||||
}
|
||||
* {
|
||||
outline: none;
|
||||
|
||||
/* Custom slider styles for volume control */
|
||||
.slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: #e5e7eb;
|
||||
border-radius: 5px;
|
||||
height: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #374151;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #374151;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
|
||||
@@ -9,17 +9,18 @@ import AnswerIcon from '@/assets/images/answer.svg'
|
||||
import Header from './Components/Header'
|
||||
import ReactQuill from 'react-quill-new';
|
||||
import Select from '@/components/Select'
|
||||
import { useGetInbox, useGetMessageDetail, useSendEmail } from './hooks/useEmailData'
|
||||
import { useGetMessageDetail, useSendEmail } from './hooks/useEmailData'
|
||||
import { useAuthStore } from '../auth/store/AuthStore'
|
||||
import { SendEmailDto } from './types/Types'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { formatDate, sanitizeEmailHTML } from '@/config/func'
|
||||
import { formatDate, sanitizeEmailHTML, detectTextDirection } from '@/config/func'
|
||||
import SkeletonDetail from './Components/SkeletonDetail'
|
||||
import { useEmailActions } from '@/hooks/useEmailActions'
|
||||
|
||||
const DetailEmail: FC = () => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { refetch } = useGetInbox({ page: 1 })
|
||||
const emailActions = useEmailActions()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const { email: userEmail } = useAuthStore()
|
||||
const [showAnswer, setShowAnswer] = useState<boolean>(false)
|
||||
@@ -27,7 +28,7 @@ const DetailEmail: FC = () => {
|
||||
const [priority, setPriority] = useState<string>('')
|
||||
|
||||
// API hooks
|
||||
const { data: messageDetail, isLoading, error, isSuccess } = useGetMessageDetail(id || '', !!id)
|
||||
const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', !!id)
|
||||
const sendEmailMutation = useSendEmail()
|
||||
// const saveDraftMutation = useSaveDraft()
|
||||
|
||||
@@ -108,11 +109,13 @@ const DetailEmail: FC = () => {
|
||||
toast('قابلیت ارسال به دیگران به زودی اضافه خواهد شد', 'info')
|
||||
}
|
||||
|
||||
// Mark message as seen when it's loaded and not already seen
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
refetch()
|
||||
if (messageDetail && !messageDetail.seen && id) {
|
||||
console.log('Marking message as seen:', id, 'Current seen status:', messageDetail.seen)
|
||||
emailActions.markAsSeen(Number(id))
|
||||
}
|
||||
}, [isSuccess])
|
||||
}, [messageDetail, emailActions, id])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -166,10 +169,15 @@ const DetailEmail: FC = () => {
|
||||
{messageDetail.html && messageDetail.html.length > 0 ? (
|
||||
<div
|
||||
className='email-content-container border border-gray-200 rounded-lg p-4 overflow-hidden'
|
||||
style={{
|
||||
direction: detectTextDirection(messageDetail.html.join(''))
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeEmailHTML(messageDetail.html.join('')) }}
|
||||
/>
|
||||
) : (
|
||||
<p>{messageDetail.text || 'محتوای پیام'}</p>
|
||||
<p style={{
|
||||
direction: detectTextDirection(messageDetail.text || '')
|
||||
}}>{messageDetail.text || 'محتوای پیام'}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import Filters, { FilterValues } from '../../components/Filters';
|
||||
import { FC, useState } from 'react'
|
||||
import { FC, useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Table from '../../components/Table';
|
||||
import { ColumnType } from '../../components/types/TableTypes';
|
||||
import { InfoCircle, More, Refresh2, Trash, Edit, Archive, ArchiveTick, Star1 } from 'iconsax-react';
|
||||
import { RowActionItem } from '../../components/RowActionsDropdown';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useGetInbox } from './hooks/useEmailData';
|
||||
import { InboxMessage } from './types/Types';
|
||||
import { formatDate } from '@/config/func';
|
||||
import MarkAsRead from '@/assets/images/mark_as_read.svg'
|
||||
import { useEmailActions } from '@/hooks/useEmailActions';
|
||||
import { useEmailWebSocketContext } from '@/contexts/EmailWebSocketContext';
|
||||
import { useEmailEvents } from '@/hooks/useEmailEvents';
|
||||
|
||||
const List: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState<FilterValues>({});
|
||||
@@ -21,10 +24,38 @@ const List: FC = () => {
|
||||
|
||||
const { data: inboxData, isLoading, refetch, isFetching } = useGetInbox({
|
||||
page: currentPage,
|
||||
limit: 10,
|
||||
limit: 25,
|
||||
...filters
|
||||
});
|
||||
|
||||
// WebSocket integration
|
||||
const { socket } = useEmailWebSocketContext();
|
||||
|
||||
const handleNewEmailEvent = useCallback(() => {
|
||||
// Refresh inbox when new email arrives
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
const handleEmailActionEvent = useCallback(() => {
|
||||
// Refresh inbox when email actions occur
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
useEmailEvents(socket, {
|
||||
onNewEmail: handleNewEmailEvent,
|
||||
onEmailRead: handleEmailActionEvent,
|
||||
onEmailDeleted: handleEmailActionEvent,
|
||||
// onMailboxUpdated: handleEmailActionEvent,
|
||||
});
|
||||
|
||||
// Force refresh when component mounts or location changes
|
||||
useEffect(() => {
|
||||
// Refetch when coming back to inbox from other pages
|
||||
if (location.pathname === '/') {
|
||||
refetch();
|
||||
}
|
||||
}, [location.pathname, refetch]);
|
||||
|
||||
const [selectedMessages, setSelectedMessages] = useState<InboxMessage[]>([]);
|
||||
|
||||
const columns: ColumnType<InboxMessage>[] = [
|
||||
@@ -32,7 +63,14 @@ const List: FC = () => {
|
||||
key: 'subject',
|
||||
title: 'عنوان پیام',
|
||||
render: (item) => (
|
||||
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
|
||||
<div className="truncate max-w-[250px]">{item.subject || 'بدون موضوع'}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'intro',
|
||||
title: 'پیش نمایش',
|
||||
render: (item) => (
|
||||
<div className="truncate max-w-[250px]">{item.intro || 'بدون موضوع'}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,7 +26,8 @@ export const useGetInbox = (query: MessageListQueryDto) => {
|
||||
return useQuery({
|
||||
queryKey: ['inbox', query],
|
||||
queryFn: () => api.getInbox(query),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
staleTime: 0, // Always fetch fresh data
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -35,7 +36,7 @@ export const useGetMessageDetail = (messageId: string, enabled: boolean = true)
|
||||
queryKey: ['message', messageId],
|
||||
queryFn: () => api.getMessageDetail(messageId),
|
||||
enabled: !!messageId && enabled,
|
||||
staleTime: 1000 * 60 * 10, // 10 minutes
|
||||
staleTime: 0, // Always fetch fresh data for message details
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@ const List: FC = () => {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'intro',
|
||||
title: 'پیش نمایش',
|
||||
render: (item) => (
|
||||
<div className="truncate max-w-[250px]">{item.intro || 'بدون موضوع'}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'to',
|
||||
title: 'گیرنده',
|
||||
@@ -55,7 +62,7 @@ const List: FC = () => {
|
||||
title: 'تاریخ ارسال',
|
||||
align: 'center',
|
||||
render: (item) => (
|
||||
<span className="text-xs md:text-sm">
|
||||
<span className="">
|
||||
{formatDate(item.date)}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -37,7 +37,6 @@ const List: FC = () => {
|
||||
title: 'فرستنده',
|
||||
render: (item) => (
|
||||
<div className="flex items-center">
|
||||
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
|
||||
<span className="truncate">{item.from.name || item.from.address}</span>
|
||||
</div>
|
||||
)
|
||||
@@ -109,7 +108,7 @@ const List: FC = () => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const messages = spamData?.data?.results || [];
|
||||
const messages = spamData?.data?.junkMails || [];
|
||||
const pager = spamData?.data?.pager;
|
||||
|
||||
return (
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface MessageListQueryDto {
|
||||
export interface SpamResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
results: SpamMessage[];
|
||||
junkMails: SpamMessage[];
|
||||
pager: {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
|
||||
@@ -15,6 +15,7 @@ import Login from '@/pages/auth/Login'
|
||||
const AppRouter: FC = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path={'/'} element={<ReceivedList />} />
|
||||
<Route path={Paths.received} element={<ReceivedList />} />
|
||||
<Route path={Paths.sent} element={<SentList />} />
|
||||
<Route path={Paths.draft} element={<DraftList />} />
|
||||
|
||||
+12
-3
@@ -2,9 +2,9 @@ import { FC, useEffect, useState } from 'react'
|
||||
import Input from '../components/Input'
|
||||
import { Element3, HambergerMenu, Wallet } from 'iconsax-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Paths } from '@/utils/Paths'
|
||||
// import Notifications from '../pages/notification/Notification'
|
||||
import { EmailNotifications } from '@/components/EmailNotifications'
|
||||
import { useSharedStore } from './store/sharedStore'
|
||||
// import { useGetProfile } from '../pages/profile/hooks/useProfileData'
|
||||
// import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
||||
@@ -14,12 +14,16 @@ import { useSharedStore } from './store/sharedStore'
|
||||
const Header: FC = () => {
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [popoverKey, setPopoverKey] = useState(0);
|
||||
const { t } = useTranslation('global')
|
||||
const { setOpenSidebar, openSidebar } = useSharedStore()
|
||||
// const { data } = useGetProfile()
|
||||
// const getWalletBalance = useGetWalletBalance()
|
||||
|
||||
// فرض میکنیم که token از localStorage گرفته میشود
|
||||
const userToken = localStorage.getItem('jwt-token') || '';
|
||||
|
||||
console.log(popoverKey);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -57,7 +61,12 @@ const Header: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
{/* <Notifications /> */}
|
||||
{userToken && (
|
||||
<EmailNotifications
|
||||
userToken={userToken}
|
||||
onEmailClick={(email) => navigate(`/mail/${email.messageId}`)}
|
||||
/>
|
||||
)}
|
||||
{/* {
|
||||
data && (
|
||||
<Popover className="relative" key={popoverKey}>
|
||||
|
||||
Reference in New Issue
Block a user