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