Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import React, {useCallback} 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 = React.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 (embedPlayerParams?.source === 'tenor') {
75 const parsedAlt = parseAltFromGIFDescription(link.description)
76 return (
77 <View style={style}>
78 <GifEmbed
79 params={embedPlayerParams}
80 thumb={link.thumb}
81 altText={parsedAlt.alt}
82 isPreferredAltText={parsedAlt.isPreferred}
83 hideAlt={hideAlt}
84 />
85 </View>
86 )
87 }
88
89 return (
90 <Link
91 label={link.title || _(msg`Open link to ${niceUrl}`)}
92 to={link.uri}
93 shouldProxy={true}
94 onPress={onPress}
95 onLongPress={onShareExternal}>
96 {({hovered}) => (
97 <View
98 style={[
99 a.transition_color,
100 a.flex_col,
101 a.rounded_md,
102 a.overflow_hidden,
103 a.w_full,
104 a.border,
105 style,
106 hovered
107 ? t.atoms.border_contrast_high
108 : t.atoms.border_contrast_low,
109 ]}>
110 {imageUri && !embedPlayerParams ? (
111 <Image
112 style={[a.aspect_card]}
113 source={{uri: imageUri}}
114 accessibilityIgnoresInvertColors
115 loading="lazy"
116 />
117 ) : undefined}
118
119 {embedPlayerParams?.isGif ? (
120 <ExternalGif link={link} params={embedPlayerParams} />
121 ) : embedPlayerParams ? (
122 <ExternalPlayer link={link} params={embedPlayerParams} />
123 ) : undefined}
124
125 <View
126 style={[
127 a.flex_1,
128 a.pt_sm,
129 {gap: 3},
130 hasMedia && a.border_t,
131 hovered
132 ? t.atoms.border_contrast_high
133 : t.atoms.border_contrast_low,
134 ]}>
135 <View style={[{gap: 3}, a.pb_xs, a.px_md]}>
136 {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && (
137 <Text
138 emoji
139 numberOfLines={3}
140 style={[a.text_md, a.font_semi_bold, a.leading_snug]}>
141 {link.title || link.uri}
142 </Text>
143 )}
144 {link.description ? (
145 <Text
146 emoji
147 numberOfLines={imageUri ? 2 : 4}
148 style={[a.text_sm, a.leading_snug]}>
149 {link.description}
150 </Text>
151 ) : undefined}
152 </View>
153 <View style={[a.px_md]}>
154 <Divider />
155 <View
156 style={[
157 a.flex_row,
158 a.align_center,
159 a.gap_2xs,
160 a.pb_sm,
161 {
162 paddingTop: 6, // off menu
163 },
164 ]}>
165 <Globe
166 size="xs"
167 style={[
168 a.transition_color,
169 hovered
170 ? t.atoms.text_contrast_medium
171 : t.atoms.text_contrast_low,
172 ]}
173 />
174 <Text
175 numberOfLines={1}
176 style={[
177 a.transition_color,
178 a.text_xs,
179 a.leading_snug,
180 hovered
181 ? t.atoms.text_contrast_high
182 : t.atoms.text_contrast_medium,
183 ]}>
184 {toNiceDomain(link.uri)}
185 </Text>
186 </View>
187 </View>
188 </View>
189 </View>
190 )}
191 </Link>
192 )
193}