remove comments

This commit is contained in:
hamid zarghami
2025-08-17 11:31:29 +03:30
parent bec4c2c1d1
commit 561bf9d5d5
26 changed files with 13 additions and 172 deletions
+1 -9
View File
@@ -40,7 +40,6 @@ i18next.init({
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: async (error: IApiErrorRepsonse) => {
// نمایش پیغام خطا از API
const errorMessage = error?.response?.error?.message?.[0] || undefined;
if (errorMessage) {
@@ -49,9 +48,8 @@ const queryClient = new QueryClient({
if (error?.response?.status === 401) {
try {
// Use a flag to track if refresh is in progress
if (window.isRefreshingToken) {
// Wait for the refresh to complete
await new Promise(resolve => {
const checkComplete = setInterval(() => {
if (!window.isRefreshingToken) {
@@ -63,7 +61,6 @@ const queryClient = new QueryClient({
return;
}
// Set the flag to indicate refresh is in progress
window.isRefreshingToken = true;
try {
@@ -71,15 +68,12 @@ const queryClient = new QueryClient({
const { data } = await refreshToken({ refreshToken: refreshTokenValue || '' });
if (data?.accessToken?.token) {
// Save the new token
await setToken(data?.accessToken?.token);
// If refresh token also returned, update it
if (data.refreshToken?.token) {
await setRefreshToken(data.refreshToken?.token);
}
// Retry the failed request
queryClient.invalidateQueries();
return;
} else {
@@ -90,7 +84,6 @@ const queryClient = new QueryClient({
}
} catch (refreshError: unknown) {
console.error('Token refresh failed:', refreshError);
// Clear tokens and redirect to login
await removeToken();
await removeRefreshToken();
window.location.href = '/auth/login';
@@ -102,7 +95,6 @@ const queryClient = new QueryClient({
queries: {
refetchOnWindowFocus: false,
retry: false
// staleTime: 86400000 // 1 day in milliseconds
}
}
});
+2 -16
View File
@@ -23,11 +23,9 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
onError: () => null,
});
// 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');
@@ -40,26 +38,21 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
}
}, []);
// Notification sound hook
const { playSound } = useNotificationSound({
enabled: soundEnabled,
volume: soundVolume,
});
const handleNewEmail = useCallback((email: EmailPayload) => {
setNotifications(prev => [email, ...prev.slice(0, 9)]);
// 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
tag: `email-${email.messageId}`,
});
}
// Play notification sound
playSound();
}, [playSound]);
@@ -68,14 +61,12 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
}, []);
const handleEmailRead = useCallback((data: { messageId: number }) => {
// Remove from notifications if it was there
setNotifications(prev =>
prev.filter(notification => notification.messageId !== data.messageId)
);
}, []);
const handleEmailSent = useCallback((data: EmailStatusPayload) => {
// Remove from notifications if it was there
setNotifications(prev =>
prev.filter(notification => notification.messageId !== data.messageId)
);
@@ -88,7 +79,6 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
onEmailSent: handleEmailSent,
});
// Request notification permission on mount
React.useEffect(() => {
if (Notification.permission === 'default') {
Notification.requestPermission();
@@ -97,11 +87,9 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
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={() => {
playSound();
@@ -112,7 +100,6 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
<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)}
@@ -125,7 +112,6 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
)}
</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">
-1
View File
@@ -11,7 +11,6 @@ interface EmojiReactionProps {
const EmojiReaction: FC<EmojiReactionProps> = ({ onEmojiSelect, className = '' }) => {
const [isOpen, setIsOpen] = useState(false)
// استفاده از hook موجود برای تشخیص کلیک خارج از المان
const containerRef = useOutsideClick(() => {
if (isOpen) {
setIsOpen(false)
+2 -13
View File
@@ -7,7 +7,6 @@ import DefaulModal from './DefaulModal';
import moment from 'moment-jalaali'
import Button from './Button';
// تعریف نوع داده برای فیلدهای مختلف
export type DateFieldType = {
type: 'date';
name: string;
@@ -32,7 +31,6 @@ export type InputFieldType = {
export type FieldType = DateFieldType | SelectFieldType | InputFieldType;
// تعریف نوع داده برای مقادیر فیلترها
export type FilterValues = Record<string, string | null>;
interface FiltersProps {
@@ -41,7 +39,7 @@ interface FiltersProps {
initialValues?: FilterValues;
className?: string;
fieldClassName?: string;
searchField?: string; // نام فیلد جستجو که باید در انتها نمایش داده شود
searchField?: string;
}
const Filters: FC<FiltersProps> = ({
@@ -50,23 +48,20 @@ const Filters: FC<FiltersProps> = ({
initialValues = {},
className = "mt-6 md:mt-8 xl:mt-10 flex flex-col md:flex-row justify-between items-start md:items-center gap-4",
fieldClassName = "flex flex-col sm:flex-row gap-3 md:gap-4 w-full md:w-auto",
searchField = "search" // پیش‌فرض فیلد سرچ با نام search
searchField = "search"
}) => {
const [filters, setFilters] = useState<FilterValues>({});
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
const [inputValues, setInputValues] = useState<Record<string, string>>({});
const onChangeRef = useRef(onChange);
// آپدیت کردن ref وقتی onChange تغییر می‌کند
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
// تنظیم مقادیر اولیه
useEffect(() => {
if (Object.keys(initialValues).length > 0) {
setFilters(initialValues);
// تنظیم مقادیر input برای فیلدهای نوع input
const inputFields: Record<string, string> = {};
fields.forEach(field => {
if (field.type === 'input' && initialValues[field.name]) {
@@ -75,7 +70,6 @@ const Filters: FC<FiltersProps> = ({
});
setInputValues(inputFields);
} else {
// تنظیم مقادیر پیش‌فرض از فیلدها
const defaultValues: FilterValues = {};
const defaultInputs: Record<string, string> = {};
fields.forEach(field => {
@@ -95,7 +89,6 @@ const Filters: FC<FiltersProps> = ({
}
}, [fields, initialValues, onChange]);
// Debounce برای فیلدهای input
useEffect(() => {
const timeouts: Record<string, number> = {};
@@ -170,13 +163,11 @@ const Filters: FC<FiltersProps> = ({
}
};
// جداسازی فیلد جستجو از سایر فیلدها
const searchFieldObj = fields.find(field => field.name === searchField);
const otherFields = fields.filter(field => field.name !== searchField);
return (
<div className={className}>
{/* آیکون فیلتر برای موبایل */}
<div className="flex md:hidden items-center justify-between w-full mb-2">
<button
onClick={() => setIsFiltersOpen(!isFiltersOpen)}
@@ -192,7 +183,6 @@ const Filters: FC<FiltersProps> = ({
)}
</div>
{/* فیلترها برای دسکتاپ (همیشه نمایش داده می‌شوند) */}
<div className="hidden md:flex md:flex-row justify-between items-center gap-4 w-full">
<div className={fieldClassName}>
{otherFields.map(renderField)}
@@ -204,7 +194,6 @@ const Filters: FC<FiltersProps> = ({
)}
</div>
{/* فیلترها برای موبایل (در قالب مودال) */}
<DefaulModal
open={isFiltersOpen}
close={() => setIsFiltersOpen(false)}
+1 -3
View File
@@ -42,13 +42,11 @@ const Input: FC<Props> = (props: Props) => {
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (props.seprator) {
const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها
const inputValue = event.target.value.replace(/,/g, '');
const formatted = formatNumber(inputValue);
// به‌روزرسانی مقدار قالب‌بندی‌شده
setFormattedValue(formatted);
// ارسال مقدار خام به `onChange` والد
props.onChange?.({
...event,
target: {
-1
View File
@@ -16,7 +16,6 @@ const Pagination: React.FC<PaginationProps> = ({
onNext,
onPrevious,
}) => {
// اگر hasNext فالس باشد، pagination نمایش داده نمی‌شود
if (!hasNext && !previousCursor) return null;
return (
+1 -4
View File
@@ -40,22 +40,19 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, classN
const rect = buttonRef.current.getBoundingClientRect();
const isMobile = window.innerWidth < 768;
const dropdownWidth = isMobile ? 140 : 150;
const dropdownHeight = actions.length * (isMobile ? 36 : 40); // تخمین ارتفاع منو
const dropdownHeight = actions.length * (isMobile ? 36 : 40);
let left = rect.left + window.scrollX - dropdownWidth + rect.width;
let top = rect.bottom + window.scrollY;
// بررسی اینکه منو از سمت راست خارج نشود
if (left + dropdownWidth > window.innerWidth) {
left = rect.left + window.scrollX - dropdownWidth;
}
// بررسی اینکه منو از سمت چپ خارج نشود
if (left < 0) {
left = isMobile ? 20 : 40;
}
// بررسی اینکه منو از پایین خارج نشود
if (top + dropdownHeight > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - dropdownHeight;
}
-6
View File
@@ -15,7 +15,6 @@ export const SoundSettings: React.FC<SoundSettingsProps> = ({ className = '' })
volume: volume,
});
// Load settings from localStorage on mount
useEffect(() => {
const savedEnabled = localStorage.getItem('notificationSoundEnabled');
const savedVolume = localStorage.getItem('notificationSoundVolume');
@@ -28,7 +27,6 @@ export const SoundSettings: React.FC<SoundSettingsProps> = ({ className = '' })
}
}, []);
// Save settings to localStorage when changed
useEffect(() => {
localStorage.setItem('notificationSoundEnabled', isEnabled.toString());
localStorage.setItem('notificationSoundVolume', volume.toString());
@@ -37,7 +35,6 @@ export const SoundSettings: React.FC<SoundSettingsProps> = ({ className = '' })
const handleVolumeChange = (newVolume: number) => {
setVolume(newVolume);
if (isEnabled) {
// Test sound with new volume
setTimeout(() => playSound(), 100);
}
};
@@ -46,7 +43,6 @@ export const SoundSettings: React.FC<SoundSettingsProps> = ({ className = '' })
const newEnabled = !isEnabled;
setIsEnabled(newEnabled);
if (newEnabled) {
// Test sound when enabling
setTimeout(() => playSound(), 100);
}
};
@@ -59,7 +55,6 @@ export const SoundSettings: React.FC<SoundSettingsProps> = ({ className = '' })
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"
@@ -68,7 +63,6 @@ export const SoundSettings: React.FC<SoundSettingsProps> = ({ className = '' })
{getVolumeIcon()}
</button>
{/* Volume Slider */}
{isEnabled && (
<div className="flex items-center gap-2">
<input
+1 -26
View File
@@ -35,21 +35,17 @@ const Table = <T extends RowDataType>({
const longPressTimer = useRef<number | null>(null);
const [longPressActiveId, setLongPressActiveId] = useState<string | number | null>(null);
// استفاده از selectedRows خارجی اگر موجود باشد، در غیر این صورت از internal استفاده کن
const selectedRows = externalSelectedRows !== undefined ? externalSelectedRows : internalSelectedRows;
// پاک کردن انتخاب‌ها وقتی clearSelection تغییر کند
React.useEffect(() => {
if (clearSelection && externalSelectedRows === undefined) {
setInternalSelectedRows([]);
}
// خروج از حالت انتخاب موبایل هم
if (clearSelection) {
setMobileSelectionMode(false);
}
}, [clearSelection, externalSelectedRows]);
// تمیز کردن timer هنگام unmount
React.useEffect(() => {
return () => {
if (longPressTimer.current) {
@@ -58,7 +54,6 @@ const Table = <T extends RowDataType>({
};
}, []);
// خروج از حالت انتخاب اگر هیچ آیتمی انتخاب نشده، ورود اگر آیتمی انتخاب شده
React.useEffect(() => {
if (selectedRows.length === 0 && mobileSelectionMode) {
setMobileSelectionMode(false);
@@ -73,10 +68,9 @@ const Table = <T extends RowDataType>({
setLongPressActiveId(item.id);
longPressTimer.current = window.setTimeout(() => {
setMobileSelectionMode(true);
// انتخاب آیتم فعلی
handleSingleRowSelect(item, true);
setLongPressActiveId(null);
}, 500); // 500ms برای long press
}, 500);
};
const handleLongPressEnd = () => {
@@ -89,11 +83,9 @@ const Table = <T extends RowDataType>({
const handleMobileRowClick = (item: T) => {
if (mobileSelectionMode) {
// در حالت انتخاب، آیتم را انتخاب/عدم انتخاب کن
const isSelected = isRowSelected(item);
handleSingleRowSelect(item, !isSelected);
} else if (longPressActiveId !== item.id) {
// در حالت عادی، row click عادی را اجرا کن
handleRowClick(item);
}
};
@@ -120,12 +112,10 @@ const Table = <T extends RowDataType>({
const newSelectedRows = selectedRows.length === data.length ? [] : [...data];
if (externalSelectedRows !== undefined) {
// اگر selectedRows از خارج کنترل می‌شود، فقط callback را فراخوانی کن
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
} else {
// در غیر این صورت internal state را به‌روزرسانی کن
setInternalSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
@@ -139,12 +129,10 @@ const Table = <T extends RowDataType>({
: selectedRows.filter((row) => row.id !== item.id);
if (externalSelectedRows !== undefined) {
// اگر selectedRows از خارج کنترل می‌شود، فقط callback را فراخوانی کن
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
} else {
// در غیر این صورت internal state را به‌روزرسانی کن
setInternalSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
@@ -156,7 +144,6 @@ const Table = <T extends RowDataType>({
return selectedRows.some((row) => row.id === item.id);
};
// Gmail-style layout برای وقتی header نمایش داده نمی‌شود
const renderGmailStyleRows = () => {
const hasActions = (actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions));
const roundedClass = hasActions ? 'rounded-b-2xl' : 'rounded-2xl';
@@ -183,7 +170,6 @@ const Table = <T extends RowDataType>({
return (
<div className={`bg-white ${roundedClass} overflow-hidden`}>
{data.map((item, rowIndex) => {
// Gmail-style: خوانده نشده = پس‌زمینه سفید، خوانده شده = پس‌زمینه خاکستری
const isUnread = 'seen' in item && !item.seen;
const bgColor = isUnread ? 'bg-white' : 'bg-gray-50';
const hoverColor = isUnread ? 'hover:bg-gray-50' : 'hover:bg-gray-100';
@@ -213,16 +199,11 @@ const Table = <T extends RowDataType>({
</div>
)}
{/* Inline Actions - بعد از checkbox */}
<div className="flex-1 min-w-0">
{/* محتوای اصلی - ستون اول */}
<div className={`flex items-center mb-0.5 text-sm ${fontWeight}`}>
{columns[0]?.render ? columns[0].render(item) : (item[columns[0]?.key] as React.ReactNode)}
</div>
{/* اطلاعات اضافی - سه ستون آخر در یک خط */}
{columns.length > 1 && (
<div className={`flex items-center gap-3 text-xs text-gray-500 ${fontWeight}`}>
{columns.slice(1).map((col) => (
@@ -269,7 +250,6 @@ const Table = <T extends RowDataType>({
);
}
return data.map((item, rowIndex) => {
// Gmail-style: خوانده نشده = پس‌زمینه سفید، خوانده شده = پس‌زمینه خاکستری
const isUnread = 'seen' in item && !item.seen;
const bgColor = isUnread ? 'bg-white' : 'bg-gray-50';
const hoverColor = isUnread ? 'hover:bg-gray-50' : 'hover:bg-gray-100';
@@ -378,11 +358,9 @@ const Table = <T extends RowDataType>({
);
};
// اگر header نشان داده نمی‌شود، از Gmail-style layout فقط در موبایل استفاده کن
if (!showHeader) {
return (
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
{/* نسخه موبایل - Gmail style */}
<div className={`md:hidden`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
<div className="h-[64px] bg-white flex items-center justify-between px-4 rounded-t-2xl border-b border-[#EAEDF5]">
@@ -394,7 +372,6 @@ const Table = <T extends RowDataType>({
<button
onClick={() => {
setMobileSelectionMode(false);
// پاک کردن انتخاب‌ها
if (externalSelectedRows !== undefined) {
if (onSelectionChange) {
onSelectionChange([]);
@@ -416,7 +393,6 @@ const Table = <T extends RowDataType>({
{renderGmailStyleRows()}
</div>
{/* نسخه دسکتاپ - Table معمولی */}
<div className={`hidden md:block`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
<div className="h-[74px] bg-white flex items-center px-6 rounded-t-3xl">
@@ -509,7 +485,6 @@ const MyComponent = () => {
const handlePageChange = (page: number) => {
setCurrentPage(page);
// اینجا می‌توانید API call جدید برای دریافت داده‌های صفحه جدید بزنید
};
return (
+1 -5
View File
@@ -13,24 +13,20 @@ let addToast: (toast: Toast) => void;
const ToastContainer: React.FC = () => {
const [toasts, setToasts] = useState<Toast[]>([]);
// ثبت toast جدید
const showToast = (toast: Toast) => {
setToasts((prev) => [...prev, toast]);
// حذف خودکار بعد از ۳ ثانیه
setTimeout(() => {
setToasts((prev) => prev.map(t =>
t.id === toast.id ? { ...t, isExiting: true } : t
));
// حذف از DOM بعد از اتمام انیمیشن
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== toast.id));
}, 300); // مدت زمان انیمیشن خروج
}, 300);
}, 3000);
};
// تخصیص تابع نمایش toast به متغیر سراسری
useEffect(() => {
addToast = showToast;
}, []);
-3
View File
@@ -38,7 +38,6 @@ const EmailInput: FC<EmailInputProps> = ({
return
}
// Always search via API for fresh results
if (onSearchSuggestions) {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current)
@@ -51,7 +50,6 @@ const EmailInput: FC<EmailInputProps> = ({
setSelectedSuggestionIndex(-1)
}
// Update filtered suggestions when API suggestions change
useEffect(() => {
if (suggestions.length > 0) {
const filtered = suggestions
@@ -63,7 +61,6 @@ const EmailInput: FC<EmailInputProps> = ({
setFilteredSuggestions(filtered)
setShowSuggestions(filtered.length > 0 && to.trim().length > 0)
} else if (to.trim() === '') {
// Clear suggestions when input is empty
setFilteredSuggestions([])
setShowSuggestions(false)
}
@@ -20,35 +20,29 @@ export const useNewMessage = () => {
} = useSharedStore();
const { email: userEmail } = useAuthStore();
// Form state
const [toEmails, setToEmails] = useState<string[]>([]);
const [subject, setSubject] = useState("");
const [content, setContent] = useState("");
const [priority, setPriority] = useState("");
const [attachments, setAttachments] = useState<EmailAttachmentDto[]>([]);
// Loading states
const [isSending, setIsSending] = useState(false);
const [isSavingDraft, setIsSavingDraft] = useState(false);
// API hooks
const sendEmailMutation = useSendEmail();
const updateDraftMutation = useUpdateDraft();
const sendDraftMutation = useSendDraft();
// Email suggestions
const {
suggestions: emailSuggestions,
searchSuggestions,
} = useEmailSuggestions();
// Get draft detail if draftData exists
const { data: draftDetail } = useGetMessageDetail(
draftData?.id.toString() || "",
draftData?.mailbox || ""
);
// Load draft data into form
useEffect(() => {
if (draftDetail) {
const toEmailsList =
@@ -81,7 +75,6 @@ export const useNewMessage = () => {
return emailRegex.test(email);
};
// Function to search email suggestions
const searchEmailSuggestions = (query: string) => {
searchSuggestions(query);
};
@@ -237,7 +230,6 @@ export const useNewMessage = () => {
};
return {
// Form state
toEmails,
subject,
content,
@@ -245,11 +237,9 @@ export const useNewMessage = () => {
attachments,
emailSuggestions,
// Loading states
isSending,
isSavingDraft,
// Form handlers
addEmail,
removeEmail,
setSubject,
@@ -257,10 +247,8 @@ export const useNewMessage = () => {
setPriority,
setAttachments,
// Search functions
searchEmailSuggestions,
// Actions
handleSend,
handleSaveDraft,
resetForm,
@@ -20,7 +20,6 @@ export const useReply = ({
const [content, setContent] = useState<string>("");
const [isExpanded, setIsExpanded] = useState<boolean>(false);
// API hooks
const sendEmailMutation = useSendEmail();
const resetForm = () => {
@@ -39,7 +38,6 @@ export const useReply = ({
return false;
}
// Format date to Persian
const originalDate = new Date(originalMessage.date);
const persianDate = new Intl.DateTimeFormat("fa-IR", {
weekday: "long",
@@ -143,12 +141,10 @@ ${
};
return {
// State
content,
isExpanded,
isSending: sendEmailMutation.isPending,
// Handlers
setContent,
expandReply,
collapseReply,
@@ -156,7 +152,6 @@ ${
handleSaveDraft,
resetForm,
// Original message data
originalMessage,
};
};
+4 -23
View File
@@ -41,7 +41,7 @@ export const removeRefreshToken = async () => {
export const timeAgo = (date: string | Date): string => {
const now = new Date();
const past = new Date(date);
const diff = Math.floor((now.getTime() - past.getTime()) / 1000); // اختلاف بر حسب ثانیه
const diff = Math.floor((now.getTime() - past.getTime()) / 1000);
const units = [
{ name: "سال", value: 60 * 60 * 24 * 365 },
@@ -82,48 +82,33 @@ export const formatDate = (dateString: string) => {
};
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
let sanitized = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
// Remove link tags (CSS files)
sanitized = sanitized.replace(/<link[^>]*>/gi, "");
// Remove script tags for security
sanitized = sanitized.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
// Remove potentially harmful attributes
sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, "");
// تشخیص جهت متن اصلی
const detectedDirection = detectTextDirection(sanitized);
// فقط font-family، font-size و font-weight رو از inline styles حذف کن
// text-align و direction رو فقط اگر با جهت تشخیص داده شده مطابقت نداشته باشد حذف کن
sanitized = sanitized.replace(
/style\s*=\s*["']([^"']*?)["']/gi,
(_, styles) => {
@@ -132,19 +117,17 @@ export const sanitizeEmailHTML = (html: string): string => {
.replace(/font-size\s*:\s*[^;]*;?/gi, "")
.replace(/font-weight\s*:\s*[^;]*;?/gi, "");
// اگر جهت تشخیص داده شده LTR است، direction و text-align را حفظ کن
if (detectedDirection === "ltr") {
// direction و text-align را حفظ کن
// TODO: Implement LTR
} 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, ""); // حذف سمیکالن‌های ابتدا و انتها
.replace(/;\s*;/g, ";")
.replace(/^\s*;\s*|\s*;\s*$/g, "");
return cleanedStyles ? `style="${cleanedStyles}"` : "";
}
@@ -157,7 +140,6 @@ export const sanitizeEmailHTML = (html: string): string => {
}
};
// Interface برای mailbox structure
interface MailboxItem {
id: string;
name: string;
@@ -165,7 +147,6 @@ interface MailboxItem {
total: number;
}
// Utility function برای گرفتن mailbox id از localStorage
export const getMailboxId = (mailboxName: string): string | null => {
try {
const mailboxesStr = localStorage.getItem("mailboxes");
-6
View File
@@ -20,9 +20,7 @@ import { toast } from '../components/Toast';
export const useEmailActions = (customRefetch?: () => void) => {
const queryClient = useQueryClient();
// تمام query key های موجود در سیستم
const invalidateAllQueries = () => {
// صفحه‌های اصلی
queryClient.invalidateQueries({ queryKey: ['inbox'] });
queryClient.invalidateQueries({ queryKey: ['drafts'] });
queryClient.invalidateQueries({ queryKey: ['archive-messages'] });
@@ -31,11 +29,9 @@ export const useEmailActions = (customRefetch?: () => void) => {
queryClient.invalidateQueries({ queryKey: ['trash-messages'] });
queryClient.invalidateQueries({ queryKey: ['search-messages'] });
// پیام‌های تکی
queryClient.invalidateQueries({ queryKey: ['message'] });
queryClient.invalidateQueries({ queryKey: ['draft'] });
// صفحه‌هایی که از React Query استفاده نمی‌کنند (مثل sent)
if (customRefetch) {
customRefetch();
}
@@ -162,7 +158,6 @@ export const useEmailActions = (customRefetch?: () => void) => {
restoreMutation.isPending;
return {
// Bulk actions
bulkAction: bulkActionMutation.mutate,
bulkMarkAsSeen: (messageIds: number[], mailbox: string) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.SEEN, mailbox: mailbox }),
@@ -183,7 +178,6 @@ export const useEmailActions = (customRefetch?: () => void) => {
bulkRestore: (messageIds: number[], mailbox: string) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.RESTORE, mailbox: mailbox }),
// Single actions (keeping for backward compatibility)
markAsSeen: markAsSeenMutation.mutate,
archive: archiveMutation.mutate,
favorite: favoriteMutation.mutate,
-1
View File
@@ -54,7 +54,6 @@ export const useEmailEvents = (
useEffect(() => {
if (!socket) return;
// Register event handlers
if (handlers.onNewEmail) {
socket.on("new_email", handlers.onNewEmail);
}
-2
View File
@@ -26,7 +26,6 @@ export const useEmailWebSocket = ({
transports: ["websocket", "polling"],
});
// Connection events
newSocket.on("connect", () => {
setIsConnected(true);
onConnect?.();
@@ -43,7 +42,6 @@ export const useEmailWebSocket = ({
onError?.(error.error);
});
// Health monitoring
newSocket.on("ping", () => {
newSocket.emit("pong", { timestamp: new Date().toISOString() });
});
-2
View File
@@ -8,11 +8,9 @@ const useDetectLatinAndNumbers = () => {
if (node.nodeType === Node.TEXT_NODE) {
const textContent = node.nodeValue;
if (textContent) {
// اگر عدد داشت
if (/\d/.test(textContent)) {
element.classList.add("has-number");
}
// اگر حرف لاتین داشت
if (/[a-zA-Z]/.test(textContent)) {
element.classList.add("has-latin");
}
-5
View File
@@ -32,7 +32,6 @@ const List: FC = () => {
const [selectedMessages, setSelectedMessages] = useState<TrashMessage[]>([]);
const [selectAll, setSelectAll] = useState(false);
// تابع برای گرفتن فقط ساعت
const formatTime = (dateString: string) => {
try {
if (!dateString) return '--:--';
@@ -49,7 +48,6 @@ const List: FC = () => {
}
};
// ستون‌های دسکتاپ
const desktopColumns: ColumnType<TrashMessage>[] = [
{
key: 'subject',
@@ -78,7 +76,6 @@ const List: FC = () => {
},
];
// ستون‌های موبایل (سه خطی)
const mobileColumns: ColumnType<TrashMessage>[] = [
{
key: 'content',
@@ -106,7 +103,6 @@ const List: FC = () => {
}
];
// انتخاب ستون‌ها بر اساس سایز صفحه
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
React.useEffect(() => {
@@ -170,7 +166,6 @@ const List: FC = () => {
const handleSelectionChange = (selectedRows: TrashMessage[]) => {
setSelectedMessages(selectedRows);
// تنظیم وضعیت select all بر اساس انتخاب شده‌ها
const messages = trashData?.data?.trashMails || [];
setSelectAll(selectedRows.length === messages.length && messages.length > 0);
};
-3
View File
@@ -10,7 +10,6 @@ export const getTrashMessages = async (
!!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/trash";
// اضافه کردن mailbox id از localStorage
const mailboxId = getMailboxId("Trash");
const queryWithMailbox = {
...query,
@@ -23,8 +22,6 @@ export const getTrashMessages = async (
return data;
};
// Note: deleteMessagePermanently and restoreMessage are now handled by EmailActionsService
export const emptyTrash = async () => {
const mailboxId = getMailboxId(MailboxEnum.TRASH);
const { data } = await axios.patch(
-5
View File
@@ -28,7 +28,6 @@ const List: FC = () => {
const [selectedMessages, setSelectedMessages] = useState<SearchMessage[]>([]);
const [selectAll, setSelectAll] = useState(false);
// تابع برای گرفتن فقط ساعت
const formatTime = (dateString: string) => {
try {
if (!dateString) return '--:--';
@@ -45,7 +44,6 @@ const List: FC = () => {
}
};
// ستون‌های دسکتاپ
const desktopColumns: ColumnType<SearchMessage>[] = [
{
key: 'subject',
@@ -81,7 +79,6 @@ const List: FC = () => {
},
];
// ستون‌های موبایل (سه خطی)
const mobileColumns: ColumnType<SearchMessage>[] = [
{
key: 'content',
@@ -112,7 +109,6 @@ const List: FC = () => {
}
];
// انتخاب ستون‌ها بر اساس سایز صفحه
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
React.useEffect(() => {
@@ -171,7 +167,6 @@ const List: FC = () => {
const handleSelectionChange = (selectedRows: SearchMessage[]) => {
setSelectedMessages(selectedRows);
// تنظیم وضعیت select all بر اساس انتخاب شده‌ها
const messages = searchData?.data?.results || [];
setSelectAll(selectedRows.length === messages.length && messages.length > 0);
};
-5
View File
@@ -29,7 +29,6 @@ const List: FC = () => {
const [selectedMessages, setSelectedMessages] = useState<InboxMessage[]>([]);
const [selectAll, setSelectAll] = useState(false);
// تابع برای گرفتن فقط ساعت
const formatTime = (dateString: string) => {
try {
if (!dateString) return '--:--';
@@ -46,7 +45,6 @@ const List: FC = () => {
}
};
// ستون‌های دسکتاپ
const desktopColumns: ColumnType<InboxMessage>[] = [
{
key: 'subject',
@@ -89,7 +87,6 @@ const List: FC = () => {
},
];
// ستون‌های موبایل (سه خطی)
const mobileColumns: ColumnType<InboxMessage>[] = [
{
key: 'content',
@@ -117,7 +114,6 @@ const List: FC = () => {
}
];
// انتخاب ستون‌ها بر اساس سایز صفحه
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
React.useEffect(() => {
@@ -179,7 +175,6 @@ const List: FC = () => {
const handleSelectionChange = (selectedRows: InboxMessage[]) => {
setSelectedMessages(selectedRows);
// تنظیم وضعیت select all بر اساس انتخاب شده‌ها
const messages = sentData?.data?.sentMails || [];
setSelectAll(selectedRows.length === messages.length && messages.length > 0);
};
-1
View File
@@ -14,7 +14,6 @@ export const getSentMessages = async (
!!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/sent";
// اضافه کردن mailbox id از localStorage
const mailboxId = getMailboxId("Sent Mail");
const queryWithMailbox = {
...query,
-1
View File
@@ -10,7 +10,6 @@ export const getSpamMessages = async (
!!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/junk";
// اضافه کردن mailbox id از localStorage
const mailboxId = getMailboxId("Junk");
const queryWithMailbox = {
...query,
-13
View File
@@ -1,7 +1,6 @@
import axios from "../config/axios";
import { BulkActionRequest } from "../pages/received/types/Types";
// Bulk action for multiple messages
export const bulkAction = async (request: BulkActionRequest) => {
const { data } = await axios.post(
`/email/messages/bulk-action?mailbox=${request.mailbox}`,
@@ -10,7 +9,6 @@ export const bulkAction = async (request: BulkActionRequest) => {
return data;
};
// Mark message as seen
export const markMessageAsSeen = async (messageId: number, mailbox: string) => {
const { data } = await axios.patch(
`/email/messages/${messageId}/seen?mailbox=${mailbox}`
@@ -18,7 +16,6 @@ export const markMessageAsSeen = async (messageId: number, mailbox: string) => {
return data;
};
// Move message to archive
export const moveMessageToArchive = async (
messageId: number,
mailbox: string
@@ -29,7 +26,6 @@ export const moveMessageToArchive = async (
return data;
};
// Move message to favorite
export const moveMessageToFavorite = async (
messageId: number,
mailbox: string
@@ -40,7 +36,6 @@ export const moveMessageToFavorite = async (
return data;
};
// Move message to trash
export const moveMessageToTrash = async (
messageId: number,
mailbox: string
@@ -51,7 +46,6 @@ export const moveMessageToTrash = async (
return data;
};
// Delete message permanently (only for trash mailbox)
export const deleteMessagePermanently = async (
messageId: number,
mailbox: string
@@ -62,13 +56,11 @@ export const deleteMessagePermanently = async (
return data;
};
// Send email
export const sendEmail = async (messageId: number) => {
const { data } = await axios.post(`/email/send?messageId=${messageId}`);
return data;
};
// Update draft
export const updateDraft = async (messageId: number, draftData: unknown) => {
const { data } = await axios.patch(
`/email/messages/drafts/${messageId}`,
@@ -77,7 +69,6 @@ export const updateDraft = async (messageId: number, draftData: unknown) => {
return data;
};
// Delete draft
export const deleteDraft = async (messageId: number, mailbox: string) => {
const { data } = await axios.delete(
`/email/messages/drafts/${messageId}?mailbox=${mailbox}`
@@ -85,13 +76,11 @@ export const deleteDraft = async (messageId: number, mailbox: string) => {
return data;
};
// Send draft
export const sendDraft = async (messageId: number) => {
const { data } = await axios.post(`/email/messages/drafts/${messageId}/send`);
return data;
};
// Unfavorite message
export const unfavoriteMessage = async (messageId: number, mailbox: string) => {
const { data } = await axios.patch(
`/email/messages/${messageId}/unfavorite?mailbox=${mailbox}`
@@ -99,7 +88,6 @@ export const unfavoriteMessage = async (messageId: number, mailbox: string) => {
return data;
};
// Unarchive message
export const unarchiveMessage = async (messageId: number, mailbox: string) => {
const { data } = await axios.patch(
`/email/messages/${messageId}/unarchive?mailbox=${mailbox}`
@@ -107,7 +95,6 @@ export const unarchiveMessage = async (messageId: number, mailbox: string) => {
return data;
};
// Restore message from trash
export const restoreMessage = async (messageId: number, mailbox: string) => {
const { data } = await axios.patch(
`/email/messages/${messageId}/restore?mailbox=${mailbox}`
-1
View File
@@ -31,7 +31,6 @@ const SideBar: FC = () => {
const iconSizeSideBar = 20
// if (!mailboxCount?.data) return null
return (
<>
{