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) =>