ultisuite-client/components/gmail/email-view/sandboxed-content.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- Updated .env.example to include configuration for OnlyOffice Document Server.
- Modified the workspace configuration to remove the drive-suite path.
- Adjusted TypeScript environment imports for consistency.
- Enhanced Next.js configuration to disable canvas in Webpack.
- Updated package.json to include new dependencies for OnlyOffice and PDF.js.
- Added global styles for OnlyOffice theme integration in the CSS.
- Created new layout and page components for the Drive feature, including public sharing and editing functionalities.
- Updated metadata handling across various layouts to reflect the new app structure.
2026-06-07 15:49:21 +02:00

262 lines
7.4 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,
loadAppFont: !blockRemoteContent,
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}
/>
)
}