From 9356edb84a92c1a65aa6dc09fc0b25285eb267b9 Mon Sep 17 00:00:00 2001 From: Gagancreates Date: Tue, 5 May 2026 00:37:57 +0530 Subject: [PATCH] feat: restyle email block with Gmail-style layout and avatar --- .../renderer/src/extensions/email-block.tsx | 316 +++++++++++------ apps/x/apps/renderer/src/styles/editor.css | 331 +++++++++++------- apps/x/pnpm-lock.yaml | 141 +++++--- 3 files changed, 511 insertions(+), 277 deletions(-) diff --git a/apps/x/apps/renderer/src/extensions/email-block.tsx b/apps/x/apps/renderer/src/extensions/email-block.tsx index 7356c94c..da39fc30 100644 --- a/apps/x/apps/renderer/src/extensions/email-block.tsx +++ b/apps/x/apps/renderer/src/extensions/email-block.tsx @@ -1,6 +1,6 @@ import { mergeAttributes, Node } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' -import { X, Mail, ChevronDown, ExternalLink, Copy, Check, MessageSquare } from 'lucide-react' +import { X, ExternalLink, Copy, Check, MessageSquare, ChevronDown } from 'lucide-react' import { blocks } from '@x/shared' import { useState, useEffect, useRef, useCallback } from 'react' import { useTheme } from '@/contexts/theme-context' @@ -11,17 +11,57 @@ function formatEmailDate(dateStr: string): string { try { const d = new Date(dateStr) if (isNaN(d.getTime())) return dateStr - return d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }) + - ' ' + d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) + const now = new Date() + const isToday = d.getDate() === now.getDate() && d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear() + if (isToday) return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) + return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) } catch { return dateStr } } -/** Extract just the name part from "Name " format */ -function senderFirstName(from: string): string { - const name = from.replace(/<.*>/, '').trim() - return name.split(/\s+/)[0] || name +function formatFullDate(dateStr: string): string { + try { + const d = new Date(dateStr) + if (isNaN(d.getTime())) return dateStr + return d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }) + + ', ' + d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) + } catch { + return dateStr + } +} + +/** Extract display name from "Name " or plain email */ +function extractName(from: string): string { + const match = from.match(/^([^<]+)>> 0 + return GMAIL_AVATAR_COLORS[hash % GMAIL_AVATAR_COLORS.length] } declare global { @@ -51,13 +91,11 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { const { resolvedTheme } = useTheme() - // Local draft state for editing const [draftBody, setDraftBody] = useState(config?.draft_response || '') - const [emailExpanded, setEmailExpanded] = useState(false) + const [expanded, setExpanded] = useState(false) const [copied, setCopied] = useState(false) const bodyRef = useRef(null) - // Sync draft from external changes useEffect(() => { try { const parsed = blocks.EmailBlockSchema.parse(JSON.parse(raw)) @@ -65,7 +103,6 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { } catch { /* ignore */ } }, [raw]) - // Auto-resize textarea useEffect(() => { if (bodyRef.current) { bodyRef.current.style.height = 'auto' @@ -102,7 +139,6 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { return (
- Invalid email block
@@ -113,11 +149,13 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { ? `https://mail.google.com/mail/u/0/#all/${config.threadId}` : null - // Build summary: use explicit summary, or auto-generate from sender + subject - const summary = config.summary - || (config.from && config.subject - ? `${senderFirstName(config.from)} reached out about ${config.subject}` - : config.subject || 'New email') + const senderName = config.from ? extractName(config.from) : 'Unknown' + const initial = config.from ? getInitial(config.from) : '?' + const color = config.from ? avatarColor(config.from) : '#5f6368' + + // Snippet: use summary if present, else truncate latest_email + const snippet = config.summary + || (config.latest_email ? config.latest_email.slice(0, 120).replace(/\s+/g, ' ').trim() : '') return ( @@ -126,110 +164,168 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { - {/* Header: Email badge */} -
- - Email -
- - {/* Summary */} -
{summary}
- - {/* Expandable email details */} - + {/* Avatar */} + - {emailExpanded && ( -
-
-
-
-
-
{config.from || 'Unknown'}
- {config.date &&
{formatEmailDate(config.date)}
} -
- {config.subject &&
Subject: {config.subject}
} + {/* Content */} +
+
+ {senderName} + {config.date && ( + {formatEmailDate(config.date)} + )} +
+
+ {config.subject && ( + {config.subject} + )} + {snippet && ( + + {config.subject ? ` — ${snippet}` : snippet} + + )} +
+
+ + {/* Chevron */} + +
+ + {/* Expanded email detail */} + {expanded && ( +
+ {/* Subject heading */} + {config.subject && ( +
{config.subject}
+ )} + + {/* Metadata strip */} +
+
+ {initial} +
+
+
{config.from || 'Unknown'}
+
+ {config.to && to {config.to}} + {config.date && {formatFullDate(config.date)}}
-
{config.latest_email}
+ {gmailUrl && ( + + )}
+ + {/* Email body */} +
{config.latest_email}
+ + {/* Earlier conversation */} {hasPastSummary && ( -
-
Earlier conversation
-
{config.past_summary}
+
+
Earlier conversation
+
{config.past_summary}
+
+ )} + + {/* Draft compose area */} + {hasDraft && ( +
+
+ Reply + {config.from && ( + {config.from} + )} +
+