diff --git a/package.json b/package.json
index 51231cef06..66f6ddd0a6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
- "version": "1.120.0",
+ "version": "1.121.0",
"private": true,
"engines": {
"node": ">=20"
@@ -81,14 +81,15 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
- "@atproto/api": "^0.19.6",
+ "@atproto/api": "^0.19.8",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.7",
"@bsky.app/expo-image-crop-tool": "^0.5.0",
+ "@bsky.app/expo-scroll-edge-effect": "^0.1.4",
"@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/react-native-mmkv": "2.12.5",
- "@bsky.app/sift": "^0.3.1",
+ "@bsky.app/sift": "^0.3.2",
"@bsky.app/tapper": "^0.5.0",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/data": "^1.2.1",
@@ -142,7 +143,6 @@
"bcp-47-match": "^2.0.3",
"date-fns": "^2.30.0",
"email-validator": "^2.0.4",
- "embla-carousel-react": "^8.6.0",
"emoji-mart": "^5.6.0",
"emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1",
@@ -157,6 +157,7 @@
"expo-device": "~8.0.10",
"expo-file-system": "~19.0.21",
"expo-font": "~14.0.11",
+ "expo-glass-effect": "55.0.8",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-image-manipulator": "~14.0.8",
@@ -169,7 +170,7 @@
"expo-location": "~19.0.8",
"expo-media-library": "~18.2.1",
"expo-notifications": "~0.32.16",
- "expo-paste-input": "^0.1.12",
+ "expo-paste-input": "^0.1.15",
"expo-privacy-sensitive": "^0.1.0",
"expo-screen-orientation": "~9.0.8",
"expo-sharing": "~14.0.8",
@@ -214,7 +215,7 @@
"react-native-drawer-layout": "^4.2.2",
"react-native-edge-to-edge": "^1.6.0",
"react-native-gesture-handler": "~2.28.0",
- "react-native-keyboard-controller": "^1.21.0",
+ "react-native-keyboard-controller": "^1.21.5",
"react-native-pager-view": "6.8.0",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3",
@@ -246,7 +247,6 @@
"@babel/runtime": "^7.26.0",
"@crowdin/cli": "^4.14.1",
"@eslint/js": "^9.39.2",
- "@expo/config-plugins": "~54.0.4",
"@lingui/babel-plugin-lingui-macro": "^5.9.2",
"@lingui/cli": "^5.9.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx
index 28e7c859c4..0df31da625 100644
--- a/src/view/com/composer/photos/Gallery.tsx
+++ b/src/view/com/composer/photos/Gallery.tsx
@@ -1,12 +1,14 @@
-import {memo, useState} from 'react'
+import {memo, useMemo, useState} from 'react'
import {
findNodeHandle,
+ type ImageStyle,
Keyboard,
type LayoutChangeEvent,
Platform,
StyleSheet,
TouchableOpacity,
View,
+ type ViewStyle,
} from 'react-native'
import {Image} from 'expo-image'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -14,10 +16,10 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type Dimensions} from '#/lib/media/types'
import {colors} from '#/lib/styles'
import {type ComposerImage, cropImage} from '#/state/gallery'
-import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import * as Dialog from '#/components/Dialog'
@@ -30,7 +32,6 @@ import {EditImageDialog} from './EditImageDialog'
import {ImageAltTextDialog} from './ImageAltTextDialog'
const IMAGE_GAP = 8
-const CONTAINER_HEIGHT = 200
interface GalleryProps {
images: ComposerImage[]
@@ -62,32 +63,53 @@ interface GalleryInnerProps extends GalleryProps {
containerInfo: Dimensions
}
-const getItemWidth = (image: ComposerImage, height: number) => {
- const source = image.transformed ?? image.source
- if (source.width > 0 && source.height > 0) {
- const ratio = source.width / source.height
- const w = height * ratio
- // Clamp: at least 60% of height, at most 1.5x height
- return Math.max(height * 0.6, Math.min(w, height * 1.5))
- }
- return height
-}
+const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
+ const {isMobile} = useWebMediaQueries()
+
+ const {altTextControlStyle, imageControlsStyle, imageStyle} = useMemo(() => {
+ const side =
+ images.length === 1
+ ? 250
+ : (containerInfo.width - IMAGE_GAP * (images.length - 1)) /
+ images.length
+
+ const isOverflow = isMobile && images.length > 2
+
+ return {
+ altTextControlStyle: isOverflow
+ ? {left: 4, bottom: 4}
+ : !isMobile && images.length < 3
+ ? {left: 8, top: 8}
+ : {left: 4, top: 4},
+ imageControlsStyle: {
+ display: 'flex' as const,
+ flexDirection: 'row' as const,
+ position: 'absolute' as const,
+ ...(isOverflow
+ ? {top: 4, right: 4, gap: 4}
+ : !isMobile && images.length < 3
+ ? {top: 8, right: 8, gap: 8}
+ : {top: 4, right: 4, gap: 4}),
+ zIndex: 1,
+ },
+ imageStyle: {
+ height: side,
+ width: side,
+ },
+ }
+ }, [images.length, containerInfo, isMobile])
-const GalleryInner = ({images, dispatch}: GalleryInnerProps) => {
return images.length !== 0 ? (
<>
-
+
{images.map(image => {
return (
{
dispatch({type: 'embed_update_image', image: next})
}}
@@ -97,7 +119,7 @@ const GalleryInner = ({images, dispatch}: GalleryInnerProps) => {
/>
)
})}
-
+
{images.some(image => !image.alt) && (
@@ -112,16 +134,18 @@ const GalleryInner = ({images, dispatch}: GalleryInnerProps) => {
type GalleryItemProps = {
image: ComposerImage
- itemWidth: number
- itemHeight: number
+ altTextControlStyle?: ViewStyle
+ imageControlsStyle?: ViewStyle
+ imageStyle?: ImageStyle
onChange: (next: ComposerImage) => void
onRemove: () => void
}
const GalleryItem = ({
image,
- itemWidth,
- itemHeight,
+ altTextControlStyle,
+ imageControlsStyle,
+ imageStyle,
onChange,
onRemove,
}: GalleryItemProps): React.ReactNode => {
@@ -134,6 +158,7 @@ const GalleryItem = ({
const [altBtnViewTag, setAltBtnViewTag] = useState()
const altBtnRef = (node: View | null) => {
+ // for iOS 26 fluid transition
if (IS_IOS && node) {
const tag = findNodeHandle(node)
if (tag != null) setAltBtnViewTag(tag)
@@ -162,8 +187,33 @@ const GalleryItem = ({
return (
+
+ {image.alt.length !== 0 ? (
+
+ ) : (
+
+ )}
+
+ ALT
+
+
-
-
- {image.alt.length !== 0 ? (
-
- ) : (
-
- )}
-
- ALT
-
-
-