Fallback file upload for room/space images

This commit is contained in:
Jon Staab 2026-07-03 13:03:04 -07:00
parent 647375b8c9
commit c2684497f9
7 changed files with 58 additions and 54 deletions

View file

@ -22,7 +22,7 @@
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/drafts"
import {canEnforceNip70} from "@app/relays"
import {uploadFile} from "@app/uploads"
import {compressFileForUpload, uploadFile} from "@app/uploads"
type Values = {
d: string
@ -103,7 +103,7 @@
if (typeof image === "string") {
tags.push(["image", image])
} else {
const {result, error} = await uploadFile(image, {url})
const {result, error} = await uploadFile(await compressFileForUpload(image), {url})
if (error) {
return pushToast({

View file

@ -1,12 +1,12 @@
<script lang="ts">
import {randomId} from "@welshman/lib"
import {preventDefault, stopPropagation, compressFile} from "@lib/html"
import {preventDefault, stopPropagation} from "@lib/html"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import {uploadFile} from "@app/uploads"
import {compressFileForUpload, uploadFileOrFallback} from "@app/uploads"
interface Props {
file?: File | undefined
@ -43,33 +43,17 @@
submit(e.target.files[0])
}
const submit = async (file: File) => {
const submit = async (newFile: File) => {
if (!newFile) {
url = initialUrl
return
}
loading = true
try {
file = await compressFile(file, compressOptions)
if (file) {
const {result} = await uploadFile(file)
if (result?.url) {
url = result.url
} else {
const reader = new FileReader()
reader.addEventListener(
"load",
() => {
url = reader.result as string
},
false,
)
reader.readAsDataURL(file)
}
} else {
url = initialUrl
}
file = await compressFileForUpload(newFile, compressOptions)
url = (await uploadFileOrFallback(file)).url
} finally {
loading = false
}

View file

@ -12,7 +12,6 @@
tagPubkey,
loadRelayList,
} from "@welshman/app"
import {compressFile} from "@lib/html"
import Copy from "@assets/icons/copy.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl"
@ -32,7 +31,7 @@
import WotScore from "@app/components/WotScore.svelte"
import Content from "@app/components/Content.svelte"
import {updateProfile} from "@app/profiles"
import {uploadFile} from "@app/uploads"
import {compressFileForUpload, uploadFile} from "@app/uploads"
import {pushModal} from "@app/modal"
import {clip, pushToast} from "@app/toast"
import {goToChat} from "@app/routes"
@ -90,7 +89,7 @@
bannerLoading = true
try {
const {result} = await uploadFile(await compressFile(file))
const {result} = await uploadFile(await compressFileForUpload(file))
if (result?.url) {
await updateProfile({profile: {...$profile, banner: result.url}})

View file

@ -15,7 +15,7 @@
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import {pushToast} from "@app/toast"
import {uploadFile} from "@app/uploads"
import {compressFileForUpload, uploadFileOrFallback} from "@app/uploads"
import {deriveHasLivekit} from "@app/relays"
import {getRoomType, RoomType} from "@app/groups"
@ -45,17 +45,14 @@
room.livekit = roomType === RoomType.Voice
if (imageFile) {
const {error, result} = await uploadFile(imageFile, {
maxWidth: 256,
maxHeight: 256,
const compressedFile = await compressFileForUpload(imageFile, {
maxWidth: 128,
maxHeight: 128,
})
if (error) {
return pushToast({theme: "error", message: error})
}
const result = await uploadFileOrFallback(compressedFile)
room.picture = result.url
room.pictureMeta = result.tags
}
const createMessage = await waitForThunkError(createRoom(url, room))

View file

@ -21,7 +21,7 @@
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
import {pushToast} from "@app/toast"
import {clearModals} from "@app/modal"
import {uploadFile} from "@app/uploads"
import {compressFileForUpload, uploadFileOrFallback} from "@app/uploads"
type Props = {
url: string
@ -58,11 +58,8 @@
}
if (imageFile) {
const {error, result} = await uploadFile(imageFile, {maxWidth: 256, maxHeight: 256})
if (error) {
return pushToast({theme: "error", message: error})
}
const compressedFile = await compressFileForUpload(imageFile, {maxWidth: 128, maxHeight: 128})
const result = await uploadFileOrFallback(compressedFile)
const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayIcon,

View file

@ -27,7 +27,7 @@ import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension"
import RoomSuggestion from "@app/editor/RoomSuggestion.svelte"
import {NativeClipboardPasteExtension} from "@app/editor/clipboard"
import {uploadFile} from "@app/uploads"
import {compressFileForUpload, uploadFile} from "@app/uploads"
import {deriveSpaceMembers} from "@app/members"
import {makeRoomId, splitRoomId, userSpaceUrls, roomsByUrl} from "@app/groups"
import {pushToast} from "@app/toast"
@ -137,8 +137,8 @@ export const makeEditor = async ({
},
fileUpload: {
config: {
upload: (attrs: FileAttributes) =>
uploadFile(attrs.file, {url, encrypt: encryptFiles}),
upload: async (attrs: FileAttributes) =>
uploadFile(await compressFileForUpload(attrs.file), {url, encrypt: encryptFiles}),
onDrop: () => uploading?.set(true),
onComplete: () => uploading?.set(false),
onUploadError(currentEditor, task) {

View file

@ -67,11 +67,20 @@ export const getBlossomServer = async (options: GetBlossomServerOptions = {}) =>
return first(DEFAULT_BLOSSOM_SERVERS)!
}
export type CompressFileOptions = {
maxWidth?: number
maxHeight?: number
quality?: number
mimeType?: string
}
// Webp/gif/svg either break the compressor or lose animation, pass them through untouched
export const compressFileForUpload = async (file: File, options: CompressFileOptions = {}) =>
file.type.match("image/(webp|gif|svg)") ? file : compressFile(file, options)
export type UploadFileOptions = {
url?: string
encrypt?: boolean
maxWidth?: number
maxHeight?: number
}
export type UploadFileResult = {
@ -83,10 +92,6 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
try {
const {name, type} = file
if (!type.match("image/(webp|gif|svg)")) {
file = await compressFile(file, options)
}
const tags: string[][] = []
if (options.encrypt) {
@ -140,3 +145,25 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
return {error: e.toString()}
}
}
const readFileAsDataUrl = (file: File) =>
new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.addEventListener("load", () => resolve(reader.result as string))
reader.addEventListener("error", () => reject(reader.error))
reader.readAsDataURL(file)
})
export const uploadFileOrFallback = async (
file: File,
options: UploadFileOptions = {},
): Promise<{url: string; tags: string[][]}> => {
const {result} = await uploadFile(file, options)
if (result?.url) {
return result
}
return {url: await readFileAsDataUrl(file), tags: []}
}