feat([issue-4188]): mood boards gain gallery/upload pickers and playable video items - #4203
Merged
Conversation
…video items Phase 2 of #4188. Board items can now be picked from the shared image gallery (with upload) and the video gallery; a new type:'video' item plays inline with a poster thumbnail. Video items store their mediaKey as video:<filename> so playback and peer-sync asset transfer both resolve without an id-to-filename guess. Uploads that a board will reference must federate, so video uploads land in the shared gallery via a new POST /api/video-gen/upload (saveUploadedGalleryVideo: PATHS.videos + thumbnail + a source:'upload' video-history entry, mirroring the video-download tail) instead of the non-syncing data/uploads scratch dir. GalleryVideoPicker gains an opt-in uploadToGallery mode; its existing /api/uploads path is unchanged for prompt-from-media consumers. moodBoards sync schema bumped 1 -> 2: a v1 peer renders a video item as a blank note and its updateItem would mangle one on edit, so older receivers now 412-reject the ahead-version push until they upgrade. The board asset manifest ships video bytes for video items (poster thumbnails regenerate on the receiving peer).
… fallback, one upload cap - Video item mediaKeys must be a filename with extension (isVideoItemMediaKey, shared by the create schema and the item-PATCH invariant): a bare video:<id> would 404 as /data/videos/<id> and mis-guess .mp4 on the peer asset manifest, and a PATCH could previously swap a video item onto an image key. - Board posters fall back to the derived <stem>.jpg on error: a downloaded video's sender thumbnail is <id>.jpg but a receiving peer regenerates <filename-stem>.jpg on pull, so the stored URL 404s there. - Upload caps derive from server/lib/uploadLimits.js: the route schema's string max is the JSON body-parser limit (anything longer 413s before the route) and the saver's binary cap is MAX_BASE64_UPLOAD_BYTES, replacing two hand-rolled literals that straddled the parser limit.
This was referenced Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 of #4188 (mood boards as a universe style-building tool): board items can now come from the shared galleries, videos are first-class board items, and board-referenced uploads land in peer-syncable storage.
GalleryImagePicker allowUpload) and "Pick video" (GalleryVideoPicker) alongside the existing URL/Note tabs. Picks storemediaKey+ a directly-renderable preview, mirroringPinToMoodBoardMenu's shape.type:'video'items — added toMOOD_BOARD_ITEM_TYPES,normalizeItem, the Zod schemas, and the item-PATCH invariants. A video item's mediaKey ref is the on-disk filename (video:<file>.mp4), so playback (/data/videos/<ref>) and the peer-sync asset manifest (collectionVideoRefToFilenamepasses an extensioned ref through untouched) both resolve without an id→filename guess — including non-mp4 containers. The card renders a poster with a play affordance and plays inline; posters fall back to the derived<stem>.jpgon error (a downloaded video's sender thumbnail is<id>.jpg, but a receiving peer regenerates<stem>.jpgwhen it pulls the video).POST /api/video-gen/upload→saveUploadedGalleryVideo: container-sniffed (mp4/mov/webm), landsupload-<id>.<ext>underPATHS.videoswith a thumbnail and asource:'upload'video-history entry (the same tail asdownloadVideoIntoLibrary), and emits the media-indexcompletedevent.GalleryVideoPickergains an opt-inuploadToGallerymode; its existing/api/uploadspath is unchanged for prompt-from-media consumers. Board-referenced media never lands in the non-federatingdata/uploads/scratch dir. Upload caps derive fromserver/lib/uploadLimits.js(route schema = the JSON body-parser limit; saver =MAX_BASE64_UPLOAD_BYTES).moodBoardssync gate bumped 1 → 2 — a v1 peer renders a video item as a blank note and itsupdateItemgates a video item onto the text editable-keys, so an older receiver that edits the board could mangle video items and LWW the damage back; the bump makes v1 receivers 412-reject the ahead-version push until they upgrade.buildBoardAssetManifestnow ships video bytes for video items (poster thumbnails deliberately not shipped — receivers regenerate them on pull, per the existing deterministic-name design).A codex review pass surfaced four P2s, fixed in the second commit: filename-with-extension enforcement on video mediaKeys (create and PATCH — a bare
video:<id>would 404 locally and mis-guess.mp4on the wire; a PATCH could swap a video item onto an image key), the derived-poster fallback for synced download pins, and unifying the hand-rolled upload caps ontouploadLimits.js(the schema previously advertised a cap above the body-parser limit).Remaining phases of #4188 (per-item prompt-from-media analysis, board→style synthesis) are not in this PR — see the reconciliation comment on the issue.
Refs #4188
Test plan
server/lib/moodBoardValidation.test.js— video item create-schema cases (accept filename keys ± poster, reject missing/wrong-kind/extension-less keys) +isVideoItemMediaKeyunit cases (traversal, extension, kind).server/services/moodBoard/logic.test.js— video item normalize (mediaKey + poster, text nulled), media-style PATCH keys, and merged-key invariants (cannot clear, cannot swap toimage:/extension-less).server/services/videoUpload.test.js— container sniff (mp4/mov/webm/reject), history-entry shape, pre-write validation errors (empty/oversize/non-video).server/services/sharing/peerSyncAssets.test.js—buildBoardAssetManifestbundles video items by filename-as-ref (extension preserved, no.mp4guess), skips missing bytes and poster thumbnails, mixes with image items.client/src/lib/moodBoardItemSrc.test.js— derived stem poster, stored-poster preference,moodBoardItemVideoSrcplayback resolution and legacy-pin null cases.client/src/components/videoGen/GalleryVideoPicker.test.jsx—uploadToGalleryuploads via the new endpoint and selects the normalized entry; legacy/api/uploadspath untouched.client/src/pages/MoodBoardDetail.test.jsx— video item renders poster + play affordance, plays inline with the right src, and the poster onError fallback swaps to the derived stem name.biome checkclean.