diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index dea220c5..4a55c23a 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -48,11 +48,6 @@ export interface AltTextImageModal { image: ImageModel } -export interface AltTextImageReadModal { - name: 'alt-text-image-read' - altText: string -} - export interface DeleteAccountModal { name: 'delete-account' } @@ -106,7 +101,6 @@ export type Modal = // Posts | AltTextImageModal - | AltTextImageReadModal | CropImageModal | ServerInputModal | RepostModal @@ -127,9 +121,14 @@ export class ProfileImageLightbox implements LightboxModel { } } +interface ImagesLightboxItem { + uri: string + alt?: string +} + export class ImagesLightbox implements LightboxModel { name = 'images' - constructor(public uris: string[], public index: number) { + constructor(public images: ImagesLightboxItem[], public index: number) { makeAutoObservable(this) } setIndex(index: number) { @@ -173,7 +172,7 @@ export class ShellUiModel { isModalActive = false activeModals: Modal[] = [] isLightboxActive = false - activeLightbox: ProfileImageLightbox | ImagesLightbox | undefined + activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null isComposerActive = false composerOpts: ComposerOpts | undefined @@ -262,7 +261,7 @@ export class ShellUiModel { closeLightbox() { this.isLightboxActive = false - this.activeLightbox = undefined + this.activeLightbox = null } openComposer(opts: ComposerOpts) { diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 06b48143..c4bc88cf 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -1,31 +1,75 @@ import React from 'react' +import {Pressable, StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import ImageView from './ImageViewing' import {useStores} from 'state/index' import * as models from 'state/models/ui/shell' import {saveImageModal} from 'lib/media/manip' -import {ImageSource} from './ImageViewing/@types' +import {Text} from '../util/text/Text' +import {s, colors} from 'lib/styles' +import {Button} from '../util/forms/Button' +import {isIOS} from 'platform/detection' export const Lightbox = observer(function Lightbox() { const store = useStores() - if (!store.shell.isLightboxActive) { - return null - } + const [isAltExpanded, setAltExpanded] = React.useState(false) - const onClose = () => { + const onClose = React.useCallback(() => { store.shell.closeLightbox() - } - const onLongPress = (image: ImageSource) => { - if ( - typeof image === 'object' && - 'uri' in image && - typeof image.uri === 'string' - ) { - saveImageModal({uri: image.uri}) - } - } + }, [store]) - if (store.shell.activeLightbox?.name === 'profile-image') { + const LightboxFooter = React.useCallback( + ({imageIndex}: {imageIndex: number}) => { + const lightbox = store.shell.activeLightbox + if (!lightbox) { + return null + } + + let altText = '' + let uri + if (lightbox.name === 'images') { + const opts = store.shell.activeLightbox as models.ImagesLightbox + uri = opts.images[imageIndex].uri + altText = opts.images[imageIndex].alt + } else if (store.shell.activeLightbox.name === 'profile-image') { + const opts = store.shell.activeLightbox as models.ProfileImageLightbox + uri = opts.profileView.avatar + } + + return ( + <View style={[styles.footer]}> + {altText ? ( + <Pressable + onPress={() => setAltExpanded(!isAltExpanded)} + accessibilityRole="button"> + <Text + style={[s.gray3, styles.footerText]} + numberOfLines={isAltExpanded ? undefined : 3}> + {altText} + </Text> + </Pressable> + ) : null} + <View style={styles.footerBtns}> + <Button + type="primary-outline" + style={styles.footerBtn} + onPress={() => saveImageModal({uri})}> + <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> + <Text type="xl" style={s.white}> + Share + </Text> + </Button> + </View> + </View> + ) + }, + [store.shell.activeLightbox, isAltExpanded, setAltExpanded], + ) + + if (!store.shell.activeLightbox) { + return null + } else if (store.shell.activeLightbox.name === 'profile-image') { const opts = store.shell.activeLightbox as models.ProfileImageLightbox return ( <ImageView @@ -33,20 +77,44 @@ export const Lightbox = observer(function Lightbox() { imageIndex={0} visible onRequestClose={onClose} + FooterComponent={LightboxFooter} /> ) - } else if (store.shell.activeLightbox?.name === 'images') { + } else if (store.shell.activeLightbox.name === 'images') { const opts = store.shell.activeLightbox as models.ImagesLightbox return ( <ImageView - images={opts.uris.map(uri => ({uri}))} + images={opts.images.map(({uri}) => ({uri}))} imageIndex={opts.index} visible onRequestClose={onClose} - onLongPress={onLongPress} + FooterComponent={LightboxFooter} /> ) } else { return null } }) + +const styles = StyleSheet.create({ + footer: { + paddingTop: 16, + paddingBottom: isIOS ? 40 : 24, + paddingHorizontal: 24, + backgroundColor: '#000d', + }, + footerText: { + paddingBottom: isIOS ? 20 : 16, + }, + footerBtns: { + flexDirection: 'row', + justifyContent: 'center', + }, + footerBtn: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: 'transparent', + borderColor: colors.white, + }, +}) diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index 3388b54b..eff9af2d 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -10,11 +10,13 @@ import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useStores} from 'state/index' import * as models from 'state/models/ui/shell' -import {colors} from 'lib/styles' +import {colors, s} from 'lib/styles' import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader' +import {Text} from '../util/text/Text' interface Img { uri: string + alt?: string } export const Lightbox = observer(function Lightbox() { @@ -37,7 +39,7 @@ export const Lightbox = observer(function Lightbox() { } } else if (activeLightbox instanceof models.ImagesLightbox) { const opts = activeLightbox - imgs = opts.uris.map(uri => ({uri})) + imgs = opts.images } if (!imgs) { @@ -131,6 +133,11 @@ function LightboxInner({ )} </View> </TouchableWithoutFeedback> + {imgs[index].alt ? ( + <View style={styles.footer}> + <Text style={s.white}>{imgs[index].alt}</Text> + </View> + ) : null} <View style={styles.closeBtn}> <ImageDefaultHeader onRequestClose={onClose} /> </View> @@ -183,4 +190,9 @@ const styles = StyleSheet.create({ right: 30, top: '50%', }, + footer: { + paddingHorizontal: 32, + paddingVertical: 24, + backgroundColor: colors.black, + }, }) diff --git a/src/view/com/modals/AltImageRead.tsx b/src/view/com/modals/AltImageRead.tsx deleted file mode 100644 index 98547728..00000000 --- a/src/view/com/modals/AltImageRead.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, {useCallback} from 'react' -import {StyleSheet, View} from 'react-native' -import {usePalette} from 'lib/hooks/usePalette' -import {gradients, s} from 'lib/styles' -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 = ['70%'] - -interface Props { - altText: string -} - -export function Component({altText}: Props) { - const pal = usePalette('default') - const store = useStores() - - const onPress = useCallback(() => { - store.shell.closeModal() - }, [store]) - - return ( - <View - testID="altTextImageModal" - style={[pal.view, styles.container, s.flex1]}> - <Text style={[styles.title, pal.text]}>Image description</Text> - <View style={[styles.text, pal.viewLight]}> - <Text style={pal.text}>{altText}</Text> - </View> - <TouchableOpacity - testID="altTextImageSaveBtn" - onPress={onPress} - accessibilityRole="button" - accessibilityLabel="Done" - accessibilityHint="Closes alt text modal"> - <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]}> - Done - </Text> - </LinearGradient> - </TouchableOpacity> - </View> - ) -} - -const styles = StyleSheet.create({ - container: { - gap: 18, - paddingVertical: isDesktopWeb ? 0 : 18, - paddingHorizontal: isDesktopWeb ? 0 : 12, - height: '100%', - width: '100%', - }, - title: { - textAlign: 'center', - fontWeight: 'bold', - fontSize: 24, - }, - text: { - borderRadius: 5, - marginVertical: 18, - paddingHorizontal: 18, - paddingVertical: 16, - }, - button: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - borderRadius: 32, - padding: 10, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index b5d71a11..18b7ae4c 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -13,7 +13,6 @@ import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './ReportPost' import * as RepostModal from './Repost' import * as AltImageModal from './AltImage' -import * as AltImageReadModal from './AltImageRead' import * as ReportAccountModal from './ReportAccount' import * as DeleteAccountModal from './DeleteAccount' import * as ChangeHandleModal from './ChangeHandle' @@ -76,9 +75,6 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'alt-text-image') { snapPoints = AltImageModal.snapPoints element = <AltImageModal.Component {...activeModal} /> - } else if (activeModal?.name === 'alt-text-image-read') { - snapPoints = AltImageReadModal.snapPoints - element = <AltImageReadModal.Component {...activeModal} /> } else if (activeModal?.name === 'change-handle') { snapPoints = ChangeHandleModal.snapPoints element = <ChangeHandleModal.Component {...activeModal} /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 50487e3e..9dcc8fa7 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -15,7 +15,6 @@ import * as DeleteAccountModal from './DeleteAccount' import * as RepostModal from './Repost' import * as CropImageModal from './crop-image/CropImage.web' import * as AltTextImageModal from './AltImage' -import * as AltTextImageReadModal from './AltImageRead' import * as ChangeHandleModal from './ChangeHandle' import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' @@ -89,8 +88,6 @@ function Modal({modal}: {modal: ModalIface}) { element = <ContentLanguagesSettingsModal.Component /> } else if (modal.name === 'alt-text-image') { element = <AltTextImageModal.Component {...modal} /> - } else if (modal.name === 'alt-text-image-read') { - element = <AltTextImageReadModal.Component {...modal} /> } else { return null } diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 5b6c3384..1a29b453 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -1,8 +1,7 @@ import {AppBskyEmbedImages} from '@atproto/api' -import React, {ComponentProps, FC, useCallback} from 'react' -import {Pressable, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import React, {ComponentProps, FC} from 'react' +import {StyleSheet, Text, TouchableOpacity, View} from 'react-native' import {Image} from 'expo-image' -import {useStores} from 'state/index' type EventFunction = (index: number) => void @@ -26,22 +25,14 @@ export const GalleryItem: FC<GalleryItemProps> = ({ onLongPress, }) => { const image = images[index] - const store = useStores() - - const onPressAltText = useCallback(() => { - store.shell.openModal({ - name: 'alt-text-image-read', - altText: image.alt, - }) - }, [image.alt, store.shell]) return ( <View> <TouchableOpacity delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(index)} - onPressIn={() => onPressIn?.(index)} - onLongPress={() => onLongPress?.(index)} + onPress={onPress ? () => onPress(index) : undefined} + onPressIn={onPressIn ? () => onPressIn(index) : undefined} + onLongPress={onLongPress ? () => onLongPress(index) : undefined} accessibilityRole="button" accessibilityLabel="View image" accessibilityHint=""> @@ -54,15 +45,7 @@ export const GalleryItem: FC<GalleryItemProps> = ({ accessibilityIgnoresInvertColors /> </TouchableOpacity> - {image.alt === '' ? null : ( - <Pressable - onPress={onPressAltText} - accessibilityRole="button" - accessibilityLabel="View alt text" - accessibilityHint="Opens modal with alt text"> - <Text style={styles.alt}>ALT</Text> - </Pressable> - )} + {image.alt === '' ? null : <Text style={styles.alt}>ALT</Text>} </View> ) } @@ -78,8 +61,8 @@ const styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 3, position: 'absolute', - left: 10, - top: -26, + left: 6, + bottom: 6, width: 46, }, }) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 929c85ad..2dda9069 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -1,11 +1,10 @@ -import React, {useCallback} from 'react' +import React from 'react' import { StyleSheet, StyleProp, View, ViewStyle, Image as RNImage, - Pressable, Text, } from 'react-native' import { @@ -20,7 +19,6 @@ import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ImagesLightbox} from 'state/models/ui/shell' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {saveImageModal} from 'lib/media/manip' import {YoutubeEmbed} from './YoutubeEmbed' import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {getYoutubeVideoId} from 'lib/strings/url-helpers' @@ -44,16 +42,6 @@ export function PostEmbeds({ const pal = usePalette('default') const store = useStores() - const onPressAltText = useCallback( - (alt: string) => { - store.shell.openModal({ - name: 'alt-text-image-read', - altText: alt, - }) - }, - [store.shell], - ) - if ( AppBskyEmbedRecordWithMedia.isView(embed) && AppBskyEmbedRecord.isViewRecord(embed.record.record) && @@ -103,20 +91,17 @@ export function PostEmbeds({ const {images} = embed if (images.length > 0) { - const uris = embed.images.map(img => img.fullsize) + const items = embed.images.map(img => ({uri: img.fullsize, alt: img.alt})) const openLightbox = (index: number) => { - store.shell.openLightbox(new ImagesLightbox(uris, index)) - } - const onLongPress = (index: number) => { - saveImageModal({uri: uris[index]}) + store.shell.openLightbox(new ImagesLightbox(items, index)) } const onPressIn = (index: number) => { - const firstImageToShow = uris[index] + const firstImageToShow = items[index].uri RNImage.prefetch(firstImageToShow) - uris.forEach(uri => { - if (firstImageToShow !== uri) { + items.forEach(item => { + if (firstImageToShow !== item.uri) { // First image already prefeched above - RNImage.prefetch(uri) + RNImage.prefetch(item.uri) } }) } @@ -129,20 +114,9 @@ export function PostEmbeds({ alt={alt} uri={thumb} onPress={() => openLightbox(0)} - onLongPress={() => onLongPress(0)} onPressIn={() => onPressIn(0)} style={styles.singleImage}> - {alt === '' ? null : ( - <Pressable - onPress={() => { - onPressAltText(alt) - }} - accessibilityRole="button" - accessibilityLabel="View alt text" - accessibilityHint="Opens modal with alt text"> - <Text style={styles.alt}>ALT</Text> - </Pressable> - )} + {alt === '' ? null : <Text style={styles.alt}>ALT</Text>} </AutoSizedImage> </View> ) @@ -153,7 +127,6 @@ export function PostEmbeds({ <ImageLayoutGrid images={embed.images} onPress={openLightbox} - onLongPress={onLongPress} onPressIn={onPressIn} style={embed.images.length === 1 ? styles.singleImage : undefined} /> @@ -209,8 +182,8 @@ const styles = StyleSheet.create({ paddingHorizontal: 10, paddingVertical: 3, position: 'absolute', - left: 10, - top: -26, + left: 6, + bottom: 6, width: 46, }, })