Bluesky app fork with some witchin' additions 💫

Change lightbox to use Pager (#1666)

* Change lightbox to use Pager

* Fix crash issue on ios

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

authored by danabra.mov

Paul Frazee and committed by
GitHub
209d8b68 aa085b0b

+46 -141
+12 -37
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
··· 1 - import React, {MutableRefObject, useState} from 'react' 1 + import React, {useState} from 'react' 2 2 3 3 import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native' 4 4 import {Image} from 'expo-image' 5 5 import Animated, { 6 - measure, 7 6 runOnJS, 8 7 useAnimatedRef, 9 8 useAnimatedStyle, ··· 12 11 withDecay, 13 12 withSpring, 14 13 } from 'react-native-reanimated' 15 - import { 16 - GestureDetector, 17 - Gesture, 18 - GestureType, 19 - } from 'react-native-gesture-handler' 14 + import {GestureDetector, Gesture} from 'react-native-gesture-handler' 20 15 import useImageDimensions from '../../hooks/useImageDimensions' 21 16 import { 22 17 createTransform, ··· 40 35 imageSrc: ImageSource 41 36 onRequestClose: () => void 42 37 onZoom: (isZoomed: boolean) => void 43 - pinchGestureRef: MutableRefObject<GestureType | undefined> 44 38 isScrollViewBeingDragged: boolean 45 39 } 46 40 const ImageItem = ({ ··· 48 42 onZoom, 49 43 onRequestClose, 50 44 isScrollViewBeingDragged, 51 - pinchGestureRef, 52 45 }: Props) => { 53 46 const [isScaled, setIsScaled] = useState(false) 54 47 const [isLoaded, setIsLoaded] = useState(false) ··· 140 133 return [dx, dy] 141 134 } 142 135 143 - // This is a hack. 144 - // We need to disallow any gestures (and let the native parent scroll view scroll) while you're scrolling it. 145 - // However, there is no great reliable way to coordinate this yet in RGNH. 146 - // This "fake" manual gesture handler whenever you're trying to touch something while the parent scrollview is not at rest. 147 - const consumeHScroll = Gesture.Manual().onTouchesDown((e, manager) => { 148 - if (isScrollViewBeingDragged) { 149 - // Steal the gesture (and do nothing, so native ScrollView does its thing). 150 - manager.activate() 151 - return 152 - } 153 - const measurement = measure(containerRef) 154 - if (!measurement || measurement.pageX !== 0) { 155 - // Steal the gesture (and do nothing, so native ScrollView does its thing). 156 - manager.activate() 157 - return 158 - } 159 - // Fail this "fake" gesture so that the gestures after it can proceed. 160 - manager.fail() 161 - }) 162 - 163 136 const pinch = Gesture.Pinch() 164 - .withRef(pinchGestureRef) 165 137 .onStart(e => { 166 138 pinchOrigin.value = { 167 139 x: e.focalX - SCREEN.width / 2, ··· 318 290 } 319 291 }) 320 292 293 + const composedGesture = isScrollViewBeingDragged 294 + ? // If the parent is not at rest, provide a no-op gesture. 295 + Gesture.Manual() 296 + : Gesture.Exclusive( 297 + dismissSwipePan, 298 + Gesture.Simultaneous(pinch, pan), 299 + doubleTap, 300 + ) 301 + 321 302 const isLoading = !isLoaded || !imageDimensions 322 303 return ( 323 304 <Animated.View ref={containerRef} style={styles.container}> 324 305 {isLoading && ( 325 306 <ActivityIndicator size="small" color="#FFF" style={styles.loading} /> 326 307 )} 327 - <GestureDetector 328 - gesture={Gesture.Exclusive( 329 - consumeHScroll, 330 - dismissSwipePan, 331 - Gesture.Simultaneous(pinch, pan), 332 - doubleTap, 333 - )}> 308 + <GestureDetector gesture={composedGesture}> 334 309 <AnimatedImage 335 310 source={imageSrc} 336 311 contentFit="contain"
+2 -4
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
··· 6 6 * 7 7 */ 8 8 9 - import React, {MutableRefObject, useCallback, useState} from 'react' 9 + import React, {useCallback, useState} from 'react' 10 10 11 11 import { 12 12 Dimensions, ··· 25 25 useAnimatedStyle, 26 26 useSharedValue, 27 27 } from 'react-native-reanimated' 28 - import {GestureType} from 'react-native-gesture-handler' 29 28 30 29 import useImageDimensions from '../../hooks/useImageDimensions' 31 30 ··· 43 42 imageSrc: ImageSource 44 43 onRequestClose: () => void 45 44 onZoom: (scaled: boolean) => void 46 - pinchGestureRef: MutableRefObject<GestureType> 47 45 isScrollViewBeingDragged: boolean 48 46 } 49 47 ··· 145 143 accessibilityHint=""> 146 144 <AnimatedImage 147 145 contentFit="contain" 148 - source={imageSrc} 146 + source={{uri: imageSrc.uri}} 149 147 style={[styles.image, animatedStyle]} 150 148 onLoad={() => setLoaded(true)} 151 149 />
+1 -3
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx
··· 1 1 // default implementation fallback for web 2 2 3 - import React, {MutableRefObject} from 'react' 3 + import React from 'react' 4 4 import {View} from 'react-native' 5 - import {GestureType} from 'react-native-gesture-handler' 6 5 import {ImageSource} from '../../@types' 7 6 8 7 type Props = { 9 8 imageSrc: ImageSource 10 9 onRequestClose: () => void 11 10 onZoom: (scaled: boolean) => void 12 - pinchGestureRef: MutableRefObject<GestureType | undefined> 13 11 isScrollViewBeingDragged: boolean 14 12 } 15 13
+31 -97
src/view/com/lightbox/ImageViewing/index.tsx
··· 8 8 // Original code copied and simplified from the link below as the codebase is currently not maintained: 9 9 // https://github.com/jobtoday/react-native-image-viewing 10 10 11 - import React, { 12 - ComponentType, 13 - createRef, 14 - useCallback, 15 - useRef, 16 - useMemo, 17 - useState, 18 - } from 'react' 19 - import { 20 - Animated, 21 - Dimensions, 22 - NativeSyntheticEvent, 23 - NativeScrollEvent, 24 - StyleSheet, 25 - View, 26 - VirtualizedList, 27 - ModalProps, 28 - Platform, 29 - } from 'react-native' 11 + import React, {ComponentType, useMemo, useState} from 'react' 12 + import {Animated, StyleSheet, View, ModalProps, Platform} from 'react-native' 30 13 31 14 import ImageItem from './components/ImageItem/ImageItem' 32 15 import ImageDefaultHeader from './components/ImageDefaultHeader' 33 16 34 17 import {ImageSource} from './@types' 35 - import {ScrollView, GestureType} from 'react-native-gesture-handler' 36 18 import {Edge, SafeAreaView} from 'react-native-safe-area-context' 19 + import PagerView from 'react-native-pager-view' 37 20 38 21 type Props = { 39 22 images: ImageSource[] ··· 48 31 } 49 32 50 33 const DEFAULT_BG_COLOR = '#000' 51 - const SCREEN = Dimensions.get('screen') 52 - const SCREEN_WIDTH = SCREEN.width 53 34 const INITIAL_POSITION = {x: 0, y: 0} 54 35 const ANIMATION_CONFIG = { 55 36 duration: 200, ··· 65 46 HeaderComponent, 66 47 FooterComponent, 67 48 }: Props) { 68 - const imageList = useRef<VirtualizedList<ImageSource>>(null) 69 49 const [isScaled, setIsScaled] = useState(false) 70 50 const [isDragging, setIsDragging] = useState(false) 71 51 const [imageIndex, setImageIndex] = useState(initialImageIndex) ··· 96 76 } 97 77 } 98 78 99 - const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => { 100 - const { 101 - nativeEvent: { 102 - contentOffset: {x: scrollX}, 103 - }, 104 - } = event 105 - 106 - if (SCREEN.width) { 107 - const nextIndex = Math.round(scrollX / SCREEN.width) 108 - setImageIndex(nextIndex < 0 ? 0 : nextIndex) 109 - } 110 - } 111 - 112 79 const onZoom = (nextIsScaled: boolean) => { 113 80 toggleBarsVisible(!nextIsScaled) 114 81 setIsScaled(false) ··· 121 88 return ['left', 'right'] satisfies Edge[] // iOS, so no top/bottom safe area 122 89 }, []) 123 90 124 - const onLayout = useCallback(() => { 125 - if (initialImageIndex) { 126 - imageList.current?.scrollToIndex({ 127 - index: initialImageIndex, 128 - animated: false, 129 - }) 130 - } 131 - }, [imageList, initialImageIndex]) 132 - 133 - // This is a hack. 134 - // RNGH doesn't have an easy way to express that pinch of individual items 135 - // should "steal" all pinches from the scroll view. So we're keeping a ref 136 - // to all pinch gestures so that we may give them to <ScrollView waitFor={...}>. 137 - const [pinchGestureRefs] = useState(new Map()) 138 - for (let imageSrc of images) { 139 - if (!pinchGestureRefs.get(imageSrc)) { 140 - pinchGestureRefs.set(imageSrc, createRef<GestureType | undefined>()) 141 - } 142 - } 143 - 144 91 if (!visible) { 145 92 return null 146 93 } ··· 150 97 return ( 151 98 <SafeAreaView 152 99 style={styles.screen} 153 - onLayout={onLayout} 154 100 edges={edges} 155 101 aria-modal 156 102 accessibilityViewIsModal> ··· 164 110 <ImageDefaultHeader onRequestClose={onRequestClose} /> 165 111 )} 166 112 </Animated.View> 167 - <VirtualizedList 168 - ref={imageList} 169 - data={images} 170 - horizontal 171 - pagingEnabled 172 - scrollEnabled={!isScaled || isDragging} 173 - showsHorizontalScrollIndicator={false} 174 - showsVerticalScrollIndicator={false} 175 - getItem={(_, index) => images[index]} 176 - getItemCount={() => images.length} 177 - getItemLayout={(_, index) => ({ 178 - length: SCREEN_WIDTH, 179 - offset: SCREEN_WIDTH * index, 180 - index, 181 - })} 182 - renderItem={({item: imageSrc}) => ( 183 - <ImageItem 184 - onZoom={onZoom} 185 - imageSrc={imageSrc} 186 - onRequestClose={onRequestClose} 187 - pinchGestureRef={pinchGestureRefs.get(imageSrc)} 188 - isScrollViewBeingDragged={isDragging} 189 - /> 190 - )} 191 - renderScrollComponent={props => ( 192 - <ScrollView 193 - {...props} 194 - waitFor={Array.from(pinchGestureRefs.values())} 195 - /> 196 - )} 197 - onScrollBeginDrag={() => { 198 - setIsDragging(true) 199 - }} 200 - onScrollEndDrag={() => { 201 - setIsDragging(false) 202 - }} 203 - onMomentumScrollEnd={e => { 113 + <PagerView 114 + scrollEnabled={!isScaled} 115 + initialPage={initialImageIndex} 116 + onPageSelected={e => { 117 + setImageIndex(e.nativeEvent.position) 204 118 setIsScaled(false) 205 - onScroll(e) 206 119 }} 207 - keyExtractor={imageSrc => imageSrc.uri} 208 - /> 120 + onPageScrollStateChanged={e => { 121 + setIsDragging(e.nativeEvent.pageScrollState !== 'idle') 122 + }} 123 + overdrag={true} 124 + style={styles.pager}> 125 + {images.map(imageSrc => ( 126 + <View key={imageSrc.uri}> 127 + <ImageItem 128 + onZoom={onZoom} 129 + imageSrc={imageSrc} 130 + onRequestClose={onRequestClose} 131 + isScrollViewBeingDragged={isDragging} 132 + /> 133 + </View> 134 + ))} 135 + </PagerView> 209 136 {typeof FooterComponent !== 'undefined' && ( 210 137 <Animated.View style={[styles.footer, {transform: footerTransform}]}> 211 138 {React.createElement(FooterComponent, { ··· 221 148 const styles = StyleSheet.create({ 222 149 screen: { 223 150 position: 'absolute', 151 + top: 0, 152 + left: 0, 153 + bottom: 0, 154 + right: 0, 224 155 }, 225 156 container: { 226 157 flex: 1, 227 158 backgroundColor: '#000', 159 + }, 160 + pager: { 161 + flex: 1, 228 162 }, 229 163 header: { 230 164 position: 'absolute',