- Created a .cursorignore file to manage local environment files. - Updated .env.example to reflect changes in the public app URL. - Modified the gmail workspace configuration to include the drive-suite path. - Enhanced email view components to support attachment handling and fallback for plain text bodies. - Improved user experience by updating attachment display logic and integrating inline attachment support.
261 lines
7.3 KiB
TypeScript
261 lines
7.3 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type CSSProperties,
|
|
} from "react"
|
|
import { useTheme } from "next-themes"
|
|
import {
|
|
emailPreviewBaseCss,
|
|
emailPreviewDarkOverrideCss,
|
|
emailPreviewDarkRemoteBodyTailCss,
|
|
emailPreviewDarkTailOverrideCss,
|
|
emailPreviewLightOverrideCss,
|
|
emailPreviewWrapperCss,
|
|
} from "@/lib/email-preview-dark-styles"
|
|
import {
|
|
EMAIL_PREVIEW_MIN_IFRAME_HEIGHT,
|
|
measureEmailPreviewIframeHeight,
|
|
} from "@/lib/email-preview-iframe-height"
|
|
import {
|
|
buildEmailPreviewSrcdoc,
|
|
prepareEmailHtmlForIframe,
|
|
} from "@/lib/mail-html-iframe"
|
|
import { buildEmailPreviewCsp } from "@/lib/mail-remote-content"
|
|
import {
|
|
isEmailPreviewContrastDebugEnabled,
|
|
logEmailPreviewContrastIssues,
|
|
repairEmailPreviewContrast,
|
|
} from "@/lib/email-preview-contrast"
|
|
|
|
const EMAIL_PREVIEW_IFRAME_STYLE: CSSProperties = {
|
|
display: "block",
|
|
background: "transparent",
|
|
}
|
|
|
|
function documentIsDark(): boolean {
|
|
return document.documentElement.classList.contains("dark")
|
|
}
|
|
|
|
export function SandboxedContent({
|
|
html,
|
|
blockRemoteContent,
|
|
restrictPopups = false,
|
|
senderEmail,
|
|
cidUrlMap,
|
|
plainTextFallback,
|
|
messageId,
|
|
previewPart = "body",
|
|
}: {
|
|
html: string
|
|
blockRemoteContent: boolean
|
|
restrictPopups?: boolean
|
|
senderEmail?: string
|
|
cidUrlMap?: Record<string, string>
|
|
/** Plain body when HTML is image-only or empty with remote content blocked. */
|
|
plainTextFallback?: string
|
|
/** Pour les logs dev [email-preview:low-contrast]. */
|
|
messageId?: string
|
|
previewPart?: "body" | "quoted"
|
|
}) {
|
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
const contrastLoggedKeyRef = useRef<string | null>(null)
|
|
const contrastDelayTimerRef = useRef<number | null>(null)
|
|
const resizeObserverRef = useRef<ResizeObserver | null>(null)
|
|
const contentGenerationRef = useRef(0)
|
|
const [height, setHeight] = useState(EMAIL_PREVIEW_MIN_IFRAME_HEIGHT)
|
|
|
|
const sandboxValue = restrictPopups
|
|
? "allow-same-origin"
|
|
: "allow-same-origin allow-popups"
|
|
|
|
const { resolvedTheme } = useTheme()
|
|
const isDark =
|
|
resolvedTheme === "dark" ||
|
|
((resolvedTheme === "system" || resolvedTheme === undefined) &&
|
|
typeof document !== "undefined" &&
|
|
documentIsDark())
|
|
|
|
const parsedEmail = useMemo(
|
|
() =>
|
|
prepareEmailHtmlForIframe(html, {
|
|
blockRemoteContent,
|
|
isDark,
|
|
senderEmail,
|
|
cidUrlMap,
|
|
plainTextFallback,
|
|
}),
|
|
[html, blockRemoteContent, isDark, senderEmail, cidUrlMap, plainTextFallback]
|
|
)
|
|
|
|
const themeCss = useMemo(() => {
|
|
if (!blockRemoteContent) {
|
|
const wrapper = emailPreviewWrapperCss(isDark)
|
|
return isDark ? `${wrapper}${emailPreviewDarkOverrideCss()}` : wrapper
|
|
}
|
|
return `${emailPreviewBaseCss(isDark)}${
|
|
isDark ? emailPreviewDarkOverrideCss() : emailPreviewLightOverrideCss()
|
|
}`
|
|
}, [blockRemoteContent, isDark])
|
|
|
|
const cspContent = useMemo(
|
|
() => buildEmailPreviewCsp(blockRemoteContent),
|
|
[blockRemoteContent]
|
|
)
|
|
|
|
const srcdoc = useMemo(
|
|
() =>
|
|
buildEmailPreviewSrcdoc(parsedEmail, {
|
|
csp: cspContent,
|
|
wrapperCss: themeCss,
|
|
plainTextFallback,
|
|
bodyTailCss: isDark
|
|
? blockRemoteContent
|
|
? emailPreviewDarkTailOverrideCss()
|
|
: emailPreviewDarkRemoteBodyTailCss()
|
|
: undefined,
|
|
}),
|
|
[
|
|
parsedEmail,
|
|
cspContent,
|
|
themeCss,
|
|
plainTextFallback,
|
|
isDark,
|
|
blockRemoteContent,
|
|
]
|
|
)
|
|
|
|
const iframeKey = `${messageId ?? "no-id"}:${previewPart}:${blockRemoteContent ? "remote-blocked" : "remote-allowed"}:${isDark ? "dark" : "light"}`
|
|
|
|
const syncHeight = useCallback((generation: number) => {
|
|
if (generation !== contentGenerationRef.current) return
|
|
const doc = iframeRef.current?.contentDocument
|
|
if (!doc?.body) return
|
|
const next = measureEmailPreviewIframeHeight(doc)
|
|
setHeight((prev) => (prev === next ? prev : next))
|
|
}, [])
|
|
|
|
const scheduleHeightSync = useCallback(
|
|
(generation: number) => {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
syncHeight(generation)
|
|
})
|
|
})
|
|
},
|
|
[syncHeight]
|
|
)
|
|
|
|
const runContrastPipeline = useCallback(
|
|
(doc: Document, pass: "initial" | "delayed", generation: number) => {
|
|
if (!isDark || generation !== contentGenerationRef.current) return
|
|
|
|
const repair = repairEmailPreviewContrast(doc, {
|
|
isDark: true,
|
|
repairMode: blockRemoteContent ? "dark-only" : "all",
|
|
newsletterLightCanvas: false,
|
|
assumedCanvasRgb: [32, 33, 36],
|
|
})
|
|
|
|
if (repair && (repair.lightSurfaces > 0 || repair.darkSurfaces > 0)) {
|
|
scheduleHeightSync(generation)
|
|
if (isEmailPreviewContrastDebugEnabled()) {
|
|
console.info("[email-preview:contrast-repaired]", {
|
|
messageId,
|
|
part: previewPart,
|
|
pass,
|
|
blockRemoteContent,
|
|
...repair,
|
|
})
|
|
}
|
|
}
|
|
|
|
if (!isEmailPreviewContrastDebugEnabled()) return
|
|
|
|
const logKey = `${messageId ?? "no-id"}:${previewPart}:${blockRemoteContent}:${isDark}:${srcdoc.length}:${pass}`
|
|
if (contrastLoggedKeyRef.current === logKey) return
|
|
contrastLoggedKeyRef.current = logKey
|
|
logEmailPreviewContrastIssues(
|
|
{
|
|
messageId,
|
|
part: previewPart,
|
|
blockRemoteContent,
|
|
isDark,
|
|
senderEmail,
|
|
},
|
|
doc,
|
|
{ newsletterLightCanvas: false, assumedCanvasRgb: [32, 33, 36] }
|
|
)
|
|
},
|
|
[
|
|
messageId,
|
|
previewPart,
|
|
blockRemoteContent,
|
|
isDark,
|
|
senderEmail,
|
|
srcdoc,
|
|
scheduleHeightSync,
|
|
]
|
|
)
|
|
|
|
const handleIframeLoad = useCallback(() => {
|
|
const generation = contentGenerationRef.current
|
|
const doc = iframeRef.current?.contentDocument
|
|
|
|
resizeObserverRef.current?.disconnect()
|
|
if (doc?.body) {
|
|
resizeObserverRef.current = new ResizeObserver(() => {
|
|
syncHeight(generation)
|
|
})
|
|
resizeObserverRef.current.observe(doc.body)
|
|
}
|
|
|
|
scheduleHeightSync(generation)
|
|
if (doc) runContrastPipeline(doc, "initial", generation)
|
|
|
|
if (contrastDelayTimerRef.current !== null) {
|
|
window.clearTimeout(contrastDelayTimerRef.current)
|
|
}
|
|
contrastDelayTimerRef.current = window.setTimeout(() => {
|
|
contrastDelayTimerRef.current = null
|
|
if (generation !== contentGenerationRef.current) return
|
|
const lateDoc = iframeRef.current?.contentDocument
|
|
if (lateDoc) {
|
|
scheduleHeightSync(generation)
|
|
runContrastPipeline(lateDoc, "delayed", generation)
|
|
}
|
|
}, 1000)
|
|
}, [scheduleHeightSync, runContrastPipeline, syncHeight])
|
|
|
|
useLayoutEffect(() => {
|
|
contentGenerationRef.current += 1
|
|
setHeight(EMAIL_PREVIEW_MIN_IFRAME_HEIGHT)
|
|
contrastLoggedKeyRef.current = null
|
|
resizeObserverRef.current?.disconnect()
|
|
resizeObserverRef.current = null
|
|
if (contrastDelayTimerRef.current !== null) {
|
|
window.clearTimeout(contrastDelayTimerRef.current)
|
|
contrastDelayTimerRef.current = null
|
|
}
|
|
}, [srcdoc, messageId])
|
|
|
|
return (
|
|
<iframe
|
|
key={iframeKey}
|
|
ref={iframeRef}
|
|
sandbox={sandboxValue}
|
|
title="Contenu du message"
|
|
className="w-full border-0 bg-transparent"
|
|
style={{ ...EMAIL_PREVIEW_IFRAME_STYLE, height: `${height}px` }}
|
|
srcDoc={srcdoc}
|
|
onLoad={handleIframeLoad}
|
|
tabIndex={-1}
|
|
/>
|
|
)
|
|
}
|