fix iframe caching

This commit is contained in:
Arjun 2026-05-13 19:15:36 +05:30
parent b45259fe20
commit 5dd5a338e7
2 changed files with 152 additions and 25 deletions

View file

@ -365,7 +365,7 @@
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
gap: 12px;
padding: 16px 0;
padding: 12px 0;
border-top: 1px solid var(--gm-border);
}
@ -374,6 +374,32 @@
padding-top: 4px;
}
.gmail-message-header {
display: block;
width: 100%;
padding: 0;
margin: 0;
border: none;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.gmail-message-snippet {
margin-top: 2px;
color: var(--gm-text-muted);
font-size: 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.gmail-message:not(.gmail-message-expanded) .gmail-message-header:hover .gmail-message-from strong {
color: var(--gm-accent);
}
.gmail-message-avatar {
display: flex;
align-items: center;
@ -445,6 +471,36 @@
background: var(--gm-bg-card);
}
.gmail-quote-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 8px;
height: 22px;
padding: 0 10px;
border: 1px solid var(--gm-border-strong);
border-radius: 4px;
background: var(--gm-bg-pill);
color: var(--gm-text-muted);
font: inherit;
font-size: 12px;
letter-spacing: 0.04em;
cursor: pointer;
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.gmail-quote-toggle:hover {
background: var(--gm-bg-pill-hover);
color: var(--gm-text-strong);
border-color: var(--gm-border-strong);
}
.gmail-quote-toggle[aria-expanded="true"] {
background: var(--gm-bg-row-selected);
color: var(--gm-accent);
border-color: var(--gm-accent);
}
.gmail-thread-actions {
display: flex;
gap: 8px;

View file

@ -116,6 +116,17 @@ function escapeHtml(text: string): string {
.replace(/'/g, ''')
}
function splitPlainTextQuote(text: string): { visible: string; quoted: string | null } {
const re = /(?:^|\n)On\s+.+?\swrote:\s*(?:\n|$)/
const match = re.exec(text)
if (!match) return { visible: text, quoted: null }
const start = match.index === 0 ? 0 : match.index + 1
const visible = text.slice(0, start).trimEnd()
const quoted = text.slice(start)
if (!quoted.trim()) return { visible: text, quoted: null }
return { visible, quoted }
}
function buildEmailDocument(
html: string,
opts: { theme: 'light' | 'dark'; plainText: boolean }
@ -153,6 +164,12 @@ function buildEmailDocument(
border-left: 2px solid ${quoteBorder};
color: ${quoteColor};
}
blockquote.gmail_quote,
blockquote[type="cite"],
.email-quote-block { display: none; }
[data-show-quotes="true"] blockquote.gmail_quote,
[data-show-quotes="true"] blockquote[type="cite"],
[data-show-quotes="true"] .email-quote-block { display: block; }
</style>
</head><body>${html}</body></html>`
}
@ -164,6 +181,8 @@ function MessageBody({ message, threadId }: { message: GmailThreadMessage; threa
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const lastSavedHeightRef = useRef<number>(message.bodyHeight ?? 0)
const [height, setHeight] = useState(message.bodyHeight ?? 80)
const [hasQuote, setHasQuote] = useState(false)
const [showQuotes, setShowQuotes] = useState(false)
const isPlainText = !(message.bodyHtml && message.bodyHtml.trim())
const useDarkBody = isPlainText && resolvedTheme === 'dark'
@ -173,18 +192,21 @@ function MessageBody({ message, threadId }: { message: GmailThreadMessage; threa
return buildEmailDocument(message.bodyHtml, { theme: resolvedTheme, plainText: false })
}
const text = (message.body || '(No message body)').trim()
return buildEmailDocument(
`<pre style="white-space: pre-wrap; font: inherit; margin: 0;">${escapeHtml(text)}</pre>`,
{ theme: resolvedTheme, plainText: true },
)
const { visible, quoted } = splitPlainTextQuote(text)
const visibleBlock = `<pre style="white-space: pre-wrap; font: inherit; margin: 0;">${escapeHtml(visible)}</pre>`
const quotedBlock = quoted
? `<pre class="email-quote-block" style="white-space: pre-wrap; font: inherit; margin: 0;">${escapeHtml(quoted)}</pre>`
: ''
return buildEmailDocument(visibleBlock + quotedBlock, { theme: resolvedTheme, plainText: true })
}, [message.bodyHtml, message.body, resolvedTheme])
const handleLoad = useCallback(() => {
const iframe = iframeRef.current
const doc = iframe?.contentDocument
if (!doc?.body) return
setHasQuote(!!doc.querySelector('blockquote.gmail_quote, blockquote[type="cite"], .email-quote-block'))
const measure = () => {
const next = Math.max(40, doc.documentElement.scrollHeight)
const next = Math.max(40, doc.body.scrollHeight)
setHeight((current) => (current === next ? current : next))
if (!message.id) return
if (Math.abs(next - lastSavedHeightRef.current) < 4) return
@ -206,21 +228,43 @@ function MessageBody({ message, threadId }: { message: GmailThreadMessage; threa
}
}, [message.id, threadId])
const toggleQuotes = useCallback(() => {
setShowQuotes((prev) => {
const next = !prev
const doc = iframeRef.current?.contentDocument
if (doc) doc.documentElement.dataset.showQuotes = next ? 'true' : ''
return next
})
}, [])
useEffect(() => () => {
observerRef.current?.disconnect()
if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
}, [])
return (
<iframe
ref={iframeRef}
srcDoc={srcDoc}
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"
title="Email content"
className={cn('gmail-message-iframe', useDarkBody && 'gmail-message-iframe-dark')}
style={{ height }}
onLoad={handleLoad}
/>
<>
<iframe
ref={iframeRef}
srcDoc={srcDoc}
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"
title="Email content"
className={cn('gmail-message-iframe', useDarkBody && 'gmail-message-iframe-dark')}
style={{ height }}
onLoad={handleLoad}
/>
{hasQuote && (
<button
type="button"
className="gmail-quote-toggle"
onClick={toggleQuotes}
aria-label={showQuotes ? 'Hide quoted text' : 'Show quoted text'}
aria-expanded={showQuotes}
>
<span></span>
</button>
)}
</>
)
}
@ -309,6 +353,18 @@ function ThreadDetail({
hidden?: boolean
}) {
const [composeMode, setComposeMode] = useState<ComposeMode | null>(null)
const [expandedIndices, setExpandedIndices] = useState<Set<number>>(
() => new Set(thread.messages.length > 0 ? [thread.messages.length - 1] : [])
)
const toggleExpand = useCallback((index: number) => {
setExpandedIndices((prev) => {
const next = new Set(prev)
if (next.has(index)) next.delete(index)
else next.add(index)
return next
})
}, [])
return (
<div className={cn('gmail-detail gmail-detail-inline', hidden && 'gmail-detail-hidden')}>
@ -322,22 +378,37 @@ function ThreadDetail({
<div className="gmail-thread-body">
<div className="gmail-message-stack">
{thread.messages.map((message, index) => {
const isLast = index === thread.messages.length - 1
const isExpanded = expandedIndices.has(index)
return (
<div key={message.id || index} className={cn('gmail-message', isLast && 'gmail-message-open')}>
<div key={message.id || index} className={cn('gmail-message', isExpanded && 'gmail-message-expanded')}>
<div className="gmail-message-avatar" style={{ backgroundColor: avatarColor(message.from) }}>
{getInitial(message.from)}
</div>
<div className="gmail-message-main">
<div className="gmail-message-meta">
<div className="gmail-message-from">
<strong>{extractName(message.from)}</strong>
<span>{extractAddress(message.from)}</span>
<button
type="button"
className="gmail-message-header"
onClick={() => toggleExpand(index)}
aria-expanded={isExpanded}
>
<div className="gmail-message-meta">
<div className="gmail-message-from">
<strong>{extractName(message.from)}</strong>
{isExpanded && <span>{extractAddress(message.from)}</span>}
</div>
<div className="gmail-message-date">
{isExpanded ? formatFullDate(message.date) : formatInboxTime(message.date)}
</div>
</div>
<div className="gmail-message-date">{formatFullDate(message.date)}</div>
</div>
<div className="gmail-message-to">to {message.to || 'me'}</div>
<MessageBody message={message} threadId={thread.threadId} />
{isExpanded ? (
<div className="gmail-message-to">to {message.to || 'me'}</div>
) : (
<div className="gmail-message-snippet">{snippet(message.body)}</div>
)}
</button>
{isExpanded && (
<MessageBody message={message} threadId={thread.threadId} />
)}
</div>
</div>
)