From 26c48373e29643ca7a3248315506131d53d05b8b Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 4 Nov 2024 21:31:21 +0000 Subject: [PATCH] Refactor lightbox prop drilling (#6073) * Refactor lightbox footer to render prop * Unify lightbox types * Unindent * Refactor LightboxFooter props * Move LightboxFooter into the implementation file --- src/screens/Profile/Header/Shell.tsx | 14 +- src/state/lightbox.tsx | 23 +-- src/view/com/lightbox/ImageViewing/index.tsx | 146 +++++++++++++--- src/view/com/lightbox/Lightbox.tsx | 164 ++---------------- src/view/com/lightbox/Lightbox.web.tsx | 20 +-- src/view/com/profile/ProfileSubpageHeader.tsx | 1 - src/view/com/util/post-embeds/index.tsx | 1 - 7 files changed, 155 insertions(+), 214 deletions(-) diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 26e9406883..925066d72e 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -55,8 +55,18 @@ let ProfileHeaderShell = ({ const modui = moderation.ui('avatar') if (profile.avatar && !(modui.blur && modui.noOverride)) { openLightbox({ - type: 'profile-image', - profile: profile, + images: [ + { + uri: profile.avatar, + thumbUri: profile.avatar, + dimensions: { + // It's fine if it's actually smaller but we know it's 1:1. + height: 1000, + width: 1000, + }, + }, + ], + index: 0, thumbDims: null, }) } diff --git a/src/state/lightbox.tsx b/src/state/lightbox.tsx index 53409e3ecf..1dae67932c 100644 --- a/src/state/lightbox.tsx +++ b/src/state/lightbox.tsx @@ -1,32 +1,15 @@ import React from 'react' import type {MeasuredDimensions} from 'react-native-reanimated' -import {AppBskyActorDefs} from '@atproto/api' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {Dimensions} from '#/lib/media/types' +import {ImageSource} from '#/view/com/lightbox/ImageViewing/@types' -type ProfileImageLightbox = { - type: 'profile-image' - profile: AppBskyActorDefs.ProfileViewDetailed - thumbDims: null -} - -type ImagesLightboxItem = { - uri: string - thumbUri: string - alt?: string - dimensions: Dimensions | null -} - -type ImagesLightbox = { - type: 'images' - images: ImagesLightboxItem[] +type Lightbox = { + images: ImageSource[] thumbDims: MeasuredDimensions | null index: number } -type Lightbox = ProfileImageLightbox | ImagesLightbox - const LightboxContext = React.createContext<{ activeLightbox: Lightbox | null }>({ diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index 0d0ac4df1f..40df4c8199 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -8,13 +8,27 @@ // Original code copied and simplified from the link below as the codebase is currently not maintained: // https://github.com/jobtoday/react-native-image-viewing -import React, {ComponentType, useCallback, useMemo, useState} from 'react' -import {Platform, StyleSheet, View} from 'react-native' +import React, {useCallback, useMemo, useState} from 'react' +import { + Dimensions, + LayoutAnimation, + Platform, + StyleSheet, + View, +} from 'react-native' import PagerView from 'react-native-pager-view' import {MeasuredDimensions} from 'react-native-reanimated' import Animated, {useAnimatedStyle, withSpring} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Edge, SafeAreaView} from 'react-native-safe-area-context' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Trans} from '@lingui/macro' +import {colors, s} from '#/lib/styles' +import {isIOS} from '#/platform/detection' +import {Button} from '#/view/com/util/forms/Button' +import {Text} from '#/view/com/util/text/Text' +import {ScrollView} from '#/view/com/util/Views' import {ImageSource} from './@types' import ImageDefaultHeader from './components/ImageDefaultHeader' import ImageItem from './components/ImageItem/ImageItem' @@ -26,10 +40,11 @@ type Props = { visible: boolean onRequestClose: () => void backgroundColor?: string - HeaderComponent?: ComponentType<{imageIndex: number}> - FooterComponent?: ComponentType<{imageIndex: number}> + onPressSave: (uri: string) => void + onPressShare: (uri: string) => void } +const SCREEN_HEIGHT = Dimensions.get('window').height const DEFAULT_BG_COLOR = '#000' function ImageViewing({ @@ -39,8 +54,8 @@ function ImageViewing({ visible, onRequestClose, backgroundColor = DEFAULT_BG_COLOR, - HeaderComponent, - FooterComponent, + onPressSave, + onPressShare, }: Props) { const [isScaled, setIsScaled] = useState(false) const [isDragging, setIsDragging] = useState(false) @@ -96,13 +111,7 @@ function ImageViewing({ accessibilityViewIsModal> - {typeof HeaderComponent !== 'undefined' ? ( - React.createElement(HeaderComponent, { - imageIndex, - }) - ) : ( - - )} + ))} - {typeof FooterComponent !== 'undefined' && ( - - {React.createElement(FooterComponent, { - imageIndex, - })} - - )} + + + ) } +function LightboxFooter({ + images, + index, + onPressSave, + onPressShare, +}: { + images: ImageSource[] + index: number + onPressSave: (uri: string) => void + onPressShare: (uri: string) => void +}) { + const {alt: altText, uri} = images[index] + const [isAltExpanded, setAltExpanded] = React.useState(false) + const insets = useSafeAreaInsets() + const svMaxHeight = SCREEN_HEIGHT - insets.top - 50 + const isMomentumScrolling = React.useRef(false) + return ( + { + isMomentumScrolling.current = true + }} + onMomentumScrollEnd={() => { + isMomentumScrolling.current = false + }} + contentContainerStyle={{ + paddingTop: 16, + paddingBottom: insets.bottom + 10, + paddingHorizontal: 24, + }}> + {altText ? ( + + { + if (isMomentumScrolling.current) { + return + } + LayoutAnimation.configureNext({ + duration: 450, + update: {type: 'spring', springDamping: 1}, + }) + setAltExpanded(prev => !prev) + }} + onLongPress={() => {}}> + {altText} + + + ) : null} + + + + + + ) +} + const styles = StyleSheet.create({ screen: { position: 'absolute', @@ -169,6 +260,21 @@ const styles = StyleSheet.create({ zIndex: 1, bottom: 0, }, + footerText: { + paddingBottom: isIOS ? 20 : 16, + }, + footerBtns: { + flexDirection: 'row', + justifyContent: 'center', + gap: 8, + }, + footerBtn: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: 'transparent', + borderColor: colors.white, + }, }) const EnhancedImageViewing = (props: Props) => ( diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 83ea2e941b..ed570d5a79 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -1,82 +1,25 @@ import React from 'react' -import {Dimensions, LayoutAnimation, StyleSheet, View} from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import * as MediaLibrary from 'expo-media-library' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' +import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {saveImageToMediaLibrary, shareImageModal} from '#/lib/media/manip' -import {colors, s} from '#/lib/styles' -import {isIOS} from '#/platform/detection' import {useLightbox, useLightboxControls} from '#/state/lightbox' -import {ScrollView} from '#/view/com/util/Views' -import {Button} from '../util/forms/Button' -import {Text} from '../util/text/Text' import * as Toast from '../util/Toast' import ImageView from './ImageViewing' -const SCREEN_HEIGHT = Dimensions.get('window').height - export function Lightbox() { const {activeLightbox} = useLightbox() const {closeLightbox} = useLightboxControls() + const onClose = React.useCallback(() => { closeLightbox() }, [closeLightbox]) - if (!activeLightbox) { - return null - } else if (activeLightbox.type === 'profile-image') { - const opts = activeLightbox - return ( - - ) - } else if (activeLightbox.type === 'images') { - const opts = activeLightbox - return ( - ({...img}))} - initialImageIndex={opts.index} - thumbDims={opts.thumbDims} - visible - onRequestClose={onClose} - FooterComponent={LightboxFooter} - /> - ) - } else { - return null - } -} - -function LightboxFooter({imageIndex}: {imageIndex: number}) { const {_} = useLingui() - const {activeLightbox} = useLightbox() - const [isAltExpanded, setAltExpanded] = React.useState(false) const [permissionResponse, requestPermission] = MediaLibrary.usePermissions({ granularPermissions: ['photo'], }) - const insets = useSafeAreaInsets() - const svMaxHeight = SCREEN_HEIGHT - insets.top - 50 - const isMomentumScrolling = React.useRef(false) - const saveImageToAlbumWithToasts = React.useCallback( async (uri: string) => { if (!permissionResponse || permissionResponse.granted === false) { @@ -96,7 +39,6 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) { } return } - try { await saveImageToMediaLibrary({uri}) Toast.show(_(msg`Saved to your camera roll`)) @@ -107,101 +49,19 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) { [permissionResponse, requestPermission, _], ) - const lightbox = activeLightbox - if (!lightbox) { + if (!activeLightbox) { return null } - let altText = '' - let uri = '' - if (lightbox.type === 'images') { - const opts = lightbox - uri = opts.images[imageIndex].uri - altText = opts.images[imageIndex].alt || '' - } else if (lightbox.type === 'profile-image') { - const opts = lightbox - uri = opts.profile.avatar || '' - } - return ( - { - isMomentumScrolling.current = true - }} - onMomentumScrollEnd={() => { - isMomentumScrolling.current = false - }} - contentContainerStyle={{ - paddingTop: 16, - paddingBottom: insets.bottom + 10, - paddingHorizontal: 24, - }}> - {altText ? ( - - { - if (isMomentumScrolling.current) { - return - } - LayoutAnimation.configureNext({ - duration: 450, - update: {type: 'spring', springDamping: 1}, - }) - setAltExpanded(prev => !prev) - }} - onLongPress={() => {}}> - {altText} - - - ) : null} - - - - - + shareImageModal({uri})} + /> ) } - -const styles = StyleSheet.create({ - footerText: { - paddingBottom: isIOS ? 20 : 16, - }, - footerBtns: { - flexDirection: 'row', - justifyContent: 'center', - gap: 8, - }, - footerBtn: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - backgroundColor: 'transparent', - borderColor: colors.white, - }, -}) diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index 9a0fa5d331..2ba7d06cc5 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -38,24 +38,8 @@ export function Lightbox() { return null } - const initialIndex = - activeLightbox.type === 'images' ? activeLightbox.index : 0 - - let imgs: Img[] | undefined - if (activeLightbox.type === 'profile-image') { - const opts = activeLightbox - if (opts.profile.avatar) { - imgs = [{uri: opts.profile.avatar}] - } - } else if (activeLightbox.type === 'images') { - const opts = activeLightbox - imgs = opts.images - } - - if (!imgs) { - return null - } - + const initialIndex = activeLightbox.index + const imgs = activeLightbox.images return ( { openLightbox({ - type: 'images', images: items, index, thumbDims,