Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Show almost-instant preview when opening lightbox (#6000)

* Plumb thumbUri down to the lightbox

* Remove onLoad tracking from lightbox

* Hook up placeholder URI to the image

* Fix NaN causing crash on double tap while offline

* Protect against NaNs in the future

authored by

dan and committed by
GitHub
ab492cd7 339f45cc

+56 -78
+1
src/state/lightbox.tsx
··· 10 10 11 11 type ImagesLightboxItem = { 12 12 uri: string 13 + thumbUri: string 13 14 alt?: string 14 15 } 15 16
+1 -1
src/view/com/lightbox/ImageViewing/@types/index.ts
··· 16 16 y: number 17 17 } 18 18 19 - export type ImageSource = {uri: string; alt?: string} 19 + export type ImageSource = {uri: string; thumbUri: string; alt?: string}
+11 -10
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
··· 34 34 const MIN_DOUBLE_TAP_SCALE = 2 35 35 const MAX_ORIGINAL_IMAGE_ZOOM = 2 36 36 37 - const AnimatedImage = Animated.createAnimatedComponent(Image) 38 37 const initialTransform = createTransform() 39 38 40 39 type Props = { ··· 53 52 isScrollViewBeingDragged, 54 53 }: Props) => { 55 54 const [isScaled, setIsScaled] = useState(false) 56 - const [isLoaded, setIsLoaded] = useState(false) 57 55 const imageDimensions = useImageDimensions(imageSrc) 58 56 const committedTransform = useSharedValue(initialTransform) 59 57 const panTranslation = useSharedValue({x: 0, y: 0}) ··· 313 311 singleTap, 314 312 ) 315 313 316 - const isLoading = !isLoaded || !imageDimensions 317 314 return ( 318 - <Animated.View ref={containerRef} style={styles.container}> 319 - {isLoading && ( 320 - <ActivityIndicator size="small" color="#FFF" style={styles.loading} /> 321 - )} 315 + <Animated.View 316 + ref={containerRef} 317 + // Necessary to make opacity work for both children together. 318 + renderToHardwareTextureAndroid 319 + style={[styles.container, animatedStyle]}> 320 + <ActivityIndicator size="small" color="#FFF" style={styles.loading} /> 322 321 <GestureDetector gesture={composedGesture}> 323 - <AnimatedImage 322 + <Image 324 323 contentFit="contain" 325 324 source={{uri: imageSrc.uri}} 326 - style={[styles.image, animatedStyle]} 325 + placeholderContentFit="contain" 326 + placeholder={{uri: imageSrc.thumbUri}} 327 + style={styles.image} 327 328 accessibilityLabel={imageSrc.alt} 328 329 accessibilityHint="" 329 - onLoad={() => setIsLoaded(true)} 330 + accessibilityIgnoresInvertColors 330 331 cachePolicy="memory" 331 332 /> 332 333 </GestureDetector>
+26 -22
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
··· 7 7 */ 8 8 9 9 import React, {useState} from 'react' 10 - 11 - import {Dimensions, StyleSheet} from 'react-native' 12 - import {Image} from 'expo-image' 10 + import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native' 11 + import {Gesture, GestureDetector} from 'react-native-gesture-handler' 13 12 import Animated, { 14 13 interpolate, 15 14 runOnJS, ··· 17 16 useAnimatedStyle, 18 17 useSharedValue, 19 18 } from 'react-native-reanimated' 20 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 21 - import {Gesture, GestureDetector} from 'react-native-gesture-handler' 19 + import {Image} from 'expo-image' 22 20 21 + import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 22 + import {Dimensions as ImageDimensions, ImageSource} from '../../@types' 23 23 import useImageDimensions from '../../hooks/useImageDimensions' 24 - 25 - import {ImageSource, Dimensions as ImageDimensions} from '../../@types' 26 - import {ImageLoading} from './ImageLoading' 27 24 28 25 const SWIPE_CLOSE_OFFSET = 75 29 26 const SWIPE_CLOSE_VELOCITY = 1 ··· 40 37 showControls: boolean 41 38 } 42 39 43 - const AnimatedImage = Animated.createAnimatedComponent(Image) 44 - 45 40 const ImageItem = ({ 46 41 imageSrc, 47 42 onTap, ··· 51 46 }: Props) => { 52 47 const scrollViewRef = useAnimatedRef<Animated.ScrollView>() 53 48 const translationY = useSharedValue(0) 54 - const [loaded, setLoaded] = useState(false) 55 49 const [scaled, setScaled] = useState(false) 56 50 const imageDimensions = useImageDimensions(imageSrc) 57 51 const maxZoomScale = imageDimensions ··· 141 135 showsHorizontalScrollIndicator={false} 142 136 showsVerticalScrollIndicator={false} 143 137 maximumZoomScale={maxZoomScale} 144 - contentContainerStyle={styles.imageScrollContainer} 145 138 onScroll={scrollHandler}> 146 - {(!loaded || !imageDimensions) && <ImageLoading />} 147 - <AnimatedImage 148 - contentFit="contain" 149 - source={{uri: imageSrc.uri}} 150 - style={[styles.image, animatedStyle]} 151 - accessibilityLabel={imageSrc.alt} 152 - accessibilityHint="" 153 - onLoad={() => setLoaded(true)} 154 - enableLiveTextInteraction={showControls && !scaled} 155 - /> 139 + <Animated.View style={[styles.imageScrollContainer, animatedStyle]}> 140 + <ActivityIndicator size="small" color="#FFF" style={styles.loading} /> 141 + <Image 142 + contentFit="contain" 143 + source={{uri: imageSrc.uri}} 144 + placeholderContentFit="contain" 145 + placeholder={{uri: imageSrc.thumbUri}} 146 + style={styles.image} 147 + accessibilityLabel={imageSrc.alt} 148 + accessibilityHint="" 149 + enableLiveTextInteraction={showControls && !scaled} 150 + accessibilityIgnoresInvertColors 151 + /> 152 + </Animated.View> 156 153 </Animated.ScrollView> 157 154 </GestureDetector> 158 155 ) ··· 169 166 image: { 170 167 width: SCREEN.width, 171 168 height: SCREEN.height, 169 + }, 170 + loading: { 171 + position: 'absolute', 172 + top: 0, 173 + left: 0, 174 + right: 0, 175 + bottom: 0, 172 176 }, 173 177 }) 174 178
-37
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx
··· 1 - /** 2 - * Copyright (c) JOB TODAY S.A. and its affiliates. 3 - * 4 - * This source code is licensed under the MIT license found in the 5 - * LICENSE file in the root directory of this source tree. 6 - * 7 - */ 8 - 9 - import React from 'react' 10 - 11 - import {ActivityIndicator, Dimensions, StyleSheet, View} from 'react-native' 12 - 13 - const SCREEN = Dimensions.get('screen') 14 - const SCREEN_WIDTH = SCREEN.width 15 - const SCREEN_HEIGHT = SCREEN.height 16 - 17 - export const ImageLoading = () => ( 18 - <View style={styles.loading}> 19 - <ActivityIndicator size="small" color="#FFF" /> 20 - </View> 21 - ) 22 - 23 - const styles = StyleSheet.create({ 24 - listItem: { 25 - width: SCREEN_WIDTH, 26 - height: SCREEN_HEIGHT, 27 - }, 28 - loading: { 29 - width: SCREEN_WIDTH, 30 - height: SCREEN_HEIGHT, 31 - alignItems: 'center', 32 - justifyContent: 'center', 33 - }, 34 - imageScrollContainer: { 35 - height: SCREEN_HEIGHT, 36 - }, 37 - })
+12 -6
src/view/com/lightbox/ImageViewing/hooks/useImageDimensions.ts
··· 8 8 9 9 import {useEffect, useState} from 'react' 10 10 import {Image, ImageURISource} from 'react-native' 11 + 11 12 import {Dimensions, ImageSource} from '../@types' 12 13 13 14 const CACHE_SIZE = 50 ··· 36 37 const useImageDimensions = (image: ImageSource): Dimensions | null => { 37 38 const [dimensions, setDimensions] = useState<Dimensions | null>(null) 38 39 39 - // eslint-disable-next-line @typescript-eslint/no-shadow 40 - const getImageDimensions = (image: ImageSource): Promise<Dimensions> => { 40 + const getImageDimensions = ( 41 + image: ImageSource, 42 + ): Promise<Dimensions | null> => { 41 43 return new Promise(resolve => { 42 44 if (image.uri) { 43 45 const source = image as ImageURISource ··· 51 53 source.uri, 52 54 source.headers, 53 55 (width: number, height: number) => { 54 - imageDimensionsCache.set(cacheKey, {width, height}) 55 - resolve({width, height}) 56 + if (width > 0 && height > 0) { 57 + imageDimensionsCache.set(cacheKey, {width, height}) 58 + resolve({width, height}) 59 + } else { 60 + resolve(null) 61 + } 56 62 }, 57 63 () => { 58 - resolve({width: 0, height: 0}) 64 + resolve(null) 59 65 }, 60 66 ) 61 67 } 62 68 } else { 63 - resolve({width: 0, height: 0}) 69 + resolve(null) 64 70 } 65 71 }) 66 72 }
+3 -1
src/view/com/lightbox/Lightbox.tsx
··· 31 31 const opts = activeLightbox 32 32 return ( 33 33 <ImageView 34 - images={[{uri: opts.profile.avatar || ''}]} 34 + images={[ 35 + {uri: opts.profile.avatar || '', thumbUri: opts.profile.avatar || ''}, 36 + ]} 35 37 initialImageIndex={0} 36 38 visible 37 39 onRequestClose={onClose}
+1 -1
src/view/com/profile/ProfileSubpageHeader.tsx
··· 72 72 ) { 73 73 openLightbox({ 74 74 type: 'images', 75 - images: [{uri: avatar}], 75 + images: [{uri: avatar, thumbUri: avatar}], 76 76 index: 0, 77 77 }) 78 78 }
+1
src/view/com/util/post-embeds/index.tsx
··· 134 134 if (images.length > 0) { 135 135 const items = embed.images.map(img => ({ 136 136 uri: img.fullsize, 137 + thumbUri: img.thumb, 137 138 alt: img.alt, 138 139 aspectRatio: img.aspectRatio, 139 140 }))