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