courrier refactor rebuild 2
This commit is contained in:
parent
46d8220466
commit
de728b9139
@ -98,53 +98,23 @@ export default function CourrierPage() {
|
|||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||||
const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(true);
|
const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(true);
|
||||||
const [foldersOpen, setFoldersOpen] = useState(true);
|
|
||||||
const [currentView, setCurrentView] = useState('INBOX');
|
const [currentView, setCurrentView] = useState('INBOX');
|
||||||
const [unreadCount, setUnreadCount] = useState(0);
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [userEmail, setUserEmail] = useState('');
|
|
||||||
|
|
||||||
// Mock accounts for the sidebar
|
// Mock accounts for the sidebar
|
||||||
const [accounts, setAccounts] = useState<Account[]>([
|
const [accounts, setAccounts] = useState<Account[]>([
|
||||||
{ id: 1, name: 'Mail', email: userEmail || 'Loading...', color: 'bg-blue-500', folders: mailboxes }
|
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' },
|
||||||
|
{ id: 1, name: 'Mail', email: 'user@example.com', color: 'bg-blue-500', folders: mailboxes }
|
||||||
]);
|
]);
|
||||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
||||||
|
|
||||||
// Fetch user email from credentials when component mounts
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchUserEmail() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/courrier/credentials');
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.credentials?.email) {
|
|
||||||
setUserEmail(data.credentials.email);
|
|
||||||
|
|
||||||
// Update account with the email address
|
|
||||||
setAccounts(prev => {
|
|
||||||
const updated = [...prev];
|
|
||||||
if (updated[0]) {
|
|
||||||
updated[0].email = data.credentials.email;
|
|
||||||
updated[0].name = data.credentials.email;
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching user email:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchUserEmail();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Update account folders when mailboxes change
|
// Update account folders when mailboxes change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAccounts(prev => {
|
setAccounts(prev => {
|
||||||
const updated = [...prev];
|
const updated = [...prev];
|
||||||
if (updated[0]) {
|
if (updated[1]) {
|
||||||
updated[0].folders = mailboxes;
|
updated[1].folders = mailboxes;
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
@ -341,68 +311,56 @@ export default function CourrierPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full justify-between px-2 py-1.5 text-sm group"
|
className="w-full justify-between px-2 py-1.5 text-sm group"
|
||||||
onClick={() => {
|
onClick={() => setSelectedAccount(account)}
|
||||||
setSelectedAccount(account);
|
|
||||||
setFoldersOpen(!foldersOpen);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 truncate">
|
<div className="flex items-center gap-2">
|
||||||
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
|
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
|
||||||
<span className="font-medium text-gray-700 truncate">{account.email}</span>
|
<span className="font-medium text-gray-700">{account.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Arrow to indicate toggle */}
|
|
||||||
{account.folders && account.folders.length > 0 && (
|
|
||||||
<div className="ml-1 flex-shrink-0">
|
|
||||||
{foldersOpen ?
|
|
||||||
<ChevronUp className="h-3.5 w-3.5 text-gray-400" /> :
|
|
||||||
<ChevronDown className="h-3.5 w-3.5 text-gray-400" />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Show folders for email accounts without the "Folders" header */}
|
{/* Show folders for email accounts (not for "All" account) without the "Folders" header */}
|
||||||
{account.folders && account.folders.length > 0 && foldersOpen && (
|
{account.id !== 0 && (
|
||||||
<div className="pl-4 mt-1 mb-2 space-y-0.5 border-l border-gray-200">
|
<div className="pl-4 mt-1 mb-2 space-y-0.5 border-l border-gray-200">
|
||||||
{account.folders.map((folder) => (
|
{account.folders && account.folders.length > 0 ? (
|
||||||
<Button
|
account.folders.map((folder) => (
|
||||||
key={folder}
|
<Button
|
||||||
variant="ghost"
|
key={folder}
|
||||||
className={`w-full justify-start py-1 px-2 text-xs ${
|
variant="ghost"
|
||||||
currentView === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
|
className={`w-full justify-start py-1 px-2 text-xs ${
|
||||||
}`}
|
currentView === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
|
||||||
onClick={(e) => {
|
}`}
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
handleMailboxChange(folder);
|
e.stopPropagation();
|
||||||
}}
|
handleMailboxChange(folder);
|
||||||
>
|
}}
|
||||||
<div className="flex items-center justify-between w-full gap-1.5">
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center justify-between w-full gap-1.5">
|
||||||
{React.createElement(getFolderIcon(folder), { className: "h-3.5 w-3.5" })}
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="truncate">{folder}</span>
|
{React.createElement(getFolderIcon(folder), { className: "h-3.5 w-3.5" })}
|
||||||
|
<span className="truncate">{folder}</span>
|
||||||
|
</div>
|
||||||
|
{folder === 'INBOX' && unreadCount > 0 && (
|
||||||
|
<span className="ml-auto bg-blue-600 text-white text-xs px-1.5 py-0.5 rounded-full text-[10px]">
|
||||||
|
{unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{folder === 'INBOX' && unreadCount > 0 && (
|
</Button>
|
||||||
<span className="ml-auto bg-blue-600 text-white text-xs px-1.5 py-0.5 rounded-full text-[10px]">
|
))
|
||||||
{unreadCount}
|
) : (
|
||||||
</span>
|
<div className="px-2 py-2">
|
||||||
)}
|
<div className="flex flex-col space-y-2">
|
||||||
|
{/* Create placeholder folder items with shimmer effect */}
|
||||||
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-1.5 animate-pulse">
|
||||||
|
<div className="h-3.5 w-3.5 bg-gray-200 rounded-sm"></div>
|
||||||
|
<div className="h-3 w-24 bg-gray-200 rounded"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{account.folders && account.folders.length === 0 && (
|
|
||||||
<div className="px-2 py-2">
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
{/* Create placeholder folder items with shimmer effect */}
|
|
||||||
{Array.from({ length: 5 }).map((_, index) => (
|
|
||||||
<div key={index} className="flex items-center gap-1.5 animate-pulse">
|
|
||||||
<div className="h-3.5 w-3.5 bg-gray-200 rounded-sm"></div>
|
|
||||||
<div className="h-3 w-24 bg-gray-200 rounded"></div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
// Import sub-components
|
// Import sub-components
|
||||||
import ComposeEmailHeader from './ComposeEmailHeader';
|
import ComposeEmailHeader from './ComposeEmailHeader';
|
||||||
@ -109,18 +108,26 @@ function EmailMessageToQuotedContentAdapter({
|
|||||||
email: EmailMessage,
|
email: EmailMessage,
|
||||||
type: 'reply' | 'reply-all' | 'forward'
|
type: 'reply' | 'reply-all' | 'forward'
|
||||||
}) {
|
}) {
|
||||||
if (!email) return null;
|
// Get the email content
|
||||||
|
const content = email.content || email.html || email.text || '';
|
||||||
|
|
||||||
|
// Get the sender
|
||||||
|
const sender = email.from && email.from.length > 0
|
||||||
|
? {
|
||||||
|
name: email.from[0].name,
|
||||||
|
email: email.from[0].address
|
||||||
|
}
|
||||||
|
: { email: 'unknown@example.com' };
|
||||||
|
|
||||||
|
// Map the type to what QuotedEmailContent expects
|
||||||
|
const mappedType = type === 'reply-all' ? 'reply' : type;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QuotedEmailContent
|
<QuotedEmailContent
|
||||||
content={email.content || email.html || email.text || ''}
|
content={content}
|
||||||
sender={{
|
sender={sender}
|
||||||
name: email.from?.[0]?.name || '',
|
|
||||||
email: email.from?.[0]?.address || ''
|
|
||||||
}}
|
|
||||||
date={email.date}
|
date={email.date}
|
||||||
type={type === 'reply-all' ? 'reply' : type}
|
type={mappedType}
|
||||||
className="mt-4"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -139,7 +146,7 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
const [cc, setCc] = useState<string>('');
|
const [cc, setCc] = useState<string>('');
|
||||||
const [bcc, setBcc] = useState<string>('');
|
const [bcc, setBcc] = useState<string>('');
|
||||||
const [subject, setSubject] = useState<string>('');
|
const [subject, setSubject] = useState<string>('');
|
||||||
const [emailContent, setEmailContent] = useState<string>('<div></div>');
|
const [emailContent, setEmailContent] = useState<string>('');
|
||||||
const [showCc, setShowCc] = useState<boolean>(false);
|
const [showCc, setShowCc] = useState<boolean>(false);
|
||||||
const [showBcc, setShowBcc] = useState<boolean>(false);
|
const [showBcc, setShowBcc] = useState<boolean>(false);
|
||||||
const [sending, setSending] = useState<boolean>(false);
|
const [sending, setSending] = useState<boolean>(false);
|
||||||
@ -149,53 +156,132 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
type: string;
|
type: string;
|
||||||
}>>([]);
|
}>>([]);
|
||||||
|
|
||||||
// Initialize the form with the provided email data
|
// Initialize the form when replying to or forwarding an email
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
if (initialEmail && type !== 'new') {
|
||||||
// Only process if there's an initial email and it's a reply/forward
|
try {
|
||||||
if (initialEmail && type !== 'new') {
|
// Set recipients based on type
|
||||||
// For replies: set the recipient to the sender of the original email
|
|
||||||
if (type === 'reply' || type === 'reply-all') {
|
if (type === 'reply' || type === 'reply-all') {
|
||||||
// Set recipients and subject using the formatReplyEmail utility
|
// Reply goes to the original sender
|
||||||
const formattedEmail = formatReplyEmail(initialEmail, type as 'reply' | 'reply-all');
|
|
||||||
|
|
||||||
// Set recipients
|
|
||||||
setTo(formatEmailAddresses(initialEmail.from || []));
|
setTo(formatEmailAddresses(initialEmail.from || []));
|
||||||
|
|
||||||
// For reply-all: add original recipients to CC
|
// For reply-all, include all original recipients in CC
|
||||||
if (type === 'reply-all') {
|
if (type === 'reply-all') {
|
||||||
const allRecipients = [
|
const allRecipients = [
|
||||||
...(initialEmail.to || []),
|
...(initialEmail.to || []),
|
||||||
...(initialEmail.cc || [])
|
...(initialEmail.cc || [])
|
||||||
];
|
];
|
||||||
|
// Filter out the current user if they were a recipient
|
||||||
if (allRecipients.length > 0) {
|
// This would need some user context to properly implement
|
||||||
setCc(formatEmailAddresses(allRecipients));
|
setCc(formatEmailAddresses(allRecipients));
|
||||||
setShowCc(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set subject with "Re:" prefix if needed
|
// Set subject with Re: prefix
|
||||||
setSubject(formattedEmail.subject);
|
const subjectBase = initialEmail.subject || '(No subject)';
|
||||||
|
const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`;
|
||||||
|
setSubject(subject);
|
||||||
|
|
||||||
// Initialize with empty content - we'll use QuotedEmailContent in the render
|
// Format the reply content with the quoted message included directly
|
||||||
setEmailContent('<div></div>');
|
const content = initialEmail.content || initialEmail.html || initialEmail.text || '';
|
||||||
}
|
const sender = initialEmail.from && initialEmail.from.length > 0
|
||||||
|
? initialEmail.from[0].name || initialEmail.from[0].address
|
||||||
// For forwards: set forwarded subject
|
: 'Unknown sender';
|
||||||
|
const date = initialEmail.date ?
|
||||||
|
(typeof initialEmail.date === 'string' ? new Date(initialEmail.date) : initialEmail.date) :
|
||||||
|
new Date();
|
||||||
|
|
||||||
|
// Format date for display
|
||||||
|
const formattedDate = date.toLocaleString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create reply content with quote
|
||||||
|
const replyContent = `
|
||||||
|
<div><br></div>
|
||||||
|
<div><br></div>
|
||||||
|
<div><br></div>
|
||||||
|
<div><br></div>
|
||||||
|
<div style="font-weight: 400; color: #555; margin: 20px 0 8px 0; font-size: 13px;">On ${formattedDate}, ${sender} wrote:</div>
|
||||||
|
<blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left: 2px solid #ddd; color: #505050; background-color: #f9f9f9; border-radius: 4px;">
|
||||||
|
<div style="font-size: 13px;">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
|
||||||
|
setEmailContent(replyContent);
|
||||||
|
|
||||||
|
// Show CC field if there are CC recipients
|
||||||
|
if (initialEmail.cc && initialEmail.cc.length > 0) {
|
||||||
|
setShowCc(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (type === 'forward') {
|
else if (type === 'forward') {
|
||||||
// Format the email for forwarding
|
// Set subject with Fwd: prefix
|
||||||
const formattedEmail = formatForwardedEmail(initialEmail);
|
const subjectBase = initialEmail.subject || '(No subject)';
|
||||||
|
const subject = subjectBase.match(/^(Fwd|FW|Forward):/i) ? subjectBase : `Fwd: ${subjectBase}`;
|
||||||
|
setSubject(subject);
|
||||||
|
|
||||||
// Set subject with "Fwd:" prefix
|
// Format the forward content with the original email included directly
|
||||||
setSubject(formattedEmail.subject);
|
const content = initialEmail.content || initialEmail.html || initialEmail.text || '';
|
||||||
|
const fromString = formatEmailAddresses(initialEmail.from || []);
|
||||||
|
const toString = formatEmailAddresses(initialEmail.to || []);
|
||||||
|
const date = initialEmail.date ?
|
||||||
|
(typeof initialEmail.date === 'string' ? new Date(initialEmail.date) : initialEmail.date) :
|
||||||
|
new Date();
|
||||||
|
|
||||||
// Initialize with empty content - we'll use QuotedEmailContent in the render
|
// Format date for display
|
||||||
setEmailContent('<div></div>');
|
const formattedDate = date.toLocaleString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create forwarded content
|
||||||
|
const forwardContent = `
|
||||||
|
<div><br></div>
|
||||||
|
<div><br></div>
|
||||||
|
<div><br></div>
|
||||||
|
<div><br></div>
|
||||||
|
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;">
|
||||||
|
<div style="font-family: Arial, sans-serif; color: #333;">
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<div>---------- Forwarded message ---------</div>
|
||||||
|
<div><b>From:</b> ${fromString}</div>
|
||||||
|
<div><b>Date:</b> ${formattedDate}</div>
|
||||||
|
<div><b>Subject:</b> ${initialEmail.subject || ''}</div>
|
||||||
|
<div><b>To:</b> ${toString}</div>
|
||||||
|
</div>
|
||||||
|
<div class="email-original-content">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
setEmailContent(forwardContent);
|
||||||
|
|
||||||
|
// If the original email has attachments, we should include them
|
||||||
|
if (initialEmail.attachments && initialEmail.attachments.length > 0) {
|
||||||
|
const formattedAttachments = initialEmail.attachments.map(att => ({
|
||||||
|
name: att.filename || 'attachment',
|
||||||
|
type: att.contentType || 'application/octet-stream',
|
||||||
|
content: att.content || ''
|
||||||
|
}));
|
||||||
|
setAttachments(formattedAttachments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing compose form:', error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error initializing compose form:', error);
|
|
||||||
}
|
}
|
||||||
}, [initialEmail, type]);
|
}, [initialEmail, type]);
|
||||||
|
|
||||||
@ -364,26 +450,18 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email Content Section */}
|
{/* Message Body */}
|
||||||
<div className="flex-1 border border-gray-200 rounded-md overflow-hidden">
|
<div className="flex-1 min-h-[200px] flex flex-col overflow-hidden">
|
||||||
<RichEmailEditor
|
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
|
||||||
initialContent={emailContent}
|
<div className="flex-1 border border-gray-300 rounded-md overflow-hidden">
|
||||||
onChange={setEmailContent}
|
<RichEmailEditor
|
||||||
/>
|
initialContent={emailContent}
|
||||||
{/* Add QuotedEmailContent for replies and forwards */}
|
onChange={setEmailContent}
|
||||||
{initialEmail && (type === 'reply' || type === 'reply-all' || type === 'forward') && (
|
minHeight="200px"
|
||||||
<div className="px-4 pb-4">
|
maxHeight="none"
|
||||||
<QuotedEmailContent
|
preserveFormatting={true}
|
||||||
content={initialEmail.content || initialEmail.html || ''}
|
/>
|
||||||
sender={{
|
</div>
|
||||||
name: initialEmail.from[0]?.name || '',
|
|
||||||
email: initialEmail.from[0]?.address || ''
|
|
||||||
}}
|
|
||||||
date={initialEmail.date}
|
|
||||||
type={type === 'forward' ? 'forward' : 'reply'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Attachments */}
|
{/* Attachments */}
|
||||||
@ -735,12 +813,18 @@ function LegacyAdapter({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email Content Section */}
|
{/* Message Body */}
|
||||||
<div className="flex-1 border border-gray-200 rounded-md overflow-hidden">
|
<div className="flex-1 min-h-[200px] flex flex-col overflow-hidden">
|
||||||
<RichEmailEditor
|
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
|
||||||
initialContent={composeBody}
|
<div className="flex-1 border border-gray-300 rounded-md overflow-hidden">
|
||||||
onChange={setComposeBody}
|
<RichEmailEditor
|
||||||
/>
|
initialContent={composeBody}
|
||||||
|
onChange={setComposeBody}
|
||||||
|
minHeight="200px"
|
||||||
|
maxHeight="none"
|
||||||
|
preserveFormatting={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Attachments */}
|
{/* Attachments */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user