From 4fe5f9da3717e68740e4407441d38336687f8c24 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Wed, 3 Jun 2026 17:48:04 -0400 Subject: [PATCH] Fix createComposerState count-blind init, tighten gallery offset guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B1: `createComposerState` always typed the initial media as `ImagesMedia` regardless of `initImageUris.length`. With the picker cap raised to 10 in ComposerPrompt + the URL intent handler not capping at all, the FAB and deep-link flows could land the composer with 5-10 images in an `images` embed, which `app.bsky.embed.images` rejects (`maxLength: 4`) on submit. Extract `imagesToMediaVariant` and reuse from `createComposerState`, `embed_add_images`, and `embed_remove_image` so the variant decision ("<=4 = images, >4 = gallery, cap at 10") lives in one place. The remove-image demote branch collapses into the helper too — same effect, less code. W1: `maybeApplyGalleryOffsetStyles` only short-circuited single-image galleries, applying offset for 0-item galleries when nothing renders below. Loosened to `<= 1` (covers both 0 and 1 with the same intent). --- .../Gallery/maybeApplyGalleryOffsetStyles.ts | 8 +-- src/view/com/composer/state/composer.ts | 72 +++++++------------ 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts b/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts index 1749daf7ea..45f5bc5f31 100644 --- a/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts +++ b/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts @@ -69,8 +69,8 @@ export function maybeApplyGalleryOffsetStyles( hasImages = true } if (isGalleryEmbed) { - // one image, not a gallery - if (embed.items.length === 1) return + // single (or empty) gallery — no offset needed + if (embed.items.length <= 1) return hasImages = true } if (isRecordWithMedia) { @@ -89,8 +89,8 @@ export function maybeApplyGalleryOffsetStyles( AppBskyEmbedGallery.isMain, ) ) { - // one image, not a gallery - if (embed.media.items.length === 1) return + // single (or empty) gallery — no offset needed + if (embed.media.items.length <= 1) return } hasImages = true } diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index ada15940c9..5b3779c2b9 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -162,6 +162,20 @@ export type ComposerAction = export const MAX_IMAGES = 4 export const MAX_GALLERY_IMAGES = 10 +/** + * Picks the embed variant for a set of images. ≤4 lands in the legacy + * `app.bsky.embed.images` shape; >4 promotes to `app.bsky.embed.gallery`. + * Anything beyond the gallery cap is silently dropped — callers should + * already have enforced the cap upstream (picker, paste, etc). + */ +function imagesToMediaVariant( + images: ComposerImage[], +): ImagesMedia | GalleryMedia { + return images.length <= MAX_IMAGES + ? {type: 'images', images: images.slice(0, MAX_IMAGES)} + : {type: 'gallery', images: images.slice(0, MAX_GALLERY_IMAGES)} +} + export function composerReducer( state: ComposerState, action: ComposerAction, @@ -344,39 +358,12 @@ function postReducer(state: PostDraft, action: PostAction): PostDraft { const prevMedia = state.embed.media let nextMedia = prevMedia if (!prevMedia) { - // First selection: pick the variant based on count. ImagesMedia caps - // at 4 (legacy `app.bsky.embed.images`); above that promotes to the - // new `app.bsky.embed.gallery` (capped at 10). - if (action.images.length <= MAX_IMAGES) { - nextMedia = { - type: 'images', - images: action.images.slice(0, MAX_IMAGES), - } - } else { - nextMedia = { - type: 'gallery', - images: action.images.slice(0, MAX_GALLERY_IMAGES), - } - } - } else if (prevMedia.type === 'images') { - const combined = [...prevMedia.images, ...action.images] - if (combined.length <= MAX_IMAGES) { - nextMedia = {...prevMedia, images: combined} - } else { - // Adding more than 4 promotes the existing images into a gallery. - nextMedia = { - type: 'gallery', - images: combined.slice(0, MAX_GALLERY_IMAGES), - } - } - } else if (prevMedia.type === 'gallery') { - nextMedia = { - ...prevMedia, - images: [...prevMedia.images, ...action.images].slice( - 0, - MAX_GALLERY_IMAGES, - ), - } + nextMedia = imagesToMediaVariant(action.images) + } else if (prevMedia.type === 'images' || prevMedia.type === 'gallery') { + nextMedia = imagesToMediaVariant([ + ...prevMedia.images, + ...action.images, + ]) } return { ...state, @@ -423,15 +410,11 @@ function postReducer(state: PostDraft, action: PostAction): PostDraft { if (!state.embed.link) { nextLabels = [] } - } else if ( - prevMedia.type === 'gallery' && - remainingImages.length <= MAX_IMAGES - ) { - // Drop back to the legacy `app.bsky.embed.images` shape when a - // gallery shrinks to <=4 items, so old clients still see it. - nextMedia = {type: 'images', images: remainingImages} } else { - nextMedia = {...prevMedia, images: remainingImages} + // Re-pick the variant so a gallery that shrinks to <=4 demotes + // back to the legacy `app.bsky.embed.images` shape — keeps old + // clients rendering it when possible. + nextMedia = imagesToMediaVariant(remainingImages) } return { ...state, @@ -618,12 +601,9 @@ export function createComposerState({ | AppBskyActorDefs.PostInteractionSettingsPref | undefined }): ComposerState { - let media: ImagesMedia | undefined + let media: ImagesMedia | GalleryMedia | undefined if (initImageUris?.length) { - media = { - type: 'images', - images: createInitialImages(initImageUris), - } + media = imagesToMediaVariant(createInitialImages(initImageUris)) } let quote: Link | undefined if (initQuoteUri) {