import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' import {Image} from 'expo-image' 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 {app} from '#/lexicons' import * as bsky from '#/types/bsky' /** * Streamlined MediaPreview component which just handles images, gifs, and videos */ export function Embed({ embed, style, peekable = false, }: { embed: app.bsky.feed.defs.PostView['embed'] style?: StyleProp peekable?: boolean }) { const e = bsky.post.parseEmbed(embed) if (!e) return null if (e.type === 'images') { return ( {e.view.images.map(image => peekable ? ( ) : ( ), )} ) } 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 (!bsky.isType(app.bsky.embed.gallery.viewImage, item)) continue if (peekable) { const image: app.bsky.embed.images.ViewImage = { thumb: item.thumbnail, fullsize: item.fullsize, alt: item.alt, aspectRatio: item.aspectRatio, } tiles.push() } else { tiles.push( , ) } } return {tiles} } else if (e.type === 'link') { if (!e.view.external.thumb) return null if (!isGifEmbed(e.view.external.uri)) return null return ( ) } else if (e.type === 'video') { return ( {e.view.presentation === 'gif' ? ( ) : ( )} ) } else if ( e.type === 'post_with_media' && // ignore further "nested" RecordWithMedia e.media.type !== 'post_with_media' && // ignore any unknowns e.media.view !== null ) { /* * Every media arm's view is a `$Typed<...>` of a def that `PostView.embed` * also admits, but TS will not narrow the parsed union back into the raw * embed union, so the arm is re-asserted here. */ return ( ) } return null } export function Outer({ children, style, }: { children?: React.ReactNode style?: StyleProp }) { return {children} } export function ImageItem({ thumbnail, alt, children, maxWidth = 100, }: { thumbnail?: string alt?: string children?: React.ReactNode maxWidth?: number }) { const t = useTheme() if (!thumbnail) { return ( {children} ) } return ( {alt} {children} ) } export function GifItem({thumbnail, alt}: {thumbnail?: string; alt?: string}) { return ( GIF ) } export function VideoItem({ thumbnail, alt, }: { thumbnail?: string alt?: string }) { return ( ) } function PeekableImageItem({image}: {image: app.bsky.embed.images.ViewImage}) { const {t: l} = useLingui() const saveImage = useSaveImageToMediaLibrary() const aspect = image.aspectRatio && image.aspectRatio.height > 0 ? image.aspectRatio.width / image.aspectRatio.height : undefined return ( 0 ? aspect : 1, }} borderRadius={tokens.borderRadius.xs}> void saveImage(image.fullsize)}> {l`Save image`} void shareImageModal({uri: image.fullsize})}> {l`Share`} ) } 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', }, })