Add alt text support and rework image layout (#503)
* Add alt text support and rework image layout * Add additional BottomSheet implementation to account for nested Composer modal * Use mobile gallery layout on mobile web * Missing key * Fix lint * Move altimage modal into the standard modal system * Fix overflow wrapping of images * Fixes to the alt-image modal * Remove unnecessary switch * Restore old imagelayoutgrid code --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>zio/stable
parent
0f5735b616
commit
f0706dbe9f
|
@ -64,7 +64,7 @@
|
||||||
"expo-build-properties": "~0.5.1",
|
"expo-build-properties": "~0.5.1",
|
||||||
"expo-camera": "~13.2.1",
|
"expo-camera": "~13.2.1",
|
||||||
"expo-dev-client": "~2.1.1",
|
"expo-dev-client": "~2.1.1",
|
||||||
"expo-image": "~1.0.0",
|
"expo-image": "^1.2.1",
|
||||||
"expo-image-picker": "~14.1.1",
|
"expo-image-picker": "~14.1.1",
|
||||||
"expo-localization": "~14.1.1",
|
"expo-localization": "~14.1.1",
|
||||||
"expo-media-library": "~15.2.3",
|
"expo-media-library": "~15.2.3",
|
||||||
|
|
|
@ -10,15 +10,15 @@ import {
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri} from '@atproto/api'
|
||||||
import {RootStoreModel} from 'state/models/root-store'
|
import {RootStoreModel} from 'state/models/root-store'
|
||||||
import {isNetworkError} from 'lib/strings/errors'
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
import {Image} from 'lib/media/types'
|
|
||||||
import {LinkMeta} from '../link-meta/link-meta'
|
import {LinkMeta} from '../link-meta/link-meta'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {ImageModel} from 'state/models/media/image'
|
||||||
|
|
||||||
export interface ExternalEmbedDraft {
|
export interface ExternalEmbedDraft {
|
||||||
uri: string
|
uri: string
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
meta?: LinkMeta
|
meta?: LinkMeta
|
||||||
localThumb?: Image
|
localThumb?: ImageModel
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveName(store: RootStoreModel, didOrHandle: string) {
|
export async function resolveName(store: RootStoreModel, didOrHandle: string) {
|
||||||
|
@ -61,7 +61,7 @@ interface PostOpts {
|
||||||
cid: string
|
cid: string
|
||||||
}
|
}
|
||||||
extLink?: ExternalEmbedDraft
|
extLink?: ExternalEmbedDraft
|
||||||
images?: string[]
|
images?: ImageModel[]
|
||||||
knownHandles?: Set<string>
|
knownHandles?: Set<string>
|
||||||
onStateChange?: (state: string) => void
|
onStateChange?: (state: string) => void
|
||||||
}
|
}
|
||||||
|
@ -109,10 +109,11 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
const images: AppBskyEmbedImages.Image[] = []
|
const images: AppBskyEmbedImages.Image[] = []
|
||||||
for (const image of opts.images) {
|
for (const image of opts.images) {
|
||||||
opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
|
opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
|
||||||
const res = await uploadBlob(store, image, 'image/jpeg')
|
const path = image.compressed?.path ?? image.path
|
||||||
|
const res = await uploadBlob(store, path, 'image/jpeg')
|
||||||
images.push({
|
images.push({
|
||||||
image: res.data.blob,
|
image: res.data.blob,
|
||||||
alt: '', // TODO supply alt text
|
alt: image.altText ?? '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,10 @@ export const FEEDBACK_FORM_URL =
|
||||||
export const MAX_DISPLAY_NAME = 64
|
export const MAX_DISPLAY_NAME = 64
|
||||||
export const MAX_DESCRIPTION = 256
|
export const MAX_DESCRIPTION = 256
|
||||||
|
|
||||||
|
// Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html
|
||||||
|
// but adding buffer room to account for languages like German
|
||||||
|
export const MAX_ALT_TEXT = 120
|
||||||
|
|
||||||
export const PROD_TEAM_HANDLES = [
|
export const PROD_TEAM_HANDLES = [
|
||||||
'jay.bsky.social',
|
'jay.bsky.social',
|
||||||
'pfrazee.com',
|
'pfrazee.com',
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
|
||||||
|
export async function openAltTextModal(store: RootStoreModel): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
store.shell.openModal({
|
||||||
|
name: 'alt-text-image',
|
||||||
|
onAltTextSet: (altText?: string) => {
|
||||||
|
if (altText) {
|
||||||
|
resolve(altText)
|
||||||
|
} else {
|
||||||
|
reject(new Error('Canceled'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -65,6 +65,10 @@ export class GalleryModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAltText(image: ImageModel) {
|
||||||
|
image.setAltText()
|
||||||
|
}
|
||||||
|
|
||||||
crop(image: ImageModel) {
|
crop(image: ImageModel) {
|
||||||
image.crop()
|
image.crop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {openCropper} from 'lib/media/picker'
|
import {openCropper} from 'lib/media/picker'
|
||||||
import {POST_IMG_MAX} from 'lib/constants'
|
import {POST_IMG_MAX} from 'lib/constants'
|
||||||
import {scaleDownDimensions} from 'lib/media/util'
|
import {scaleDownDimensions} from 'lib/media/util'
|
||||||
|
import {openAltTextModal} from 'lib/media/alt-text'
|
||||||
|
|
||||||
// TODO: EXIF embed
|
// TODO: EXIF embed
|
||||||
// Cases to consider: ExternalEmbed
|
// Cases to consider: ExternalEmbed
|
||||||
|
@ -14,6 +15,7 @@ export class ImageModel implements RNImage {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
size: number
|
size: number
|
||||||
|
altText?: string = undefined
|
||||||
cropped?: RNImage = undefined
|
cropped?: RNImage = undefined
|
||||||
compressed?: RNImage = undefined
|
compressed?: RNImage = undefined
|
||||||
scaledWidth: number = POST_IMG_MAX.width
|
scaledWidth: number = POST_IMG_MAX.width
|
||||||
|
@ -41,6 +43,18 @@ export class ImageModel implements RNImage {
|
||||||
this.scaledHeight = height
|
this.scaledHeight = height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setAltText() {
|
||||||
|
try {
|
||||||
|
const altText = await openAltTextModal(this.rootStore)
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.altText = altText
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
this.rootStore.log.error('Failed to set alt text', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async crop() {
|
async crop() {
|
||||||
try {
|
try {
|
||||||
const cropped = await openCropper(this.rootStore, {
|
const cropped = await openCropper(this.rootStore, {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {RootStoreModel} from '../root-store'
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {ProfileModel} from '../content/profile'
|
import {ProfileModel} from '../content/profile'
|
||||||
import {isObj, hasProp} from 'lib/type-guards'
|
import {isObj, hasProp} from 'lib/type-guards'
|
||||||
import {Image} from 'lib/media/types'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
|
|
||||||
export interface ConfirmModal {
|
export interface ConfirmModal {
|
||||||
name: 'confirm'
|
name: 'confirm'
|
||||||
|
@ -38,7 +38,12 @@ export interface ReportAccountModal {
|
||||||
export interface CropImageModal {
|
export interface CropImageModal {
|
||||||
name: 'crop-image'
|
name: 'crop-image'
|
||||||
uri: string
|
uri: string
|
||||||
onSelect: (img?: Image) => void
|
onSelect: (img?: RNImage) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AltTextImageModal {
|
||||||
|
name: 'alt-text-image'
|
||||||
|
onAltTextSet: (altText?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteAccountModal {
|
export interface DeleteAccountModal {
|
||||||
|
@ -70,18 +75,30 @@ export interface ContentFilteringSettingsModal {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Modal =
|
export type Modal =
|
||||||
| ConfirmModal
|
// Account
|
||||||
| EditProfileModal
|
|
||||||
| ServerInputModal
|
|
||||||
| ReportPostModal
|
|
||||||
| ReportAccountModal
|
|
||||||
| CropImageModal
|
|
||||||
| DeleteAccountModal
|
|
||||||
| RepostModal
|
|
||||||
| ChangeHandleModal
|
| ChangeHandleModal
|
||||||
|
| DeleteAccountModal
|
||||||
|
| EditProfileModal
|
||||||
|
|
||||||
|
// Curation
|
||||||
|
| ContentFilteringSettingsModal
|
||||||
|
|
||||||
|
// Reporting
|
||||||
|
| ReportAccountModal
|
||||||
|
| ReportPostModal
|
||||||
|
|
||||||
|
// Posting
|
||||||
|
| AltTextImageModal
|
||||||
|
| CropImageModal
|
||||||
|
| ServerInputModal
|
||||||
|
| RepostModal
|
||||||
|
|
||||||
|
// Bluesky access
|
||||||
| WaitlistModal
|
| WaitlistModal
|
||||||
| InviteCodesModal
|
| InviteCodesModal
|
||||||
| ContentFilteringSettingsModal
|
|
||||||
|
// Generic
|
||||||
|
| ConfirmModal
|
||||||
|
|
||||||
interface LightboxModel {}
|
interface LightboxModel {}
|
||||||
|
|
||||||
|
|
|
@ -142,7 +142,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
await apilib.post(store, {
|
await apilib.post(store, {
|
||||||
rawText: rt.text,
|
rawText: rt.text,
|
||||||
replyTo: replyTo?.uri,
|
replyTo: replyTo?.uri,
|
||||||
images: gallery.paths,
|
images: gallery.images,
|
||||||
quote: quote,
|
quote: quote,
|
||||||
extLink: extLink,
|
extLink: extLink,
|
||||||
onStateChange: setProcessingState,
|
onStateChange: setProcessingState,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, {useCallback} from 'react'
|
import React, {useCallback} from 'react'
|
||||||
|
import {ImageStyle, Keyboard} from 'react-native'
|
||||||
import {GalleryModel} from 'state/models/media/gallery'
|
import {GalleryModel} from 'state/models/media/gallery'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
@ -6,6 +7,8 @@ import {colors} from 'lib/styles'
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {ImageModel} from 'state/models/media/image'
|
import {ImageModel} from 'state/models/media/image'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
gallery: GalleryModel
|
gallery: GalleryModel
|
||||||
|
@ -13,17 +16,28 @@ interface Props {
|
||||||
|
|
||||||
export const Gallery = observer(function ({gallery}: Props) {
|
export const Gallery = observer(function ({gallery}: Props) {
|
||||||
const getImageStyle = useCallback(() => {
|
const getImageStyle = useCallback(() => {
|
||||||
switch (gallery.size) {
|
let side: number
|
||||||
case 1:
|
|
||||||
return styles.image250
|
if (gallery.size === 1) {
|
||||||
case 2:
|
side = 250
|
||||||
return styles.image175
|
} else {
|
||||||
default:
|
side = (isDesktopWeb ? 560 : 350) / gallery.size
|
||||||
return styles.image85
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: side,
|
||||||
|
width: side,
|
||||||
}
|
}
|
||||||
}, [gallery])
|
}, [gallery])
|
||||||
|
|
||||||
const imageStyle = getImageStyle()
|
const imageStyle = getImageStyle()
|
||||||
|
const handleAddImageAltText = useCallback(
|
||||||
|
(image: ImageModel) => {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
gallery.setAltText(image)
|
||||||
|
},
|
||||||
|
[gallery],
|
||||||
|
)
|
||||||
const handleRemovePhoto = useCallback(
|
const handleRemovePhoto = useCallback(
|
||||||
(image: ImageModel) => {
|
(image: ImageModel) => {
|
||||||
gallery.remove(image)
|
gallery.remove(image)
|
||||||
|
@ -38,14 +52,68 @@ export const Gallery = observer(function ({gallery}: Props) {
|
||||||
[gallery],
|
[gallery],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isOverflow = !isDesktopWeb && gallery.size > 2
|
||||||
|
|
||||||
|
const imageControlLabelStyle = {
|
||||||
|
borderRadius: 5,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
position: 'absolute' as const,
|
||||||
|
width: 46,
|
||||||
|
zIndex: 1,
|
||||||
|
...(isOverflow
|
||||||
|
? {
|
||||||
|
left: 4,
|
||||||
|
bottom: 4,
|
||||||
|
}
|
||||||
|
: isDesktopWeb && gallery.size < 3
|
||||||
|
? {
|
||||||
|
left: 8,
|
||||||
|
top: 8,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
left: 4,
|
||||||
|
top: 4,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageControlsSubgroupStyle = {
|
||||||
|
display: 'flex' as const,
|
||||||
|
flexDirection: 'row' as const,
|
||||||
|
position: 'absolute' as const,
|
||||||
|
...(isOverflow
|
||||||
|
? {
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
gap: 4,
|
||||||
|
}
|
||||||
|
: isDesktopWeb && gallery.size < 3
|
||||||
|
? {
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
gap: 8,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
gap: 4,
|
||||||
|
}),
|
||||||
|
zIndex: 1,
|
||||||
|
}
|
||||||
|
|
||||||
return !gallery.isEmpty ? (
|
return !gallery.isEmpty ? (
|
||||||
<View testID="selectedPhotosView" style={styles.gallery}>
|
<View testID="selectedPhotosView" style={styles.gallery}>
|
||||||
{gallery.images.map(image =>
|
{gallery.images.map(image =>
|
||||||
image.compressed !== undefined ? (
|
image.compressed !== undefined ? (
|
||||||
<View
|
<View key={`selected-image-${image.path}`} style={[imageStyle]}>
|
||||||
key={`selected-image-${image.path}`}
|
<TouchableOpacity
|
||||||
style={[styles.imageContainer, imageStyle]}>
|
testID="altTextButton"
|
||||||
<View style={styles.imageControls}>
|
onPress={() => {
|
||||||
|
handleAddImageAltText(image)
|
||||||
|
}}
|
||||||
|
style={[styles.imageControl, imageControlLabelStyle]}>
|
||||||
|
<Text style={styles.imageControlTextContent}>ALT</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={imageControlsSubgroupStyle}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="cropPhotoButton"
|
testID="cropPhotoButton"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
@ -72,7 +140,7 @@ export const Gallery = observer(function ({gallery}: Props) {
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
testID="selectedPhotoImage"
|
testID="selectedPhotoImage"
|
||||||
style={[styles.image, imageStyle]}
|
style={[styles.image, imageStyle] as ImageStyle}
|
||||||
source={{
|
source={{
|
||||||
uri: image.compressed.path,
|
uri: image.compressed.path,
|
||||||
}}
|
}}
|
||||||
|
@ -88,36 +156,13 @@ const styles = StyleSheet.create({
|
||||||
gallery: {
|
gallery: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
},
|
},
|
||||||
imageContainer: {
|
|
||||||
margin: 2,
|
|
||||||
},
|
|
||||||
image: {
|
image: {
|
||||||
resizeMode: 'cover',
|
resizeMode: 'cover',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
image250: {
|
|
||||||
width: 250,
|
|
||||||
height: 250,
|
|
||||||
},
|
|
||||||
image175: {
|
|
||||||
width: 175,
|
|
||||||
height: 175,
|
|
||||||
},
|
|
||||||
image85: {
|
|
||||||
width: 85,
|
|
||||||
height: 85,
|
|
||||||
},
|
|
||||||
imageControls: {
|
|
||||||
position: 'absolute',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 4,
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
imageControl: {
|
imageControl: {
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
|
@ -127,4 +172,10 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
imageControlTextContent: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import React, {useCallback, useState} from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {TextInput} from './util'
|
||||||
|
import {gradients, s} from 'lib/styles'
|
||||||
|
import {enforceLen} from 'lib/strings/helpers'
|
||||||
|
import {MAX_ALT_TEXT} from 'lib/constants'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {TouchableOpacity} from 'react-native-gesture-handler'
|
||||||
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
|
||||||
|
export const snapPoints = [330]
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onAltTextSet: (altText?: string | undefined) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Component({onAltTextSet}: Props) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const store = useStores()
|
||||||
|
const theme = useTheme()
|
||||||
|
const [altText, setAltText] = useState('')
|
||||||
|
|
||||||
|
const onPressSave = useCallback(() => {
|
||||||
|
onAltTextSet(altText)
|
||||||
|
store.shell.closeModal()
|
||||||
|
}, [store, altText, onAltTextSet])
|
||||||
|
|
||||||
|
const onPressCancel = () => {
|
||||||
|
store.shell.closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View testID="altTextImageModal" style={[pal.view, styles.container]}>
|
||||||
|
<Text style={[styles.title, pal.text]}>Add alt text</Text>
|
||||||
|
<TextInput
|
||||||
|
testID="altTextImageInput"
|
||||||
|
style={[styles.textArea, pal.border, pal.text]}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
multiline
|
||||||
|
value={altText}
|
||||||
|
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
||||||
|
/>
|
||||||
|
<View style={styles.buttonControls}>
|
||||||
|
<TouchableOpacity testID="altTextImageSaveBtn" onPress={onPressSave}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
|
start={{x: 0, y: 0}}
|
||||||
|
end={{x: 1, y: 1}}
|
||||||
|
style={[styles.button]}>
|
||||||
|
<Text type="button-lg" style={[s.white, s.bold]}>
|
||||||
|
Save
|
||||||
|
</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="altTextImageCancelBtn"
|
||||||
|
onPress={onPressCancel}>
|
||||||
|
<View style={[styles.button]}>
|
||||||
|
<Text type="button-lg" style={[pal.textLight]}>
|
||||||
|
Cancel
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
gap: 18,
|
||||||
|
bottom: 0,
|
||||||
|
paddingVertical: 18,
|
||||||
|
paddingHorizontal: isDesktopWeb ? 0 : 12,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 24,
|
||||||
|
},
|
||||||
|
textArea: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingTop: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
height: 100,
|
||||||
|
textAlignVertical: 'top',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 32,
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
buttonControls: {
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,5 +1,5 @@
|
||||||
import React, {useRef, useEffect} from 'react'
|
import React, {useRef, useEffect} from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import BottomSheet from '@gorhom/bottom-sheet'
|
import BottomSheet from '@gorhom/bottom-sheet'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
|
@ -11,6 +11,7 @@ import * as EditProfileModal from './EditProfile'
|
||||||
import * as ServerInputModal from './ServerInput'
|
import * as ServerInputModal from './ServerInput'
|
||||||
import * as ReportPostModal from './ReportPost'
|
import * as ReportPostModal from './ReportPost'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
|
import * as AltImageModal from './AltImage'
|
||||||
import * as ReportAccountModal from './ReportAccount'
|
import * as ReportAccountModal from './ReportAccount'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as ChangeHandleModal from './ChangeHandle'
|
import * as ChangeHandleModal from './ChangeHandle'
|
||||||
|
@ -68,6 +69,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
} else if (activeModal?.name === 'repost') {
|
} else if (activeModal?.name === 'repost') {
|
||||||
snapPoints = RepostModal.snapPoints
|
snapPoints = RepostModal.snapPoints
|
||||||
element = <RepostModal.Component {...activeModal} />
|
element = <RepostModal.Component {...activeModal} />
|
||||||
|
} else if (activeModal?.name === 'alt-text-image') {
|
||||||
|
snapPoints = AltImageModal.snapPoints
|
||||||
|
element = <AltImageModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'change-handle') {
|
} else if (activeModal?.name === 'change-handle') {
|
||||||
snapPoints = ChangeHandleModal.snapPoints
|
snapPoints = ChangeHandleModal.snapPoints
|
||||||
element = <ChangeHandleModal.Component {...activeModal} />
|
element = <ChangeHandleModal.Component {...activeModal} />
|
||||||
|
@ -81,7 +85,7 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
snapPoints = ContentFilteringSettingsModal.snapPoints
|
snapPoints = ContentFilteringSettingsModal.snapPoints
|
||||||
element = <ContentFilteringSettingsModal.Component />
|
element = <ContentFilteringSettingsModal.Component />
|
||||||
} else {
|
} else {
|
||||||
return <View />
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -14,6 +14,7 @@ import * as ReportAccountModal from './ReportAccount'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
import * as CropImageModal from './crop-image/CropImage.web'
|
import * as CropImageModal from './crop-image/CropImage.web'
|
||||||
|
import * as AltTextImageModal from './AltImage'
|
||||||
import * as ChangeHandleModal from './ChangeHandle'
|
import * as ChangeHandleModal from './ChangeHandle'
|
||||||
import * as WaitlistModal from './Waitlist'
|
import * as WaitlistModal from './Waitlist'
|
||||||
import * as InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
|
@ -78,6 +79,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
element = <InviteCodesModal.Component />
|
element = <InviteCodesModal.Component />
|
||||||
} else if (modal.name === 'content-filtering-settings') {
|
} else if (modal.name === 'content-filtering-settings') {
|
||||||
element = <ContentFilteringSettingsModal.Component />
|
element = <ContentFilteringSettingsModal.Component />
|
||||||
|
} else if (modal.name === 'alt-text-image') {
|
||||||
|
element = <AltTextImageModal.Component {...modal} />
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -369,10 +369,7 @@ function AdditionalPostText({
|
||||||
<>
|
<>
|
||||||
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
|
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
|
||||||
{images && images?.length > 0 && (
|
{images && images?.length > 0 && (
|
||||||
<ImageHorzList
|
<ImageHorzList images={images} style={styles.additionalPostImages} />
|
||||||
uris={images?.map(img => img.thumb)}
|
|
||||||
style={styles.additionalPostImages}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,29 +9,33 @@ import {
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {clamp} from 'lib/numbers'
|
import {clamp} from 'lib/numbers'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {Dim} from 'lib/media/manip'
|
import {Dimensions} from 'lib/media/types'
|
||||||
|
|
||||||
export const DELAY_PRESS_IN = 500
|
export const DELAY_PRESS_IN = 500
|
||||||
const MIN_ASPECT_RATIO = 0.33 // 1/3
|
const MIN_ASPECT_RATIO = 0.33 // 1/3
|
||||||
const MAX_ASPECT_RATIO = 5 // 5/1
|
const MAX_ASPECT_RATIO = 5 // 5/1
|
||||||
|
|
||||||
export function AutoSizedImage({
|
interface Props {
|
||||||
uri,
|
alt?: string
|
||||||
onPress,
|
|
||||||
onLongPress,
|
|
||||||
onPressIn,
|
|
||||||
style,
|
|
||||||
children = null,
|
|
||||||
}: {
|
|
||||||
uri: string
|
uri: string
|
||||||
onPress?: () => void
|
onPress?: () => void
|
||||||
onLongPress?: () => void
|
onLongPress?: () => void
|
||||||
onPressIn?: () => void
|
onPressIn?: () => void
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
export function AutoSizedImage({
|
||||||
|
alt,
|
||||||
|
uri,
|
||||||
|
onPress,
|
||||||
|
onLongPress,
|
||||||
|
onPressIn,
|
||||||
|
style,
|
||||||
|
children = null,
|
||||||
|
}: Props) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [dim, setDim] = React.useState<Dim | undefined>(
|
const [dim, setDim] = React.useState<Dimensions | undefined>(
|
||||||
store.imageSizes.get(uri),
|
store.imageSizes.get(uri),
|
||||||
)
|
)
|
||||||
const [aspectRatio, setAspectRatio] = React.useState<number>(
|
const [aspectRatio, setAspectRatio] = React.useState<number>(
|
||||||
|
@ -59,20 +63,31 @@ export function AutoSizedImage({
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
delayPressIn={DELAY_PRESS_IN}
|
delayPressIn={DELAY_PRESS_IN}
|
||||||
style={[styles.container, style]}>
|
style={[styles.container, style]}>
|
||||||
<Image style={[styles.image, {aspectRatio}]} source={uri} />
|
<Image
|
||||||
|
style={[styles.image, {aspectRatio}]}
|
||||||
|
source={uri}
|
||||||
|
accessible={true} // Must set for `accessibilityLabel` to work
|
||||||
|
accessibilityLabel={alt}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, style]}>
|
<View style={[styles.container, style]}>
|
||||||
<Image style={[styles.image, {aspectRatio}]} source={{uri}} />
|
<Image
|
||||||
|
style={[styles.image, {aspectRatio}]}
|
||||||
|
source={{uri}}
|
||||||
|
accessible={true} // Must set for `accessibilityLabel` to work
|
||||||
|
accessibilityLabel={alt}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function calc(dim: Dim) {
|
function calc(dim: Dimensions) {
|
||||||
if (dim.width === 0 || dim.height === 0) {
|
if (dim.width === 0 || dim.height === 0) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,21 +7,25 @@ import {
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
|
import {AppBskyEmbedImages} from '@atproto/api'
|
||||||
|
|
||||||
export function ImageHorzList({
|
interface Props {
|
||||||
uris,
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
onPress,
|
|
||||||
style,
|
|
||||||
}: {
|
|
||||||
uris: string[]
|
|
||||||
onPress?: (index: number) => void
|
onPress?: (index: number) => void
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
export function ImageHorzList({images, onPress, style}: Props) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.flexRow, style]}>
|
<View style={[styles.flexRow, style]}>
|
||||||
{uris.map((uri, i) => (
|
{images.map(({thumb, alt}, i) => (
|
||||||
<TouchableWithoutFeedback key={i} onPress={() => onPress?.(i)}>
|
<TouchableWithoutFeedback key={i} onPress={() => onPress?.(i)}>
|
||||||
<Image source={{uri}} style={styles.image} />
|
<Image
|
||||||
|
source={{uri: thumb}}
|
||||||
|
style={styles.image}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={alt}
|
||||||
|
/>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -9,26 +9,25 @@ import {
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {Image, ImageStyle} from 'expo-image'
|
import {Image, ImageStyle} from 'expo-image'
|
||||||
import {Dimensions} from 'lib/media/types'
|
import {Dimensions} from 'lib/media/types'
|
||||||
|
import {AppBskyEmbedImages} from '@atproto/api'
|
||||||
|
|
||||||
export const DELAY_PRESS_IN = 500
|
export const DELAY_PRESS_IN = 500
|
||||||
|
|
||||||
export type ImageLayoutGridType = number
|
interface ImageLayoutGridProps {
|
||||||
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
export function ImageLayoutGrid({
|
|
||||||
type,
|
|
||||||
uris,
|
|
||||||
onPress,
|
|
||||||
onLongPress,
|
|
||||||
onPressIn,
|
|
||||||
style,
|
|
||||||
}: {
|
|
||||||
type: ImageLayoutGridType
|
|
||||||
uris: string[]
|
|
||||||
onPress?: (index: number) => void
|
onPress?: (index: number) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
export function ImageLayoutGrid({
|
||||||
|
images,
|
||||||
|
onPress,
|
||||||
|
onLongPress,
|
||||||
|
onPressIn,
|
||||||
|
style,
|
||||||
|
}: ImageLayoutGridProps) {
|
||||||
const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>()
|
const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>()
|
||||||
|
|
||||||
const onLayout = (evt: LayoutChangeEvent) => {
|
const onLayout = (evt: LayoutChangeEvent) => {
|
||||||
|
@ -42,8 +41,7 @@ export function ImageLayoutGrid({
|
||||||
<View style={style} onLayout={onLayout}>
|
<View style={style} onLayout={onLayout}>
|
||||||
{containerInfo ? (
|
{containerInfo ? (
|
||||||
<ImageLayoutGridInner
|
<ImageLayoutGridInner
|
||||||
type={type}
|
images={images}
|
||||||
uris={uris}
|
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
|
@ -54,41 +52,42 @@ export function ImageLayoutGrid({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageLayoutGridInner({
|
interface ImageLayoutGridInnerProps {
|
||||||
type,
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
uris,
|
|
||||||
onPress,
|
|
||||||
onLongPress,
|
|
||||||
onPressIn,
|
|
||||||
containerInfo,
|
|
||||||
}: {
|
|
||||||
type: ImageLayoutGridType
|
|
||||||
uris: string[]
|
|
||||||
onPress?: (index: number) => void
|
onPress?: (index: number) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
containerInfo: Dimensions
|
containerInfo: Dimensions
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
function ImageLayoutGridInner({
|
||||||
|
images,
|
||||||
|
onPress,
|
||||||
|
onLongPress,
|
||||||
|
onPressIn,
|
||||||
|
containerInfo,
|
||||||
|
}: ImageLayoutGridInnerProps) {
|
||||||
|
const count = images.length
|
||||||
const size1 = useMemo<ImageStyle>(() => {
|
const size1 = useMemo<ImageStyle>(() => {
|
||||||
if (type === 3) {
|
if (count === 3) {
|
||||||
const size = (containerInfo.width - 10) / 3
|
const size = (containerInfo.width - 10) / 3
|
||||||
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
||||||
} else {
|
} else {
|
||||||
const size = (containerInfo.width - 5) / 2
|
const size = (containerInfo.width - 5) / 2
|
||||||
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
||||||
}
|
}
|
||||||
}, [type, containerInfo])
|
}, [count, containerInfo])
|
||||||
const size2 = React.useMemo<ImageStyle>(() => {
|
const size2 = React.useMemo<ImageStyle>(() => {
|
||||||
if (type === 3) {
|
if (count === 3) {
|
||||||
const size = ((containerInfo.width - 10) / 3) * 2 + 5
|
const size = ((containerInfo.width - 10) / 3) * 2 + 5
|
||||||
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
||||||
} else {
|
} else {
|
||||||
const size = (containerInfo.width - 5) / 2
|
const size = (containerInfo.width - 5) / 2
|
||||||
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
return {width: size, height: size, resizeMode: 'cover', borderRadius: 4}
|
||||||
}
|
}
|
||||||
}, [type, containerInfo])
|
}, [count, containerInfo])
|
||||||
|
|
||||||
if (type === 2) {
|
if (count === 2) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.flexRow}>
|
<View style={styles.flexRow}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -96,7 +95,12 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(0)}
|
onPress={() => onPress?.(0)}
|
||||||
onPressIn={() => onPressIn?.(0)}
|
onPressIn={() => onPressIn?.(0)}
|
||||||
onLongPress={() => onLongPress?.(0)}>
|
onLongPress={() => onLongPress?.(0)}>
|
||||||
<Image source={{uri: uris[0]}} style={size1} />
|
<Image
|
||||||
|
source={{uri: images[0].thumb}}
|
||||||
|
style={size1}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[0].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.wSpace} />
|
<View style={styles.wSpace} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -104,12 +108,17 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(1)}
|
onPress={() => onPress?.(1)}
|
||||||
onPressIn={() => onPressIn?.(1)}
|
onPressIn={() => onPressIn?.(1)}
|
||||||
onLongPress={() => onLongPress?.(1)}>
|
onLongPress={() => onLongPress?.(1)}>
|
||||||
<Image source={{uri: uris[1]}} style={size1} />
|
<Image
|
||||||
|
source={{uri: images[1].thumb}}
|
||||||
|
style={size1}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[1].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (type === 3) {
|
if (count === 3) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.flexRow}>
|
<View style={styles.flexRow}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -117,7 +126,12 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(0)}
|
onPress={() => onPress?.(0)}
|
||||||
onPressIn={() => onPressIn?.(0)}
|
onPressIn={() => onPressIn?.(0)}
|
||||||
onLongPress={() => onLongPress?.(0)}>
|
onLongPress={() => onLongPress?.(0)}>
|
||||||
<Image source={{uri: uris[0]}} style={size2} />
|
<Image
|
||||||
|
source={{uri: images[0].thumb}}
|
||||||
|
style={size2}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[0].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.wSpace} />
|
<View style={styles.wSpace} />
|
||||||
<View>
|
<View>
|
||||||
|
@ -126,7 +140,12 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(1)}
|
onPress={() => onPress?.(1)}
|
||||||
onPressIn={() => onPressIn?.(1)}
|
onPressIn={() => onPressIn?.(1)}
|
||||||
onLongPress={() => onLongPress?.(1)}>
|
onLongPress={() => onLongPress?.(1)}>
|
||||||
<Image source={{uri: uris[1]}} style={size1} />
|
<Image
|
||||||
|
source={{uri: images[1].thumb}}
|
||||||
|
style={size1}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[1].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.hSpace} />
|
<View style={styles.hSpace} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -134,13 +153,18 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(2)}
|
onPress={() => onPress?.(2)}
|
||||||
onPressIn={() => onPressIn?.(2)}
|
onPressIn={() => onPressIn?.(2)}
|
||||||
onLongPress={() => onLongPress?.(2)}>
|
onLongPress={() => onLongPress?.(2)}>
|
||||||
<Image source={{uri: uris[2]}} style={size1} />
|
<Image
|
||||||
|
source={{uri: images[2].thumb}}
|
||||||
|
style={size1}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[2].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (type === 4) {
|
if (count === 4) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.flexRow}>
|
<View style={styles.flexRow}>
|
||||||
<View>
|
<View>
|
||||||
|
@ -149,7 +173,12 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(0)}
|
onPress={() => onPress?.(0)}
|
||||||
onPressIn={() => onPressIn?.(0)}
|
onPressIn={() => onPressIn?.(0)}
|
||||||
onLongPress={() => onLongPress?.(0)}>
|
onLongPress={() => onLongPress?.(0)}>
|
||||||
<Image source={{uri: uris[0]}} style={size1} />
|
<Image
|
||||||
|
source={{uri: images[0].thumb}}
|
||||||
|
style={size1}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[0].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.hSpace} />
|
<View style={styles.hSpace} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -157,7 +186,12 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(2)}
|
onPress={() => onPress?.(2)}
|
||||||
onPressIn={() => onPressIn?.(2)}
|
onPressIn={() => onPressIn?.(2)}
|
||||||
onLongPress={() => onLongPress?.(2)}>
|
onLongPress={() => onLongPress?.(2)}>
|
||||||
<Image source={{uri: uris[2]}} style={size1} />
|
<Image
|
||||||
|
source={{uri: images[2].thumb}}
|
||||||
|
style={size1}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[2].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.wSpace} />
|
<View style={styles.wSpace} />
|
||||||
|
@ -167,7 +201,12 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(1)}
|
onPress={() => onPress?.(1)}
|
||||||
onPressIn={() => onPressIn?.(1)}
|
onPressIn={() => onPressIn?.(1)}
|
||||||
onLongPress={() => onLongPress?.(1)}>
|
onLongPress={() => onLongPress?.(1)}>
|
||||||
<Image source={{uri: uris[1]}} style={size1} />
|
<Image
|
||||||
|
source={{uri: images[1].thumb}}
|
||||||
|
style={size1}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[1].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.hSpace} />
|
<View style={styles.hSpace} />
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -175,7 +214,12 @@ function ImageLayoutGridInner({
|
||||||
onPress={() => onPress?.(3)}
|
onPress={() => onPress?.(3)}
|
||||||
onPressIn={() => onPressIn?.(3)}
|
onPressIn={() => onPressIn?.(3)}
|
||||||
onLongPress={() => onLongPress?.(3)}>
|
onLongPress={() => onLongPress?.(3)}>
|
||||||
<Image source={{uri: uris[3]}} style={size1} />
|
<Image
|
||||||
|
source={{uri: images[3].thumb}}
|
||||||
|
style={size1}
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel={images[3].alt}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -112,6 +112,7 @@ export function PostEmbeds({
|
||||||
return (
|
return (
|
||||||
<View style={[styles.imagesContainer, style]}>
|
<View style={[styles.imagesContainer, style]}>
|
||||||
<AutoSizedImage
|
<AutoSizedImage
|
||||||
|
alt={embed.images[0].alt}
|
||||||
uri={embed.images[0].thumb}
|
uri={embed.images[0].thumb}
|
||||||
onPress={() => openLightbox(0)}
|
onPress={() => openLightbox(0)}
|
||||||
onLongPress={() => onLongPress(0)}
|
onLongPress={() => onLongPress(0)}
|
||||||
|
@ -124,8 +125,7 @@ export function PostEmbeds({
|
||||||
return (
|
return (
|
||||||
<View style={[styles.imagesContainer, style]}>
|
<View style={[styles.imagesContainer, style]}>
|
||||||
<ImageLayoutGrid
|
<ImageLayoutGrid
|
||||||
type={embed.images.length}
|
images={embed.images}
|
||||||
uris={embed.images.map(img => img.thumb)}
|
|
||||||
onPress={openLightbox}
|
onPress={openLightbox}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
|
|
|
@ -54,7 +54,6 @@ const ShellInner = observer(() => {
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</View>
|
</View>
|
||||||
<ModalsContainer />
|
|
||||||
<Lightbox />
|
<Lightbox />
|
||||||
<Composer
|
<Composer
|
||||||
active={store.shell.isComposerActive}
|
active={store.shell.isComposerActive}
|
||||||
|
@ -64,6 +63,7 @@ const ShellInner = observer(() => {
|
||||||
onPost={store.shell.composerOpts?.onPost}
|
onPost={store.shell.composerOpts?.onPost}
|
||||||
quote={store.shell.composerOpts?.quote}
|
quote={store.shell.composerOpts?.quote}
|
||||||
/>
|
/>
|
||||||
|
<ModalsContainer />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -8364,10 +8364,10 @@ expo-image-picker@~14.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
expo-image-loader "~4.1.0"
|
expo-image-loader "~4.1.0"
|
||||||
|
|
||||||
expo-image@~1.0.0:
|
expo-image@^1.2.1:
|
||||||
version "1.0.0"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.0.0.tgz#a3670d20815d99e2527307a33761c9b0088823b1"
|
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-1.2.1.tgz#3f377cb3142de2107903f4e4f88a7f44785dee18"
|
||||||
integrity sha512-A1amVExKhBa/eRXuceauYtPkf9izeje5AbxEWL09tgK91rf3GSIZXM5PSDGlIM0s7dpCV+Iet2jhwcFUfWaZrw==
|
integrity sha512-pYZFN0ctuIBA+sqUiw70rHQQ04WDyEcF549ObArdj0MNgSUCBJMFmu/jrWDmxOpEMF40lfLVIZKigJT7Bw+GYA==
|
||||||
|
|
||||||
expo-json-utils@~0.5.0:
|
expo-json-utils@~0.5.0:
|
||||||
version "0.5.1"
|
version "0.5.1"
|
||||||
|
|
Loading…
Reference in New Issue