Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useMemo} from 'react'
2import {type StyleProp, View, type ViewStyle} from 'react-native'
3import {Image} from 'expo-image'
4import {type AppBskyEmbedExternal} from '@atproto/api'
5import {msg} from '@lingui/core/macro'
6import {useLingui} from '@lingui/react'
7
8import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
9import {useHaptics} from '#/lib/haptics'
10import {shareUrl} from '#/lib/sharing'
11import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
12import {toNiceDomain} from '#/lib/strings/url-helpers'
13import {useExternalEmbedsPrefs} from '#/state/preferences'
14import {useHighQualityImages} from '#/state/preferences/high-quality-images'
15import {
16 applyImageTransforms,
17 useImageCdnHost,
18} from '#/state/preferences/image-cdn-host'
19import {atoms as a, useTheme} from '#/alf'
20import {Divider} from '#/components/Divider'
21import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
22import {Link} from '#/components/Link'
23import {Text} from '#/components/Typography'
24import {IS_NATIVE} from '#/env'
25import {ExternalGif} from './ExternalGif'
26import {ExternalPlayer} from './ExternalPlayer'
27import {GifEmbed} from './Gif'
28
29export const ExternalEmbed = ({
30 link,
31 onOpen,
32 style,
33 hideAlt,
34}: {
35 link: AppBskyEmbedExternal.ViewExternal
36 onOpen?: () => void
37 style?: StyleProp<ViewStyle>
38 hideAlt?: boolean
39}) => {
40 const {_} = useLingui()
41 const t = useTheme()
42 const playHaptic = useHaptics()
43 const externalEmbedPrefs = useExternalEmbedsPrefs()
44 const highQualityImages = useHighQualityImages()
45 const imageCdnHost = useImageCdnHost()
46 const niceUrl = toNiceDomain(link.uri)
47 const imageUri = link.thumb
48 ? applyImageTransforms(link.thumb, {
49 imageCdnHost,
50 highQualityImages,
51 })
52 : undefined
53 const embedPlayerParams = useMemo(() => {
54 const params = parseEmbedPlayerFromUrl(link.uri)
55
56 if (params && externalEmbedPrefs?.[params.source] !== 'hide') {
57 return params
58 }
59 }, [link.uri, externalEmbedPrefs])
60 const hasMedia = Boolean(imageUri || embedPlayerParams)
61
62 const onPress = useCallback(() => {
63 playHaptic('Light')
64 onOpen?.()
65 }, [playHaptic, onOpen])
66
67 const onShareExternal = useCallback(() => {
68 if (link.uri && IS_NATIVE) {
69 playHaptic('Heavy')
70 shareUrl(link.uri)
71 }
72 }, [link.uri, playHaptic])
73
74 if (
75 embedPlayerParams?.source === 'tenor' ||
76 embedPlayerParams?.source === 'klipy'
77 ) {
78 const parsedAlt = parseAltFromGIFDescription(link.description)
79 return (
80 <View style={style}>
81 <GifEmbed
82 params={embedPlayerParams}
83 thumb={link.thumb}
84 altText={parsedAlt.alt}
85 isPreferredAltText={parsedAlt.isPreferred}
86 hideAlt={hideAlt}
87 />
88 </View>
89 )
90 }
91
92 return (
93 <Link
94 label={link.title || _(msg`Open link to ${niceUrl}`)}
95 to={link.uri}
96 shouldProxy={true}
97 onPress={onPress}
98 onLongPress={onShareExternal}>
99 {({hovered}) => (
100 <View
101 style={[
102 a.transition_color,
103 a.flex_col,
104 a.rounded_md,
105 a.overflow_hidden,
106 a.w_full,
107 a.border,
108 style,
109 hovered
110 ? t.atoms.border_contrast_high
111 : t.atoms.border_contrast_low,
112 ]}>
113 {imageUri && !embedPlayerParams ? (
114 <Image
115 style={[a.aspect_card]}
116 source={{uri: imageUri}}
117 accessibilityIgnoresInvertColors
118 loading="lazy"
119 />
120 ) : undefined}
121
122 {embedPlayerParams?.isGif ? (
123 <ExternalGif link={link} params={embedPlayerParams} />
124 ) : embedPlayerParams ? (
125 <ExternalPlayer link={link} params={embedPlayerParams} />
126 ) : undefined}
127
128 <View
129 style={[
130 a.flex_1,
131 a.pt_sm,
132 {gap: 3},
133 hasMedia && a.border_t,
134 hovered
135 ? t.atoms.border_contrast_high
136 : t.atoms.border_contrast_low,
137 ]}>
138 <View style={[{gap: 3}, a.pb_xs, a.px_md]}>
139 {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && (
140 <Text
141 emoji
142 numberOfLines={3}
143 style={[a.text_md, a.font_semi_bold, a.leading_snug]}>
144 {link.title || link.uri}
145 </Text>
146 )}
147 {link.description ? (
148 <Text
149 emoji
150 numberOfLines={imageUri ? 2 : 4}
151 style={[a.text_sm, a.leading_snug]}>
152 {link.description}
153 </Text>
154 ) : undefined}
155 </View>
156 <View style={[a.px_md]}>
157 <Divider />
158 <View
159 style={[
160 a.flex_row,
161 a.align_center,
162 a.gap_2xs,
163 a.pb_sm,
164 {
165 paddingTop: 6, // off menu
166 },
167 ]}>
168 <Globe
169 size="xs"
170 style={[
171 a.transition_color,
172 hovered
173 ? t.atoms.text_contrast_medium
174 : t.atoms.text_contrast_low,
175 ]}
176 />
177 <Text
178 numberOfLines={1}
179 style={[
180 a.transition_color,
181 a.text_xs,
182 a.leading_snug,
183 hovered
184 ? t.atoms.text_contrast_high
185 : t.atoms.text_contrast_medium,
186 ]}>
187 {toNiceDomain(link.uri)}
188 </Text>
189 </View>
190 </View>
191 </View>
192 </View>
193 )}
194 </Link>
195 )
196}