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