✨ feat: Support transfer status filter.
This commit is contained in:
parent
1575a84a9b
commit
8f48c7939f
6 changed files with 191 additions and 82 deletions
|
|
@ -175,8 +175,8 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Future<JsonObject> getChatFiles(long chatId, Map<String, String> filter) {
|
public Future<JsonObject> getChatFiles(long chatId, Map<String, String> filter) {
|
||||||
String status = filter.get("status");
|
String downloadStatus = filter.get("downloadStatus");
|
||||||
if (Arrays.asList("downloading", "paused", "completed", "error").contains(status)) {
|
if (Arrays.asList("downloading", "paused", "completed", "error").contains(downloadStatus)) {
|
||||||
return DataVerticle.fileRepository.getFiles(chatId, filter)
|
return DataVerticle.fileRepository.getFiles(chatId, filter)
|
||||||
.compose(r -> {
|
.compose(r -> {
|
||||||
long[] messageIds = r.v1.stream().mapToLong(FileRecord::messageId).toArray();
|
long[] messageIds = r.v1.stream().mapToLong(FileRecord::messageId).toArray();
|
||||||
|
|
@ -215,7 +215,7 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
searchChatMessages.limit = Convert.toInt(filter.get("limit"), 20);
|
searchChatMessages.limit = Convert.toInt(filter.get("limit"), 20);
|
||||||
searchChatMessages.filter = TdApiHelp.getSearchMessagesFilter(filter.get("type"));
|
searchChatMessages.filter = TdApiHelp.getSearchMessagesFilter(filter.get("type"));
|
||||||
|
|
||||||
return (Objects.equals(filter.get("status"), FileRecord.DownloadStatus.idle.name()) ?
|
return (Objects.equals(filter.get("downloadStatus"), FileRecord.DownloadStatus.idle.name()) ?
|
||||||
this.getIdleChatFiles(searchChatMessages, 0) :
|
this.getIdleChatFiles(searchChatMessages, 0) :
|
||||||
client.execute(searchChatMessages))
|
client.execute(searchChatMessages))
|
||||||
.compose(foundChatMessages ->
|
.compose(foundChatMessages ->
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<Tuple3<List<FileRecord>, Long, Long>> getFiles(long chatId, Map<String, String> filter) {
|
public Future<Tuple3<List<FileRecord>, Long, Long>> getFiles(long chatId, Map<String, String> filter) {
|
||||||
String status = filter.get("status");
|
String downloadStatus = filter.get("downloadStatus");
|
||||||
String transferStatus = filter.get("transferStatus");
|
String transferStatus = filter.get("transferStatus");
|
||||||
String search = filter.get("search");
|
String search = filter.get("search");
|
||||||
Long fromMessageId = Convert.toLong(filter.get("fromMessageId"), 0L);
|
Long fromMessageId = Convert.toLong(filter.get("fromMessageId"), 0L);
|
||||||
|
|
@ -88,9 +88,9 @@ public class FileRepositoryImpl implements FileRepository {
|
||||||
String whereClause = "chat_id = #{chatId}";
|
String whereClause = "chat_id = #{chatId}";
|
||||||
Map<String, Object> params = MapUtil.of("chatId", chatId);
|
Map<String, Object> params = MapUtil.of("chatId", chatId);
|
||||||
params.put("limit", limit);
|
params.put("limit", limit);
|
||||||
if (StrUtil.isNotBlank(status)) {
|
if (StrUtil.isNotBlank(downloadStatus)) {
|
||||||
whereClause += " AND download_status = #{status}";
|
whereClause += " AND download_status = #{downloadStatus}";
|
||||||
params.put("status", status);
|
params.put("downloadStatus", downloadStatus);
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(transferStatus)) {
|
if (StrUtil.isNotBlank(transferStatus)) {
|
||||||
whereClause += " AND transfer_status = #{transferStatus}";
|
whereClause += " AND transfer_status = #{transferStatus}";
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
import { type DownloadStatus, type FileFilter } from "@/lib/types";
|
import {
|
||||||
|
type DownloadStatus,
|
||||||
|
type FileFilter,
|
||||||
|
type TransferStatus,
|
||||||
|
} from "@/lib/types";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectGroup,
|
||||||
|
SelectLabel,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
@ -14,9 +19,13 @@ import { type RowHeight } from "@/components/table-row-height-switch";
|
||||||
import FileTypeFilter from "@/components/file-type-filter";
|
import FileTypeFilter from "@/components/file-type-filter";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { useEffect, useState } from "react";
|
import * as React from "react";
|
||||||
import { X } from "lucide-react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Check, X } from "lucide-react";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import { DOWNLOAD_STATUS, TRANSFER_STATUS } from "@/components/file-status";
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface FileFiltersProps {
|
interface FileFiltersProps {
|
||||||
telegramId: string;
|
telegramId: string;
|
||||||
|
|
@ -37,6 +46,17 @@ const TableRowHeightSwitch = dynamic(
|
||||||
{ ssr: false },
|
{ ssr: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const statusOptions = {
|
||||||
|
downloadStatus: {
|
||||||
|
text: "Download Status",
|
||||||
|
status: DOWNLOAD_STATUS,
|
||||||
|
},
|
||||||
|
transferStatus: {
|
||||||
|
text: "Transfer Status",
|
||||||
|
status: TRANSFER_STATUS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function FileFilters({
|
export function FileFilters({
|
||||||
telegramId,
|
telegramId,
|
||||||
chatId,
|
chatId,
|
||||||
|
|
@ -53,19 +73,69 @@ export function FileFilters({
|
||||||
onFiltersChange({ ...filters, search });
|
onFiltersChange({ ...filters, search });
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
const handleStatusSelect = (value: string) => {
|
||||||
|
const [group, item] = value.split("||");
|
||||||
|
|
||||||
|
if (group === "downloadStatus") {
|
||||||
|
onFiltersChange({
|
||||||
|
...filters,
|
||||||
|
downloadStatus:
|
||||||
|
item === filters.downloadStatus
|
||||||
|
? undefined
|
||||||
|
: (item as DownloadStatus),
|
||||||
|
});
|
||||||
|
} else if (group === "transferStatus") {
|
||||||
|
onFiltersChange({
|
||||||
|
...filters,
|
||||||
|
transferStatus:
|
||||||
|
item === filters.transferStatus
|
||||||
|
? undefined
|
||||||
|
: (item as TransferStatus),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusDisplayValue = useMemo(() => {
|
||||||
|
if (!filters.downloadStatus && !filters.transferStatus) {
|
||||||
|
console.log("no status filter");
|
||||||
|
return "Filter Status";
|
||||||
|
}
|
||||||
|
const downloadStatus =
|
||||||
|
filters.downloadStatus && DOWNLOAD_STATUS[filters.downloadStatus];
|
||||||
|
const transferStatus =
|
||||||
|
filters.transferStatus && TRANSFER_STATUS[filters.transferStatus];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{downloadStatus && (
|
||||||
|
<div className="flex items-center space-x-1 rounded bg-gray-100 p-1 dark:bg-gray-800">
|
||||||
|
<span className="text-xs">Download</span>
|
||||||
|
<downloadStatus.icon className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{transferStatus && (
|
||||||
|
<div className="flex items-center space-x-1 rounded bg-gray-100 p-1 dark:bg-gray-800">
|
||||||
|
<span className="text-xs">Transfer</span>
|
||||||
|
<transferStatus.icon className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [filters.downloadStatus, filters.transferStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleSearchChange(search);
|
handleSearchChange(search);
|
||||||
}, [search, handleSearchChange]);
|
}, [search, handleSearchChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 flex flex-col justify-between md:flex-row">
|
<div className="mb-6 flex flex-col justify-between md:flex-row">
|
||||||
<div className="grid grid-cols-2 gap-4 md:flex md:flex-row">
|
<div className="grid grid-cols-3 gap-4 md:flex md:flex-row">
|
||||||
<div className="relative">
|
<div className="relative col-span-3">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search files..."
|
placeholder="Search files..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="relative col-span-2 md:w-[300px]"
|
className="relative md:w-[300px]"
|
||||||
/>
|
/>
|
||||||
{search && (
|
{search && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -87,24 +157,41 @@ export function FileFilters({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={filters.status}
|
value={`${filters.downloadStatus ?? ""}||${filters.transferStatus ?? ""}`}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => handleStatusSelect(value)}
|
||||||
onFiltersChange({
|
|
||||||
...filters,
|
|
||||||
status: value as DownloadStatus | "all",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full md:w-[150px]">
|
<SelectTrigger className="col-span-2 w-full md:w-[210px]">
|
||||||
<SelectValue placeholder="Download status" />
|
<SelectValue placeholder="Filter Status">
|
||||||
|
{statusDisplayValue}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All statuses</SelectItem>
|
{Object.entries(statusOptions).map(([groupKey, group]) => (
|
||||||
<SelectItem value="idle">Not downloaded</SelectItem>
|
<SelectGroup key={groupKey}>
|
||||||
<SelectItem value="downloading">Downloading</SelectItem>
|
<SelectLabel>{group.text}</SelectLabel>
|
||||||
<SelectItem value="paused">Paused</SelectItem>
|
{Object.entries(group.status).map(([statusKey, item]) => (
|
||||||
<SelectItem value="completed">Completed</SelectItem>
|
<SelectPrimitive.Item
|
||||||
<SelectItem value="error">Error</SelectItem>
|
key={`${groupKey}||${statusKey}`}
|
||||||
|
value={`${groupKey}||${statusKey}`}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{filters[groupKey as keyof FileFilter] === statusKey && (
|
||||||
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<SelectPrimitive.ItemText>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{item.icon && <item.icon className="mr-2 h-4 w-4" />}
|
||||||
|
{item.text}
|
||||||
|
</div>
|
||||||
|
</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,50 +4,60 @@ import React from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { TooltipWrapper } from "@/components/ui/tooltip";
|
import { TooltipWrapper } from "@/components/ui/tooltip";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import {CheckCircle2, Clock, Download, FolderSync, Pause, XCircle} from "lucide-react";
|
||||||
|
|
||||||
|
export const DOWNLOAD_STATUS = {
|
||||||
|
idle: {
|
||||||
|
icon: Clock,
|
||||||
|
className: "bg-gray-100 text-gray-600",
|
||||||
|
text: "Idle",
|
||||||
|
},
|
||||||
|
downloading: {
|
||||||
|
icon: Download,
|
||||||
|
className: "bg-blue-100 text-blue-600",
|
||||||
|
text: "Downloading",
|
||||||
|
},
|
||||||
|
paused: {
|
||||||
|
icon: Pause,
|
||||||
|
className: "bg-yellow-100 text-yellow-600",
|
||||||
|
text: "Paused",
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
icon: CheckCircle2,
|
||||||
|
className: "bg-green-100 text-green-600",
|
||||||
|
text: "Completed",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
icon: XCircle,
|
||||||
|
className: "bg-red-100 text-red-600",
|
||||||
|
text: "Error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TRANSFER_STATUS = {
|
||||||
|
idle: {
|
||||||
|
icon: Clock,
|
||||||
|
className: "bg-gray-100 text-gray-600",
|
||||||
|
text: "Idle",
|
||||||
|
},
|
||||||
|
transferring: {
|
||||||
|
icon: FolderSync,
|
||||||
|
className: "bg-blue-100 text-blue-600",
|
||||||
|
text: "Transferring",
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
icon: CheckCircle2,
|
||||||
|
className: "bg-green-100 text-green-600",
|
||||||
|
text: "Transferred",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
icon: XCircle,
|
||||||
|
className: "bg-red-100 text-red-600",
|
||||||
|
text: "Transfer Error",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function FileStatus({ file }: { file: TelegramFile }) {
|
export default function FileStatus({ file }: { file: TelegramFile }) {
|
||||||
const DOWNLOAD_STATUS = {
|
|
||||||
idle: {
|
|
||||||
className: "bg-gray-100 text-gray-600",
|
|
||||||
text: "Idle",
|
|
||||||
},
|
|
||||||
downloading: {
|
|
||||||
className: "bg-blue-100 text-blue-600",
|
|
||||||
text: "Downloading",
|
|
||||||
},
|
|
||||||
paused: {
|
|
||||||
className: "bg-yellow-100 text-yellow-600",
|
|
||||||
text: "Paused",
|
|
||||||
},
|
|
||||||
completed: {
|
|
||||||
className: "bg-green-100 text-green-600",
|
|
||||||
text: "Completed",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
className: "bg-red-100 text-red-600",
|
|
||||||
text: "Error",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const TRANSFER_STATUS = {
|
|
||||||
idle: {
|
|
||||||
className: "bg-gray-100 text-gray-600",
|
|
||||||
text: "Idle",
|
|
||||||
},
|
|
||||||
transferring: {
|
|
||||||
className: "bg-blue-100 text-blue-600",
|
|
||||||
text: "Transferring",
|
|
||||||
},
|
|
||||||
completed: {
|
|
||||||
className: "bg-green-100 text-green-600",
|
|
||||||
text: "Transferred",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
className: "bg-red-100 text-red-600",
|
|
||||||
text: "Transfer Error",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const badgeVariants = {
|
const badgeVariants = {
|
||||||
initial: { opacity: 0, scale: 0.9 },
|
initial: { opacity: 0, scale: 0.9 },
|
||||||
animate: {
|
animate: {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
type DownloadStatus,
|
type DownloadStatus,
|
||||||
type FileFilter,
|
type FileFilter,
|
||||||
type TelegramFile, type TransferStatus,
|
type TelegramFile,
|
||||||
|
type TransferStatus,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import useSWRInfinite from "swr/infinite";
|
||||||
import { useWebsocket } from "@/hooks/use-websocket";
|
import { useWebsocket } from "@/hooks/use-websocket";
|
||||||
|
|
@ -13,7 +14,8 @@ import { useDebounce } from "use-debounce";
|
||||||
const DEFAULT_FILTERS: FileFilter = {
|
const DEFAULT_FILTERS: FileFilter = {
|
||||||
search: "",
|
search: "",
|
||||||
type: "media",
|
type: "media",
|
||||||
status: "all",
|
downloadStatus: undefined,
|
||||||
|
transferStatus: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
type FileResponse = {
|
type FileResponse = {
|
||||||
|
|
@ -43,9 +45,12 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
);
|
);
|
||||||
const getKey = (page: number, previousPageData: FileResponse) => {
|
const getKey = (page: number, previousPageData: FileResponse) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
...(filters.search && { search: window.encodeURIComponent(filters.search) }),
|
...(filters.search && {
|
||||||
|
search: window.encodeURIComponent(filters.search),
|
||||||
|
}),
|
||||||
...(filters.type && { type: filters.type }),
|
...(filters.type && { type: filters.type }),
|
||||||
...(filters.status && { status: filters.status })
|
...(filters.downloadStatus && { downloadStatus: filters.downloadStatus }),
|
||||||
|
...(filters.transferStatus && { transferStatus: filters.transferStatus }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
|
|
@ -133,14 +138,19 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
...file,
|
...file,
|
||||||
id: latestFilesStatus[file.uniqueId]?.fileId ?? file.id,
|
id: latestFilesStatus[file.uniqueId]?.fileId ?? file.id,
|
||||||
downloadStatus:
|
downloadStatus:
|
||||||
latestFilesStatus[file.uniqueId]?.downloadStatus ?? file.downloadStatus,
|
latestFilesStatus[file.uniqueId]?.downloadStatus ??
|
||||||
localPath: latestFilesStatus[file.uniqueId]?.localPath ?? file.localPath,
|
file.downloadStatus,
|
||||||
|
localPath:
|
||||||
|
latestFilesStatus[file.uniqueId]?.localPath ?? file.localPath,
|
||||||
completionDate:
|
completionDate:
|
||||||
latestFilesStatus[file.uniqueId]?.completionDate ?? file.completionDate,
|
latestFilesStatus[file.uniqueId]?.completionDate ??
|
||||||
|
file.completionDate,
|
||||||
downloadedSize:
|
downloadedSize:
|
||||||
latestFilesStatus[file.uniqueId]?.downloadedSize ?? file.downloadedSize,
|
latestFilesStatus[file.uniqueId]?.downloadedSize ??
|
||||||
|
file.downloadedSize,
|
||||||
transferStatus:
|
transferStatus:
|
||||||
latestFilesStatus[file.uniqueId]?.transferStatus ?? file.transferStatus,
|
latestFilesStatus[file.uniqueId]?.transferStatus ??
|
||||||
|
file.transferStatus,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -169,7 +179,8 @@ export function useFiles(accountId: string, chatId: string) {
|
||||||
if (
|
if (
|
||||||
newFilters.search === filters.search &&
|
newFilters.search === filters.search &&
|
||||||
newFilters.type === filters.type &&
|
newFilters.type === filters.type &&
|
||||||
newFilters.status === filters.status
|
newFilters.downloadStatus === filters.downloadStatus &&
|
||||||
|
newFilters.transferStatus === filters.transferStatus
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ export type TDFile = {
|
||||||
export type FileFilter = {
|
export type FileFilter = {
|
||||||
search: string;
|
search: string;
|
||||||
type: FileType;
|
type: FileType;
|
||||||
status: DownloadStatus | "all";
|
downloadStatus?: DownloadStatus;
|
||||||
|
transferStatus?: TransferStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TelegramApiResult = {
|
export type TelegramApiResult = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue