Merge branch 'dev'
This commit is contained in:
commit
e37a5e3625
11 changed files with 236 additions and 90 deletions
|
|
@ -168,20 +168,19 @@ export default definePlugin({
|
|||
},
|
||||
// member list
|
||||
{
|
||||
find: "._areActivitiesExperimentallyHidden=(",
|
||||
replacement: {
|
||||
match: /(?<=user:(\i),guildId:\i,channel:(\i).*?)BOOST_GEM_ICON.{0,10}\);/,
|
||||
replace: "$&if($self.shouldHideUser($1.id, $2.id)) return null; "
|
||||
}
|
||||
},
|
||||
// stop the role header from displaying if all users with that role are hidden (wip sorta)
|
||||
{
|
||||
find: "._areActivitiesExperimentallyHidden=(",
|
||||
replacement: {
|
||||
match: /\i.memo\(function\(\i\){/,
|
||||
replace: "$&if($self.isRoleAllBlockedMembers(arguments[0].id, arguments[0].guildId)) return null;"
|
||||
},
|
||||
predicate: () => settings.store.hideEmptyRoles
|
||||
find: "this.updateMaxContentFeedRowSeen()",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=user:(\i),guildId:\i,channel:(\i).*?)BOOST_GEM_ICON.{0,10}\);/,
|
||||
replace: "$&if($self.shouldHideUser($1.id, $2.id)) return null; "
|
||||
},
|
||||
// stop the role header from displaying if all users with that role are hidden (wip sorta)
|
||||
{
|
||||
match: /\i.memo\(function\(\i\){/,
|
||||
replace: "$&if($self.isRoleAllBlockedMembers(arguments[0].id, arguments[0].guildId)) return null;",
|
||||
predicate: () => settings.store.hideEmptyRoles
|
||||
},
|
||||
]
|
||||
},
|
||||
// "1 blocked message"
|
||||
{
|
||||
|
|
|
|||
11
src/equicordplugins/fileUpload/constants.ts
Normal file
11
src/equicordplugins/fileUpload/constants.ts
Normal file
|
|
@ -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)}`;
|
||||
}
|
||||
|
|
@ -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 <ProgressBar />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection tag="label" name="Intercept Discord Upload Button" description="Use FileUpload when uploading through Discord's file picker" inlineSetting>
|
||||
<Switch
|
||||
checked={Boolean((store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload)}
|
||||
onChange={v => (store as { interceptDiscordUpload?: boolean; }).interceptDiscordUpload = v}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection name="Upload Timeout" description="Maximum time to wait per upload attempt before switching to fallback">
|
||||
<Select
|
||||
options={[
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { toProxiedUrl } from "@equicordplugins/fileUpload/constants";
|
||||
import { settings } from "@equicordplugins/fileUpload/settings";
|
||||
import { PluginNative } from "@utils/types";
|
||||
|
||||
|
|
@ -187,7 +188,7 @@ export async function uploadToS3(
|
|||
throw new Error(result.error || "Upload failed");
|
||||
}
|
||||
} else {
|
||||
const response = await fetch(uploadUrl.toString(), {
|
||||
const response = await fetch(toProxiedUrl(uploadUrl.toString()), {
|
||||
method: "PUT",
|
||||
headers: requestHeaders,
|
||||
body: fileBlob
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { CORS_PROXY, toProxiedUrl } from "@equicordplugins/fileUpload/constants";
|
||||
import { settings } from "@equicordplugins/fileUpload/settings";
|
||||
import { serviceLabels, ServiceType, ShareXUploaderConfig, UploadResponse } from "@equicordplugins/fileUpload/types";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
|
|
@ -24,7 +25,13 @@ const Native = IS_DISCORD_DESKTOP
|
|||
|
||||
const logger = new Logger("FileUpload", "#7cb7ff");
|
||||
|
||||
const CORS_PROXY = "https://cors.keiran0.workers.dev"; // im hosting this on cloudflare workers so uptime and latency should be reliable
|
||||
function toProxyUrl(url: string): string {
|
||||
if (Native || url.startsWith(`${CORS_PROXY}?url=`)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return toProxiedUrl(url);
|
||||
}
|
||||
|
||||
let isUploading = false;
|
||||
|
||||
|
|
@ -59,6 +66,14 @@ const uploadStateListeners = new Set<() => void>();
|
|||
let activeAbortController: AbortController | null = null;
|
||||
let cancelRequested = false;
|
||||
|
||||
function isUploadCancelledError(error: unknown): boolean {
|
||||
if (cancelRequested) return true;
|
||||
if (!(error instanceof Error)) return false;
|
||||
|
||||
const message = error.message.toLowerCase();
|
||||
return message.includes("cancelled") || message.includes("canceled") || message.includes("aborted") || message.includes("aborterror");
|
||||
}
|
||||
|
||||
function emitUploadState() {
|
||||
for (const listener of uploadStateListeners) {
|
||||
listener();
|
||||
|
|
@ -112,9 +127,10 @@ async function fetchWithTimeout(url: string, options: RequestInit): Promise<Resp
|
|||
const controller = new AbortController();
|
||||
activeAbortController = controller;
|
||||
const timeout = setTimeout(() => controller.abort(), getUploadTimeoutMs());
|
||||
const requestUrl = toProxyUrl(url);
|
||||
|
||||
try {
|
||||
return await fetch(url, {
|
||||
return await fetch(requestUrl, {
|
||||
...options,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
|
@ -194,17 +210,7 @@ async function uploadToShareX(fileBlob: Blob, filename: string): Promise<string>
|
|||
throw new Error(`Unsupported ShareX Body type: ${config.Body || "unknown"}`);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetchWithTimeout(requestUrl, { method, headers, body });
|
||||
} catch (error) {
|
||||
if (Native) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const proxiedUrl = `${CORS_PROXY}?url=${encodeURIComponent(requestUrl)}`;
|
||||
response = await fetchWithTimeout(proxiedUrl, { method, headers, body });
|
||||
}
|
||||
const response = await fetchWithTimeout(requestUrl, { method, headers, body });
|
||||
|
||||
const responseText = await response.text();
|
||||
let responseJson: unknown = null;
|
||||
|
|
@ -302,9 +308,7 @@ async function uploadToNest(fileBlob: Blob, filename: string): Promise<string> {
|
|||
const formData = new FormData();
|
||||
formData.append("file", fileBlob, filename);
|
||||
|
||||
const proxiedUrl = `${CORS_PROXY}?url=${encodeURIComponent("https://nest.rip/api/files/upload")}`;
|
||||
|
||||
const response = await fetchWithTimeout(proxiedUrl, {
|
||||
const response = await fetchWithTimeout("https://nest.rip/api/files/upload", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": nestToken
|
||||
|
|
@ -394,8 +398,7 @@ async function uploadToEzHost(fileBlob: Blob, filename: string): Promise<string>
|
|||
|
||||
const headers: Record<string, string> = { key: ezHostKey };
|
||||
|
||||
const proxiedUrl = `${CORS_PROXY}?url=${encodeURIComponent("https://api.e-z.host/files")}`;
|
||||
const response = await fetchWithTimeout(proxiedUrl, {
|
||||
const response = await fetchWithTimeout("https://api.e-z.host/files", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: formData
|
||||
|
|
@ -428,7 +431,7 @@ async function uploadToCatbox(fileBlob: Blob, filename: string): Promise<string>
|
|||
if (catboxUserhash) formData.append("userhash", catboxUserhash);
|
||||
formData.append("fileToUpload", fileBlob, filename);
|
||||
|
||||
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent("https://catbox.moe/user/api.php")}`, {
|
||||
const response = await fetchWithTimeout("https://catbox.moe/user/api.php", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
|
@ -472,7 +475,7 @@ async function uploadToLitterbox(fileBlob: Blob, filename: string): Promise<stri
|
|||
formData.append("time", expiry);
|
||||
formData.append("fileToUpload", fileBlob, filename);
|
||||
|
||||
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent("https://litterbox.catbox.moe/resources/internals/api.php")}`, {
|
||||
const response = await fetchWithTimeout("https://litterbox.catbox.moe/resources/internals/api.php", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
|
@ -499,8 +502,7 @@ async function uploadToGofile(fileBlob: Blob, filename: string): Promise<string>
|
|||
formData.append("file", fileBlob, filename);
|
||||
|
||||
const uploadUrl = "https://upload.gofile.io/uploadfile";
|
||||
const requestUrl = Native ? uploadUrl : `${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`;
|
||||
const response = await fetchWithTimeout(requestUrl, {
|
||||
const response = await fetchWithTimeout(uploadUrl, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
|
@ -535,7 +537,7 @@ async function uploadToTmpfiles(fileBlob: Blob, filename: string): Promise<strin
|
|||
formData.append("file", fileBlob, filename);
|
||||
|
||||
const uploadUrl = "https://tmpfiles.org/api/v1/upload";
|
||||
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`, {
|
||||
const response = await fetchWithTimeout(uploadUrl, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
|
@ -563,7 +565,7 @@ async function uploadToBuzzheavier(fileBlob: Blob, filename: string): Promise<st
|
|||
}
|
||||
|
||||
const uploadUrl = `https://w.buzzheavier.com/${encodeURIComponent(filename)}`;
|
||||
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`, {
|
||||
const response = await fetchWithTimeout(uploadUrl, {
|
||||
method: "PUT",
|
||||
body: fileBlob
|
||||
});
|
||||
|
|
@ -598,7 +600,7 @@ async function uploadToTempSh(fileBlob: Blob, filename: string): Promise<string>
|
|||
formData.append("file", fileBlob, filename);
|
||||
|
||||
const uploadUrl = "https://temp.sh/upload";
|
||||
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`, {
|
||||
const response = await fetchWithTimeout(uploadUrl, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
|
@ -630,7 +632,7 @@ async function uploadToFilebin(fileBlob: Blob, filename: string): Promise<string
|
|||
const formData = new FormData();
|
||||
formData.append("file", fileBlob, filename);
|
||||
|
||||
const response = await fetchWithTimeout(`${CORS_PROXY}?url=${encodeURIComponent(uploadUrl)}`, {
|
||||
const response = await fetchWithTimeout(uploadUrl, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
|
@ -756,10 +758,20 @@ function finalizeUploadedUrl(url: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
function notifyUploadSuccess(finalUrl: string): void {
|
||||
async function notifyUploadSuccess(finalUrl: string): Promise<void> {
|
||||
if (settings.store.autoCopy) {
|
||||
copyToClipboard(finalUrl);
|
||||
showToast("Upload successful, URL copied to clipboard", Toasts.Type.SUCCESS);
|
||||
if (!finalUrl || !finalUrl.trim()) {
|
||||
showToast("Upload successful, but no URL was available to copy", Toasts.Type.MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyToClipboard(finalUrl);
|
||||
showToast("Upload successful, URL copied to clipboard", Toasts.Type.SUCCESS);
|
||||
} catch (error) {
|
||||
logger.warn("Upload succeeded but clipboard copy failed", error);
|
||||
showToast("Upload successful, but failed to copy URL", Toasts.Type.MESSAGE);
|
||||
}
|
||||
} else {
|
||||
showToast("Upload successful", Toasts.Type.SUCCESS);
|
||||
}
|
||||
|
|
@ -789,6 +801,8 @@ async function uploadWithFallbacks(fileBlob: Blob, filename: string, primary: Se
|
|||
});
|
||||
|
||||
for (const service of uploadOrder) {
|
||||
if (cancelRequested) throw new Error("Upload cancelled by user");
|
||||
|
||||
const attempt = attempted.length + 1;
|
||||
setUploadState({
|
||||
phase: attempt === 1 ? "uploading" : "retrying",
|
||||
|
|
@ -819,11 +833,12 @@ async function uploadWithFallbacks(fileBlob: Blob, filename: string, primary: Se
|
|||
|
||||
return uploadedUrl;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
if (message === "Upload cancelled by user") {
|
||||
if (isUploadCancelledError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
|
||||
attempted.push(serviceLabels[service]);
|
||||
lastError = message;
|
||||
logger.warn(`Upload failed for ${serviceLabels[service]}: ${message}`);
|
||||
|
|
@ -875,7 +890,7 @@ async function uploadPreparedBlob(blob: Blob, sourceUrl?: string): Promise<void>
|
|||
setUploadState({ fileName: filename, status: "File ready, starting upload...", percent: 4 });
|
||||
const uploadedUrl = await uploadWithFallbacks(normalizedBlob, filename, primary);
|
||||
const finalUrl = finalizeUploadedUrl(uploadedUrl);
|
||||
notifyUploadSuccess(finalUrl);
|
||||
await notifyUploadSuccess(finalUrl);
|
||||
}
|
||||
|
||||
export async function uploadFile(url: string): Promise<void> {
|
||||
|
|
@ -926,7 +941,9 @@ export async function uploadFile(url: string): Promise<void> {
|
|||
blob = await response.blob();
|
||||
}
|
||||
} else {
|
||||
const response = await fetch(fetchUrl);
|
||||
const response = await fetchWithTimeout(fetchUrl, {
|
||||
method: "GET"
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch file: ${response.status}`);
|
||||
}
|
||||
|
|
@ -941,7 +958,7 @@ export async function uploadFile(url: string): Promise<void> {
|
|||
await uploadPreparedBlob(blob, url);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
if (message === "Upload cancelled by user") {
|
||||
if (isUploadCancelledError(error)) {
|
||||
showToast("Upload cancelled", Toasts.Type.MESSAGE);
|
||||
setUploadState({ phase: "cancelled", status: "Upload cancelled.", canCancel: false, percent: 0 });
|
||||
} else {
|
||||
|
|
@ -957,6 +974,13 @@ export async function uploadFile(url: string): Promise<void> {
|
|||
}
|
||||
|
||||
export async function uploadPickedFile(): Promise<void> {
|
||||
const file = await chooseFile("*/*");
|
||||
if (!file) return;
|
||||
|
||||
await uploadProvidedFiles([file]);
|
||||
}
|
||||
|
||||
export async function uploadProvidedFiles(files: readonly File[]): Promise<void> {
|
||||
if (isUploading) {
|
||||
showToast("Upload already in progress", Toasts.Type.MESSAGE);
|
||||
return;
|
||||
|
|
@ -967,30 +991,37 @@ export async function uploadPickedFile(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const file = await chooseFile("*/*");
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (!files.length) return;
|
||||
|
||||
const uploadFiles = files.filter(file => Boolean(file));
|
||||
if (!uploadFiles.length) return;
|
||||
|
||||
isUploading = true;
|
||||
cancelRequested = false;
|
||||
setUploadState({
|
||||
phase: "preparing",
|
||||
fileName: file.name,
|
||||
currentService: null,
|
||||
currentServiceLabel: "",
|
||||
attempt: 0,
|
||||
totalAttempts: 0,
|
||||
percent: 2,
|
||||
status: `Preparing ${file.name}...`,
|
||||
canCancel: true
|
||||
});
|
||||
|
||||
try {
|
||||
await uploadPreparedBlob(file);
|
||||
for (let i = 0; i < uploadFiles.length; i++) {
|
||||
const file = uploadFiles[i];
|
||||
const current = i + 1;
|
||||
const suffix = uploadFiles.length > 1 ? ` (${current}/${uploadFiles.length})` : "";
|
||||
|
||||
setUploadState({
|
||||
phase: "preparing",
|
||||
fileName: file.name,
|
||||
currentService: null,
|
||||
currentServiceLabel: "",
|
||||
attempt: 0,
|
||||
totalAttempts: 0,
|
||||
percent: 2,
|
||||
status: `Preparing ${file.name}${suffix}...`,
|
||||
canCancel: true
|
||||
});
|
||||
|
||||
await uploadPreparedBlob(file);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
if (message === "Upload cancelled by user") {
|
||||
if (isUploadCancelledError(error)) {
|
||||
showToast("Upload cancelled", Toasts.Type.MESSAGE);
|
||||
setUploadState({ phase: "cancelled", status: "Upload cancelled.", canCancel: false, percent: 0 });
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default definePlugin({
|
|||
authors: [Devs.Samwich],
|
||||
patches: [
|
||||
{
|
||||
find: "._areActivitiesExperimentallyHidden=(",
|
||||
find: "this.updateMaxContentFeedRowSeen()",
|
||||
replacement: {
|
||||
match: /return \i===\i\.\i\.UNKNOWN/,
|
||||
replace: "return null;$&"
|
||||
|
|
|
|||
|
|
@ -1586,7 +1586,7 @@ export default definePlugin({
|
|||
// Whether preloading assets is enabled or not, the placeholders loading
|
||||
// before the assets causes a lot of element shifting, whereas if
|
||||
// the elements load immediately instead, it doesn't.
|
||||
find: ",{rewardWithArticleHook:()",
|
||||
find: ".LEARN_MORE_CTA_AND_EXPRESSIVE_BUTTON_TREATMENT_FOUR_OPEN_GAME_LINK,sourceQuestContent:",
|
||||
replacement: {
|
||||
match: /showPlaceholder:!\i/,
|
||||
replace: "showPlaceholder:false"
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
renderProfileComponent: ErrorBoundary.wrap(({ user, isSideBar = false }: { user: User; isSideBar?: boolean; }) => {
|
||||
const [reviewData] = useAwaiter(() => getReviews(user.id), { deps: [user.id], fallbackValue: null });
|
||||
const [reviewData] = useAwaiter(() => getReviews(user.id, 0, true), { deps: [user.id], fallbackValue: null });
|
||||
|
||||
// Discord are masters at using a crap ton of html elements and css classes to create a simple ui that could have
|
||||
// been made with less than half of the number of elements, so we have to do this insanity to replicate their ui
|
||||
|
|
@ -170,20 +170,24 @@ export default definePlugin({
|
|||
<Paragraph size={isSideBar ? "sm" : "xs"} weight="medium">User Reviews</Paragraph>
|
||||
{!!reviewData?.reviewCount && (
|
||||
<div className={ProfileCardContainerClasses.icons}>
|
||||
{reviewData.reviews.slice(0, 4).map((review, idx) => {
|
||||
const showCount = idx === 3 && reviewData.reviewCount > 4;
|
||||
{reviewData.reviews
|
||||
.filter(review => review.id !== 0)
|
||||
.slice(0, 4)
|
||||
.reverse()
|
||||
.map((review, idx) => {
|
||||
const showCount = idx === 3 && reviewData.reviewCount > 4;
|
||||
|
||||
return (
|
||||
<div className={ProfileCardContainerClasses.icon} key={review.id}>
|
||||
<img src={review.sender.profilePhoto} alt={review.sender.username} className={showCount ? ProfileCardContainerClasses.displayCount : undefined} />
|
||||
{showCount && (
|
||||
<div className={ProfileCardContainerClasses.displayCountText}>
|
||||
<Span className={ProfileCardContainerClasses.displayCountTextColor} size="xs" weight="medium">+{reviewData.reviewCount - 3}</Span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<div className={ProfileCardContainerClasses.icon} key={review.id}>
|
||||
<img src={review.sender.profilePhoto} alt={review.sender.username} className={showCount ? ProfileCardContainerClasses.displayCount : undefined} />
|
||||
{showCount && (
|
||||
<div className={ProfileCardContainerClasses.displayCountText}>
|
||||
<Span className={ProfileCardContainerClasses.displayCountTextColor} size="xs" weight="medium">+{reviewData.reviewCount - 3}</Span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,14 +47,14 @@ async function rdbRequest(path: string, options: RequestInit = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getReviews(id: string, offset = 0): Promise<UserReviewsData> {
|
||||
export async function getReviews(id: string, offset = 0, isProfileComponent = false): Promise<UserReviewsData> {
|
||||
let flags = 0;
|
||||
if (!settings.store.showWarning) flags |= WarningFlag;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
flags: String(flags),
|
||||
offset: String(offset),
|
||||
limit: "4"
|
||||
...(isProfileComponent ? { limit: "4" } : {})
|
||||
});
|
||||
const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -1272,6 +1272,10 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "m.shl",
|
||||
id: 1025245410224263258n,
|
||||
},
|
||||
ScattrdBlade: {
|
||||
name: "ScattrdBlade",
|
||||
id: 678007540608532491n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue