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