diff options
-rw-r--r-- | src/screens/Profile/Header/Shell.tsx | 14 | ||||
-rw-r--r-- | src/state/lightbox.tsx | 23 | ||||
-rw-r--r-- | src/view/com/lightbox/ImageViewing/index.tsx | 146 | ||||
-rw-r--r-- | src/view/com/lightbox/Lightbox.tsx | 164 | ||||
-rw-r--r-- | src/view/com/lightbox/Lightbox.web.tsx | 20 | ||||
-rw-r--r-- | src/view/com/profile/ProfileSubpageHeader.tsx | 1 | ||||
-rw-r--r-- | 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 26e940688..925066d72 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 53409e3ec..1dae67932 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 0d0ac4df1..40df4c819 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> <View style={[styles.container, {backgroundColor}]}> <Animated.View style={[styles.header, animatedHeaderStyle]}> - {typeof HeaderComponent !== 'undefined' ? ( - React.createElement(HeaderComponent, { - imageIndex, - }) - ) : ( - <ImageDefaultHeader onRequestClose={onRequestClose} /> - )} + <ImageDefaultHeader onRequestClose={onRequestClose} /> </Animated.View> <PagerView scrollEnabled={!isScaled} @@ -129,18 +138,100 @@ function ImageViewing({ </View> ))} </PagerView> - {typeof FooterComponent !== 'undefined' && ( - <Animated.View style={[styles.footer, animatedFooterStyle]}> - {React.createElement(FooterComponent, { - imageIndex, - })} - </Animated.View> - )} + <Animated.View style={[styles.footer, animatedFooterStyle]}> + <LightboxFooter + images={images} + index={imageIndex} + onPressSave={onPressSave} + onPressShare={onPressShare} + /> + </Animated.View> </View> </SafeAreaView> ) } +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 ( + <ScrollView + style={[ + { + backgroundColor: '#000d', + }, + {maxHeight: svMaxHeight}, + ]} + scrollEnabled={isAltExpanded} + onMomentumScrollBegin={() => { + isMomentumScrolling.current = true + }} + onMomentumScrollEnd={() => { + isMomentumScrolling.current = false + }} + contentContainerStyle={{ + paddingTop: 16, + paddingBottom: insets.bottom + 10, + paddingHorizontal: 24, + }}> + {altText ? ( + <View accessibilityRole="button" style={styles.footerText}> + <Text + style={[s.gray3]} + numberOfLines={isAltExpanded ? undefined : 3} + selectable + onPress={() => { + if (isMomentumScrolling.current) { + return + } + LayoutAnimation.configureNext({ + duration: 450, + update: {type: 'spring', springDamping: 1}, + }) + setAltExpanded(prev => !prev) + }} + onLongPress={() => {}}> + {altText} + </Text> + </View> + ) : null} + <View style={styles.footerBtns}> + <Button + type="primary-outline" + style={styles.footerBtn} + onPress={() => onPressSave(uri)}> + <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> + <Text type="xl" style={s.white}> + <Trans context="action">Save</Trans> + </Text> + </Button> + <Button + type="primary-outline" + style={styles.footerBtn} + onPress={() => onPressShare(uri)}> + <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> + <Text type="xl" style={s.white}> + <Trans context="action">Share</Trans> + </Text> + </Button> + </View> + </ScrollView> + ) +} + 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 83ea2e941..ed570d5a7 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 ( - <ImageView - images={[ - { - uri: opts.profile.avatar || '', - thumbUri: opts.profile.avatar || '', - dimensions: { - // It's fine if it's actually smaller but we know it's 1:1. - height: 1000, - width: 1000, - }, - }, - ]} - initialImageIndex={0} - thumbDims={opts.thumbDims} - visible - onRequestClose={onClose} - FooterComponent={LightboxFooter} - /> - ) - } else if (activeLightbox.type === 'images') { - const opts = activeLightbox - return ( - <ImageView - images={opts.images.map(img => ({...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 ( - <ScrollView - style={[ - { - backgroundColor: '#000d', - }, - {maxHeight: svMaxHeight}, - ]} - scrollEnabled={isAltExpanded} - onMomentumScrollBegin={() => { - isMomentumScrolling.current = true - }} - onMomentumScrollEnd={() => { - isMomentumScrolling.current = false - }} - contentContainerStyle={{ - paddingTop: 16, - paddingBottom: insets.bottom + 10, - paddingHorizontal: 24, - }}> - {altText ? ( - <View accessibilityRole="button" style={styles.footerText}> - <Text - style={[s.gray3]} - numberOfLines={isAltExpanded ? undefined : 3} - selectable - onPress={() => { - if (isMomentumScrolling.current) { - return - } - LayoutAnimation.configureNext({ - duration: 450, - update: {type: 'spring', springDamping: 1}, - }) - setAltExpanded(prev => !prev) - }} - onLongPress={() => {}}> - {altText} - </Text> - </View> - ) : null} - <View style={styles.footerBtns}> - <Button - type="primary-outline" - style={styles.footerBtn} - onPress={() => saveImageToAlbumWithToasts(uri)}> - <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> - <Text type="xl" style={s.white}> - <Trans context="action">Save</Trans> - </Text> - </Button> - <Button - type="primary-outline" - style={styles.footerBtn} - onPress={() => shareImageModal({uri})}> - <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> - <Text type="xl" style={s.white}> - <Trans context="action">Share</Trans> - </Text> - </Button> - </View> - </ScrollView> + <ImageView + images={activeLightbox.images} + initialImageIndex={activeLightbox.index} + thumbDims={activeLightbox.thumbDims} + visible + onRequestClose={onClose} + onPressSave={saveImageToAlbumWithToasts} + onPressShare={uri => 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 9a0fa5d33..2ba7d06cc 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 ( <LightboxInner imgs={imgs} diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index d5b7c2b68..b712b346b 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -71,7 +71,6 @@ export function ProfileSubpageHeader({ avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) ) { openLightbox({ - type: 'images', images: [ { uri: avatar, diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 13e4d12e0..d686d2bd3 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -152,7 +152,6 @@ export function PostEmbeds({ thumbDims: MeasuredDimensions | null, ) => { openLightbox({ - type: 'images', images: items, index, thumbDims, |