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
+11 -4
View File
@@ -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>
+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>
);
};
+43 -4
View File
@@ -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, ""); // حذف سمیکالن‌های ابتدا و انتها
+38
View File
@@ -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;
};
+92
View File
@@ -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]);
};
+66
View File
@@ -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 };
};
+72
View File
@@ -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,
};
};
+31
View File
@@ -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,
+16 -8
View File
@@ -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>
+42 -4
View File
@@ -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>
)
},
{
+3 -2
View File
@@ -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
});
};
+8 -1
View File
@@ -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>
)
+1 -2
View File
@@ -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 (
+1 -1
View File
@@ -47,7 +47,7 @@ export interface MessageListQueryDto {
export interface SpamResponse {
success: boolean;
data: {
results: SpamMessage[];
junkMails: SpamMessage[];
pager: {
page: number;
totalPages: number;
+1
View File
@@ -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
View File
@@ -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}>