From 52fd30f5574f7463fde6c9a9c52595646a6e2c1b Mon Sep 17 00:00:00 2001 From: lgmarchi Date: Fri, 20 Dec 2024 12:57:20 -0300 Subject: [PATCH] feat(chat): implement file sharing and downloading functionality for files in chats --- .../messaging/embeds/FileEmbed.svelte | 2 + src/lib/utils/Functions.ts | 45 +++++++++++++++++++ src/lib/wasm/RaygunStore.ts | 25 ++++++----- src/routes/files/+page.svelte | 34 ++------------ 4 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/lib/components/messaging/embeds/FileEmbed.svelte b/src/lib/components/messaging/embeds/FileEmbed.svelte index 72d3ba4df..7e7c1b496 100644 --- a/src/lib/components/messaging/embeds/FileEmbed.svelte +++ b/src/lib/components/messaging/embeds/FileEmbed.svelte @@ -8,6 +8,8 @@ import prettyBytes from "pretty-bytes" import { createEventDispatcher } from "svelte" import { _ } from "svelte-i18n" + import { Store } from "$lib/state/Store" + import { ToastMessage } from "$lib/state/ui/toast" export let altBackgroundColor: boolean = false diff --git a/src/lib/utils/Functions.ts b/src/lib/utils/Functions.ts index 4b75d2735..3f43c72a4 100644 --- a/src/lib/utils/Functions.ts +++ b/src/lib/utils/Functions.ts @@ -1,5 +1,13 @@ import { Color, Format } from "$lib/enums" +import { Store } from "$lib/state/Store" +import { ToastMessage } from "$lib/state/ui/toast" +import { Filesystem, Encoding } from "@capacitor/filesystem" +import { Directory as LocalDirectory } from "@capacitor/filesystem" +import { Share } from "@capacitor/share" import TimeAgo from "javascript-time-ago" +import { log } from "./Logger" +import { _ } from "svelte-i18n" +import { get } from "svelte/store" export const debounce = (fn: Function, ms = 300) => { let timeoutId: ReturnType @@ -66,3 +74,40 @@ export function formatStyledText(text: string): string { return formattedText } + +export async function shareFile(fileName: string, combinedArray: Buffer) { + try { + const base64Data = combinedArray.toString("base64") + + const filePath = await Filesystem.writeFile({ + path: fileName, + data: base64Data!, + directory: LocalDirectory.Cache, + }) + + await Share.share({ + text: fileName, + url: filePath.uri, + }) + + log.info(`File shared: ${fileName} successfully`) + } catch (error) { + let errorMessage = `${error}` + log.error("Error when to share file:", fileName, "Error:", errorMessage) + if (errorMessage.includes("Share canceled")) { + Store.addToastNotification(new ToastMessage("", get(_)("files.shareFileCanceled"), 2)) + return + } + } +} + +export async function downloadFileFromWeb(data: any[], size: number, name: string) { + let options: { size?: number; type?: string } = { size } + let blob = new File([new Uint8Array(data)], name, { type: options?.type }) + const elem = window.document.createElement("a") + elem.href = window.URL.createObjectURL(blob) + elem.download = name + document.body.appendChild(elem) + elem.click() + document.body.removeChild(elem) +} diff --git a/src/lib/wasm/RaygunStore.ts b/src/lib/wasm/RaygunStore.ts index 26e6d24b4..094a6dad4 100644 --- a/src/lib/wasm/RaygunStore.ts +++ b/src/lib/wasm/RaygunStore.ts @@ -19,7 +19,8 @@ import { SettingsStore } from "$lib/state" import { ToastMessage } from "$lib/state/ui/toast" import { page } from "$app/stores" import { goto } from "$app/navigation" -import { Readable } from "stream" +import { isAndroidOriOS } from "$lib/utils/Mobile" +import { downloadFileFromWeb, shareFile } from "$lib/utils/Functions" const MAX_PINNED_MESSAGES = 100 export type FetchMessagesConfig = @@ -363,7 +364,12 @@ class RaygunStore { async downloadAttachment(conversation_id: string, message_id: string, file: string, size?: number) { return await this.get(async r => { let result = await r.download_stream(conversation_id, message_id, file) - return createFileDownloadHandler(file, result, size) + let data = await createFileDownloadHandler(file, result, size) + if (isAndroidOriOS()) { + await shareFile(file, Buffer.from(data)) + } else { + await downloadFileFromWeb(data, size || 0, file) + } }, `Error downloading attachment from ${conversation_id} for message ${message_id}`) } @@ -820,7 +826,7 @@ class RaygunStore { } } -export async function createFileDownloadHandlerRaw(name: string, it: wasm.AsyncIterator, options?: { size?: number; type?: string }): Promise { +export async function createFileDownloadHandlerRaw(name: string, it: wasm.AsyncIterator, options?: { size?: number; type?: string }): Promise { let listener = { [Symbol.asyncIterator]() { return it @@ -832,17 +838,12 @@ export async function createFileDownloadHandlerRaw(name: string, it: wasm.AsyncI data = [...data, ...value] } } catch (_) {} - return new File([new Uint8Array(data)], name, { type: options?.type }) + return data } -export async function createFileDownloadHandler(name: string, it: wasm.AsyncIterator, size?: number) { - let blob = await createFileDownloadHandlerRaw(name, it, { size }) - const elem = window.document.createElement("a") - elem.href = window.URL.createObjectURL(blob) - elem.download = name - document.body.appendChild(elem) - elem.click() - document.body.removeChild(elem) +export async function createFileDownloadHandler(name: string, it: wasm.AsyncIterator, size?: number): Promise { + let data = await createFileDownloadHandlerRaw(name, it, { size }) + return data } /** diff --git a/src/routes/files/+page.svelte b/src/routes/files/+page.svelte index ecfac7813..850db81b0 100644 --- a/src/routes/files/+page.svelte +++ b/src/routes/files/+page.svelte @@ -11,8 +11,8 @@ import { ChatPreview, ImageEmbed, ImageFile, Modal, FileFolder, ProgressButton, ContextMenu, ChatFilter, ProfilePicture, ProfilePictureMany, ChatIcon } from "$lib/components" import Controls from "$lib/layouts/Controls.svelte" import { onMount } from "svelte" - import type { FileInfo, User } from "$lib/types" - import { writable, readable } from "svelte/store" + import type { FileInfo } from "$lib/types" + import { writable } from "svelte/store" import { UIStore } from "$lib/state/ui" import FolderItem from "./FolderItem.svelte" import { v4 as uuidv4 } from "uuid" @@ -25,10 +25,8 @@ import { Store } from "$lib/state/Store" import path from "path" import { MultipassStoreInstance } from "$lib/wasm/MultipassStore" - import { Share } from "@capacitor/share" import { isAndroidOriOS } from "$lib/utils/Mobile" - import { Filesystem, Directory, Encoding } from "@capacitor/filesystem" - import { log } from "$lib/utils/Logger" + import { shareFile } from "$lib/utils/Functions" export let browseFilesForChatMode: boolean = false @@ -478,32 +476,6 @@ ) } - async function shareFile(fileName: string, combinedArray: Buffer) { - try { - const base64Data = combinedArray.toString("base64") - - const filePath = await Filesystem.writeFile({ - path: fileName, - data: base64Data!, - directory: Directory.Cache, - }) - - await Share.share({ - text: fileName, - url: filePath.uri, - }) - - log.info(`File shared: ${fileName} successfully`) - } catch (error) { - let errorMessage = `${error}` - log.error("Error when to share file:", fileName, "Error:", errorMessage) - if (errorMessage.includes("Share canceled")) { - Store.addToastNotification(new ToastMessage("", $_("files.shareFileCanceled"), 2)) - return - } - } - } - $: chats = UIStore.state.chats $: activeChat = Store.state.activeChat