fork
Configure Feed
Select the types of activity you want to include in your feed.
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.
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)