web updates
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import {useRef, useState} from 'react'
|
import {useRef, useState} from 'react'
|
||||||
import {Pressable, ScrollView, View} from 'react-native'
|
import {Pressable, ScrollView, useWindowDimensions, View} from 'react-native'
|
||||||
import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {type AppBskyEmbedImages} from '@atproto/api'
|
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||||
@@ -16,8 +16,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
const CONTAINER_ASPECT_RATIO = 4 / 3
|
const CONTAINER_ASPECT_RATIO = 4 / 3
|
||||||
const PEEK_WIDTH = 40
|
const ITEM_GAP = 8 // tokens.space.sm
|
||||||
const ITEM_GAP = 6
|
|
||||||
|
|
||||||
interface GalleryProps {
|
interface GalleryProps {
|
||||||
images: AppBskyEmbedImages.ViewImage[]
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
@@ -40,9 +39,9 @@ export function Gallery({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const largeAltBadge = useLargeAltBadgeEnabled()
|
const largeAltBadge = useLargeAltBadgeEnabled()
|
||||||
const [currentPage, setCurrentPage] = useState(0)
|
|
||||||
const currentPageRef = useRef(0)
|
const currentPageRef = useRef(0)
|
||||||
const scrollRef = useRef<ScrollView>(null)
|
const scrollRef = useRef<ScrollView>(null)
|
||||||
|
const {width: windowWidth} = useWindowDimensions()
|
||||||
const [containerWidth, setContainerWidth] = useState(0)
|
const [containerWidth, setContainerWidth] = useState(0)
|
||||||
|
|
||||||
const containerRefs = useRef<AnimatedRef<any>[]>([]).current
|
const containerRefs = useRef<AnimatedRef<any>[]>([]).current
|
||||||
@@ -57,33 +56,63 @@ export function Gallery({
|
|||||||
containerRefs[i] = refs[i]
|
containerRefs[i] = refs[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideBadges =
|
const isWithinQuote =
|
||||||
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||||
|
const hideBadges = isWithinQuote
|
||||||
|
|
||||||
const itemWidth = containerWidth > 0 ? containerWidth - PEEK_WIDTH : 0
|
const containerHeight =
|
||||||
const snapInterval = itemWidth + ITEM_GAP
|
containerWidth > 0 ? containerWidth / CONTAINER_ASPECT_RATIO : 0
|
||||||
|
const QUOTE_PADDING = 12
|
||||||
|
const bleed = !isWithinQuote
|
||||||
|
const insetLeft = bleed
|
||||||
|
? Math.max(windowWidth - containerWidth, 0)
|
||||||
|
: QUOTE_PADDING
|
||||||
|
const insetRight = bleed
|
||||||
|
? Math.max(windowWidth - insetLeft - containerWidth, 0)
|
||||||
|
: QUOTE_PADDING
|
||||||
|
|
||||||
const goToPage = (index: number) => {
|
const getItemWidth = (image: AppBskyEmbedImages.ViewImage) => {
|
||||||
scrollRef.current?.scrollTo({
|
const ar = image.aspectRatio
|
||||||
x: index * snapInterval,
|
if (ar && ar.width > 0 && ar.height > 0) {
|
||||||
animated: true,
|
const ratio = ar.width / ar.height
|
||||||
})
|
const w = containerHeight * ratio
|
||||||
|
return Math.max(containerWidth * 0.4, Math.min(w, containerWidth))
|
||||||
|
}
|
||||||
|
return containerWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'ArrowLeft' && currentPage > 0) {
|
const scrollEl = scrollRef.current as unknown as {
|
||||||
|
scrollTo: (opts: {x: number; animated: boolean}) => void
|
||||||
|
}
|
||||||
|
if (!scrollEl) return
|
||||||
|
|
||||||
|
if (e.key === 'ArrowLeft') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
goToPage(currentPage - 1)
|
scrollEl.scrollTo({
|
||||||
} else if (e.key === 'ArrowRight' && currentPage < images.length - 1) {
|
x: Math.max(0, currentScrollX.current - 200),
|
||||||
|
animated: true,
|
||||||
|
})
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
goToPage(currentPage + 1)
|
scrollEl.scrollTo({x: currentScrollX.current + 200, animated: true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const currentScrollX = useRef(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[a.rounded_md, a.overflow_hidden]}
|
style={
|
||||||
onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}
|
containerWidth > 0
|
||||||
|
? {height: containerHeight, overflow: 'visible'}
|
||||||
|
: {aspectRatio: CONTAINER_ASPECT_RATIO}
|
||||||
|
}
|
||||||
|
onLayout={e => {
|
||||||
|
const w = e.nativeEvent.layout.width
|
||||||
|
if (w > 0) {
|
||||||
|
setContainerWidth(w)
|
||||||
|
}
|
||||||
|
}}
|
||||||
// @ts-expect-error web-only prop
|
// @ts-expect-error web-only prop
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -97,26 +126,42 @@ export function Gallery({
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
style={[
|
style={[
|
||||||
{aspectRatio: CONTAINER_ASPECT_RATIO},
|
{
|
||||||
|
width: bleed ? windowWidth : containerWidth + QUOTE_PADDING * 2,
|
||||||
|
height: containerHeight,
|
||||||
|
marginLeft: -insetLeft,
|
||||||
|
},
|
||||||
web({
|
web({
|
||||||
scrollSnapType: 'x mandatory',
|
scrollBehavior: 'smooth',
|
||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
contentContainerStyle={{gap: ITEM_GAP}}
|
contentContainerStyle={{
|
||||||
|
gap: ITEM_GAP,
|
||||||
|
paddingLeft: insetLeft,
|
||||||
|
paddingRight: insetRight,
|
||||||
|
}}
|
||||||
onScroll={e => {
|
onScroll={e => {
|
||||||
const offsetX = e.nativeEvent.contentOffset.x
|
const offsetX = e.nativeEvent.contentOffset.x
|
||||||
if (snapInterval > 0) {
|
currentScrollX.current = offsetX
|
||||||
const page = Math.round(offsetX / snapInterval)
|
let accumulated = insetLeft
|
||||||
if (page !== currentPageRef.current) {
|
let page = 0
|
||||||
ax.metric('post:gallery:swipe', {
|
for (let i = 0; i < images.length; i++) {
|
||||||
fromIndex: currentPageRef.current,
|
const w = getItemWidth(images[i]) + ITEM_GAP
|
||||||
toIndex: page,
|
if (offsetX < accumulated + w / 2) {
|
||||||
totalImages: images.length,
|
page = i
|
||||||
})
|
break
|
||||||
currentPageRef.current = page
|
|
||||||
setCurrentPage(page)
|
|
||||||
}
|
}
|
||||||
|
accumulated += w
|
||||||
|
page = i
|
||||||
|
}
|
||||||
|
if (page !== currentPageRef.current) {
|
||||||
|
ax.metric('post:gallery:swipe', {
|
||||||
|
fromIndex: currentPageRef.current,
|
||||||
|
toIndex: page,
|
||||||
|
totalImages: images.length,
|
||||||
|
})
|
||||||
|
currentPageRef.current = page
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
@@ -126,10 +171,9 @@ export function Gallery({
|
|||||||
collapsable={false}
|
collapsable={false}
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
width: itemWidth,
|
width: getItemWidth(image),
|
||||||
aspectRatio: CONTAINER_ASPECT_RATIO,
|
height: containerHeight,
|
||||||
},
|
},
|
||||||
web({scrollSnapAlign: 'start'}),
|
|
||||||
]}
|
]}
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
aria-label={
|
aria-label={
|
||||||
@@ -216,96 +260,6 @@ export function Gallery({
|
|||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
{images.length > 1 && (
|
|
||||||
<>
|
|
||||||
<View
|
|
||||||
accessible={false}
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.rounded_full,
|
|
||||||
t.atoms.bg_contrast_975,
|
|
||||||
{
|
|
||||||
bottom: a.p_xs.padding,
|
|
||||||
left: a.p_xs.padding,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
opacity: 0.75,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.font_bold,
|
|
||||||
{fontSize: 11, color: t.atoms.text_inverted.color},
|
|
||||||
]}>
|
|
||||||
{currentPage + 1}/{images.length}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{currentPage > 0 && (
|
|
||||||
<Pressable
|
|
||||||
onPress={() => goToPage(currentPage - 1)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Previous image`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
a.rounded_full,
|
|
||||||
t.atoms.bg_contrast_975,
|
|
||||||
{
|
|
||||||
left: a.p_xs.padding,
|
|
||||||
top: '50%',
|
|
||||||
marginTop: -16,
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
opacity: 0.75,
|
|
||||||
},
|
|
||||||
web({cursor: 'pointer'}),
|
|
||||||
]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.font_bold,
|
|
||||||
a.text_md,
|
|
||||||
{color: t.atoms.text_inverted.color},
|
|
||||||
]}>
|
|
||||||
{'<'}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
{currentPage < images.length - 1 && (
|
|
||||||
<Pressable
|
|
||||||
onPress={() => goToPage(currentPage + 1)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Next image`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
a.rounded_full,
|
|
||||||
t.atoms.bg_contrast_975,
|
|
||||||
{
|
|
||||||
right: a.p_xs.padding,
|
|
||||||
top: '50%',
|
|
||||||
marginTop: -16,
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
opacity: 0.75,
|
|
||||||
},
|
|
||||||
web({cursor: 'pointer'}),
|
|
||||||
]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.font_bold,
|
|
||||||
a.text_md,
|
|
||||||
{color: t.atoms.text_inverted.color},
|
|
||||||
]}>
|
|
||||||
{'>'}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user