diff --git a/src/equicordplugins/fileUpload/constants.ts b/src/equicordplugins/fileUpload/constants.ts
new file mode 100644
index 000000000..9742e2dbe
--- /dev/null
+++ b/src/equicordplugins/fileUpload/constants.ts
@@ -0,0 +1,11 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2026 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+export const CORS_PROXY = "https://cors.keiran0.workers.dev";
+
+export function toProxiedUrl(url: string): string {
+ return `${CORS_PROXY}?url=${encodeURIComponent(url)}`;
+}
diff --git a/src/equicordplugins/fileUpload/index.tsx b/src/equicordplugins/fileUpload/index.tsx
index f7cda948e..9515ee36b 100644
--- a/src/equicordplugins/fileUpload/index.tsx
+++ b/src/equicordplugins/fileUpload/index.tsx
@@ -12,23 +12,75 @@ import { OpenExternalIcon } from "@components/Icons";
import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import definePlugin from "@utils/types";
-import { Menu, PermissionsBits, PermissionStore, React, useEffect, useState } from "@webpack/common";
+import { DraftType, FluxDispatcher, Menu, PermissionsBits, PermissionStore, React, useEffect, useState } from "@webpack/common";
import { settings } from "./settings";
import { serviceLabels, ServiceType } from "./types";
import { getMediaUrl } from "./utils/getMediaUrl";
-import { cancelCurrentUpload, getUploadState, subscribeUploadState, uploadFile, uploadPickedFile } from "./utils/upload";
+import { cancelCurrentUpload, getUploadState, isConfigured, subscribeUploadState, uploadFile, uploadPickedFile, uploadProvidedFiles } from "./utils/upload";
const cl = classNameFactory("vc-file-upload-");
+let uploadAddFilesInterceptor: ((event: unknown) => void) | null = null;
+
+type UploadAddFilesEvent = {
+ type: string;
+ files?: unknown;
+ uploads?: unknown;
+ items?: unknown;
+ draftType?: unknown;
+};
+
+function extractFilesFromValue(value: unknown): File[] {
+ if (value instanceof File) return [value];
+
+ if (!Array.isArray(value)) return [];
+
+ return value.flatMap(entry => {
+ if (entry instanceof File) return [entry];
+
+ if (!entry || typeof entry !== "object") return [];
+
+ const uploadFile = "file" in entry ? entry.file : null;
+ if (uploadFile instanceof File) return [uploadFile];
+
+ const item = "item" in entry && entry.item && typeof entry.item === "object" ? entry.item : null;
+ if (!item || !("file" in item)) return [];
+
+ return item.file instanceof File ? [item.file] : [];
+ });
+}
+
+function interceptUploadAddFiles(event: unknown): void {
+ if (!event || typeof event !== "object" || !("type" in event)) return;
+
+ const payload = event as UploadAddFilesEvent;
+ if (payload.type !== "UPLOAD_ATTACHMENT_ADD_FILES") return;
+
+ if (payload.draftType !== DraftType.ChannelMessage) return;
+
+ if (!Boolean((settings.store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload) || !isConfigured()) return;
+
+ const files = [
+ ...extractFilesFromValue(payload.files),
+ ...extractFilesFromValue(payload.uploads),
+ ...extractFilesFromValue(payload.items)
+ ];
+ const uniqueFiles = Array.from(new Set(files));
+
+ if (!uniqueFiles.length) return;
+
+ payload.files = [];
+ payload.uploads = [];
+ payload.items = [];
+ void uploadProvidedFiles(uniqueFiles);
+}
const ProgressBarInner = () => {
const [state, setState] = useState(getUploadState);
useEffect(() => subscribeUploadState(() => setState(getUploadState())), []);
- if (state.phase === "idle") {
- return null;
- }
+ if (state.phase === "idle") return null;
const percentage = Math.max(0, Math.min(100, state.percent));
@@ -157,13 +209,44 @@ export default definePlugin({
match: /(textValue:.{0,50}channelId:\i\.id\}\))(?:,\i(,))?/,
replace: "$1,$self.renderUploadProgress()$2"
}
- }
+ },
+ // forces an early return on the file size limit nitro upsell modal
+ {
+ find: "#{intl::tRuxk9::raw}",
+ replacement: {
+ match: /(?<=\.limits\.fileSize.{0,50})Array\.from\(\i\)\.some/,
+ replace: "$self.shouldBypassDiscordUploadSizeCheck()?false:$&"
+ }
+ },
],
contextMenus: {
"message": messageContextMenuPatch,
"image-context": imageContextMenuPatch,
"channel-attach": channelAttachMenuPatch
},
+ start() {
+ if (uploadAddFilesInterceptor) {
+ return;
+ }
+
+ uploadAddFilesInterceptor = event => interceptUploadAddFiles(event);
+ FluxDispatcher.addInterceptor(uploadAddFilesInterceptor);
+ },
+ stop() {
+ if (!uploadAddFilesInterceptor) {
+ return;
+ }
+
+ const index = FluxDispatcher._interceptors.indexOf(uploadAddFilesInterceptor);
+ if (index > -1) {
+ FluxDispatcher._interceptors.splice(index, 1);
+ }
+
+ uploadAddFilesInterceptor = null;
+ },
+ shouldBypassDiscordUploadSizeCheck(): boolean {
+ return Boolean((settings.store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload) && isConfigured();
+ },
renderUploadProgress() {
return ;
}
diff --git a/src/equicordplugins/fileUpload/settings.tsx b/src/equicordplugins/fileUpload/settings.tsx
index 89ad2bc44..a682afc21 100644
--- a/src/equicordplugins/fileUpload/settings.tsx
+++ b/src/equicordplugins/fileUpload/settings.tsx
@@ -166,6 +166,12 @@ export const settings = definePluginSettings({
default: false,
hidden: true
},
+ interceptDiscordUpload: {
+ type: OptionType.BOOLEAN,
+ description: "Intercept Discord uploads and use FileUpload instead.",
+ default: false,
+ hidden: true
+ },
gofileToken: {
type: OptionType.STRING,
description: "Optional GoFile API token",
@@ -505,6 +511,13 @@ export function SettingsComponent() {
/>
+
+ (store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload = v}
+ />
+
+