[Lightbox] Always rely on Expo Image cache (#6189)
* Inline useImageAspectRatio * Switch AutoSizedImage to read dimensions from Expo Image cache * Include thumbnail dimensions in image data * Use dims from Expo Image cache in lightbox * Fix wiring so all thumbnails get dimensions * Fix type * Oops
This commit is contained in:
@@ -1,93 +0,0 @@
|
|||||||
import {useEffect, useState} from 'react'
|
|
||||||
import {Image} from 'react-native'
|
|
||||||
|
|
||||||
import type {Dimensions} from '#/lib/media/types'
|
|
||||||
|
|
||||||
type CacheStorageItem<T> = {key: string; value: T}
|
|
||||||
const createCache = <T>(cacheSize: number) => ({
|
|
||||||
_storage: [] as CacheStorageItem<T>[],
|
|
||||||
get(key: string) {
|
|
||||||
const {value} =
|
|
||||||
this._storage.find(({key: storageKey}) => storageKey === key) || {}
|
|
||||||
return value
|
|
||||||
},
|
|
||||||
set(key: string, value: T) {
|
|
||||||
if (this._storage.length >= cacheSize) {
|
|
||||||
this._storage.shift()
|
|
||||||
}
|
|
||||||
this._storage.push({key, value})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const sizes = createCache<Dimensions>(50)
|
|
||||||
const activeRequests: Map<string, Promise<Dimensions>> = new Map()
|
|
||||||
|
|
||||||
export function get(uri: string): Dimensions | undefined {
|
|
||||||
return sizes.get(uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetch(uri: string): Promise<Dimensions> {
|
|
||||||
const dims = sizes.get(uri)
|
|
||||||
if (dims) {
|
|
||||||
return Promise.resolve(dims)
|
|
||||||
}
|
|
||||||
const activeRequest = activeRequests.get(uri)
|
|
||||||
if (activeRequest) {
|
|
||||||
return activeRequest
|
|
||||||
}
|
|
||||||
const prom = new Promise<Dimensions>((resolve, reject) => {
|
|
||||||
Image.getSize(
|
|
||||||
uri,
|
|
||||||
(width: number, height: number) => {
|
|
||||||
const size = {width, height}
|
|
||||||
sizes.set(uri, size)
|
|
||||||
resolve(size)
|
|
||||||
},
|
|
||||||
(err: any) => {
|
|
||||||
console.error('Failed to fetch image dimensions for', uri, err)
|
|
||||||
reject(new Error('Could not fetch dimensions'))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}).finally(() => {
|
|
||||||
activeRequests.delete(uri)
|
|
||||||
})
|
|
||||||
activeRequests.set(uri, prom)
|
|
||||||
return prom
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useImageDimensions({
|
|
||||||
src,
|
|
||||||
knownDimensions,
|
|
||||||
}: {
|
|
||||||
src: string
|
|
||||||
knownDimensions: Dimensions | null
|
|
||||||
}): [number | undefined, Dimensions | undefined] {
|
|
||||||
const [dims, setDims] = useState(() => knownDimensions ?? get(src))
|
|
||||||
const [prevSrc, setPrevSrc] = useState(src)
|
|
||||||
if (src !== prevSrc) {
|
|
||||||
setDims(knownDimensions ?? get(src))
|
|
||||||
setPrevSrc(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let aborted = false
|
|
||||||
if (dims !== undefined) return
|
|
||||||
fetch(src).then(newDims => {
|
|
||||||
if (aborted) return
|
|
||||||
setDims(newDims)
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
aborted = true
|
|
||||||
}
|
|
||||||
}, [dims, setDims, src])
|
|
||||||
|
|
||||||
let aspectRatio: number | undefined
|
|
||||||
if (dims) {
|
|
||||||
aspectRatio = dims.width / dims.height
|
|
||||||
if (Number.isNaN(aspectRatio)) {
|
|
||||||
aspectRatio = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [aspectRatio, dims]
|
|
||||||
}
|
|
||||||
@@ -72,6 +72,7 @@ let ProfileHeaderShell = ({
|
|||||||
height: 1000,
|
height: 1000,
|
||||||
width: 1000,
|
width: 1000,
|
||||||
},
|
},
|
||||||
|
thumbDimensions: null,
|
||||||
type: 'circle-avi',
|
type: 'circle-avi',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ export type Position = {
|
|||||||
|
|
||||||
export type ImageSource = {
|
export type ImageSource = {
|
||||||
uri: string
|
uri: string
|
||||||
|
dimensions: Dimensions | null
|
||||||
thumbUri: string
|
thumbUri: string
|
||||||
|
thumbDimensions: Dimensions | null
|
||||||
thumbRect: MeasuredDimensions | null
|
thumbRect: MeasuredDimensions | null
|
||||||
alt?: string
|
alt?: string
|
||||||
dimensions: Dimensions | null
|
|
||||||
type: 'image' | 'circle-avi' | 'rect-avi'
|
type: 'image' | 'circle-avi' | 'rect-avi'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Props = {
|
|||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
onTap: () => void
|
onTap: () => void
|
||||||
onZoom: (isZoomed: boolean) => void
|
onZoom: (isZoomed: boolean) => void
|
||||||
|
onLoad: (dims: ImageDimensions) => void
|
||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
showControls: boolean
|
showControls: boolean
|
||||||
measureSafeArea: () => {
|
measureSafeArea: () => {
|
||||||
@@ -66,6 +67,7 @@ const ImageItem = ({
|
|||||||
imageSrc,
|
imageSrc,
|
||||||
onTap,
|
onTap,
|
||||||
onZoom,
|
onZoom,
|
||||||
|
onLoad,
|
||||||
isScrollViewBeingDragged,
|
isScrollViewBeingDragged,
|
||||||
measureSafeArea,
|
measureSafeArea,
|
||||||
imageAspect,
|
imageAspect,
|
||||||
@@ -330,8 +332,8 @@ const ImageItem = ({
|
|||||||
transform: scaleAndMoveTransform.concat(manipulationTransform),
|
transform: scaleAndMoveTransform.concat(manipulationTransform),
|
||||||
width: screenSize.width,
|
width: screenSize.width,
|
||||||
maxHeight: screenSize.height,
|
maxHeight: screenSize.height,
|
||||||
aspectRatio: imageAspect,
|
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
aspectRatio: imageAspect ?? 1 /* force onLoad */,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -349,6 +351,7 @@ const ImageItem = ({
|
|||||||
return {
|
return {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
transform: cropContentTransform,
|
transform: cropContentTransform,
|
||||||
|
opacity: imageAspect === undefined ? 0 : 1,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -393,7 +396,10 @@ const ImageItem = ({
|
|||||||
placeholderContentFit="cover"
|
placeholderContentFit="cover"
|
||||||
placeholder={{uri: imageSrc.thumbUri}}
|
placeholder={{uri: imageSrc.thumbUri}}
|
||||||
accessibilityLabel={imageSrc.alt}
|
accessibilityLabel={imageSrc.alt}
|
||||||
onLoad={() => setHasLoaded(false)}
|
onLoad={e => {
|
||||||
|
setHasLoaded(true)
|
||||||
|
onLoad({width: e.source.width, height: e.source.height})
|
||||||
|
}}
|
||||||
style={{flex: 1, borderRadius}}
|
style={{flex: 1, borderRadius}}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Props = {
|
|||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
onTap: () => void
|
onTap: () => void
|
||||||
onZoom: (scaled: boolean) => void
|
onZoom: (scaled: boolean) => void
|
||||||
|
onLoad: (dims: ImageDimensions) => void
|
||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
showControls: boolean
|
showControls: boolean
|
||||||
measureSafeArea: () => {
|
measureSafeArea: () => {
|
||||||
@@ -64,6 +65,7 @@ const ImageItem = ({
|
|||||||
imageSrc,
|
imageSrc,
|
||||||
onTap,
|
onTap,
|
||||||
onZoom,
|
onZoom,
|
||||||
|
onLoad,
|
||||||
showControls,
|
showControls,
|
||||||
measureSafeArea,
|
measureSafeArea,
|
||||||
imageAspect,
|
imageAspect,
|
||||||
@@ -162,8 +164,9 @@ const ImageItem = ({
|
|||||||
transform: cropFrameTransform,
|
transform: cropFrameTransform,
|
||||||
width: screenSize.width,
|
width: screenSize.width,
|
||||||
maxHeight: screenSize.height,
|
maxHeight: screenSize.height,
|
||||||
aspectRatio: imageAspect,
|
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
aspectRatio: imageAspect ?? 1 /* force onLoad */,
|
||||||
|
opacity: imageAspect === undefined ? 0 : 1,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -172,7 +175,8 @@ const ImageItem = ({
|
|||||||
return {
|
return {
|
||||||
transform: cropContentTransform,
|
transform: cropContentTransform,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
aspectRatio: imageAspect,
|
aspectRatio: imageAspect ?? 1 /* force onLoad */,
|
||||||
|
opacity: imageAspect === undefined ? 0 : 1,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -224,7 +228,10 @@ const ImageItem = ({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
enableLiveTextInteraction={showControls && !scaled}
|
enableLiveTextInteraction={showControls && !scaled}
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
onLoad={() => setHasLoaded(true)}
|
onLoad={e => {
|
||||||
|
setHasLoaded(true)
|
||||||
|
onLoad({width: e.source.width, height: e.source.height})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {View} from 'react-native'
|
|||||||
import {PanGesture} from 'react-native-gesture-handler'
|
import {PanGesture} from 'react-native-gesture-handler'
|
||||||
import {SharedValue} from 'react-native-reanimated'
|
import {SharedValue} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
import {Dimensions} from '#/lib/media/types'
|
||||||
import {
|
import {
|
||||||
Dimensions as ImageDimensions,
|
Dimensions as ImageDimensions,
|
||||||
ImageSource,
|
ImageSource,
|
||||||
@@ -16,6 +17,7 @@ type Props = {
|
|||||||
onRequestClose: () => void
|
onRequestClose: () => void
|
||||||
onTap: () => void
|
onTap: () => void
|
||||||
onZoom: (scaled: boolean) => void
|
onZoom: (scaled: boolean) => void
|
||||||
|
onLoad: (dims: Dimensions) => void
|
||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
showControls: boolean
|
showControls: boolean
|
||||||
measureSafeArea: () => {
|
measureSafeArea: () => {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
import {useImageDimensions} from '#/lib/media/image-sizes'
|
import {Dimensions} from '#/lib/media/types'
|
||||||
import {colors, s} from '#/lib/styles'
|
import {colors, s} from '#/lib/styles'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
import {Lightbox} from '#/state/lightbox'
|
import {Lightbox} from '#/state/lightbox'
|
||||||
@@ -92,7 +92,9 @@ export default function ImageViewRoot({
|
|||||||
|
|
||||||
const canAnimate =
|
const canAnimate =
|
||||||
!PlatformInfo.getIsReducedMotionEnabled() &&
|
!PlatformInfo.getIsReducedMotionEnabled() &&
|
||||||
nextLightbox.images.every(img => img.dimensions && img.thumbRect)
|
nextLightbox.images.every(
|
||||||
|
img => img.thumbRect && (img.dimensions || img.thumbDimensions),
|
||||||
|
)
|
||||||
|
|
||||||
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
// https://github.com/software-mansion/react-native-reanimated/issues/6677
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -345,10 +347,15 @@ function LightboxImage({
|
|||||||
openProgress: SharedValue<number>
|
openProgress: SharedValue<number>
|
||||||
dismissSwipeTranslateY: SharedValue<number>
|
dismissSwipeTranslateY: SharedValue<number>
|
||||||
}) {
|
}) {
|
||||||
const [imageAspect, imageDimensions] = useImageDimensions({
|
const [fetchedDims, setFetchedDims] = React.useState<Dimensions | null>(null)
|
||||||
src: imageSrc.uri,
|
const dims = fetchedDims ?? imageSrc.dimensions ?? imageSrc.thumbDimensions
|
||||||
knownDimensions: imageSrc.dimensions,
|
let imageAspect: number | undefined
|
||||||
})
|
if (dims) {
|
||||||
|
imageAspect = dims.width / dims.height
|
||||||
|
if (Number.isNaN(imageAspect)) {
|
||||||
|
imageAspect = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const safeFrameDelayedForJSThreadOnly = useSafeAreaFrame()
|
const safeFrameDelayedForJSThreadOnly = useSafeAreaFrame()
|
||||||
const safeInsetsDelayedForJSThreadOnly = useSafeAreaInsets()
|
const safeInsetsDelayedForJSThreadOnly = useSafeAreaInsets()
|
||||||
@@ -452,11 +459,12 @@ function LightboxImage({
|
|||||||
onTap={onTap}
|
onTap={onTap}
|
||||||
onZoom={onZoom}
|
onZoom={onZoom}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
|
onLoad={setFetchedDims}
|
||||||
isScrollViewBeingDragged={isScrollViewBeingDragged}
|
isScrollViewBeingDragged={isScrollViewBeingDragged}
|
||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
measureSafeArea={measureSafeArea}
|
measureSafeArea={measureSafeArea}
|
||||||
imageAspect={imageAspect}
|
imageAspect={imageAspect}
|
||||||
imageDimensions={imageDimensions}
|
imageDimensions={dims ?? undefined}
|
||||||
dismissSwipePan={dismissSwipePan}
|
dismissSwipePan={dismissSwipePan}
|
||||||
transforms={transforms}
|
transforms={transforms}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export function ProfileSubpageHeader({
|
|||||||
height: 1000,
|
height: 1000,
|
||||||
width: 1000,
|
width: 1000,
|
||||||
},
|
},
|
||||||
|
thumbDimensions: null,
|
||||||
type: 'rect-avi',
|
type: 'rect-avi',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import {AppBskyEmbedImages} from '@atproto/api'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useImageDimensions} from '#/lib/media/image-sizes'
|
import type {Dimensions} from '#/lib/media/types'
|
||||||
import {Dimensions} from '#/lib/media/types'
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
@@ -15,30 +14,6 @@ import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/compone
|
|||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
function useImageAspectRatio({
|
|
||||||
src,
|
|
||||||
knownDimensions,
|
|
||||||
}: {
|
|
||||||
src: string
|
|
||||||
knownDimensions: Dimensions | null
|
|
||||||
}) {
|
|
||||||
const [raw] = useImageDimensions({src, knownDimensions})
|
|
||||||
let constrained: number | undefined
|
|
||||||
let max: number | undefined
|
|
||||||
let isCropped: boolean | undefined
|
|
||||||
if (raw !== undefined) {
|
|
||||||
const ratio = 1 / 2 // max of 1:2 ratio in feeds
|
|
||||||
constrained = Math.max(raw, ratio)
|
|
||||||
max = Math.max(raw, 0.25) // max of 1:4 in thread
|
|
||||||
isCropped = raw < constrained
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
constrained,
|
|
||||||
max,
|
|
||||||
isCropped,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConstrainedImage({
|
export function ConstrainedImage({
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
fullBleed,
|
fullBleed,
|
||||||
@@ -93,23 +68,38 @@ export function AutoSizedImage({
|
|||||||
image: AppBskyEmbedImages.ViewImage
|
image: AppBskyEmbedImages.ViewImage
|
||||||
crop?: 'none' | 'square' | 'constrained'
|
crop?: 'none' | 'square' | 'constrained'
|
||||||
hideBadge?: boolean
|
hideBadge?: boolean
|
||||||
onPress?: (containerRef: AnimatedRef<React.Component<{}, {}, any>>) => void
|
onPress?: (
|
||||||
|
containerRef: AnimatedRef<React.Component<{}, {}, any>>,
|
||||||
|
fetchedDims: Dimensions | null,
|
||||||
|
) => void
|
||||||
onLongPress?: () => void
|
onLongPress?: () => void
|
||||||
onPressIn?: () => void
|
onPressIn?: () => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const largeAlt = useLargeAltBadgeEnabled()
|
const largeAlt = useLargeAltBadgeEnabled()
|
||||||
const {
|
|
||||||
constrained,
|
|
||||||
max,
|
|
||||||
isCropped: rawIsCropped,
|
|
||||||
} = useImageAspectRatio({
|
|
||||||
src: image.thumb,
|
|
||||||
knownDimensions: image.aspectRatio ?? null,
|
|
||||||
})
|
|
||||||
const containerRef = useAnimatedRef()
|
const containerRef = useAnimatedRef()
|
||||||
|
|
||||||
|
const [fetchedDims, setFetchedDims] = React.useState<Dimensions | null>(null)
|
||||||
|
const dims = fetchedDims ?? image.aspectRatio
|
||||||
|
let aspectRatio: number | undefined
|
||||||
|
if (dims) {
|
||||||
|
aspectRatio = dims.width / dims.height
|
||||||
|
if (Number.isNaN(aspectRatio)) {
|
||||||
|
aspectRatio = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let constrained: number | undefined
|
||||||
|
let max: number | undefined
|
||||||
|
let rawIsCropped: boolean | undefined
|
||||||
|
if (aspectRatio !== undefined) {
|
||||||
|
const ratio = 1 / 2 // max of 1:2 ratio in feeds
|
||||||
|
constrained = Math.max(aspectRatio, ratio)
|
||||||
|
max = Math.max(aspectRatio, 0.25) // max of 1:4 in thread
|
||||||
|
rawIsCropped = aspectRatio < constrained
|
||||||
|
}
|
||||||
|
|
||||||
const cropDisabled = crop === 'none'
|
const cropDisabled = crop === 'none'
|
||||||
const isCropped = rawIsCropped && !cropDisabled
|
const isCropped = rawIsCropped && !cropDisabled
|
||||||
const hasAlt = !!image.alt
|
const hasAlt = !!image.alt
|
||||||
@@ -123,6 +113,9 @@ export function AutoSizedImage({
|
|||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
accessibilityLabel={image.alt}
|
accessibilityLabel={image.alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
onLoad={e => {
|
||||||
|
setFetchedDims({width: e.source.width, height: e.source.height})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<MediaInsetBorder />
|
<MediaInsetBorder />
|
||||||
|
|
||||||
@@ -194,7 +187,7 @@ export function AutoSizedImage({
|
|||||||
if (cropDisabled) {
|
if (cropDisabled) {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => onPress?.(containerRef)}
|
onPress={() => onPress?.(containerRef, fetchedDims)}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
// alt here is what screen readers actually use
|
// alt here is what screen readers actually use
|
||||||
@@ -216,7 +209,7 @@ export function AutoSizedImage({
|
|||||||
fullBleed={crop === 'square'}
|
fullBleed={crop === 'square'}
|
||||||
aspectRatio={constrained ?? 1}>
|
aspectRatio={constrained ?? 1}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => onPress?.(containerRef)}
|
onPress={() => onPress?.(containerRef, fetchedDims)}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
// alt here is what screen readers actually use
|
// alt here is what screen readers actually use
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {AppBskyEmbedImages} from '@atproto/api'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {Dimensions} from '#/lib/media/types'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -20,6 +21,7 @@ interface Props {
|
|||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
||||||
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: EventFunction
|
onLongPress?: EventFunction
|
||||||
onPressIn?: EventFunction
|
onPressIn?: EventFunction
|
||||||
@@ -27,6 +29,7 @@ interface Props {
|
|||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
insetBorderStyle?: StyleProp<ViewStyle>
|
insetBorderStyle?: StyleProp<ViewStyle>
|
||||||
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[]
|
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[]
|
||||||
|
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GalleryItem({
|
export function GalleryItem({
|
||||||
@@ -39,6 +42,7 @@ export function GalleryItem({
|
|||||||
viewContext,
|
viewContext,
|
||||||
insetBorderStyle,
|
insetBorderStyle,
|
||||||
containerRefs,
|
containerRefs,
|
||||||
|
thumbDimsRef,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -53,7 +57,11 @@ export function GalleryItem({
|
|||||||
ref={containerRefs[index]}
|
ref={containerRefs[index]}
|
||||||
collapsable={false}>
|
collapsable={false}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress ? () => onPress(index, containerRefs) : undefined}
|
onPress={
|
||||||
|
onPress
|
||||||
|
? () => onPress(index, containerRefs, thumbDimsRef.current.slice())
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
|
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
|
||||||
onLongPress={onLongPress ? () => onLongPress(index) : undefined}
|
onLongPress={onLongPress ? () => onLongPress(index) : undefined}
|
||||||
style={[
|
style={[
|
||||||
@@ -72,6 +80,12 @@ export function GalleryItem({
|
|||||||
accessibilityLabel={image.alt}
|
accessibilityLabel={image.alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
|
onLoad={e => {
|
||||||
|
thumbDimsRef.current[index] = {
|
||||||
|
width: e.source.width,
|
||||||
|
height: e.source.height,
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<MediaInsetBorder style={insetBorderStyle} />
|
<MediaInsetBorder style={insetBorderStyle} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {AppBskyEmbedImages} from '@atproto/api'
|
|||||||
|
|
||||||
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
||||||
import {atoms as a, useBreakpoints} from '#/alf'
|
import {atoms as a, useBreakpoints} from '#/alf'
|
||||||
|
import {Dimensions} from '../../lightbox/ImageViewing/@types'
|
||||||
import {GalleryItem} from './Gallery'
|
import {GalleryItem} from './Gallery'
|
||||||
|
|
||||||
interface ImageLayoutGridProps {
|
interface ImageLayoutGridProps {
|
||||||
@@ -12,6 +13,7 @@ interface ImageLayoutGridProps {
|
|||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
||||||
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
@@ -42,6 +44,7 @@ interface ImageLayoutGridInnerProps {
|
|||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
containerRefs: AnimatedRef<React.Component<{}, {}, any>>[],
|
||||||
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
@@ -57,6 +60,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
const containerRef2 = useAnimatedRef()
|
const containerRef2 = useAnimatedRef()
|
||||||
const containerRef3 = useAnimatedRef()
|
const containerRef3 = useAnimatedRef()
|
||||||
const containerRef4 = useAnimatedRef()
|
const containerRef4 = useAnimatedRef()
|
||||||
|
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
|
||||||
|
|
||||||
switch (count) {
|
switch (count) {
|
||||||
case 2: {
|
case 2: {
|
||||||
@@ -69,6 +73,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
index={0}
|
index={0}
|
||||||
insetBorderStyle={noCorners(['topRight', 'bottomRight'])}
|
insetBorderStyle={noCorners(['topRight', 'bottomRight'])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_1, {aspectRatio: 1}]}>
|
<View style={[a.flex_1, {aspectRatio: 1}]}>
|
||||||
@@ -77,6 +82,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
index={1}
|
index={1}
|
||||||
insetBorderStyle={noCorners(['topLeft', 'bottomLeft'])}
|
insetBorderStyle={noCorners(['topLeft', 'bottomLeft'])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -93,6 +99,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
index={0}
|
index={0}
|
||||||
insetBorderStyle={noCorners(['topRight', 'bottomRight'])}
|
insetBorderStyle={noCorners(['topRight', 'bottomRight'])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_1, {aspectRatio: 1}, gap]}>
|
<View style={[a.flex_1, {aspectRatio: 1}, gap]}>
|
||||||
@@ -106,6 +113,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
'bottomRight',
|
'bottomRight',
|
||||||
])}
|
])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
@@ -118,6 +126,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
'topRight',
|
'topRight',
|
||||||
])}
|
])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -145,6 +154,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
'bottomRight',
|
'bottomRight',
|
||||||
])}
|
])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_1, {aspectRatio: 1.5}]}>
|
<View style={[a.flex_1, {aspectRatio: 1.5}]}>
|
||||||
@@ -157,6 +167,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
'bottomRight',
|
'bottomRight',
|
||||||
])}
|
])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -171,6 +182,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
'bottomRight',
|
'bottomRight',
|
||||||
])}
|
])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_1, {aspectRatio: 1.5}]}>
|
<View style={[a.flex_1, {aspectRatio: 1.5}]}>
|
||||||
@@ -183,6 +195,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
'topRight',
|
'topRight',
|
||||||
])}
|
])}
|
||||||
containerRefs={containerRefs}
|
containerRefs={containerRefs}
|
||||||
|
thumbDimsRef={thumbDimsRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import * as ListCard from '#/components/ListCard'
|
import * as ListCard from '#/components/ListCard'
|
||||||
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||||
import {ContentHider} from '../../../../components/moderation/ContentHider'
|
import {ContentHider} from '../../../../components/moderation/ContentHider'
|
||||||
|
import {Dimensions} from '../../lightbox/ImageViewing/@types'
|
||||||
import {AutoSizedImage} from '../images/AutoSizedImage'
|
import {AutoSizedImage} from '../images/AutoSizedImage'
|
||||||
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
|
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
|
||||||
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
||||||
@@ -148,11 +149,13 @@ export function PostEmbeds({
|
|||||||
const _openLightbox = (
|
const _openLightbox = (
|
||||||
index: number,
|
index: number,
|
||||||
thumbRects: (MeasuredDimensions | null)[],
|
thumbRects: (MeasuredDimensions | null)[],
|
||||||
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => {
|
) => {
|
||||||
openLightbox({
|
openLightbox({
|
||||||
images: items.map((item, i) => ({
|
images: items.map((item, i) => ({
|
||||||
...item,
|
...item,
|
||||||
thumbRect: thumbRects[i] ?? null,
|
thumbRect: thumbRects[i] ?? null,
|
||||||
|
thumbDimensions: fetchedDims[i] ?? null,
|
||||||
type: 'image',
|
type: 'image',
|
||||||
})),
|
})),
|
||||||
index,
|
index,
|
||||||
@@ -161,11 +164,12 @@ export function PostEmbeds({
|
|||||||
const onPress = (
|
const onPress = (
|
||||||
index: number,
|
index: number,
|
||||||
refs: AnimatedRef<React.Component<{}, {}, any>>[],
|
refs: AnimatedRef<React.Component<{}, {}, any>>[],
|
||||||
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => {
|
) => {
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rects = refs.map(ref => (ref ? measure(ref) : null))
|
const rects = refs.map(ref => (ref ? measure(ref) : null))
|
||||||
runOnJS(_openLightbox)(index, rects)
|
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
const onPressIn = (_: number) => {
|
const onPressIn = (_: number) => {
|
||||||
@@ -189,7 +193,9 @@ export function PostEmbeds({
|
|||||||
: 'constrained'
|
: 'constrained'
|
||||||
}
|
}
|
||||||
image={image}
|
image={image}
|
||||||
onPress={containerRef => onPress(0, [containerRef])}
|
onPress={(containerRef, dims) =>
|
||||||
|
onPress(0, [containerRef], [dims])
|
||||||
|
}
|
||||||
onPressIn={() => onPressIn(0)}
|
onPressIn={() => onPressIn(0)}
|
||||||
hideBadge={
|
hideBadge={
|
||||||
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||||
|
|||||||
Reference in New Issue
Block a user