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