Fix createComposerState count-blind init, tighten gallery offset guard

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).
This commit is contained in:
vineyardbovines
2026-06-03 17:48:04 -04:00
parent a222ec4de0
commit 4fe5f9da37
2 changed files with 30 additions and 50 deletions
@@ -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
}
+26 -46
View File
@@ -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) {