feat(lightbox): replace native chrome with new header/footer

Wire Header and Footer from src/features/lightbox/chrome/ into ImagePager.tsx,
removing the old ImageDefaultHeader and LightboxFooter inline function, and
deleting the now-empty src/view/com/lightbox/ tree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
vineyardbovines
2026-04-21 12:44:05 -04:00
parent 0e5717b742
commit acdb94be30
2 changed files with 17 additions and 199 deletions
+17 -138
View File
@@ -7,14 +7,8 @@
*/
// 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 {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {
LayoutAnimation,
PixelRatio,
StyleSheet,
useWindowDimensions,
View,
} from 'react-native'
import {useCallback, useEffect, useMemo, useState} from 'react'
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
import {SystemBars} from 'react-native-edge-to-edge'
import {Gesture} from 'react-native-gesture-handler'
import PagerView from 'react-native-pager-view'
@@ -37,22 +31,16 @@ import Animated, {
withSpring,
type WithSpringConfig,
} from 'react-native-reanimated'
import {SafeAreaView} from 'react-native-safe-area-context'
import * as ScreenOrientation from 'expo-screen-orientation'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans} from '@lingui/react/macro'
import {type Dimensions} from '#/lib/media/types'
import {colors, s} from '#/lib/styles'
import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views'
import {useTheme} from '#/alf'
import {setSystemUITheme} from '#/alf/util/systemUI'
import {IS_IOS} from '#/env'
import {type Lightbox} from '#/features/lightbox/state'
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
import ImageDefaultHeader from '../../../view/com/lightbox/ImageViewing/components/ImageDefaultHeader'
import {Footer} from '../chrome/Footer'
import {Header} from '../chrome/Header'
import {
type ImageSource,
type LightboxTransforms,
@@ -413,22 +401,27 @@ function ImageView({
</View>
))}
</PagerView>
<View style={styles.controls}>
<View style={styles.controls} pointerEvents="box-none">
<Animated.View
style={animatedHeaderStyle}
pointerEvents="box-none"
renderToHardwareTextureAndroid>
<ImageDefaultHeader onRequestClose={handleRequestClose} />
<Header
onRequestClose={handleRequestClose}
onPressShare={() => onPressShare(images[imageIndex].uri)}
onPressSave={() => onPressSave(images[imageIndex].uri)}
/>
</Animated.View>
<Animated.View
style={animatedFooterStyle}
pointerEvents="box-none"
renderToHardwareTextureAndroid={!isAltExpanded}>
<LightboxFooter
images={images}
index={imageIndex}
<Footer
altText={images[imageIndex].alt}
isAltExpanded={isAltExpanded}
toggleAltExpanded={() => setIsAltExpanded(e => !e)}
onPressSave={onPressSave}
onPressShare={onPressShare}
onToggleAltExpanded={() => setIsAltExpanded(e => !e)}
imageCount={images.length}
activeIndex={imageIndex}
/>
</Animated.View>
</View>
@@ -607,85 +600,6 @@ function LightboxImage({
)
}
function LightboxFooter({
images,
index,
isAltExpanded,
toggleAltExpanded,
onPressSave,
onPressShare,
}: {
images: ImageSource[]
index: number
isAltExpanded: boolean
toggleAltExpanded: () => void
onPressSave: (uri: string) => void
onPressShare: (uri: string) => void
}) {
const {alt: altText, uri} = images[index]
const isMomentumScrolling = useRef(false)
return (
<ScrollView
style={styles.footerScrollView}
scrollEnabled={isAltExpanded}
onMomentumScrollBegin={() => {
isMomentumScrolling.current = true
}}
onMomentumScrollEnd={() => {
isMomentumScrolling.current = false
}}
contentContainerStyle={{
paddingVertical: 12,
paddingHorizontal: 24,
}}>
<SafeAreaView edges={['bottom']}>
{altText ? (
<View accessibilityRole="button" style={styles.footerText}>
<Text
style={{color: colors.gray3}}
numberOfLines={isAltExpanded ? undefined : 3}
selectable
onPress={() => {
if (isMomentumScrolling.current) {
return
}
LayoutAnimation.configureNext({
duration: 450,
update: {type: 'spring', springDamping: 1},
})
toggleAltExpanded()
}}
onLongPress={() => {}}
emoji>
{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>
</SafeAreaView>
</ScrollView>
)
}
const styles = StyleSheet.create({
screen: {
position: 'absolute',
@@ -722,41 +636,6 @@ const styles = StyleSheet.create({
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: IS_IOS ? 20 : 16,
},
footerBtns: {
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
},
footerBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
backgroundColor: 'transparent',
borderColor: colors.white,
},
})
function interpolatePx(
@@ -1,61 +0,0 @@
/**
* 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.
*
*/
import {StyleSheet, TouchableOpacity, type ViewStyle} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {createHitslop} from '#/lib/constants'
type Props = {
onRequestClose: () => void
}
const HIT_SLOP = createHitslop(16)
const ImageDefaultHeader = ({onRequestClose}: Props) => {
const {_} = useLingui()
return (
<SafeAreaView style={styles.root}>
<TouchableOpacity
style={[styles.closeButton, styles.blurredBackground]}
onPress={onRequestClose}
hitSlop={HIT_SLOP}
accessibilityRole="button"
accessibilityLabel={_(msg`Close image`)}
accessibilityHint={_(msg`Closes viewer for header image`)}
onAccessibilityEscape={onRequestClose}>
<FontAwesomeIcon icon="close" color={'#fff'} size={22} />
</TouchableOpacity>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
root: {
alignItems: 'flex-end',
pointerEvents: 'box-none',
},
closeButton: {
marginRight: 10,
marginTop: 10,
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 22,
backgroundColor: '#00000077',
},
blurredBackground: {
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
} as ViewStyle,
})
export default ImageDefaultHeader