diff options
| author | Bobby <[email protected]> | 2025-12-29 10:46:00 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-12-29 10:46:00 +0530 |
| commit | f59ca1976a1075e9e8fdf1e5fcdb7cfc853493b8 (patch) | |
| tree | 1123c3d0785a44261151bc46f2b38a4db9eb57b7 /static/js/mail.js | |
| parent | cccf44496a056d15d5d86d9fbd74633f21e852bb (diff) | |
| download | lain-f59ca1976a1075e9e8fdf1e5fcdb7cfc853493b8.tar.xz lain-f59ca1976a1075e9e8fdf1e5fcdb7cfc853493b8.zip | |
feat: Enhance email viewer with new UI actions, sender profile pictures, and raw header display.
Diffstat (limited to 'static/js/mail.js')
| -rw-r--r-- | static/js/mail.js | 390 |
1 files changed, 243 insertions, 147 deletions
diff --git a/static/js/mail.js b/static/js/mail.js index fd02120..1de18ca 100644 --- a/static/js/mail.js +++ b/static/js/mail.js @@ -5,8 +5,11 @@ document.addEventListener('DOMContentLoaded', function () { let currentEmailId = null; let markAsReadTimer = null; + let currentEmail = null; + let viewMode = 'html'; + let wordWrap = true; + let showDetails = false; - // Parse preferences from data attributes const prefs = { MarkMessagesAsRead: prefsData ? prefsData.dataset.markAsRead : 'Immediately', ShowEmailAddressWithDisplayName: prefsData ? prefsData.dataset.showAddress === 'true' : true, @@ -30,7 +33,6 @@ document.addEventListener('DOMContentLoaded', function () { currentEmailId = emailId; - // Clear previous timer if (markAsReadTimer) { clearTimeout(markAsReadTimer); } @@ -40,9 +42,15 @@ document.addEventListener('DOMContentLoaded', function () { if (!response.ok) throw new Error('Failed to fetch email'); const email = await response.json(); + currentEmail = email; + showDetails = false; renderEmail(email); - // Handle mark as read based on preference + document.querySelectorAll('.subnav .nav-subitem').forEach(item => { + item.removeAttribute('disabled'); + item.classList.remove('disabled'); + }); + if (!email.IsRead) { handleMarkAsRead(emailId, this); } @@ -53,7 +61,6 @@ document.addEventListener('DOMContentLoaded', function () { }); }); - // Handle flag clicks document.querySelectorAll('.email-flag').forEach(flag => { flag.addEventListener('click', async function (e) { e.stopPropagation(); @@ -97,9 +104,7 @@ document.addEventListener('DOMContentLoaded', function () { const delay = delays[markOption]; - if (delay === null) { - return; // Never mark as read - } + if (delay === null) return; markAsReadTimer = setTimeout(async () => { try { @@ -116,182 +121,285 @@ document.addEventListener('DOMContentLoaded', function () { }, delay); } - function renderEmail(email) { + async function renderEmail(email) { preview.innerHTML = ''; - // Header - const header = createHeader(email); + const header = await createHeader(email); preview.appendChild(header); - // Sender info - const sender = createSenderInfo(email); - preview.appendChild(sender); - - // Recipients - const recipients = createRecipients(email); - preview.appendChild(recipients); - - // Attachments - if (email.Attachments && email.Attachments.length > 0) { - const attachments = createAttachments(email.Attachments); - preview.appendChild(attachments); - } - - // Body const body = createBody(email); preview.appendChild(body); } - function createHeader(email) { + async function createHeader(email) { const header = document.createElement('div'); - header.className = 'email-header'; + header.className = 'email-card-header'; + + // Card container + const card = document.createElement('div'); + card.className = 'email-card'; + + // Header Top Row (Subject + Actions) + const headerRow = document.createElement('div'); + headerRow.className = 'header-top-row'; + + // Subject container + const subjectContainer = document.createElement('div'); + subjectContainer.className = 'subject-container'; + subjectContainer.innerHTML = ` + <div class="field-label">SUBJECT</div> + <div class="field-value subject-text">${email.Subject || '[No Subject]'}</div> + `; + + // Action buttons grid + const actionsGrid = document.createElement('div'); + actionsGrid.className = 'card-actions-grid'; + + const actions = [ + { icon: Icons.Html, label: 'Switch to HTML View', active: viewMode === 'html', onClick: () => switchViewMode('html') }, + { icon: Icons.Plain, label: 'Switch to Plain Text View', active: viewMode === 'plain', onClick: () => switchViewMode('plain') }, + { icon: Icons.Reply, label: 'Reply to Sender', onClick: () => console.log('Reply') }, + { icon: Icons.Forward, label: 'Forward Message', onClick: () => console.log('Forward') }, + { + id: 'btn-details', + icon: showDetails ? Icons.Summary : Icons.Details, + label: showDetails ? 'Hide Details' : 'Show Details', + onClick: toggleDetails + }, + { icon: Icons.Wrap, label: 'Toggle Word Wrap', active: wordWrap, onClick: () => toggleWordWrap() }, + { icon: Icons.Headers, label: 'View Message Headers', onClick: () => showHeaders(email) }, + { icon: Icons.Window, label: 'Open in New Window', onClick: () => console.log('Window') } + ]; - const subject = document.createElement('h2'); - subject.className = 'email-subject'; + actions.forEach(action => { + const btn = document.createElement('button'); + btn.className = 'card-action-btn'; + if (action.id) btn.id = action.id; + if (action.active) btn.classList.add('active'); + btn.innerHTML = action.icon; + btn.title = action.label; // Tooltip + btn.onclick = action.onClick; + actionsGrid.appendChild(btn); + }); - if (email.Subject) { - subject.textContent = email.Subject; - } else { - const noSubject = document.createElement('span'); - noSubject.className = 'no-subject'; - noSubject.textContent = '[No Subject]'; - subject.appendChild(noSubject); + headerRow.appendChild(subjectContainer); + headerRow.appendChild(actionsGrid); + + // From field with profile + const fromField = document.createElement('div'); + fromField.className = 'card-field card-field-with-pic'; + + const picUrl = await EmailUtils.getProfilePicture(email.FromEmail, email.FromName); + const escapedName = (email.FromName || '').replace(/'/g, "\\'"); + const escapedEmail = (email.FromEmail || '').replace(/'/g, "\\'"); + + fromField.innerHTML = ` + <div class="field-label">FROM</div> + <div class="field-value-with-pic"> + <img src="${picUrl}" class="card-pic" onerror="this.outerHTML='<div class=card-pic-init>${EmailUtils.getInitials(email.FromName, email.FromEmail)}</div>'"> + <div class="sender-details"> + <div class="sender-name-card" onclick="showSenderMenu(event, '${escapedName}', '${escapedEmail}')">${email.FromName || email.FromEmail}</div> + <div class="sender-email-card" onclick="showSenderMenu(event, '${escapedName}', '${escapedEmail}')">${email.FromEmail}</div> + </div> + </div> + `; + + // Date field + const dateField = document.createElement('div'); + dateField.className = 'card-field'; + dateField.innerHTML = ` + <div class="field-label">DATE</div> + <div class="field-value">${email.DateFormatted}</div> + `; + + // Details container (hidden by default) + const detailsContainer = document.createElement('div'); + detailsContainer.id = 'email-details'; + detailsContainer.style.display = 'none'; + + // TO field + const toRow = document.createElement('div'); + toRow.className = 'card-field'; + toRow.innerHTML = ` + <div class="field-label">TO</div> + <div class="field-value">${email.To}</div> + `; + detailsContainer.appendChild(toRow); + + // CC field + if (email.CC) { + const ccRow = document.createElement('div'); + ccRow.className = 'card-field'; + ccRow.innerHTML = ` + <div class="field-label">CC</div> + <div class="field-value">${email.CC}</div> + `; + detailsContainer.appendChild(ccRow); } - const actions = document.createElement('div'); - actions.className = 'email-actions'; + card.appendChild(headerRow); + card.appendChild(fromField); + card.appendChild(dateField); + card.appendChild(detailsContainer); - const actionButtons = [ - { title: 'Reply', symbol: '↶' }, - { title: 'Reply All', symbol: '⇄' }, - { title: 'Forward', symbol: '→' }, - { title: 'Archive', symbol: '▼' }, - { title: 'Delete', symbol: '×' } - ]; + // Attachments field + if (email.Attachments && email.Attachments.length > 0) { + const attRow = document.createElement('div'); + attRow.className = 'card-field'; - actionButtons.forEach(btn => { - const button = document.createElement('button'); - button.className = 'btn-icon'; - button.title = btn.title; - button.textContent = btn.symbol; - actions.appendChild(button); - }); + let attHtml = ''; + email.Attachments.forEach(att => { + attHtml += `<a href="/api/mail/attachment/${att.ID}" class="attachment" download="${att.Filename}">${att.Filename} (${att.Size})</a>`; + }); - header.appendChild(subject); - header.appendChild(actions); + attRow.innerHTML = ` + <div class="field-label">ATTACHMENTS</div> + <div class="field-value">${attHtml}</div> + `; + card.appendChild(attRow); + } + header.appendChild(card); return header; } - function createSenderInfo(email) { - const sender = document.createElement('div'); - sender.className = 'email-sender'; - const senderInfo = document.createElement('div'); - senderInfo.className = 'sender-info'; - // Respect ShowEmailAddressWithDisplayName preference - const showAddress = prefs.ShowEmailAddressWithDisplayName; + function toggleDetails() { + showDetails = !showDetails; - const strong = document.createElement('strong'); - strong.textContent = email.FromName || email.From; - senderInfo.appendChild(strong); + const detailsContainer = document.getElementById('email-details'); + const detailsBtn = document.getElementById('btn-details'); - if (showAddress && email.FromName) { - const address = document.createTextNode(` <${email.From}>`); - senderInfo.appendChild(address); + if (detailsContainer) { + detailsContainer.style.display = showDetails ? 'block' : 'none'; } - const dateDiv = document.createElement('div'); - dateDiv.className = 'email-date'; - dateDiv.textContent = formatDate(email.Date); + if (detailsBtn) { + const icon = showDetails ? Icons.Summary : Icons.Details; + const label = showDetails ? 'Hide Details' : 'Show Details'; + detailsBtn.innerHTML = icon; + detailsBtn.title = label; + } + } - sender.appendChild(senderInfo); - sender.appendChild(dateDiv); + function showSenderMenu(e, name, email) { + const existingMenu = document.querySelector('.sender-menu'); + if (existingMenu) existingMenu.remove(); - return sender; - } + const menu = document.createElement('div'); + menu.className = 'sender-menu'; - function createRecipients(email) { - const recipients = document.createElement('div'); - recipients.className = 'email-recipients'; + const addContact = document.createElement('div'); + addContact.className = 'sender-menu-item'; + addContact.innerHTML = Icons.addContact + ' <span>Add to Address Book</span>'; + addContact.onclick = () => { + console.log('Add to address book:', name, email); + menu.remove(); + }; - const toDiv = document.createElement('div'); - const toStrong = document.createElement('strong'); - toStrong.textContent = 'To: '; - toDiv.appendChild(toStrong); - toDiv.appendChild(document.createTextNode(email.To || '')); - recipients.appendChild(toDiv); + const composeMail = document.createElement('div'); + composeMail.className = 'sender-menu-item'; + composeMail.innerHTML = Icons.composeMail + ' <span>Compose Mail to</span>'; + composeMail.onclick = () => { + console.log('Compose mail to:', name, email); + menu.remove(); + }; - if (email.CC) { - const ccDiv = document.createElement('div'); - const ccStrong = document.createElement('strong'); - ccStrong.textContent = 'Cc: '; - ccDiv.appendChild(ccStrong); - ccDiv.appendChild(document.createTextNode(email.CC)); - recipients.appendChild(ccDiv); - } + menu.appendChild(addContact); + menu.appendChild(composeMail); - return recipients; - } + document.body.appendChild(menu); - function createAttachments(attachments) { - const container = document.createElement('div'); - container.className = 'email-attachments'; - - const label = document.createElement('strong'); - label.textContent = 'Attachments: '; - container.appendChild(label); - - attachments.forEach(att => { - const link = document.createElement('a'); - link.href = `/api/mail/attachment/${att.ID}`; - link.className = 'attachment'; - link.download = att.Filename; - link.textContent = `${att.Filename} (${att.Size})`; - container.appendChild(link); - }); + const rect = e.target.getBoundingClientRect(); + menu.style.top = rect.bottom + 5 + 'px'; + menu.style.left = rect.left + 'px'; - return container; + setTimeout(() => { + document.addEventListener('click', function closeMenu() { + menu.remove(); + document.removeEventListener('click', closeMenu); + }); + }, 0); } + window.showSenderMenu = showSenderMenu; function createBody(email) { + const container = document.createElement('div'); + container.className = 'email-body-container'; + const body = document.createElement('div'); body.className = 'email-body'; - const displayHTML = prefs.DisplayHTML; - if (displayHTML && email.Body) { - const shadow = ShadowRenderer.render(body, email.Body); - handleRemoteContent(shadow); + const hasHTML = email.Body && email.Body.trim() !== '' && email.Body !== '<pre></pre>'; + + if (hasHTML && viewMode === 'html') { + ShadowRenderer.render(body, email.Body); } else { const pre = document.createElement('pre'); - pre.textContent = email.Body || '[Empty Content]'; + if (wordWrap) { + pre.style.whiteSpace = 'pre-wrap'; + pre.style.wordWrap = 'break-word'; + } else { + pre.style.whiteSpace = 'pre'; + } + + if (email.BodyText && email.BodyText.trim()) { + pre.innerHTML = EmailUtils.linkifyText(email.BodyText); + } else { + pre.textContent = '[Empty Content]'; + } + body.appendChild(pre); } - return body; + container.appendChild(body); + return container; + } + + + + function switchViewMode(mode) { + viewMode = mode; + if (currentEmail) { + renderEmail(currentEmail); + } } - function handleRemoteContent(container) { - const loadOption = prefs.LoadRemoteContent || 'Never'; - - if (loadOption === 'Never') { - // Block all external images - const images = container.querySelectorAll('img'); - images.forEach(img => { - const src = img.getAttribute('src'); - if (src && src.startsWith('http')) { - img.removeAttribute('src'); - img.dataset.src = src; // Store original src - img.alt = '[Remote image blocked]'; - img.style.border = '1px dashed #ccc'; - img.style.padding = '5px'; - img.style.display = 'inline-block'; - } - }); + function toggleWordWrap() { + wordWrap = !wordWrap; + if (currentEmail) { + renderEmail(currentEmail); } - // TODO: Implement "From my contacts" check - // For "Always", images load normally + } + + function showHeaders(email) { + const modal = document.createElement('div'); + modal.className = 'modal-overlay'; + modal.onclick = () => modal.remove(); + + const dialog = document.createElement('div'); + dialog.className = 'modal-dialog'; + dialog.onclick = (e) => e.stopPropagation(); + + const title = document.createElement('h3'); + title.textContent = 'Message Headers'; + title.className = 'modal-title'; + + const content = document.createElement('pre'); + content.className = 'modal-content'; + content.textContent = email.RawHeaders || 'No headers available'; + + const closeBtn = document.createElement('button'); + closeBtn.textContent = 'Close'; + closeBtn.className = 'btn-close'; + closeBtn.onclick = () => modal.remove(); + + dialog.appendChild(title); + dialog.appendChild(content); + dialog.appendChild(closeBtn); + modal.appendChild(dialog); + document.body.appendChild(modal); } function showError(message) { @@ -303,16 +411,4 @@ document.addEventListener('DOMContentLoaded', function () { error.appendChild(p); preview.appendChild(error); } - - function formatDate(dateString) { - const date = new Date(dateString); - return date.toLocaleString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } });
\ No newline at end of file |
