diff --git a/src/equicordplugins/fileUpload/utils/getMediaUrl.ts b/src/equicordplugins/fileUpload/utils/getMediaUrl.ts index cc1caab65..11e94cc79 100644 --- a/src/equicordplugins/fileUpload/utils/getMediaUrl.ts +++ b/src/equicordplugins/fileUpload/utils/getMediaUrl.ts @@ -55,8 +55,8 @@ const extensionToMime: Record = { "mkv": "video/x-matroska" }; -export function getMimeFromExtension(ext: string): string { - return extensionToMime[ext.toLowerCase()] || "application/octet-stream"; +export function getMimeFromExtension(ext?: string): string { + return extensionToMime[ext?.toLowerCase() ?? ""] || "application/octet-stream"; } export async function getExtensionFromBytes(blob: Blob): Promise { @@ -113,7 +113,7 @@ export function isSupported(url: string): boolean { return ext ? supportedExtensions.includes(ext) : false; } -export function getMediaUrl(props: { src?: string; href?: string; itemSrc?: string; itemHref?: string; target?: any }): string | null { +export function getMediaUrl(props: { src?: string; href?: string; itemSrc?: string; itemHref?: string; target?: any; }): string | null { const url = props.src || props.href || props.itemSrc || props.itemHref; if (url && isSupported(url)) return url; diff --git a/src/equicordplugins/moreStickers/index.tsx b/src/equicordplugins/moreStickers/index.tsx index 19a9b104b..97bfbdaee 100644 --- a/src/equicordplugins/moreStickers/index.tsx +++ b/src/equicordplugins/moreStickers/index.tsx @@ -18,7 +18,12 @@ import { getStickerPack, getStickerPackMetas } from "./stickers"; import { StickerPack, StickerPackMeta } from "./types"; import { cl, FFmpegStateContext, loadFFmpeg } from "./utils"; -const settings = definePluginSettings({ +export const settings = definePluginSettings({ + promptToUpload: { + type: OptionType.BOOLEAN, + description: "Inserts the sticker into your chatbar instead of sending immediately", + default: false + }, packs: { type: OptionType.COMPONENT, description: "Packs", @@ -72,13 +77,6 @@ export default definePlugin({ } ] }, - { - find: '==="remove_text"', - replacement: { - match: /,\i\.insertText=\i=>{[\w ;]*?1===\i\.length&&.+?==="remove_text"/, - replace: ",$self.textEditor=arguments[0]$&" - } - } ], stickerButton({ innerClassName, diff --git a/src/equicordplugins/moreStickers/upload.ts b/src/equicordplugins/moreStickers/upload.ts index 64d3e48b2..75cf1ae53 100644 --- a/src/equicordplugins/moreStickers/upload.ts +++ b/src/equicordplugins/moreStickers/upload.ts @@ -4,17 +4,23 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { getMimeFromExtension } from "@equicordplugins/fileUpload/utils/getMediaUrl"; import { FFmpeg } from "@ffmpeg/ffmpeg"; import { fetchFile } from "@ffmpeg/util"; -import { findByPropsLazy } from "@webpack"; -import { ChannelStore, UploadHandler } from "@webpack/common"; +import { insertTextIntoChatInputBox, MessageOptions } from "@utils/discord"; +import { CloudUploadPlatform } from "@vencord/discord-types/enums"; +import { ChannelStore, CloudUploader, Constants, DraftStore, FluxDispatcher, MessageActions, PendingReplyStore, RestAPI, showToast, SnowflakeUtils, Toasts, UploadHandler } from "@webpack/common"; -import moreStickers from "."; +import { settings } from "."; import { FFmpegState, Sticker } from "./types"; -const PendingReplyStore = findByPropsLazy("getPendingReply"); -const MessageUtils = findByPropsLazy("sendMessage"); -const DraftStore = findByPropsLazy("getDraft", "getState"); +type SendStickerOptions = { + channelId: string; + sticker: Sticker; + ctrlKey: boolean; + shiftKey: boolean; + ffmpegState?: FFmpegState; +}; export const ffmpeg = new FFmpeg(); @@ -97,88 +103,64 @@ async function toGIF(url: string, ffmpeg: FFmpeg): Promise { }); } -export async function sendSticker({ - channelId, - sticker, - sendAsLink, - ctrlKey, - shiftKey, - ffmpegState -}: { channelId: string; sticker: Sticker; sendAsLink?: boolean; ctrlKey: boolean; shiftKey: boolean; ffmpegState?: FFmpegState; }) { +export async function sendSticker({ channelId, sticker, ctrlKey, shiftKey, ffmpegState }: SendStickerOptions) { + const reply = PendingReplyStore.getPendingReply(channelId); + let content = DraftStore.getDraft(channelId, 0); + let options: Partial = {}; + let file: File; - let messageContent = ""; - const { textEditor } = moreStickers; - if (DraftStore) { - messageContent = DraftStore.getDraft(channelId, 0); + if (reply) { + FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId }); + options = MessageActions.getSendMessageOptionsForReply(reply); } - let messageOptions = {}; - if (PendingReplyStore) { - const pendingReply = PendingReplyStore.getPendingReply(channelId); - if (pendingReply) { - messageOptions = MessageUtils.getSendMessageOptionsForReply(pendingReply); - } + if (shiftKey) { + if (!content.endsWith(" ") && !content.endsWith("\n")) content = " "; + content += sticker.image; + + return ctrlKey + ? insertTextIntoChatInputBox(content) + : MessageActions._sendMessage(channelId, { content: sticker.image }, options); } - if ((ctrlKey || !sendAsLink) && !shiftKey) { - let file: File | null = null; - - if (sticker?.isAnimated) { - if (!ffmpegState) { - throw new Error("FFmpeg state is not provided"); - } - if (!ffmpegState?.ffmpeg) { - throw new Error("FFmpeg is not provided"); - } - if (!ffmpegState?.isLoaded) { - throw new Error("FFmpeg is not loaded"); - } - - file = await toGIF(sticker.image, ffmpegState.ffmpeg); - } - else { - const url = new URL(sticker.image); - url.searchParams.set("t", Date.now().toString()); // To prevent caching, in order to avoid CORS bug in Chrome - const response = await fetch(sticker.image); - const orgImageUrl = URL.createObjectURL(await response.blob()); - const processedImage = await resizeImage(orgImageUrl); - - const filename = sticker.filename ?? (new URL(sticker.image)).pathname.split("/").pop(); - let mimeType = "image/png"; - switch (filename?.split(".").pop()?.toLowerCase()) { - case "jpg": - case "jpeg": - mimeType = "image/jpeg"; - break; - case "gif": - mimeType = "image/gif"; - break; - case "webp": - mimeType = "image/webp"; - break; - case "svg": - mimeType = "image/svg+xml"; - break; - } - file = new File([processedImage], filename!, { type: mimeType }); - } - - UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), 0); - return; - } else if (shiftKey) { - if (!messageContent.endsWith(" ") || !messageContent.endsWith("\n")) messageContent += " "; - messageContent += sticker.image; - - if (ctrlKey && textEditor && textEditor.insertText && typeof textEditor.insertText === "function") { - textEditor.insertText(messageContent); - } else { - MessageUtils._sendMessage(channelId, { - content: sticker.image - }, messageOptions || {}); - } + if (sticker?.isAnimated) { + if (!ffmpegState?.ffmpeg || !ffmpegState.isLoaded) throw new Error("FFmpeg not ready"); + file = await toGIF(sticker.image, ffmpegState.ffmpeg); } else { - MessageUtils._sendMessage(channelId, { - content: `${messageContent} ${sticker.image}`.trim() - }, messageOptions || {}); + const res = await fetch(sticker.image); + const blobUrl = URL.createObjectURL(await res.blob()); + const processed = await resizeImage(blobUrl); + const filename = sticker.filename ?? new URL(sticker.image).pathname.split("/").pop()!; + const mimeType = getMimeFromExtension(filename.split(".").pop()); + + file = new File([processed], filename, { type: mimeType }); } + + if (settings.store.promptToUpload || content) return UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), 0); + + const upload = new CloudUploader({ file, platform: CloudUploadPlatform.WEB }, channelId); + + upload.on("complete", () => { + RestAPI.post({ + url: Constants.Endpoints.MESSAGES(channelId), + body: { + flags: 0, + channel_id: channelId, + content: "", + nonce: SnowflakeUtils.fromTimestamp(Date.now()), + sticker_ids: [], + type: 0, + attachments: [{ + id: "0", + filename: upload.filename, + uploaded_filename: upload.uploadedFilename, + }], + message_reference: reply ? options?.messageReference : null, + } + }); + }); + + upload.on("error", () => showToast("Failed to upload sticker", Toasts.Type.FAILURE)); + + upload.upload(); } diff --git a/src/equicordplugins/scheduledMessages/utils.ts b/src/equicordplugins/scheduledMessages/utils.ts index d15b35392..6fcf8e171 100644 --- a/src/equicordplugins/scheduledMessages/utils.ts +++ b/src/equicordplugins/scheduledMessages/utils.ts @@ -6,18 +6,14 @@ import * as DataStore from "@api/DataStore"; import { Logger } from "@utils/Logger"; -import { CloudUpload, Message } from "@vencord/discord-types"; +import { Message } from "@vencord/discord-types"; import { CloudUploadPlatform } from "@vencord/discord-types/enums"; -import { findLazy } from "@webpack"; -import { ChannelStore, Constants, FluxDispatcher, GuildStore, IconUtils, MessageActions, MessageStore, RestAPI, showToast, SnowflakeUtils, Toasts, UserStore } from "@webpack/common"; +import { ChannelStore, CloudUploader, Constants, FluxDispatcher, GuildStore, IconUtils, MessageActions, MessageStore, RestAPI, showToast, SnowflakeUtils, Toasts, UserStore } from "@webpack/common"; import { settings } from "."; import { PhantomMessageData, ScheduledAttachment, ScheduledMessage, ScheduledReaction } from "./types"; const logger = new Logger("ScheduledMessages"); - -const CloudUploader = findLazy(m => m.prototype?.trackUploadFinished) as typeof CloudUpload; - const STORAGE_KEY = "ScheduledMessages_queue"; let scheduledMessages: ScheduledMessage[] = []; @@ -297,7 +293,7 @@ async function uploadAttachment(channelId: string, att: ScheduledAttachment): Pr const file = new File([bytes], att.filename, { type: att.type }); const upload = new CloudUploader({ file, platform: CloudUploadPlatform.WEB }, channelId); - upload.on("complete", () => resolve({ id: "", filename: upload.filename, uploaded_filename: upload.uploadedFilename })); + upload.on("complete", () => resolve({ id: "0", filename: upload.filename, uploaded_filename: upload.uploadedFilename })); upload.on("error", () => resolve(null)); upload.upload(); }); diff --git a/src/plugins/voiceMessages/index.tsx b/src/plugins/voiceMessages/index.tsx index 2afa4e5c4..acad7fa6f 100644 --- a/src/plugins/voiceMessages/index.tsx +++ b/src/plugins/voiceMessages/index.tsx @@ -33,10 +33,8 @@ import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModa import { useAwaiter } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; import { chooseFile } from "@utils/web"; -import { CloudUpload } from "@vencord/discord-types"; import { CloudUploadPlatform } from "@vencord/discord-types/enums"; -import { findLazy, findStoreLazy } from "@webpack"; -import { Button, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; +import { Button, CloudUploader, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PendingReplyStore, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common"; import { VoiceRecorderDesktop } from "./components/DesktopRecorder"; import { VoicePreview } from "./components/VoicePreview"; @@ -56,9 +54,6 @@ const EMPTY_META: AudioMetadata = { duration: DEFAULT_DURATION, }; -const CloudUploadConstructor = findLazy(m => m.prototype?.trackUploadFinished) as typeof CloudUpload; -const PendingReplyStore = findStoreLazy("PendingReplyStore"); - export const cl = classNameFactory("vc-vmsg-"); export type VoiceRecorder = React.ComponentType<{ @@ -108,7 +103,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) { const reply = PendingReplyStore.getPendingReply(channelId); if (reply) FluxDispatcher.dispatch({ type: "DELETE_PENDING_REPLY", channelId }); - const upload = new CloudUploadConstructor({ + const upload = new CloudUploader({ file: new File([blob], "voice-message.ogg", { type: "audio/ogg; codecs=opus" }), isThumbnail: false, platform: CloudUploadPlatform.WEB, diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index 53606c80e..942c149be 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -124,7 +124,7 @@ export async function copyWithToast(text: string, toastMessage = "Copied to clip }); } -interface MessageOptions { +export interface MessageOptions { messageReference: Message["messageReference"]; allowedMentions: { parse: string[]; diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index d622f9f53..2baaab5ec 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -82,6 +82,7 @@ export let PopoutWindowStore: t.PopoutWindowStore; export let ApplicationCommandIndexStore: t.ApplicationCommandIndexStore; export let EditMessageStore: t.EditMessageStore; export let QuestStore: t.QuestStore; +export let PendingReplyStore: GenericStore; /** * @see jsdoc of {@link t.useStateFromStores} @@ -135,6 +136,7 @@ waitForStore("SoundboardStore", m => SoundboardStore = m); waitForStore("PopoutWindowStore", m => PopoutWindowStore = m); waitForStore("ApplicationCommandIndexStore", m => ApplicationCommandIndexStore = m); waitForStore("EditMessageStore", m => EditMessageStore = m); +waitForStore("PendingReplyStore", m => PendingReplyStore = m); waitForStore("QuestStore", m => QuestStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index bfc84c21c..00baf2184 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -226,3 +226,5 @@ export const DateUtils: t.DateUtils = mapMangledModuleLazy("millisecondsInUnit:" export const MessageTypeSets: t.MessageTypeSets = findByPropsLazy("REPLYABLE", "FORWARDABLE"); export const fetchApplicationsRPC = findByCodeLazy('"Invalid Origin"', ".application"); + +export const CloudUploader = findLazy(m => m.prototype?.trackUploadFinished) as typeof t.CloudUpload;