- 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.
215 lines
5.9 KiB
TypeScript
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>
|
|
)
|
|
}
|