Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Image alt text view modal (#551)

* Image alt text view modal

* Minor style tweaks

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

authored by

Ollie H
Paul Frazee
and committed by
GitHub
dbb3c5c1 0ec98c77

+272 -192
+7 -1
src/state/models/ui/shell.ts
··· 47 47 onAltTextSet: (altText?: string) => void 48 48 } 49 49 50 + export interface AltTextImageReadModal { 51 + name: 'alt-text-image-read' 52 + altText: string 53 + } 54 + 50 55 export interface DeleteAccountModal { 51 56 name: 'delete-account' 52 57 } ··· 93 98 | ReportAccountModal 94 99 | ReportPostModal 95 100 96 - // Posting 101 + // Posts 97 102 | AltTextImageModal 103 + | AltTextImageReadModal 98 104 | CropImageModal 99 105 | ServerInputModal 100 106 | RepostModal
+75
src/view/com/modals/AltImageRead.tsx
··· 1 + import React, {useCallback} from 'react' 2 + import {StyleSheet, View} from 'react-native' 3 + import {usePalette} from 'lib/hooks/usePalette' 4 + import {gradients, s} from 'lib/styles' 5 + import {Text} from '../util/text/Text' 6 + import {TouchableOpacity} from 'react-native-gesture-handler' 7 + import LinearGradient from 'react-native-linear-gradient' 8 + import {useStores} from 'state/index' 9 + import {isDesktopWeb} from 'platform/detection' 10 + 11 + export const snapPoints = ['70%'] 12 + 13 + interface Props { 14 + altText: string 15 + } 16 + 17 + export function Component({altText}: Props) { 18 + const pal = usePalette('default') 19 + const store = useStores() 20 + 21 + const onPress = useCallback(() => { 22 + store.shell.closeModal() 23 + }, [store]) 24 + 25 + return ( 26 + <View 27 + testID="altTextImageModal" 28 + style={[pal.view, styles.container, s.flex1]}> 29 + <Text style={[styles.title, pal.text]}>Image description</Text> 30 + <View style={[styles.text, pal.viewLight]}> 31 + <Text style={pal.text}>{altText}</Text> 32 + </View> 33 + <TouchableOpacity testID="altTextImageSaveBtn" onPress={onPress}> 34 + <LinearGradient 35 + colors={[gradients.blueLight.start, gradients.blueLight.end]} 36 + start={{x: 0, y: 0}} 37 + end={{x: 1, y: 1}} 38 + style={[styles.button]}> 39 + <Text type="button-lg" style={[s.white, s.bold]}> 40 + Done 41 + </Text> 42 + </LinearGradient> 43 + </TouchableOpacity> 44 + </View> 45 + ) 46 + } 47 + 48 + const styles = StyleSheet.create({ 49 + container: { 50 + gap: 18, 51 + paddingVertical: isDesktopWeb ? 0 : 18, 52 + paddingHorizontal: isDesktopWeb ? 0 : 12, 53 + height: '100%', 54 + width: '100%', 55 + }, 56 + title: { 57 + textAlign: 'center', 58 + fontWeight: 'bold', 59 + fontSize: 24, 60 + }, 61 + text: { 62 + borderRadius: 5, 63 + marginVertical: 18, 64 + paddingHorizontal: 18, 65 + paddingVertical: 16, 66 + }, 67 + button: { 68 + flexDirection: 'row', 69 + alignItems: 'center', 70 + justifyContent: 'center', 71 + width: '100%', 72 + borderRadius: 32, 73 + padding: 10, 74 + }, 75 + })
+4
src/view/com/modals/Modal.tsx
··· 13 13 import * as ReportPostModal from './ReportPost' 14 14 import * as RepostModal from './Repost' 15 15 import * as AltImageModal from './AltImage' 16 + import * as AltImageReadModal from './AltImageRead' 16 17 import * as ReportAccountModal from './ReportAccount' 17 18 import * as DeleteAccountModal from './DeleteAccount' 18 19 import * as ChangeHandleModal from './ChangeHandle' ··· 74 75 } else if (activeModal?.name === 'alt-text-image') { 75 76 snapPoints = AltImageModal.snapPoints 76 77 element = <AltImageModal.Component {...activeModal} /> 78 + } else if (activeModal?.name === 'alt-text-image-read') { 79 + snapPoints = AltImageReadModal.snapPoints 80 + element = <AltImageReadModal.Component {...activeModal} /> 77 81 } else if (activeModal?.name === 'change-handle') { 78 82 snapPoints = ChangeHandleModal.snapPoints 79 83 element = <ChangeHandleModal.Component {...activeModal} />
+3
src/view/com/modals/Modal.web.tsx
··· 15 15 import * as RepostModal from './Repost' 16 16 import * as CropImageModal from './crop-image/CropImage.web' 17 17 import * as AltTextImageModal from './AltImage' 18 + import * as AltTextImageReadModal from './AltImageRead' 18 19 import * as ChangeHandleModal from './ChangeHandle' 19 20 import * as WaitlistModal from './Waitlist' 20 21 import * as InviteCodesModal from './InviteCodes' ··· 84 85 element = <ContentFilteringSettingsModal.Component /> 85 86 } else if (modal.name === 'alt-text-image') { 86 87 element = <AltTextImageModal.Component {...modal} /> 88 + } else if (modal.name === 'alt-text-image-read') { 89 + element = <AltTextImageReadModal.Component {...modal} /> 87 90 } else { 88 91 return null 89 92 }
+76
src/view/com/util/images/Gallery.tsx
··· 1 + import {AppBskyEmbedImages} from '@atproto/api' 2 + import React, {ComponentProps, FC, useCallback} from 'react' 3 + import {Pressable, StyleSheet, Text, TouchableOpacity, View} from 'react-native' 4 + import {Image} from 'expo-image' 5 + import {useStores} from 'state/index' 6 + 7 + type EventFunction = (index: number) => void 8 + 9 + interface GalleryItemProps { 10 + images: AppBskyEmbedImages.ViewImage[] 11 + index: number 12 + onPress?: EventFunction 13 + onLongPress?: EventFunction 14 + onPressIn?: EventFunction 15 + imageStyle: ComponentProps<typeof Image>['style'] 16 + } 17 + 18 + const DELAY_PRESS_IN = 500 19 + 20 + export const GalleryItem: FC<GalleryItemProps> = ({ 21 + images, 22 + index, 23 + imageStyle, 24 + onPress, 25 + onPressIn, 26 + onLongPress, 27 + }) => { 28 + const image = images[index] 29 + const store = useStores() 30 + 31 + const onPressAltText = useCallback(() => { 32 + store.shell.openModal({ 33 + name: 'alt-text-image-read', 34 + altText: image.alt, 35 + }) 36 + }, [image.alt, store.shell]) 37 + 38 + return ( 39 + <View> 40 + <TouchableOpacity 41 + delayPressIn={DELAY_PRESS_IN} 42 + onPress={() => onPress?.(index)} 43 + onPressIn={() => onPressIn?.(index)} 44 + onLongPress={() => onLongPress?.(index)}> 45 + <Image 46 + source={{uri: image.thumb}} 47 + style={imageStyle} 48 + accessible={true} 49 + accessibilityLabel={image.alt} 50 + /> 51 + </TouchableOpacity> 52 + {image.alt === '' ? null : ( 53 + <Pressable onPress={onPressAltText}> 54 + <Text style={styles.alt}>ALT</Text> 55 + </Pressable> 56 + )} 57 + </View> 58 + ) 59 + } 60 + 61 + const styles = StyleSheet.create({ 62 + alt: { 63 + backgroundColor: 'rgba(0, 0, 0, 0.75)', 64 + borderRadius: 6, 65 + color: 'white', 66 + fontSize: 12, 67 + fontWeight: 'bold', 68 + letterSpacing: 1, 69 + paddingHorizontal: 10, 70 + paddingVertical: 3, 71 + position: 'absolute', 72 + left: 10, 73 + top: -26, 74 + width: 46, 75 + }, 76 + })
+41 -163
src/view/com/util/images/ImageLayoutGrid.tsx
··· 3 3 LayoutChangeEvent, 4 4 StyleProp, 5 5 StyleSheet, 6 - TouchableOpacity, 7 6 View, 8 7 ViewStyle, 9 8 } from 'react-native' 10 - import {Image, ImageStyle} from 'expo-image' 9 + import {ImageStyle} from 'expo-image' 11 10 import {Dimensions} from 'lib/media/types' 12 11 import {AppBskyEmbedImages} from '@atproto/api' 13 - 14 - export const DELAY_PRESS_IN = 500 12 + import {GalleryItem} from './Gallery' 15 13 16 14 interface ImageLayoutGridProps { 17 15 images: AppBskyEmbedImages.ViewImage[] ··· 21 19 style?: StyleProp<ViewStyle> 22 20 } 23 21 24 - export function ImageLayoutGrid({ 25 - images, 26 - onPress, 27 - onLongPress, 28 - onPressIn, 29 - style, 30 - }: ImageLayoutGridProps) { 22 + export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) { 31 23 const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>() 32 24 33 25 const onLayout = (evt: LayoutChangeEvent) => { 26 + const {width, height} = evt.nativeEvent.layout 34 27 setContainerInfo({ 35 - width: evt.nativeEvent.layout.width, 36 - height: evt.nativeEvent.layout.height, 28 + width, 29 + height, 37 30 }) 38 31 } 39 32 40 33 return ( 41 34 <View style={style} onLayout={onLayout}> 42 35 {containerInfo ? ( 43 - <ImageLayoutGridInner 44 - images={images} 45 - onPress={onPress} 46 - onPressIn={onPressIn} 47 - onLongPress={onLongPress} 48 - containerInfo={containerInfo} 49 - /> 36 + <ImageLayoutGridInner {...props} containerInfo={containerInfo} /> 50 37 ) : undefined} 51 38 </View> 52 39 ) ··· 61 48 } 62 49 63 50 function ImageLayoutGridInner({ 64 - images, 65 - onPress, 66 - onLongPress, 67 - onPressIn, 68 51 containerInfo, 52 + ...props 69 53 }: ImageLayoutGridInnerProps) { 70 - const count = images.length 54 + const count = props.images.length 71 55 const size1 = useMemo<ImageStyle>(() => { 72 56 if (count === 3) { 73 57 const size = (containerInfo.width - 10) / 3 ··· 87 71 } 88 72 }, [count, containerInfo]) 89 73 90 - if (count === 2) { 91 - return ( 92 - <View style={styles.flexRow}> 93 - <TouchableOpacity 94 - delayPressIn={DELAY_PRESS_IN} 95 - onPress={() => onPress?.(0)} 96 - onPressIn={() => onPressIn?.(0)} 97 - onLongPress={() => onLongPress?.(0)}> 98 - <Image 99 - source={{uri: images[0].thumb}} 100 - style={size1} 101 - accessible={true} 102 - accessibilityLabel={images[0].alt} 103 - /> 104 - </TouchableOpacity> 105 - <View style={styles.wSpace} /> 106 - <TouchableOpacity 107 - delayPressIn={DELAY_PRESS_IN} 108 - onPress={() => onPress?.(1)} 109 - onPressIn={() => onPressIn?.(1)} 110 - onLongPress={() => onLongPress?.(1)}> 111 - <Image 112 - source={{uri: images[1].thumb}} 113 - style={size1} 114 - accessible={true} 115 - accessibilityLabel={images[1].alt} 116 - /> 117 - </TouchableOpacity> 118 - </View> 119 - ) 120 - } 121 - if (count === 3) { 122 - return ( 123 - <View style={styles.flexRow}> 124 - <TouchableOpacity 125 - delayPressIn={DELAY_PRESS_IN} 126 - onPress={() => onPress?.(0)} 127 - onPressIn={() => onPressIn?.(0)} 128 - onLongPress={() => onLongPress?.(0)}> 129 - <Image 130 - source={{uri: images[0].thumb}} 131 - style={size2} 132 - accessible={true} 133 - accessibilityLabel={images[0].alt} 134 - /> 135 - </TouchableOpacity> 136 - <View style={styles.wSpace} /> 137 - <View> 138 - <TouchableOpacity 139 - delayPressIn={DELAY_PRESS_IN} 140 - onPress={() => onPress?.(1)} 141 - onPressIn={() => onPressIn?.(1)} 142 - onLongPress={() => onLongPress?.(1)}> 143 - <Image 144 - source={{uri: images[1].thumb}} 145 - style={size1} 146 - accessible={true} 147 - accessibilityLabel={images[1].alt} 148 - /> 149 - </TouchableOpacity> 150 - <View style={styles.hSpace} /> 151 - <TouchableOpacity 152 - delayPressIn={DELAY_PRESS_IN} 153 - onPress={() => onPress?.(2)} 154 - onPressIn={() => onPressIn?.(2)} 155 - onLongPress={() => onLongPress?.(2)}> 156 - <Image 157 - source={{uri: images[2].thumb}} 158 - style={size1} 159 - accessible={true} 160 - accessibilityLabel={images[2].alt} 161 - /> 162 - </TouchableOpacity> 74 + switch (count) { 75 + case 2: 76 + return ( 77 + <View style={styles.flexRow}> 78 + <GalleryItem index={0} {...props} imageStyle={size1} /> 79 + <GalleryItem index={1} {...props} imageStyle={size1} /> 163 80 </View> 164 - </View> 165 - ) 166 - } 167 - if (count === 4) { 168 - return ( 169 - <View style={styles.flexRow}> 170 - <View> 171 - <TouchableOpacity 172 - delayPressIn={DELAY_PRESS_IN} 173 - onPress={() => onPress?.(0)} 174 - onPressIn={() => onPressIn?.(0)} 175 - onLongPress={() => onLongPress?.(0)}> 176 - <Image 177 - source={{uri: images[0].thumb}} 178 - style={size1} 179 - accessible={true} 180 - accessibilityLabel={images[0].alt} 181 - /> 182 - </TouchableOpacity> 183 - <View style={styles.hSpace} /> 184 - <TouchableOpacity 185 - delayPressIn={DELAY_PRESS_IN} 186 - onPress={() => onPress?.(2)} 187 - onPressIn={() => onPressIn?.(2)} 188 - onLongPress={() => onLongPress?.(2)}> 189 - <Image 190 - source={{uri: images[2].thumb}} 191 - style={size1} 192 - accessible={true} 193 - accessibilityLabel={images[2].alt} 194 - /> 195 - </TouchableOpacity> 81 + ) 82 + case 3: 83 + return ( 84 + <View style={styles.flexRow}> 85 + <GalleryItem index={0} {...props} imageStyle={size2} /> 86 + <View style={styles.flexColumn}> 87 + <GalleryItem index={1} {...props} imageStyle={size1} /> 88 + <GalleryItem index={2} {...props} imageStyle={size1} /> 89 + </View> 196 90 </View> 197 - <View style={styles.wSpace} /> 198 - <View> 199 - <TouchableOpacity 200 - delayPressIn={DELAY_PRESS_IN} 201 - onPress={() => onPress?.(1)} 202 - onPressIn={() => onPressIn?.(1)} 203 - onLongPress={() => onLongPress?.(1)}> 204 - <Image 205 - source={{uri: images[1].thumb}} 206 - style={size1} 207 - accessible={true} 208 - accessibilityLabel={images[1].alt} 209 - /> 210 - </TouchableOpacity> 211 - <View style={styles.hSpace} /> 212 - <TouchableOpacity 213 - delayPressIn={DELAY_PRESS_IN} 214 - onPress={() => onPress?.(3)} 215 - onPressIn={() => onPressIn?.(3)} 216 - onLongPress={() => onLongPress?.(3)}> 217 - <Image 218 - source={{uri: images[3].thumb}} 219 - style={size1} 220 - accessible={true} 221 - accessibilityLabel={images[3].alt} 222 - /> 223 - </TouchableOpacity> 91 + ) 92 + case 4: 93 + return ( 94 + <View style={styles.flexRow}> 95 + <View style={styles.flexColumn}> 96 + <GalleryItem index={0} {...props} imageStyle={size1} /> 97 + <GalleryItem index={2} {...props} imageStyle={size1} /> 98 + </View> 99 + <View style={styles.flexColumn}> 100 + <GalleryItem index={1} {...props} imageStyle={size1} /> 101 + <GalleryItem index={3} {...props} imageStyle={size1} /> 102 + </View> 224 103 </View> 225 - </View> 226 - ) 104 + ) 105 + default: 106 + return null 227 107 } 228 - return <View /> 229 108 } 230 109 231 110 const styles = StyleSheet.create({ 232 - flexRow: {flexDirection: 'row'}, 233 - wSpace: {width: 5}, 234 - hSpace: {height: 5}, 111 + flexRow: {flexDirection: 'row', gap: 5}, 112 + flexColumn: {flexDirection: 'column', gap: 5}, 235 113 })
+66 -28
src/view/com/util/post-embeds/index.tsx
··· 1 - import React from 'react' 1 + import React, {useCallback} from 'react' 2 2 import { 3 3 StyleSheet, 4 4 StyleProp, 5 5 View, 6 6 ViewStyle, 7 7 Image as RNImage, 8 + Pressable, 9 + Text, 8 10 } from 'react-native' 9 11 import { 10 12 AppBskyEmbedImages, ··· 14 16 AppBskyFeedPost, 15 17 } from '@atproto/api' 16 18 import {Link} from '../Link' 17 - import {AutoSizedImage} from '../images/AutoSizedImage' 18 19 import {ImageLayoutGrid} from '../images/ImageLayoutGrid' 19 20 import {ImagesLightbox} from 'state/models/ui/shell' 20 21 import {useStores} from 'state/index' ··· 24 25 import {ExternalLinkEmbed} from './ExternalLinkEmbed' 25 26 import {getYoutubeVideoId} from 'lib/strings/url-helpers' 26 27 import QuoteEmbed from './QuoteEmbed' 28 + import {AutoSizedImage} from '../images/AutoSizedImage' 27 29 28 30 type Embed = 29 31 | AppBskyEmbedRecord.View ··· 42 44 const pal = usePalette('default') 43 45 const store = useStores() 44 46 47 + const onPressAltText = useCallback( 48 + (alt: string) => { 49 + store.shell.openModal({ 50 + name: 'alt-text-image-read', 51 + altText: alt, 52 + }) 53 + }, 54 + [store.shell], 55 + ) 56 + 45 57 if ( 46 58 AppBskyEmbedRecordWithMedia.isView(embed) && 47 59 AppBskyEmbedRecord.isViewRecord(embed.record.record) && ··· 88 100 } 89 101 90 102 if (AppBskyEmbedImages.isView(embed)) { 91 - if (embed.images.length > 0) { 103 + const {images} = embed 104 + 105 + if (images.length > 0) { 92 106 const uris = embed.images.map(img => img.fullsize) 93 107 const openLightbox = (index: number) => { 94 108 store.shell.openLightbox(new ImagesLightbox(uris, index)) ··· 107 121 }) 108 122 } 109 123 110 - switch (embed.images.length) { 111 - case 1: 112 - return ( 113 - <View style={[styles.imagesContainer, style]}> 114 - <AutoSizedImage 115 - alt={embed.images[0].alt} 116 - uri={embed.images[0].thumb} 117 - onPress={() => openLightbox(0)} 118 - onLongPress={() => onLongPress(0)} 119 - onPressIn={() => onPressIn(0)} 120 - style={styles.singleImage} 121 - /> 122 - </View> 123 - ) 124 - default: 125 - return ( 126 - <View style={[styles.imagesContainer, style]}> 127 - <ImageLayoutGrid 128 - images={embed.images} 129 - onPress={openLightbox} 130 - onLongPress={onLongPress} 131 - onPressIn={onPressIn} 132 - /> 133 - </View> 134 - ) 124 + if (images.length === 1) { 125 + const {alt, thumb} = images[0] 126 + return ( 127 + <View style={[styles.imagesContainer, style]}> 128 + <AutoSizedImage 129 + alt={alt} 130 + uri={thumb} 131 + onPress={() => openLightbox(0)} 132 + onLongPress={() => onLongPress(0)} 133 + onPressIn={() => onPressIn(0)} 134 + style={styles.singleImage}> 135 + {alt === '' ? null : ( 136 + <Pressable 137 + onPress={() => { 138 + onPressAltText(alt) 139 + }}> 140 + <Text style={styles.alt}>ALT</Text> 141 + </Pressable> 142 + )} 143 + </AutoSizedImage> 144 + </View> 145 + ) 135 146 } 147 + 148 + return ( 149 + <View style={[styles.imagesContainer, style]}> 150 + <ImageLayoutGrid 151 + images={embed.images} 152 + onPress={openLightbox} 153 + onLongPress={onLongPress} 154 + onPressIn={onPressIn} 155 + style={embed.images.length === 1 ? styles.singleImage : undefined} 156 + /> 157 + </View> 158 + ) 159 + // } 136 160 } 137 161 } 138 162 ··· 171 195 borderWidth: 1, 172 196 borderRadius: 8, 173 197 marginTop: 4, 198 + }, 199 + alt: { 200 + backgroundColor: 'rgba(0, 0, 0, 0.75)', 201 + borderRadius: 6, 202 + color: 'white', 203 + fontSize: 12, 204 + fontWeight: 'bold', 205 + letterSpacing: 1, 206 + paddingHorizontal: 10, 207 + paddingVertical: 3, 208 + position: 'absolute', 209 + left: 10, 210 + top: -26, 211 + width: 46, 174 212 }, 175 213 })