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 | |
| 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')
| -rw-r--r-- | static/js/dropdown.js | 18 | ||||
| -rw-r--r-- | static/js/icons.js | 24 | ||||
| -rw-r--r-- | static/js/mail.js | 390 | ||||
| -rw-r--r-- | static/js/shadow.js | 50 | ||||
| -rw-r--r-- | static/js/utils.js | 113 |
5 files changed, 410 insertions, 185 deletions
diff --git a/static/js/dropdown.js b/static/js/dropdown.js index 00b7ee6..80efd19 100644 --- a/static/js/dropdown.js +++ b/static/js/dropdown.js @@ -1,11 +1,12 @@ document.addEventListener('DOMContentLoaded', function () { - // Handle dropdown clicks - document.querySelectorAll('.options-subitem > a').forEach(function (item) { - item.addEventListener('click', function (e) { + document.addEventListener('click', function (e) { + const toggleLink = e.target.closest('a'); + + if (toggleLink && toggleLink.parentElement && toggleLink.parentElement.classList.contains('options-subitem')) { e.preventDefault(); - const parent = this.parentElement; + const parent = toggleLink.parentElement; - if (parent.classList.contains('disabled')) { + if (parent.classList.contains('disabled') || parent.getAttribute('disabled') !== null) { return; } @@ -16,11 +17,10 @@ document.addEventListener('DOMContentLoaded', function () { }); parent.classList.toggle('open'); - }); - }); + return; + } - // Close dropdowns when clicking outside - document.addEventListener('click', function (e) { + // Handle clicking outside if (!e.target.closest('.options-subitem')) { document.querySelectorAll('.options-subitem.open').forEach(function (item) { item.classList.remove('open'); diff --git a/static/js/icons.js b/static/js/icons.js new file mode 100644 index 0000000..d445ed0 --- /dev/null +++ b/static/js/icons.js @@ -0,0 +1,24 @@ +const Icons = { + Html: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="2" y="2" width="28" height="28" fill="#00CED1" stroke="#000" stroke-width="3"/><rect x="8" y="10" width="4" height="4" fill="#000"/><rect x="12" y="14" width="4" height="4" fill="#000"/><rect x="8" y="18" width="4" height="4" fill="#000"/><rect x="18" y="18" width="6" height="4" fill="#000"/></svg>`, + + Plain: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="2" width="24" height="28" fill="#E8E8E8" stroke="#000" stroke-width="3"/><rect x="8" y="8" width="16" height="3" fill="#666"/><rect x="8" y="14" width="16" height="3" fill="#666"/><rect x="8" y="20" width="12" height="3" fill="#666"/></svg>`, + + Reply: `<svg width="32" height="32" viewBox="0 0 32 32"><path d="M4 16L4 16M16 8L16 24L16 24M4 16L16 8M4 16L16 24" fill="none"/><rect x="4" y="14" width="20" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/><rect x="4" y="10" width="4" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/><rect x="4" y="18" width="4" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/><rect x="8" y="6" width="4" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/><rect x="8" y="22" width="4" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/></svg>`, + + Forward: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="8" y="14" width="20" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/><rect x="24" y="10" width="4" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/><rect x="24" y="18" width="4" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/><rect x="20" y="6" width="4" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/><rect x="20" y="22" width="4" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/></svg>`, + + Details: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="14" y="4" width="4" height="24" fill="#90EE90" stroke="#000" stroke-width="3"/><rect x="4" y="14" width="24" height="4" fill="#90EE90" stroke="#000" stroke-width="3"/></svg>`, + + Wrap: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="6" width="24" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="4" y="12" width="18" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="22" y="12" width="3" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="22" y="15" width="3" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="16" y="18" width="6" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="12" y="18" width="4" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="12" y="21" width="3" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/></svg>`, + + Headers: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="4" width="24" height="24" fill="#F0E68C" stroke="#000" stroke-width="3"/><rect x="4" y="12" width="24" height="3" fill="#000"/><rect x="4" y="20" width="24" height="3" fill="#000"/><rect x="15" y="4" width="3" height="24" fill="#000"/></svg>`, + + Summary: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="14" width="24" height="4" fill="#FFcccb" stroke="#000" stroke-width="3"/></svg>`, + + Window: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="8" width="18" height="18" fill="#B0C4DE" stroke="#000" stroke-width="3"/><rect x="10" y="4" width="18" height="4" fill="#B0C4DE" stroke="#000" stroke-width="3"/><rect x="24" y="4" width="4" height="12" fill="#B0C4DE" stroke="#000" stroke-width="3"/><rect x="20" y="4" width="4" height="4" fill="#B0C4DE"/><rect x="16" y="8" width="4" height="4" fill="#B0C4DE"/><rect x="12" y="12" width="4" height="4" fill="#B0C4DE"/></svg>`, + + addContact: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="5" r="2" stroke="currentColor" stroke-width="1.5"/><path d="M3 12c0-2 1.5-3 4-3s4 1 4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/></svg>', + + composeMail: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 3h10v8H2V3zM2 3l5 4 5-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/></svg>' + +};
\ No newline at end of file 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 diff --git a/static/js/shadow.js b/static/js/shadow.js index 03d23cf..20c35e1 100644 --- a/static/js/shadow.js +++ b/static/js/shadow.js @@ -1,5 +1,5 @@ const ShadowRenderer = { - render: function (hostElement, htmlContent, options = {}) { + render: function (hostElement, htmlContent) { let shadow = hostElement.shadowRoot; if (!shadow) { shadow = hostElement.attachShadow({ mode: 'open' }); @@ -8,40 +8,32 @@ const ShadowRenderer = { const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, 'text/html'); - const styles = doc.querySelectorAll('style'); - styles.forEach(style => { - style.textContent = style.textContent.replace(/(^|[\}\s,;])body(?=[\s,\.\{])/gi, '$1.mail-body-content'); - }); - - const wrapper = document.createElement('div'); - wrapper.className = 'mail-body-content'; + shadow.innerHTML = ''; - if (doc.body) { - Array.from(doc.body.attributes).forEach(attr => { - if (attr.name === 'class') { - if (attr.value) wrapper.classList.add(...attr.value.split(' ')); - } else { - wrapper.setAttribute(attr.name, attr.value); - } + if (doc.head) { + doc.head.querySelectorAll('style').forEach(style => { + const styleClone = style.cloneNode(true); + let css = styleClone.textContent; + css = css.replace(/\bbody\b/g, ':host'); + styleClone.textContent = css; + shadow.appendChild(styleClone); }); - if (doc.body.bgColor) wrapper.style.backgroundColor = doc.body.bgColor; - while (doc.body.firstChild) wrapper.appendChild(doc.body.firstChild); + doc.head.querySelectorAll('link[rel="stylesheet"]').forEach(link => { + shadow.appendChild(link.cloneNode(true)); + }); } - shadow.innerHTML = ''; - if (doc.head) { - while (doc.head.firstChild) shadow.appendChild(doc.head.firstChild); + if (doc.body) { + while (doc.body.firstChild) { + shadow.appendChild(doc.body.firstChild); + } } - shadow.appendChild(wrapper); - const defaultStyle = document.createElement('style'); - defaultStyle.textContent = ` - :host { display: block; overflow: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } - img { max-width: 100%; height: auto; } - a { color: #1a73e8; } - `; - shadow.prepend(defaultStyle); + + setTimeout(() => { + EmailUtils.adjustContrast(shadow); + }, 100); return shadow; } -}; +};
\ No newline at end of file diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..edfa2e9 --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,113 @@ +const EmailUtils = { + getProfilePicture: async function (email, name) { + const gravatarUrl = await this.checkGravatar(email); + if (gravatarUrl) return gravatarUrl; + + let domain = email.split('@')[1]; + + // Remove all subdomains from the domain + const domainParts = domain.split('.'); + if (domainParts.length > 2) { + domainParts.shift(); + domain = domainParts.join('.'); + } + + return `https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://${domain}&size=128`; + }, + + checkGravatar: async function (email) { + const hash = await this.sha256(email.toLowerCase().trim()); + const testUrl = `https://www.gravatar.com/avatar/${hash}?s=128&d=404`; + + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => resolve(testUrl); + img.onerror = () => resolve(null); + img.src = testUrl; + }); + }, + + sha256: async function (message) { + const msgBuffer = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + }, + + getInitials: function (name, email) { + if (name && name !== email) { + const parts = name.trim().split(' '); + if (parts.length >= 2) { + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + } + return name.substring(0, 2).toUpperCase(); + } + return email.substring(0, 2).toUpperCase(); + }, + + formatEmailDisplay: function (name, email, showAddress) { + if (!name || name === email) { + return email; + } + + if (showAddress) { + return `${name} <${email}>`; + } + + return name; + }, + + linkifyText: function (text) { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const emailRegex = /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/g; + + return text + .replace(urlRegex, '<a href="$1" target="_blank" style="color: var(--accent-primary); text-decoration: underline;">$1</a>') + .replace(emailRegex, '<a href="mailto:$1" style="color: var(--accent-primary); text-decoration: underline;">$1</a>'); + }, + + adjustContrast: function (container) { + const allElements = container.querySelectorAll('*'); + + allElements.forEach(el => { + const style = window.getComputedStyle(el); + const color = style.color; + const bgColor = style.backgroundColor; + + const textLum = this.getLuminance(color); + const bgLum = this.getLuminance(bgColor); + + const isTransparent = bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent'; + + let actualBgLum = bgLum; + if (isTransparent) { + let parent = el.parentElement; + while (parent && (window.getComputedStyle(parent).backgroundColor === 'rgba(0, 0, 0, 0)' || + window.getComputedStyle(parent).backgroundColor === 'transparent')) { + parent = parent.parentElement; + } + if (parent) { + actualBgLum = this.getLuminance(window.getComputedStyle(parent).backgroundColor); + } + } + + if (actualBgLum > 0.8 && textLum > 0.55) { + el.style.setProperty('color', '#000000', 'important'); + } else if (actualBgLum < 0.3 && textLum < 0.45) { + el.style.setProperty('color', '#e8e8f0', 'important'); + } + }); + }, + + getLuminance: function (color) { + const rgb = color.match(/\d+/g); + if (!rgb || rgb.length < 3) return 1; + + const [r, g, b] = rgb.map(val => { + const v = val / 255; + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }); + + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } +};
\ No newline at end of file |
