/** * Copyright (c) JOB TODAY S.A. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ // 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, {useCallback, useState} from 'react' import {LayoutAnimation, Platform, StyleSheet, View} from 'react-native' import PagerView from 'react-native-pager-view' import Animated, { AnimatedRef, useAnimatedRef, useAnimatedStyle, withSpring, } from 'react-native-reanimated' 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 {Lightbox} from '#/state/lightbox' 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' const EDGES = Platform.OS === 'android' ? (['top', 'bottom', 'left', 'right'] satisfies Edge[]) : (['left', 'right'] satisfies Edge[]) // iOS, so no top/bottom safe area export default function ImageViewRoot({ lightbox, onRequestClose, onPressSave, onPressShare, }: { lightbox: Lightbox | null onRequestClose: () => void onPressSave: (uri: string) => void onPressShare: (uri: string) => void }) { const ref = useAnimatedRef() return ( // Keep it always mounted to avoid flicker on the first frame. {lightbox && ( )} ) } function ImageView({ lightbox, onRequestClose, onPressSave, onPressShare, safeAreaRef, }: { lightbox: Lightbox onRequestClose: () => void onPressSave: (uri: string) => void onPressShare: (uri: string) => void safeAreaRef: AnimatedRef }) { const {images, index: initialImageIndex} = lightbox const [isScaled, setIsScaled] = useState(false) const [isDragging, setIsDragging] = useState(false) const [imageIndex, setImageIndex] = useState(initialImageIndex) const [showControls, setShowControls] = useState(true) const animatedHeaderStyle = useAnimatedStyle(() => ({ pointerEvents: showControls ? 'box-none' : 'none', opacity: withClampedSpring(showControls ? 1 : 0), transform: [ { translateY: withClampedSpring(showControls ? 0 : -30), }, ], })) const animatedFooterStyle = useAnimatedStyle(() => ({ flexGrow: 1, pointerEvents: showControls ? 'box-none' : 'none', opacity: withClampedSpring(showControls ? 1 : 0), transform: [ { translateY: withClampedSpring(showControls ? 0 : 30), }, ], })) const onTap = useCallback(() => { setShowControls(show => !show) }, []) const onZoom = useCallback((nextIsScaled: boolean) => { setIsScaled(nextIsScaled) if (nextIsScaled) { setShowControls(false) } }, []) return ( { setImageIndex(e.nativeEvent.position) setIsScaled(false) }} onPageScrollStateChanged={e => { setIsDragging(e.nativeEvent.pageScrollState !== 'idle') }} overdrag={true} style={styles.pager}> {images.map(imageSrc => ( ))} ) } 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 isMomentumScrolling = React.useRef(false) return ( { isMomentumScrolling.current = true }} onMomentumScrollEnd={() => { isMomentumScrolling.current = false }} contentContainerStyle={{ paddingVertical: 12, 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', top: 0, left: 0, bottom: 0, right: 0, }, screenHidden: { opacity: 0, pointerEvents: 'none', }, container: { flex: 1, backgroundColor: '#000', }, controls: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, gap: 20, zIndex: 1, pointerEvents: 'box-none', }, pager: { flex: 1, }, header: { position: 'absolute', width: '100%', top: 0, pointerEvents: 'box-none', }, footer: { position: 'absolute', width: '100%', maxHeight: '100%', bottom: 0, }, footerScrollView: { backgroundColor: '#000d', flex: 1, position: 'absolute', bottom: 0, width: '100%', maxHeight: '100%', }, footerText: { paddingBottom: isIOS ? 20 : 16, }, footerBtns: { flexDirection: 'row', justifyContent: 'center', gap: 8, }, footerBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: 'transparent', borderColor: colors.white, }, }) function withClampedSpring(value: any) { 'worklet' return withSpring(value, {overshootClamping: true, stiffness: 300}) }