forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useEffect, useMemo, useState} from 'react'
2import {LayoutAnimation, View} from 'react-native'
3import {
4 AppBskyFeedPost,
5 AppBskyRichtextFacet,
6 AtUri,
7 moderatePost,
8 RichText as RichTextAPI,
9} from '@atproto/api'
10import {msg} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native'
13
14import {makeProfileLink} from '#/lib/routes/links'
15import {
16 type CommonNavigatorParams,
17 type NavigationProp,
18} from '#/lib/routes/types'
19import {
20 convertBskyAppUrlIfNeeded,
21 isBskyPostUrl,
22 makeRecordUri,
23} from '#/lib/strings/url-helpers'
24import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
25import {useModerationOpts} from '#/state/preferences/moderation-opts'
26import {usePostQuery} from '#/state/queries/post'
27import {PostMeta} from '#/view/com/util/PostMeta'
28import {atoms as a, useTheme} from '#/alf'
29import {Button, ButtonIcon} from '#/components/Button'
30import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
31import {Loader} from '#/components/Loader'
32import * as MediaPreview from '#/components/MediaPreview'
33import {ContentHider} from '#/components/moderation/ContentHider'
34import {PostAlerts} from '#/components/moderation/PostAlerts'
35import {RichText} from '#/components/RichText'
36import {Text} from '#/components/Typography'
37import * as bsky from '#/types/bsky'
38
39export function useMessageEmbed() {
40 const route =
41 useRoute<RouteProp<CommonNavigatorParams, 'MessagesConversation'>>()
42 const navigation = useNavigation<NavigationProp>()
43 const embedFromParams = route.params.embed
44
45 const [embedUri, setEmbed] = useState(embedFromParams)
46
47 if (embedFromParams && embedUri !== embedFromParams) {
48 setEmbed(embedFromParams)
49 }
50
51 return {
52 embedUri,
53 setEmbed: useCallback(
54 (embedUrl: string | undefined) => {
55 if (!embedUrl) {
56 navigation.setParams({embed: ''})
57 setEmbed(undefined)
58 return
59 }
60
61 if (embedFromParams) return
62
63 const url = convertBskyAppUrlIfNeeded(embedUrl)
64 const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
65 const uri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
66
67 setEmbed(uri)
68 },
69 [embedFromParams, navigation],
70 ),
71 }
72}
73
74export function useExtractEmbedFromFacets(
75 message: string,
76 setEmbed: (embedUrl: string | undefined) => void,
77) {
78 const rt = new RichTextAPI({text: message})
79 rt.detectFacetsWithoutResolution()
80
81 let uriFromFacet: string | undefined
82
83 for (const facet of rt.facets ?? []) {
84 for (const feature of facet.features) {
85 if (AppBskyRichtextFacet.isLink(feature) && isBskyPostUrl(feature.uri)) {
86 uriFromFacet = feature.uri
87 break
88 }
89 }
90 }
91
92 useEffect(() => {
93 if (uriFromFacet) {
94 setEmbed(uriFromFacet)
95 }
96 }, [uriFromFacet, setEmbed])
97}
98
99export function MessageInputEmbed({
100 embedUri,
101 setEmbed,
102}: {
103 embedUri: string | undefined
104 setEmbed: (embedUrl: string | undefined) => void
105}) {
106 const t = useTheme()
107 const {_} = useLingui()
108
109 const enableSquareButtons = useEnableSquareButtons()
110
111 const {data: post, status} = usePostQuery(embedUri)
112
113 const moderationOpts = useModerationOpts()
114 const moderation = useMemo(
115 () =>
116 moderationOpts && post ? moderatePost(post, moderationOpts) : undefined,
117 [moderationOpts, post],
118 )
119
120 const {rt, record} = useMemo(() => {
121 if (
122 post &&
123 bsky.dangerousIsType<AppBskyFeedPost.Record>(
124 post.record,
125 AppBskyFeedPost.isRecord,
126 )
127 ) {
128 return {
129 rt: new RichTextAPI({
130 text: post.record.text,
131 facets: post.record.facets,
132 }),
133 record: post.record,
134 }
135 }
136
137 return {rt: undefined, record: undefined}
138 }, [post])
139
140 if (!embedUri) {
141 return null
142 }
143
144 let content = null
145 switch (status) {
146 case 'pending':
147 content = (
148 <View
149 style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
150 <Loader />
151 </View>
152 )
153 break
154 case 'error':
155 content = (
156 <View
157 style={[a.flex_1, {minHeight: 64}, a.justify_center, a.align_center]}>
158 <Text style={a.text_center}>Could not fetch post</Text>
159 </View>
160 )
161 break
162 case 'success':
163 const itemUrip = new AtUri(post.uri)
164 const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
165
166 if (!post || !moderation || !rt || !record) {
167 return null
168 }
169
170 content = (
171 <View
172 style={[
173 a.flex_1,
174 t.atoms.bg,
175 t.atoms.border_contrast_low,
176 a.rounded_md,
177 a.border,
178 a.p_sm,
179 a.mb_sm,
180 ]}
181 pointerEvents="none">
182 <PostMeta
183 showAvatar
184 author={post.author}
185 moderation={moderation}
186 timestamp={post.indexedAt}
187 postHref={itemHref}
188 style={a.flex_0}
189 />
190 <ContentHider modui={moderation.ui('contentView')}>
191 <PostAlerts modui={moderation.ui('contentView')} style={a.py_xs} />
192 {rt.text && (
193 <View style={a.mt_xs}>
194 <RichText
195 enableTags
196 testID="postText"
197 value={rt}
198 style={[a.text_sm, t.atoms.text_contrast_high]}
199 authorHandle={post.author.handle}
200 numberOfLines={3}
201 />
202 </View>
203 )}
204 <MediaPreview.Embed embed={post.embed} style={a.mt_sm} />
205 </ContentHider>
206 </View>
207 )
208 break
209 }
210
211 return (
212 <View style={[a.flex_row, a.gap_sm]}>
213 {content}
214 <Button
215 label={_(msg`Remove embed`)}
216 onPress={() => {
217 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
218 setEmbed(undefined)
219 }}
220 size="tiny"
221 variant="solid"
222 color="secondary"
223 shape={enableSquareButtons ? 'square' : 'round'}>
224 <ButtonIcon icon={X} />
225 </Button>
226 </View>
227 )
228}