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