inbox streamlining

This commit is contained in:
debu-2 2026-10-05 22:47:56 +05:30
parent 220168abd0
commit 79188e0f59
2 changed files with 193 additions and 166 deletions

View file

@ -15,13 +15,16 @@ import {
ShieldAlert,
Trash2,
ChevronDown,
FileText,
} from "lucide-react";
import AgentSpark from "./AgentSpark";
import SenderAvatar from "./SenderAvatar";
import { addressText, senderDisplayName } from "../utils/senderAvatar";
import { attachmentCount } from "../utils/inboxSignals";
const ICON = { size: 16, strokeWidth: 1.7 };
const TOOLBAR_ICON = { size: 16, strokeWidth: 1.7 };
const ACTION_ICON = { size: 14, strokeWidth: 1.7 };
const REPLY_ICON = { size: 14, strokeWidth: 1.7 };
function joinAddresses(value) {
if (!value) return "";
@ -121,6 +124,20 @@ function ToolbarButton({ title, label, onClick, disabled, destructive, children
);
}
function MessageAction({ title, label, onClick, active, children }) {
return (
<button
type="button"
className={`mail-detail-message-action${active ? " active" : ""}`}
title={title}
aria-label={label || title}
onClick={onClick}
>
{children}
</button>
);
}
export default function MailDetail({
email,
loading,
@ -160,17 +177,17 @@ export default function MailDetail({
<header className="mail-detail-toolbar">
<div className="mail-detail-toolbar-group">
<ToolbarButton title="Back to inbox" label="Back" onClick={onClose}>
<ArrowLeft size={16} strokeWidth={1.7} />
<ArrowLeft {...TOOLBAR_ICON} />
</ToolbarButton>
</div>
{email && (
<>
<div className="mail-detail-toolbar-group">
<ToolbarButton title="Archive" onClick={onArchive} disabled={busy}>
<Archive {...ICON} />
<Archive {...TOOLBAR_ICON} />
</ToolbarButton>
<ToolbarButton title="Report spam" onClick={onSpam} disabled={busy}>
<ShieldAlert {...ICON} />
<ShieldAlert {...TOOLBAR_ICON} />
</ToolbarButton>
<ToolbarButton
title={mailbox === "trash" ? "Delete permanently" : "Delete"}
@ -178,7 +195,7 @@ export default function MailDetail({
disabled={busy}
destructive
>
<Trash2 {...ICON} />
<Trash2 {...TOOLBAR_ICON} />
</ToolbarButton>
</div>
<span className="mail-detail-toolbar-sep" aria-hidden="true" />
@ -188,22 +205,22 @@ export default function MailDetail({
onClick={() => onToggleRead(!email.is_read)}
disabled={busy}
>
{email.is_read ? <Mail {...ICON} /> : <MailOpen {...ICON} />}
{email.is_read ? <Mail {...TOOLBAR_ICON} /> : <MailOpen {...TOOLBAR_ICON} />}
</ToolbarButton>
<ToolbarButton title="Snooze" onClick={onSnooze} disabled={busy}>
<Clock {...ICON} />
<Clock {...TOOLBAR_ICON} />
</ToolbarButton>
</div>
<span className="mail-detail-toolbar-sep" aria-hidden="true" />
<div className="mail-detail-toolbar-group">
<ToolbarButton title="Move to folder" onClick={onMove} disabled={busy}>
<FolderInput {...ICON} />
<FolderInput {...TOOLBAR_ICON} />
</ToolbarButton>
<ToolbarButton title="Apply labels" onClick={onMove} disabled={busy}>
<Tag {...ICON} />
<Tag {...TOOLBAR_ICON} />
</ToolbarButton>
<ToolbarButton title="More actions" onClick={onMove} disabled={busy}>
<MoreHorizontal {...ICON} />
<MoreHorizontal {...TOOLBAR_ICON} />
</ToolbarButton>
</div>
{mailbox === "trash" && (
@ -211,7 +228,7 @@ export default function MailDetail({
<span className="mail-detail-toolbar-sep" aria-hidden="true" />
<div className="mail-detail-toolbar-group">
<ToolbarButton title="Restore from trash" onClick={onRestore} disabled={busy}>
<RotateCcw {...ICON} />
<RotateCcw {...TOOLBAR_ICON} />
</ToolbarButton>
</div>
</>
@ -225,121 +242,126 @@ export default function MailDetail({
{email && !loading && (
<div className="mail-detail-sheet">
<div className="mail-detail-content">
<div className="mail-detail-reading-column">
<div className="mail-detail-subject-row">
<h1>{email.subject?.trim() || "(No subject)"}</h1>
<div className="mail-detail-labels">
<span className="mail-detail-label-pill">{mailboxLabel(mailbox)}</span>
{email.is_starred && <span className="mail-detail-label-pill starred">Starred</span>}
</div>
<div className="mail-detail-subject-row">
<div className="mail-detail-subject-main">
<h1>{email.subject?.trim() || "(No subject)"}</h1>
<div className="mail-detail-labels">
<span className="mail-detail-label-pill">{mailboxLabel(mailbox)}</span>
{email.is_starred && <span className="mail-detail-label-pill starred">Starred</span>}
</div>
</div>
<div className="mail-detail-ai-chips">
<button type="button" className="mail-detail-ai-chip" onClick={onSummarizeThread}>
<AgentSpark /> {summarizeLabel}
</button>
<button type="button" className="mail-detail-ai-chip" onClick={onExtractActions}>
<AgentSpark /> Extract actions
</button>
{files > 0 ? (
<button type="button" className="mail-detail-ai-chip" onClick={onAnalyzeAttachment}>
<AgentSpark /> Analyze attachment
</button>
) : null}
</div>
</div>
<div className="mail-detail-ai-chips">
<button type="button" className="mail-detail-ai-chip" onClick={onSummarizeThread}>
<AgentSpark /> {summarizeLabel}
</button>
<button type="button" className="mail-detail-ai-chip" onClick={onExtractActions}>
<AgentSpark /> Extract actions
</button>
{files > 0 && (
<button type="button" className="mail-detail-ai-chip" onClick={onAnalyzeAttachment}>
<AgentSpark /> Analyze attachment
</button>
<div className="mail-detail-message-header">
<SenderAvatar email={email} />
<div className="mail-detail-sender-meta">
<div className="mail-detail-sender-line">
<strong>{sender?.name}</strong>
{sender?.address && (
<span className="mail-detail-sender-email">&lt;{sender.address}&gt;</span>
)}
</div>
<div className="mail-detail-sender-row">
<SenderAvatar email={email} />
<div className="mail-detail-sender-meta">
<div className="mail-detail-sender-line">
<strong>{sender?.name}</strong>
{sender?.address && <span className="mail-detail-sender-email">&lt;{sender.address}&gt;</span>}
</div>
<button
type="button"
className={`mail-detail-recipient-toggle${headersOpen ? " open" : ""}`}
onClick={() => setHeadersOpen((open) => !open)}
aria-expanded={headersOpen}
>
{recipientLabel(email, viewerEmail)}
<ChevronDown size={12} strokeWidth={1.75} />
</button>
{headersOpen && (
<div className="mail-detail-recipient-details">
<small>From: {joinAddresses(email.sender) || sender?.display}</small>
<small>To: {joinAddresses(email.recipients || email.to) || "Undisclosed"}</small>
{joinAddresses(email.cc) && <small>Cc: {joinAddresses(email.cc)}</small>}
{joinAddresses(email.bcc) && <small>Bcc: {joinAddresses(email.bcc)}</small>}
<small>Date: {time.absolute}{time.relative ? ` (${time.relative})` : ""}</small>
<small>Subject: {email.subject?.trim() || "(No subject)"}</small>
</div>
)}
</div>
<div className="mail-detail-sender-actions">
<time dateTime={email.created_at}>
{time.absolute}
{time.relative ? ` (${time.relative})` : ""}
</time>
<button
type="button"
className={`mail-detail-icon-btn${email.is_starred ? " active" : ""}`}
title={email.is_starred ? "Unstar" : "Star"}
aria-label={email.is_starred ? "Unstar" : "Star"}
onClick={() => onToggleStar?.(!email.is_starred)}
>
<Star size={16} strokeWidth={1.75} />
</button>
<button type="button" className="mail-detail-icon-btn" title="Reply" onClick={onReply}>
<Reply size={16} strokeWidth={1.75} />
</button>
<button type="button" className="mail-detail-icon-btn" title="More actions" onClick={onMove}>
<MoreHorizontal size={16} strokeWidth={1.75} />
</button>
</div>
</div>
<div className={`mail-detail-body${body?.mode === "html" ? " is-html" : " is-text"}`}>
{body?.mode === "html" ? (
<div className="mail-detail-body-html" dangerouslySetInnerHTML={{ __html: body.value }} />
) : (
<div className="mail-detail-body-text">{body?.value}</div>
)}
</div>
{attachments.length > 0 && (
<div className="mail-detail-attachments" aria-label="Attachments">
{attachments.map((file) => (
<div key={`${file.filename}-${file.size}`} className="mail-detail-attachment">
<span className="mail-detail-attachment-icon">📄</span>
<div>
<strong>{file.filename}</strong>
<small>{file.content_type || "File"} · {formatFileSize(file.size)}</small>
</div>
<div className="mail-detail-attachment-actions">
<button type="button">Preview</button>
<button type="button">Download</button>
</div>
</div>
))}
<button
type="button"
className={`mail-detail-recipient-toggle${headersOpen ? " open" : ""}`}
onClick={() => setHeadersOpen((open) => !open)}
aria-expanded={headersOpen}
>
{recipientLabel(email, viewerEmail)}
<ChevronDown size={12} strokeWidth={1.75} />
</button>
{headersOpen && (
<div className="mail-detail-recipient-details">
<small>From: {joinAddresses(email.sender) || sender?.display}</small>
<small>To: {joinAddresses(email.recipients || email.to) || "Undisclosed"}</small>
{joinAddresses(email.cc) ? <small>Cc: {joinAddresses(email.cc)}</small> : null}
{joinAddresses(email.bcc) ? <small>Bcc: {joinAddresses(email.bcc)}</small> : null}
<small>Date: {time.absolute}{time.relative ? ` (${time.relative})` : ""}</small>
<small>Subject: {email.subject?.trim() || "(No subject)"}</small>
</div>
)}
</div>
<footer className="mail-detail-reply-bar">
<button type="button" className="mail-detail-reply-btn" onClick={onReply}>
<Reply size={15} strokeWidth={1.75} /> Reply
</button>
{showReplyAll ? (
<button type="button" className="mail-detail-reply-btn subtle" onClick={onReply}>
Reply all
</button>
) : null}
<button type="button" className="mail-detail-reply-btn subtle" onClick={onForward}>
<Forward size={15} strokeWidth={1.75} /> Forward
</button>
</footer>
<div className="mail-detail-message-header-actions">
<time dateTime={email.created_at}>
{time.absolute}
{time.relative ? ` (${time.relative})` : ""}
</time>
<MessageAction
title={email.is_starred ? "Unstar" : "Star"}
label={email.is_starred ? "Unstar" : "Star"}
active={email.is_starred}
onClick={() => onToggleStar?.(!email.is_starred)}
>
<Star {...ACTION_ICON} />
</MessageAction>
<MessageAction title="Reply" onClick={onReply}>
<Reply {...ACTION_ICON} />
</MessageAction>
<MessageAction title="More actions" onClick={onMove}>
<MoreHorizontal {...ACTION_ICON} />
</MessageAction>
</div>
</div>
<div className={`mail-detail-message-content${body?.mode === "html" ? " is-html" : ""}`}>
<div className={`mail-detail-body${body?.mode === "html" ? " is-html" : " is-text"}`}>
{body?.mode === "html" ? (
<div className="mail-detail-body-html" dangerouslySetInnerHTML={{ __html: body.value }} />
) : (
<div className="mail-detail-body-text">{body?.value}</div>
)}
</div>
{attachments.length > 0 && (
<div className="mail-detail-attachments" aria-label="Attachments">
{attachments.map((file) => (
<div key={`${file.filename}-${file.size}`} className="mail-detail-attachment">
<span className="mail-detail-attachment-icon" aria-hidden="true">
<FileText size={18} strokeWidth={1.7} />
</span>
<div>
<strong>{file.filename}</strong>
<small>{file.content_type || "File"} · {formatFileSize(file.size)}</small>
</div>
<div className="mail-detail-attachment-actions">
<button type="button">Preview</button>
<button type="button">Download</button>
</div>
</div>
))}
</div>
)}
</div>
<footer className="mail-detail-message-footer">
<div className="mail-detail-reply-bar">
{showReplyAll ? (
<button type="button" className="mail-detail-reply-btn" onClick={onReply}>
<Reply {...REPLY_ICON} /> Reply all
</button>
) : (
<button type="button" className="mail-detail-reply-btn" onClick={onReply}>
<Reply {...REPLY_ICON} /> Reply
</button>
)}
<button type="button" className="mail-detail-reply-btn subtle" onClick={onForward}>
<Forward {...REPLY_ICON} /> Forward
</button>
</div>
</footer>
</div>
)}
</div>