Something went wrong. Try again.
hardened bluesky app fork with microcosm
Something went wrong. Try again.
7.1 kB · 260 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'import {Image} from 'expo-image'import { AppBskyEmbedGallery, type AppBskyEmbedImages, type AppBskyFeedDefs,} from '@atproto/api'import {Trans, useLingui} from '@lingui/react/macro'
import {shareImageModal} from '#/lib/media/manip'import {useSaveImageToMediaLibrary} from '#/lib/media/save-image'import {isGifEmbed} from '#/lib/strings/embed-player'import {atoms as a, tokens, useTheme} from '#/alf'import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight'import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download'import {MediaInsetBorder} from '#/components/MediaInsetBorder'import * as PeekMenu from '#/components/PeekMenu'import {Text} from '#/components/Typography'import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'import * as bsky from '#/types/bsky'
/** * Streamlined MediaPreview component which just handles images, gifs, and videos */export function Embed({ embed, style, peekable = false,}: { embed: AppBskyFeedDefs.PostView['embed'] style?: StyleProp<ViewStyle> peekable?: boolean}) { const e = bsky.post.parseEmbed(embed)
if (!e) return null
if (e.type === 'images') { return ( <Outer style={style}> {e.view.images.map(image => peekable ? ( <PeekableImageItem key={image.thumb} image={image} /> ) : ( <ImageItem key={image.thumb} thumbnail={image.thumb} alt={image.alt} /> ), )} </Outer> ) } else if (e.type === 'gallery') { // Notification/DM preview is a narrow inline strip; cap at 4 tiles so // a 10-image gallery doesn't blow out the row width. Single pass instead // of filter().slice().map() so we stop at 4 viewable items rather than // walking every item in a 10-image gallery. const tiles: React.ReactNode[] = [] for (const item of e.view.items) { if (tiles.length >= 4) break if (!AppBskyEmbedGallery.isViewImage(item)) continue if (peekable) { const image: AppBskyEmbedImages.ViewImage = { thumb: item.thumbnail, fullsize: item.fullsize, alt: item.alt, aspectRatio: item.aspectRatio, } tiles.push(<PeekableImageItem key={item.thumbnail} image={image} />) } else { tiles.push( <ImageItem key={item.thumbnail} thumbnail={item.thumbnail} alt={item.alt} />, ) } } return <Outer style={style}>{tiles}</Outer> } else if (e.type === 'link') { if (!e.view.external.thumb) return null if (!isGifEmbed(e.view.external.uri)) return null return ( <Outer style={style}> <GifItem thumbnail={e.view.external.thumb} alt={e.view.external.title} /> </Outer> ) } else if (e.type === 'video') { return ( <Outer style={style}> {e.view.presentation === 'gif' ? ( <GifItem thumbnail={e.view.thumbnail} alt={e.view.alt} /> ) : ( <VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} /> )} </Outer> ) } else if ( e.type === 'post_with_media' && // ignore further "nested" RecordWithMedia e.media.type !== 'post_with_media' && // ignore any unknowns e.media.view !== null ) { return <Embed embed={e.media.view} style={style} /> }
return null}
export function Outer({ children, style,}: { children?: React.ReactNode style?: StyleProp<ViewStyle>}) { return <View style={[a.flex_row, a.gap_xs, style]}>{children}</View>}
export function ImageItem({ thumbnail, alt, children, maxWidth = 100,}: { thumbnail?: string alt?: string children?: React.ReactNode maxWidth?: number}) { const t = useTheme()
if (!thumbnail) { return ( <View style={[ {backgroundColor: 'black'}, a.flex_1, a.aspect_square, {maxWidth}, a.rounded_xs, ]} accessibilityLabel={alt} accessibilityHint=""> {children} </View> ) }
return ( <View style={[a.flex_grow, a.relative, a.aspect_square, {maxWidth}]}> <Image key={thumbnail} source={{uri: thumbnail}} alt={alt} style={[a.flex_1, a.rounded_xs, t.atoms.bg_contrast_25]} contentFit="cover" accessible={true} accessibilityIgnoresInvertColors useAppleWebpCodec /> <MediaInsetBorder style={[a.rounded_xs]} /> {children} </View> )}
export function GifItem({thumbnail, alt}: {thumbnail?: string; alt?: string}) { return ( <ImageItem thumbnail={thumbnail} alt={alt}> <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> <PlayButtonIcon size={24} /> </View> <View style={styles.altContainer}> <Text style={styles.alt}> <Trans>GIF</Trans> </Text> </View> </ImageItem> )}
export function VideoItem({ thumbnail, alt,}: { thumbnail?: string alt?: string}) { return ( <ImageItem thumbnail={thumbnail} alt={alt}> <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> <PlayButtonIcon size={24} /> </View> </ImageItem> )}
function PeekableImageItem({image}: {image: AppBskyEmbedImages.ViewImage}) { const {t: l} = useLingui() const saveImage = useSaveImageToMediaLibrary()
const aspect = image.aspectRatio && image.aspectRatio.height > 0 ? image.aspectRatio.width / image.aspectRatio.height : undefined
return ( <PeekMenu.Root style={[a.flex_1, {maxWidth: 100}]}> <PeekMenu.Trigger preview={{ type: 'image', uri: image.fullsize, thumbUri: image.thumb, aspectRatio: aspect && aspect > 0 ? aspect : 1, }} borderRadius={tokens.borderRadius.xs}> <ImageItem thumbnail={image.thumb} alt={image.alt} /> </PeekMenu.Trigger> <PeekMenu.Menu> <PeekMenu.MenuItem id="save" onSelect={() => void saveImage(image.fullsize)}> <PeekMenu.MenuItemIcon icon={DownloadIcon} /> <PeekMenu.MenuItemText>{l`Save image`}</PeekMenu.MenuItemText> </PeekMenu.MenuItem> <PeekMenu.MenuItem id="share" onSelect={() => void shareImageModal({uri: image.fullsize})}> <PeekMenu.MenuItemIcon icon={ShareIcon} /> <PeekMenu.MenuItemText>{l`Share`}</PeekMenu.MenuItemText> </PeekMenu.MenuItem> </PeekMenu.Menu> </PeekMenu.Root> )}
const styles = StyleSheet.create({ altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, paddingHorizontal: 6, paddingVertical: 3, position: 'absolute', left: 5, bottom: 5, zIndex: 2, }, alt: { color: 'white', fontSize: 7, fontWeight: '600', },})