From dbb3c5c15524c517291356a4918d043348906aad Mon Sep 17 00:00:00 2001 From: Ollie H Date: Mon, 1 May 2023 11:59:17 -0700 Subject: [PATCH] Image alt text view modal (#551) * Image alt text view modal * Minor style tweaks --------- Co-authored-by: Paul Frazee --- src/state/models/ui/shell.ts | 8 +- src/view/com/modals/AltImageRead.tsx | 75 +++++++ src/view/com/modals/Modal.tsx | 4 + src/view/com/modals/Modal.web.tsx | 3 + src/view/com/util/images/Gallery.tsx | 76 +++++++ src/view/com/util/images/ImageLayoutGrid.tsx | 204 ++++--------------- src/view/com/util/post-embeds/index.tsx | 94 ++++++--- 7 files changed, 272 insertions(+), 192 deletions(-) create mode 100644 src/view/com/modals/AltImageRead.tsx create mode 100644 src/view/com/util/images/Gallery.tsx diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 797d53f8..98e98ef8 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 00000000..e7b4797e --- /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 ( + + Image description + + {altText} + + + + + Done + + + + + ) +} + +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 df7d7f04..2e053e3a 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 = + } else if (activeModal?.name === 'alt-text-image-read') { + snapPoints = AltImageReadModal.snapPoints + element = } else if (activeModal?.name === 'change-handle') { snapPoints = ChangeHandleModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 07d5168e..de748b3a 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 = } else if (modal.name === 'alt-text-image') { element = + } else if (modal.name === 'alt-text-image-read') { + element = } 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 00000000..78ced066 --- /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['style'] +} + +const DELAY_PRESS_IN = 500 + +export const GalleryItem: FC = ({ + 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 ( + + onPress?.(index)} + onPressIn={() => onPressIn?.(index)} + onLongPress={() => onLongPress?.(index)}> + + + {image.alt === '' ? null : ( + + ALT + + )} + + ) +} + +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 51bb04fe..4c090130 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 } -export function ImageLayoutGrid({ - images, - onPress, - onLongPress, - onPressIn, - style, -}: ImageLayoutGridProps) { +export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) { const [containerInfo, setContainerInfo] = useState() const onLayout = (evt: LayoutChangeEvent) => { + const {width, height} = evt.nativeEvent.layout setContainerInfo({ - width: evt.nativeEvent.layout.width, - height: evt.nativeEvent.layout.height, + width, + height, }) } return ( {containerInfo ? ( - + ) : undefined} ) @@ -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(() => { if (count === 3) { const size = (containerInfo.width - 10) / 3 @@ -87,149 +71,43 @@ function ImageLayoutGridInner({ } }, [count, containerInfo]) - if (count === 2) { - return ( - - onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - - - - onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - - - - ) - } - if (count === 3) { - return ( - - onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - - - - - onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - - - - onPress?.(2)} - onPressIn={() => onPressIn?.(2)} - onLongPress={() => onLongPress?.(2)}> - - + switch (count) { + case 2: + return ( + + + - - ) - } - if (count === 4) { - return ( - - - onPress?.(0)} - onPressIn={() => onPressIn?.(0)} - onLongPress={() => onLongPress?.(0)}> - - - - onPress?.(2)} - onPressIn={() => onPressIn?.(2)} - onLongPress={() => onLongPress?.(2)}> - - + ) + case 3: + return ( + + + + + + - - - onPress?.(1)} - onPressIn={() => onPressIn?.(1)} - onLongPress={() => onLongPress?.(1)}> - - - - onPress?.(3)} - onPressIn={() => onPressIn?.(3)} - onLongPress={() => onLongPress?.(3)}> - - + ) + case 4: + return ( + + + + + + + + + - - ) + ) + default: + return null } - return } 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 f37fba34..6a775984 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 ( - - openLightbox(0)} - onLongPress={() => onLongPress(0)} - onPressIn={() => onPressIn(0)} - style={styles.singleImage} - /> - - ) - default: - return ( - - - - ) + if (images.length === 1) { + const {alt, thumb} = images[0] + return ( + + openLightbox(0)} + onLongPress={() => onLongPress(0)} + onPressIn={() => onPressIn(0)} + style={styles.singleImage}> + {alt === '' ? null : ( + { + onPressAltText(alt) + }}> + ALT + + )} + + + ) } + + return ( + + + + ) + // } } } @@ -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, + }, })