my fork of the bluesky client
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})