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:
@@ -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}`}>
|
||||
<Pressable
|
||||
ref={itemRef}
|
||||
tabIndex={index === 0 ? 0 : -1}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onFocus={() => 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}]},
|
||||
]),
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: image.thumb}}
|
||||
contentFit="cover"
|
||||
accessible={true}
|
||||
accessibilityLabel={image.alt}
|
||||
accessibilityHint=""
|
||||
accessibilityIgnoresInvertColors
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
style={[dims]}
|
||||
onLoad={e => {
|
||||
const ar = getAspectRatio(e.source)
|
||||
if (ar && ar !== aspectRatio) {
|
||||
setAspectRatio(ar)
|
||||
}
|
||||
onThumbDims(index, {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
})
|
||||
<ImageContextMenu
|
||||
fullsizeUri={image.fullsize}
|
||||
aspectRatio={aspectRatio}
|
||||
borderRadius={tokens.borderRadius.md}
|
||||
onPreviewPress={onPreviewPress}>
|
||||
<Pressable
|
||||
ref={itemRef}
|
||||
tabIndex={index === 0 ? 0 : -1}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onFocus={() => 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 ? (
|
||||
<View
|
||||
accessible={false}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.flex_row,
|
||||
style={({pressed}) => [
|
||||
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 && (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
largeAltBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
]}>
|
||||
<Fullscreen
|
||||
fill={t.atoms.text_contrast_high.color}
|
||||
width={largeAltBadge ? 18 : 12}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{hasAlt && (
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
largeAltBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
largeAltBadge ? a.text_xs : {fontSize: 8},
|
||||
]}>
|
||||
<Trans>ALT</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
a.transition_transform,
|
||||
{transitionDuration: '200ms'},
|
||||
pressed && {transform: [{scale: 0.99}]},
|
||||
]),
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: image.thumb}}
|
||||
contentFit="cover"
|
||||
accessible={true}
|
||||
accessibilityLabel={image.alt}
|
||||
accessibilityHint=""
|
||||
accessibilityIgnoresInvertColors
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
style={[dims]}
|
||||
onLoad={e => {
|
||||
const ar = getAspectRatio(e.source)
|
||||
if (ar && ar !== aspectRatio) {
|
||||
setAspectRatio(ar)
|
||||
}
|
||||
onThumbDims(index, {
|
||||
width: e.source.width,
|
||||
height: e.source.height,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<MediaInsetBorder
|
||||
style={
|
||||
focused && {
|
||||
borderWidth: 2,
|
||||
{(hasAlt || isCropped) && !hideBadges ? (
|
||||
<View
|
||||
accessible={false}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.flex_row,
|
||||
{
|
||||
bottom: a.p_xs.padding,
|
||||
right: a.p_xs.padding,
|
||||
gap: 3,
|
||||
},
|
||||
largeAltBadge && {
|
||||
gap: 4,
|
||||
},
|
||||
]}>
|
||||
{isCropped && (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
largeAltBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
]}>
|
||||
<Fullscreen
|
||||
fill={t.atoms.text_contrast_high.color}
|
||||
width={largeAltBadge ? 18 : 12}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{hasAlt && (
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.p_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
opacity: 0.8,
|
||||
},
|
||||
largeAltBadge && {
|
||||
padding: 6,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
largeAltBadge ? a.text_xs : {fontSize: 8},
|
||||
]}>
|
||||
<Trans>ALT</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<MediaInsetBorder
|
||||
style={
|
||||
focused && {
|
||||
borderWidth: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
/>
|
||||
</Pressable>
|
||||
</ImageContextMenu>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user