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 { fullContactDisplayName } from "@/lib/contacts/types"
|
||||
import { useMailSearch } from "@/lib/api/hooks/use-mail-queries"
|
||||
import { repairSnippet } from "@/lib/mail-mime-body"
|
||||
import { useComposeActions } from "@/lib/compose-context"
|
||||
import { useNavStore } from "@/lib/stores/nav-store"
|
||||
import {
|
||||
@ -90,7 +91,7 @@ export function ContactDetailView({ contactId }: ContactDetailViewProps) {
|
||||
return searchResult.data.slice(0, 10).map((msg) => ({
|
||||
id: msg.id,
|
||||
subject: msg.subject,
|
||||
preview: msg.snippet,
|
||||
preview: repairSnippet(msg.snippet) ?? msg.snippet,
|
||||
date: msg.date,
|
||||
}))
|
||||
}, [searchResult])
|
||||
|
||||
@ -103,7 +103,7 @@ function apiToLegacyEmail(
|
||||
senderEmail: m.from[0]?.address ?? "",
|
||||
date: m.date,
|
||||
body: m.body_html ?? m.body_text ?? "",
|
||||
preview: m.snippet,
|
||||
preview: repairSnippet(m.snippet) ?? m.snippet,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
type MessageHeaderDetails,
|
||||
} from "@/lib/mail-message-header-details"
|
||||
import type { EmailAttachment } from "@/lib/email-data"
|
||||
import { repairMimeBodies, repairSnippet } from "@/lib/mail-mime-body"
|
||||
import { ContactHoverCard } from "@/components/gmail/contact-hover-card"
|
||||
import { EmailViewMessageToolbar } from "@/components/gmail/email-view/email-view-toolbar"
|
||||
import { MessageBodyContent } from "@/components/gmail/email-view/message-body-content"
|
||||
@ -31,7 +32,6 @@ import {
|
||||
MAIL_MESSAGE_HOVER_CLASS,
|
||||
MAIL_TOOLTIP_CONTENT_CLASS,
|
||||
} from "@/lib/mail-chrome-classes"
|
||||
import { repairMimeBodies } from "@/lib/mail-mime-body"
|
||||
import { plainTextToDisplayHtml } from "@/lib/mail-plain-text-html"
|
||||
|
||||
export function plainTextBodyFallback(
|
||||
@ -47,8 +47,9 @@ export function formatApiMessageBody(
|
||||
snippet: string | undefined,
|
||||
loading: boolean
|
||||
): string {
|
||||
const snippetHtml = snippet?.trim()
|
||||
? `<p style="color:var(--muted-foreground);">${snippet.trim()}</p>`
|
||||
const preview = repairSnippet(snippet) ?? snippet
|
||||
const snippetHtml = preview?.trim()
|
||||
? `<p style="color:var(--muted-foreground);">${preview.trim()}</p>`
|
||||
: ""
|
||||
|
||||
if (loading) {
|
||||
@ -62,13 +63,13 @@ export function formatApiMessageBody(
|
||||
return plainTextToDisplayHtml(text)
|
||||
}
|
||||
if (full) {
|
||||
const s = snippet?.trim()
|
||||
const s = preview?.trim()
|
||||
if (s) {
|
||||
return `<p style="color:var(--muted-foreground);">${s}</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
|
||||
? `<p style="color:var(--muted-foreground);">${s}</p>`
|
||||
: ""
|
||||
@ -242,7 +243,9 @@ export function CollapsedMessage({
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
|
||||
@ -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 {
|
||||
if (!s) return s
|
||||
let repaired = s
|
||||
try {
|
||||
new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
Uint8Array.from(s, (c) => c.charCodeAt(0) & 0xff)
|
||||
)
|
||||
return s
|
||||
} catch {
|
||||
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 {
|
||||
@ -203,13 +241,29 @@ export function repairMimeBodies(
|
||||
text = stripInvisibleTextRuns(text)
|
||||
|
||||
if (!looksLikeRawMime(text) && !looksLikeRawMime(html)) {
|
||||
return { bodyText: text || bodyText, bodyHtml: html || bodyHtml }
|
||||
return finalizeMimeBodies(text, bodyText, html, bodyHtml)
|
||||
}
|
||||
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 {
|
||||
bodyText: parsed.text || text || bodyText,
|
||||
bodyHtml: parsed.html || html || bodyHtml,
|
||||
bodyText: outText || bodyText,
|
||||
bodyHtml: outHtml || bodyHtml,
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,6 +472,6 @@ export function repairSnippet(snippet?: string): string | undefined {
|
||||
const decoded = decodeBareBase64IfNeeded(qp)
|
||||
const raw = decoded !== trimmed ? decoded : snippet
|
||||
const cleaned = stripInvisibleTextRuns(raw)
|
||||
const polished = polishSnippetPreview(cleaned)
|
||||
const polished = repairUtf8Mojibake(polishSnippetPreview(cleaned))
|
||||
return polished || undefined
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user