use embla for web
This commit is contained in:
@@ -140,6 +140,7 @@
|
|||||||
"bcp-47-match": "^2.0.3",
|
"bcp-47-match": "^2.0.3",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
"emoji-mart": "^5.6.0",
|
"emoji-mart": "^5.6.0",
|
||||||
"emoji-regex": "^10.4.0",
|
"emoji-regex": "^10.4.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import {useEffect, useRef, useState} from 'react'
|
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||||
import {Pressable, ScrollView, View} from 'react-native'
|
import {Pressable, 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'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
import useEmblaCarousel from 'embla-carousel-react'
|
||||||
|
|
||||||
import {type Dimensions} from '#/lib/media/types'
|
import {type Dimensions} from '#/lib/media/types'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
const CONTAINER_ASPECT_RATIO = 3 / 2
|
const CONTAINER_ASPECT_RATIO = 3 / 2
|
||||||
const ITEM_GAP = 8 // tokens.space.sm
|
const ITEM_GAP = 8
|
||||||
|
|
||||||
interface GalleryProps {
|
interface GalleryProps {
|
||||||
images: AppBskyEmbedImages.ViewImage[]
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
@@ -38,11 +39,11 @@ export function Gallery({
|
|||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const largeAltBadge = useLargeAltBadgeEnabled()
|
const largeAltBadge = useLargeAltBadgeEnabled()
|
||||||
const currentPageRef = useRef(0)
|
const currentPageRef = useRef(0)
|
||||||
const scrollRef = useRef<ScrollView>(null)
|
|
||||||
const containerRef = useRef<View>(null)
|
const containerRef = useRef<View>(null)
|
||||||
const [containerWidth, setContainerWidth] = useState(0)
|
const [containerWidth, setContainerWidth] = useState(0)
|
||||||
const [insetLeft, setInsetLeft] = useState(0)
|
const [insetLeft, setInsetLeft] = useState(0)
|
||||||
const [insetRight, setInsetRight] = useState(0)
|
const [insetRight, setInsetRight] = useState(0)
|
||||||
|
const insetLeftRef = useRef(0)
|
||||||
|
|
||||||
const containerRefs = useRef<AnimatedRef<any>[]>([]).current
|
const containerRefs = useRef<AnimatedRef<any>[]>([]).current
|
||||||
const thumbDimsRef = useRef<(Dimensions | null)[]>([])
|
const thumbDimsRef = useRef<(Dimensions | null)[]>([])
|
||||||
@@ -63,187 +64,29 @@ export function Gallery({
|
|||||||
const QUOTE_PADDING = 12
|
const QUOTE_PADDING = 12
|
||||||
const containerHeight =
|
const containerHeight =
|
||||||
containerWidth > 0 ? containerWidth / CONTAINER_ASPECT_RATIO : 0
|
containerWidth > 0 ? containerWidth / CONTAINER_ASPECT_RATIO : 0
|
||||||
const scrollWidth = isWithinQuote
|
|
||||||
? containerWidth + QUOTE_PADDING * 2
|
|
||||||
: insetLeft + insetRight > 0
|
|
||||||
? containerWidth + insetLeft + insetRight
|
|
||||||
: containerWidth
|
|
||||||
|
|
||||||
const getItemWidth = (image: AppBskyEmbedImages.ViewImage) => {
|
const getItemWidth = (image: AppBskyEmbedImages.ViewImage) => {
|
||||||
const ar = image.aspectRatio
|
const ar = image.aspectRatio
|
||||||
if (ar && ar.width > 0 && ar.height > 0) {
|
if (ar && ar.width > 0 && ar.height > 0) {
|
||||||
const ratio = ar.width / ar.height
|
const ratio = ar.width / ar.height
|
||||||
// Clamp aspect ratio between 2:3 (portrait) and 3:2 (landscape)
|
|
||||||
const clamped = Math.max(2 / 3, Math.min(ratio, 3 / 2))
|
const clamped = Math.max(2 / 3, Math.min(ratio, 3 / 2))
|
||||||
return containerHeight * clamped
|
return containerHeight * clamped
|
||||||
}
|
}
|
||||||
return containerHeight // default to square-ish
|
return containerHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click-and-drag scrolling via DOM listeners
|
// Embla carousel
|
||||||
const hasDragged = useRef(false)
|
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||||
|
align: () => insetLeftRef.current,
|
||||||
useEffect(() => {
|
containScroll: false,
|
||||||
const el = scrollRef.current as unknown as HTMLElement
|
dragFree: true,
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
let isDragging = false
|
|
||||||
let startX = 0
|
|
||||||
let scrollStart = 0
|
|
||||||
let prevX = 0
|
|
||||||
let prevTime = 0
|
|
||||||
let velocity = 0
|
|
||||||
let momentumId = 0
|
|
||||||
|
|
||||||
el.style.cursor = 'grab'
|
|
||||||
|
|
||||||
const onMouseDown = (e: MouseEvent) => {
|
|
||||||
if (e.button !== 0) return
|
|
||||||
cancelAnimationFrame(momentumId)
|
|
||||||
isDragging = true
|
|
||||||
hasDragged.current = false
|
|
||||||
startX = e.pageX - el.offsetLeft
|
|
||||||
scrollStart = el.scrollLeft
|
|
||||||
prevX = e.pageX
|
|
||||||
prevTime = Date.now()
|
|
||||||
velocity = 0
|
|
||||||
el.style.cursor = 'grabbing'
|
|
||||||
e.preventDefault() // prevents native image drag
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
|
||||||
if (!isDragging) return
|
|
||||||
const x = e.pageX - el.offsetLeft
|
|
||||||
if (Math.abs(x - startX) > 3) {
|
|
||||||
hasDragged.current = true
|
|
||||||
}
|
|
||||||
// Track velocity from recent movement
|
|
||||||
const now = Date.now()
|
|
||||||
const dt = now - prevTime
|
|
||||||
if (dt > 0) {
|
|
||||||
velocity = (e.pageX - prevX) / dt
|
|
||||||
}
|
|
||||||
prevX = e.pageX
|
|
||||||
prevTime = now
|
|
||||||
el.scrollLeft = scrollStart - (x - startX)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
|
||||||
if (!isDragging) return
|
|
||||||
if (hasDragged.current) {
|
|
||||||
el.addEventListener('click', e => e.stopPropagation(), {once: true})
|
|
||||||
}
|
|
||||||
isDragging = false
|
|
||||||
el.style.cursor = 'grab'
|
|
||||||
|
|
||||||
// Apply momentum with friction
|
|
||||||
const friction = 0.95
|
|
||||||
let v = -velocity * 15 // scale velocity to px/frame
|
|
||||||
const coast = () => {
|
|
||||||
if (Math.abs(v) < 0.5) return
|
|
||||||
el.scrollLeft += v
|
|
||||||
v *= friction
|
|
||||||
momentumId = requestAnimationFrame(coast)
|
|
||||||
}
|
|
||||||
coast()
|
|
||||||
}
|
|
||||||
|
|
||||||
el.addEventListener('mousedown', onMouseDown)
|
|
||||||
window.addEventListener('mousemove', onMouseMove)
|
|
||||||
window.addEventListener('mouseup', onMouseUp)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
el.removeEventListener('mousedown', onMouseDown)
|
|
||||||
window.removeEventListener('mousemove', onMouseMove)
|
|
||||||
window.removeEventListener('mouseup', onMouseUp)
|
|
||||||
}
|
|
||||||
}, [containerWidth]) // re-attach when scroll view mounts
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={
|
|
||||||
containerWidth > 0
|
|
||||||
? {height: containerHeight, overflow: 'visible'}
|
|
||||||
: {aspectRatio: CONTAINER_ASPECT_RATIO}
|
|
||||||
}
|
|
||||||
ref={containerRef}
|
|
||||||
onLayout={e => {
|
|
||||||
const w = e.nativeEvent.layout.width
|
|
||||||
if (w > 0) {
|
|
||||||
setContainerWidth(w)
|
|
||||||
}
|
|
||||||
// Measure distance to post edges for bleed
|
|
||||||
if (!isWithinQuote) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const el = containerRef.current as unknown as HTMLElement
|
|
||||||
if (!el) return
|
|
||||||
const galleryRect = el.getBoundingClientRect()
|
|
||||||
// Walk up to find the post outer container (has paddingLeft/paddingRight)
|
|
||||||
let parent: HTMLElement | null = el.parentElement
|
|
||||||
while (parent) {
|
|
||||||
const ps = window.getComputedStyle(parent)
|
|
||||||
const pl = parseFloat(ps.paddingLeft)
|
|
||||||
const pr = parseFloat(ps.paddingRight)
|
|
||||||
if (pl >= 8 && pr >= 8 && ps.cursor === 'pointer') {
|
|
||||||
const parentRect = parent.getBoundingClientRect()
|
|
||||||
setInsetLeft(galleryRect.left - parentRect.left)
|
|
||||||
setInsetRight(parentRect.right - galleryRect.right)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
parent = parent.parentElement
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}}
|
// Track page changes for analytics
|
||||||
role="group"
|
useEffect(() => {
|
||||||
aria-roledescription="carousel"
|
if (!emblaApi) return
|
||||||
aria-label={l`Image gallery, ${images.length} images`}>
|
const onSelect = () => {
|
||||||
{containerWidth > 0 && (
|
const page = emblaApi.selectedScrollSnap()
|
||||||
<ScrollView
|
|
||||||
ref={scrollRef}
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
scrollEventThrottle={16}
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
height: containerHeight,
|
|
||||||
width: scrollWidth,
|
|
||||||
marginLeft: isWithinQuote
|
|
||||||
? -QUOTE_PADDING
|
|
||||||
: insetLeft > 0
|
|
||||||
? -insetLeft
|
|
||||||
: 0,
|
|
||||||
},
|
|
||||||
web({
|
|
||||||
WebkitOverflowScrolling: 'touch',
|
|
||||||
}),
|
|
||||||
]}
|
|
||||||
contentContainerStyle={{
|
|
||||||
gap: ITEM_GAP,
|
|
||||||
paddingLeft: isWithinQuote
|
|
||||||
? QUOTE_PADDING
|
|
||||||
: insetLeft > 0
|
|
||||||
? insetLeft
|
|
||||||
: 0,
|
|
||||||
paddingRight: isWithinQuote
|
|
||||||
? QUOTE_PADDING
|
|
||||||
: insetRight > 0
|
|
||||||
? insetRight
|
|
||||||
: 0,
|
|
||||||
}}
|
|
||||||
onScroll={e => {
|
|
||||||
const offsetX = e.nativeEvent.contentOffset.x
|
|
||||||
let accumulated = 0
|
|
||||||
let page = 0
|
|
||||||
for (let i = 0; i < images.length; i++) {
|
|
||||||
const w = getItemWidth(images[i]) + ITEM_GAP
|
|
||||||
if (offsetX < accumulated + w / 2) {
|
|
||||||
page = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
accumulated += w
|
|
||||||
page = i
|
|
||||||
}
|
|
||||||
if (page !== currentPageRef.current) {
|
if (page !== currentPageRef.current) {
|
||||||
ax.metric('post:gallery:swipe', {
|
ax.metric('post:gallery:swipe', {
|
||||||
fromIndex: currentPageRef.current,
|
fromIndex: currentPageRef.current,
|
||||||
@@ -252,17 +95,142 @@ export function Gallery({
|
|||||||
})
|
})
|
||||||
currentPageRef.current = page
|
currentPageRef.current = page
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
emblaApi.on('select', onSelect)
|
||||||
|
return () => {
|
||||||
|
emblaApi.off('select', onSelect)
|
||||||
|
}
|
||||||
|
}, [emblaApi, ax, images.length])
|
||||||
|
|
||||||
|
// Re-initialize Embla when bleed measurements change
|
||||||
|
useEffect(() => {
|
||||||
|
if (emblaApi) emblaApi.reInit()
|
||||||
|
}, [emblaApi, insetLeft, insetRight])
|
||||||
|
|
||||||
|
// Suppress click after drag
|
||||||
|
const pointerDown = useRef(false)
|
||||||
|
const dragged = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!emblaApi) return
|
||||||
|
const root = emblaApi.rootNode()
|
||||||
|
|
||||||
|
const onPointerDown = () => {
|
||||||
|
pointerDown.current = true
|
||||||
|
dragged.current = false
|
||||||
|
}
|
||||||
|
const onPointerMove = () => {
|
||||||
|
if (pointerDown.current) dragged.current = true
|
||||||
|
}
|
||||||
|
const onPointerUp = () => {
|
||||||
|
pointerDown.current = false
|
||||||
|
}
|
||||||
|
const onClick = (e: MouseEvent) => {
|
||||||
|
if (dragged.current) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.addEventListener('pointerdown', onPointerDown)
|
||||||
|
root.addEventListener('pointermove', onPointerMove)
|
||||||
|
root.addEventListener('pointerup', onPointerUp)
|
||||||
|
root.addEventListener('click', onClick, true)
|
||||||
|
return () => {
|
||||||
|
root.removeEventListener('pointerdown', onPointerDown)
|
||||||
|
root.removeEventListener('pointermove', onPointerMove)
|
||||||
|
root.removeEventListener('pointerup', onPointerUp)
|
||||||
|
root.removeEventListener('click', onClick, true)
|
||||||
|
}
|
||||||
|
}, [emblaApi])
|
||||||
|
|
||||||
|
// Bleed measurement
|
||||||
|
const measureBleed = useCallback(() => {
|
||||||
|
if (isWithinQuote) return
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const el = containerRef.current as unknown as HTMLElement
|
||||||
|
if (!el) return
|
||||||
|
const galleryRect = el.getBoundingClientRect()
|
||||||
|
let parent: HTMLElement | null = el.parentElement
|
||||||
|
while (parent) {
|
||||||
|
const ps = window.getComputedStyle(parent)
|
||||||
|
const pl = parseFloat(ps.paddingLeft)
|
||||||
|
const pr = parseFloat(ps.paddingRight)
|
||||||
|
if (pl >= 8 && pr >= 8 && ps.cursor === 'pointer') {
|
||||||
|
const parentRect = parent.getBoundingClientRect()
|
||||||
|
const il = galleryRect.left - parentRect.left
|
||||||
|
insetLeftRef.current = il
|
||||||
|
setInsetLeft(il)
|
||||||
|
setInsetRight(parentRect.right - galleryRect.right)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parent = parent.parentElement
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [isWithinQuote])
|
||||||
|
|
||||||
|
const isBleed = !isWithinQuote && (insetLeft > 0 || insetRight > 0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
containerWidth > 0
|
||||||
|
? isWithinQuote
|
||||||
|
? {
|
||||||
|
height: containerHeight,
|
||||||
|
overflow: 'hidden' as const,
|
||||||
|
width: containerWidth + QUOTE_PADDING * 2,
|
||||||
|
marginLeft: -QUOTE_PADDING,
|
||||||
|
}
|
||||||
|
: {height: containerHeight, overflow: 'visible' as const}
|
||||||
|
: {aspectRatio: CONTAINER_ASPECT_RATIO}
|
||||||
|
}
|
||||||
|
ref={containerRef}
|
||||||
|
onLayout={e => {
|
||||||
|
const w = e.nativeEvent.layout.width
|
||||||
|
if (w > 0 && containerWidth === 0) setContainerWidth(w)
|
||||||
|
measureBleed()
|
||||||
|
}}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
aria-label={l`Image gallery, ${images.length} images`}>
|
||||||
|
{containerWidth > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: isBleed ? containerWidth + insetLeft + insetRight : '100%',
|
||||||
|
marginLeft: isBleed ? -insetLeft : 0,
|
||||||
|
height: containerHeight,
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
ref={emblaRef}
|
||||||
|
style={{
|
||||||
|
overflow: 'visible',
|
||||||
|
width: isBleed ? containerWidth + insetLeft : '100%',
|
||||||
|
height: containerHeight,
|
||||||
|
cursor: 'grab',
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: ITEM_GAP,
|
||||||
|
paddingLeft: isWithinQuote ? QUOTE_PADDING : 0,
|
||||||
|
paddingRight: isWithinQuote ? QUOTE_PADDING : 0,
|
||||||
|
height: containerHeight,
|
||||||
}}>
|
}}>
|
||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
<View
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
|
style={{
|
||||||
|
flex: `0 0 ${getItemWidth(image)}px`,
|
||||||
|
minWidth: 0,
|
||||||
|
height: containerHeight,
|
||||||
|
}}>
|
||||||
|
<View
|
||||||
ref={containerRefs[index]}
|
ref={containerRefs[index]}
|
||||||
collapsable={false}
|
collapsable={false}
|
||||||
style={[
|
style={[
|
||||||
{
|
{width: getItemWidth(image), height: containerHeight},
|
||||||
width: getItemWidth(image),
|
|
||||||
height: containerHeight,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
aria-label={
|
aria-label={
|
||||||
@@ -272,7 +240,7 @@ export function Gallery({
|
|||||||
onPress={
|
onPress={
|
||||||
onPress
|
onPress
|
||||||
? () => {
|
? () => {
|
||||||
if (hasDragged.current) return
|
if (dragged.current) return
|
||||||
ax.metric('post:gallery:openLightbox', {
|
ax.metric('post:gallery:openLightbox', {
|
||||||
imageIndex: index,
|
imageIndex: index,
|
||||||
totalImages: images.length,
|
totalImages: images.length,
|
||||||
@@ -346,8 +314,11 @@ export function Gallery({
|
|||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ function Inner(props: ReportDialogProps) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}, [
|
}, [
|
||||||
props,
|
props.subject,
|
||||||
allLabelers,
|
allLabelers,
|
||||||
state.selectedOption,
|
state.selectedOption,
|
||||||
isBskyOnlyReason,
|
isBskyOnlyReason,
|
||||||
@@ -241,7 +241,17 @@ function Inner(props: ReportDialogProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setPending(false)
|
setPending(false)
|
||||||
}
|
}
|
||||||
}, [_, submitReport, state, dispatch, props, setPending, setSuccess])
|
}, [
|
||||||
|
_,
|
||||||
|
submitReport,
|
||||||
|
state,
|
||||||
|
dispatch,
|
||||||
|
props.subject,
|
||||||
|
props.control,
|
||||||
|
props.onAfterSubmit,
|
||||||
|
setPending,
|
||||||
|
setSuccess,
|
||||||
|
])
|
||||||
|
|
||||||
useCallOnce(() => {
|
useCallOnce(() => {
|
||||||
ax.metric('reportDialog:open', {
|
ax.metric('reportDialog:open', {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Keyboard,
|
Keyboard,
|
||||||
type LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
@@ -18,6 +17,7 @@ import {Trans} from '@lingui/react/macro'
|
|||||||
import {type Dimensions} from '#/lib/media/types'
|
import {type Dimensions} from '#/lib/media/types'
|
||||||
import {colors} from '#/lib/styles'
|
import {colors} from '#/lib/styles'
|
||||||
import {type ComposerImage, cropImage} from '#/state/gallery'
|
import {type ComposerImage, cropImage} from '#/state/gallery'
|
||||||
|
import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
|
||||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
@@ -76,9 +76,8 @@ const getItemWidth = (image: ComposerImage, height: number) => {
|
|||||||
const GalleryInner = ({images, dispatch}: GalleryInnerProps) => {
|
const GalleryInner = ({images, dispatch}: GalleryInnerProps) => {
|
||||||
return images.length !== 0 ? (
|
return images.length !== 0 ? (
|
||||||
<>
|
<>
|
||||||
<ScrollView
|
<DraggableScrollView
|
||||||
testID="selectedPhotosView"
|
testID="selectedPhotosView"
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={{gap: IMAGE_GAP, paddingTop: 16}}
|
contentContainerStyle={{gap: IMAGE_GAP, paddingTop: 16}}
|
||||||
style={{height: CONTAINER_HEIGHT + 16}}>
|
style={{height: CONTAINER_HEIGHT + 16}}>
|
||||||
@@ -98,7 +97,7 @@ const GalleryInner = ({images, dispatch}: GalleryInnerProps) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ScrollView>
|
</DraggableScrollView>
|
||||||
{images.some(image => !image.alt) && (
|
{images.some(image => !image.alt) && (
|
||||||
<Admonition type="info" style={[a.mt_sm]}>
|
<Admonition type="info" style={[a.mt_sm]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
|
|||||||
@@ -2174,7 +2174,7 @@
|
|||||||
"@babel/parser" "^7.28.6"
|
"@babel/parser" "^7.28.6"
|
||||||
"@babel/types" "^7.28.6"
|
"@babel/types" "^7.28.6"
|
||||||
|
|
||||||
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3":
|
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
||||||
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
||||||
@@ -2235,19 +2235,6 @@
|
|||||||
debug "^4.3.1"
|
debug "^4.3.1"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
|
|
||||||
"@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
|
|
||||||
version "7.25.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
|
||||||
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/code-frame" "^7.25.9"
|
|
||||||
"@babel/generator" "^7.25.9"
|
|
||||||
"@babel/parser" "^7.25.9"
|
|
||||||
"@babel/template" "^7.25.9"
|
|
||||||
"@babel/types" "^7.25.9"
|
|
||||||
debug "^4.3.1"
|
|
||||||
globals "^11.1.0"
|
|
||||||
|
|
||||||
"@babel/traverse@^7.26.10":
|
"@babel/traverse@^7.26.10":
|
||||||
version "7.26.10"
|
version "7.26.10"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
|
||||||
@@ -8061,6 +8048,24 @@ email-validator@^2.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed"
|
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed"
|
||||||
integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==
|
integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==
|
||||||
|
|
||||||
|
embla-carousel-react@^8.6.0:
|
||||||
|
version "8.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz#b737042a32761c38d6614593653b3ac619477bd1"
|
||||||
|
integrity sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==
|
||||||
|
dependencies:
|
||||||
|
embla-carousel "8.6.0"
|
||||||
|
embla-carousel-reactive-utils "8.6.0"
|
||||||
|
|
||||||
|
embla-carousel-reactive-utils@8.6.0:
|
||||||
|
version "8.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz#607f1d8ab9921c906a555c206251b2c6db687223"
|
||||||
|
integrity sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==
|
||||||
|
|
||||||
|
embla-carousel@8.6.0:
|
||||||
|
version "8.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.6.0.tgz#abcedff2bff36992ea8ac27cd30080ca5b6a3f58"
|
||||||
|
integrity sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==
|
||||||
|
|
||||||
emittery@^0.13.1:
|
emittery@^0.13.1:
|
||||||
version "0.13.1"
|
version "0.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
|
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
|
||||||
@@ -15218,16 +15223,7 @@ string-length@^5.0.1:
|
|||||||
char-regex "^2.0.0"
|
char-regex "^2.0.0"
|
||||||
strip-ansi "^7.0.1"
|
strip-ansi "^7.0.1"
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -15372,7 +15368,7 @@ string_decoder@~1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@@ -15386,13 +15382,6 @@ strip-ansi@^5.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^4.1.0"
|
ansi-regex "^4.1.0"
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^7.0.1:
|
strip-ansi@^7.0.1:
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||||
@@ -16542,7 +16531,7 @@ wonka@^6.3.2:
|
|||||||
resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.3.4.tgz#76eb9316e3d67d7febf4945202b5bdb2db534594"
|
resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.3.4.tgz#76eb9316e3d67d7febf4945202b5bdb2db534594"
|
||||||
integrity sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==
|
integrity sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
@@ -16560,15 +16549,6 @@ wrap-ansi@^6.2.0:
|
|||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
|
|||||||
Reference in New Issue
Block a user