@novacraft-engineering/mailbox 0.4.21 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/mail/page.tsx +55 -10
- package/package.json +1 -1
package/app/mail/page.tsx
CHANGED
|
@@ -663,6 +663,12 @@ function htmlToQuoteText(html: string | null, text: string | null): string {
|
|
|
663
663
|
* imported without ids fall back to the names mail clients give such parts. Anything
|
|
664
664
|
* larger, or not an image, is a real file.
|
|
665
665
|
*/
|
|
666
|
+
// Some senders put the plain-text body straight into the HTML part. Rendered as markup its
|
|
667
|
+
// line breaks are just whitespace and the message collapses into one paragraph, so a part
|
|
668
|
+
// carrying no tags has to be treated as the text it actually is.
|
|
669
|
+
const hasMarkup = (html: string | null | undefined): html is string =>
|
|
670
|
+
typeof html === 'string' && /<[a-z!/][^>]*>/i.test(html)
|
|
671
|
+
|
|
666
672
|
const INLINE_IMAGE_MAX_BYTES = 150_000
|
|
667
673
|
const INLINE_IMAGE_NAME = /^(image\d*|outlook-[\w-]+|blocked|[0-9a-f]{8}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.(png|gif|jpe?g)$/i
|
|
668
674
|
function inlineAttachmentNames(message: InboundEmail): Set<string> {
|
|
@@ -3712,6 +3718,9 @@ export default function DevMailPage() {
|
|
|
3712
3718
|
.catch(() => {})
|
|
3713
3719
|
.finally(() => {
|
|
3714
3720
|
if (flags.read === true) ids.forEach(id => pendingRead.current.delete(id))
|
|
3721
|
+
// The starred tally counts conversations, not messages, so it cannot be derived
|
|
3722
|
+
// from the ids alone — the server says what it is now.
|
|
3723
|
+
if (flags.starred !== undefined) loadCounts(true)
|
|
3715
3724
|
})
|
|
3716
3725
|
},
|
|
3717
3726
|
[apiHeaders],
|
|
@@ -3748,6 +3757,40 @@ export default function DevMailPage() {
|
|
|
3748
3757
|
},
|
|
3749
3758
|
[apiHeaders, setInboundFlag, threadIds, threadKeys, loadCounts],
|
|
3750
3759
|
)
|
|
3760
|
+
/**
|
|
3761
|
+
* The row carries the newest message's id, but the star belongs to the conversation —
|
|
3762
|
+
* starring the newest message leaves an older starred one lit and the row never clears.
|
|
3763
|
+
*/
|
|
3764
|
+
const toggleRowStar = useCallback(
|
|
3765
|
+
(id: string, threadId: string | null, starred: boolean) => {
|
|
3766
|
+
if (!threadId) {
|
|
3767
|
+
setInboundFlag(threadIds(id), { starred })
|
|
3768
|
+
return
|
|
3769
|
+
}
|
|
3770
|
+
setInboxEmails(list =>
|
|
3771
|
+
list.map(entry => (threadKeys.get(entry.id) === threadKeys.get(id) ? { ...entry, starred } : entry)),
|
|
3772
|
+
)
|
|
3773
|
+
setThreads(list =>
|
|
3774
|
+
list.map(thread =>
|
|
3775
|
+
thread.threadId === threadId
|
|
3776
|
+
? { ...thread, starredCount: starred ? Math.max(1, thread.count) : 0 }
|
|
3777
|
+
: thread,
|
|
3778
|
+
),
|
|
3779
|
+
)
|
|
3780
|
+
setServerCounts(current =>
|
|
3781
|
+
current ? { ...current, starred: Math.max(0, current.starred + (starred ? 1 : -1)) } : current,
|
|
3782
|
+
)
|
|
3783
|
+
fetch('/api/mail/inbox', {
|
|
3784
|
+
method: 'PATCH',
|
|
3785
|
+
headers: apiHeaders(),
|
|
3786
|
+
body: JSON.stringify({ threadId, starred }),
|
|
3787
|
+
})
|
|
3788
|
+
.then(() => loadCounts(true))
|
|
3789
|
+
.catch(() => {})
|
|
3790
|
+
},
|
|
3791
|
+
[apiHeaders, setInboundFlag, threadIds, threadKeys, loadCounts],
|
|
3792
|
+
)
|
|
3793
|
+
|
|
3751
3794
|
useEffect(() => { swipeCommitRef.current = commitRowFlags }, [commitRowFlags])
|
|
3752
3795
|
useEffect(() => { setInboundFlagRef.current = setInboundFlag }, [setInboundFlag])
|
|
3753
3796
|
|
|
@@ -4978,7 +5021,7 @@ export default function DevMailPage() {
|
|
|
4978
5021
|
img { max-width: 100%; }
|
|
4979
5022
|
</style>`
|
|
4980
5023
|
doc.open()
|
|
4981
|
-
doc.write(`<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(subject)}</title>${printCss}</head><body>${meta}${html ? stripOwnPixel(html) : `<pre style="font-family:Arial,sans-serif;white-space:pre-wrap;">${escapeHtml(text
|
|
5024
|
+
doc.write(`<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(subject)}</title>${printCss}</head><body>${meta}${hasMarkup(html) ? stripOwnPixel(html) : `<pre style="font-family:Arial,sans-serif;white-space:pre-wrap;">${escapeHtml(text?.trim() || html || '')}</pre>`}</body></html>`)
|
|
4982
5025
|
doc.close()
|
|
4983
5026
|
frame.contentWindow?.focus()
|
|
4984
5027
|
window.setTimeout(() => {
|
|
@@ -5881,13 +5924,14 @@ export default function DevMailPage() {
|
|
|
5881
5924
|
if (readerMode === 'html') return <pre className={styles.readerSource}>{formatHtmlSource(message.html ?? '')}</pre>
|
|
5882
5925
|
if (readerMode === 'raw') return <pre className={styles.readerSource}>{rawInboundMessage(message)}</pre>
|
|
5883
5926
|
// preview
|
|
5884
|
-
if (!message.html) {
|
|
5885
|
-
const
|
|
5927
|
+
if (!hasMarkup(message.html)) {
|
|
5928
|
+
const source = message.text?.trim() ? message.text : (message.html ?? '')
|
|
5929
|
+
const plain = splitQuotedText(source)
|
|
5886
5930
|
const textShown = quoteOpen.has(message.id)
|
|
5887
5931
|
return (
|
|
5888
5932
|
<>
|
|
5889
|
-
{(textShown ?
|
|
5890
|
-
? <pre className={styles.readerText}>{textShown ?
|
|
5933
|
+
{(textShown ? source : plain.head)
|
|
5934
|
+
? <pre className={styles.readerText}>{textShown ? source : plain.head}</pre>
|
|
5891
5935
|
: renderBodylessAttachment(inboundAttachments[message.id] ?? []) ?? <pre className={styles.readerText}>(no content)</pre>}
|
|
5892
5936
|
{plain.tail && renderQuoteLine(message.id, textShown)}
|
|
5893
5937
|
</>
|
|
@@ -5936,7 +5980,7 @@ export default function DevMailPage() {
|
|
|
5936
5980
|
const renderSentBody = (sent: SentEmail) => {
|
|
5937
5981
|
const detail = detailCache[sent.id]
|
|
5938
5982
|
if (!detail) return <div className={styles.threadPending}>Loading message…</div>
|
|
5939
|
-
if (detail.html
|
|
5983
|
+
if (hasMarkup(detail.html)) {
|
|
5940
5984
|
return (
|
|
5941
5985
|
<iframe
|
|
5942
5986
|
className={styles.threadFrame}
|
|
@@ -5952,8 +5996,9 @@ export default function DevMailPage() {
|
|
|
5952
5996
|
/>
|
|
5953
5997
|
)
|
|
5954
5998
|
}
|
|
5955
|
-
|
|
5956
|
-
|
|
5999
|
+
const plainSent = detail.text?.trim() ? detail.text : (detail.html ?? '').trim()
|
|
6000
|
+
return plainSent
|
|
6001
|
+
? <pre className={styles.readerText}>{plainSent}</pre>
|
|
5957
6002
|
: renderBodylessAttachment(detail.attachments ?? []) ?? <pre className={styles.readerText}>(no content)</pre>
|
|
5958
6003
|
}
|
|
5959
6004
|
|
|
@@ -7891,12 +7936,12 @@ export default function DevMailPage() {
|
|
|
7891
7936
|
className={`${styles.itemStar} ${item.starred ? styles.itemStarOn : ''}`}
|
|
7892
7937
|
onClick={event => {
|
|
7893
7938
|
event.stopPropagation()
|
|
7894
|
-
|
|
7939
|
+
toggleRowStar(item.id, item.kind === 'inbound' ? item.threadId : null, !item.starred)
|
|
7895
7940
|
}}
|
|
7896
7941
|
onKeyDown={event => {
|
|
7897
7942
|
if (event.key === 'Enter') {
|
|
7898
7943
|
event.stopPropagation()
|
|
7899
|
-
|
|
7944
|
+
toggleRowStar(item.id, item.kind === 'inbound' ? item.threadId : null, !item.starred)
|
|
7900
7945
|
}
|
|
7901
7946
|
}}
|
|
7902
7947
|
>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novacraft-engineering/mailbox",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.23",
|
|
4
4
|
"description": "A shared webmail app: one codebase, one deployment per mailbox. Threads, a rich composer with signatures, attachments on S3-compatible storage, sharing links, full-text search, web push and PWA install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"webmail",
|