/* * Vencord, a Discord client mod * Copyright (c) 2026 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import { Button } from "@components/Button"; import { Flex } from "@components/Flex"; import { Heading } from "@components/Heading"; import { Paragraph } from "@components/Paragraph"; import type { RenderModalProps } from "@vencord/discord-types"; import { Checkbox, FluxDispatcher, MessageActions, Modal, openModal, PendingReplyStore, showToast, Toasts, useEffect, useState } from "@webpack/common"; import { ApplicationField, BooleanField, DateTimeField, getDateTimeLocalValue, ParticipantField, TextField } from "./fields"; import { abortActiveClipUploads, type ClipMetadata, getClipCreatedAt, getClipTitleFromName, getDefaultClipTitle, getDefaultFileName, getErrorMessage, getParticipantIds, getString, isValidDate, pickClipFile, uploadClipFile } from "./upload"; export function openUploadClipFileModal(channelId: string, clip?: ClipMetadata | null) { openModal(modalProps => ( )); } function UploadClipFileModal({ modalProps, channelId, clip }: { modalProps: RenderModalProps; channelId: string; clip?: ClipMetadata | null; }) { const defaultFileName = getDefaultFileName(); const [file, setFile] = useState(null); const [fileName, setFileName] = useState(defaultFileName); const [participants, setParticipants] = useState(getParticipantIds(clip)); const [title, setTitle] = useState(getDefaultClipTitle(clip)); const [spoiler, setSpoiler] = useState(false); const [remix, setRemix] = useState(false); const [thumbnail, setThumbnail] = useState(false); const [createdAt, setCreatedAt] = useState(getDateTimeLocalValue(getClipCreatedAt(clip))); const [message, setMessage] = useState(""); const [applicationId, setApplicationId] = useState(getString(clip?.applicationId) ?? ""); const [uploading, setUploading] = useState(false); const [parseMetadata, setParseMetadata] = useState(false); const canUpload = Boolean(file && fileName.trim() && title.trim() && isValidDate(createdAt)) && !uploading; const notice = createdAt && !isValidDate(createdAt) ? { message: "Created at must be a valid date.", type: "critical" as const } : undefined; useEffect(() => abortActiveClipUploads, []); async function chooseClipFile() { let result; try { result = await pickClipFile(parseMetadata); if (!result) return; } catch (error) { showToast(getErrorMessage(error), Toasts.Type.FAILURE); return; } const { file: picked, metadata } = result; setFile(picked); setFileName(name => name === defaultFileName ? picked.name : name); const appName = metadata?.applicationName; const appUsers = metadata?.users; const appId = metadata?.applicationId; setTitle(currentTitle => appName || currentTitle || getClipTitleFromName(picked.name)); if (appUsers?.length) setParticipants(current => current.length ? current : appUsers); if (appId) setApplicationId(current => current || appId); } async function submit() { if (!file || !canUpload) return; setUploading(true); const reply = PendingReplyStore.getPendingReply(channelId); if (reply) FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId }); const success = await uploadClipFile(file, { fileName: fileName.trim(), participants, title: title.trim(), spoiler, remix, thumbnail, createdAt: new Date(createdAt).toISOString(), message, channelId, applicationId: applicationId.trim() || undefined, remoteClipId: getString(clip?.remoteClipId), eventsTimeline: clip?.eventsTimeline, messageReference: reply ? MessageActions.getSendMessageOptionsForReply(reply)?.messageReference : null, }); if (success) { modalProps.onClose(); return; } setUploading(false); } return ( void submit(), disabled: !canUpload } ]} >
File {file?.name ?? "No file selected"} setParseMetadata(checked)} disabled={uploading} type="row" > Parse clip metadata from file
); }