mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
fork

Configure Feed

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

at thread-prepare-4 267 lines 7.7 kB view raw
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 9import React, {useState} from 'react' 10import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native' 11import {Gesture, GestureDetector} from 'react-native-gesture-handler' 12import Animated, { 13 interpolate, 14 runOnJS, 15 useAnimatedRef, 16 useAnimatedStyle, 17 useSharedValue, 18} from 'react-native-reanimated' 19import {Image} from 'expo-image' 20 21import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 22import {Dimensions as ImageDimensions, ImageSource} from '../../@types' 23import useImageDimensions from '../../hooks/useImageDimensions' 24 25const SWIPE_CLOSE_OFFSET = 75 26const SWIPE_CLOSE_VELOCITY = 1 27const SCREEN = Dimensions.get('screen') 28const MAX_ORIGINAL_IMAGE_ZOOM = 2 29const MIN_DOUBLE_TAP_SCALE = 2 30 31type Props = { 32 imageSrc: ImageSource 33 onRequestClose: () => void 34 onTap: () => void 35 onZoom: (scaled: boolean) => void 36 isScrollViewBeingDragged: boolean 37 showControls: boolean 38} 39 40const ImageItem = ({ 41 imageSrc, 42 onTap, 43 onZoom, 44 onRequestClose, 45 showControls, 46}: Props) => { 47 const scrollViewRef = useAnimatedRef<Animated.ScrollView>() 48 const translationY = useSharedValue(0) 49 const [scaled, setScaled] = useState(false) 50 const imageDimensions = useImageDimensions(imageSrc) 51 const maxZoomScale = imageDimensions 52 ? (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM 53 : 1 54 55 const animatedStyle = useAnimatedStyle(() => { 56 return { 57 opacity: interpolate( 58 translationY.value, 59 [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], 60 [0.5, 1, 0.5], 61 ), 62 } 63 }) 64 65 const scrollHandler = useAnimatedScrollHandler({ 66 onScroll(e) { 67 const nextIsScaled = e.zoomScale > 1 68 translationY.value = nextIsScaled ? 0 : e.contentOffset.y 69 if (scaled !== nextIsScaled) { 70 runOnJS(handleZoom)(nextIsScaled) 71 } 72 }, 73 onEndDrag(e) { 74 const velocityY = e.velocity?.y ?? 0 75 const nextIsScaled = e.zoomScale > 1 76 if (scaled !== nextIsScaled) { 77 runOnJS(handleZoom)(nextIsScaled) 78 } 79 if (!nextIsScaled && Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) { 80 runOnJS(onRequestClose)() 81 } 82 }, 83 }) 84 85 function handleZoom(nextIsScaled: boolean) { 86 onZoom(nextIsScaled) 87 setScaled(nextIsScaled) 88 } 89 90 function handleDoubleTap(absoluteX: number, absoluteY: number) { 91 const scrollResponderRef = scrollViewRef?.current?.getScrollResponder() 92 let nextZoomRect = { 93 x: 0, 94 y: 0, 95 width: SCREEN.width, 96 height: SCREEN.height, 97 } 98 99 const willZoom = !scaled 100 if (willZoom) { 101 nextZoomRect = getZoomRectAfterDoubleTap( 102 imageDimensions, 103 absoluteX, 104 absoluteY, 105 ) 106 } 107 108 // @ts-ignore 109 scrollResponderRef?.scrollResponderZoomTo({ 110 ...nextZoomRect, // This rect is in screen coordinates 111 animated: true, 112 }) 113 } 114 115 const singleTap = Gesture.Tap().onEnd(() => { 116 runOnJS(onTap)() 117 }) 118 119 const doubleTap = Gesture.Tap() 120 .numberOfTaps(2) 121 .onEnd(e => { 122 const {absoluteX, absoluteY} = e 123 runOnJS(handleDoubleTap)(absoluteX, absoluteY) 124 }) 125 126 const composedGesture = Gesture.Exclusive(doubleTap, singleTap) 127 128 return ( 129 <GestureDetector gesture={composedGesture}> 130 <Animated.ScrollView 131 // @ts-ignore Something's up with the types here 132 ref={scrollViewRef} 133 style={styles.listItem} 134 pinchGestureEnabled 135 showsHorizontalScrollIndicator={false} 136 showsVerticalScrollIndicator={false} 137 maximumZoomScale={maxZoomScale} 138 onScroll={scrollHandler}> 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> 153 </Animated.ScrollView> 154 </GestureDetector> 155 ) 156} 157 158const styles = StyleSheet.create({ 159 imageScrollContainer: { 160 height: SCREEN.height, 161 }, 162 listItem: { 163 width: SCREEN.width, 164 height: SCREEN.height, 165 }, 166 image: { 167 width: SCREEN.width, 168 height: SCREEN.height, 169 }, 170 loading: { 171 position: 'absolute', 172 top: 0, 173 left: 0, 174 right: 0, 175 bottom: 0, 176 }, 177}) 178 179const getZoomRectAfterDoubleTap = ( 180 imageDimensions: ImageDimensions | null, 181 touchX: number, 182 touchY: number, 183): { 184 x: number 185 y: number 186 width: number 187 height: number 188} => { 189 if (!imageDimensions) { 190 return { 191 x: 0, 192 y: 0, 193 width: SCREEN.width, 194 height: SCREEN.height, 195 } 196 } 197 198 // First, let's figure out how much we want to zoom in. 199 // We want to try to zoom in at least close enough to get rid of black bars. 200 const imageAspect = imageDimensions.width / imageDimensions.height 201 const screenAspect = SCREEN.width / SCREEN.height 202 const zoom = Math.max( 203 imageAspect / screenAspect, 204 screenAspect / imageAspect, 205 MIN_DOUBLE_TAP_SCALE, 206 ) 207 // Unlike in the Android version, we don't constrain the *max* zoom level here. 208 // Instead, this is done in the ScrollView props so that it constraints pinch too. 209 210 // Next, we'll be calculating the rectangle to "zoom into" in screen coordinates. 211 // We already know the zoom level, so this gives us the rectangle size. 212 let rectWidth = SCREEN.width / zoom 213 let rectHeight = SCREEN.height / zoom 214 215 // Before we settle on the zoomed rect, figure out the safe area it has to be inside. 216 // We don't want to introduce new black bars or make existing black bars unbalanced. 217 let minX = 0 218 let minY = 0 219 let maxX = SCREEN.width - rectWidth 220 let maxY = SCREEN.height - rectHeight 221 if (imageAspect >= screenAspect) { 222 // The image has horizontal black bars. Exclude them from the safe area. 223 const renderedHeight = SCREEN.width / imageAspect 224 const horizontalBarHeight = (SCREEN.height - renderedHeight) / 2 225 minY += horizontalBarHeight 226 maxY -= horizontalBarHeight 227 } else { 228 // The image has vertical black bars. Exclude them from the safe area. 229 const renderedWidth = SCREEN.height * imageAspect 230 const verticalBarWidth = (SCREEN.width - renderedWidth) / 2 231 minX += verticalBarWidth 232 maxX -= verticalBarWidth 233 } 234 235 // Finally, we can position the rect according to its size and the safe area. 236 let rectX 237 if (maxX >= minX) { 238 // Content fills the screen horizontally so we have horizontal wiggle room. 239 // Try to keep the tapped point under the finger after zoom. 240 rectX = touchX - touchX / zoom 241 rectX = Math.min(rectX, maxX) 242 rectX = Math.max(rectX, minX) 243 } else { 244 // Keep the rect centered on the screen so that black bars are balanced. 245 rectX = SCREEN.width / 2 - rectWidth / 2 246 } 247 let rectY 248 if (maxY >= minY) { 249 // Content fills the screen vertically so we have vertical wiggle room. 250 // Try to keep the tapped point under the finger after zoom. 251 rectY = touchY - touchY / zoom 252 rectY = Math.min(rectY, maxY) 253 rectY = Math.max(rectY, minY) 254 } else { 255 // Keep the rect centered on the screen so that black bars are balanced. 256 rectY = SCREEN.height / 2 - rectHeight / 2 257 } 258 259 return { 260 x: rectX, 261 y: rectY, 262 height: rectHeight, 263 width: rectWidth, 264 } 265} 266 267export default React.memo(ImageItem)