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