courrier preview

This commit is contained in:
alma 2025-05-01 15:29:38 +02:00
parent 61c29ced21
commit 2b02742bc4
7 changed files with 335 additions and 149 deletions

View File

@ -20,7 +20,7 @@ export default function BulkActionsToolbar({
onBulkAction
}: BulkActionsToolbarProps) {
return (
<div className="bg-blue-50 border-b border-blue-100 px-4 py-2 flex items-center justify-between">
<div className="bg-blue-50 border-b border-blue-100 px-4 py-2 flex items-center justify-between shadow-md transition-all duration-200">
<span className="text-xs font-medium text-blue-700">
{selectedCount} selected
</span>
@ -32,13 +32,13 @@ export default function BulkActionsToolbar({
variant="ghost"
size="icon"
className="h-7 w-7 text-blue-600 hover:text-blue-900 hover:bg-blue-100"
onClick={() => onBulkAction('mark-read')}
onClick={() => onBulkAction('mark-unread')}
>
<EyeOff className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Mark as read</p>
<p>Mark as unread</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -170,7 +170,9 @@ export default function ComposeEmail(props: ComposeEmailProps) {
} else {
console.log('Setting reply content:', {
length: content.length,
isHtml: formatted.content.isHtml
isHtml: formatted.content.isHtml,
startsWithHtml: content.trim().startsWith('<'),
contentType: typeof content
});
setEmailContent(content);
}
@ -198,7 +200,8 @@ export default function ComposeEmail(props: ComposeEmailProps) {
if (!content) {
console.warn('Forward content is empty, falling back to a basic template');
// Provide a basic template if the content is empty
const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedInfoForEmail(initialEmail);
const { fromStr, dateStr, subject: origSubject, toStr, ccStr } = getFormattedInfoForEmail(initialEmail);
console.log('Creating forward fallback with:', { fromStr, dateStr, origSubject });
const fallbackContent = `
<div style="margin: 20px 0 10px 0; color: #666; font-family: Arial, sans-serif;">
<div style="border-bottom: 1px solid #ccc; margin-bottom: 10px; padding-bottom: 5px;">
@ -215,7 +218,7 @@ export default function ComposeEmail(props: ComposeEmailProps) {
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
<td style="padding: 3px 0;">${subject || ''}</td>
<td style="padding: 3px 0;">${origSubject || ''}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">To:</td>

View File

@ -26,10 +26,19 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
// Process content if provided
const processedContent = useMemo(() => {
if (!content) {
console.log('EmailContentDisplay: No content provided');
return { __html: '<div class="email-content-empty">No content available</div>' };
}
try {
console.log('EmailContentDisplay processing:', {
contentType: typeof content,
isNull: content === null,
isString: typeof content === 'string',
isObject: typeof content === 'object',
length: typeof content === 'string' ? content.length : null
});
let formattedContent: string;
// If it's a string, we need to determine if it's HTML or plain text
@ -41,6 +50,7 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
formattedContent = formatEmailContent({ content });
}
console.log('EmailContentDisplay processed result length:', formattedContent.length);
return { __html: formattedContent };
} catch (error) {
console.error('Error processing email content:', error);

View File

@ -119,12 +119,14 @@ export default function EmailList({
return (
<div className="w-[320px] bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col">
{/* Only show bulk actions when emails are explicitly selected via checkboxes */}
{/* Sticky toolbar - always visible at the top when emails are selected */}
{selectedEmailIds.length > 0 && (
<BulkActionsToolbar
selectedCount={selectedEmailIds.length}
onBulkAction={onBulkAction}
/>
<div className="sticky top-0 z-10">
<BulkActionsToolbar
selectedCount={selectedEmailIds.length}
onBulkAction={onBulkAction}
/>
</div>
)}
{/* Search header */}

View File

@ -119,9 +119,13 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
quillRef.current.root.innerHTML = sanitizedContent;
// Set the direction for the content
quillRef.current.format('direction', direction);
if (direction === 'rtl') {
quillRef.current.format('align', 'right');
if (quillRef.current && quillRef.current.format) {
quillRef.current.format('direction', direction);
if (direction === 'rtl') {
quillRef.current.format('align', 'right');
}
} else {
console.warn('Cannot format content: editor not fully initialized');
}
}
@ -227,23 +231,38 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
const textContent = tempDiv.textContent || tempDiv.innerText || '';
// Create simple HTML with text content
quillRef.current.setText(textContent);
if (quillRef.current) {
quillRef.current.setText(textContent || 'No content available');
}
} else {
// SIMPLIFIED: Set content directly to the root element rather than using clipboard
quillRef.current.root.innerHTML = sanitizedContent;
// Set the direction for the content
quillRef.current.format('direction', direction);
if (direction === 'rtl') {
quillRef.current.format('align', 'right');
if (quillRef.current && quillRef.current.root) {
// First set the content
quillRef.current.root.innerHTML = sanitizedContent;
// Then safely apply formatting only if quillRef is valid
try {
if (quillRef.current && quillRef.current.format && quillRef.current.root.innerHTML.trim().length > 0) {
// Set the direction for the content
quillRef.current.format('direction', direction);
if (direction === 'rtl') {
quillRef.current.format('align', 'right');
}
// Force update
quillRef.current.update();
// Set selection to beginning
quillRef.current.setSelection(0, 0);
} else {
console.warn('Skipping format - either editor not ready or content empty');
}
} catch (formatError) {
console.error('Error applying formatting:', formatError);
// Continue without formatting if there's an error
}
}
}
// Force update
quillRef.current.update();
// Set selection to beginning
quillRef.current.setSelection(0, 0);
} catch (err) {
console.error('Error updating content:', err);
// Safer fallback that avoids clipboard API
@ -252,11 +271,16 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
const tempDiv = document.createElement('div');
tempDiv.innerHTML = initialContent;
const textContent = tempDiv.textContent || tempDiv.innerText || '';
quillRef.current.setText(textContent);
if (quillRef.current) {
quillRef.current.setText(textContent || 'Error loading content');
}
} catch (e) {
console.error('All fallbacks failed:', e);
// Last resort
quillRef.current.setText('Error loading content');
if (quillRef.current) {
quillRef.current.setText('Error loading content');
}
}
}
}

View File

@ -162,16 +162,30 @@ export function formatEmailContent(email: any): string {
// Extract content from email
const { text, html } = extractEmailContent(email);
console.log('formatEmailContent processing:', {
hasHtml: !!html,
htmlLength: html?.length || 0,
hasText: !!text,
textLength: text?.length || 0,
emailType: typeof email === 'string' ? 'string' : 'object'
});
// If we have HTML content, sanitize and standardize it
if (html) {
// Process HTML content
let processedHtml = processHtmlContent(html, text);
console.log('HTML content processed:', {
processedLength: processedHtml?.length || 0,
isEmpty: !processedHtml || processedHtml.trim().length === 0
});
// Apply styling
return `<div class="email-content" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-x: auto; overflow-wrap: break-word; word-wrap: break-word;" dir="${detectTextDirection(text)}">${processedHtml}</div>`;
}
// If we only have text content, format it properly
else if (text) {
console.log('Using plain text formatting');
return formatPlainTextToHtml(text);
}

View File

@ -252,162 +252,295 @@ function getFormattedHeaderInfo(email: any): {
* Format email for reply
*/
export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessage | null, type: 'reply' | 'reply-all' = 'reply'): FormattedEmail {
console.log('formatReplyEmail called:', { type, emailId: originalEmail?.id });
if (!originalEmail) {
console.warn('formatReplyEmail: No original email provided');
return {
to: '',
cc: '',
subject: '',
content: {
text: '',
html: '',
isHtml: false,
direction: 'ltr' as const
}
content: { text: '', html: '', isHtml: false, direction: 'ltr' }
};
}
// Extract recipient addresses
const { to, cc } = getRecipientAddresses(originalEmail, type);
// Get header information
const { fromStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail);
// Extract content using the centralized extraction function
const { text, html } = extractEmailContent(originalEmail);
// Create a clearer reply header with separator line
const replyHeader = `
<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
On ${dateStr}, ${fromStr} wrote:
</div>
`;
// Adapt legacy format if needed
const email = 'content' in originalEmail ? originalEmail : adaptLegacyEmail(originalEmail);
// Use the original HTML content if available, otherwise format the text
const contentHtml = html || (text ? `<p>${text.replace(/\n/g, '</p><p>')}</p>` : '<p>No content available</p>');
// Format subject with Re: prefix
const subject = email.subject ?
(email.subject.toLowerCase().startsWith('re:') ? email.subject : `Re: ${email.subject}`) :
'Re: ';
// Wrap the original content in proper styling without losing the HTML structure
const cleanHtml = `
${replyHeader}
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
${contentHtml}
</blockquote>
`;
// Get recipient addresses
const { to, cc } = getRecipientAddresses(email, type);
// Plain text version
const plainText = `
On ${dateStr}, ${fromStr} wrote:
-------------------------------------------------------------------
${text.split('\n').join('\n> ')}
`;
return {
to,
cc,
subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`,
content: {
text: plainText.trim(),
html: cleanHtml,
isHtml: true,
direction: 'ltr'
// Get email content and sanitize it
const originalContent = email.content;
// Extract text and html content
let htmlContent = '';
let textContent = '';
let direction: 'ltr' | 'rtl' = 'ltr';
// Handle different content formats
if (typeof originalContent === 'string') {
console.log('formatReplyEmail: content is string, length:', originalContent.length);
// Simple string content
textContent = originalContent;
const isHtml = isHtmlContent(originalContent);
if (isHtml) {
htmlContent = originalContent;
} else {
// If it's plain text, convert to HTML
htmlContent = formatPlainTextToHtml(originalContent);
}
}
else if (originalContent) {
console.log('formatReplyEmail: content is object:', {
hasHtml: !!originalContent.html,
htmlLength: originalContent.html?.length || 0,
hasText: !!originalContent.text,
textLength: originalContent.text?.length || 0,
direction: originalContent.direction
});
// Standard EmailContent object
htmlContent = originalContent.html || '';
textContent = originalContent.text || '';
direction = originalContent.direction || 'ltr' as const;
// If no HTML but has text, convert text to HTML
if (!htmlContent && textContent) {
htmlContent = formatPlainTextToHtml(textContent);
}
}
// Get quote header
const { fromStr, dateStr } = getFormattedHeaderInfo(email);
// Use the from name if available, otherwise use email address
const sender = fromStr;
const date = dateStr;
// Create the quoted reply content
if (htmlContent) {
// Format HTML reply
console.log('Formatting HTML reply, quoted content length:', htmlContent.length);
htmlContent = `
<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
On ${date}, ${sender} wrote:
</div>
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
${sanitizeHtml(htmlContent)}
</blockquote>
`;
}
if (textContent) {
// Format plain text reply
const lines = textContent.split(/\r\n|\r|\n/);
textContent = `On ${date}, ${sender} wrote:\n\n${lines.map(line => `> ${line}`).join('\n')}`;
}
const result = {
to,
cc: cc || undefined,
subject,
content: {
html: htmlContent,
text: textContent,
isHtml: true,
direction,
},
attachments: email.attachments?.map(att => {
// Create properly typed attachment
if ('name' in att) {
return {
filename: att.filename || att.name || 'attachment',
contentType: att.contentType || 'application/octet-stream',
content: att.content
};
}
return {
filename: att.filename || 'attachment',
contentType: att.contentType || 'application/octet-stream',
content: att.content
};
})
};
console.log('formatReplyEmail result:', {
to: result.to,
subject: result.subject,
hasHtml: !!result.content.html,
htmlLength: result.content.html?.length || 0,
hasText: !!result.content.text,
textLength: result.content.text?.length || 0
});
return result;
}
/**
* Format email for forwarding
*/
export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMessage | null): FormattedEmail {
console.log('formatForwardedEmail called:', { emailId: originalEmail?.id });
if (!originalEmail) {
console.warn('formatForwardedEmail: No original email provided');
return {
to: '',
subject: '',
content: {
text: '',
html: '',
isHtml: false,
direction: 'ltr' as const
}
content: { text: '', html: '', isHtml: false, direction: 'ltr' }
};
}
// Adapt legacy format if needed
const email = 'content' in originalEmail ? originalEmail : adaptLegacyEmail(originalEmail);
// Format subject with Fwd: prefix
const subject = email.subject ?
(email.subject.toLowerCase().startsWith('fwd:') ? email.subject : `Fwd: ${email.subject}`) :
'Fwd: ';
// Get original email info for headers
const { fromStr, toStr, ccStr, dateStr } = getFormattedHeaderInfo(email);
console.log('Forward header info:', { fromStr, toStr, dateStr, subject });
// Original sent date
const date = dateStr;
// Get email content
const originalContent = email.content;
// Extract text and html content
let htmlContent = '';
let textContent = '';
let direction: 'ltr' | 'rtl' = 'ltr';
// Handle different content formats
if (typeof originalContent === 'string') {
console.log('formatForwardedEmail: content is string, length:', originalContent.length);
// Simple string content
textContent = originalContent;
const isHtml = isHtmlContent(originalContent);
if (isHtml) {
htmlContent = originalContent;
} else {
// If it's plain text, convert to HTML
htmlContent = formatPlainTextToHtml(originalContent);
}
}
else if (originalContent) {
console.log('formatForwardedEmail: content is object:', {
hasHtml: !!originalContent.html,
htmlLength: originalContent.html?.length || 0,
hasText: !!originalContent.text,
textLength: originalContent.text?.length || 0,
direction: originalContent.direction
});
// Standard EmailContent object
htmlContent = originalContent.html || '';
textContent = originalContent.text || '';
direction = originalContent.direction || 'ltr' as const;
// If no HTML but has text, convert text to HTML
if (!htmlContent && textContent) {
htmlContent = formatPlainTextToHtml(textContent);
}
}
// Get header information
const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail);
// Extract content using the centralized extraction function
const { text, html } = extractEmailContent(originalEmail);
// Create a traditional forward format with dashed separator
const forwardHeader = `
<div style="margin: 20px 0 10px 0; color: #666; font-family: Arial, sans-serif;">
<div style="border-bottom: 1px solid #ccc; margin-bottom: 10px; padding-bottom: 5px;">
<div>---------------------------- Forwarded Message ----------------------------</div>
// Create the forwarded email HTML content
if (htmlContent) {
console.log('Formatting HTML forward, original content length:', htmlContent.length);
htmlContent = `
<div style="margin: 20px 0 10px 0; color: #666; font-family: Arial, sans-serif;">
---------- Forwarded message ----------<br>
<table style="margin: 10px 0 15px 0; border-collapse: collapse; font-size: 13px; color: #333;">
<tbody>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">From:</td>
<td style="padding: 3px 0;">${fromStr}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Date:</td>
<td style="padding: 3px 0;">${date}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
<td style="padding: 3px 0;">${email.subject || ''}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">To:</td>
<td style="padding: 3px 0;">${toStr}</td>
</tr>
${ccStr ? `
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Cc:</td>
<td style="padding: 3px 0;">${ccStr}</td>
</tr>
` : ''}
</tbody>
</table>
</div>
<table style="margin-bottom: 10px; font-size: 14px;">
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">From:</td>
<td style="padding: 3px 0;">${fromStr}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Date:</td>
<td style="padding: 3px 0;">${dateStr}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
<td style="padding: 3px 0;">${subject || ''}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">To:</td>
<td style="padding: 3px 0;">${toStr}</td>
</tr>
${ccStr ? `
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Cc:</td>
<td style="padding: 3px 0;">${ccStr}</td>
</tr>` : ''}
</table>
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
<div>----------------------------------------------------------------------</div>
<div style="padding: 10px 0; border-top: 1px solid #ddd;">
${sanitizeHtml(htmlContent)}
</div>
</div>
`;
`;
}
// Use the original HTML content if available, otherwise format the text
const contentHtml = html || (text ? `<p>${text.replace(/\n/g, '</p><p>')}</p>` : '<p>No content available</p>');
const cleanHtml = `${forwardHeader}${contentHtml}`;
// Plain text version - with clearer formatting
const plainText = `
---------------------------- Forwarded Message ----------------------------
// Format the plain text version
if (textContent) {
textContent = `
---------- Forwarded message ----------
From: ${fromStr}
Date: ${dateStr}
Subject: ${subject || ''}
Date: ${date}
Subject: ${email.subject || ''}
To: ${toStr}
${ccStr ? `Cc: ${ccStr}` : ''}
----------------------------------------------------------------------
${ccStr ? `Cc: ${ccStr}\n` : ''}
${text}
`;
// Check if original has attachments
const attachments = originalEmail.attachments || [];
return {
${textContent}
`.trim();
}
const result = {
to: '',
subject: subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`,
subject,
content: {
text: plainText.trim(),
html: cleanHtml,
html: htmlContent,
text: textContent,
isHtml: true,
direction: 'ltr'
direction,
},
// Only include attachments if they exist
attachments: attachments.length > 0 ? attachments.map(att => ({
filename: att.filename || 'attachment',
contentType: att.contentType || 'application/octet-stream',
content: att.content
})) : undefined
attachments: email.attachments?.map(att => {
// Create properly typed attachment
if ('name' in att) {
return {
filename: att.filename || att.name || 'attachment',
contentType: att.contentType || 'application/octet-stream',
content: att.content
};
}
return {
filename: att.filename || 'attachment',
contentType: att.contentType || 'application/octet-stream',
content: att.content
};
})
};
console.log('formatForwardedEmail result:', {
subject: result.subject,
hasHtml: !!result.content.html,
htmlLength: result.content.html?.length || 0,
hasText: !!result.content.text,
textLength: result.content.text?.length || 0
});
return result;
}
/**