hold action mobile + najva token updated

This commit is contained in:
hamid zarghami
2025-07-28 12:43:37 +03:30
parent 80114f6fc7
commit f67336eebe
5 changed files with 122 additions and 52 deletions
+5 -14
View File
@@ -1,5 +1,5 @@
import { useUpdatePushToken } from '@/pages/profile/hooks/useProfileData'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { updateProfile } from '../pages/profile/service/ProfileService'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
declare global { declare global {
@@ -15,36 +15,28 @@ const NajvaToken: FC = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const [isNajvaToken, setIsNajvaToken] = useState(false) const [isNajvaToken, setIsNajvaToken] = useState(false)
const [retryCount, setRetryCount] = useState(0) const [retryCount, setRetryCount] = useState(0)
const updatePushToken = useUpdatePushToken()
const handleNajvaToken = async () => { const handleNajvaToken = async () => {
try { try {
console.log('تلاش برای دریافت توکن Najva...', { retryCount })
// بررسی وجود window.NAJVA
if (!window.NAJVA) { if (!window.NAJVA) {
console.log('window.NAJVA موجود نیست')
return false return false
} }
console.log('window.NAJVA موجود است، در حال دریافت توکن...')
const userToken = await window.NAJVA.getUserToken() const userToken = await window.NAJVA.getUserToken()
console.log('توکن دریافت شده:', userToken ? 'موجود' : 'خالی')
if (userToken) { if (userToken) {
console.log('در حال ارسال توکن به سرور...') await updatePushToken.mutateAsync({
await updateProfile({
pushToken: userToken, pushToken: userToken,
}) })
setIsNajvaToken(true) setIsNajvaToken(true)
console.log('توکن با موفقیت ارسال شد')
return true return true
} else { } else {
console.log('توکن خالی است')
return false return false
} }
} catch (error) { } catch (error) {
console.error('خطا در دریافت توکن Najva:', error) console.log(error);
return false return false
} }
} }
@@ -53,10 +45,9 @@ const NajvaToken: FC = () => {
const success = await handleNajvaToken() const success = await handleNajvaToken()
if (!success && retryCount < 5) { if (!success && retryCount < 5) {
console.log(`تلاش مجدد در ${(retryCount + 1) * 1000}ms...`)
setTimeout(() => { setTimeout(() => {
setRetryCount(prev => prev + 1) setRetryCount(prev => prev + 1)
}, (retryCount + 1) * 1000) // 1s, 2s, 3s, 4s, 5s }, (retryCount + 1) * 1000)
} }
} }
+87 -24
View File
@@ -1,4 +1,4 @@
import React, { Fragment, useState } from 'react'; import React, { Fragment, useState, useRef } from 'react';
import DefaultTableSkeleton from './DefaultTableSkeleton'; import DefaultTableSkeleton from './DefaultTableSkeleton';
import Td from './Td'; import Td from './Td';
import { TableProps, RowDataType, ColumnType } from './types/TableTypes'; import { TableProps, RowDataType, ColumnType } from './types/TableTypes';
@@ -31,6 +31,9 @@ const Table = <T extends RowDataType>({
}: TableProps<T>): React.ReactElement => { }: TableProps<T>): React.ReactElement => {
const [internalSelectedRows, setInternalSelectedRows] = useState<T[]>([]); const [internalSelectedRows, setInternalSelectedRows] = useState<T[]>([]);
const [mobileSelectionMode, setMobileSelectionMode] = useState(false);
const longPressTimer = useRef<number | null>(null);
const [longPressActiveId, setLongPressActiveId] = useState<string | number | null>(null);
// استفاده از selectedRows خارجی اگر موجود باشد، در غیر این صورت از internal استفاده کن // استفاده از selectedRows خارجی اگر موجود باشد، در غیر این صورت از internal استفاده کن
const selectedRows = externalSelectedRows !== undefined ? externalSelectedRows : internalSelectedRows; const selectedRows = externalSelectedRows !== undefined ? externalSelectedRows : internalSelectedRows;
@@ -40,8 +43,59 @@ const Table = <T extends RowDataType>({
if (clearSelection && externalSelectedRows === undefined) { if (clearSelection && externalSelectedRows === undefined) {
setInternalSelectedRows([]); setInternalSelectedRows([]);
} }
// خروج از حالت انتخاب موبایل هم
if (clearSelection) {
setMobileSelectionMode(false);
}
}, [clearSelection, externalSelectedRows]); }, [clearSelection, externalSelectedRows]);
// تمیز کردن timer هنگام unmount
React.useEffect(() => {
return () => {
if (longPressTimer.current) {
clearTimeout(longPressTimer.current);
}
};
}, []);
// خروج از حالت انتخاب اگر هیچ آیتمی انتخاب نشده
React.useEffect(() => {
if (selectedRows.length === 0 && mobileSelectionMode) {
setMobileSelectionMode(false);
}
}, [selectedRows.length, mobileSelectionMode]);
const handleLongPressStart = (item: T) => {
if (!selectable || mobileSelectionMode) return;
setLongPressActiveId(item.id);
longPressTimer.current = window.setTimeout(() => {
setMobileSelectionMode(true);
// انتخاب آیتم فعلی
handleSingleRowSelect(item, true);
setLongPressActiveId(null);
}, 500); // 500ms برای long press
};
const handleLongPressEnd = () => {
if (longPressTimer.current) {
clearTimeout(longPressTimer.current);
longPressTimer.current = null;
}
setLongPressActiveId(null);
};
const handleMobileRowClick = (item: T) => {
if (mobileSelectionMode) {
// در حالت انتخاب، آیتم را انتخاب/عدم انتخاب کن
const isSelected = isRowSelected(item);
handleSingleRowSelect(item, !isSelected);
} else if (longPressActiveId !== item.id) {
// در حالت عادی، row click عادی را اجرا کن
handleRowClick(item);
}
};
const getRowClassName = (item: T, index: number): string => { const getRowClassName = (item: T, index: number): string => {
if (typeof rowClassName === 'function') { if (typeof rowClassName === 'function') {
return rowClassName(item, index); return rowClassName(item, index);
@@ -136,36 +190,24 @@ const Table = <T extends RowDataType>({
return ( return (
<div <div
key={item.id} key={item.id}
className={`w-full min-h-[85px] ${bgColor} ${hoverColor} ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''}`} className={`w-full min-h-[85px] ${bgColor} ${hoverColor} ${mobileSelectionMode || !onRowClick ? '' : 'cursor-pointer'} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''} ${longPressActiveId === item.id ? 'bg-blue-50' : ''}`}
onClick={() => handleRowClick(item)} onClick={() => handleMobileRowClick(item)}
onTouchStart={() => handleLongPressStart(item)}
onTouchEnd={handleLongPressEnd}
onTouchCancel={handleLongPressEnd}
onMouseDown={() => handleLongPressStart(item)}
onMouseUp={handleLongPressEnd}
onMouseLeave={handleLongPressEnd}
> >
{selectable && ( {selectable && mobileSelectionMode && (
<div <div
className="flex-shrink-0 " className="flex-shrink-0"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<Checkbox <Checkbox
checked={isRowSelected(item)} checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)} onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/> />
{/* {inlineActions && (
<div
className="flex-shrink-0 flex items-center gap-2 "
onClick={(e) => e.stopPropagation()}
>
{inlineActions(item).map((action, actionIndex) => (
<div
key={actionIndex}
className={`cursor-pointer ${action.className || ''}`}
onClick={action.onClick}
title={action.tooltip}
>
{action.icon}
</div>
))}
</div>
)} */}
</div> </div>
)} )}
@@ -341,11 +383,32 @@ const Table = <T extends RowDataType>({
{/* نسخه موبایل - Gmail style */} {/* نسخه موبایل - Gmail style */}
<div className={`md:hidden`}> <div className={`md:hidden`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && ( {(actionsPosition === 'top' || actionsPosition === 'above-header') && (
<div className="h-[64px] bg-white flex items-center px-4 rounded-t-2xl border-b border-[#EAEDF5]"> <div className="h-[64px] bg-white flex items-center justify-between px-4 rounded-t-2xl border-b border-[#EAEDF5]">
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
{actions} {actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions} {selectable && selectedRows.length > 0 && selectedActions && selectedActions}
</div> </div>
{mobileSelectionMode && (
<button
onClick={() => {
setMobileSelectionMode(false);
// پاک کردن انتخاب‌ها
if (externalSelectedRows !== undefined) {
if (onSelectionChange) {
onSelectionChange([]);
}
} else {
setInternalSelectedRows([]);
if (onSelectionChange) {
onSelectionChange([]);
}
}
}}
className="text-sm text-gray-600 px-3 py-1"
>
انصراف
</button>
)}
</div> </div>
)} )}
{renderGmailStyleRows()} {renderGmailStyleRows()}
@@ -44,3 +44,9 @@ export const useSingleUpload = () => {
mutationFn: (variables: FormData) => api.singleUpload(variables), mutationFn: (variables: FormData) => api.singleUpload(variables),
}); });
}; };
export const useUpdatePushToken = () => {
return useMutation({
mutationFn: (variables: UpdateProfileType) => api.updatePushToken(variables),
});
};
@@ -22,6 +22,11 @@ export const updateProfile = async (params: UpdateProfileType) => {
return data; return data;
}; };
export const updatePushToken = async (params: UpdateProfileType) => {
const { data } = await axios.post(`/users/push-token`, params);
return data;
};
export const updateEmail = async (params: UpdateEmailType) => { export const updateEmail = async (params: UpdateEmailType) => {
const { data } = await axios.patch(`/users/change-email`, params); const { data } = await axios.patch(`/users/change-email`, params);
return data; return data;
+19 -14
View File
@@ -3,7 +3,7 @@ import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Table from '../../components/Table'; import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes'; import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, Refresh2, Trash, ArchiveTick, Star1 } from 'iconsax-react'; import { InfoCircle, Refresh2, Trash, ArchiveTick, Star1, Sms } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown'; import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useGetInbox, useMarkAllRead, useSummarizeEmail } from './hooks/useEmailData'; import { useGetInbox, useMarkAllRead, useSummarizeEmail } from './hooks/useEmailData';
@@ -86,12 +86,6 @@ const List: FC = () => {
className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`} className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()} onClick={() => refetch()}
/> />
<Button
label='علامت‌گذاری همه به عنوان خوانده شده'
className='h-8'
onClick={() => setShowModal(true)}
loading={isMarkingAllRead}
/>
</div> </div>
); );
@@ -103,23 +97,21 @@ const List: FC = () => {
onClick={() => handleMarkAsReadSelected()} onClick={() => handleMarkAsReadSelected()}
/> />
<ArchiveTick <ArchiveTick
size={18}
color='black' color='black'
onClick={() => handleArchiveSelected()} onClick={() => handleArchiveSelected()}
className="cursor-pointer" className="cursor-pointer lg:size-[18px] size-[20px]"
/> />
<InfoCircle size={18} color='black' className="cursor-pointer" /> <InfoCircle color='black' className="cursor-pointer lg:size-[18px] size-[20px]" />
<Star1 <Star1
size={18}
color='black' color='black'
className="cursor-pointer" className="cursor-pointer lg:size-[18px] size-[20px]"
onClick={() => handleFavoriteSelected()} onClick={() => handleFavoriteSelected()}
/> />
<Trash <Trash
size={18}
color='#CD0000' color='#CD0000'
onClick={() => handleDeleteSelected()} onClick={() => handleDeleteSelected()}
className="cursor-pointer" className="cursor-pointer lg:size-[18px] size-[20px]"
/> />
</> </>
); );
@@ -236,6 +228,19 @@ const List: FC = () => {
searchField="search" searchField="search"
/> />
<div className='flex justify-end mt-5 lg:mt-10 '>
<Button
onClick={() => setShowModal(true)}
loading={isMarkingAllRead}
className='w-fit'
>
<div className='flex items-center gap-2'>
<Sms size={18} color='white' />
<span>خواندن همه</span>
</div>
</Button>
</div>
<Table<InboxMessage> <Table<InboxMessage>
columns={columns} columns={columns}
data={messages} data={messages}