Skip to content

Commit f54f98c

Browse files
authored
feat(chat): implement file sharing and downloading functionality for files in chats (#959)
1 parent 7264ed8 commit f54f98c

File tree

4 files changed

+63
-43
lines changed

4 files changed

+63
-43
lines changed

src/lib/components/messaging/embeds/FileEmbed.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import prettyBytes from "pretty-bytes"
99
import { createEventDispatcher } from "svelte"
1010
import { _ } from "svelte-i18n"
11+
import { Store } from "$lib/state/Store"
12+
import { ToastMessage } from "$lib/state/ui/toast"
1113
1214
export let altBackgroundColor: boolean = false
1315

src/lib/utils/Functions.ts

+45
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { Color, Format } from "$lib/enums"
2+
import { Store } from "$lib/state/Store"
3+
import { ToastMessage } from "$lib/state/ui/toast"
4+
import { Filesystem, Encoding } from "@capacitor/filesystem"
5+
import { Directory as LocalDirectory } from "@capacitor/filesystem"
6+
import { Share } from "@capacitor/share"
27
import TimeAgo from "javascript-time-ago"
8+
import { log } from "./Logger"
9+
import { _ } from "svelte-i18n"
10+
import { get } from "svelte/store"
311

412
export const debounce = (fn: Function, ms = 300) => {
513
let timeoutId: ReturnType<typeof setTimeout>
@@ -66,3 +74,40 @@ export function formatStyledText(text: string): string {
6674

6775
return formattedText
6876
}
77+
78+
export async function shareFile(fileName: string, combinedArray: Buffer) {
79+
try {
80+
const base64Data = combinedArray.toString("base64")
81+
82+
const filePath = await Filesystem.writeFile({
83+
path: fileName,
84+
data: base64Data!,
85+
directory: LocalDirectory.Cache,
86+
})
87+
88+
await Share.share({
89+
text: fileName,
90+
url: filePath.uri,
91+
})
92+
93+
log.info(`File shared: ${fileName} successfully`)
94+
} catch (error) {
95+
let errorMessage = `${error}`
96+
log.error("Error when to share file:", fileName, "Error:", errorMessage)
97+
if (errorMessage.includes("Share canceled")) {
98+
Store.addToastNotification(new ToastMessage("", get(_)("files.shareFileCanceled"), 2))
99+
return
100+
}
101+
}
102+
}
103+
104+
export async function downloadFileFromWeb(data: any[], size: number, name: string) {
105+
let options: { size?: number; type?: string } = { size }
106+
let blob = new File([new Uint8Array(data)], name, { type: options?.type })
107+
const elem = window.document.createElement("a")
108+
elem.href = window.URL.createObjectURL(blob)
109+
elem.download = name
110+
document.body.appendChild(elem)
111+
elem.click()
112+
document.body.removeChild(elem)
113+
}

src/lib/wasm/RaygunStore.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { SettingsStore } from "$lib/state"
1919
import { ToastMessage } from "$lib/state/ui/toast"
2020
import { page } from "$app/stores"
2121
import { goto } from "$app/navigation"
22-
import { Readable } from "stream"
22+
import { isAndroidOriOS } from "$lib/utils/Mobile"
23+
import { downloadFileFromWeb, shareFile } from "$lib/utils/Functions"
2324

2425
const MAX_PINNED_MESSAGES = 100
2526
export type FetchMessagesConfig =
@@ -363,7 +364,12 @@ class RaygunStore {
363364
async downloadAttachment(conversation_id: string, message_id: string, file: string, size?: number) {
364365
return await this.get(async r => {
365366
let result = await r.download_stream(conversation_id, message_id, file)
366-
return createFileDownloadHandler(file, result, size)
367+
let data = await createFileDownloadHandler(file, result, size)
368+
if (isAndroidOriOS()) {
369+
await shareFile(file, Buffer.from(data))
370+
} else {
371+
await downloadFileFromWeb(data, size || 0, file)
372+
}
367373
}, `Error downloading attachment from ${conversation_id} for message ${message_id}`)
368374
}
369375

@@ -820,7 +826,7 @@ class RaygunStore {
820826
}
821827
}
822828

823-
export async function createFileDownloadHandlerRaw(name: string, it: wasm.AsyncIterator, options?: { size?: number; type?: string }): Promise<Blob> {
829+
export async function createFileDownloadHandlerRaw(name: string, it: wasm.AsyncIterator, options?: { size?: number; type?: string }): Promise<any[]> {
824830
let listener = {
825831
[Symbol.asyncIterator]() {
826832
return it
@@ -832,17 +838,12 @@ export async function createFileDownloadHandlerRaw(name: string, it: wasm.AsyncI
832838
data = [...data, ...value]
833839
}
834840
} catch (_) {}
835-
return new File([new Uint8Array(data)], name, { type: options?.type })
841+
return data
836842
}
837843

838-
export async function createFileDownloadHandler(name: string, it: wasm.AsyncIterator, size?: number) {
839-
let blob = await createFileDownloadHandlerRaw(name, it, { size })
840-
const elem = window.document.createElement("a")
841-
elem.href = window.URL.createObjectURL(blob)
842-
elem.download = name
843-
document.body.appendChild(elem)
844-
elem.click()
845-
document.body.removeChild(elem)
844+
export async function createFileDownloadHandler(name: string, it: wasm.AsyncIterator, size?: number): Promise<any[]> {
845+
let data = await createFileDownloadHandlerRaw(name, it, { size })
846+
return data
846847
}
847848

848849
/**

src/routes/files/+page.svelte

+3-31
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
import { ChatPreview, ImageEmbed, ImageFile, Modal, FileFolder, ProgressButton, ContextMenu, ChatFilter, ProfilePicture, ProfilePictureMany, ChatIcon } from "$lib/components"
1212
import Controls from "$lib/layouts/Controls.svelte"
1313
import { onMount } from "svelte"
14-
import type { FileInfo, User } from "$lib/types"
15-
import { writable, readable } from "svelte/store"
14+
import type { FileInfo } from "$lib/types"
15+
import { writable } from "svelte/store"
1616
import { UIStore } from "$lib/state/ui"
1717
import FolderItem from "./FolderItem.svelte"
1818
import { v4 as uuidv4 } from "uuid"
@@ -25,10 +25,8 @@
2525
import { Store } from "$lib/state/Store"
2626
import path from "path"
2727
import { MultipassStoreInstance } from "$lib/wasm/MultipassStore"
28-
import { Share } from "@capacitor/share"
2928
import { isAndroidOriOS } from "$lib/utils/Mobile"
30-
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem"
31-
import { log } from "$lib/utils/Logger"
29+
import { shareFile } from "$lib/utils/Functions"
3230
3331
export let browseFilesForChatMode: boolean = false
3432
@@ -478,32 +476,6 @@
478476
)
479477
}
480478
481-
async function shareFile(fileName: string, combinedArray: Buffer) {
482-
try {
483-
const base64Data = combinedArray.toString("base64")
484-
485-
const filePath = await Filesystem.writeFile({
486-
path: fileName,
487-
data: base64Data!,
488-
directory: Directory.Cache,
489-
})
490-
491-
await Share.share({
492-
text: fileName,
493-
url: filePath.uri,
494-
})
495-
496-
log.info(`File shared: ${fileName} successfully`)
497-
} catch (error) {
498-
let errorMessage = `${error}`
499-
log.error("Error when to share file:", fileName, "Error:", errorMessage)
500-
if (errorMessage.includes("Share canceled")) {
501-
Store.addToastNotification(new ToastMessage("", $_("files.shareFileCanceled"), 2))
502-
return
503-
}
504-
}
505-
}
506-
507479
$: chats = UIStore.state.chats
508480
$: activeChat = Store.state.activeChat
509481

0 commit comments

Comments
 (0)