From ce686fae89e2c0003fb32c8825777e6e39ca7fd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 07:05:11 +0000 Subject: [PATCH] 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 --- .../Post/Embed/ImageContextMenu.tsx | 5 +- src/components/Post/Embed/ImageEmbed.tsx | 28 +- src/components/images/Gallery/index.tsx | 242 +++++++++--------- src/components/images/ImageLayoutGrid.tsx | 2 + src/components/images/ImageLayoutGridItem.tsx | 90 ++++--- 5 files changed, 203 insertions(+), 164 deletions(-) diff --git a/src/components/Post/Embed/ImageContextMenu.tsx b/src/components/Post/Embed/ImageContextMenu.tsx index 3867fe9acf..b6bed6c49d 100644 --- a/src/components/Post/Embed/ImageContextMenu.tsx +++ b/src/components/Post/Embed/ImageContextMenu.tsx @@ -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 children: ReactNode }) { const {_} = useLingui() @@ -46,7 +49,7 @@ export function ImageContextMenu({ } return ( - + + 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)}> @@ -126,6 +127,7 @@ export function ImageEmbed({ images={images} onPress={onPress} onPressIn={onPressIn} + onPreviewPress={onPreviewPress} viewContext={rest.viewContext} /> diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 1566f09736..1f2056c466 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -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,124 +424,130 @@ function GalleryImage({ collapsable={false} aria-roledescription={l`slide`} aria-label={image.alt || l`Image ${index + 1} of ${imageCount}`}> - setFocused(true)} - onBlur={() => setFocused(false)} - accessibilityRole="button" - accessibilityLabel={image.alt || l`Image ${index + 1}`} - accessibilityHint={l`Opens full image`} - android_ripple={{ - color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), - foreground: true, - }} - style={({pressed}) => [ - a.rounded_md, - a.overflow_hidden, - t.atoms.bg_contrast_25, - web([ - { - cursor: 'inherit', - outline: 0, - border: 0, - }, - a.transition_transform, - {transitionDuration: '200ms'}, - pressed && {transform: [{scale: 0.99}]}, - ]), - ]}> - { - const ar = getAspectRatio(e.source) - if (ar && ar !== aspectRatio) { - setAspectRatio(ar) - } - onThumbDims(index, { - width: e.source.width, - height: e.source.height, - }) + + setFocused(true)} + onBlur={() => setFocused(false)} + accessibilityRole="button" + accessibilityLabel={image.alt || l`Image ${index + 1}`} + accessibilityHint={l`Opens full image`} + android_ripple={{ + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, }} - /> - - {(hasAlt || isCropped) && !hideBadges ? ( - [ + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + web([ { - bottom: a.p_xs.padding, - right: a.p_xs.padding, - gap: 3, + cursor: 'inherit', + outline: 0, + border: 0, }, - largeAltBadge && { - gap: 4, - }, - ]}> - {isCropped && ( - - - - )} - {hasAlt && ( - - - ALT - - - )} - - ) : null} + a.transition_transform, + {transitionDuration: '200ms'}, + pressed && {transform: [{scale: 0.99}]}, + ]), + ]}> + { + const ar = getAspectRatio(e.source) + if (ar && ar !== aspectRatio) { + setAspectRatio(ar) + } + onThumbDims(index, { + width: e.source.width, + height: e.source.height, + }) + }} + /> - + {isCropped && ( + + + + )} + {hasAlt && ( + + + ALT + + + )} + + ) : null} + + - + /> + + ) } diff --git a/src/components/images/ImageLayoutGrid.tsx b/src/components/images/ImageLayoutGrid.tsx index 0017ddf9cf..ef9f351b10 100644 --- a/src/components/images/ImageLayoutGrid.tsx +++ b/src/components/images/ImageLayoutGrid.tsx @@ -17,6 +17,7 @@ interface ImageLayoutGridProps { ) => void onLongPress?: (index: number) => void onPressIn?: (index: number) => void + onPreviewPress?: (index: number) => void style?: StyleProp 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} } diff --git a/src/components/images/ImageLayoutGridItem.tsx b/src/components/images/ImageLayoutGridItem.tsx index b809a783fa..2f694ddf52 100644 --- a/src/components/images/ImageLayoutGridItem.tsx +++ b/src/components/images/ImageLayoutGridItem.tsx @@ -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 viewContext?: PostEmbedViewContext insetBorderStyle?: StyleProp @@ -40,6 +43,7 @@ export function GalleryItem({ onPress, onPressIn, onLongPress, + onPreviewPress, viewContext, insetBorderStyle, containerRefs, @@ -52,46 +56,60 @@ 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 ( - onPress(index, containerRefs, thumbDimsRef.current.slice()) - : undefined + onPreviewPress(index) : undefined } - onPressIn={onPressIn ? () => onPressIn(index) : undefined} - onLongPress={onLongPress ? () => onLongPress(index) : undefined} - android_ripple={{ - color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), - foreground: true, - }} - style={[ - a.flex_1, - a.overflow_hidden, - t.atoms.bg_contrast_25, - imageStyle, - ]} - accessibilityRole="button" - accessibilityLabel={image.alt || _(msg`Image`)} - accessibilityHint=""> - { - thumbDimsRef.current[index] = { - width: e.source.width, - height: e.source.height, - } + style={a.flex_1}> + onPress(index, containerRefs, thumbDimsRef.current.slice()) + : undefined + } + onPressIn={onPressIn ? () => onPressIn(index) : undefined} + onLongPress={onLongPress ? () => onLongPress(index) : undefined} + android_ripple={{ + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), + foreground: true, }} - loading="lazy" - /> - - + style={[ + a.flex_1, + a.overflow_hidden, + t.atoms.bg_contrast_25, + imageStyle, + ]} + accessibilityRole="button" + accessibilityLabel={image.alt || _(msg`Image`)} + accessibilityHint=""> + { + thumbDimsRef.current[index] = { + width: e.source.width, + height: e.source.height, + } + }} + loading="lazy" + /> + + + {hasAlt && !hideBadges ? (