diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/media/image-sizes.ts | 100 | ||||
-rw-r--r-- | src/state/lightbox.tsx | 2 | ||||
-rw-r--r-- | src/view/com/lightbox/ImageViewing/@types/index.ts | 7 | ||||
-rw-r--r-- | src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx | 17 | ||||
-rw-r--r-- | src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx | 16 | ||||
-rw-r--r-- | src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts | 93 | ||||
-rw-r--r-- | src/view/com/lightbox/Lightbox.tsx | 10 | ||||
-rw-r--r-- | src/view/com/profile/ProfileSubpageHeader.tsx | 12 | ||||
-rw-r--r-- | src/view/com/util/images/AutoSizedImage.tsx | 61 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 2 |
10 files changed, 144 insertions, 176 deletions
diff --git a/src/lib/media/image-sizes.ts b/src/lib/media/image-sizes.ts index 7a1555688..8eaa9467f 100644 --- a/src/lib/media/image-sizes.ts +++ b/src/lib/media/image-sizes.ts @@ -1,35 +1,93 @@ +import {useEffect, useState} from 'react' import {Image} from 'react-native' import type {Dimensions} from '#/lib/media/types' -const sizes: Map<string, Dimensions> = new Map() +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 async function fetch(uri: string): Promise<Dimensions> { - const Dimensions = sizes.get(uri) - if (Dimensions) { - return Dimensions +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) } - const prom = - activeRequests.get(uri) || - new Promise<Dimensions>(resolve => { - Image.getSize( - uri, - (width: number, height: number) => resolve({width, height}), - (err: any) => { - console.error('Failed to fetch image dimensions for', uri, err) - resolve({width: 0, height: 0}) - }, - ) + useEffect(() => { + let aborted = false + if (dims !== undefined) return + fetch(src).then(newDims => { + if (aborted) return + setDims(newDims) }) - activeRequests.set(uri, prom) - const res = await prom - activeRequests.delete(uri) - sizes.set(uri, res) - return res + 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] } diff --git a/src/state/lightbox.tsx b/src/state/lightbox.tsx index eb5a88864..53409e3ec 100644 --- a/src/state/lightbox.tsx +++ b/src/state/lightbox.tsx @@ -3,6 +3,7 @@ import type {MeasuredDimensions} from 'react-native-reanimated' import {AppBskyActorDefs} from '@atproto/api' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {Dimensions} from '#/lib/media/types' type ProfileImageLightbox = { type: 'profile-image' @@ -14,6 +15,7 @@ type ImagesLightboxItem = { uri: string thumbUri: string alt?: string + dimensions: Dimensions | null } type ImagesLightbox = { diff --git a/src/view/com/lightbox/ImageViewing/@types/index.ts b/src/view/com/lightbox/ImageViewing/@types/index.ts index 8fdc3f364..f5ab8bba9 100644 --- a/src/view/com/lightbox/ImageViewing/@types/index.ts +++ b/src/view/com/lightbox/ImageViewing/@types/index.ts @@ -16,4 +16,9 @@ export type Position = { y: number } -export type ImageSource = {uri: string; thumbUri: string; alt?: string} +export type ImageSource = { + uri: string + thumbUri: string + alt?: string + dimensions: Dimensions | null +} diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx index 3de15b379..487acf931 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -12,8 +12,8 @@ import Animated, { } from 'react-native-reanimated' import {Image} from 'expo-image' +import {useImageDimensions} from '#/lib/media/image-sizes' import type {Dimensions as ImageDimensions, ImageSource} from '../../@types' -import useImageDimensions from '../../hooks/useImageDimensions' import { applyRounding, createTransform, @@ -52,7 +52,10 @@ const ImageItem = ({ isScrollViewBeingDragged, }: Props) => { const [isScaled, setIsScaled] = useState(false) - const imageDimensions = useImageDimensions(imageSrc) + const [imageAspect, imageDimensions] = useImageDimensions({ + src: imageSrc.uri, + knownDimensions: imageSrc.dimensions, + }) const committedTransform = useSharedValue(initialTransform) const panTranslation = useSharedValue({x: 0, y: 0}) const pinchOrigin = useSharedValue({x: 0, y: 0}) @@ -119,12 +122,12 @@ const ImageItem = ({ candidateTransform: TransformMatrix, ) { 'worklet' - if (!imageDimensions) { + if (!imageAspect) { return [0, 0] } const [nextTranslateX, nextTranslateY, nextScale] = readTransform(candidateTransform) - const scaledDimensions = getScaledDimensions(imageDimensions, nextScale) + const scaledDimensions = getScaledDimensions(imageAspect, nextScale) const clampedTranslateX = clampTranslation( nextTranslateX, scaledDimensions.width, @@ -248,7 +251,7 @@ const ImageItem = ({ .numberOfTaps(2) .onEnd(e => { 'worklet' - if (!imageDimensions) { + if (!imageDimensions || !imageAspect) { return } const [, , committedScale] = readTransform(committedTransform.value) @@ -260,7 +263,6 @@ const ImageItem = ({ } // Try to zoom in so that we get rid of the black bars (whatever the orientation was). - const imageAspect = imageDimensions.width / imageDimensions.height const screenAspect = SCREEN.width / SCREEN.height const candidateScale = Math.max( imageAspect / screenAspect, @@ -363,11 +365,10 @@ const styles = StyleSheet.create({ }) function getScaledDimensions( - imageDimensions: ImageDimensions, + imageAspect: number, scale: number, ): ImageDimensions { 'worklet' - const imageAspect = imageDimensions.width / imageDimensions.height const screenAspect = SCREEN.width / SCREEN.height const isLandscape = imageAspect > screenAspect if (isLandscape) { diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index f21953498..a96a1c913 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -19,8 +19,8 @@ import Animated, { import {Image} from 'expo-image' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' -import {Dimensions as ImageDimensions, ImageSource} from '../../@types' -import useImageDimensions from '../../hooks/useImageDimensions' +import {useImageDimensions} from '#/lib/media/image-sizes' +import {ImageSource} from '../../@types' const SWIPE_CLOSE_OFFSET = 75 const SWIPE_CLOSE_VELOCITY = 1 @@ -47,7 +47,10 @@ const ImageItem = ({ const scrollViewRef = useAnimatedRef<Animated.ScrollView>() const translationY = useSharedValue(0) const [scaled, setScaled] = useState(false) - const imageDimensions = useImageDimensions(imageSrc) + const [imageAspect, imageDimensions] = useImageDimensions({ + src: imageSrc.uri, + knownDimensions: imageSrc.dimensions, + }) const maxZoomScale = imageDimensions ? (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM : 1 @@ -99,7 +102,7 @@ const ImageItem = ({ const willZoom = !scaled if (willZoom) { nextZoomRect = getZoomRectAfterDoubleTap( - imageDimensions, + imageAspect, absoluteX, absoluteY, ) @@ -179,7 +182,7 @@ const styles = StyleSheet.create({ }) const getZoomRectAfterDoubleTap = ( - imageDimensions: ImageDimensions | null, + imageAspect: number | undefined, touchX: number, touchY: number, ): { @@ -188,7 +191,7 @@ const getZoomRectAfterDoubleTap = ( width: number height: number } => { - if (!imageDimensions) { + if (!imageAspect) { return { x: 0, y: 0, @@ -199,7 +202,6 @@ const getZoomRectAfterDoubleTap = ( // First, let's figure out how much we want to zoom in. // We want to try to zoom in at least close enough to get rid of black bars. - const imageAspect = imageDimensions.width / imageDimensions.height const screenAspect = SCREEN.width / SCREEN.height const zoom = Math.max( imageAspect / screenAspect, diff --git a/src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts b/src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts deleted file mode 100644 index 8b5bc1b87..000000000 --- a/src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) JOB TODAY S.A. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import {useEffect, useState} from 'react' -import {Image, ImageURISource} from 'react-native' - -import {Dimensions, ImageSource} from '../@types' - -const CACHE_SIZE = 50 - -type CacheStorageItem = {key: string; value: any} - -const createCache = (cacheSize: number) => ({ - _storage: [] as CacheStorageItem[], - get(key: string): any { - const {value} = - this._storage.find(({key: storageKey}) => storageKey === key) || {} - - return value - }, - set(key: string, value: any) { - if (this._storage.length >= cacheSize) { - this._storage.shift() - } - - this._storage.push({key, value}) - }, -}) - -const imageDimensionsCache = createCache(CACHE_SIZE) - -const useImageDimensions = (image: ImageSource): Dimensions | null => { - const [dimensions, setDimensions] = useState<Dimensions | null>(null) - - const getImageDimensions = ( - image: ImageSource, - ): Promise<Dimensions | null> => { - return new Promise(resolve => { - if (image.uri) { - const source = image as ImageURISource - const cacheKey = source.uri as string - const imageDimensions = imageDimensionsCache.get(cacheKey) - if (imageDimensions) { - resolve(imageDimensions) - } else { - Image.getSizeWithHeaders( - // @ts-ignore - source.uri, - source.headers, - (width: number, height: number) => { - if (width > 0 && height > 0) { - imageDimensionsCache.set(cacheKey, {width, height}) - resolve({width, height}) - } else { - resolve(null) - } - }, - () => { - resolve(null) - }, - ) - } - } else { - resolve(null) - } - }) - } - - let isImageUnmounted = false - - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-shadow - getImageDimensions(image).then(dimensions => { - if (!isImageUnmounted) { - setDimensions(dimensions) - } - }) - - return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps - isImageUnmounted = true - } - }, [image]) - - return dimensions -} - -export default useImageDimensions diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 891be3f9c..83ea2e941 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -32,7 +32,15 @@ export function Lightbox() { return ( <ImageView images={[ - {uri: opts.profile.avatar || '', thumbUri: opts.profile.avatar || ''}, + { + uri: opts.profile.avatar || '', + thumbUri: opts.profile.avatar || '', + dimensions: { + // It's fine if it's actually smaller but we know it's 1:1. + height: 1000, + width: 1000, + }, + }, ]} initialImageIndex={0} thumbDims={opts.thumbDims} diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index 785080c4b..d5b7c2b68 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -72,7 +72,17 @@ export function ProfileSubpageHeader({ ) { openLightbox({ type: 'images', - images: [{uri: avatar, thumbUri: avatar}], + images: [ + { + uri: avatar, + thumbUri: avatar, + dimensions: { + // It's fine if it's actually smaller but we know it's 1:1. + height: 1000, + width: 1000, + }, + }, + ], index: 0, thumbDims: null, }) diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index a9bfc1c96..ce2389ce2 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -5,7 +5,7 @@ import {AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import * as imageSizes from '#/lib/media/image-sizes' +import {useImageDimensions} from '#/lib/media/image-sizes' import {Dimensions} from '#/lib/media/types' import {isNative} from '#/platform/detection' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' @@ -14,44 +14,24 @@ import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/compone import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' -export function useImageAspectRatio({ +function useImageAspectRatio({ src, - dimensions, + knownDimensions, }: { src: string - dimensions: Dimensions | undefined + knownDimensions: Dimensions | null }) { - const [raw, setAspectRatio] = React.useState<number>( - dimensions ? calc(dimensions) : 1, - ) - // this basically controls the width of the image - const {isCropped, constrained, max} = React.useMemo(() => { + 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 - const constrained = Math.max(raw, ratio) - const max = Math.max(raw, 0.25) // max of 1:4 in thread - const isCropped = raw < constrained - return { - isCropped, - constrained, - max, - } - }, [raw]) - - React.useEffect(() => { - let aborted = false - if (dimensions) return - imageSizes.fetch(src).then(newDim => { - if (aborted) return - setAspectRatio(calc(newDim)) - }) - return () => { - aborted = true - } - }, [dimensions, setAspectRatio, src]) - + constrained = Math.max(raw, ratio) + max = Math.max(raw, 0.25) // max of 1:4 in thread + isCropped = raw < constrained + } return { - dimensions, - raw, constrained, max, isCropped, @@ -125,7 +105,7 @@ export function AutoSizedImage({ isCropped: rawIsCropped, } = useImageAspectRatio({ src: image.thumb, - dimensions: image.aspectRatio, + knownDimensions: image.aspectRatio ?? null, }) const cropDisabled = crop === 'none' const isCropped = rawIsCropped && !cropDisabled @@ -222,14 +202,16 @@ export function AutoSizedImage({ a.rounded_md, a.overflow_hidden, t.atoms.bg_contrast_25, - {aspectRatio: max}, + {aspectRatio: max ?? 1}, ]}> {contents} </Pressable> ) } else { return ( - <ConstrainedImage fullBleed={crop === 'square'} aspectRatio={constrained}> + <ConstrainedImage + fullBleed={crop === 'square'} + aspectRatio={constrained ?? 1}> <Pressable onPress={onPress} onLongPress={onLongPress} @@ -244,10 +226,3 @@ export function AutoSizedImage({ ) } } - -function calc(dim: Dimensions) { - if (dim.width === 0 || dim.height === 0) { - return 1 - } - return dim.width / dim.height -} diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 19769bcd7..13e4d12e0 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -145,7 +145,7 @@ export function PostEmbeds({ uri: img.fullsize, thumbUri: img.thumb, alt: img.alt, - aspectRatio: img.aspectRatio, + dimensions: img.aspectRatio ?? null, })) const _openLightbox = ( index: number, |