Wire image context menu into grid and gallery embeds

Thread onPreviewPress(index) through ImageLayoutGrid / GalleryItem and the
Gallery scroll item so multi-image embeds get the same iOS peek behaviour as
single-image embeds. Each grid cell wraps its Pressable in ImageContextMenu
with the image's true aspect ratio, so the preview lifts to the full
uncropped image even when the cell is cropped to square.

https://claude.ai/code/session_015REmux3R9uuEMMJUHxTyQT
This commit is contained in:
Claude
2026-04-19 07:05:11 +00:00
committed by Samuel Newman
parent d4fd15dc20
commit ce686fae89
5 changed files with 203 additions and 164 deletions
@@ -1,4 +1,5 @@
import {type ReactNode} from 'react'
import {type StyleProp, type ViewStyle} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -22,6 +23,7 @@ export function ImageContextMenu({
aspectRatio,
borderRadius,
onPreviewPress,
style,
children,
}: {
fullsizeUri: string
@@ -29,6 +31,7 @@ export function ImageContextMenu({
aspectRatio: number | undefined
borderRadius?: number
onPreviewPress?: () => void
style?: StyleProp<ViewStyle>
children: ReactNode
}) {
const {_} = useLingui()
@@ -46,7 +49,7 @@ export function ImageContextMenu({
}
return (
<ContextMenu.Root>
<ContextMenu.Root style={style}>
<ContextMenu.Trigger
preview={{
type: 'image',
+15 -13
View File
@@ -57,6 +57,18 @@ export function ImageEmbed({
)
})
}
const onPreviewPress = (index: number) =>
openLightbox({
images: items.map(item => ({
...item,
thumbRect: null,
thumbRef: null,
thumbDimensions: null,
thumbBorderRadius: tokens.borderRadius.md,
type: 'image',
})),
index,
})
if (images.length === 1) {
const image = images[0]
@@ -70,19 +82,7 @@ export function ImageEmbed({
fullsizeUri={image.fullsize}
aspectRatio={aspect}
borderRadius={tokens.borderRadius.md}
onPreviewPress={() =>
openLightbox({
images: items.map(item => ({
...item,
thumbRect: null,
thumbRef: null,
thumbDimensions: null,
thumbBorderRadius: tokens.borderRadius.md,
type: 'image',
})),
index: 0,
})
}>
onPreviewPress={() => onPreviewPress(0)}>
<AutoSizedImage
crop={
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
@@ -114,6 +114,7 @@ export function ImageEmbed({
images={images}
onPress={onPress}
onPressIn={onPressIn}
onPreviewPress={onPreviewPress}
viewContext={rest.viewContext}
/>
</View>
@@ -126,6 +127,7 @@ export function ImageEmbed({
images={images}
onPress={onPress}
onPressIn={onPressIn}
onPreviewPress={onPreviewPress}
viewContext={rest.viewContext}
/>
</View>
+15 -1
View File
@@ -24,7 +24,7 @@ import {mergeRefs} from '#/lib/merge-refs'
import {useA11y} from '#/state/a11y'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {atoms as a, tokens, useBreakpoints, useTheme, web} from '#/alf'
import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
import {
@@ -36,6 +36,7 @@ import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandle
import {usePointerHandlers} from '#/components/images/Gallery/usePointerHandlers'
import {getAspectRatio} from '#/components/images/Gallery/utils'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu'
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
@@ -52,6 +53,7 @@ interface GalleryProps {
fetchedDims: (Dimensions | null)[],
) => void
onPressIn?: (index: number) => void
onPreviewPress?: (index: number) => void
viewContext?: PostEmbedViewContext
}
@@ -95,6 +97,7 @@ export function Gallery({
images,
onPress,
onPressIn,
onPreviewPress,
viewContext,
}: GalleryProps) {
const {t: l} = useLingui()
@@ -306,6 +309,9 @@ export function Gallery({
: undefined
}
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
onPreviewPress={
onPreviewPress ? () => onPreviewPress(index) : undefined
}
/>
)
}}
@@ -378,6 +384,7 @@ function GalleryImage({
onThumbDims,
onPress,
onPressIn,
onPreviewPress,
}: {
contentHeight: number
image: AppBskyEmbedImages.ViewImage
@@ -391,6 +398,7 @@ function GalleryImage({
onThumbDims: (index: number, dims: Dimensions) => void
onPress?: () => void
onPressIn?: () => void
onPreviewPress?: () => void
}) {
const t = useTheme()
const {t: l} = useLingui()
@@ -416,6 +424,11 @@ function GalleryImage({
collapsable={false}
aria-roledescription={l`slide`}
aria-label={image.alt || l`Image ${index + 1} of ${imageCount}`}>
<ImageContextMenu
fullsizeUri={image.fullsize}
aspectRatio={aspectRatio}
borderRadius={tokens.borderRadius.md}
onPreviewPress={onPreviewPress}>
<Pressable
ref={itemRef}
tabIndex={index === 0 ? 0 : -1}
@@ -534,6 +547,7 @@ function GalleryImage({
}
/>
</Pressable>
</ImageContextMenu>
</Animated.View>
)
}
@@ -17,6 +17,7 @@ interface ImageLayoutGridProps {
) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
onPreviewPress?: (index: number) => void
style?: StyleProp<ViewStyle>
viewContext?: PostEmbedViewContext
}
@@ -48,6 +49,7 @@ interface ImageLayoutGridInnerProps {
) => void
onLongPress?: (index: number) => void
onPressIn?: (index: number) => void
onPreviewPress?: (index: number) => void
viewContext?: PostEmbedViewContext
gap: {gap: number}
}
@@ -11,6 +11,7 @@ import {type Dimensions} from '#/lib/media/types'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {atoms as a, useTheme} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu'
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
import {Text} from '#/components/Typography'
@@ -26,6 +27,8 @@ interface Props {
) => void
onLongPress?: EventFunction
onPressIn?: EventFunction
/** Fired from the native iOS peek preview tap. */
onPreviewPress?: EventFunction
imageStyle?: StyleProp<ImageStyle>
viewContext?: PostEmbedViewContext
insetBorderStyle?: StyleProp<ViewStyle>
@@ -40,6 +43,7 @@ export function GalleryItem({
onPress,
onPressIn,
onLongPress,
onPreviewPress,
viewContext,
insetBorderStyle,
containerRefs,
@@ -52,8 +56,21 @@ export function GalleryItem({
const hasAlt = !!image.alt
const hideBadges =
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
const aspect =
image.aspectRatio && image.aspectRatio.height > 0
? image.aspectRatio.width / image.aspectRatio.height
: undefined
return (
<View style={a.flex_1} ref={containerRefs[index]} collapsable={false}>
<ImageContextMenu
fullsizeUri={image.fullsize}
aspectRatio={aspect}
onPreviewPress={
onPreviewPress ? () => onPreviewPress(index) : undefined
}
style={a.flex_1}>
<Pressable
onPress={
onPress
@@ -92,6 +109,7 @@ export function GalleryItem({
/>
<MediaInsetBorder style={insetBorderStyle} />
</Pressable>
</ImageContextMenu>
{hasAlt && !hideBadges ? (
<View
accessible={false}