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