import React from 'react' import { type ImageStyle, Keyboard, type LayoutChangeEvent, StyleSheet, TouchableOpacity, View, type ViewStyle, } from 'react-native' import {Image} from 'expo-image' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type Dimensions} from '#/lib/media/types' import {colors, s} from '#/lib/styles' import {isNative} from '#/platform/detection' import {type ComposerImage, cropImage} from '#/state/gallery' import {Text} from '#/view/com/util/text/Text' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {type PostAction} from '../state/composer' import {EditImageDialog} from './EditImageDialog' import {ImageAltTextDialog} from './ImageAltTextDialog' const IMAGE_GAP = 8 interface GalleryProps { images: ComposerImage[] dispatch: (action: PostAction) => void } export let Gallery = (props: GalleryProps): React.ReactNode => { const [containerInfo, setContainerInfo] = React.useState() const onLayout = (evt: LayoutChangeEvent) => { const {width, height} = evt.nativeEvent.layout setContainerInfo({ width, height, }) } return ( {containerInfo ? ( ) : undefined} ) } Gallery = React.memo(Gallery) interface GalleryInnerProps extends GalleryProps { containerInfo: Dimensions } const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => { const {isMobile} = useWebMediaQueries() const {altTextControlStyle, imageControlsStyle, imageStyle} = React.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]) return images.length !== 0 ? ( <> {images.map(image => { return ( { dispatch({type: 'embed_update_image', image: next}) }} onRemove={() => { dispatch({type: 'embed_remove_image', image}) }} /> ) })} ) : null } type GalleryItemProps = { image: ComposerImage altTextControlStyle?: ViewStyle imageControlsStyle?: ViewStyle imageStyle?: ImageStyle onChange: (next: ComposerImage) => void onRemove: () => void } const GalleryItem = ({ image, altTextControlStyle, imageControlsStyle, imageStyle, onChange, onRemove, }: GalleryItemProps): React.ReactNode => { const {_} = useLingui() const t = useTheme() const altTextControl = Dialog.useDialogControl() const editControl = Dialog.useDialogControl() const onImageEdit = () => { if (isNative) { cropImage(image).then(next => { onChange(next) }) } else { editControl.open() } } const onAltTextEdit = () => { Keyboard.dismiss() altTextControl.open() } return ( {image.alt.length !== 0 ? ( ) : ( )} ALT ) } export function AltTextReminder() { const t = useTheme() return ( Alt text describes images for blind and low-vision users, and helps give context to everyone. ) } const styles = StyleSheet.create({ gallery: { flex: 1, flexDirection: 'row', gap: IMAGE_GAP, marginTop: 16, }, image: { resizeMode: 'cover', borderRadius: 8, }, imageControl: { width: 24, height: 24, borderRadius: 12, backgroundColor: 'rgba(0, 0, 0, 0.75)', alignItems: 'center', justifyContent: 'center', }, altTextControl: { position: 'absolute', zIndex: 1, borderRadius: 6, backgroundColor: 'rgba(0, 0, 0, 0.75)', paddingHorizontal: 8, paddingVertical: 3, flexDirection: 'row', alignItems: 'center', gap: 4, }, altTextControlLabel: { color: 'white', fontSize: 12, fontWeight: '600', letterSpacing: 1, }, altTextHiddenRegion: { position: 'absolute', left: 4, right: 4, bottom: 4, top: 30, zIndex: 1, }, reminder: { flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 8, paddingVertical: 14, }, infoIcon: { width: 22, height: 22, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, })