Refresh lightbox designs (#10330)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -127,3 +127,6 @@ bskyweb/static/media/*.webp
|
|||||||
bskyweb/static/media/*.jpg
|
bskyweb/static/media/*.jpg
|
||||||
bskyweb/static/media/*.png
|
bskyweb/static/media/*.png
|
||||||
bskyweb/static/media/*.svg
|
bskyweb/static/media/*.svg
|
||||||
|
|
||||||
|
# superpowers plugin plans/specs — local-only workspace
|
||||||
|
docs/superpowers/
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,6 @@ import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
|||||||
import {listenSessionDropped} from '#/state/events'
|
import {listenSessionDropped} from '#/state/events'
|
||||||
import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
|
import {GlobalGestureEventsProvider} from '#/state/global-gesture-events'
|
||||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
|
||||||
import {MessagesProvider} from '#/state/messages'
|
import {MessagesProvider} from '#/state/messages'
|
||||||
import {Provider as ModalStateProvider} from '#/state/modals'
|
import {Provider as ModalStateProvider} from '#/state/modals'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
@@ -62,6 +61,7 @@ import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
|||||||
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
|
import {Provider as LightboxStateProvider} from '#/components/Lightbox/state'
|
||||||
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,6 @@ import {Provider as DialogStateProvider} from '#/state/dialogs'
|
|||||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||||
import {listenSessionDropped} from '#/state/events'
|
import {listenSessionDropped} from '#/state/events'
|
||||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
|
||||||
import {MessagesProvider} from '#/state/messages'
|
import {MessagesProvider} from '#/state/messages'
|
||||||
import {Provider as ModalStateProvider} from '#/state/modals'
|
import {Provider as ModalStateProvider} from '#/state/modals'
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
import {init as initPersistedState} from '#/state/persisted'
|
||||||
@@ -54,6 +53,7 @@ import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
|||||||
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
|
import {Provider as LightboxStateProvider} from '#/components/Lightbox/state'
|
||||||
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import {useCallback} from 'react'
|
|||||||
|
|
||||||
import {shareImageModal} from '#/lib/media/manip'
|
import {shareImageModal} from '#/lib/media/manip'
|
||||||
import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'
|
import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'
|
||||||
import {useLightbox, useLightboxControls} from '#/state/lightbox'
|
import ImageView from '#/components/Lightbox/pager/ImagePager'
|
||||||
import ImageView from './ImageViewing'
|
import {useLightbox, useLightboxControls} from '#/components/Lightbox/state'
|
||||||
|
|
||||||
export function Lightbox() {
|
export function Lightbox() {
|
||||||
const {activeLightbox} = useLightbox()
|
const {activeLightbox} = useLightbox()
|
||||||
@@ -9,7 +9,6 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
|||||||
|
|
||||||
import {saveImageToMediaLibrary} from '#/lib/media/manip'
|
import {saveImageToMediaLibrary} from '#/lib/media/manip'
|
||||||
import {useA11y} from '#/state/a11y'
|
import {useA11y} from '#/state/a11y'
|
||||||
import {useLightbox, useLightboxControls} from '#/state/lightbox'
|
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
flatten,
|
flatten,
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {Backdrop} from '#/components/Dialog'
|
import {Backdrop} from '#/components/Dialog'
|
||||||
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox'
|
||||||
import {
|
import {
|
||||||
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeftIcon,
|
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeftIcon,
|
||||||
ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon,
|
ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon,
|
||||||
@@ -26,11 +26,14 @@ import {
|
|||||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
||||||
import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download'
|
import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||||
|
import {CircleChromeButton} from '#/components/Lightbox/chrome/CircleChromeButton'
|
||||||
|
import {PagerDots} from '#/components/Lightbox/chrome/PagerDots'
|
||||||
|
import {useLightbox, useLightboxControls} from '#/components/Lightbox/state'
|
||||||
|
import {type ImageSource} from '#/components/Lightbox/types'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {type ImageSource} from './ImageViewing/@types'
|
|
||||||
|
|
||||||
export function Lightbox() {
|
export function Lightbox() {
|
||||||
const {activeLightbox} = useLightbox()
|
const {activeLightbox} = useLightbox()
|
||||||
@@ -225,7 +228,13 @@ function LightboxGallery({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{img.alt ? (
|
{img.alt ? (
|
||||||
<View style={[a.px_4xl, a.py_2xl, t.atoms.bg, delayedFadeInAnim]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.px_4xl,
|
||||||
|
a.py_2xl,
|
||||||
|
{backgroundColor: 'rgba(0, 0, 0, 0.45)'},
|
||||||
|
delayedFadeInAnim,
|
||||||
|
]}>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityLabel={_(msg`Expand alt text`)}
|
accessibilityLabel={_(msg`Expand alt text`)}
|
||||||
accessibilityHint={_(
|
accessibilityHint={_(
|
||||||
@@ -235,7 +244,7 @@ function LightboxGallery({
|
|||||||
setAltExpanded(!isAltExpanded)
|
setAltExpanded(!isAltExpanded)
|
||||||
}}>
|
}}>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_md, a.leading_snug]}
|
style={[a.text_md, a.leading_snug, {color: '#fff'}]}
|
||||||
numberOfLines={isAltExpanded ? 0 : 3}
|
numberOfLines={isAltExpanded ? 0 : 3}
|
||||||
ellipsizeMode="tail">
|
ellipsizeMode="tail">
|
||||||
{img.alt}
|
{img.alt}
|
||||||
@@ -251,29 +260,51 @@ function LightboxGallery({
|
|||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Trigger label={_(msg`Image options`)}>
|
<Menu.Trigger label={_(msg`Image options`)}>
|
||||||
{({props}) => (
|
{({props}) => (
|
||||||
<Button
|
<Pressable
|
||||||
{...props}
|
{...props}
|
||||||
style={[
|
accessible={false}
|
||||||
a.absolute,
|
style={[a.absolute, styles.menuBtn, delayedFadeInAnim]}>
|
||||||
styles.menuBtn,
|
<CircleChromeButton
|
||||||
styles.blurredBackdrop,
|
icon={EllipsisIcon}
|
||||||
a.transition_color,
|
iconStyle={{transform: [{rotate: '90deg'}]}}
|
||||||
delayedFadeInAnim,
|
|
||||||
]}
|
|
||||||
hoverStyle={styles.blurredBackdropHover}
|
|
||||||
color="secondary"
|
|
||||||
label={_(msg`Image options`)}
|
label={_(msg`Image options`)}
|
||||||
shape="round"
|
|
||||||
size={gtPhone ? 'large' : 'small'}>
|
|
||||||
<EllipsisIcon
|
|
||||||
size={gtPhone ? 'md' : 'sm'}
|
|
||||||
style={{color: t.palette.white}}
|
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
</Menu.Trigger>
|
</Menu.Trigger>
|
||||||
<Menu.Outer>
|
<Menu.Outer>
|
||||||
<Menu.Group>
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
label={_(msg`Share image`)}
|
||||||
|
onPress={async () => {
|
||||||
|
const url = img.uri
|
||||||
|
if (
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
'share' in navigator &&
|
||||||
|
navigator.share
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await navigator.share({url})
|
||||||
|
} catch {
|
||||||
|
// User cancelled or share failed; no-op
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
navigator.clipboard
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url)
|
||||||
|
Toast.show(_(msg`Link copied to clipboard`))
|
||||||
|
} catch {
|
||||||
|
Toast.show(_(msg`Failed to copy link`), {type: 'error'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Share image</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={ShareIcon} position="right" />
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={_(msg`Download image`)}
|
label={_(msg`Download image`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -294,22 +325,20 @@ function LightboxGallery({
|
|||||||
</Menu.Group>
|
</Menu.Group>
|
||||||
</Menu.Outer>
|
</Menu.Outer>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
<Button
|
<View style={[a.absolute, styles.closeBtn, delayedFadeInAnim]}>
|
||||||
onPress={onClose}
|
<CircleChromeButton
|
||||||
style={[
|
icon={XIcon}
|
||||||
a.absolute,
|
|
||||||
styles.closeBtn,
|
|
||||||
styles.blurredBackdrop,
|
|
||||||
a.transition_color,
|
|
||||||
delayedFadeInAnim,
|
|
||||||
]}
|
|
||||||
hoverStyle={styles.blurredBackdropHover}
|
|
||||||
color="secondary"
|
|
||||||
label={_(msg`Close image viewer`)}
|
label={_(msg`Close image viewer`)}
|
||||||
shape="round"
|
onPress={onClose}
|
||||||
size={gtPhone ? 'large' : 'small'}>
|
/>
|
||||||
<XIcon size={gtPhone ? 'md' : 'sm'} style={{color: t.palette.white}} />
|
</View>
|
||||||
</Button>
|
{imgs.length > 1 && (
|
||||||
|
<View
|
||||||
|
style={[a.absolute, styles.pagerDots, delayedFadeInAnim]}
|
||||||
|
pointerEvents="none">
|
||||||
|
<PagerDots count={imgs.length} activeIndex={index} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -429,6 +458,14 @@ const styles = StyleSheet.create({
|
|||||||
top: 20,
|
top: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
},
|
},
|
||||||
|
pagerDots: {
|
||||||
|
top: 20,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 44,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
leftBtn: {
|
leftBtn: {
|
||||||
left: 20,
|
left: 20,
|
||||||
right: 'auto',
|
right: 'auto',
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {type ComponentType} from 'react'
|
||||||
|
import {
|
||||||
|
Pressable,
|
||||||
|
type PressableProps,
|
||||||
|
type StyleProp,
|
||||||
|
StyleSheet,
|
||||||
|
type TextStyle,
|
||||||
|
} from 'react-native'
|
||||||
|
import {BlurView} from 'expo-blur'
|
||||||
|
|
||||||
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
|
import {type Props as IconProps} from '#/components/icons/common'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
icon: ComponentType<IconProps>
|
||||||
|
iconStyle?: StyleProp<TextStyle>
|
||||||
|
label: string
|
||||||
|
onPress?: PressableProps['onPress']
|
||||||
|
testID?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZE = 44
|
||||||
|
const RADIUS = 24
|
||||||
|
const ICON = 24
|
||||||
|
|
||||||
|
export function CircleChromeButton({
|
||||||
|
icon: Icon,
|
||||||
|
iconStyle,
|
||||||
|
label,
|
||||||
|
onPress,
|
||||||
|
testID,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={label}
|
||||||
|
accessibilityHint=""
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
onPress={onPress}
|
||||||
|
testID={testID}
|
||||||
|
style={({pressed}) => [styles.root, pressed && styles.pressed]}>
|
||||||
|
<BlurView intensity={20} tint="dark" style={styles.inner}>
|
||||||
|
<Icon width={ICON} fill="#fff" style={iconStyle} />
|
||||||
|
</BlurView>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
width: SIZE,
|
||||||
|
height: SIZE,
|
||||||
|
borderRadius: RADIUS,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
inner: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
},
|
||||||
|
pressed: {
|
||||||
|
opacity: 0.85,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {type ComponentType} from 'react'
|
||||||
|
import {
|
||||||
|
Pressable,
|
||||||
|
type PressableProps,
|
||||||
|
type StyleProp,
|
||||||
|
StyleSheet,
|
||||||
|
type TextStyle,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
|
import {type Props as IconProps} from '#/components/icons/common'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
icon: ComponentType<IconProps>
|
||||||
|
iconStyle?: StyleProp<TextStyle>
|
||||||
|
label: string
|
||||||
|
onPress?: PressableProps['onPress']
|
||||||
|
testID?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZE = 44
|
||||||
|
const RADIUS = 24
|
||||||
|
const ICON = 24
|
||||||
|
|
||||||
|
export function CircleChromeButton({
|
||||||
|
icon: Icon,
|
||||||
|
iconStyle,
|
||||||
|
label,
|
||||||
|
onPress,
|
||||||
|
testID,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={label}
|
||||||
|
accessibilityHint=""
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
onPress={onPress}
|
||||||
|
testID={testID}
|
||||||
|
style={({pressed}) => [styles.root, pressed && styles.pressed]}>
|
||||||
|
<View style={styles.inner}>
|
||||||
|
<Icon width={ICON} fill="#fff" style={iconStyle} />
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
width: SIZE,
|
||||||
|
height: SIZE,
|
||||||
|
borderRadius: RADIUS,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
inner: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
|
// @ts-expect-error web-only
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
WebkitBackdropFilter: 'blur(8px)',
|
||||||
|
},
|
||||||
|
pressed: {
|
||||||
|
opacity: 0.85,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import {useRef} from 'react'
|
||||||
|
import {
|
||||||
|
LayoutAnimation,
|
||||||
|
Pressable,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
import {msg} from '@lingui/core/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
altText: string | undefined
|
||||||
|
isAltExpanded: boolean
|
||||||
|
onToggleAltExpanded: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const isMomentumScrolling = useRef(false)
|
||||||
|
|
||||||
|
if (!altText) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[styles.root, {paddingBottom: insets.bottom + 8}]}
|
||||||
|
pointerEvents="box-none">
|
||||||
|
<View style={[a.mx_md, styles.altWrap]}>
|
||||||
|
<ScrollView
|
||||||
|
scrollEnabled={isAltExpanded}
|
||||||
|
onMomentumScrollBegin={() => {
|
||||||
|
isMomentumScrolling.current = true
|
||||||
|
}}
|
||||||
|
onMomentumScrollEnd={() => {
|
||||||
|
isMomentumScrolling.current = false
|
||||||
|
}}
|
||||||
|
contentContainerStyle={[a.px_md, a.py_sm]}>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Expand alt text`)}
|
||||||
|
accessibilityHint=""
|
||||||
|
onPress={() => {
|
||||||
|
if (isMomentumScrolling.current) return
|
||||||
|
LayoutAnimation.configureNext({
|
||||||
|
duration: 450,
|
||||||
|
update: {type: 'spring', springDamping: 1},
|
||||||
|
})
|
||||||
|
onToggleAltExpanded()
|
||||||
|
}}>
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
selectable
|
||||||
|
style={[a.text_sm, styles.altText]}
|
||||||
|
numberOfLines={isAltExpanded ? undefined : 3}>
|
||||||
|
{altText}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
altWrap: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.45)',
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
altText: {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
import {msg} from '@lingui/core/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {TimesLarge_Stroke2_Corner0_Rounded as CloseIcon} from '#/components/icons/Times'
|
||||||
|
import {CircleChromeButton} from './CircleChromeButton'
|
||||||
|
import {ImageMenu} from './ImageMenu'
|
||||||
|
import {PagerDots} from './PagerDots'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onRequestClose: () => void
|
||||||
|
onPressShare: () => void
|
||||||
|
onPressSave: () => void
|
||||||
|
imageCount: number
|
||||||
|
activeIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({
|
||||||
|
onRequestClose,
|
||||||
|
onPressShare,
|
||||||
|
onPressSave,
|
||||||
|
imageCount,
|
||||||
|
activeIndex,
|
||||||
|
}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.root,
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_center,
|
||||||
|
a.px_md,
|
||||||
|
{paddingTop: insets.top + 8},
|
||||||
|
]}
|
||||||
|
pointerEvents="box-none">
|
||||||
|
<ImageMenu onPressShare={onPressShare} onPressSave={onPressSave} />
|
||||||
|
<PagerDots count={imageCount} activeIndex={activeIndex} />
|
||||||
|
<CircleChromeButton
|
||||||
|
icon={CloseIcon}
|
||||||
|
label={_(msg`Close image`)}
|
||||||
|
onPress={onRequestClose}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import {useRef, useState} from 'react'
|
||||||
|
import {Modal, Pressable, StyleSheet, View} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
interpolate,
|
||||||
|
runOnJS,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withSpring,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {msg} from '@lingui/core/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight'
|
||||||
|
import {type Props as IconProps} from '#/components/icons/common'
|
||||||
|
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid'
|
||||||
|
import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {CircleChromeButton} from './CircleChromeButton'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onPressShare: () => void
|
||||||
|
onPressSave: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type Anchor = {x: number; y: number; width: number; height: number}
|
||||||
|
|
||||||
|
const MENU_WIDTH = 160
|
||||||
|
const GAP = 6
|
||||||
|
const CARD_BG = '#000000'
|
||||||
|
const CARD_BORDER = '#232e3e'
|
||||||
|
const ITEM_TEXT = '#f9fafb'
|
||||||
|
|
||||||
|
const SPRING_IN = {damping: 18, mass: 0.6, stiffness: 240}
|
||||||
|
const TIMING_OUT = {duration: 150}
|
||||||
|
|
||||||
|
export function ImageMenu({onPressShare, onPressSave}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const triggerRef = useRef<View>(null)
|
||||||
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
|
const [anchor, setAnchor] = useState<Anchor | null>(null)
|
||||||
|
const progress = useSharedValue(0)
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
triggerRef.current?.measureInWindow((x, y, width, height) => {
|
||||||
|
setAnchor({x, y, width, height})
|
||||||
|
setIsMounted(true)
|
||||||
|
progress.set(withSpring(1, SPRING_IN))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
progress.set(
|
||||||
|
withTiming(0, TIMING_OUT, finished => {
|
||||||
|
if (finished) {
|
||||||
|
runOnJS(setIsMounted)(false)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const runAction = (action: () => void) => {
|
||||||
|
close()
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View ref={triggerRef} collapsable={false}>
|
||||||
|
<CircleChromeButton
|
||||||
|
icon={DotsIcon}
|
||||||
|
iconStyle={{transform: [{rotate: '90deg'}]}}
|
||||||
|
label={_(msg`Image options`)}
|
||||||
|
onPress={open}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Modal
|
||||||
|
transparent
|
||||||
|
visible={isMounted}
|
||||||
|
animationType="none"
|
||||||
|
onRequestClose={close}
|
||||||
|
statusBarTranslucent>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={_(msg`Close menu`)}
|
||||||
|
accessibilityHint=""
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
onPress={close}
|
||||||
|
/>
|
||||||
|
{anchor && (
|
||||||
|
<MenuCard anchor={anchor} progress={progress}>
|
||||||
|
<MenuItem
|
||||||
|
icon={ShareIcon}
|
||||||
|
label={_(msg`Share image`)}
|
||||||
|
onPress={() => runAction(onPressShare)}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
icon={DownloadIcon}
|
||||||
|
label={_(msg`Save image`)}
|
||||||
|
onPress={() => runAction(onPressSave)}
|
||||||
|
/>
|
||||||
|
</MenuCard>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuCard({
|
||||||
|
anchor,
|
||||||
|
progress,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
anchor: Anchor
|
||||||
|
progress: ReturnType<typeof useSharedValue<number>>
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
opacity: progress.get(),
|
||||||
|
transform: [{scale: interpolate(progress.get(), [0, 1], [0.9, 1])}],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
styles.card,
|
||||||
|
{
|
||||||
|
top: anchor.y + anchor.height + GAP,
|
||||||
|
left: anchor.x,
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
},
|
||||||
|
animatedStyle,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
icon: React.ComponentType<IconProps>
|
||||||
|
label: string
|
||||||
|
onPress: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={label}
|
||||||
|
accessibilityHint=""
|
||||||
|
onPress={onPress}
|
||||||
|
style={({pressed}) => [styles.item, pressed && styles.itemPressed]}>
|
||||||
|
<Icon width={18} fill={ITEM_TEXT} />
|
||||||
|
<Text style={styles.itemText}>{label}</Text>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
width: MENU_WIDTH,
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 16,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: CARD_BORDER,
|
||||||
|
backgroundColor: CARD_BG,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {width: 0, height: 4},
|
||||||
|
shadowOpacity: 0.16,
|
||||||
|
shadowRadius: 20,
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
height: 44,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
itemPressed: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
},
|
||||||
|
itemText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '500',
|
||||||
|
lineHeight: 19.5,
|
||||||
|
color: ITEM_TEXT,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
count: number
|
||||||
|
activeIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTIVE = 6
|
||||||
|
const INACTIVE = 4
|
||||||
|
const GAP = 5
|
||||||
|
|
||||||
|
export function PagerDots({count, activeIndex}: Props) {
|
||||||
|
if (count <= 1) return null
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.justify_center, styles.row]}>
|
||||||
|
{Array.from({length: count}).map((_, i) => {
|
||||||
|
const isActive = i === activeIndex
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
style={[
|
||||||
|
isActive ? styles.active : styles.inactive,
|
||||||
|
isActive ? styles.activeDot : styles.inactiveDot,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
row: {
|
||||||
|
gap: GAP,
|
||||||
|
},
|
||||||
|
activeDot: {
|
||||||
|
width: ACTIVE,
|
||||||
|
height: ACTIVE,
|
||||||
|
borderRadius: ACTIVE / 2,
|
||||||
|
},
|
||||||
|
inactiveDot: {
|
||||||
|
width: INACTIVE,
|
||||||
|
height: INACTIVE,
|
||||||
|
borderRadius: INACTIVE / 2,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
inactive: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {Lightbox} from './Lightbox'
|
||||||
+4
-3
@@ -6,6 +6,7 @@ import {
|
|||||||
type PanGesture,
|
type PanGesture,
|
||||||
} from 'react-native-gesture-handler'
|
} from 'react-native-gesture-handler'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
|
type AnimatableValue,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
type SharedValue,
|
type SharedValue,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
@@ -20,7 +21,7 @@ import {
|
|||||||
type Dimensions as ImageDimensions,
|
type Dimensions as ImageDimensions,
|
||||||
type ImageSource,
|
type ImageSource,
|
||||||
type LightboxTransforms,
|
type LightboxTransforms,
|
||||||
} from '../../@types'
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
applyRounding,
|
applyRounding,
|
||||||
createTransform,
|
createTransform,
|
||||||
@@ -29,7 +30,7 @@ import {
|
|||||||
prependTransform,
|
prependTransform,
|
||||||
readTransform,
|
readTransform,
|
||||||
type TransformMatrix,
|
type TransformMatrix,
|
||||||
} from '../../transforms'
|
} from '../transforms'
|
||||||
|
|
||||||
const MIN_SCREEN_ZOOM = 2
|
const MIN_SCREEN_ZOOM = 2
|
||||||
const MAX_ORIGINAL_IMAGE_ZOOM = 2
|
const MAX_ORIGINAL_IMAGE_ZOOM = 2
|
||||||
@@ -460,7 +461,7 @@ function clampTranslation(
|
|||||||
return clampedValue
|
return clampedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
function withClampedSpring(value: any) {
|
function withClampedSpring<T extends AnimatableValue>(value: T): T {
|
||||||
'worklet'
|
'worklet'
|
||||||
return withSpring(value, {overshootClamping: true})
|
return withSpring(value, {overshootClamping: true})
|
||||||
}
|
}
|
||||||
+1
-1
@@ -30,7 +30,7 @@ import {
|
|||||||
type Dimensions as ImageDimensions,
|
type Dimensions as ImageDimensions,
|
||||||
type ImageSource,
|
type ImageSource,
|
||||||
type LightboxTransforms,
|
type LightboxTransforms,
|
||||||
} from '../../@types'
|
} from '../../types'
|
||||||
|
|
||||||
const MAX_ORIGINAL_IMAGE_ZOOM = 2
|
const MAX_ORIGINAL_IMAGE_ZOOM = 2
|
||||||
const MIN_SCREEN_ZOOM = 2
|
const MIN_SCREEN_ZOOM = 2
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
|||||||
type Dimensions as ImageDimensions,
|
type Dimensions as ImageDimensions,
|
||||||
type ImageSource,
|
type ImageSource,
|
||||||
type LightboxTransforms,
|
type LightboxTransforms,
|
||||||
} from '../../@types'
|
} from '../../types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
imageSrc: ImageSource
|
imageSrc: ImageSource
|
||||||
+27
-144
@@ -7,18 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
// Original code copied and simplified from the link below as the codebase is currently not maintained:
|
// Original code copied and simplified from the link below as the codebase is currently not maintained:
|
||||||
// https://github.com/jobtoday/react-native-image-viewing
|
// https://github.com/jobtoday/react-native-image-viewing
|
||||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||||
import {
|
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
|
||||||
LayoutAnimation,
|
|
||||||
PixelRatio,
|
|
||||||
StyleSheet,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {SystemBars} from 'react-native-edge-to-edge'
|
import {SystemBars} from 'react-native-edge-to-edge'
|
||||||
import {Gesture} from 'react-native-gesture-handler'
|
import {Gesture} from 'react-native-gesture-handler'
|
||||||
import PagerView from 'react-native-pager-view'
|
import PagerView from 'react-native-pager-view'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
|
type AnimatableValue,
|
||||||
type AnimatedRef,
|
type AnimatedRef,
|
||||||
cancelAnimation,
|
cancelAnimation,
|
||||||
interpolate,
|
interpolate,
|
||||||
@@ -37,28 +32,22 @@ import Animated, {
|
|||||||
withSpring,
|
withSpring,
|
||||||
type WithSpringConfig,
|
type WithSpringConfig,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {SafeAreaView} from 'react-native-safe-area-context'
|
|
||||||
import * as ScreenOrientation from 'expo-screen-orientation'
|
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 {type Dimensions} from '#/lib/media/types'
|
||||||
import {colors, s} from '#/lib/styles'
|
|
||||||
import {type 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 {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {setSystemUITheme} from '#/alf/util/systemUI'
|
import {setSystemUITheme} from '#/alf/util/systemUI'
|
||||||
|
import {type Lightbox} from '#/components/Lightbox/state'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army'
|
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
||||||
|
import {Footer} from '../chrome/Footer'
|
||||||
|
import {Header} from '../chrome/Header'
|
||||||
import {
|
import {
|
||||||
type ImageSource,
|
type ImageSource,
|
||||||
type LightboxTransforms,
|
type LightboxTransforms,
|
||||||
type Transform,
|
type Transform,
|
||||||
} from './@types'
|
} from '../types'
|
||||||
import ImageDefaultHeader from './components/ImageDefaultHeader'
|
import ImageItem from './ImageItem/ImageItem'
|
||||||
import ImageItem from './components/ImageItem/ImageItem'
|
|
||||||
|
|
||||||
type Rect = {x: number; y: number; width: number; height: number}
|
type Rect = {x: number; y: number; width: number; height: number}
|
||||||
|
|
||||||
@@ -393,7 +382,7 @@ function ImageView({
|
|||||||
overdrag={true}
|
overdrag={true}
|
||||||
style={styles.pager}>
|
style={styles.pager}>
|
||||||
{images.map((imageSrc, i) => (
|
{images.map((imageSrc, i) => (
|
||||||
<View key={imageSrc.uri}>
|
<View key={`${i}-${imageSrc.uri}`}>
|
||||||
<LightboxImage
|
<LightboxImage
|
||||||
onTap={onTap}
|
onTap={onTap}
|
||||||
onZoom={onZoom}
|
onZoom={onZoom}
|
||||||
@@ -413,22 +402,27 @@ function ImageView({
|
|||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</PagerView>
|
</PagerView>
|
||||||
<View style={styles.controls}>
|
<View style={styles.controls} pointerEvents="box-none">
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={animatedHeaderStyle}
|
style={animatedHeaderStyle}
|
||||||
|
pointerEvents="box-none"
|
||||||
renderToHardwareTextureAndroid>
|
renderToHardwareTextureAndroid>
|
||||||
<ImageDefaultHeader onRequestClose={handleRequestClose} />
|
<Header
|
||||||
|
onRequestClose={handleRequestClose}
|
||||||
|
onPressShare={() => onPressShare(images[imageIndex].uri)}
|
||||||
|
onPressSave={() => onPressSave(images[imageIndex].uri)}
|
||||||
|
imageCount={images.length}
|
||||||
|
activeIndex={imageIndex}
|
||||||
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={animatedFooterStyle}
|
style={animatedFooterStyle}
|
||||||
|
pointerEvents="box-none"
|
||||||
renderToHardwareTextureAndroid={!isAltExpanded}>
|
renderToHardwareTextureAndroid={!isAltExpanded}>
|
||||||
<LightboxFooter
|
<Footer
|
||||||
images={images}
|
altText={images[imageIndex].alt}
|
||||||
index={imageIndex}
|
|
||||||
isAltExpanded={isAltExpanded}
|
isAltExpanded={isAltExpanded}
|
||||||
toggleAltExpanded={() => setIsAltExpanded(e => !e)}
|
onToggleAltExpanded={() => setIsAltExpanded(e => !e)}
|
||||||
onPressSave={onPressSave}
|
|
||||||
onPressShare={onPressShare}
|
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
@@ -607,85 +601,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({
|
const styles = StyleSheet.create({
|
||||||
screen: {
|
screen: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -722,41 +637,6 @@ const styles = StyleSheet.create({
|
|||||||
pager: {
|
pager: {
|
||||||
flex: 1,
|
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(
|
function interpolatePx(
|
||||||
@@ -856,7 +736,10 @@ function interpolateTransform(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function withClampedSpring(value: any, config: WithSpringConfig) {
|
function withClampedSpring<T extends AnimatableValue>(
|
||||||
|
value: T,
|
||||||
|
config: WithSpringConfig,
|
||||||
|
): T {
|
||||||
'worklet'
|
'worklet'
|
||||||
return withSpring(value, {...config, overshootClamping: true})
|
return withSpring(value, {...config, overshootClamping: true})
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import {type Position} from './@types'
|
import {type Position} from '../types'
|
||||||
|
|
||||||
export type TransformMatrix = [
|
export type TransformMatrix = [
|
||||||
number,
|
number,
|
||||||
@@ -9,7 +9,7 @@ import {nanoid} from 'nanoid/non-secure'
|
|||||||
|
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||||
import {type ImageSource} from '#/view/com/lightbox/ImageViewing/@types'
|
import {type ImageSource} from '#/components/Lightbox/types'
|
||||||
|
|
||||||
export type Lightbox = {
|
export type Lightbox = {
|
||||||
id: string
|
id: string
|
||||||
+2
-1
@@ -6,6 +6,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {type Component} from 'react'
|
||||||
import {type TransformsStyle} from 'react-native'
|
import {type TransformsStyle} from 'react-native'
|
||||||
import {
|
import {
|
||||||
type AnimatedRef,
|
type AnimatedRef,
|
||||||
@@ -28,7 +29,7 @@ export type ImageSource = {
|
|||||||
thumbUri: string
|
thumbUri: string
|
||||||
thumbDimensions: Dimensions | null
|
thumbDimensions: Dimensions | null
|
||||||
thumbRect: MeasuredDimensions | null
|
thumbRect: MeasuredDimensions | null
|
||||||
thumbRef?: AnimatedRef<any> | null
|
thumbRef?: AnimatedRef<Component> | null
|
||||||
thumbBorderRadius?: number
|
thumbBorderRadius?: number
|
||||||
alt?: string
|
alt?: string
|
||||||
type: 'image' | 'circle-avi' | 'rect-avi'
|
type: 'image' | 'circle-avi' | 'rect-avi'
|
||||||
@@ -2,12 +2,12 @@ import {InteractionManager, View} from 'react-native'
|
|||||||
import {type AnimatedRef} from 'react-native-reanimated'
|
import {type AnimatedRef} from 'react-native-reanimated'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
|
|
||||||
import {useLightboxControls} from '#/state/lightbox'
|
|
||||||
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
|
||||||
import {atoms as a, tokens} from '#/alf'
|
import {atoms as a, tokens} from '#/alf'
|
||||||
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
||||||
import {Gallery} from '#/components/images/Gallery'
|
import {Gallery} from '#/components/images/Gallery'
|
||||||
import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid'
|
import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid'
|
||||||
|
import {useLightboxControls} from '#/components/Lightbox/state'
|
||||||
|
import {type Dimensions} from '#/components/Lightbox/types'
|
||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {type EmbedType} from '#/types/bsky/post'
|
import {type EmbedType} from '#/types/bsky/post'
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {type StyleProp, View, type ViewStyle} from 'react-native'
|
|||||||
import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
||||||
import {type AppBskyEmbedImages} from '@atproto/api'
|
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||||
|
|
||||||
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
|
||||||
import {atoms as a, useBreakpoints} from '#/alf'
|
import {atoms as a, useBreakpoints} from '#/alf'
|
||||||
|
import {type Dimensions} from '#/components/Lightbox/types'
|
||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
import {GalleryItem} from './ImageLayoutGridItem'
|
import {GalleryItem} from './ImageLayoutGridItem'
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {BACK_HITSLOP} from '#/lib/constants'
|
|||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {type Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {useLightboxControls} from '#/state/lightbox'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
@@ -23,6 +22,7 @@ import {atoms as a, platform, useTheme} from '#/alf'
|
|||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
|
||||||
|
import {useLightboxControls} from '#/components/Lightbox/state'
|
||||||
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
|
||||||
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
|
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
|
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {useLightboxControls} from './lightbox'
|
import {useLightboxControls} from '#/components/Lightbox/state'
|
||||||
import {useModalControls} from './modals'
|
import {useModalControls} from './modals'
|
||||||
import {useComposerControls} from './shell/composer'
|
import {useComposerControls} from './shell/composer'
|
||||||
import {useSetDrawerOpen} from './shell/drawer-open'
|
import {useSetDrawerOpen} from './shell/drawer-open'
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -13,13 +13,13 @@ import {makeProfileLink} from '#/lib/routes/links'
|
|||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {emitSoftReset} from '#/state/events'
|
import {emitSoftReset} from '#/state/events'
|
||||||
import {useLightboxControls} from '#/state/lightbox'
|
|
||||||
import {TextLink} from '#/view/com/util/Link'
|
import {TextLink} from '#/view/com/util/Link'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {UserAvatar, type UserAvatarType} from '#/view/com/util/UserAvatar'
|
import {UserAvatar, type UserAvatarType} from '#/view/com/util/UserAvatar'
|
||||||
import {StarterPack} from '#/components/icons/StarterPack'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {useLightboxControls} from '#/components/Lightbox/state'
|
||||||
|
|
||||||
export function ProfileSubpageHeader({
|
export function ProfileSubpageHeader({
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {useDedupe} from '#/lib/hooks/useDedupe'
|
|||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {addStyle} from '#/lib/styles'
|
import {addStyle} from '#/lib/styles'
|
||||||
import {useLightbox} from '#/state/lightbox'
|
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
|
import {useLightbox} from '#/components/Lightbox/state'
|
||||||
import {IS_IOS} from '#/env'
|
import {IS_IOS} from '#/env'
|
||||||
import {FlatList_INTERNAL} from './Views'
|
import {FlatList_INTERNAL} from './Views'
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
useSetDrawerOpen,
|
useSetDrawerOpen,
|
||||||
} from '#/state/shell'
|
} from '#/state/shell'
|
||||||
import {useCloseAnyActiveElement} from '#/state/util'
|
import {useCloseAnyActiveElement} from '#/state/util'
|
||||||
import {Lightbox} from '#/view/com/lightbox/Lightbox'
|
|
||||||
import {ModalsContainer} from '#/view/com/modals/Modal'
|
import {ModalsContainer} from '#/view/com/modals/Modal'
|
||||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
import {Deactivated} from '#/screens/Deactivated'
|
import {Deactivated} from '#/screens/Deactivated'
|
||||||
@@ -33,6 +32,7 @@ import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
|||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
|
import {Lightbox} from '#/components/Lightbox'
|
||||||
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
|
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
|
||||||
import {
|
import {
|
||||||
Outlet as PolicyUpdateOverlayPortalOutlet,
|
Outlet as PolicyUpdateOverlayPortalOutlet,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {type NavigationProp} from '#/lib/routes/types'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
|
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
import {Lightbox} from '#/view/com/lightbox/Lightbox'
|
|
||||||
import {ModalsContainer} from '#/view/com/modals/Modal'
|
import {ModalsContainer} from '#/view/com/modals/Modal'
|
||||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
import {Deactivated} from '#/screens/Deactivated'
|
import {Deactivated} from '#/screens/Deactivated'
|
||||||
@@ -23,6 +22,7 @@ import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
|||||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
|
import {useWelcomeModal} from '#/components/hooks/useWelcomeModal'
|
||||||
|
import {Lightbox} from '#/components/Lightbox'
|
||||||
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
|
import {GlobalReportDialog} from '#/components/moderation/ReportDialog'
|
||||||
import {
|
import {
|
||||||
Outlet as PolicyUpdateOverlayPortalOutlet,
|
Outlet as PolicyUpdateOverlayPortalOutlet,
|
||||||
|
|||||||
Reference in New Issue
Block a user