courrier preview
This commit is contained in:
parent
9e5c2cb92e
commit
193a265109
@ -15,6 +15,8 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import RichTextEditor from '@/components/ui/rich-text-editor';
|
||||
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||
|
||||
// Import from the centralized utils
|
||||
import {
|
||||
@ -346,12 +348,13 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
||||
</div>
|
||||
|
||||
{/* Message Body */}
|
||||
<div
|
||||
<RichTextEditor
|
||||
ref={editorRef}
|
||||
className="min-h-[320px] outline-none p-2 border rounded-md bg-white text-gray-800 flex-1"
|
||||
contentEditable={true}
|
||||
dangerouslySetInnerHTML={{ __html: emailContent }}
|
||||
onInput={(e) => setEmailContent(e.currentTarget.innerHTML)}
|
||||
initialContent={emailContent}
|
||||
initialDirection={detectTextDirection(emailContent)}
|
||||
onChange={setEmailContent}
|
||||
className="min-h-[320px] border rounded-md bg-white text-gray-800 flex-1"
|
||||
placeholder="Write your message here..."
|
||||
/>
|
||||
|
||||
{/* Attachments */}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EmailContent } from '@/types/email';
|
||||
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
interface EmailContentDisplayProps {
|
||||
content: EmailContent | null | undefined;
|
||||
@ -13,7 +15,7 @@ interface EmailContentDisplayProps {
|
||||
|
||||
/**
|
||||
* Unified component for displaying email content in a consistent way
|
||||
* This handles both HTML and plain text content with proper styling
|
||||
* This handles both HTML and plain text content with proper styling and RTL support
|
||||
*/
|
||||
const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||
content,
|
||||
@ -22,42 +24,95 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||
type = 'auto',
|
||||
debug = false
|
||||
}) => {
|
||||
if (!content) {
|
||||
return <div className={className}>No content available</div>;
|
||||
}
|
||||
// Create a safe content object with fallback values for missing properties
|
||||
const safeContent = useMemo(() => {
|
||||
if (!content) {
|
||||
return {
|
||||
text: '',
|
||||
html: undefined,
|
||||
isHtml: false,
|
||||
direction: 'ltr' as const
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: content.text || '',
|
||||
html: content.html,
|
||||
isHtml: content.isHtml,
|
||||
// If direction is missing, detect it from the text content
|
||||
direction: content.direction || detectTextDirection(content.text)
|
||||
};
|
||||
}, [content]);
|
||||
|
||||
// Determine what content to display based on type preference and available content
|
||||
const htmlToDisplay = useMemo(() => {
|
||||
// If no content is available, show a placeholder
|
||||
if (!safeContent.text && !safeContent.html) {
|
||||
return '<div class="text-gray-400">No content available</div>';
|
||||
}
|
||||
|
||||
// If type is explicitly set to text, or we don't have HTML and auto mode
|
||||
if (type === 'text' || (type === 'auto' && !safeContent.isHtml)) {
|
||||
// Format plain text with line breaks for HTML display
|
||||
if (safeContent.text) {
|
||||
const formattedText = safeContent.text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise use HTML content if available
|
||||
if (safeContent.isHtml && safeContent.html) {
|
||||
return safeContent.html;
|
||||
}
|
||||
|
||||
// Fallback to text content if there's no HTML
|
||||
if (safeContent.text) {
|
||||
const formattedText = safeContent.text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
|
||||
return '<div class="text-gray-400">No content available</div>';
|
||||
}, [safeContent, type]);
|
||||
|
||||
let htmlContent = '';
|
||||
// Handle quoted text display
|
||||
const processedHTML = useMemo(() => {
|
||||
if (!showQuotedText) {
|
||||
// This is simplified - a more robust approach would parse and handle
|
||||
// quoted sections more intelligently
|
||||
return htmlToDisplay.replace(/<blockquote[^>]*>[\s\S]*?<\/blockquote>/gi,
|
||||
'<div class="text-gray-400">[Quoted text hidden]</div>');
|
||||
}
|
||||
return htmlToDisplay;
|
||||
}, [htmlToDisplay, showQuotedText]);
|
||||
|
||||
// Simple content rendering
|
||||
if (content.isHtml && content.html) {
|
||||
// Use HTML content
|
||||
htmlContent = content.html;
|
||||
} else if (content.text) {
|
||||
// Format text content with line breaks
|
||||
htmlContent = content.text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
} else {
|
||||
// No content available
|
||||
htmlContent = 'No content available';
|
||||
}
|
||||
// Sanitize HTML content before rendering
|
||||
const sanitizedHTML = useMemo(() => {
|
||||
return DOMPurify.sanitize(processedHTML);
|
||||
}, [processedHTML]);
|
||||
|
||||
return (
|
||||
<div className={`email-content-display ${className}`}>
|
||||
<div className={`email-content-display ${className}`} dir={safeContent.direction}>
|
||||
<div
|
||||
className="email-content-inner"
|
||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
|
||||
/>
|
||||
|
||||
{/* Debug output if enabled */}
|
||||
{debug && (
|
||||
<div className="mt-4 p-2 text-xs bg-gray-100 border rounded">
|
||||
<p><strong>Content Type:</strong> {content.isHtml ? 'HTML' : 'Text'}</p>
|
||||
<p><strong>Direction:</strong> {content.direction}</p>
|
||||
<p><strong>HTML Length:</strong> {content.html?.length || 0}</p>
|
||||
<p><strong>Text Length:</strong> {content.text?.length || 0}</p>
|
||||
<p><strong>Content Type:</strong> {safeContent.isHtml ? 'HTML' : 'Text'}</p>
|
||||
<p><strong>Direction:</strong> {safeContent.direction}</p>
|
||||
<p><strong>HTML Length:</strong> {safeContent.html?.length || 0}</p>
|
||||
<p><strong>Text Length:</strong> {safeContent.text?.length || 0}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -66,10 +121,30 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.email-content-display[dir="rtl"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.email-content-inner img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.email-content-inner blockquote {
|
||||
margin: 10px 0;
|
||||
padding-left: 15px;
|
||||
border-left: 2px solid #ddd;
|
||||
color: #666;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.email-content-display[dir="rtl"] .email-content-inner blockquote {
|
||||
padding-left: 0;
|
||||
padding-right: 15px;
|
||||
border-left: none;
|
||||
border-right: 2px solid #ddd;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
171
components/ui/rich-text-editor.tsx
Normal file
171
components/ui/rich-text-editor.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
interface RichTextEditorProps {
|
||||
/** Initial HTML content */
|
||||
initialContent?: string;
|
||||
|
||||
/** Callback when content changes */
|
||||
onChange?: (html: string) => void;
|
||||
|
||||
/** Additional CSS class names */
|
||||
className?: string;
|
||||
|
||||
/** Editor placeholder text */
|
||||
placeholder?: string;
|
||||
|
||||
/** Whether the editor is read-only */
|
||||
readOnly?: boolean;
|
||||
|
||||
/** Minimum height of the editor */
|
||||
minHeight?: string;
|
||||
|
||||
/** Initial text direction */
|
||||
initialDirection?: 'ltr' | 'rtl';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified rich text editor component with proper RTL support
|
||||
* Handles email composition with appropriate text direction detection
|
||||
*/
|
||||
const RichTextEditor = forwardRef<HTMLDivElement, RichTextEditorProps>(({
|
||||
initialContent = '',
|
||||
onChange,
|
||||
className = '',
|
||||
placeholder = 'Write your message...',
|
||||
readOnly = false,
|
||||
minHeight = '200px',
|
||||
initialDirection
|
||||
}, ref) => {
|
||||
const internalEditorRef = useRef<HTMLDivElement>(null);
|
||||
const [direction, setDirection] = useState<'ltr' | 'rtl'>(
|
||||
initialDirection || detectTextDirection(initialContent)
|
||||
);
|
||||
|
||||
// Forward the ref to parent components
|
||||
useImperativeHandle(ref, () => internalEditorRef.current as HTMLDivElement);
|
||||
|
||||
// Initialize editor with clean content
|
||||
useEffect(() => {
|
||||
if (internalEditorRef.current) {
|
||||
// Clean the initial content
|
||||
const cleanContent = DOMPurify.sanitize(initialContent);
|
||||
internalEditorRef.current.innerHTML = cleanContent;
|
||||
|
||||
// Set initial direction
|
||||
internalEditorRef.current.setAttribute('dir', direction);
|
||||
|
||||
// Focus editor if not read-only
|
||||
if (!readOnly) {
|
||||
setTimeout(() => {
|
||||
internalEditorRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [initialContent, direction, readOnly]);
|
||||
|
||||
// Handle content changes and detect direction changes
|
||||
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
|
||||
if (onChange && e.currentTarget.innerHTML !== initialContent) {
|
||||
onChange(e.currentTarget.innerHTML);
|
||||
}
|
||||
|
||||
// Re-detect direction on significant content changes
|
||||
// Only do this when the content length has changed significantly
|
||||
const newContent = e.currentTarget.innerText;
|
||||
if (newContent.length > 5 && newContent.length % 10 === 0) {
|
||||
const newDirection = detectTextDirection(newContent);
|
||||
if (newDirection !== direction) {
|
||||
setDirection(newDirection);
|
||||
e.currentTarget.setAttribute('dir', newDirection);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle direction manually
|
||||
const toggleDirection = () => {
|
||||
const newDirection = direction === 'ltr' ? 'rtl' : 'ltr';
|
||||
setDirection(newDirection);
|
||||
if (internalEditorRef.current) {
|
||||
internalEditorRef.current.setAttribute('dir', newDirection);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rich-text-editor-container">
|
||||
{!readOnly && (
|
||||
<div className="editor-toolbar border-b p-1 flex items-center space-x-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleDirection}
|
||||
className="px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded"
|
||||
title={`Switch to ${direction === 'ltr' ? 'right-to-left' : 'left-to-right'} text`}
|
||||
>
|
||||
{direction === 'ltr' ? 'LTR → RTL' : 'RTL → LTR'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={internalEditorRef}
|
||||
contentEditable={!readOnly}
|
||||
className={`rich-text-editor outline-none p-3 ${className}`}
|
||||
onInput={handleInput}
|
||||
dir={direction}
|
||||
style={{
|
||||
minHeight,
|
||||
cursor: readOnly ? 'default' : 'text',
|
||||
}}
|
||||
data-placeholder={placeholder}
|
||||
suppressContentEditableWarning
|
||||
/>
|
||||
|
||||
<style jsx>{`
|
||||
.rich-text-editor {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.rich-text-editor:empty:before {
|
||||
content: attr(data-placeholder);
|
||||
color: #aaa;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rich-text-editor[dir="rtl"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rich-text-editor blockquote {
|
||||
margin: 10px 0;
|
||||
padding-left: 15px;
|
||||
border-left: 2px solid #ddd;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.rich-text-editor[dir="rtl"] blockquote {
|
||||
padding-left: 0;
|
||||
padding-right: 15px;
|
||||
border-left: none;
|
||||
border-right: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.rich-text-editor img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
RichTextEditor.displayName = 'RichTextEditor';
|
||||
|
||||
export default RichTextEditor;
|
||||
@ -1,6 +1,8 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useToast } from './use-toast';
|
||||
import { EmailMessage, EmailContent } from '@/types/email';
|
||||
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||
import { sanitizeHtml } from '@/lib/utils/email-utils';
|
||||
|
||||
interface EmailFetchState {
|
||||
email: EmailMessage | null;
|
||||
@ -73,6 +75,50 @@ export function useEmailFetch({ onEmailLoaded, onError }: UseEmailFetchProps = {
|
||||
const data = await response.json();
|
||||
|
||||
// Create a valid email message object with required fields
|
||||
const processContent = (data: any): EmailContent => {
|
||||
// Determine the text content - using all possible paths
|
||||
let textContent = '';
|
||||
if (typeof data.content === 'string') {
|
||||
textContent = data.content;
|
||||
} else if (data.content?.text) {
|
||||
textContent = data.content.text;
|
||||
} else if (data.text) {
|
||||
textContent = data.text;
|
||||
} else if (data.plainText) {
|
||||
textContent = data.plainText;
|
||||
}
|
||||
|
||||
// Determine the HTML content - using all possible paths
|
||||
let htmlContent = undefined;
|
||||
if (data.content?.html) {
|
||||
htmlContent = data.content.html;
|
||||
} else if (data.html) {
|
||||
htmlContent = data.html;
|
||||
} else if (typeof data.content === 'string' && data.content.includes('<')) {
|
||||
// If the content string appears to be HTML
|
||||
htmlContent = data.content;
|
||||
// We should still keep the text version, will be extracted if needed
|
||||
}
|
||||
|
||||
// Clean HTML content if present
|
||||
if (htmlContent) {
|
||||
htmlContent = sanitizeHtml(htmlContent);
|
||||
}
|
||||
|
||||
// Determine if content is HTML
|
||||
const isHtml = !!htmlContent;
|
||||
|
||||
// Detect text direction
|
||||
const direction = data.content?.direction || detectTextDirection(textContent);
|
||||
|
||||
return {
|
||||
text: textContent,
|
||||
html: htmlContent,
|
||||
isHtml,
|
||||
direction
|
||||
};
|
||||
};
|
||||
|
||||
const transformedEmail: EmailMessage = {
|
||||
id: data.id || emailId,
|
||||
subject: data.subject || '',
|
||||
@ -82,14 +128,7 @@ export function useEmailFetch({ onEmailLoaded, onError }: UseEmailFetchProps = {
|
||||
bcc: data.bcc,
|
||||
date: data.date || new Date().toISOString(),
|
||||
flags: Array.isArray(data.flags) ? data.flags : [],
|
||||
content: {
|
||||
text: typeof data.content === 'string' ? data.content :
|
||||
data.content?.text || data.text || '',
|
||||
html: data.content?.html || data.html || undefined,
|
||||
isHtml: !!(data.content?.html || data.html ||
|
||||
(typeof data.content === 'string' && data.content.includes('<'))),
|
||||
direction: data.content?.direction || 'ltr'
|
||||
},
|
||||
content: processContent(data),
|
||||
attachments: data.attachments
|
||||
};
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
} from '@/types/email';
|
||||
import { adaptLegacyEmail } from '@/lib/utils/email-adapters';
|
||||
import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder';
|
||||
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||
|
||||
// Reset any existing hooks to start clean
|
||||
DOMPurify.removeAllHooks();
|
||||
@ -38,15 +39,6 @@ DOMPurify.setConfig({
|
||||
ALLOWED_ATTR: ['style', 'class', 'id', 'dir']
|
||||
});
|
||||
|
||||
/**
|
||||
* Detect if text contains RTL characters
|
||||
*/
|
||||
export function detectTextDirection(text: string): 'ltr' | 'rtl' {
|
||||
// Pattern for RTL characters (Arabic, Hebrew, etc.)
|
||||
const rtlLangPattern = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||
return rtlLangPattern.test(text) ? 'rtl' : 'ltr';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format email addresses for display
|
||||
* Can handle both array of EmailAddress objects or a string
|
||||
@ -224,10 +216,18 @@ export function renderEmailContent(content: EmailContent): string {
|
||||
return `<div class="email-content plain-text" dir="${content.direction || 'ltr'}">${formattedText}</div>`;
|
||||
}
|
||||
|
||||
// Add interface for email formatting functions
|
||||
interface FormattedEmail {
|
||||
to: string;
|
||||
cc?: string;
|
||||
subject: string;
|
||||
content: EmailContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format email for reply
|
||||
*/
|
||||
export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all' = 'reply') {
|
||||
export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessage | null, type: 'reply' | 'reply-all' = 'reply'): FormattedEmail {
|
||||
if (!originalEmail) {
|
||||
return {
|
||||
to: '',
|
||||
@ -244,7 +244,7 @@ export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all'
|
||||
|
||||
// Format the recipients
|
||||
const to = Array.isArray(originalEmail.from)
|
||||
? originalEmail.from.map(addr => {
|
||||
? originalEmail.from.map((addr: any) => {
|
||||
if (typeof addr === 'string') return addr;
|
||||
return addr.address ? addr.address : '';
|
||||
}).filter(Boolean).join(', ')
|
||||
@ -256,7 +256,7 @@ export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all'
|
||||
let cc = '';
|
||||
if (type === 'reply-all') {
|
||||
const toRecipients = Array.isArray(originalEmail.to)
|
||||
? originalEmail.to.map(addr => {
|
||||
? originalEmail.to.map((addr: any) => {
|
||||
if (typeof addr === 'string') return addr;
|
||||
return addr.address ? addr.address : '';
|
||||
}).filter(Boolean)
|
||||
@ -265,7 +265,7 @@ export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all'
|
||||
: [];
|
||||
|
||||
const ccRecipients = Array.isArray(originalEmail.cc)
|
||||
? originalEmail.cc.map(addr => {
|
||||
? originalEmail.cc.map((addr: any) => {
|
||||
if (typeof addr === 'string') return addr;
|
||||
return addr.address ? addr.address : '';
|
||||
}).filter(Boolean)
|
||||
@ -286,7 +286,7 @@ export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all'
|
||||
const dateStr = originalDate.toLocaleString();
|
||||
|
||||
const fromStr = Array.isArray(originalEmail.from)
|
||||
? originalEmail.from.map(addr => {
|
||||
? originalEmail.from.map((addr: any) => {
|
||||
if (typeof addr === 'string') return addr;
|
||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||
}).join(', ')
|
||||
@ -295,7 +295,7 @@ export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all'
|
||||
: 'Unknown Sender';
|
||||
|
||||
const toStr = Array.isArray(originalEmail.to)
|
||||
? originalEmail.to.map(addr => {
|
||||
? originalEmail.to.map((addr: any) => {
|
||||
if (typeof addr === 'string') return addr;
|
||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||
}).join(', ')
|
||||
@ -303,24 +303,40 @@ export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all'
|
||||
? originalEmail.to
|
||||
: '';
|
||||
|
||||
// Create HTML content
|
||||
// Extract original content
|
||||
const originalTextContent =
|
||||
originalEmail.content?.text ||
|
||||
(typeof originalEmail.content === 'string' ? originalEmail.content : '');
|
||||
|
||||
const originalHtmlContent =
|
||||
originalEmail.content?.html ||
|
||||
originalEmail.html ||
|
||||
(typeof originalEmail.content === 'string' && originalEmail.content.includes('<')
|
||||
? originalEmail.content
|
||||
: '');
|
||||
|
||||
// Get the direction from the original email
|
||||
const originalDirection =
|
||||
originalEmail.content?.direction ||
|
||||
(originalTextContent ? detectTextDirection(originalTextContent) : 'ltr');
|
||||
|
||||
// Create HTML content that preserves the directionality
|
||||
const htmlContent = `
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="email-original-content">
|
||||
<div class="email-original-content" dir="${originalDirection}">
|
||||
<blockquote style="border-left: 2px solid #ddd; padding-left: 10px; margin: 10px 0; color: #505050;">
|
||||
<p>On ${dateStr}, ${fromStr} wrote:</p>
|
||||
${originalEmail.content?.html || originalEmail.content?.text || ''}
|
||||
${originalHtmlContent || originalTextContent.replace(/\n/g, '<br>')}
|
||||
</blockquote>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create plain text content
|
||||
const plainText = originalEmail.content?.text || '';
|
||||
const textContent = `
|
||||
|
||||
On ${dateStr}, ${fromStr} wrote:
|
||||
> ${plainText.split('\n').join('\n> ')}
|
||||
> ${originalTextContent.split('\n').join('\n> ')}
|
||||
`;
|
||||
|
||||
return {
|
||||
@ -331,7 +347,7 @@ On ${dateStr}, ${fromStr} wrote:
|
||||
text: textContent,
|
||||
html: htmlContent,
|
||||
isHtml: true,
|
||||
direction: 'ltr' as const
|
||||
direction: 'ltr' as const // Reply is LTR, but original content keeps its direction in the blockquote
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -339,7 +355,7 @@ On ${dateStr}, ${fromStr} wrote:
|
||||
/**
|
||||
* Format email for forwarding
|
||||
*/
|
||||
export function formatForwardedEmail(originalEmail: any) {
|
||||
export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMessage | null): FormattedEmail {
|
||||
if (!originalEmail) {
|
||||
return {
|
||||
to: '',
|
||||
@ -360,7 +376,7 @@ export function formatForwardedEmail(originalEmail: any) {
|
||||
|
||||
// Format from, to, cc for the header
|
||||
const fromStr = Array.isArray(originalEmail.from)
|
||||
? originalEmail.from.map(addr => {
|
||||
? originalEmail.from.map((addr: any) => {
|
||||
if (typeof addr === 'string') return addr;
|
||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||
}).join(', ')
|
||||
@ -369,7 +385,7 @@ export function formatForwardedEmail(originalEmail: any) {
|
||||
: 'Unknown Sender';
|
||||
|
||||
const toStr = Array.isArray(originalEmail.to)
|
||||
? originalEmail.to.map(addr => {
|
||||
? originalEmail.to.map((addr: any) => {
|
||||
if (typeof addr === 'string') return addr;
|
||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||
}).join(', ')
|
||||
@ -378,7 +394,7 @@ export function formatForwardedEmail(originalEmail: any) {
|
||||
: '';
|
||||
|
||||
const ccStr = Array.isArray(originalEmail.cc)
|
||||
? originalEmail.cc.map(addr => {
|
||||
? originalEmail.cc.map((addr: any) => {
|
||||
if (typeof addr === 'string') return addr;
|
||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||
}).join(', ')
|
||||
@ -388,19 +404,36 @@ export function formatForwardedEmail(originalEmail: any) {
|
||||
|
||||
const dateStr = originalEmail.date ? new Date(originalEmail.date).toLocaleString() : 'Unknown Date';
|
||||
|
||||
// Create HTML content
|
||||
// Extract original content
|
||||
const originalTextContent =
|
||||
originalEmail.content?.text ||
|
||||
(typeof originalEmail.content === 'string' ? originalEmail.content : '');
|
||||
|
||||
const originalHtmlContent =
|
||||
originalEmail.content?.html ||
|
||||
originalEmail.html ||
|
||||
(typeof originalEmail.content === 'string' && originalEmail.content.includes('<')
|
||||
? originalEmail.content
|
||||
: '');
|
||||
|
||||
// Get the direction from the original email
|
||||
const originalDirection =
|
||||
originalEmail.content?.direction ||
|
||||
(originalTextContent ? detectTextDirection(originalTextContent) : 'ltr');
|
||||
|
||||
// Create HTML content that preserves the directionality
|
||||
const htmlContent = `
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="email-original-content">
|
||||
<div class="email-forwarded-content">
|
||||
<p>---------- Forwarded message ---------</p>
|
||||
<p><strong>From:</strong> ${fromStr}</p>
|
||||
<p><strong>Date:</strong> ${dateStr}</p>
|
||||
<p><strong>Subject:</strong> ${originalEmail.subject || ''}</p>
|
||||
<p><strong>To:</strong> ${toStr}</p>
|
||||
${ccStr ? `<p><strong>Cc:</strong> ${ccStr}</p>` : ''}
|
||||
<div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;">
|
||||
${originalEmail.content?.html || originalEmail.content?.text || ''}
|
||||
<div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;" dir="${originalDirection}">
|
||||
${originalHtmlContent || originalTextContent.replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -415,7 +448,7 @@ Subject: ${originalEmail.subject || ''}
|
||||
To: ${toStr}
|
||||
${ccStr ? `Cc: ${ccStr}\n` : ''}
|
||||
|
||||
${originalEmail.content?.text || ''}
|
||||
${originalTextContent}
|
||||
`;
|
||||
|
||||
return {
|
||||
@ -425,7 +458,7 @@ ${originalEmail.content?.text || ''}
|
||||
text: textContent,
|
||||
html: htmlContent,
|
||||
isHtml: true,
|
||||
direction: 'ltr' as const
|
||||
direction: 'ltr' as const // Forward is LTR, but original content keeps its direction
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
64
lib/utils/text-direction.ts
Normal file
64
lib/utils/text-direction.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Text Direction Utilities
|
||||
*
|
||||
* Centralized utilities for handling text direction (RTL/LTR)
|
||||
* to ensure consistent behavior across the application.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Detects if text contains RTL characters and should be displayed right-to-left
|
||||
* Uses a comprehensive regex pattern that covers Arabic, Hebrew, and other RTL scripts
|
||||
*
|
||||
* @param text Text to analyze for direction
|
||||
* @returns 'rtl' if RTL characters are detected, otherwise 'ltr'
|
||||
*/
|
||||
export function detectTextDirection(text: string | undefined | null): 'ltr' | 'rtl' {
|
||||
if (!text) return 'ltr';
|
||||
|
||||
// Comprehensive pattern for RTL languages:
|
||||
// - Arabic (0600-06FF, FB50-FDFF, FE70-FEFF)
|
||||
// - Hebrew (0590-05FF, FB1D-FB4F)
|
||||
// - RTL marks and controls (200F, 202B, 202E)
|
||||
const rtlPattern = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||
|
||||
return rtlPattern.test(text) ? 'rtl' : 'ltr';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds appropriate direction attribute to HTML content based on content analysis
|
||||
*
|
||||
* @param htmlContent HTML content to analyze and enhance with direction
|
||||
* @param textContent Plain text version for direction analysis (optional)
|
||||
* @returns HTML with appropriate direction attribute
|
||||
*/
|
||||
export function applyTextDirection(htmlContent: string, textContent?: string): string {
|
||||
if (!htmlContent) return '';
|
||||
|
||||
// If text content is provided, use it for direction detection
|
||||
// Otherwise extract text from HTML for direction detection
|
||||
const textForAnalysis = textContent ||
|
||||
htmlContent.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&');
|
||||
|
||||
const direction = detectTextDirection(textForAnalysis);
|
||||
|
||||
// If the HTML already has a dir attribute, don't override it
|
||||
if (htmlContent.includes('dir="rtl"') || htmlContent.includes('dir="ltr"')) {
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
// Check if we already have an email-content wrapper
|
||||
if (htmlContent.startsWith('<div class="email-content')) {
|
||||
// Replace opening div with one that includes direction
|
||||
return htmlContent.replace(
|
||||
/<div class="email-content([^"]*)"/,
|
||||
`<div class="email-content$1" dir="${direction}"`
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, wrap the content with a direction-aware container
|
||||
return `<div class="email-content" dir="${direction}">${htmlContent}</div>`;
|
||||
}
|
||||
@ -19,10 +19,24 @@ export interface EmailAttachment {
|
||||
contentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard email content structure used throughout the application
|
||||
* Ensures consistent handling of HTML/text content and text direction
|
||||
*/
|
||||
export interface EmailContent {
|
||||
/** Plain text version of the content (always required) */
|
||||
text: string;
|
||||
|
||||
/** HTML version of the content (optional) */
|
||||
html?: string;
|
||||
|
||||
/** Whether the primary display format should be HTML */
|
||||
isHtml: boolean;
|
||||
|
||||
/**
|
||||
* Text direction - 'rtl' for right-to-left languages (Arabic, Hebrew, etc.)
|
||||
* or 'ltr' for left-to-right languages (default)
|
||||
*/
|
||||
direction: 'ltr' | 'rtl';
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user