forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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/macro'
5
6import {isTenorGifUri} from '#/lib/strings/embed-player'
7import {atoms as a, useTheme} from '#/alf'
8import {MediaInsetBorder} from '#/components/MediaInsetBorder'
9import {Text} from '#/components/Typography'
10import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
11import * as bsky from '#/types/bsky'
12
13/**
14 * Streamlined MediaPreview component which just handles images, gifs, and videos
15 */
16export function Embed({
17 embed,
18 style,
19}: {
20 embed: AppBskyFeedDefs.PostView['embed']
21 style?: StyleProp<ViewStyle>
22}) {
23 const e = bsky.post.parseEmbed(embed)
24
25 if (!e) return null
26
27 if (e.type === 'images') {
28 return (
29 <Outer style={style}>
30 {e.view.images.map(image => (
31 <ImageItem
32 key={image.thumb}
33 thumbnail={image.thumb}
34 alt={image.alt}
35 />
36 ))}
37 </Outer>
38 )
39 } else if (e.type === 'link') {
40 if (!e.view.external.thumb) return null
41 if (!isTenorGifUri(e.view.external.uri)) return null
42 return (
43 <Outer style={style}>
44 <GifItem
45 thumbnail={e.view.external.thumb}
46 alt={e.view.external.title}
47 />
48 </Outer>
49 )
50 } else if (e.type === 'video') {
51 return (
52 <Outer style={style}>
53 {e.view.presentation === 'gif' ? (
54 <GifItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
55 ) : (
56 <VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
57 )}
58 </Outer>
59 )
60 } else if (
61 e.type === 'post_with_media' &&
62 // ignore further "nested" RecordWithMedia
63 e.media.type !== 'post_with_media' &&
64 // ignore any unknowns
65 e.media.view !== null
66 ) {
67 return <Embed embed={e.media.view} style={style} />
68 }
69
70 return null
71}
72
73export function Outer({
74 children,
75 style,
76}: {
77 children?: React.ReactNode
78 style?: StyleProp<ViewStyle>
79}) {
80 return <View style={[a.flex_row, a.gap_xs, style]}>{children}</View>
81}
82
83export function ImageItem({
84 thumbnail,
85 alt,
86 children,
87}: {
88 thumbnail?: string
89 alt?: string
90 children?: React.ReactNode
91}) {
92 const t = useTheme()
93
94 if (!thumbnail) {
95 return (
96 <View
97 style={[
98 {backgroundColor: 'black'},
99 a.flex_1,
100 a.aspect_square,
101 {maxWidth: 100},
102 a.rounded_xs,
103 ]}
104 accessibilityLabel={alt}
105 accessibilityHint="">
106 {children}
107 </View>
108 )
109 }
110
111 return (
112 <View style={[a.relative, a.flex_1, a.aspect_square, {maxWidth: 100}]}>
113 <Image
114 key={thumbnail}
115 source={{uri: thumbnail}}
116 alt={alt}
117 style={[a.flex_1, a.rounded_xs, t.atoms.bg_contrast_25]}
118 contentFit="cover"
119 accessible={true}
120 accessibilityIgnoresInvertColors
121 />
122 <MediaInsetBorder style={[a.rounded_xs]} />
123 {children}
124 </View>
125 )
126}
127
128export function GifItem({thumbnail, alt}: {thumbnail?: string; alt?: string}) {
129 return (
130 <ImageItem thumbnail={thumbnail} alt={alt}>
131 <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
132 <PlayButtonIcon size={24} />
133 </View>
134 <View style={styles.altContainer}>
135 <Text style={styles.alt}>
136 <Trans>GIF</Trans>
137 </Text>
138 </View>
139 </ImageItem>
140 )
141}
142
143export function VideoItem({
144 thumbnail,
145 alt,
146}: {
147 thumbnail?: string
148 alt?: string
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 left: 5,
167 bottom: 5,
168 zIndex: 2,
169 },
170 alt: {
171 color: 'white',
172 fontSize: 7,
173 fontWeight: '600',
174 },
175})