diff --git a/api/src/main/java/telegram/files/HttpVerticle.java b/api/src/main/java/telegram/files/HttpVerticle.java index 3c74a73..f6dff9f 100644 --- a/api/src/main/java/telegram/files/HttpVerticle.java +++ b/api/src/main/java/telegram/files/HttpVerticle.java @@ -13,6 +13,7 @@ import io.vertx.core.http.CookieSameSite; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.healthchecks.HealthCheckHandler; import io.vertx.ext.healthchecks.HealthChecks; @@ -140,6 +141,7 @@ public class HttpVerticle extends AbstractVerticle { router.get("/file/preview").handler(this::handleFilePreview); router.post("/file/start-download").handler(this::handleFileStartDownload); + router.post("/file/start-download-multiple").handler(this::handleFileStartDownloadMultiple); router.post("/file/cancel-download").handler(this::handleFileCancelDownload); router.post("/file/toggle-pause-download").handler(this::handleFileTogglePauseDownload); router.post("/file/auto-download").handler(this::handleAutoDownload); @@ -315,11 +317,11 @@ public class HttpVerticle extends AbstractVerticle { } TelegramVerticle telegramVerticle = telegramVerticleOptional.get(); telegramVerticle.close(true) - .onSuccess(r -> { - telegramVerticles.remove(telegramVerticle); - sessionTelegramVerticles.entrySet().removeIf(e -> e.getValue().equals(telegramVerticle)); - ctx.end(); - }); + .onSuccess(r -> { + telegramVerticles.remove(telegramVerticle); + sessionTelegramVerticles.entrySet().removeIf(e -> e.getValue().equals(telegramVerticle)); + ctx.end(); + }); } private void handleTelegrams(RoutingContext ctx) { @@ -511,6 +513,36 @@ public class HttpVerticle extends AbstractVerticle { .onFailure(ctx::fail); } + private void handleFileStartDownloadMultiple(RoutingContext ctx) { + TelegramVerticle telegramVerticle = getTelegramVerticle(ctx); + if (telegramVerticle == null) { + return; + } + + JsonObject jsonObject = ctx.body().asJsonObject(); + Long chatId = jsonObject.getLong("chatId"); + JsonArray files = jsonObject.getJsonArray("files"); + if (chatId == null || CollUtil.isEmpty(files)) { + ctx.fail(400); + return; + } + + Future.any(files.stream() + .map(f -> { + JsonObject file = (JsonObject) f; + Long messageId = file.getLong("messageId"); + Integer fileId = file.getInteger("fileId"); + return telegramVerticle.startDownload(chatId, messageId, fileId); + }) + .toList()) + .onSuccess(ctx::json) + .onFailure(r -> { + log.error(r, "Failed to start download multiple files"); + ctx.json(JsonObject.of("error", "Part of the files failed to start download")); + ctx.response().setStatusCode(400).end(); + }); + } + private void handleFileCancelDownload(RoutingContext ctx) { TelegramVerticle telegramVerticle = getTelegramVerticle(ctx); if (telegramVerticle == null) { diff --git a/web/src/components/file-list.tsx b/web/src/components/file-list.tsx index 756a744..b5a1231 100644 --- a/web/src/components/file-list.tsx +++ b/web/src/components/file-list.tsx @@ -24,6 +24,7 @@ import { FileAudioIcon, FileIcon, ImageIcon, + LoaderCircle, LoaderPinwheel, VideoIcon, } from "lucide-react"; @@ -51,6 +52,8 @@ import FileNotFount from "@/components/file-not-found"; import FileExtra from "@/components/file-extra"; import useIsMobile from "@/hooks/use-is-mobile"; import FileStatus from "@/components/file-status"; +import useSWRMutation from "swr/mutation"; +import { POST } from "@/lib/api"; interface FileListProps { accountId: string; @@ -112,6 +115,28 @@ export function FileList({ accountId, chatId }: FileListProps) { ); const { filters, handleFilterChange, isLoading, files, handleLoadMore } = useFiles(accountId, chatId); + const { + trigger: startDownloadMultiple, + isMutating: startDownloadMultipleMutating, + } = useSWRMutation( + "/file/start-download-multiple", + ( + key, + { + arg, + }: { + arg: { + chatId: number; + files: Array<{ messageId: number; fileId: number }>; + }; + }, + ) => POST(key, arg), + { + onSuccess: () => { + setSelectedFiles(new Set()); + }, + }, + ); useEffect(() => { const observer = new IntersectionObserver( @@ -182,7 +207,13 @@ export function FileList({ accountId, chatId }: FileListProps) { if (selectedFiles.size === files.length) { setSelectedFiles(new Set()); } else { - setSelectedFiles(new Set(files.map((file) => file.id))); + setSelectedFiles( + new Set( + files + .filter((file) => file.downloadStatus === "idle") + .map((file) => file.id), + ), + ); } }; @@ -296,16 +327,32 @@ export function FileList({ accountId, chatId }: FileListProps) { )} @@ -350,6 +397,7 @@ export function FileList({ accountId, chatId }: FileListProps) { handleSelectFile(file.id)} + disabled={file.downloadStatus !== "idle"} /> {columns.map((col) =>