Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Move mail, compose, contacts, and accounts off mocks onto REST + WS. Add client, auth store, IDB-backed query cache, offline queue, and sync bar; hybrid Zustand for UI-only state. Settings still local until backend has preferences API.
173 lines
5.3 KiB
TypeScript
173 lines
5.3 KiB
TypeScript
"use client"
|
|
|
|
import { Star, Info } from "lucide-react"
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip"
|
|
import { cn } from "@/lib/utils"
|
|
import {
|
|
avatarColor,
|
|
cleanSenderName,
|
|
senderInitial,
|
|
} from "@/lib/sender-display"
|
|
import { MailDateText } from "@/components/gmail/mail-date-text"
|
|
import type { ApiMessageFull } from "@/lib/api/types"
|
|
import type { EmailAttachment } from "@/lib/email-data"
|
|
import { ContactHoverCard } from "@/components/gmail/contact-hover-card"
|
|
import { EmailViewMessageToolbar } from "@/components/gmail/email-view/email-view-toolbar"
|
|
import { SandboxedContent } from "@/components/gmail/email-view/sandboxed-content"
|
|
import { MessageAttachmentsSection } from "@/components/gmail/email-view/message-attachments"
|
|
import {
|
|
MAIL_MESSAGE_HOVER_CLASS,
|
|
MAIL_TOOLTIP_CONTENT_CLASS,
|
|
} from "@/lib/mail-chrome-classes"
|
|
|
|
export function CollapsedMessage({
|
|
message,
|
|
onClick,
|
|
}: {
|
|
message: ApiMessageFull
|
|
onClick: () => void
|
|
}) {
|
|
const senderName = message.from[0]?.name ?? ""
|
|
const senderAddr = message.from[0]?.address ?? ""
|
|
const name = cleanSenderName(senderName)
|
|
const color = avatarColor(name)
|
|
|
|
return (
|
|
<div
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={onClick}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault()
|
|
onClick()
|
|
}
|
|
}}
|
|
className={cn("group flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-left transition-colors", MAIL_MESSAGE_HOVER_CLASS)}
|
|
>
|
|
<div
|
|
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-bold text-white"
|
|
style={{ backgroundColor: color }}
|
|
>
|
|
{senderInitial(name)}
|
|
</div>
|
|
<div className="min-w-0 flex-1 flex flex-col gap-1" data-selectable-text>
|
|
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
<ContactHoverCard displayName={senderName} email={senderAddr} className="min-w-0">
|
|
<span className="truncate text-sm font-semibold text-foreground">{name}</span>
|
|
</ContactHoverCard>
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
<MailDateText
|
|
iso={message.date}
|
|
variant="preview"
|
|
className="text-xs text-muted-foreground"
|
|
/>
|
|
<Star
|
|
strokeWidth={1.25}
|
|
className="ml-1 size-4 fill-transparent stroke-[#c2c2c2]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="min-w-0 truncate text-sm leading-snug text-muted-foreground">{message.snippet}</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ExpandedMessage({
|
|
sender,
|
|
senderEmail,
|
|
dateIso,
|
|
body,
|
|
isSpam,
|
|
isLast,
|
|
starred,
|
|
attachments = [],
|
|
onToggleStar,
|
|
onCollapse,
|
|
onPrintConversation,
|
|
}: {
|
|
sender: string
|
|
senderEmail: string
|
|
dateIso: string
|
|
body: string
|
|
isSpam: boolean
|
|
isLast: boolean
|
|
starred: boolean
|
|
attachments?: EmailAttachment[]
|
|
onToggleStar?: () => void
|
|
onCollapse?: () => void
|
|
onPrintConversation?: () => void
|
|
}) {
|
|
return (
|
|
<div>
|
|
<EmailViewMessageToolbar
|
|
sender={sender}
|
|
senderEmail={senderEmail}
|
|
dateIso={dateIso}
|
|
isSpam={isSpam}
|
|
isLast={isLast}
|
|
starred={starred}
|
|
onToggleStar={onToggleStar}
|
|
onCollapse={onCollapse}
|
|
onPrintConversation={onPrintConversation}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
"px-4 pl-[68px] max-sm:pl-4 max-sm:pr-4",
|
|
attachments.length > 0 ? "pb-0" : "pb-4"
|
|
)}
|
|
data-selectable-text
|
|
>
|
|
<SandboxedContent html={body} isSpam={isSpam} />
|
|
</div>
|
|
|
|
{attachments.length > 0 && (
|
|
<MessageAttachmentsSection attachments={attachments} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function SpamWhyBanner({ onNotSpam }: { onNotSpam?: () => void }) {
|
|
return (
|
|
<div className="mx-6 mb-4 flex items-start gap-3 rounded-lg border border-border bg-muted px-4 py-3.5 max-sm:mx-4">
|
|
<div className="min-w-0 flex-1 space-y-3">
|
|
<p className="text-sm leading-snug text-foreground/80">
|
|
<span className="font-medium text-foreground">Pourquoi ce message est-il dans le spam ?</span>{" "}
|
|
Ce message est semblable à des messages identifiés comme spam par le passé.
|
|
</p>
|
|
{onNotSpam && (
|
|
<button
|
|
type="button"
|
|
onClick={onNotSpam}
|
|
className="rounded-md border border-border bg-mail-surface px-4 py-2 text-sm font-medium text-primary shadow-sm transition-colors hover:bg-accent"
|
|
>
|
|
Signaler comme non-spam
|
|
</button>
|
|
)}
|
|
</div>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent"
|
|
aria-label="En savoir plus sur le filtre anti-spam"
|
|
>
|
|
<Info className="h-[18px] w-[18px]" strokeWidth={1.75} />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="left" className={cn(MAIL_TOOLTIP_CONTENT_CLASS, "max-w-xs text-xs")}>
|
|
Les filtres peuvent se tromper. Si le message est légitime, signalez-le comme non-spam pour
|
|
l'améliorer.
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
)
|
|
}
|