- Updated .env.example to include configuration for OnlyOffice Document Server. - Modified the workspace configuration to remove the drive-suite path. - Adjusted TypeScript environment imports for consistency. - Enhanced Next.js configuration to disable canvas in Webpack. - Updated package.json to include new dependencies for OnlyOffice and PDF.js. - Added global styles for OnlyOffice theme integration in the CSS. - Created new layout and page components for the Drive feature, including public sharing and editing functionalities. - Updated metadata handling across various layouts to reflect the new app structure.
372 lines
11 KiB
TypeScript
372 lines
11 KiB
TypeScript
"use client"
|
||
|
||
import { useMemo, useState } from "react"
|
||
import { Star, Info, Paperclip } 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 n’a 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,
|
||
attachments = [],
|
||
}: {
|
||
message: ApiMessageFull
|
||
isExpanded: boolean
|
||
onToggle: () => void
|
||
onPrintConversation?: () => void
|
||
onReply?: () => void
|
||
onForward?: () => void
|
||
selfEmails: string[]
|
||
selfDisplayName?: string
|
||
collapseQuotedReplies?: boolean
|
||
attachments?: EmailAttachment[]
|
||
}) {
|
||
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}
|
||
attachmentCount={
|
||
attachments.length > 0
|
||
? attachments.length
|
||
: message.has_attachments
|
||
? 1
|
||
: 0
|
||
}
|
||
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 ?? [])}
|
||
attachments={attachments}
|
||
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,
|
||
attachmentCount = 0,
|
||
onClick,
|
||
}: {
|
||
message: ApiMessageFull
|
||
senderName?: string
|
||
senderEmail?: string
|
||
attachmentCount?: number
|
||
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">
|
||
{attachmentCount > 0 ? (
|
||
<span
|
||
className="flex items-center gap-0.5 text-xs text-muted-foreground"
|
||
title={
|
||
attachmentCount === 1
|
||
? "Une pièce jointe"
|
||
: `${attachmentCount} pièces jointes`
|
||
}
|
||
>
|
||
<Paperclip className="size-3.5 shrink-0" strokeWidth={1.75} aria-hidden />
|
||
{attachmentCount > 1 ? <span>{attachmentCount}</span> : null}
|
||
</span>
|
||
) : null}
|
||
<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 messageId={messageId} 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>
|
||
)
|
||
}
|