diff --git a/components/upload-modal.html b/components/upload-modal.html index 3cf5a035..cf8fae25 100644 --- a/components/upload-modal.html +++ b/components/upload-modal.html @@ -153,6 +153,11 @@ + +
Provider diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index 28d45373..9a1714ed 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -31,6 +31,8 @@ import { ensureStorageUnlockedForUpload, resetThumbnailPicker, promptStoredNsecUnlock, + pickTargetConnection, + renderConnectionPicker, } from "./uploadModalStorageUnlock.js"; const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/; @@ -116,6 +118,8 @@ export class UploadModal { this.activeProvider = null; this.isStorageUnlocked = false; this.storageConfigured = false; + // #44: per-modal upload-destination override (null = account default). + this.selectedConnectionId = null; // Upload State this.videoUploadState = { @@ -247,6 +251,7 @@ export class UploadModal { progress: $("#upload-progress-container"), thumbnailProgress: $("#thumbnail-progress-container"), results: $("#upload-results-container"), + connectionPicker: $("#storage-connection-picker"), }; this.storageViews = { @@ -274,6 +279,9 @@ export class UploadModal { // Progress progress: $("#input-progress"), thumbnailProgress: $("#thumbnail-progress"), + + // #44: upload-destination picker + storageConnection: $("#select-storage-connection"), }; // Results (Generated Links) @@ -334,6 +342,14 @@ export class UploadModal { this.toggles.storageUnlock.addEventListener("click", () => this.handleUnlock()); } + if (this.inputs.storageConnection) { + // #44: switching the destination re-resolves summary + credentials. + this.inputs.storageConnection.addEventListener("change", () => { + this.selectedConnectionId = this.inputs.storageConnection.value || null; + void this.loadFromStorage(); + }); + } + // Manage Storage if (this.toggles.manageStorage) { this.toggles.manageStorage.addEventListener("click", () => { @@ -910,9 +926,9 @@ export class UploadModal { try { const connections = await this.storageService.listConnections(pubkey); - // Prefer default connection - const defaultConn = connections.find(c => c.meta?.defaultForUploads); - const targetConn = defaultConn || connections[0]; + // #44: a per-modal selection wins over the account default. + const { targetConn, defaultConn } = pickTargetConnection(this, connections); + renderConnectionPicker(this, connections, targetConn, defaultConn); if (targetConn) { this.storageConfigured = true; diff --git a/js/ui/components/uploadModalStorageUnlock.js b/js/ui/components/uploadModalStorageUnlock.js index f5b4a1f7..8915fb5b 100644 --- a/js/ui/components/uploadModalStorageUnlock.js +++ b/js/ui/components/uploadModalStorageUnlock.js @@ -1,9 +1,10 @@ // js/ui/components/uploadModalStorageUnlock.js // -// Storage-unlock glue for the Upload modal (#36 / #56), kept out of -// UploadModal.js to stay within its file-size budget. Every function operates -// on the UploadModal instance (`modal`); UploadModal exposes them as thin -// prototype delegates so callers and tests keep using the instance methods. +// Storage glue for the Upload modal (#36 / #56 unlock + #44 connection +// picker), kept out of UploadModal.js to stay within its file-size budget. +// Every function operates on the UploadModal instance (`modal`); UploadModal +// exposes them as thin prototype delegates so callers and tests keep using +// the instance methods. import { devLogger, userLogger } from "../../utils/logger.js"; @@ -67,6 +68,52 @@ export function resetThumbnailPicker(modal) { } } +// #44: resolve which configured connection this upload targets. A per-modal +// selection wins; a stale selection (connection deleted meanwhile) is reset; +// otherwise the account default, then the first configured connection. +export function pickTargetConnection(modal, connections) { + const list = Array.isArray(connections) ? connections : []; + const defaultConn = list.find((conn) => conn?.meta?.defaultForUploads) || null; + if (modal.selectedConnectionId) { + const selected = list.find((conn) => conn?.id === modal.selectedConnectionId); + if (selected) { + return { targetConn: selected, defaultConn }; + } + modal.selectedConnectionId = null; + } + return { targetConn: defaultConn || list[0] || null, defaultConn }; +} + +// #44: render the "Upload destination"
', + ); + const doc = dom.window.document; + global.HTMLElement = dom.window.HTMLElement; + return { + modal: { + selectedConnectionId: null, + inputs: { storageConnection: doc.getElementById("sel") }, + sourceSections: { connectionPicker: doc.getElementById("wrap") }, + getProviderLabel(provider) { + return provider === "cloudflare_r2" ? "Cloudflare R2" : provider; + }, + }, + doc, + }; +} + +test("picker renders options (default labelled, target selected); hidden under two connections", () => { + const { modal } = makeModal(); + + renderConnectionPicker(modal, [r2, b2, s3], b2, b2); + const wrap = modal.sourceSections.connectionPicker; + const options = Array.from(modal.inputs.storageConnection.options); + assert.equal(wrap.classList.contains("hidden"), false, "picker shown"); + assert.deepEqual( + options.map((o) => o.value), + ["c1", "c2", "c3"], + ); + assert.match(options[0].textContent, /Cloudflare R2 — vids/); + assert.match(options[1].textContent, /\(default\)$/, "default is labelled"); + assert.equal( + modal.inputs.storageConnection.value, + "c2", + "target connection pre-selected", + ); + + renderConnectionPicker(modal, [r2, b2, s3], s3, b2); + assert.equal(modal.inputs.storageConnection.value, "c3", "re-render follows the target"); + + renderConnectionPicker(modal, [r2], r2, null); + assert.equal( + wrap.classList.contains("hidden"), + true, + "hidden when there is nothing to choose", + ); +}); diff --git a/todo/TODO_2026-06-20_pre-launch.md b/todo/TODO_2026-06-20_pre-launch.md index 62682d9d..85353db3 100644 --- a/todo/TODO_2026-06-20_pre-launch.md +++ b/todo/TODO_2026-06-20_pre-launch.md @@ -555,21 +555,34 @@ toward freshness and looked identical. Gave each a structural identity: the active signer if ever reused. ### 43. Logged-in profile card at the top of the profile modal (UX) -- [ ] Show the **logged-in user's profile photo + name** at the **top-center** of the - profile modal, styled as a "profile card" that sits **half-on / half-off** the - modal edge (overlapping the top border) for a polished look. Goal: users can see - WHICH profile they're acting as (e.g. uploading as) at a glance. Pull avatar/name - from the profile cache for the active pubkey; update on profile switch. +- [x] **DONE 2026-07-03.** An "Acting as " identity pill now renders + top-center on EVERY pane, straddling the header divider (negative + block-start margin — the modal is full-screen, so the header edge is the + overlap surface). Avatar + name come from the same resolveMeta path as + the Account pane card (profile cache first), populated in + renderSavedProfiles so it refreshes on every profile switch; hidden when + logged out; clicking it jumps to the Account pane. Markup in + components/profile-modal.html, styles in css/tailwind.source.css + (.profile-modal__identity*), wiring in profileModalController. Test in + tests/profile-modal-controller.test.mjs (26 pass). Lint + build green. +- [ ] **VERIFY on unstable:** open any pane → pill shows the active identity; + switch accounts → it updates; log out → it disappears; click → Account. ### 44. In-modal storage selector for uploads (UX) -- [ ] Make storage unlock + selection easier from the **video upload modal**: let the - user **pick any configured storage connection** (R2 / B2 / Custom S3) and unlock it - **without leaving the upload modal** — today they must close it, open Profile → - Storage, change the default/selected connection, then reopen upload. Add a provider - picker (driven by `storageService.listConnections`) + inline unlock in the upload - modal's storage section; selecting one sets it as the active upload target for this - upload (and optionally the default). Reuse `loadFromStorage` / the per-provider - connection model. +- [x] **DONE 2026-07-03.** The upload modal's storage summary now has an + "Upload destination"