Show image count badge on in-feed carousel

Add a persistent "{n}/{total}" badge to the top-right of the in-feed image
carousel so the number of images in a set is always obvious, including on
mobile (previously the count was only implied by the peeking next image).

- Mirror the existing currentIndexRef into reactive state, updated only when
  the index actually changes, so the badge stays live without re-rendering on
  every scroll frame.
- Styling follows the Lightbox PagerDots pattern (white bold text, rounded),
  on a translucent dark pill so it stays legible over arbitrary in-feed images.
- Hidden in quote posts (hideBadges) and for single images. A web aria-live
  region announces position changes; native screen-reader users continue to
  get the existing per-image stacked layout.

APP-2285
This commit is contained in:
vineyardbovines
2026-06-03 19:28:55 -04:00
parent 153772b760
commit d8b540d90c
+67
View File
@@ -165,6 +165,13 @@ export function Gallery({
const containerRefsRef = useRef<Map<number, AnimatedRef<any>>>(new Map())
const thumbDimsRef = useRef<Map<number, Dimensions>>(new Map())
const currentIndexRef = useRef(0)
/*
* Reactive mirror of currentIndexRef for the numbering badge. The ref drives
* scroll/keyboard logic without re-rendering; this state only updates when
* the index actually changes (once per image, not per scroll frame), so the
* badge stays live and cheap.
*/
const [currentIndex, setCurrentIndexState] = useState(0)
const emitSwipeMetric = useMemo(
() =>
@@ -182,6 +189,7 @@ export function Gallery({
const prev = currentIndexRef.current
if (prev !== index) {
currentIndexRef.current = index
setCurrentIndexState(index)
emitSwipeMetric(prev, index)
}
}
@@ -348,10 +356,69 @@ export function Gallery({
}}
/>
</BlockDrawerGesture>
{!hideBadges && images.length > 1 ? (
<GalleryCounter
currentIndex={currentIndex}
imageCount={images.length}
/>
) : null}
</View>
)
}
/*
* Top-right "{n}/{total}" badge for the in-feed carousel. The visible pill is
* decorative (accessible={false}) since the digits are redundant for screen
* reader users.
*
* On native, screen reader users never reach this carousel branch - the Gallery
* renders a separate per-image stack early when screenReaderEnabled is true,
* and each slide there is labelled "Image N of M". So the live region only
* needs to exist on web, where screenReaderEnabled is always false and the
* carousel is the only branch. There we announce position changes via a
* visually-hidden aria-live region.
*/
function GalleryCounter({
currentIndex,
imageCount,
}: {
currentIndex: number
imageCount: number
}) {
const {t: l} = useLingui()
return (
<>
<View
accessible={false}
pointerEvents="none"
style={[
a.absolute,
a.justify_center,
{
top: a.p_xs.padding,
right: a.p_xs.padding,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 999,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
},
]}>
<Text style={[a.font_bold, a.text_xs, a.leading_tight, {color: '#fff'}]}>
{currentIndex + 1}/{imageCount}
</Text>
</View>
{IS_WEB ? (
<View aria-live="polite" style={a.sr_only}>
<Text>{l`Image ${currentIndex + 1} of ${imageCount}`}</Text>
</View>
) : null}
</>
)
}
function computeDims({
height,
aspectRatio,