diff --git a/apps/x/apps/renderer/src/App.css b/apps/x/apps/renderer/src/App.css index 7d46a400..05957587 100644 --- a/apps/x/apps/renderer/src/App.css +++ b/apps/x/apps/renderer/src/App.css @@ -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; diff --git a/apps/x/apps/renderer/src/components/email-view.tsx b/apps/x/apps/renderer/src/components/email-view.tsx index 9839a375..b1de48b5 100644 --- a/apps/x/apps/renderer/src/components/email-view.tsx +++ b/apps/x/apps/renderer/src/components/email-view.tsx @@ -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; } ${html}` } @@ -164,6 +181,8 @@ function MessageBody({ message, threadId }: { message: GmailThreadMessage; threa const saveTimerRef = useRef | null>(null) const lastSavedHeightRef = useRef(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( - `
${escapeHtml(text)}
`, - { theme: resolvedTheme, plainText: true }, - ) + const { visible, quoted } = splitPlainTextQuote(text) + const visibleBlock = `
${escapeHtml(visible)}
` + const quotedBlock = quoted + ? `` + : '' + 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 ( -