Bluesky app fork with some witchin' additions 馃挮
at jean/pds-label 183 lines 4.4 kB view raw
1import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' 2import {Image} from 'expo-image' 3import {type AppBskyFeedDefs} from '@atproto/api' 4import {Trans} from '@lingui/react/macro' 5 6import {isTenorGifUri} from '#/lib/strings/embed-player' 7import { 8 maybeModifyHighQualityImage, 9 useHighQualityImages, 10} from '#/state/preferences/high-quality-images' 11import {atoms as a, useTheme} from '#/alf' 12import {MediaInsetBorder} from '#/components/MediaInsetBorder' 13import {Text} from '#/components/Typography' 14import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 15import * as bsky from '#/types/bsky' 16 17/** 18 * Streamlined MediaPreview component which just handles images, gifs, and videos 19 */ 20export function Embed({ 21 embed, 22 style, 23}: { 24 embed: AppBskyFeedDefs.PostView['embed'] 25 style?: StyleProp<ViewStyle> 26}) { 27 const highQualityImages = useHighQualityImages() 28 const e = bsky.post.parseEmbed(embed) 29 30 if (!e) return null 31 32 if (e.type === 'images') { 33 return ( 34 <Outer style={style}> 35 {e.view.images.map(image => ( 36 <ImageItem 37 key={image.thumb} 38 thumbnail={maybeModifyHighQualityImage( 39 image.thumb, 40 highQualityImages, 41 )} 42 alt={image.alt} 43 /> 44 ))} 45 </Outer> 46 ) 47 } else if (e.type === 'link') { 48 if (!e.view.external.thumb) return null 49 if (!isTenorGifUri(e.view.external.uri)) return null 50 return ( 51 <Outer style={style}> 52 <GifItem 53 thumbnail={e.view.external.thumb} 54 alt={e.view.external.title} 55 /> 56 </Outer> 57 ) 58 } else if (e.type === 'video') { 59 return ( 60 <Outer style={style}> 61 {e.view.presentation === 'gif' ? ( 62 <GifItem thumbnail={e.view.thumbnail} alt={e.view.alt} /> 63 ) : ( 64 <VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} /> 65 )} 66 </Outer> 67 ) 68 } else if ( 69 e.type === 'post_with_media' && 70 // ignore further "nested" RecordWithMedia 71 e.media.type !== 'post_with_media' && 72 // ignore any unknowns 73 e.media.view !== null 74 ) { 75 return <Embed embed={e.media.view} style={style} /> 76 } 77 78 return null 79} 80 81export function Outer({ 82 children, 83 style, 84}: { 85 children?: React.ReactNode 86 style?: StyleProp<ViewStyle> 87}) { 88 return <View style={[a.flex_row, a.gap_xs, style]}>{children}</View> 89} 90 91export function ImageItem({ 92 thumbnail, 93 alt, 94 children, 95}: { 96 thumbnail?: string 97 alt?: string 98 children?: React.ReactNode 99}) { 100 const t = useTheme() 101 102 if (!thumbnail) { 103 return ( 104 <View 105 style={[ 106 {backgroundColor: 'black'}, 107 a.flex_1, 108 a.aspect_square, 109 {maxWidth: 100}, 110 a.rounded_xs, 111 ]} 112 accessibilityLabel={alt} 113 accessibilityHint=""> 114 {children} 115 </View> 116 ) 117 } 118 119 return ( 120 <View style={[a.relative, a.flex_1, a.aspect_square, {maxWidth: 100}]}> 121 <Image 122 key={thumbnail} 123 source={{uri: thumbnail}} 124 alt={alt} 125 style={[a.flex_1, a.rounded_xs, t.atoms.bg_contrast_25]} 126 contentFit="cover" 127 accessible={true} 128 accessibilityIgnoresInvertColors 129 /> 130 <MediaInsetBorder style={[a.rounded_xs]} /> 131 {children} 132 </View> 133 ) 134} 135 136export function GifItem({thumbnail, alt}: {thumbnail?: string; alt?: string}) { 137 return ( 138 <ImageItem thumbnail={thumbnail} alt={alt}> 139 <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> 140 <PlayButtonIcon size={24} /> 141 </View> 142 <View style={styles.altContainer}> 143 <Text style={styles.alt}> 144 <Trans>GIF</Trans> 145 </Text> 146 </View> 147 </ImageItem> 148 ) 149} 150 151export function VideoItem({ 152 thumbnail, 153 alt, 154}: { 155 thumbnail?: string 156 alt?: string 157}) { 158 return ( 159 <ImageItem thumbnail={thumbnail} alt={alt}> 160 <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> 161 <PlayButtonIcon size={24} /> 162 </View> 163 </ImageItem> 164 ) 165} 166 167const styles = StyleSheet.create({ 168 altContainer: { 169 backgroundColor: 'rgba(0, 0, 0, 0.75)', 170 borderRadius: 6, 171 paddingHorizontal: 6, 172 paddingVertical: 3, 173 position: 'absolute', 174 left: 5, 175 bottom: 5, 176 zIndex: 2, 177 }, 178 alt: { 179 color: 'white', 180 fontSize: 7, 181 fontWeight: '600', 182 }, 183})