Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Refactor lightbox prop drilling (#6073)

* Refactor lightbox footer to render prop

* Unify lightbox types

* Unindent

* Refactor LightboxFooter props

* Move LightboxFooter into the implementation file

authored by

dan and committed by
GitHub
26c48373 174988bc

+155 -214
+12 -2
src/screens/Profile/Header/Shell.tsx
··· 55 55 const modui = moderation.ui('avatar') 56 56 if (profile.avatar && !(modui.blur && modui.noOverride)) { 57 57 openLightbox({ 58 - type: 'profile-image', 59 - profile: profile, 58 + images: [ 59 + { 60 + uri: profile.avatar, 61 + thumbUri: profile.avatar, 62 + dimensions: { 63 + // It's fine if it's actually smaller but we know it's 1:1. 64 + height: 1000, 65 + width: 1000, 66 + }, 67 + }, 68 + ], 69 + index: 0, 60 70 thumbDims: null, 61 71 }) 62 72 }
+3 -20
src/state/lightbox.tsx
··· 1 1 import React from 'react' 2 2 import type {MeasuredDimensions} from 'react-native-reanimated' 3 - import {AppBskyActorDefs} from '@atproto/api' 4 3 5 4 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 6 - import {Dimensions} from '#/lib/media/types' 7 - 8 - type ProfileImageLightbox = { 9 - type: 'profile-image' 10 - profile: AppBskyActorDefs.ProfileViewDetailed 11 - thumbDims: null 12 - } 5 + import {ImageSource} from '#/view/com/lightbox/ImageViewing/@types' 13 6 14 - type ImagesLightboxItem = { 15 - uri: string 16 - thumbUri: string 17 - alt?: string 18 - dimensions: Dimensions | null 19 - } 20 - 21 - type ImagesLightbox = { 22 - type: 'images' 23 - images: ImagesLightboxItem[] 7 + type Lightbox = { 8 + images: ImageSource[] 24 9 thumbDims: MeasuredDimensions | null 25 10 index: number 26 11 } 27 - 28 - type Lightbox = ProfileImageLightbox | ImagesLightbox 29 12 30 13 const LightboxContext = React.createContext<{ 31 14 activeLightbox: Lightbox | null
+126 -20
src/view/com/lightbox/ImageViewing/index.tsx
··· 8 8 // Original code copied and simplified from the link below as the codebase is currently not maintained: 9 9 // https://github.com/jobtoday/react-native-image-viewing 10 10 11 - import React, {ComponentType, useCallback, useMemo, useState} from 'react' 12 - import {Platform, StyleSheet, View} from 'react-native' 11 + import React, {useCallback, useMemo, useState} from 'react' 12 + import { 13 + Dimensions, 14 + LayoutAnimation, 15 + Platform, 16 + StyleSheet, 17 + View, 18 + } from 'react-native' 13 19 import PagerView from 'react-native-pager-view' 14 20 import {MeasuredDimensions} from 'react-native-reanimated' 15 21 import Animated, {useAnimatedStyle, withSpring} from 'react-native-reanimated' 22 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 16 23 import {Edge, SafeAreaView} from 'react-native-safe-area-context' 24 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 25 + import {Trans} from '@lingui/macro' 17 26 27 + import {colors, s} from '#/lib/styles' 28 + import {isIOS} from '#/platform/detection' 29 + import {Button} from '#/view/com/util/forms/Button' 30 + import {Text} from '#/view/com/util/text/Text' 31 + import {ScrollView} from '#/view/com/util/Views' 18 32 import {ImageSource} from './@types' 19 33 import ImageDefaultHeader from './components/ImageDefaultHeader' 20 34 import ImageItem from './components/ImageItem/ImageItem' ··· 26 40 visible: boolean 27 41 onRequestClose: () => void 28 42 backgroundColor?: string 29 - HeaderComponent?: ComponentType<{imageIndex: number}> 30 - FooterComponent?: ComponentType<{imageIndex: number}> 43 + onPressSave: (uri: string) => void 44 + onPressShare: (uri: string) => void 31 45 } 32 46 47 + const SCREEN_HEIGHT = Dimensions.get('window').height 33 48 const DEFAULT_BG_COLOR = '#000' 34 49 35 50 function ImageViewing({ ··· 39 54 visible, 40 55 onRequestClose, 41 56 backgroundColor = DEFAULT_BG_COLOR, 42 - HeaderComponent, 43 - FooterComponent, 57 + onPressSave, 58 + onPressShare, 44 59 }: Props) { 45 60 const [isScaled, setIsScaled] = useState(false) 46 61 const [isDragging, setIsDragging] = useState(false) ··· 96 111 accessibilityViewIsModal> 97 112 <View style={[styles.container, {backgroundColor}]}> 98 113 <Animated.View style={[styles.header, animatedHeaderStyle]}> 99 - {typeof HeaderComponent !== 'undefined' ? ( 100 - React.createElement(HeaderComponent, { 101 - imageIndex, 102 - }) 103 - ) : ( 104 - <ImageDefaultHeader onRequestClose={onRequestClose} /> 105 - )} 114 + <ImageDefaultHeader onRequestClose={onRequestClose} /> 106 115 </Animated.View> 107 116 <PagerView 108 117 scrollEnabled={!isScaled} ··· 129 138 </View> 130 139 ))} 131 140 </PagerView> 132 - {typeof FooterComponent !== 'undefined' && ( 133 - <Animated.View style={[styles.footer, animatedFooterStyle]}> 134 - {React.createElement(FooterComponent, { 135 - imageIndex, 136 - })} 137 - </Animated.View> 138 - )} 141 + <Animated.View style={[styles.footer, animatedFooterStyle]}> 142 + <LightboxFooter 143 + images={images} 144 + index={imageIndex} 145 + onPressSave={onPressSave} 146 + onPressShare={onPressShare} 147 + /> 148 + </Animated.View> 139 149 </View> 140 150 </SafeAreaView> 141 151 ) 142 152 } 143 153 154 + function LightboxFooter({ 155 + images, 156 + index, 157 + onPressSave, 158 + onPressShare, 159 + }: { 160 + images: ImageSource[] 161 + index: number 162 + onPressSave: (uri: string) => void 163 + onPressShare: (uri: string) => void 164 + }) { 165 + const {alt: altText, uri} = images[index] 166 + const [isAltExpanded, setAltExpanded] = React.useState(false) 167 + const insets = useSafeAreaInsets() 168 + const svMaxHeight = SCREEN_HEIGHT - insets.top - 50 169 + const isMomentumScrolling = React.useRef(false) 170 + return ( 171 + <ScrollView 172 + style={[ 173 + { 174 + backgroundColor: '#000d', 175 + }, 176 + {maxHeight: svMaxHeight}, 177 + ]} 178 + scrollEnabled={isAltExpanded} 179 + onMomentumScrollBegin={() => { 180 + isMomentumScrolling.current = true 181 + }} 182 + onMomentumScrollEnd={() => { 183 + isMomentumScrolling.current = false 184 + }} 185 + contentContainerStyle={{ 186 + paddingTop: 16, 187 + paddingBottom: insets.bottom + 10, 188 + paddingHorizontal: 24, 189 + }}> 190 + {altText ? ( 191 + <View accessibilityRole="button" style={styles.footerText}> 192 + <Text 193 + style={[s.gray3]} 194 + numberOfLines={isAltExpanded ? undefined : 3} 195 + selectable 196 + onPress={() => { 197 + if (isMomentumScrolling.current) { 198 + return 199 + } 200 + LayoutAnimation.configureNext({ 201 + duration: 450, 202 + update: {type: 'spring', springDamping: 1}, 203 + }) 204 + setAltExpanded(prev => !prev) 205 + }} 206 + onLongPress={() => {}}> 207 + {altText} 208 + </Text> 209 + </View> 210 + ) : null} 211 + <View style={styles.footerBtns}> 212 + <Button 213 + type="primary-outline" 214 + style={styles.footerBtn} 215 + onPress={() => onPressSave(uri)}> 216 + <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> 217 + <Text type="xl" style={s.white}> 218 + <Trans context="action">Save</Trans> 219 + </Text> 220 + </Button> 221 + <Button 222 + type="primary-outline" 223 + style={styles.footerBtn} 224 + onPress={() => onPressShare(uri)}> 225 + <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> 226 + <Text type="xl" style={s.white}> 227 + <Trans context="action">Share</Trans> 228 + </Text> 229 + </Button> 230 + </View> 231 + </ScrollView> 232 + ) 233 + } 234 + 144 235 const styles = StyleSheet.create({ 145 236 screen: { 146 237 position: 'absolute', ··· 168 259 width: '100%', 169 260 zIndex: 1, 170 261 bottom: 0, 262 + }, 263 + footerText: { 264 + paddingBottom: isIOS ? 20 : 16, 265 + }, 266 + footerBtns: { 267 + flexDirection: 'row', 268 + justifyContent: 'center', 269 + gap: 8, 270 + }, 271 + footerBtn: { 272 + flexDirection: 'row', 273 + alignItems: 'center', 274 + gap: 8, 275 + backgroundColor: 'transparent', 276 + borderColor: colors.white, 171 277 }, 172 278 }) 173 279
+12 -152
src/view/com/lightbox/Lightbox.tsx
··· 1 1 import React from 'react' 2 - import {Dimensions, LayoutAnimation, StyleSheet, View} from 'react-native' 3 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 2 import * as MediaLibrary from 'expo-media-library' 5 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 6 - import {msg, Trans} from '@lingui/macro' 3 + import {msg} from '@lingui/macro' 7 4 import {useLingui} from '@lingui/react' 8 5 9 6 import {saveImageToMediaLibrary, shareImageModal} from '#/lib/media/manip' 10 - import {colors, s} from '#/lib/styles' 11 - import {isIOS} from '#/platform/detection' 12 7 import {useLightbox, useLightboxControls} from '#/state/lightbox' 13 - import {ScrollView} from '#/view/com/util/Views' 14 - import {Button} from '../util/forms/Button' 15 - import {Text} from '../util/text/Text' 16 8 import * as Toast from '../util/Toast' 17 9 import ImageView from './ImageViewing' 18 10 19 - const SCREEN_HEIGHT = Dimensions.get('window').height 20 - 21 11 export function Lightbox() { 22 12 const {activeLightbox} = useLightbox() 23 13 const {closeLightbox} = useLightboxControls() 14 + 24 15 const onClose = React.useCallback(() => { 25 16 closeLightbox() 26 17 }, [closeLightbox]) 27 18 28 - if (!activeLightbox) { 29 - return null 30 - } else if (activeLightbox.type === 'profile-image') { 31 - const opts = activeLightbox 32 - return ( 33 - <ImageView 34 - images={[ 35 - { 36 - uri: opts.profile.avatar || '', 37 - thumbUri: opts.profile.avatar || '', 38 - dimensions: { 39 - // It's fine if it's actually smaller but we know it's 1:1. 40 - height: 1000, 41 - width: 1000, 42 - }, 43 - }, 44 - ]} 45 - initialImageIndex={0} 46 - thumbDims={opts.thumbDims} 47 - visible 48 - onRequestClose={onClose} 49 - FooterComponent={LightboxFooter} 50 - /> 51 - ) 52 - } else if (activeLightbox.type === 'images') { 53 - const opts = activeLightbox 54 - return ( 55 - <ImageView 56 - images={opts.images.map(img => ({...img}))} 57 - initialImageIndex={opts.index} 58 - thumbDims={opts.thumbDims} 59 - visible 60 - onRequestClose={onClose} 61 - FooterComponent={LightboxFooter} 62 - /> 63 - ) 64 - } else { 65 - return null 66 - } 67 - } 68 - 69 - function LightboxFooter({imageIndex}: {imageIndex: number}) { 70 19 const {_} = useLingui() 71 - const {activeLightbox} = useLightbox() 72 - const [isAltExpanded, setAltExpanded] = React.useState(false) 73 20 const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({ 74 21 granularPermissions: ['photo'], 75 22 }) 76 - const insets = useSafeAreaInsets() 77 - const svMaxHeight = SCREEN_HEIGHT - insets.top - 50 78 - const isMomentumScrolling = React.useRef(false) 79 - 80 23 const saveImageToAlbumWithToasts = React.useCallback( 81 24 async (uri: string) => { 82 25 if (!permissionResponse || permissionResponse.granted === false) { ··· 96 39 } 97 40 return 98 41 } 99 - 100 42 try { 101 43 await saveImageToMediaLibrary({uri}) 102 44 Toast.show(_(msg`Saved to your camera roll`)) ··· 107 49 [permissionResponse, requestPermission, _], 108 50 ) 109 51 110 - const lightbox = activeLightbox 111 - if (!lightbox) { 52 + if (!activeLightbox) { 112 53 return null 113 54 } 114 55 115 - let altText = '' 116 - let uri = '' 117 - if (lightbox.type === 'images') { 118 - const opts = lightbox 119 - uri = opts.images[imageIndex].uri 120 - altText = opts.images[imageIndex].alt || '' 121 - } else if (lightbox.type === 'profile-image') { 122 - const opts = lightbox 123 - uri = opts.profile.avatar || '' 124 - } 125 - 126 56 return ( 127 - <ScrollView 128 - style={[ 129 - { 130 - backgroundColor: '#000d', 131 - }, 132 - {maxHeight: svMaxHeight}, 133 - ]} 134 - scrollEnabled={isAltExpanded} 135 - onMomentumScrollBegin={() => { 136 - isMomentumScrolling.current = true 137 - }} 138 - onMomentumScrollEnd={() => { 139 - isMomentumScrolling.current = false 140 - }} 141 - contentContainerStyle={{ 142 - paddingTop: 16, 143 - paddingBottom: insets.bottom + 10, 144 - paddingHorizontal: 24, 145 - }}> 146 - {altText ? ( 147 - <View accessibilityRole="button" style={styles.footerText}> 148 - <Text 149 - style={[s.gray3]} 150 - numberOfLines={isAltExpanded ? undefined : 3} 151 - selectable 152 - onPress={() => { 153 - if (isMomentumScrolling.current) { 154 - return 155 - } 156 - LayoutAnimation.configureNext({ 157 - duration: 450, 158 - update: {type: 'spring', springDamping: 1}, 159 - }) 160 - setAltExpanded(prev => !prev) 161 - }} 162 - onLongPress={() => {}}> 163 - {altText} 164 - </Text> 165 - </View> 166 - ) : null} 167 - <View style={styles.footerBtns}> 168 - <Button 169 - type="primary-outline" 170 - style={styles.footerBtn} 171 - onPress={() => saveImageToAlbumWithToasts(uri)}> 172 - <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> 173 - <Text type="xl" style={s.white}> 174 - <Trans context="action">Save</Trans> 175 - </Text> 176 - </Button> 177 - <Button 178 - type="primary-outline" 179 - style={styles.footerBtn} 180 - onPress={() => shareImageModal({uri})}> 181 - <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> 182 - <Text type="xl" style={s.white}> 183 - <Trans context="action">Share</Trans> 184 - </Text> 185 - </Button> 186 - </View> 187 - </ScrollView> 57 + <ImageView 58 + images={activeLightbox.images} 59 + initialImageIndex={activeLightbox.index} 60 + thumbDims={activeLightbox.thumbDims} 61 + visible 62 + onRequestClose={onClose} 63 + onPressSave={saveImageToAlbumWithToasts} 64 + onPressShare={uri => shareImageModal({uri})} 65 + /> 188 66 ) 189 67 } 190 - 191 - const styles = StyleSheet.create({ 192 - footerText: { 193 - paddingBottom: isIOS ? 20 : 16, 194 - }, 195 - footerBtns: { 196 - flexDirection: 'row', 197 - justifyContent: 'center', 198 - gap: 8, 199 - }, 200 - footerBtn: { 201 - flexDirection: 'row', 202 - alignItems: 'center', 203 - gap: 8, 204 - backgroundColor: 'transparent', 205 - borderColor: colors.white, 206 - }, 207 - })
+2 -18
src/view/com/lightbox/Lightbox.web.tsx
··· 38 38 return null 39 39 } 40 40 41 - const initialIndex = 42 - activeLightbox.type === 'images' ? activeLightbox.index : 0 43 - 44 - let imgs: Img[] | undefined 45 - if (activeLightbox.type === 'profile-image') { 46 - const opts = activeLightbox 47 - if (opts.profile.avatar) { 48 - imgs = [{uri: opts.profile.avatar}] 49 - } 50 - } else if (activeLightbox.type === 'images') { 51 - const opts = activeLightbox 52 - imgs = opts.images 53 - } 54 - 55 - if (!imgs) { 56 - return null 57 - } 58 - 41 + const initialIndex = activeLightbox.index 42 + const imgs = activeLightbox.images 59 43 return ( 60 44 <LightboxInner 61 45 imgs={imgs}
-1
src/view/com/profile/ProfileSubpageHeader.tsx
··· 71 71 avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) 72 72 ) { 73 73 openLightbox({ 74 - type: 'images', 75 74 images: [ 76 75 { 77 76 uri: avatar,
-1
src/view/com/util/post-embeds/index.tsx
··· 152 152 thumbDims: MeasuredDimensions | null, 153 153 ) => { 154 154 openLightbox({ 155 - type: 'images', 156 155 images: items, 157 156 index, 158 157 thumbDims,