ultisuite-client/components/gmail/email-view/email-view-messages.tsx
R3D347HR4Y 8a02c10ba3 Add environment configuration and update email view components
- Created a .cursorignore file to manage local environment files.
- Updated .env.example to reflect changes in the public app URL.
- Modified the gmail workspace configuration to include the drive-suite path.
- Enhanced email view components to support attachment handling and fallback for plain text bodies.
- Improved user experience by updating attachment display logic and integrating inline attachment support.
2026-06-04 00:12:43 +02:00

347 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useMemo, useState } from "react"
import { Star, Info } from "lucide-react"
import { useMessage } from "@/lib/api/hooks/use-mail-queries"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { mailFlagIsStarred, messageIsSpam } from "@/lib/mail-flags"
import {
avatarColor,
cleanSenderName,
senderInitial,
} from "@/lib/sender-display"
import { MailDateText } from "@/components/gmail/mail-date-text"
import type { ApiMessageFull, Recipient } from "@/lib/api/types"
import { resolveMessageFrom } from "@/lib/mail-message-participants"
import {
buildMessageHeaderDetails,
type MessageHeaderDetails,
} from "@/lib/mail-message-header-details"
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 { MessageBodyContent } from "@/components/gmail/email-view/message-body-content"
import { MessageAttachmentsSection } from "@/components/gmail/email-view/message-attachments"
import {
MAIL_MESSAGE_HOVER_CLASS,
MAIL_TOOLTIP_CONTENT_CLASS,
} from "@/lib/mail-chrome-classes"
import { repairMimeBodies } from "@/lib/mail-mime-body"
import { plainTextToDisplayHtml } from "@/lib/mail-plain-text-html"
export function plainTextBodyFallback(
full: { body_text?: string; body_html?: string } | null | undefined
): string | undefined {
const { bodyText } = repairMimeBodies(full?.body_text, full?.body_html)
const t = bodyText?.trim()
return t || undefined
}
export function formatApiMessageBody(
full: { body_html?: string; body_text?: string } | null | undefined,
snippet: string | undefined,
loading: boolean
): string {
const snippetHtml = snippet?.trim()
? `<p style="color:var(--muted-foreground);">${snippet.trim()}</p>`
: ""
if (loading) {
return snippetHtml
}
const repaired = repairMimeBodies(full?.body_text, full?.body_html)
const html = repaired.bodyHtml?.trim()
if (html) return html
const text = repaired.bodyText?.trim()
if (text) {
return plainTextToDisplayHtml(text)
}
if (full) {
const s = snippet?.trim()
if (s) {
return `<p style="color:var(--muted-foreground);">${s}</p>`
}
return `<p style="color:var(--muted-foreground);">Ce message na pas de contenu.</p>`
}
const s = snippet?.trim()
return s
? `<p style="color:var(--muted-foreground);">${s}</p>`
: ""
}
/** Prior message in a thread: loads full body on expand via GET /mail/messages/:id. */
export function ThreadPriorMessage({
message,
isExpanded,
onToggle,
onPrintConversation,
onReply,
onForward,
selfEmails,
selfDisplayName,
collapseQuotedReplies = false,
}: {
message: ApiMessageFull
isExpanded: boolean
onToggle: () => void
onPrintConversation?: () => void
onReply?: () => void
onForward?: () => void
selfEmails: string[]
selfDisplayName?: string
collapseQuotedReplies?: boolean
}) {
const [detailsOpen, setDetailsOpen] = useState(false)
const loadFull = isExpanded || detailsOpen
const { data: fullMessage, isPending } = useMessage(loadFull ? message.id : null)
const merged = fullMessage ?? message
const resolved = useMemo(
() =>
resolveMessageFrom(merged.from, { selfEmails, selfDisplayName }),
[merged.from, selfEmails, selfDisplayName]
)
const headerDetails = useMemo(
() =>
buildMessageHeaderDetails(merged, {
selfEmails,
selfDisplayName,
subject: message.subject,
}),
[merged, selfEmails, selfDisplayName, message.subject]
)
const body = useMemo(
() =>
formatApiMessageBody(
fullMessage,
message.snippet,
isExpanded && isPending && !fullMessage
),
[fullMessage, message.snippet, isExpanded, isPending]
)
const plainTextFallback = useMemo(
() => plainTextBodyFallback(fullMessage),
[fullMessage]
)
const isSpam = messageIsSpam(merged.flags, merged.labels)
if (!isExpanded) {
return (
<CollapsedMessage
message={message}
senderName={resolved.name}
senderEmail={resolved.email}
onClick={onToggle}
/>
)
}
return (
<ExpandedMessage
sender={resolved.name}
senderEmail={resolved.email}
headerDetails={headerDetails}
dateIso={message.date}
body={body}
isSpam={isSpam}
isLast={false}
starred={mailFlagIsStarred(message.flags ?? [])}
onCollapse={onToggle}
onPrintConversation={onPrintConversation}
onReply={onReply}
onForward={onForward}
detailsOpen={detailsOpen}
onDetailsOpenChange={setDetailsOpen}
collapseQuotedReplies={collapseQuotedReplies}
messageId={message.id}
plainTextFallback={plainTextFallback}
/>
)
}
export function CollapsedMessage({
message,
senderName: senderNameProp,
senderEmail: senderEmailProp,
onClick,
}: {
message: ApiMessageFull
senderName?: string
senderEmail?: string
onClick: () => void
}) {
const senderName = senderNameProp ?? message.from[0]?.name ?? ""
const senderAddr = senderEmailProp ?? message.from[0]?.address ?? ""
const name = cleanSenderName(senderName || senderAddr)
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,
headerDetails,
dateIso,
body,
isSpam,
isLast,
starred,
attachments = [],
onToggleStar,
onCollapse,
onPrintConversation,
onReply,
onForward,
detailsOpen,
onDetailsOpenChange,
collapseQuotedReplies = false,
messageId,
plainTextFallback,
}: {
sender: string
senderEmail: string
headerDetails: MessageHeaderDetails
messageId: string
dateIso: string
body: string
isSpam: boolean
isLast: boolean
starred: boolean
attachments?: EmailAttachment[]
onToggleStar?: () => void
onCollapse?: () => void
onPrintConversation?: () => void
onReply?: () => void
onForward?: () => void
detailsOpen?: boolean
onDetailsOpenChange?: (open: boolean) => void
collapseQuotedReplies?: boolean
plainTextFallback?: string
}) {
return (
<div>
<EmailViewMessageToolbar
sender={sender}
senderEmail={senderEmail}
headerDetails={headerDetails}
dateIso={dateIso}
isSpam={isSpam}
isLast={isLast}
starred={starred}
onToggleStar={onToggleStar}
onCollapse={onCollapse}
onPrintConversation={onPrintConversation}
onReply={onReply}
onForward={onForward}
detailsOpen={detailsOpen}
onDetailsOpenChange={onDetailsOpenChange}
messageId={messageId}
/>
<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
>
<MessageBodyContent
html={body}
isSpam={isSpam}
senderEmail={senderEmail}
messageId={messageId}
collapseQuotedReplies={collapseQuotedReplies}
plainTextFallback={plainTextFallback}
/>
</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&apos;améliorer.
</TooltipContent>
</Tooltip>
</div>
)
}