From 5cd384cf3a566da049cd6210419c1566c188a81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Apr 2026 19:37:24 +0400 Subject: [PATCH] Do sequential image processing/uploads on mobile Uploading multiple large files at once can fail easily --- app/components/place-photo-item.gjs | 31 ++++++++++----- app/services/blossom.js | 62 +++++++++++++++++++---------- app/services/nostr-auth.js | 4 +- app/utils/device.js | 4 ++ 4 files changed, 70 insertions(+), 31 deletions(-) create mode 100644 app/utils/device.js diff --git a/app/components/place-photo-item.gjs b/app/components/place-photo-item.gjs index ff7543e..37a2415 100644 --- a/app/components/place-photo-item.gjs +++ b/app/components/place-photo-item.gjs @@ -6,6 +6,7 @@ import { task } from 'ember-concurrency'; import Icon from '#components/icon'; import { on } from '@ember/modifier'; import { fn } from '@ember/helper'; +import { isMobile } from '../utils/device'; const MAX_IMAGE_DIMENSION = 1920; const IMAGE_QUALITY = 0.94; @@ -62,17 +63,29 @@ export default class PlacePhotoItem extends Component { false ); - // 3. Upload main image (to all servers concurrently) - const mainUploadPromise = this.blossom.upload(mainData.blob); + // 3. Upload main image + // 4. Upload thumbnail + let mainResult, thumbResult; + const isMobileDevice = isMobile(); - // 4. Upload thumbnail (to all servers concurrently) - const thumbUploadPromise = this.blossom.upload(thumbData.blob); + if (isMobileDevice) { + // Mobile: sequential uploads to preserve bandwidth and memory + mainResult = await this.blossom.upload(mainData.blob, { + sequential: true, + }); + thumbResult = await this.blossom.upload(thumbData.blob, { + sequential: true, + }); + } else { + // Desktop: concurrent uploads + const mainUploadPromise = this.blossom.upload(mainData.blob); + const thumbUploadPromise = this.blossom.upload(thumbData.blob); - // Await both uploads - const [mainResult, thumbResult] = await Promise.all([ - mainUploadPromise, - thumbUploadPromise, - ]); + [mainResult, thumbResult] = await Promise.all([ + mainUploadPromise, + thumbUploadPromise, + ]); + } this.isUploaded = true; diff --git a/app/services/blossom.js b/app/services/blossom.js index 709a4fc..05b3648 100644 --- a/app/services/blossom.js +++ b/app/services/blossom.js @@ -75,7 +75,7 @@ export default class BlossomService extends Service { return response.json(); } - async upload(file) { + async upload(file, options = { sequential: false }) { if (!this.nostrAuth.isConnected) throw new Error('Not connected'); const buffer = await file.arrayBuffer(); @@ -97,28 +97,48 @@ export default class BlossomService extends Service { const mainServer = servers[0]; const fallbackServers = servers.slice(1); - // Start all uploads concurrently - const mainPromise = this._uploadToServer(file, payloadHash, mainServer); - const fallbackPromises = fallbackServers.map((serverUrl) => - this._uploadToServer(file, payloadHash, serverUrl) - ); - - // Main server MUST succeed - const mainResult = await mainPromise; - - // Fallback servers can fail, but we log the warnings - const fallbackResults = await Promise.allSettled(fallbackPromises); const fallbackUrls = []; + let mainResult; - for (let i = 0; i < fallbackResults.length; i++) { - const result = fallbackResults[i]; - if (result.status === 'fulfilled') { - fallbackUrls.push(result.value.url); - } else { - console.warn( - `Fallback upload to ${fallbackServers[i]} failed:`, - result.reason - ); + if (options.sequential) { + // Sequential upload logic + mainResult = await this._uploadToServer(file, payloadHash, mainServer); + + for (const serverUrl of fallbackServers) { + try { + const result = await this._uploadToServer( + file, + payloadHash, + serverUrl + ); + fallbackUrls.push(result.url); + } catch (error) { + console.warn(`Fallback upload to ${serverUrl} failed:`, error); + } + } + } else { + // Concurrent upload logic + const mainPromise = this._uploadToServer(file, payloadHash, mainServer); + const fallbackPromises = fallbackServers.map((serverUrl) => + this._uploadToServer(file, payloadHash, serverUrl) + ); + + // Main server MUST succeed + mainResult = await mainPromise; + + // Fallback servers can fail, but we log the warnings + const fallbackResults = await Promise.allSettled(fallbackPromises); + + for (let i = 0; i < fallbackResults.length; i++) { + const result = fallbackResults[i]; + if (result.status === 'fulfilled') { + fallbackUrls.push(result.value.url); + } else { + console.warn( + `Fallback upload to ${fallbackServers[i]} failed:`, + result.reason + ); + } } } diff --git a/app/services/nostr-auth.js b/app/services/nostr-auth.js index 7814302..132c7dc 100644 --- a/app/services/nostr-auth.js +++ b/app/services/nostr-auth.js @@ -14,6 +14,8 @@ const STORAGE_KEY_CONNECT_RELAY = 'marco:nostr_connect_relay'; const DEFAULT_CONNECT_RELAY = 'wss://relay.nsec.app'; +import { isMobile } from '../utils/device'; + export default class NostrAuthService extends Service { @service nostrRelay; @service nostrData; @@ -73,7 +75,7 @@ export default class NostrAuthService extends Service { } get isMobile() { - return /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent); + return isMobile(); } get isConnected() { diff --git a/app/utils/device.js b/app/utils/device.js new file mode 100644 index 0000000..857cd68 --- /dev/null +++ b/app/utils/device.js @@ -0,0 +1,4 @@ +export function isMobile() { + if (typeof navigator === 'undefined') return false; + return /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent); +}