323 lines
9.6 KiB
TypeScript
323 lines
9.6 KiB
TypeScript
"use client";
|
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Download, LoaderCircle, LoaderPinwheel } from "lucide-react";
|
|
import { useFiles } from "@/hooks/use-files";
|
|
import { FileFilters } from "@/components/file-filters";
|
|
import {
|
|
getRowHeightPX,
|
|
useRowHeightLocalStorage,
|
|
} from "@/components/table-row-height-switch";
|
|
import { type Column } from "@/components/table-column-filter";
|
|
import { cn } from "@/lib/utils";
|
|
import FileNotFount from "@/components/file-not-found";
|
|
import useSWRMutation from "swr/mutation";
|
|
import { POST } from "@/lib/api";
|
|
import FileRow from "@/components/file-row";
|
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
import { type TelegramFile } from "@/lib/types";
|
|
import FileViewer from "@/components/file-viewer";
|
|
|
|
const COLUMNS: Column[] = [
|
|
{
|
|
id: "content",
|
|
label: "Content",
|
|
isVisible: true,
|
|
className: "text-center",
|
|
},
|
|
{ id: "type", label: "Type", isVisible: true, className: "w-16 text-center" },
|
|
{
|
|
id: "size",
|
|
label: "Size",
|
|
isVisible: true,
|
|
className: "w-20 text-center",
|
|
},
|
|
{
|
|
id: "status",
|
|
label: "Status",
|
|
isVisible: true,
|
|
className: "w-32 text-center",
|
|
},
|
|
{ id: "extra", label: "Extra", isVisible: true, className: "flex-1" },
|
|
{
|
|
id: "actions",
|
|
label: "Actions",
|
|
isVisible: true,
|
|
className: "text-center w-40 min-w-40",
|
|
},
|
|
];
|
|
|
|
interface FileTableProps {
|
|
accountId: string;
|
|
chatId: string;
|
|
}
|
|
|
|
export function FileTable({ accountId, chatId }: FileTableProps) {
|
|
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
|
|
const tableParentRef = useRef(null);
|
|
const [columns, setColumns] = useState<Column[]>(COLUMNS);
|
|
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
|
|
"telegramFileList",
|
|
"m",
|
|
);
|
|
const useFilesProps = useFiles(accountId, chatId);
|
|
const { filters, handleFilterChange, isLoading, files, handleLoadMore } =
|
|
useFilesProps;
|
|
const [currentViewFile, setCurrentViewFile] = useState<TelegramFile | undefined>();
|
|
const [viewerOpen, setViewerOpen] = useState(false);
|
|
const {
|
|
trigger: startDownloadMultiple,
|
|
isMutating: startDownloadMultipleMutating,
|
|
} = useSWRMutation(
|
|
`/${accountId}/file/start-download-multiple`,
|
|
(
|
|
key,
|
|
{
|
|
arg,
|
|
}: {
|
|
arg: {
|
|
chatId: number;
|
|
files: Array<{ messageId: number; fileId: number }>;
|
|
};
|
|
},
|
|
) => POST(key, arg),
|
|
{
|
|
onSuccess: () => {
|
|
setSelectedFiles(new Set());
|
|
},
|
|
},
|
|
);
|
|
const rowVirtual = useVirtualizer({
|
|
count: files.length,
|
|
getScrollElement: () => tableParentRef.current,
|
|
estimateSize: (index) => {
|
|
const file = files[index]!;
|
|
const height = getRowHeightPX(rowHeight);
|
|
|
|
if (
|
|
file.downloadStatus === "idle" ||
|
|
file.downloadStatus === "completed" ||
|
|
file.size === 0
|
|
) {
|
|
return height;
|
|
}
|
|
return height + 8;
|
|
},
|
|
paddingStart: 1,
|
|
paddingEnd: 1,
|
|
});
|
|
|
|
useEffect(() => {
|
|
rowVirtual.measure();
|
|
}, [rowHeight, rowVirtual]);
|
|
|
|
useEffect(() => {
|
|
const [lastItem] = [...rowVirtual.getVirtualItems()].reverse();
|
|
if (!lastItem) {
|
|
return;
|
|
}
|
|
|
|
if (lastItem.index >= files.length - 1) {
|
|
void handleLoadMore();
|
|
}
|
|
//eslint-disable-next-line
|
|
}, [files.length, handleLoadMore, rowVirtual.getVirtualItems()]);
|
|
|
|
useEffect(() => {
|
|
if (files.length === 0 || !currentViewFile) {
|
|
return;
|
|
}
|
|
const index = files.findIndex((f) => f.id === currentViewFile.id);
|
|
if (index === -1) {
|
|
setCurrentViewFile(undefined);
|
|
return;
|
|
}
|
|
const file = files[index]!;
|
|
if (currentViewFile.next === undefined && file.next !== undefined) {
|
|
setCurrentViewFile(file);
|
|
}
|
|
}, [currentViewFile, files]);
|
|
|
|
const dynamicClass = useMemo(() => {
|
|
switch (rowHeight) {
|
|
case "s":
|
|
return {
|
|
content: "h-6 w-6",
|
|
contentCell: "w-16",
|
|
};
|
|
case "m":
|
|
return {
|
|
content: "h-20 w-20",
|
|
contentCell: "w-24",
|
|
};
|
|
case "l":
|
|
return {
|
|
content: "h-60 w-60",
|
|
contentCell: "w-64",
|
|
};
|
|
}
|
|
}, [rowHeight]);
|
|
|
|
const handleSelectAll = () => {
|
|
if (selectedFiles.size === files.length) {
|
|
setSelectedFiles(new Set());
|
|
} else {
|
|
setSelectedFiles(
|
|
new Set(
|
|
files
|
|
.filter((file) => file.downloadStatus === "idle")
|
|
.map((file) => file.id),
|
|
),
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleSelectFile = (fileId: number) => {
|
|
const newSelected = new Set(selectedFiles);
|
|
if (newSelected.has(fileId)) {
|
|
newSelected.delete(fileId);
|
|
} else {
|
|
newSelected.add(fileId);
|
|
}
|
|
setSelectedFiles(newSelected);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<FileFilters
|
|
telegramId={accountId}
|
|
chatId={chatId}
|
|
filters={filters}
|
|
onFiltersChange={handleFilterChange}
|
|
columns={columns}
|
|
onColumnConfigChange={setColumns}
|
|
rowHeight={rowHeight}
|
|
setRowHeight={setRowHeight}
|
|
/>
|
|
{currentViewFile && (
|
|
<FileViewer
|
|
open={viewerOpen}
|
|
onOpenChange={setViewerOpen}
|
|
file={currentViewFile}
|
|
onFileChange={setCurrentViewFile}
|
|
{...useFilesProps}
|
|
/>
|
|
)}
|
|
<div className="h-[calc(100vh-13rem)] space-y-4 overflow-hidden">
|
|
{selectedFiles.size > 0 && (
|
|
<div className="flex items-center justify-between rounded-lg bg-muted/50 p-4">
|
|
<span className="text-sm">
|
|
{selectedFiles.size} {selectedFiles.size === 1 ? "file" : "files"}{" "}
|
|
selected
|
|
</span>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => {
|
|
void startDownloadMultiple({
|
|
chatId: Number(chatId),
|
|
files: Array.from(selectedFiles).map((id) => {
|
|
const file = files.find((f) => f.id === id);
|
|
return {
|
|
messageId: file?.messageId ?? 0,
|
|
fileId: file?.id ?? 0,
|
|
};
|
|
}),
|
|
});
|
|
}}
|
|
disabled={
|
|
selectedFiles.size === 0 || startDownloadMultipleMutating
|
|
}
|
|
>
|
|
{startDownloadMultipleMutating ? (
|
|
<LoaderCircle
|
|
className="mr-2 h-4 w-4 animate-spin"
|
|
style={{ strokeWidth: "0.8px" }}
|
|
/>
|
|
) : (
|
|
<>
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Download Selected
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className="relative h-full overflow-auto rounded-md border"
|
|
ref={tableParentRef}
|
|
>
|
|
<div className="sticky top-0 z-20 flex h-10 items-center border-b bg-background/90 text-sm text-muted-foreground backdrop-blur-sm">
|
|
<div className="w-[30px] text-center">
|
|
<Checkbox
|
|
checked={selectedFiles.size === files.length}
|
|
onCheckedChange={handleSelectAll}
|
|
/>
|
|
</div>
|
|
{columns.map((col) =>
|
|
col.isVisible ? (
|
|
<div
|
|
key={col.id}
|
|
suppressHydrationWarning
|
|
className={cn(
|
|
col.className ?? "",
|
|
col.id === "content" ? dynamicClass.contentCell : "",
|
|
)}
|
|
>
|
|
{col.label}
|
|
</div>
|
|
) : null,
|
|
)}
|
|
</div>
|
|
{files.length === 0 && isLoading && (
|
|
<div className="sticky left-1/2 top-0 z-10 flex h-full w-full items-center justify-center bg-accent">
|
|
<LoaderPinwheel
|
|
className="h-8 w-8 animate-spin"
|
|
style={{ strokeWidth: "0.8px" }}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="h-full">
|
|
<div
|
|
className={cn("relative w-full")}
|
|
style={{ height: `${rowVirtual.getTotalSize()}px` }}
|
|
>
|
|
{files.length !== 0 &&
|
|
rowVirtual.getVirtualItems().map((virtualRow) => {
|
|
const file = files[virtualRow.index]!;
|
|
return (
|
|
<FileRow
|
|
index={virtualRow.index}
|
|
className={cn(
|
|
"absolute left-0 top-0 flex w-full items-center",
|
|
)}
|
|
style={{
|
|
height: `${virtualRow.size}px`,
|
|
transform: `translateY(${virtualRow.start}px)`,
|
|
}}
|
|
ref={rowVirtual.measureElement}
|
|
file={file}
|
|
checked={selectedFiles.has(file.id)}
|
|
onCheckedChange={() => handleSelectFile(file.id)}
|
|
onFileClick={() => {
|
|
setCurrentViewFile(file);
|
|
setViewerOpen(true);
|
|
}}
|
|
properties={{
|
|
rowHeight: rowHeight,
|
|
dynamicClass,
|
|
columns,
|
|
}}
|
|
key={`${file.messageId}-${file.uniqueId}-${virtualRow.index}`}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
{!isLoading && files.length === 0 && <FileNotFount />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|