FileUpload: Add WebDAV support and QOL features (#1162)
* FileUpload: Add WebDAV support and QOL features - Now supports WebDAV providers such as OwnCloud and NextCloud - Added a context menu option to upload specific attachments rather than having to upload all of them at once. - File type filtering * greptile fixes
This commit is contained in:
parent
ec7886beaf
commit
3d1ea790a5
5 changed files with 280 additions and 30 deletions
|
|
@ -12,13 +12,14 @@ import { OpenExternalIcon } from "@components/Icons";
|
|||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import definePlugin from "@utils/types";
|
||||
import { CloudUpload } from "@vencord/discord-types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { DraftType, FluxDispatcher, Menu, PermissionsBits, PermissionStore, React, useEffect, UserStore, useState } from "@webpack/common";
|
||||
import { DraftType, FluxDispatcher, Menu, PermissionsBits, PermissionStore, React, showToast, Toasts, UploadAttachmentStore, useEffect, UserStore, useState } from "@webpack/common";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import { serviceLabels, ServiceType } from "./types";
|
||||
import { getMediaUrl } from "./utils/getMediaUrl";
|
||||
import { cancelCurrentUpload, getUploadState, isConfigured, subscribeUploadState, uploadFile, uploadPickedFile, uploadProvidedFiles } from "./utils/upload";
|
||||
import { cancelCurrentUpload, getUploadState, isConfigured, isFileTypeAllowed, logger, subscribeUploadState, uploadFile, uploadPickedFile, uploadProvidedFiles } from "./utils/upload";
|
||||
const cl = classNameFactory("vc-file-upload-");
|
||||
const { getUserMaxFileSize } = findByPropsLazy("getUserMaxFileSize");
|
||||
let uploadAddFilesInterceptor: ((event: unknown) => void) | null = null;
|
||||
|
|
@ -81,7 +82,7 @@ function interceptUploadAddFiles(event: unknown): void {
|
|||
...extractFilesFromValue(payload.uploads),
|
||||
...extractFilesFromValue(payload.items)
|
||||
];
|
||||
const uniqueFiles = Array.from(new Set(files));
|
||||
const uniqueFiles = Array.from(new Set(files)).filter(f => isFileTypeAllowed(f));
|
||||
|
||||
if (!uniqueFiles.length) return;
|
||||
if (!shouldInterceptUploadFiles(uniqueFiles, payload)) return;
|
||||
|
|
@ -98,10 +99,13 @@ function handlePaste(event: ClipboardEvent) {
|
|||
|
||||
if (!settings.store.autoUploadPastedFiles || !isConfigured()) return;
|
||||
|
||||
const allowed = files.filter(f => isFileTypeAllowed(f));
|
||||
if (allowed.length === 0) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
void uploadProvidedFiles(files);
|
||||
void uploadProvidedFiles(allowed);
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
|
|
@ -224,32 +228,88 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =>
|
|||
);
|
||||
};
|
||||
|
||||
async function handleUploadFileFromDraft(upload: CloudUpload) {
|
||||
const file = upload.item?.file;
|
||||
if (!file) return;
|
||||
|
||||
if (!isFileTypeAllowed(file)) {
|
||||
showToast("File type not allowed by current filter", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isConfigured()) {
|
||||
showToast("Please configure FileUpload settings first", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await uploadProvidedFiles([file], true);
|
||||
upload.removeFromMsgDraft();
|
||||
} catch (e) {
|
||||
logger.warn("Draft upload encountered an unexpected error", e);
|
||||
}
|
||||
}
|
||||
|
||||
const ExternalIcon = () => <OpenExternalIcon height={24} width={24} />;
|
||||
|
||||
const channelAttachMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
||||
const channel = props?.channel;
|
||||
if (!channel) return;
|
||||
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
|
||||
if (children.some(child => child?.props?.id === "file-upload-manual")) return;
|
||||
if (children.some(child => child?.props?.id === "file-upload-manual" || child?.props?.id === "file-upload-uploads")) return;
|
||||
|
||||
children.splice(1, 0,
|
||||
<Menu.MenuItem
|
||||
id="file-upload-manual"
|
||||
key="file-upload-manual"
|
||||
label="Upload to Host"
|
||||
iconLeft={ExternalIcon}
|
||||
leadingAccessory={{
|
||||
type: "icon",
|
||||
icon: ExternalIcon
|
||||
}}
|
||||
action={() => uploadPickedFile()}
|
||||
/>
|
||||
);
|
||||
const uploads = UploadAttachmentStore.getUploads(channel.id, DraftType.ChannelMessage);
|
||||
const draftUploads = Array.isArray(uploads) ? uploads.filter((u: CloudUpload) => u.item?.file && isFileTypeAllowed(u.item.file)) : [];
|
||||
|
||||
if (draftUploads.length > 0) {
|
||||
children.splice(1, 0,
|
||||
<Menu.MenuItem
|
||||
id="file-upload-uploads"
|
||||
key="file-upload-uploads"
|
||||
label="Upload to Host"
|
||||
iconLeft={ExternalIcon}
|
||||
leadingAccessory={{
|
||||
type: "icon",
|
||||
icon: ExternalIcon
|
||||
}}
|
||||
>
|
||||
{draftUploads.map((upload: CloudUpload) => (
|
||||
<Menu.MenuItem
|
||||
id={`file-upload-draft-${upload.id}`}
|
||||
key={upload.id}
|
||||
label={upload.filename}
|
||||
action={() => handleUploadFileFromDraft(upload)}
|
||||
/>
|
||||
))}
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
id="file-upload-manual"
|
||||
key="file-upload-manual"
|
||||
label="Choose File..."
|
||||
action={() => uploadPickedFile()}
|
||||
/>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
} else {
|
||||
children.splice(1, 0,
|
||||
<Menu.MenuItem
|
||||
id="file-upload-manual"
|
||||
key="file-upload-manual"
|
||||
label="Upload to Host"
|
||||
iconLeft={ExternalIcon}
|
||||
leadingAccessory={{
|
||||
type: "icon",
|
||||
icon: ExternalIcon
|
||||
}}
|
||||
action={() => uploadPickedFile()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "FileUpload",
|
||||
description: "Upload images and videos to file hosting services like Zipline and Nest",
|
||||
description: "Upload files to hosting services like Zipline, Nest, S3, and WebDAV",
|
||||
tags: ["Media"],
|
||||
authors: [EquicordDevs.creations, EquicordDevs.keircn, Devs.ScattrdBlade],
|
||||
settings,
|
||||
|
|
|
|||
|
|
@ -458,6 +458,43 @@ export async function uploadToPixelDrain(
|
|||
}
|
||||
}
|
||||
|
||||
function isValidHttpsUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadToWebdav(
|
||||
_: IpcMainInvokeEvent,
|
||||
fileBuffer: ArrayBuffer,
|
||||
uploadUrl: string,
|
||||
headers: Record<string, string>
|
||||
): Promise<NativeUploadResult> {
|
||||
if (!isValidHttpsUrl(uploadUrl)) {
|
||||
return { success: false, error: "Invalid or non-HTTPS upload URL" };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: "PUT",
|
||||
headers,
|
||||
body: new Blob([fileBuffer])
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return { success: false, error: `Upload failed: ${response.status} ${errorText}` };
|
||||
}
|
||||
|
||||
return { success: true, url: uploadUrl };
|
||||
} catch (e) {
|
||||
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchFile(
|
||||
|
||||
_: IpcMainInvokeEvent,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ const serviceOptions = [
|
|||
{ label: "filebin.net", value: ServiceType.FILEBIN },
|
||||
{ label: "PixelVault", value: ServiceType.PIXELVAULT },
|
||||
{ label: "PixelDrain", value: ServiceType.PIXELDRAIN },
|
||||
{ label: "ShareX Custom Uploader", value: ServiceType.SHAREX }
|
||||
{ label: "ShareX Custom Uploader", value: ServiceType.SHAREX },
|
||||
{ label: "WebDAV (Nextcloud/Owncloud)", value: ServiceType.WEBDAV }
|
||||
];
|
||||
|
||||
const litterboxOptions = [
|
||||
|
|
@ -313,6 +314,36 @@ export const settings = definePluginSettings({
|
|||
description: "Automatically upload files from clipboard to image host when pasting in chat input.",
|
||||
default: false
|
||||
},
|
||||
webdavUrl: {
|
||||
type: OptionType.STRING,
|
||||
description: "WebDAV server URL",
|
||||
default: "",
|
||||
hidden: true
|
||||
},
|
||||
webdavUsername: {
|
||||
type: OptionType.STRING,
|
||||
description: "WebDAV username",
|
||||
default: "",
|
||||
hidden: true
|
||||
},
|
||||
webdavPassword: {
|
||||
type: OptionType.STRING,
|
||||
description: "WebDAV password or app token",
|
||||
default: "",
|
||||
hidden: true
|
||||
},
|
||||
webdavDirectory: {
|
||||
type: OptionType.STRING,
|
||||
description: "Optional subdirectory on the WebDAV server",
|
||||
default: "",
|
||||
hidden: true
|
||||
},
|
||||
uploadAllowedFileTypes: {
|
||||
type: OptionType.STRING,
|
||||
description: "Comma-separated list of allowed file extensions (e.g. png,jpg,gif,mp4). Leave empty to allow all files.",
|
||||
default: "",
|
||||
hidden: true
|
||||
},
|
||||
settingsComponent: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Settings",
|
||||
|
|
@ -475,6 +506,7 @@ export function SettingsComponent() {
|
|||
const isPixelVault = store.serviceType === ServiceType.PIXELVAULT;
|
||||
const isPixelDrain = store.serviceType === ServiceType.PIXELDRAIN;
|
||||
const isShareX = store.serviceType === ServiceType.SHAREX;
|
||||
const isWebdav = store.serviceType === ServiceType.WEBDAV;
|
||||
|
||||
const validateShareXConfig = () => {
|
||||
try {
|
||||
|
|
@ -785,6 +817,39 @@ export function SettingsComponent() {
|
|||
</SettingGroup>
|
||||
)}
|
||||
|
||||
{isWebdav && (
|
||||
<SettingGroup name="WebDAV" description="Connection details for WebDAV servers (Nextcloud, Owncloud, etc.).">
|
||||
<SettingTextInput
|
||||
name="Server URL"
|
||||
description="Base WebDAV URL (e.g. https://nextcloud.example.com/remote.php/dav/files/username)"
|
||||
value={store.webdavUrl}
|
||||
onChange={v => store.webdavUrl = v}
|
||||
placeholder="https://nextcloud.example.com/remote.php/dav/files/username"
|
||||
/>
|
||||
<SettingTextInput
|
||||
name="Username"
|
||||
description="WebDAV username"
|
||||
value={store.webdavUsername}
|
||||
onChange={v => store.webdavUsername = v}
|
||||
placeholder="username"
|
||||
/>
|
||||
<SettingTextInput
|
||||
name="Password or App Token"
|
||||
description="WebDAV password or app token"
|
||||
value={store.webdavPassword}
|
||||
onChange={v => store.webdavPassword = v}
|
||||
placeholder="password or app token"
|
||||
/>
|
||||
<SettingTextInput
|
||||
name="Upload Directory"
|
||||
description="Optional subdirectory on the server to upload into (e.g. uploads)"
|
||||
value={store.webdavDirectory}
|
||||
onChange={v => store.webdavDirectory = v}
|
||||
placeholder="Leave empty for root directory"
|
||||
/>
|
||||
</SettingGroup>
|
||||
)}
|
||||
|
||||
<SettingGroup name="Upload Behavior" description="Control what FileUpload does after a host returns a URL.">
|
||||
<SettingSwitch
|
||||
name="Strip Query Parameters"
|
||||
|
|
@ -882,6 +947,14 @@ export function SettingsComponent() {
|
|||
checked={store.bypassDiscordUploadOnlyOverLimit}
|
||||
onChange={v => store.bypassDiscordUploadOnlyOverLimit = v}
|
||||
/>
|
||||
|
||||
<SettingTextInput
|
||||
name="Allowed File Types"
|
||||
description="Comma-separated list of extensions (e.g. png,jpg,gif). Leave empty to allow all."
|
||||
value={store.uploadAllowedFileTypes}
|
||||
onChange={v => store.uploadAllowedFileTypes = v}
|
||||
placeholder="png,jpg,gif,mp4,webp"
|
||||
/>
|
||||
</SettingGroup>
|
||||
|
||||
<SettingGroup name="Network" description="Configure browser upload proxying and timeouts.">
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ export enum ServiceType {
|
|||
TEMPSH = "tempsh",
|
||||
FILEBIN = "filebin",
|
||||
PIXELVAULT = "pixelvault",
|
||||
PIXELDRAIN = "pixeldrain"
|
||||
PIXELDRAIN = "pixeldrain",
|
||||
WEBDAV = "webdav"
|
||||
}
|
||||
|
||||
export const serviceLabels: Record<ServiceType, string> = {
|
||||
|
|
@ -39,7 +40,8 @@ export const serviceLabels: Record<ServiceType, string> = {
|
|||
[ServiceType.TEMPSH]: "temp.sh",
|
||||
[ServiceType.FILEBIN]: "filebin.net",
|
||||
[ServiceType.PIXELVAULT]: "PixelVault",
|
||||
[ServiceType.PIXELDRAIN]: "PixelDrain"
|
||||
[ServiceType.PIXELDRAIN]: "PixelDrain",
|
||||
[ServiceType.WEBDAV]: "WebDAV"
|
||||
};
|
||||
|
||||
export const fallbackServiceOrder: ServiceType[] = [
|
||||
|
|
@ -58,6 +60,7 @@ export const fallbackServiceOrder: ServiceType[] = [
|
|||
ServiceType.FILEBIN,
|
||||
ServiceType.PIXELVAULT,
|
||||
ServiceType.PIXELDRAIN,
|
||||
ServiceType.WEBDAV,
|
||||
ServiceType.SHAREX
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const Native = IS_DISCORD_DESKTOP
|
|||
? VencordNative.pluginHelpers.FileUpload as PluginNative<typeof import("../native")>
|
||||
: null;
|
||||
|
||||
const logger = new Logger("FileUpload", "#7cb7ff");
|
||||
export const logger = new Logger("FileUpload", "#7cb7ff");
|
||||
|
||||
function toProxyUrl(url: string): string {
|
||||
const corsProxyUrl = normalizeCorsProxyUrl((settings.store as { corsProxyUrl?: string; }).corsProxyUrl);
|
||||
|
|
@ -594,6 +594,8 @@ export function isConfigured(): boolean {
|
|||
return true;
|
||||
case ServiceType.PIXELVAULT:
|
||||
return Boolean((settings.store as { pixelVaultKey?: string; }).pixelVaultKey);
|
||||
case ServiceType.WEBDAV:
|
||||
return Boolean((settings.store as { webdavUrl?: string; }).webdavUrl);
|
||||
case ServiceType.SHAREX:
|
||||
try {
|
||||
parseShareXConfigFromSettings();
|
||||
|
|
@ -972,6 +974,54 @@ async function uploadToPixelDrain(fileBlob: Blob, filename: string): Promise<str
|
|||
return `https://pixeldrain.com/u/${data.id}`;
|
||||
}
|
||||
|
||||
async function uploadToWebdav(fileBlob: Blob, filename: string): Promise<string> {
|
||||
const { webdavUrl, webdavUsername, webdavPassword, webdavDirectory } = settings.store as {
|
||||
webdavUrl?: string;
|
||||
webdavUsername?: string;
|
||||
webdavPassword?: string;
|
||||
webdavDirectory?: string;
|
||||
};
|
||||
|
||||
if (!webdavUrl) {
|
||||
throw new Error("WebDAV server URL is required");
|
||||
}
|
||||
|
||||
const baseUrl = webdavUrl.replace(/\/+$/, "");
|
||||
const dir = (webdavDirectory || "").replace(/^\/+|\/+$/g, "");
|
||||
const path = dir ? `/${dir}/${encodeURIComponent(filename)}` : `/${encodeURIComponent(filename)}`;
|
||||
const uploadUrl = `${baseUrl}${path}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": fileBlob.type || "application/octet-stream"
|
||||
};
|
||||
|
||||
if (webdavUsername?.trim() && webdavPassword?.trim()) {
|
||||
headers.Authorization = `Basic ${btoa(`${webdavUsername.trim()}:${webdavPassword.trim()}`)}`;
|
||||
}
|
||||
|
||||
if (Native) {
|
||||
const arrayBuffer = await fileBlob.arrayBuffer();
|
||||
const result = await Native.uploadToWebdav(arrayBuffer, uploadUrl, headers);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Upload failed");
|
||||
}
|
||||
return uploadUrl;
|
||||
}
|
||||
|
||||
const response = await uploadRequestWithTimeout(uploadUrl, {
|
||||
method: "PUT",
|
||||
headers,
|
||||
body: fileBlob
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Upload failed: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
return uploadUrl;
|
||||
}
|
||||
|
||||
async function uploadToService(serviceType: ServiceType, fileBlob: Blob, filename: string): Promise<string> {
|
||||
switch (serviceType) {
|
||||
case ServiceType.ZIPLINE:
|
||||
|
|
@ -1006,6 +1056,8 @@ async function uploadToService(serviceType: ServiceType, fileBlob: Blob, filenam
|
|||
return uploadToPixelVault(fileBlob, filename);
|
||||
case ServiceType.PIXELDRAIN:
|
||||
return uploadToPixelDrain(fileBlob, filename);
|
||||
case ServiceType.WEBDAV:
|
||||
return uploadToWebdav(fileBlob, filename);
|
||||
default:
|
||||
throw new Error("Unknown service type");
|
||||
}
|
||||
|
|
@ -1116,6 +1168,25 @@ function finalizeUploadedUrl(url: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
function getAllowedExtensions(): Set<string> | null {
|
||||
const raw = (settings.store as { uploadAllowedFileTypes?: string; }).uploadAllowedFileTypes?.trim();
|
||||
if (!raw) return null;
|
||||
|
||||
const exts = raw.split(/[\s,;]+/).map(e => e.trim().toLowerCase()).filter(Boolean);
|
||||
return exts.length > 0 ? new Set(exts) : null;
|
||||
}
|
||||
|
||||
export function isFileTypeAllowed(file: File): boolean {
|
||||
const allowed = getAllowedExtensions();
|
||||
if (!allowed) return true;
|
||||
|
||||
const dotIndex = file.name.lastIndexOf(".");
|
||||
if (dotIndex < 1 || dotIndex === file.name.length - 1) return false;
|
||||
|
||||
const ext = file.name.slice(dotIndex + 1).toLowerCase();
|
||||
return allowed.has(ext);
|
||||
}
|
||||
|
||||
function getFilenameExtension(filename: string): string | undefined {
|
||||
const dotIndex = filename.lastIndexOf(".");
|
||||
if (dotIndex < 1 || dotIndex === filename.length - 1) return undefined;
|
||||
|
|
@ -1124,7 +1195,7 @@ function getFilenameExtension(filename: string): string | undefined {
|
|||
return ext.length <= 10 ? ext : undefined;
|
||||
}
|
||||
|
||||
async function notifyUploadSuccess(finalUrl: string): Promise<void> {
|
||||
async function notifyUploadSuccess(finalUrl: string, forceSend?: boolean): Promise<void> {
|
||||
if (settings.store.autoCopy) {
|
||||
if (!finalUrl || !finalUrl.trim()) {
|
||||
showToast("Upload successful, but no URL was available to copy", Toasts.Type.MESSAGE);
|
||||
|
|
@ -1142,7 +1213,7 @@ async function notifyUploadSuccess(finalUrl: string): Promise<void> {
|
|||
showToast("Upload successful", Toasts.Type.SUCCESS);
|
||||
}
|
||||
|
||||
const autoSend = Boolean((settings.store as { autoSend?: boolean; }).autoSend);
|
||||
const autoSend = forceSend || Boolean((settings.store as { autoSend?: boolean; }).autoSend);
|
||||
const autoFormat = Boolean((settings.store as { autoFormat?: boolean; }).autoFormat);
|
||||
if (autoSend) {
|
||||
insertTextIntoChatInputBox(autoFormat ? `<${finalUrl}>` : finalUrl);
|
||||
|
|
@ -1274,13 +1345,14 @@ async function normalizeUploadBlob(blob: Blob, sourceUrl?: string): Promise<{ bl
|
|||
};
|
||||
}
|
||||
|
||||
async function uploadPreparedBlob(blob: Blob, sourceUrl?: string): Promise<void> {
|
||||
async function uploadPreparedBlob(blob: Blob, sourceUrl?: string, forceSend?: boolean): Promise<string> {
|
||||
const primary = settings.store.serviceType as ServiceType;
|
||||
const { blob: normalizedBlob, filename } = await normalizeUploadBlob(blob, sourceUrl);
|
||||
setUploadState({ fileName: filename, status: "File ready, starting upload...", percent: 4 });
|
||||
const uploadedUrl = await uploadWithFallbacks(normalizedBlob, filename, primary);
|
||||
const finalUrl = applyEmbedProxy(finalizeUploadedUrl(uploadedUrl));
|
||||
await notifyUploadSuccess(finalUrl);
|
||||
await notifyUploadSuccess(finalUrl, forceSend);
|
||||
return finalUrl;
|
||||
}
|
||||
|
||||
export async function uploadFile(url: string): Promise<void> {
|
||||
|
|
@ -1368,10 +1440,15 @@ export async function uploadPickedFile(): Promise<void> {
|
|||
const file = await chooseFile("*/*");
|
||||
if (!file) return;
|
||||
|
||||
if (!isFileTypeAllowed(file)) {
|
||||
showToast("File type not allowed by current filter", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
await uploadProvidedFiles([file]);
|
||||
}
|
||||
|
||||
export async function uploadProvidedFiles(files: readonly File[]): Promise<void> {
|
||||
export async function uploadProvidedFiles(files: readonly File[], forceSend?: boolean): Promise<void> {
|
||||
if (isUploading) {
|
||||
showToast("Upload already in progress", Toasts.Type.MESSAGE);
|
||||
return;
|
||||
|
|
@ -1384,7 +1461,7 @@ export async function uploadProvidedFiles(files: readonly File[]): Promise<void>
|
|||
|
||||
if (!files.length) return;
|
||||
|
||||
const uploadFiles = files.filter(file => Boolean(file));
|
||||
const uploadFiles = files.filter(file => Boolean(file) && isFileTypeAllowed(file));
|
||||
if (!uploadFiles.length) return;
|
||||
|
||||
isUploading = true;
|
||||
|
|
@ -1408,7 +1485,7 @@ export async function uploadProvidedFiles(files: readonly File[]): Promise<void>
|
|||
canCancel: true
|
||||
});
|
||||
|
||||
await uploadPreparedBlob(file);
|
||||
await uploadPreparedBlob(file, undefined, forceSend);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue