ultisuite-client/components/gmail/email-view/mail-attachment-thumbnail.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- 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.
2026-06-07 15:49:21 +02:00

215 lines
5.9 KiB
TypeScript

"use client"
import { useEffect, useRef, useState } from "react"
import { useQueryClient } from "@tanstack/react-query"
import { File, FileText, Image as ImageIcon, Play } from "lucide-react"
import type { EmailAttachment, EmailAttachmentKind } from "@/lib/email-data"
import { resolveAttachmentKind } from "@/lib/attachment-display"
import {
mailAttachmentCanThumb,
useMailAttachmentThumb,
} from "@/lib/api/hooks/use-mail-attachment-thumb"
import { cn } from "@/lib/utils"
function useInView(rootMargin = "120px") {
const ref = useRef<HTMLDivElement>(null)
const [inView, setInView] = useState(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setInView(true)
observer.disconnect()
}
},
{ rootMargin }
)
observer.observe(el)
return () => observer.disconnect()
}, [rootMargin])
return { ref, inView }
}
function AttachmentKindFallback({ kind }: { kind: EmailAttachmentKind }) {
if (kind === "image") {
return <ImageIcon className="size-11 text-[#9aa0a6]" strokeWidth={1.25} aria-hidden />
}
if (kind === "pdf") {
return (
<div
className="rounded border border-border bg-mail-surface px-4 py-5 shadow-sm"
aria-hidden
>
<span className="text-[11px] font-bold leading-none text-[#d93025]">PDF</span>
</div>
)
}
return <File className="size-11 text-[#9aa0a6]" strokeWidth={1.25} aria-hidden />
}
function ThumbMedia({
url,
display,
onError,
}: {
url: string
display: "image" | "video"
onError: () => void
}) {
if (display === "video") {
return (
<>
<video
src={url}
muted
playsInline
preload="metadata"
className="h-full w-full object-cover"
onLoadedData={(e) => {
const v = e.currentTarget
if (v.currentTime === 0) v.currentTime = 0.1
}}
onError={onError}
/>
<div
className="pointer-events-none absolute inset-0 flex items-center justify-center bg-black/15"
aria-hidden
>
<div className="flex size-9 items-center justify-center rounded-full bg-black/50 shadow-md">
<Play className="ml-0.5 size-4 fill-white text-white" />
</div>
</div>
</>
)
}
return (
<img
src={url}
alt=""
className="h-full w-full object-cover"
draggable={false}
onError={onError}
/>
)
}
export function MailAttachmentThumbnail({
attachment,
className,
}: {
attachment: EmailAttachment
className?: string
}) {
const kind = resolveAttachmentKind(attachment.name, attachment.kind)
const { ref, inView } = useInView()
const [failed, setFailed] = useState(false)
const retriedRef = useRef(false)
const queryClient = useQueryClient()
const canThumb = mailAttachmentCanThumb(attachment)
const showThumb = canThumb && !failed
const { data, isLoading } = useMailAttachmentThumb(attachment, inView && showThumb)
useEffect(() => {
retriedRef.current = false
setFailed(false)
}, [attachment.id, attachment.drivePath, attachment.name])
const handleError = () => {
if (!retriedRef.current && data?.url.startsWith("blob:")) {
retriedRef.current = true
void queryClient.invalidateQueries({
queryKey: ["mail", "attachment-thumb", attachment.id],
})
return
}
setFailed(true)
}
const showFallback = !showThumb || failed || (!data && !isLoading)
return (
<div
ref={ref}
className={cn(
"relative flex h-[132px] shrink-0 flex-col items-center justify-center overflow-hidden bg-linear-to-b from-muted to-muted/70 dark:from-[#3c4043] dark:to-[#303134]",
className
)}
>
{showFallback ? (
<div className={cn("flex items-center justify-center", isLoading && showThumb && "opacity-40")}>
<AttachmentKindFallback kind={kind} />
</div>
) : null}
{showThumb && isLoading ? (
<div className="absolute inset-0 animate-pulse bg-muted/80" aria-hidden />
) : null}
{showThumb && data ? (
<div className="absolute inset-0 overflow-hidden bg-[#e8eaed] dark:bg-[#303134]">
<ThumbMedia url={data.url} display={data.display} onError={handleError} />
</div>
) : null}
</div>
)
}
export function MailAttachmentPillThumb({
attachment,
className,
}: {
attachment: EmailAttachment
className?: string
}) {
const kind = resolveAttachmentKind(attachment.name, attachment.kind)
const { ref, inView } = useInView("80px")
const [failed, setFailed] = useState(false)
const canThumb = mailAttachmentCanThumb(attachment) && kind === "image"
const { data, isLoading } = useMailAttachmentThumb(attachment, inView && canThumb && !failed)
if (!canThumb || failed) {
if (kind === "pdf") {
return <FileText className={cn("size-4 shrink-0 text-[#d93025]", className)} strokeWidth={1.5} aria-hidden />
}
if (kind === "image") {
return (
<ImageIcon
className={cn(
"size-4 shrink-0 text-muted-foreground [&_circle]:fill-none [&_path]:fill-none [&_path]:stroke-current [&_rect]:fill-current [&_rect]:opacity-[0.32]",
className
)}
strokeWidth={1.5}
aria-hidden
/>
)
}
return <File className={cn("size-4 shrink-0 text-[#5f6368]", className)} strokeWidth={1.5} aria-hidden />
}
return (
<span
ref={ref}
className={cn(
"relative flex size-6 shrink-0 overflow-hidden rounded-full bg-muted",
isLoading && "animate-pulse",
className
)}
>
{data ? (
<img
src={data.url}
alt=""
className="size-full object-cover"
draggable={false}
onError={() => setFailed(true)}
/>
) : null}
</span>
)
}