diff options
author | Ollie H <renahlee@outlook.com> | 2023-05-01 11:59:17 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-01 13:59:17 -0500 |
commit | dbb3c5c15524c517291356a4918d043348906aad (patch) | |
tree | 8b7a38c2d5c56c34b43dcbddccf640e8c9a52ad3 | |
parent | 0ec98c77ef65ff74e83b314d8eed9ef9b07d47d3 (diff) | |
download | voidsky-dbb3c5c15524c517291356a4918d043348906aad.tar.zst |
Image alt text view modal (#551)
* Image alt text view modal * Minor style tweaks --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
-rw-r--r-- | src/state/models/ui/shell.ts | 8 | ||||
-rw-r--r-- | src/view/com/modals/AltImageRead.tsx | 75 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/images/Gallery.tsx | 76 | ||||
-rw-r--r-- | src/view/com/util/images/ImageLayoutGrid.tsx | 204 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 94 |
7 files changed, 272 insertions, 192 deletions
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 797d53f81..98e98ef8e 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -47,6 +47,11 @@ export interface AltTextImageModal { onAltTextSet: (altText?: string) => void } +export interface AltTextImageReadModal { + name: 'alt-text-image-read' + altText: string +} + export interface DeleteAccountModal { name: 'delete-account' } @@ -93,8 +98,9 @@ export type Modal = | ReportAccountModal | ReportPostModal - // Posting + // Posts | AltTextImageModal + | AltTextImageReadModal | CropImageModal | ServerInputModal | RepostModal diff --git a/src/view/com/modals/AltImageRead.tsx b/src/view/com/modals/AltImageRead.tsx new file mode 100644 index 000000000..e7b4797ee --- /dev/null +++ b/src/view/com/modals/AltImageRead.tsx @@ -0,0 +1,75 @@ +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}> + <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 df7d7f042..2e053e3ad 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -13,6 +13,7 @@ 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' @@ -74,6 +75,9 @@ 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 07d5168ed..de748b3a8 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -15,6 +15,7 @@ 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' @@ -84,6 +85,8 @@ function Modal({modal}: {modal: ModalIface}) { element = <ContentFilteringSettingsModal.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 new file mode 100644 index 000000000..78ced0668 --- /dev/null +++ b/src/view/com/util/images/Gallery.tsx @@ -0,0 +1,76 @@ +import {AppBskyEmbedImages} from '@atproto/api' +import React, {ComponentProps, FC, useCallback} from 'react' +import {Pressable, StyleSheet, Text, TouchableOpacity, View} from 'react-native' +import {Image} from 'expo-image' +import {useStores} from 'state/index' + +type EventFunction = (index: number) => void + +interface GalleryItemProps { + images: AppBskyEmbedImages.ViewImage[] + index: number + onPress?: EventFunction + onLongPress?: EventFunction + onPressIn?: EventFunction + imageStyle: ComponentProps<typeof Image>['style'] +} + +const DELAY_PRESS_IN = 500 + +export const GalleryItem: FC<GalleryItemProps> = ({ + images, + index, + imageStyle, + onPress, + onPressIn, + 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)}> + <Image + source={{uri: image.thumb}} + style={imageStyle} + accessible={true} + accessibilityLabel={image.alt} + /> + </TouchableOpacity> + {image.alt === '' ? null : ( + <Pressable onPress={onPressAltText}> + <Text style={styles.alt}>ALT</Text> + </Pressable> + )} + </View> + ) +} + +const styles = StyleSheet.create({ + alt: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + color: 'white', + fontSize: 12, + fontWeight: 'bold', + letterSpacing: 1, + paddingHorizontal: 10, + paddingVertical: 3, + position: 'absolute', + left: 10, + top: -26, + width: 46, + }, +}) diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 51bb04fe9..4c0901304 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -3,15 +3,13 @@ import { LayoutChangeEvent, StyleProp, StyleSheet, - TouchableOpacity, View, ViewStyle, } from 'react-native' -import {Image, ImageStyle} from 'expo-image' +import {ImageStyle} from 'expo-image' import {Dimensions} from 'lib/media/types' import {AppBskyEmbedImages} from '@atproto/api' - -export const DELAY_PRESS_IN = 500 +import {GalleryItem} from './Gallery' interface ImageLayoutGridProps { images: AppBskyEmbedImages.ViewImage[] @@ -21,32 +19,21 @@ interface ImageLayoutGridProps { style?: StyleProp<ViewStyle> } -export function ImageLayoutGrid({ - images, - onPress, - onLongPress, - onPressIn, - style, -}: ImageLayoutGridProps) { +export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) { const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>() const onLayout = (evt: LayoutChangeEvent) => { + const {width, height} = evt.nativeEvent.layout setContainerInfo({ - width: evt.nativeEvent.layout.width, - height: evt.nativeEvent.layout.height, + width, + height, }) } return ( <View style={style} onLayout={onLayout}> {containerInfo ? ( - <ImageLayoutGridInner - images={images} - onPress={onPress} - onPressIn={onPressIn} - onLongPress={onLongPress} - containerInfo={containerInfo} - /> + <ImageLayoutGridInner {...props} containerInfo={containerInfo} /> ) : undefined} </View> ) @@ -61,13 +48,10 @@ interface ImageLayoutGridInnerProps { } function ImageLayoutGridInner({ - images, - onPress, - onLongPress, - onPressIn, containerInfo, + ...props }: ImageLayoutGridInnerProps) { - const count = images.length + const count = props.images.length const size1 = useMemo<ImageStyle>(() => { if (count === 3) { const size = (containerInfo.width - 10) / 3 @@ -87,149 +71,43 @@ function ImageLayoutGridInner({ } }, [count, containerInfo]) - if (count === 2) { - return ( - <View style={styles.flexRow}> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - <Image - source={{uri: images[0].thumb}} - style={size1} - accessible={true} - accessibilityLabel={images[0].alt} - /> - </TouchableOpacity> - <View style={styles.wSpace} /> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - <Image - source={{uri: images[1].thumb}} - style={size1} - accessible={true} - accessibilityLabel={images[1].alt} - /> - </TouchableOpacity> - </View> - ) - } - if (count === 3) { - return ( - <View style={styles.flexRow}> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - <Image - source={{uri: images[0].thumb}} - style={size2} - accessible={true} - accessibilityLabel={images[0].alt} - /> - </TouchableOpacity> - <View style={styles.wSpace} /> - <View> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - <Image - source={{uri: images[1].thumb}} - style={size1} - accessible={true} - accessibilityLabel={images[1].alt} - /> - </TouchableOpacity> - <View style={styles.hSpace} /> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(2)} - onPressIn={() => onPressIn?.(2)} - onLongPress={() => onLongPress?.(2)}> - <Image - source={{uri: images[2].thumb}} - style={size1} - accessible={true} - accessibilityLabel={images[2].alt} - /> - </TouchableOpacity> + switch (count) { + case 2: + return ( + <View style={styles.flexRow}> + <GalleryItem index={0} {...props} imageStyle={size1} /> + <GalleryItem index={1} {...props} imageStyle={size1} /> </View> - </View> - ) - } - if (count === 4) { - return ( - <View style={styles.flexRow}> - <View> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - <Image - source={{uri: images[0].thumb}} - style={size1} - accessible={true} - accessibilityLabel={images[0].alt} - /> - </TouchableOpacity> - <View style={styles.hSpace} /> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(2)} - onPressIn={() => onPressIn?.(2)} - onLongPress={() => onLongPress?.(2)}> - <Image - source={{uri: images[2].thumb}} - style={size1} - accessible={true} - accessibilityLabel={images[2].alt} - /> - </TouchableOpacity> + ) + case 3: + return ( + <View style={styles.flexRow}> + <GalleryItem index={0} {...props} imageStyle={size2} /> + <View style={styles.flexColumn}> + <GalleryItem index={1} {...props} imageStyle={size1} /> + <GalleryItem index={2} {...props} imageStyle={size1} /> + </View> </View> - <View style={styles.wSpace} /> - <View> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - <Image - source={{uri: images[1].thumb}} - style={size1} - accessible={true} - accessibilityLabel={images[1].alt} - /> - </TouchableOpacity> - <View style={styles.hSpace} /> - <TouchableOpacity - delayPressIn={DELAY_PRESS_IN} - onPress={() => onPress?.(3)} - onPressIn={() => onPressIn?.(3)} - onLongPress={() => onLongPress?.(3)}> - <Image - source={{uri: images[3].thumb}} - style={size1} - accessible={true} - accessibilityLabel={images[3].alt} - /> - </TouchableOpacity> + ) + case 4: + return ( + <View style={styles.flexRow}> + <View style={styles.flexColumn}> + <GalleryItem index={0} {...props} imageStyle={size1} /> + <GalleryItem index={2} {...props} imageStyle={size1} /> + </View> + <View style={styles.flexColumn}> + <GalleryItem index={1} {...props} imageStyle={size1} /> + <GalleryItem index={3} {...props} imageStyle={size1} /> + </View> </View> - </View> - ) + ) + default: + return null } - return <View /> } const styles = StyleSheet.create({ - flexRow: {flexDirection: 'row'}, - wSpace: {width: 5}, - hSpace: {height: 5}, + flexRow: {flexDirection: 'row', gap: 5}, + flexColumn: {flexDirection: 'column', gap: 5}, }) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index f37fba342..6a7759840 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -1,10 +1,12 @@ -import React from 'react' +import React, {useCallback} from 'react' import { StyleSheet, StyleProp, View, ViewStyle, Image as RNImage, + Pressable, + Text, } from 'react-native' import { AppBskyEmbedImages, @@ -14,7 +16,6 @@ import { AppBskyFeedPost, } from '@atproto/api' import {Link} from '../Link' -import {AutoSizedImage} from '../images/AutoSizedImage' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ImagesLightbox} from 'state/models/ui/shell' import {useStores} from 'state/index' @@ -24,6 +25,7 @@ import {YoutubeEmbed} from './YoutubeEmbed' import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {getYoutubeVideoId} from 'lib/strings/url-helpers' import QuoteEmbed from './QuoteEmbed' +import {AutoSizedImage} from '../images/AutoSizedImage' type Embed = | AppBskyEmbedRecord.View @@ -42,6 +44,16 @@ 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) && @@ -88,7 +100,9 @@ export function PostEmbeds({ } if (AppBskyEmbedImages.isView(embed)) { - if (embed.images.length > 0) { + const {images} = embed + + if (images.length > 0) { const uris = embed.images.map(img => img.fullsize) const openLightbox = (index: number) => { store.shell.openLightbox(new ImagesLightbox(uris, index)) @@ -107,32 +121,42 @@ export function PostEmbeds({ }) } - switch (embed.images.length) { - case 1: - return ( - <View style={[styles.imagesContainer, style]}> - <AutoSizedImage - alt={embed.images[0].alt} - uri={embed.images[0].thumb} - onPress={() => openLightbox(0)} - onLongPress={() => onLongPress(0)} - onPressIn={() => onPressIn(0)} - style={styles.singleImage} - /> - </View> - ) - default: - return ( - <View style={[styles.imagesContainer, style]}> - <ImageLayoutGrid - images={embed.images} - onPress={openLightbox} - onLongPress={onLongPress} - onPressIn={onPressIn} - /> - </View> - ) + if (images.length === 1) { + const {alt, thumb} = images[0] + return ( + <View style={[styles.imagesContainer, style]}> + <AutoSizedImage + alt={alt} + uri={thumb} + onPress={() => openLightbox(0)} + onLongPress={() => onLongPress(0)} + onPressIn={() => onPressIn(0)} + style={styles.singleImage}> + {alt === '' ? null : ( + <Pressable + onPress={() => { + onPressAltText(alt) + }}> + <Text style={styles.alt}>ALT</Text> + </Pressable> + )} + </AutoSizedImage> + </View> + ) } + + return ( + <View style={[styles.imagesContainer, style]}> + <ImageLayoutGrid + images={embed.images} + onPress={openLightbox} + onLongPress={onLongPress} + onPressIn={onPressIn} + style={embed.images.length === 1 ? styles.singleImage : undefined} + /> + </View> + ) + // } } } @@ -172,4 +196,18 @@ const styles = StyleSheet.create({ borderRadius: 8, marginTop: 4, }, + alt: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + color: 'white', + fontSize: 12, + fontWeight: 'bold', + letterSpacing: 1, + paddingHorizontal: 10, + paddingVertical: 3, + position: 'absolute', + left: 10, + top: -26, + width: 46, + }, }) |