"use client"; import Image from "next/image"; import { Check, ExternalLink, FileText, ImagePlus, Link2, Loader2, MessageSquare, Play, } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { apiClient } from "@/lib/api"; import { displayUserName } from "@/lib/shared"; import type { TaskAttachment, TaskCover } from "@/types/api"; function isImage(attachment: TaskAttachment): boolean { if (attachment.mime_type?.toLowerCase().startsWith("image/")) return true; return /\.(avif|bmp|gif|heic|heif|jpe?g|png|webp)$/i.test(attachment.file_name); } function isVideo(attachment: TaskAttachment): boolean { if (attachment.mime_type?.toLowerCase().startsWith("video/")) return true; return /\.(m4v|mkv|mov|mp4|mpeg|mpg|ogv|webm)$/i.test(attachment.file_name); } const coverVisibilityCallbacks = new Map void>(); let coverVisibilityObserver: IntersectionObserver | null = null; function observeCoverVisibility(element: Element, onVisible: () => void) { if (typeof IntersectionObserver === "undefined") { onVisible(); return () => undefined; } if (!coverVisibilityObserver) { coverVisibilityObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; const callback = coverVisibilityCallbacks.get(entry.target); if (!callback) return; coverVisibilityCallbacks.delete(entry.target); coverVisibilityObserver?.unobserve(entry.target); callback(); }); }, { rootMargin: "900px 700px", threshold: 0.01, }); } coverVisibilityCallbacks.set(element, onVisible); coverVisibilityObserver.observe(element); return () => { coverVisibilityCallbacks.delete(element); coverVisibilityObserver?.unobserve(element); if (coverVisibilityCallbacks.size === 0) { coverVisibilityObserver?.disconnect(); coverVisibilityObserver = null; } }; } function AuthenticatedCoverMedia({ taskId, attachment, }: { taskId: number; attachment: TaskAttachment; }) { const [source, setSource] = useState(null); const [sourceIsOriginal, setSourceIsOriginal] = useState(false); const [failed, setFailed] = useState(false); const video = isVideo(attachment); const attachmentId = attachment.id; const thumbnailUrl = attachment.thumbnail_url; const downloadUrl = attachment.download_url; const fileName = attachment.file_name; useEffect(() => { let active = true; let objectUrl: string | null = null; void apiClient .downloadTaskAttachmentThumbnail(taskId, { id: attachmentId, thumbnail_url: thumbnailUrl, }) .then((blob) => ({ blob, original: false })) .catch(async () => { const { blob } = await apiClient.downloadTaskAttachment(taskId, { id: attachmentId, download_url: downloadUrl, file_name: fileName, }); return { blob, original: true }; }) .then(({ blob, original }) => { if (!active) return; objectUrl = URL.createObjectURL(blob); setSourceIsOriginal(original); setSource(objectUrl); }) .catch(() => { if (active) setFailed(true); }); return () => { active = false; if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [attachmentId, downloadUrl, fileName, taskId, thumbnailUrl]); return (
{source ? ( <> {video && sourceIsOriginal ? (
); } function LazyAuthenticatedCoverMedia({ taskId, attachment, }: { taskId: number; attachment: TaskAttachment; }) { const containerRef = useRef(null); const [shouldLoad, setShouldLoad] = useState(false); useEffect(() => { const container = containerRef.current; if (!container || shouldLoad) return; return observeCoverVisibility(container, () => setShouldLoad(true)); }, [shouldLoad]); return (
{shouldLoad ? ( ) : null}
); } export default function TaskCoverPreview({ taskId, cover, className = "", }: { taskId: number; cover: TaskCover; className?: string; }) { const commentMedia = useMemo( () => cover.comment?.attachments.find((attachment) => ( isImage(attachment) || isVideo(attachment) )), [cover.comment?.attachments], ); const media = cover.kind === "attachment" ? cover.attachment : commentMedia; if (media) { return (
); } if (cover.kind === "checklist" && cover.checklist) { const { completed, items, total } = cover.checklist; if (total <= 0 || items.length === 0) return null; const progress = Math.round((completed / total) * 100); return (
{completed}/{total}
{items.map((item) => (
{item.is_completed ? : null} {item.title}
))}
); } if (cover.kind === "comment" && cover.comment) { return (
{displayUserName(cover.comment.author)}

{cover.comment.text || "Комментарий с вложением"}

); } if (cover.kind === "external_link" && cover.external_link) { let hostname = cover.external_link.url; try { hostname = new URL(cover.external_link.url).hostname; } catch { // Keep the original URL when it cannot be parsed by the browser. } return (
{cover.external_link.title || hostname}

{hostname}

{cover.external_link.url}

); } if (cover.kind === "linked_object" && cover.linked_object) { return (
{cover.linked_object.kind_display}

{cover.linked_object.title}

{cover.linked_object.description ? (

{cover.linked_object.description}

) : null}
); } return (
); }