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