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:
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user