Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify dimensions cache between lightbox and feed #6047

Merged
merged 14 commits into from
Nov 4, 2024
100 changes: 79 additions & 21 deletions src/lib/media/image-sizes.ts
Original file line number Diff line number Diff line change
@@ -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) => ({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is copypasta from deleted file

_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]
}
2 changes: 2 additions & 0 deletions src/state/lightbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -14,6 +15,7 @@ type ImagesLightboxItem = {
uri: string
thumbUri: string
alt?: string
dimensions: Dimensions | null
}

type ImagesLightbox = {
Expand Down
7 changes: 6 additions & 1 deletion src/view/com/lightbox/ImageViewing/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -248,7 +251,7 @@ const ImageItem = ({
.numberOfTaps(2)
.onEnd(e => {
'worklet'
if (!imageDimensions) {
if (!imageDimensions || !imageAspect) {
return
}
const [, , committedScale] = readTransform(committedTransform.value)
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -99,7 +102,7 @@ const ImageItem = ({
const willZoom = !scaled
if (willZoom) {
nextZoomRect = getZoomRectAfterDoubleTap(
imageDimensions,
imageAspect,
absoluteX,
absoluteY,
)
Expand Down Expand Up @@ -179,7 +182,7 @@ const styles = StyleSheet.create({
})

const getZoomRectAfterDoubleTap = (
imageDimensions: ImageDimensions | null,
imageAspect: number | undefined,
touchX: number,
touchY: number,
): {
Expand All @@ -188,7 +191,7 @@ const getZoomRectAfterDoubleTap = (
width: number
height: number
} => {
if (!imageDimensions) {
if (!imageAspect) {
return {
x: 0,
y: 0,
Expand All @@ -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,
Expand Down
93 changes: 0 additions & 93 deletions src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts

This file was deleted.

10 changes: 9 additions & 1 deletion src/view/com/lightbox/Lightbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
12 changes: 11 additions & 1 deletion src/view/com/profile/ProfileSubpageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
Loading
Loading