Neah/components/email/ComposeEmail.tsx
2025-04-27 09:56:52 +02:00

415 lines
11 KiB
TypeScript

'use client';
import { useState, useRef, useEffect } from 'react';
import {
X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import DOMPurify from 'isomorphic-dompurify';
import { Label } from '@/components/ui/label';
// Import sub-components
import ComposeEmailHeader from './ComposeEmailHeader';
import ComposeEmailForm from './ComposeEmailForm';
import ComposeEmailFooter from './ComposeEmailFooter';
// Import ONLY from the centralized formatter
import {
formatForwardedEmail,
formatReplyEmail,
formatEmailForReplyOrForward,
EmailMessage as FormatterEmailMessage,
sanitizeHtml
} from '@/lib/utils/email-formatter';
/**
* CENTRAL EMAIL COMPOSER COMPONENT
*
* This is the unified, centralized email composer component used throughout the application.
* It handles new emails, replies, and forwards with proper text direction.
*
* All code that needs to compose emails should import this component from:
* @/components/email/ComposeEmail
*
* It uses the centralized email formatter from @/lib/utils/email-formatter.ts
* for consistent handling of email content and text direction.
*/
// Define EmailMessage interface locally instead of importing from server-only file
interface EmailAddress {
name: string;
address: string;
}
interface EmailMessage {
id: string;
messageId?: string;
subject: string;
from: EmailAddress[];
to: EmailAddress[];
cc?: EmailAddress[];
bcc?: EmailAddress[];
date: Date | string;
flags?: {
seen: boolean;
flagged: boolean;
answered: boolean;
deleted: boolean;
draft: boolean;
};
preview?: string;
content?: string;
html?: string;
text?: string;
hasAttachments?: boolean;
attachments?: any[];
folder?: string;
size?: number;
contentFetched?: boolean;
}
// Legacy interface for backward compatibility with old ComposeEmail component
interface LegacyComposeEmailProps {
showCompose: boolean;
setShowCompose: (show: boolean) => void;
composeTo: string;
setComposeTo: (to: string) => void;
composeCc: string;
setComposeCc: (cc: string) => void;
composeBcc: string;
setComposeBcc: (bcc: string) => void;
composeSubject: string;
setComposeSubject: (subject: string) => void;
composeBody: string;
setComposeBody: (body: string) => void;
showCc: boolean;
setShowCc: (show: boolean) => void;
showBcc: boolean;
setShowBcc: (show: boolean) => void;
attachments: any[];
setAttachments: (attachments: any[]) => void;
handleSend: () => Promise<void>;
originalEmail?: {
content: string;
type: 'reply' | 'reply-all' | 'forward';
};
onSend: (email: any) => Promise<void>;
onCancel: () => void;
replyTo?: any | null;
forwardFrom?: any | null;
}
// New interface for the modern ComposeEmail component
interface ComposeEmailProps {
initialEmail?: EmailMessage | null;
type?: 'new' | 'reply' | 'reply-all' | 'forward';
onClose: () => void;
onSend: (emailData: {
to: string;
cc?: string;
bcc?: string;
subject: string;
body: string;
attachments?: Array<{
name: string;
content: string;
type: string;
}>;
}) => Promise<void>;
}
// Union type to handle both new and legacy props
type ComposeEmailAllProps = ComposeEmailProps | LegacyComposeEmailProps;
// Type guard to check if props are legacy
function isLegacyProps(props: ComposeEmailAllProps): props is LegacyComposeEmailProps {
return 'showCompose' in props && 'setShowCompose' in props;
}
export default function ComposeEmail(props: ComposeEmailAllProps) {
// Handle legacy props by adapting them to new component
if (isLegacyProps(props)) {
return <LegacyAdapter {...props} />;
}
// Continue with modern implementation for new props
const { initialEmail, type = 'new', onClose, onSend } = props;
// Email form state
const [to, setTo] = useState<string>('');
const [cc, setCc] = useState<string>('');
const [bcc, setBcc] = useState<string>('');
const [subject, setSubject] = useState<string>('');
const [emailContent, setEmailContent] = useState<string>('');
const [showCc, setShowCc] = useState<boolean>(false);
const [showBcc, setShowBcc] = useState<boolean>(false);
const [sending, setSending] = useState<boolean>(false);
const [attachments, setAttachments] = useState<Array<{
name: string;
content: string;
type: string;
}>>([]);
// Initialize the form when replying to or forwarding an email
useEffect(() => {
if (initialEmail && type !== 'new') {
try {
const formatterEmail: FormatterEmailMessage = {
id: initialEmail.id,
messageId: initialEmail.messageId,
subject: initialEmail.subject,
from: initialEmail.from || [],
to: initialEmail.to || [],
cc: initialEmail.cc || [],
bcc: initialEmail.bcc || [],
date: initialEmail.date,
content: initialEmail.content,
html: initialEmail.html,
text: initialEmail.text,
hasAttachments: initialEmail.hasAttachments || false
};
if (type === 'forward') {
// For forwarding, use the dedicated formatter
const { subject, content } = formatForwardedEmail(formatterEmail);
setSubject(subject);
setEmailContent(content);
} else {
// For reply/reply-all, use the reply formatter
const { to, cc, subject, content } = formatReplyEmail(formatterEmail, type as 'reply' | 'reply-all');
setTo(to);
if (cc) {
setCc(cc);
setShowCc(true);
}
setSubject(subject);
setEmailContent(content);
}
} catch (err) {
console.error('Error formatting email for reply/forward:', err);
}
}
}, [initialEmail, type]);
// Handle file attachments
const handleAttachmentAdd = async (files: FileList) => {
const newAttachments = Array.from(files).map(file => ({
name: file.name,
type: file.type,
content: URL.createObjectURL(file)
}));
setAttachments(prev => [...prev, ...newAttachments]);
};
const handleAttachmentRemove = (index: number) => {
setAttachments(prev => prev.filter((_, i) => i !== index));
};
// Handle sending email
const handleSend = async () => {
if (!to) {
alert('Please specify at least one recipient');
return;
}
setSending(true);
try {
await onSend({
to,
cc: cc || undefined,
bcc: bcc || undefined,
subject,
body: emailContent,
attachments
});
// Reset form and close
onClose();
} catch (error) {
console.error('Error sending email:', error);
alert('Failed to send email. Please try again.');
} finally {
setSending(false);
}
};
return (
<Card className="w-full max-w-3xl mx-auto flex flex-col min-h-[60vh] max-h-[80vh]">
<ComposeEmailHeader
type={type}
onClose={onClose}
/>
<div className="flex-1 overflow-y-auto">
<ComposeEmailForm
to={to}
setTo={setTo}
cc={cc}
setCc={setCc}
bcc={bcc}
setBcc={setBcc}
subject={subject}
setSubject={setSubject}
emailContent={emailContent}
setEmailContent={setEmailContent}
showCc={showCc}
setShowCc={setShowCc}
showBcc={showBcc}
setShowBcc={setShowBcc}
attachments={attachments}
onAttachmentAdd={handleAttachmentAdd}
onAttachmentRemove={handleAttachmentRemove}
/>
</div>
<ComposeEmailFooter
sending={sending}
onSend={handleSend}
onCancel={onClose}
/>
</Card>
);
}
// Legacy adapter to maintain backward compatibility
function LegacyAdapter({
showCompose,
setShowCompose,
composeTo,
setComposeTo,
composeCc,
setComposeCc,
composeBcc,
setComposeBcc,
composeSubject,
setComposeSubject,
composeBody,
setComposeBody,
showCc,
setShowCc,
showBcc,
setShowBcc,
attachments,
setAttachments,
handleSend,
originalEmail,
onSend,
onCancel,
replyTo,
forwardFrom
}: LegacyComposeEmailProps) {
const [sending, setSending] = useState(false);
// Determine the type based on legacy props
const determineType = (): 'new' | 'reply' | 'reply-all' | 'forward' => {
if (originalEmail?.type === 'forward') return 'forward';
if (originalEmail?.type === 'reply-all') return 'reply-all';
if (originalEmail?.type === 'reply') return 'reply';
if (replyTo) return 'reply';
if (forwardFrom) return 'forward';
return 'new';
};
// Converts attachments to the expected format
const convertAttachments = () => {
return attachments.map(att => ({
name: att.name || att.filename || 'attachment',
content: att.content || '',
type: att.type || att.contentType || 'application/octet-stream'
}));
};
// Handle sending in the legacy format
const handleLegacySend = async () => {
setSending(true);
try {
if (onSend) {
// New API
await onSend({
to: composeTo,
cc: composeCc,
bcc: composeBcc,
subject: composeSubject,
body: composeBody,
attachments: convertAttachments()
});
} else if (handleSend) {
// Old API
await handleSend();
}
// Close compose window
setShowCompose(false);
} catch (error) {
console.error('Error sending email:', error);
alert('Failed to send email. Please try again.');
} finally {
setSending(false);
}
};
// Handle file selection for legacy interface
const handleFileSelection = (files: FileList) => {
const newAttachments = Array.from(files).map(file => ({
name: file.name,
type: file.type,
content: URL.createObjectURL(file),
size: file.size
}));
setAttachments([...attachments, ...newAttachments]);
};
if (!showCompose) return null;
return (
<Card className="w-full max-w-3xl mx-auto flex flex-col min-h-[60vh] max-h-[80vh]">
<ComposeEmailHeader
type={determineType()}
onClose={() => {
if (onCancel) onCancel();
setShowCompose(false);
}}
/>
<div className="flex-1 overflow-y-auto">
<ComposeEmailForm
to={composeTo}
setTo={setComposeTo}
cc={composeCc}
setCc={setComposeCc}
bcc={composeBcc}
setBcc={setComposeBcc}
subject={composeSubject}
setSubject={setComposeSubject}
emailContent={composeBody}
setEmailContent={setComposeBody}
showCc={showCc}
setShowCc={setShowCc}
showBcc={showBcc}
setShowBcc={setShowBcc}
attachments={convertAttachments()}
onAttachmentAdd={handleFileSelection}
onAttachmentRemove={(index) => {
setAttachments(attachments.filter((_, i) => i !== index));
}}
/>
</div>
<ComposeEmailFooter
sending={sending}
onSend={handleLegacySend}
onCancel={() => {
if (onCancel) onCancel();
setShowCompose(false);
}}
/>
</Card>
);
}