import { useState, useCallback, useRef, useEffect } from "react"; import { createPortal } from "react-dom"; import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined"; import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined"; import type { FileId } from "@app/types/file"; import { useIndexedDB } from "@app/contexts/IndexedDBContext"; import { generateThumbnailForFile } from "@app/utils/thumbnailUtils"; import "@app/components/shared/FileSidebarFileItem.css"; const THUMBNAIL_SIZE_LIMIT = 100 * 1024 * 1024; // 100MB /** Generate + persist a thumbnail for a sidebar file that doesn't have one yet. */ function useLazyThumbnail( fileId: FileId, size: number, thumbnailUrl?: string, ): string | undefined { const [thumb, setThumb] = useState(thumbnailUrl); const attempted = useRef(false); const indexedDB = useIndexedDB(); // Sync prop changes (e.g. thumbnail arrives after TTL bump) useEffect(() => { if (thumbnailUrl) setThumb(thumbnailUrl); }, [thumbnailUrl]); useEffect(() => { if (thumbnailUrl || attempted.current || size >= THUMBNAIL_SIZE_LIMIT) return; attempted.current = true; let cancelled = false; (async () => { try { const file = await indexedDB.loadFile(fileId); if (!file || cancelled) return; const thumbnail = await generateThumbnailForFile(file); if (cancelled || !thumbnail) return; setThumb(thumbnail); void indexedDB.updateThumbnail(fileId, thumbnail); } catch { // non-critical } })(); return () => { cancelled = true; }; }, [fileId, size, thumbnailUrl, indexedDB]); return thumb; } export function getFileExtension(name: string): string { const parts = name.split("."); return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : ""; } export type DateGroup = | "today" | "yesterday" | "thisWeek" | "thisMonth" | "older"; export const DATE_GROUP_ORDER: DateGroup[] = [ "today", "yesterday", "thisWeek", "thisMonth", "older", ]; export function getDateGroup(lastModified: number | undefined): DateGroup { if (!lastModified) return "older"; const now = new Date(); const today = new Date( now.getFullYear(), now.getMonth(), now.getDate(), ).getTime(); const d = new Date(lastModified); const fileDay = new Date( d.getFullYear(), d.getMonth(), d.getDate(), ).getTime(); const daysAgo = (today - fileDay) / 86400000; if (daysAgo < 1) return "today"; if (daysAgo < 2) return "yesterday"; if (daysAgo < 7) return "thisWeek"; if (daysAgo < 30) return "thisMonth"; return "older"; } export function formatFileDate(lastModifiedTs: number): string { const lastModified = lastModifiedTs ? new Date(lastModifiedTs) : new Date(); const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const fileDay = new Date( lastModified.getFullYear(), lastModified.getMonth(), lastModified.getDate(), ); if (fileDay.getTime() === today.getTime()) { return lastModified .toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, }) .toLowerCase(); } if (fileDay.getTime() === yesterday.getTime()) { return "Yesterday"; } const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); if (fileDay >= weekAgo) { return lastModified.toLocaleDateString("en-US", { weekday: "long" }); } return lastModified.toLocaleDateString("en-US", { month: "short", day: "numeric", }); } function FilePdfIcon({ className, style, }: { className?: string; style?: React.CSSProperties; }) { return ( ); } function FileGenericIcon({ className, style, }: { className?: string; style?: React.CSSProperties; }) { return ( ); } function CheckIcon({ className, style, }: { className?: string; style?: React.CSSProperties; }) { return ( ); } export interface FileItemProps { fileId: FileId; name: string; size?: number; lastModified?: number; isSelected: boolean; isActive: boolean; isViewedInViewer: boolean; thumbnailUrl?: string; onClick: (fileId: FileId) => void; onEyeClick: (fileId: FileId, e: React.MouseEvent) => void; } export function FileItem({ fileId, name, size, lastModified, isSelected, isActive, isViewedInViewer, thumbnailUrl, onClick, onEyeClick, }: FileItemProps) { const ext = getFileExtension(name); const isPdf = ext === "pdf"; const dateLabel = lastModified ? formatFileDate(lastModified) : ""; const typeLabel = isPdf ? "" : ext ? ext.toUpperCase() : "File"; const resolvedThumbnail = useLazyThumbnail(fileId, size ?? 0, thumbnailUrl); const itemRef = useRef(null); const [hoverRect, setHoverRect] = useState(null); const handleMouseEnter = useCallback(() => { setHoverRect(itemRef.current?.getBoundingClientRect() ?? null); }, []); const handleMouseLeave = useCallback(() => setHoverRect(null), []); // Reactive: tooltip appears as soon as both hover rect and thumbnail are ready const thumbPos = hoverRect && resolvedThumbnail ? { top: hoverRect.top + hoverRect.height / 2, left: hoverRect.right + 10, } : null; return ( <>
onClick(fileId)} role="button" tabIndex={0} onKeyDown={(e) => e.key === "Enter" && onClick(fileId)} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} >
{isSelected ? (
) : ( <>
{isPdf ? ( ) : ( )} )}
{name} {dateLabel} {dateLabel && typeLabel ? " ยท " : ""} {typeLabel}
{thumbPos && resolvedThumbnail && createPortal(
, document.body, )} ); }