feat: enhance email and contact detail views with snippet repair functionality
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
- Updated email and contact detail views to utilize the repairSnippet function for improved snippet display. - Refactored email-view-messages to ensure consistent snippet formatting across different components. - Enhanced mail-mime-body utility to include additional repair logic for handling UTF-8 mojibake, improving text rendering quality.
This commit is contained in:
parent
ce364dbdb4
commit
364ef0ef77
@ -22,6 +22,7 @@ import { useContactsList } from "@/lib/contacts/use-contacts-list"
|
|||||||
import { ContactAvatar } from "@/components/gmail/contacts/contact-avatar"
|
import { ContactAvatar } from "@/components/gmail/contacts/contact-avatar"
|
||||||
import { fullContactDisplayName } from "@/lib/contacts/types"
|
import { fullContactDisplayName } from "@/lib/contacts/types"
|
||||||
import { useMailSearch } from "@/lib/api/hooks/use-mail-queries"
|
import { useMailSearch } from "@/lib/api/hooks/use-mail-queries"
|
||||||
|
import { repairSnippet } from "@/lib/mail-mime-body"
|
||||||
import { useComposeActions } from "@/lib/compose-context"
|
import { useComposeActions } from "@/lib/compose-context"
|
||||||
import { useNavStore } from "@/lib/stores/nav-store"
|
import { useNavStore } from "@/lib/stores/nav-store"
|
||||||
import {
|
import {
|
||||||
@ -90,7 +91,7 @@ export function ContactDetailView({ contactId }: ContactDetailViewProps) {
|
|||||||
return searchResult.data.slice(0, 10).map((msg) => ({
|
return searchResult.data.slice(0, 10).map((msg) => ({
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
subject: msg.subject,
|
subject: msg.subject,
|
||||||
preview: msg.snippet,
|
preview: repairSnippet(msg.snippet) ?? msg.snippet,
|
||||||
date: msg.date,
|
date: msg.date,
|
||||||
}))
|
}))
|
||||||
}, [searchResult])
|
}, [searchResult])
|
||||||
|
|||||||
@ -103,7 +103,7 @@ function apiToLegacyEmail(
|
|||||||
senderEmail: m.from[0]?.address ?? "",
|
senderEmail: m.from[0]?.address ?? "",
|
||||||
date: m.date,
|
date: m.date,
|
||||||
body: m.body_html ?? m.body_text ?? "",
|
body: m.body_html ?? m.body_text ?? "",
|
||||||
preview: m.snippet,
|
preview: repairSnippet(m.snippet) ?? m.snippet,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
type MessageHeaderDetails,
|
type MessageHeaderDetails,
|
||||||
} from "@/lib/mail-message-header-details"
|
} from "@/lib/mail-message-header-details"
|
||||||
import type { EmailAttachment } from "@/lib/email-data"
|
import type { EmailAttachment } from "@/lib/email-data"
|
||||||
|
import { repairMimeBodies, repairSnippet } from "@/lib/mail-mime-body"
|
||||||
import { ContactHoverCard } from "@/components/gmail/contact-hover-card"
|
import { ContactHoverCard } from "@/components/gmail/contact-hover-card"
|
||||||
import { EmailViewMessageToolbar } from "@/components/gmail/email-view/email-view-toolbar"
|
import { EmailViewMessageToolbar } from "@/components/gmail/email-view/email-view-toolbar"
|
||||||
import { MessageBodyContent } from "@/components/gmail/email-view/message-body-content"
|
import { MessageBodyContent } from "@/components/gmail/email-view/message-body-content"
|
||||||
@ -31,7 +32,6 @@ import {
|
|||||||
MAIL_MESSAGE_HOVER_CLASS,
|
MAIL_MESSAGE_HOVER_CLASS,
|
||||||
MAIL_TOOLTIP_CONTENT_CLASS,
|
MAIL_TOOLTIP_CONTENT_CLASS,
|
||||||
} from "@/lib/mail-chrome-classes"
|
} from "@/lib/mail-chrome-classes"
|
||||||
import { repairMimeBodies } from "@/lib/mail-mime-body"
|
|
||||||
import { plainTextToDisplayHtml } from "@/lib/mail-plain-text-html"
|
import { plainTextToDisplayHtml } from "@/lib/mail-plain-text-html"
|
||||||
|
|
||||||
export function plainTextBodyFallback(
|
export function plainTextBodyFallback(
|
||||||
@ -47,8 +47,9 @@ export function formatApiMessageBody(
|
|||||||
snippet: string | undefined,
|
snippet: string | undefined,
|
||||||
loading: boolean
|
loading: boolean
|
||||||
): string {
|
): string {
|
||||||
const snippetHtml = snippet?.trim()
|
const preview = repairSnippet(snippet) ?? snippet
|
||||||
? `<p style="color:var(--muted-foreground);">${snippet.trim()}</p>`
|
const snippetHtml = preview?.trim()
|
||||||
|
? `<p style="color:var(--muted-foreground);">${preview.trim()}</p>`
|
||||||
: ""
|
: ""
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -62,13 +63,13 @@ export function formatApiMessageBody(
|
|||||||
return plainTextToDisplayHtml(text)
|
return plainTextToDisplayHtml(text)
|
||||||
}
|
}
|
||||||
if (full) {
|
if (full) {
|
||||||
const s = snippet?.trim()
|
const s = preview?.trim()
|
||||||
if (s) {
|
if (s) {
|
||||||
return `<p style="color:var(--muted-foreground);">${s}</p>`
|
return `<p style="color:var(--muted-foreground);">${s}</p>`
|
||||||
}
|
}
|
||||||
return `<p style="color:var(--muted-foreground);">Ce message n’a pas de contenu.</p>`
|
return `<p style="color:var(--muted-foreground);">Ce message n’a pas de contenu.</p>`
|
||||||
}
|
}
|
||||||
const s = snippet?.trim()
|
const s = preview?.trim()
|
||||||
return s
|
return s
|
||||||
? `<p style="color:var(--muted-foreground);">${s}</p>`
|
? `<p style="color:var(--muted-foreground);">${s}</p>`
|
||||||
: ""
|
: ""
|
||||||
@ -242,7 +243,9 @@ export function CollapsedMessage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="min-w-0 truncate text-sm leading-snug text-muted-foreground">{message.snippet}</p>
|
<p className="min-w-0 truncate text-sm leading-snug text-muted-foreground">
|
||||||
|
{repairSnippet(message.snippet) ?? message.snippet}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -55,17 +55,55 @@ function decodeBytesToUtf8(bytes: Uint8Array, charset = ""): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UTF8_MOJIBAKE_PAIR_RE = /[\u00C2\u00C3][\u0080-\u00BF]/
|
||||||
|
|
||||||
|
function looksLikeUtf8Mojibake(s: string): boolean {
|
||||||
|
return UTF8_MOJIBAKE_PAIR_RE.test(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTF-8 misread as Latin-1: "réactivité" → "réactivité". */
|
||||||
|
function repairUtf8Mojibake(s: string): string {
|
||||||
|
if (!s || !looksLikeUtf8Mojibake(s)) return s
|
||||||
|
let out = ""
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
const code = s.charCodeAt(i)
|
||||||
|
if ((code === 0xc2 || code === 0xc3) && i + 1 < s.length) {
|
||||||
|
const next = s.charCodeAt(i + 1)
|
||||||
|
if (next >= 0x80 && next <= 0xbf) {
|
||||||
|
const bytes = new Uint8Array([code, next])
|
||||||
|
try {
|
||||||
|
out += new TextDecoder("utf-8", { fatal: true }).decode(bytes)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
} catch {
|
||||||
|
/* keep raw chars */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += s[i]!
|
||||||
|
}
|
||||||
|
if (out === s) return s
|
||||||
|
return repairLoneMojibakeLeaders(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
function repairLoneMojibakeLeaders(s: string): string {
|
||||||
|
return s
|
||||||
|
.replace(/\u00C3(?=[\s,.;:!?])/g, "à")
|
||||||
|
.replace(/\u00C2(?=[\s,.;:!?])/g, "Â")
|
||||||
|
}
|
||||||
|
|
||||||
function repairLegacyCharsetString(s: string): string {
|
function repairLegacyCharsetString(s: string): string {
|
||||||
if (!s) return s
|
if (!s) return s
|
||||||
|
let repaired = s
|
||||||
try {
|
try {
|
||||||
new TextDecoder("utf-8", { fatal: true }).decode(
|
new TextDecoder("utf-8", { fatal: true }).decode(
|
||||||
Uint8Array.from(s, (c) => c.charCodeAt(0) & 0xff)
|
Uint8Array.from(s, (c) => c.charCodeAt(0) & 0xff)
|
||||||
)
|
)
|
||||||
return s
|
|
||||||
} catch {
|
} catch {
|
||||||
const bytes = Uint8Array.from(s, (c) => c.charCodeAt(0) & 0xff)
|
const bytes = Uint8Array.from(s, (c) => c.charCodeAt(0) & 0xff)
|
||||||
return decodeBytesToUtf8(bytes)
|
repaired = decodeBytesToUtf8(bytes)
|
||||||
}
|
}
|
||||||
|
return repairUtf8Mojibake(repaired)
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeBase64Part(encoded: string, charset = ""): string {
|
function decodeBase64Part(encoded: string, charset = ""): string {
|
||||||
@ -203,13 +241,29 @@ export function repairMimeBodies(
|
|||||||
text = stripInvisibleTextRuns(text)
|
text = stripInvisibleTextRuns(text)
|
||||||
|
|
||||||
if (!looksLikeRawMime(text) && !looksLikeRawMime(html)) {
|
if (!looksLikeRawMime(text) && !looksLikeRawMime(html)) {
|
||||||
return { bodyText: text || bodyText, bodyHtml: html || bodyHtml }
|
return finalizeMimeBodies(text, bodyText, html, bodyHtml)
|
||||||
}
|
}
|
||||||
const parsed = parseEmbeddedMime(text || html)
|
const parsed = parseEmbeddedMime(text || html)
|
||||||
if (!parsed) return { bodyText: text || bodyText, bodyHtml: html || bodyHtml }
|
if (!parsed) return finalizeMimeBodies(text, bodyText, html, bodyHtml)
|
||||||
|
return finalizeMimeBodies(
|
||||||
|
parsed.text || text || bodyText,
|
||||||
|
bodyText,
|
||||||
|
parsed.html || html || bodyHtml,
|
||||||
|
bodyHtml
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeMimeBodies(
|
||||||
|
text: string | undefined,
|
||||||
|
bodyText: string | undefined,
|
||||||
|
html: string | undefined,
|
||||||
|
bodyHtml: string | undefined
|
||||||
|
): { bodyText?: string; bodyHtml?: string } {
|
||||||
|
const outText = repairUtf8Mojibake(repairLegacyCharsetString(text?.trim() ?? ""))
|
||||||
|
const outHtml = repairUtf8Mojibake(repairLegacyCharsetString(html?.trim() ?? ""))
|
||||||
return {
|
return {
|
||||||
bodyText: parsed.text || text || bodyText,
|
bodyText: outText || bodyText,
|
||||||
bodyHtml: parsed.html || html || bodyHtml,
|
bodyHtml: outHtml || bodyHtml,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,6 +472,6 @@ export function repairSnippet(snippet?: string): string | undefined {
|
|||||||
const decoded = decodeBareBase64IfNeeded(qp)
|
const decoded = decodeBareBase64IfNeeded(qp)
|
||||||
const raw = decoded !== trimmed ? decoded : snippet
|
const raw = decoded !== trimmed ? decoded : snippet
|
||||||
const cleaned = stripInvisibleTextRuns(raw)
|
const cleaned = stripInvisibleTextRuns(raw)
|
||||||
const polished = polishSnippetPreview(cleaned)
|
const polished = repairUtf8Mojibake(polishSnippetPreview(cleaned))
|
||||||
return polished || undefined
|
return polished || undefined
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user