Bluesky app fork with some witchin' additions 馃挮
at main 286 lines 7.4 kB view raw
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})