mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useCallback, useEffect, useState} from 'react'
2import {
3 Image,
4 ImageStyle,
5 TouchableOpacity,
6 TouchableWithoutFeedback,
7 StyleSheet,
8 View,
9 Pressable,
10 ViewStyle,
11} from 'react-native'
12import {
13 FontAwesomeIcon,
14 FontAwesomeIconStyle,
15} from '@fortawesome/react-native-fontawesome'
16import {colors, s} from 'lib/styles'
17import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
18import {Text} from '../util/text/Text'
19import {useLingui} from '@lingui/react'
20import {msg} from '@lingui/macro'
21import {
22 useLightbox,
23 useLightboxControls,
24 ImagesLightbox,
25 ProfileImageLightbox,
26} from '#/state/lightbox'
27import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
28import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
29
30interface Img {
31 uri: string
32 alt?: string
33}
34
35export function Lightbox() {
36 const {activeLightbox} = useLightbox()
37 const {closeLightbox} = useLightboxControls()
38 const isActive = !!activeLightbox
39 useWebBodyScrollLock(isActive)
40
41 if (!isActive) {
42 return null
43 }
44
45 const initialIndex =
46 activeLightbox instanceof ImagesLightbox ? activeLightbox.index : 0
47
48 let imgs: Img[] | undefined
49 if (activeLightbox instanceof ProfileImageLightbox) {
50 const opts = activeLightbox
51 if (opts.profile.avatar) {
52 imgs = [{uri: opts.profile.avatar}]
53 }
54 } else if (activeLightbox instanceof ImagesLightbox) {
55 const opts = activeLightbox
56 imgs = opts.images
57 }
58
59 if (!imgs) {
60 return null
61 }
62
63 return (
64 <LightboxInner
65 imgs={imgs}
66 initialIndex={initialIndex}
67 onClose={closeLightbox}
68 />
69 )
70}
71
72function LightboxInner({
73 imgs,
74 initialIndex = 0,
75 onClose,
76}: {
77 imgs: Img[]
78 initialIndex: number
79 onClose: () => void
80}) {
81 const {_} = useLingui()
82 const [index, setIndex] = useState<number>(initialIndex)
83 const [isAltExpanded, setAltExpanded] = useState(false)
84
85 const canGoLeft = index >= 1
86 const canGoRight = index < imgs.length - 1
87 const onPressLeft = useCallback(() => {
88 if (canGoLeft) {
89 setIndex(index - 1)
90 }
91 }, [index, canGoLeft])
92 const onPressRight = useCallback(() => {
93 if (canGoRight) {
94 setIndex(index + 1)
95 }
96 }, [index, canGoRight])
97
98 const onKeyDown = useCallback(
99 (e: KeyboardEvent) => {
100 if (e.key === 'Escape') {
101 onClose()
102 } else if (e.key === 'ArrowLeft') {
103 onPressLeft()
104 } else if (e.key === 'ArrowRight') {
105 onPressRight()
106 }
107 },
108 [onClose, onPressLeft, onPressRight],
109 )
110
111 useEffect(() => {
112 window.addEventListener('keydown', onKeyDown)
113 return () => window.removeEventListener('keydown', onKeyDown)
114 }, [onKeyDown])
115
116 const {isTabletOrDesktop} = useWebMediaQueries()
117 const btnStyle = React.useMemo(() => {
118 return isTabletOrDesktop ? styles.btnTablet : styles.btnMobile
119 }, [isTabletOrDesktop])
120 const iconSize = React.useMemo(() => {
121 return isTabletOrDesktop ? 32 : 24
122 }, [isTabletOrDesktop])
123
124 return (
125 <View style={styles.mask}>
126 <TouchableWithoutFeedback
127 onPress={onClose}
128 accessibilityRole="button"
129 accessibilityLabel={_(msg`Close image viewer`)}
130 accessibilityHint={_(msg`Exits image view`)}
131 onAccessibilityEscape={onClose}>
132 <View style={styles.imageCenterer}>
133 <Image
134 accessibilityIgnoresInvertColors
135 source={imgs[index]}
136 style={styles.image as ImageStyle}
137 accessibilityLabel={imgs[index].alt}
138 accessibilityHint=""
139 />
140 {canGoLeft && (
141 <TouchableOpacity
142 onPress={onPressLeft}
143 style={[
144 styles.btn,
145 btnStyle,
146 styles.leftBtn,
147 styles.blurredBackground,
148 ]}
149 accessibilityRole="button"
150 accessibilityLabel={_(msg`Previous image`)}
151 accessibilityHint="">
152 <FontAwesomeIcon
153 icon="angle-left"
154 style={styles.icon as FontAwesomeIconStyle}
155 size={iconSize}
156 />
157 </TouchableOpacity>
158 )}
159 {canGoRight && (
160 <TouchableOpacity
161 onPress={onPressRight}
162 style={[
163 styles.btn,
164 btnStyle,
165 styles.rightBtn,
166 styles.blurredBackground,
167 ]}
168 accessibilityRole="button"
169 accessibilityLabel={_(msg`Next image`)}
170 accessibilityHint="">
171 <FontAwesomeIcon
172 icon="angle-right"
173 style={styles.icon as FontAwesomeIconStyle}
174 size={iconSize}
175 />
176 </TouchableOpacity>
177 )}
178 </View>
179 </TouchableWithoutFeedback>
180 {imgs[index].alt ? (
181 <View style={styles.footer}>
182 <Pressable
183 accessibilityLabel={_(msg`Expand alt text`)}
184 accessibilityHint={_(
185 msg`If alt text is long, toggles alt text expanded state`,
186 )}
187 onPress={() => {
188 setAltExpanded(!isAltExpanded)
189 }}>
190 <Text
191 style={s.white}
192 numberOfLines={isAltExpanded ? 0 : 3}
193 ellipsizeMode="tail">
194 {imgs[index].alt}
195 </Text>
196 </Pressable>
197 </View>
198 ) : null}
199 <View style={styles.closeBtn}>
200 <ImageDefaultHeader onRequestClose={onClose} />
201 </View>
202 </View>
203 )
204}
205
206const styles = StyleSheet.create({
207 mask: {
208 // @ts-ignore
209 position: 'fixed',
210 top: 0,
211 left: 0,
212 width: '100%',
213 height: '100%',
214 backgroundColor: '#000c',
215 },
216 imageCenterer: {
217 flex: 1,
218 alignItems: 'center',
219 justifyContent: 'center',
220 },
221 image: {
222 width: '100%',
223 height: '100%',
224 resizeMode: 'contain',
225 },
226 icon: {
227 color: colors.white,
228 },
229 closeBtn: {
230 position: 'absolute',
231 top: 10,
232 right: 10,
233 },
234 btn: {
235 position: 'absolute',
236 backgroundColor: '#00000077',
237 justifyContent: 'center',
238 alignItems: 'center',
239 },
240 btnTablet: {
241 width: 50,
242 height: 50,
243 borderRadius: 25,
244 left: 30,
245 right: 30,
246 },
247 btnMobile: {
248 width: 44,
249 height: 44,
250 borderRadius: 22,
251 left: 20,
252 right: 20,
253 },
254 leftBtn: {
255 right: 'auto',
256 top: '50%',
257 },
258 rightBtn: {
259 left: 'auto',
260 top: '50%',
261 },
262 footer: {
263 paddingHorizontal: 32,
264 paddingVertical: 24,
265 backgroundColor: colors.black,
266 },
267 blurredBackground: {
268 backdropFilter: 'blur(10px)',
269 WebkitBackdropFilter: 'blur(10px)',
270 } as ViewStyle,
271})