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