propmt input in mobile + print email + ltr and rtl new message + najva token

This commit is contained in:
hamid zarghami
2025-07-28 10:09:42 +03:30
parent e687f0cb5a
commit 49a4ed3ad9
19 changed files with 216 additions and 43 deletions
+2
View File
@@ -18,6 +18,7 @@ import { Paths } from './utils/Paths';
import Login from './pages/auth/Login'; import Login from './pages/auth/Login';
import { EmailWebSocketProvider } from './contexts/EmailWebSocketContext'; import { EmailWebSocketProvider } from './contexts/EmailWebSocketContext';
import SocketManagment from './lib/SocketManagment'; import SocketManagment from './lib/SocketManagment';
import NajvaToken from './components/NajvaToken';
declare global { declare global {
interface Window { interface Window {
@@ -135,6 +136,7 @@ const App: FC = () => {
<EmailWebSocketProvider token={userToken}> <EmailWebSocketProvider token={userToken}>
<Main /> <Main />
<SocketManagment /> <SocketManagment />
<NajvaToken />
</EmailWebSocketProvider> </EmailWebSocketProvider>
) : isLogin === 'isNotLogin' ? ( ) : isLogin === 'isNotLogin' ? (
<Login /> <Login />
+33
View File
@@ -0,0 +1,33 @@
import { FC, useEffect } from 'react'
import { updateProfile } from '../pages/profile/service/ProfileService'
declare global {
interface Window {
NAJVA: {
getUserToken: () => Promise<string>
najvaUserSubscribed: (najvaUserToken: string) => void
}
}
}
const NajvaToken: FC = () => {
const handleNajvaToken = async () => {
const userToken = await window.NAJVA?.getUserToken()
if (userToken) {
updateProfile({
pushToken: userToken,
})
}
}
useEffect(() => {
handleNajvaToken()
// window.NAJVA.najvaUserSubscribed = function (najvaUserToken) {
// console.log('najvaUserToken', najvaUserToken)
// }
}, [])
return null
}
export default NajvaToken
+71 -4
View File
@@ -1,5 +1,6 @@
import { FC } from 'react' import { FC, useMemo } from 'react'
import ReactQuill from 'react-quill-new' import ReactQuill from 'react-quill-new'
import 'react-quill-new/dist/quill.snow.css'
interface EmailEditorProps { interface EmailEditorProps {
value: string value: string
@@ -10,19 +11,85 @@ interface EmailEditorProps {
const EmailEditor: FC<EmailEditorProps> = ({ const EmailEditor: FC<EmailEditorProps> = ({
value, value,
onChange, onChange,
placeholder = '' placeholder = 'متن پیام خود را وارد کنید...'
}) => { }) => {
const modules = useMemo(() => ({
toolbar: [
['bold', 'italic', 'underline'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'align': [] }],
[{ 'direction': 'rtl' }],
['link'],
[{ 'color': [] }, { 'background': [] }],
[{ 'font': [] }, { 'size': ['small', false, 'large', 'huge'] }],
['clean']
],
}), [])
const formats = [
'bold', 'italic', 'underline',
'list', 'bullet',
'align', 'direction',
'link',
'color', 'background',
'font', 'size'
]
return ( return (
<div dir="rtl" className="text-right">
<style>{`
.ql-editor {
direction: rtl;
text-align: start;
unicode-bidi: plaintext;
font-family: 'Irancell', 'Vazirmatn', sans-serif;
font-size: 14px;
line-height: 1.6;
min-height: 120px;
}
.ql-editor p {
unicode-bidi: plaintext;
text-align: start;
direction: auto;
}
.ql-editor.ql-blank::before {
direction: rtl;
text-align: right;
font-style: normal;
color: #9ca3af;
}
.ql-toolbar {
border-top: 1px solid #e5e7eb;
border-left: 1px solid #e5e7eb;
border-right: 1px solid #e5e7eb;
border-radius: 8px 8px 0 0;
}
.ql-container {
border-bottom: 1px solid #e5e7eb;
border-left: 1px solid #e5e7eb;
border-right: 1px solid #e5e7eb;
border-radius: 0 0 8px 8px;
}
.ql-snow .ql-picker-label {
color: #374151;
}
.ql-snow .ql-stroke {
stroke: #6b7280;
}
.ql-snow .ql-fill {
fill: #6b7280;
}
`}</style>
<ReactQuill <ReactQuill
theme="snow" theme="snow"
value={value} value={value}
onChange={onChange} onChange={onChange}
placeholder={placeholder} placeholder={placeholder}
style={{ minHeight: '120px' }} modules={modules}
className='text-sm' formats={formats}
className="bg-white rounded-lg"
/> />
</div>
) )
} }
+1 -1
View File
@@ -146,7 +146,7 @@ const NewMessage: FC = () => {
</div> </div>
<div className='flex gap-2 items-end'> <div className='flex gap-2 items-end'>
<div className='flex flex-1 items-end gap-2'> <div className='flex lg:flex-row flex-col flex-1 items-end gap-2'>
<Input <Input
label={t('new_message.subject')} label={t('new_message.subject')}
value={subject} value={subject}
@@ -146,7 +146,9 @@ export const useNewMessage = () => {
const draftUpdateData = { const draftUpdateData = {
to: parseEmailAddresses(toEmails), to: parseEmailAddresses(toEmails),
subject, subject,
html: ensureString(content), html: `<div dir="auto" style="text-align: start; unicode-bidi: plaintext; font-family: 'Irancell', 'Vazirmatn', sans-serif;">${ensureString(
content
)}</div>`,
text: ensureString(content).replace(/<[^>]*>/g, ""), text: ensureString(content).replace(/<[^>]*>/g, ""),
...(attachments.length > 0 && { attachments }), ...(attachments.length > 0 && { attachments }),
}; };
@@ -170,6 +172,9 @@ export const useNewMessage = () => {
messageId: editingDraftId.toString(), messageId: editingDraftId.toString(),
draftData: { draftData: {
...draftUpdateData, ...draftUpdateData,
html: `<div dir="auto" style="text-align: start; unicode-bidi: plaintext; font-family: 'Irancell', 'Vazirmatn', sans-serif;">${ensureString(
content
)}</div>`,
isDraft: true, isDraft: true,
uploadOnly: true, uploadOnly: true,
}, },
@@ -195,7 +200,9 @@ export const useNewMessage = () => {
from: { address: userEmail || "sender@example.com" }, from: { address: userEmail || "sender@example.com" },
to: parseEmailAddresses(toEmails), to: parseEmailAddresses(toEmails),
subject, subject,
html: ensureString(content), html: `<div dir="auto" style="text-align: start; unicode-bidi: plaintext; font-family: 'Irancell', 'Vazirmatn', sans-serif;">${ensureString(
content
)}</div>`,
text: ensureString(content).replace(/<[^>]*>/g, ""), text: ensureString(content).replace(/<[^>]*>/g, ""),
...(attachments.length > 0 && { attachments }), ...(attachments.length > 0 && { attachments }),
...(isDraft && { isDraft: true, uploadOnly: true }), ...(isDraft && { isDraft: true, uploadOnly: true }),
+1 -1
View File
@@ -40,7 +40,7 @@ export const useEmailWebSocket = ({
}); });
newSocket.on("connection_established", (data) => { newSocket.on("connection_established", (data) => {
console.log("🎉 اتصال WebSocket ایمیل تأیید شد:", data); console.log("🎉 اتصال We bSocket ایمیل تأیید شد:", data);
}); });
newSocket.on("connection_error", (error) => { newSocket.on("connection_error", (error) => {
+7 -1
View File
@@ -238,7 +238,13 @@ textarea::placeholder {
@apply rounded-t-xl; @apply rounded-t-xl;
} }
.ql-editor { .ql-editor {
@apply !text-right; text-align: start !important;
unicode-bidi: plaintext !important;
}
.ql-editor p {
unicode-bidi: plaintext !important;
text-align: start !important;
direction: auto !important;
} }
.dltr { .dltr {
+1
View File
@@ -1,6 +1,7 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import './assets/fonts/irancell/style.css'
import App from './App.tsx' import App from './App.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
-2
View File
@@ -6,8 +6,6 @@ import { MessageListQueryDto, TrashResponse } from "../types/TrashTypes";
export const getTrashMessages = async ( export const getTrashMessages = async (
query: MessageListQueryDto query: MessageListQueryDto
): Promise<TrashResponse> => { ): Promise<TrashResponse> => {
console.log("query", query);
const isSearch = const isSearch =
!!query.search || !!query.datestart || query.dateend || query.from; !!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/trash"; const url = isSearch ? "search" : "messages/trash";
@@ -5,8 +5,6 @@ import { MessageListQueryDto, ArchiveResponse } from "../types/ArchiveTypes";
export const getArchiveMessages = async ( export const getArchiveMessages = async (
query: MessageListQueryDto query: MessageListQueryDto
): Promise<ArchiveResponse> => { ): Promise<ArchiveResponse> => {
console.log("query", query);
const isSearch = const isSearch =
!!query.search || !!query.datestart || query.dateend || query.from; !!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/archive"; const url = isSearch ? "search" : "messages/archive";
-2
View File
@@ -10,8 +10,6 @@ import {
export const getDrafts = async ( export const getDrafts = async (
query: MessageListQueryDto query: MessageListQueryDto
): Promise<InboxResponse> => { ): Promise<InboxResponse> => {
console.log("query", query);
const isSearch = const isSearch =
!!query.search || !!query.datestart || query.dateend || query.from; !!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/drafts"; const url = isSearch ? "search" : "messages/drafts";
@@ -5,8 +5,6 @@ import { MessageListQueryDto, FavoriteResponse } from "../types/FavoriteTypes";
export const getFavoriteMessages = async ( export const getFavoriteMessages = async (
query: MessageListQueryDto query: MessageListQueryDto
): Promise<FavoriteResponse> => { ): Promise<FavoriteResponse> => {
console.log("query", query);
const isSearch = const isSearch =
!!query.search || !!query.datestart || query.dateend || query.from; !!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/favorite"; const url = isSearch ? "search" : "messages/favorite";
+1
View File
@@ -12,6 +12,7 @@ export type UpdateProfileType = {
cityId?: string; cityId?: string;
postalCode?: string; postalCode?: string;
userAddress?: string; userAddress?: string;
pushToken?: string;
}; };
export type UpdateEmailType = { export type UpdateEmailType = {
+79 -1
View File
@@ -9,12 +9,89 @@ import MessageSpam from './MessageSpam'
import { MailboxEnum } from '../enum/Enum' import { MailboxEnum } from '../enum/Enum'
import MessageFavorite from './MessageFavorite' import MessageFavorite from './MessageFavorite'
import MessageTrash from './MessageTrash' import MessageTrash from './MessageTrash'
import { MessageDetail } from '../types/Types'
import { sanitizeEmailHTML, detectTextDirection } from '@/config/func'
const Header: FC<{ mailBoxName: MailboxEnum, flagged: boolean }> = ({ mailBoxName, flagged }) => { const Header: FC<{
mailBoxName: MailboxEnum,
flagged: boolean,
messageDetail?: MessageDetail
}> = ({ mailBoxName, flagged, messageDetail }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation() const { t } = useTranslation()
const handlePrint = () => {
if (!messageDetail) return
// Create a new window for printing
const printWindow = window.open('', '_blank', 'width=800,height=600')
if (!printWindow) return
// Get the email content
let content = ''
if (messageDetail.html && messageDetail.html.length > 0) {
content = sanitizeEmailHTML(messageDetail.html.join(''))
} else {
content = messageDetail.text || 'محتوای پیام'
}
// Detect text direction
const direction = detectTextDirection(content)
// Create print content
const printContent = `
<!DOCTYPE html>
<html dir="${direction}">
<head>
<meta charset="utf-8">
<title>پرینت ایمیل</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 20px;
direction: ${direction};
}
.email-header {
border-bottom: 2px solid #333;
padding-bottom: 10px;
margin-bottom: 20px;
}
.email-content {
font-size: 14px;
line-height: 1.7;
}
@media print {
body { margin: 0; }
.no-print { display: none; }
}
</style>
</head>
<body>
<div class="email-header">
<h2>موضوع: ${messageDetail.subject || 'بدون موضوع'}</h2>
<p><strong>فرستنده:</strong> ${messageDetail.from.name || messageDetail.from.address} &lt;${messageDetail.from.address}&gt;</p>
<p><strong>تاریخ:</strong> ${new Date(messageDetail.date).toLocaleDateString('fa-IR')}</p>
</div>
<div class="email-content">
${content}
</div>
</body>
</html>
`
// Write content and print
printWindow.document.write(printContent)
printWindow.document.close()
// Wait for content to load then print
printWindow.onload = () => {
printWindow.print()
printWindow.close()
}
}
return ( return (
<div className='flex justify-between items-center border-b border-border pb-6'> <div className='flex justify-between items-center border-b border-border pb-6'>
<div className='flex flex-1 lg:gap-4'> <div className='flex flex-1 lg:gap-4'>
@@ -42,6 +119,7 @@ const Header: FC<{ mailBoxName: MailboxEnum, flagged: boolean }> = ({ mailBoxNam
</Button> </Button>
<Button <Button
variant='secondary' variant='secondary'
onClick={handlePrint}
> >
<div className='flex gap-2 items-center xl:px-5'> <div className='flex gap-2 items-center xl:px-5'>
<Printer size={20} color='black' /> <Printer size={20} color='black' />
+5 -1
View File
@@ -141,7 +141,11 @@ const DetailEmail: FC = () => {
return ( return (
<div className='bg-white rounded-4xl p-8'> <div className='bg-white rounded-4xl p-8'>
<Header flagged={messageDetail?.flagged || false} mailBoxName={messageDetail?.mailboxName as MailboxEnum} /> <Header
flagged={messageDetail?.flagged || false}
mailBoxName={messageDetail?.mailboxName as MailboxEnum}
messageDetail={messageDetail}
/>
<div className='mt-6 flex xl:flex-row flex-col justify-between xl:items-center'> <div className='mt-6 flex xl:flex-row flex-col justify-between xl:items-center'>
<div className='flex xl:flex-row flex-col-reverse gap-4 xl:items-center'> <div className='flex xl:flex-row flex-col-reverse gap-4 xl:items-center'>
-12
View File
@@ -206,24 +206,12 @@ const List: FC = () => {
}; };
const messages = inboxData?.data?.results || []; const messages = inboxData?.data?.results || [];
const pager = inboxData?.data?.pager;
const rootData = inboxData?.data as unknown as Record<string, unknown>; const rootData = inboxData?.data as unknown as Record<string, unknown>;
// Debug pagination data
console.log('Full response data:', inboxData?.data);
console.log('Pager data:', pager);
console.log('Root nextCursor:', rootData?.nextCursor);
console.log('Root previousCursor:', rootData?.previousCursor);
// Get cursors from response
const responseNextCursor = (typeof rootData?.nextCursor === 'string' && rootData.nextCursor !== '') ? rootData.nextCursor as string : undefined; const responseNextCursor = (typeof rootData?.nextCursor === 'string' && rootData.nextCursor !== '') ? rootData.nextCursor as string : undefined;
const responsePreviousCursor = (typeof rootData?.previousCursor === 'string' && rootData.previousCursor !== '') ? rootData.previousCursor as string : undefined; const responsePreviousCursor = (typeof rootData?.previousCursor === 'string' && rootData.previousCursor !== '') ? rootData.previousCursor as string : undefined;
// Debug pagination values
console.log('responseNextCursor:', responseNextCursor);
console.log('responsePreviousCursor:', responsePreviousCursor);
console.log('Will render pagination?', !!(responseNextCursor || responsePreviousCursor));
return ( return (
<div className='mt-2 md:mt-4 px-2 md:px-0'> <div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg mb-4 md:mb-0">{t('received.title')}</h1> <h1 className="text-lg mb-4 md:mb-0">{t('received.title')}</h1>
@@ -36,8 +36,6 @@ export const getDraft = async (
export const getInbox = async ( export const getInbox = async (
query: MessageListQueryDto query: MessageListQueryDto
): Promise<InboxResponse> => { ): Promise<InboxResponse> => {
console.log("query", query);
const isSearch = const isSearch =
!!query.search || !!query.search ||
!!query.datestart || !!query.datestart ||
-2
View File
@@ -10,8 +10,6 @@ import {
export const getSentMessages = async ( export const getSentMessages = async (
query: MessageListQueryDto query: MessageListQueryDto
): Promise<InboxResponse> => { ): Promise<InboxResponse> => {
console.log("query", query);
const isSearch = const isSearch =
!!query.search || !!query.datestart || query.dateend || query.from; !!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/sent"; const url = isSearch ? "search" : "messages/sent";
-2
View File
@@ -6,8 +6,6 @@ import { MailboxEnum } from "@/pages/received/enum/Enum";
export const getSpamMessages = async ( export const getSpamMessages = async (
query: MessageListQueryDto query: MessageListQueryDto
): Promise<SpamResponse> => { ): Promise<SpamResponse> => {
console.log("query", query);
const isSearch = const isSearch =
!!query.search || !!query.datestart || query.dateend || query.from; !!query.search || !!query.datestart || query.dateend || query.from;
const url = isSearch ? "search" : "messages/junk"; const url = isSearch ? "search" : "messages/junk";