mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 StyleProp,
4 StyleSheet,
5 TouchableOpacity,
6 View,
7 ViewStyle,
8} from 'react-native'
9import {
10 AppBskyEmbedExternal,
11 AppBskyEmbedImages,
12 AppBskyEmbedRecord,
13 AppBskyEmbedRecordWithMedia,
14 AppBskyEmbedVideo,
15 AppBskyFeedDefs,
16 AppBskyFeedPost,
17 ModerationDecision,
18 RichText as RichTextAPI,
19} from '@atproto/api'
20import {AtUri} from '@atproto/api'
21import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
22import {msg, Trans} from '@lingui/macro'
23import {useLingui} from '@lingui/react'
24import {useQueryClient} from '@tanstack/react-query'
25
26import {HITSLOP_20} from '#/lib/constants'
27import {usePalette} from '#/lib/hooks/usePalette'
28import {InfoCircleIcon} from '#/lib/icons'
29import {moderatePost_wrapped} from '#/lib/moderatePost_wrapped'
30import {makeProfileLink} from '#/lib/routes/links'
31import {s} from '#/lib/styles'
32import {useModerationOpts} from '#/state/preferences/moderation-opts'
33import {precacheProfile} from '#/state/queries/profile'
34import {useResolveLinkQuery} from '#/state/queries/resolve-link'
35import {useSession} from '#/state/session'
36import {atoms as a, useTheme} from '#/alf'
37import {RichText} from '#/components/RichText'
38import {SubtleWebHover} from '#/components/SubtleWebHover'
39import {ContentHider} from '../../../../components/moderation/ContentHider'
40import {PostAlerts} from '../../../../components/moderation/PostAlerts'
41import {Link} from '../Link'
42import {PostMeta} from '../PostMeta'
43import {Text} from '../text/Text'
44import {PostEmbeds} from '.'
45import {QuoteEmbedViewContext} from './types'
46
47export function MaybeQuoteEmbed({
48 embed,
49 onOpen,
50 style,
51 allowNestedQuotes,
52 viewContext,
53}: {
54 embed: AppBskyEmbedRecord.View
55 onOpen?: () => void
56 style?: StyleProp<ViewStyle>
57 allowNestedQuotes?: boolean
58 viewContext?: QuoteEmbedViewContext
59}) {
60 const t = useTheme()
61 const pal = usePalette('default')
62 const {currentAccount} = useSession()
63 if (
64 AppBskyEmbedRecord.isViewRecord(embed.record) &&
65 AppBskyFeedPost.isRecord(embed.record.value) &&
66 AppBskyFeedPost.validateRecord(embed.record.value).success
67 ) {
68 return (
69 <QuoteEmbedModerated
70 viewRecord={embed.record}
71 onOpen={onOpen}
72 style={style}
73 allowNestedQuotes={allowNestedQuotes}
74 viewContext={viewContext}
75 />
76 )
77 } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) {
78 return (
79 <View
80 style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}>
81 <InfoCircleIcon size={18} style={pal.text} />
82 <Text type="lg" style={pal.text}>
83 <Trans>Blocked</Trans>
84 </Text>
85 </View>
86 )
87 } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) {
88 return (
89 <View
90 style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}>
91 <InfoCircleIcon size={18} style={pal.text} />
92 <Text type="lg" style={pal.text}>
93 <Trans>Deleted</Trans>
94 </Text>
95 </View>
96 )
97 } else if (AppBskyEmbedRecord.isViewDetached(embed.record)) {
98 const isViewerOwner = currentAccount?.did
99 ? embed.record.uri.includes(currentAccount.did)
100 : false
101 return (
102 <View
103 style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}>
104 <InfoCircleIcon size={18} style={pal.text} />
105 <Text type="lg" style={pal.text}>
106 {isViewerOwner ? (
107 <Trans>Removed by you</Trans>
108 ) : (
109 <Trans>Removed by author</Trans>
110 )}
111 </Text>
112 </View>
113 )
114 }
115 return null
116}
117
118function QuoteEmbedModerated({
119 viewRecord,
120 onOpen,
121 style,
122 allowNestedQuotes,
123 viewContext,
124}: {
125 viewRecord: AppBskyEmbedRecord.ViewRecord
126 onOpen?: () => void
127 style?: StyleProp<ViewStyle>
128 allowNestedQuotes?: boolean
129 viewContext?: QuoteEmbedViewContext
130}) {
131 const moderationOpts = useModerationOpts()
132 const postView = React.useMemo(
133 () => viewRecordToPostView(viewRecord),
134 [viewRecord],
135 )
136 const moderation = React.useMemo(() => {
137 return moderationOpts
138 ? moderatePost_wrapped(postView, moderationOpts)
139 : undefined
140 }, [postView, moderationOpts])
141
142 return (
143 <QuoteEmbed
144 quote={postView}
145 moderation={moderation}
146 onOpen={onOpen}
147 style={style}
148 allowNestedQuotes={allowNestedQuotes}
149 viewContext={viewContext}
150 />
151 )
152}
153
154export function QuoteEmbed({
155 quote,
156 moderation,
157 onOpen,
158 style,
159 allowNestedQuotes,
160}: {
161 quote: AppBskyFeedDefs.PostView
162 moderation?: ModerationDecision
163 onOpen?: () => void
164 style?: StyleProp<ViewStyle>
165 allowNestedQuotes?: boolean
166 viewContext?: QuoteEmbedViewContext
167}) {
168 const t = useTheme()
169 const queryClient = useQueryClient()
170 const pal = usePalette('default')
171 const itemUrip = new AtUri(quote.uri)
172 const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey)
173 const itemTitle = `Post by ${quote.author.handle}`
174
175 const richText = React.useMemo(() => {
176 const text = AppBskyFeedPost.isRecord(quote.record) ? quote.record.text : ''
177 const facets = AppBskyFeedPost.isRecord(quote.record)
178 ? quote.record.facets
179 : undefined
180 return text.trim()
181 ? new RichTextAPI({text: text, facets: facets})
182 : undefined
183 }, [quote.record])
184
185 const embed = React.useMemo(() => {
186 const e = quote.embed
187
188 if (allowNestedQuotes) {
189 return e
190 } else {
191 if (
192 AppBskyEmbedImages.isView(e) ||
193 AppBskyEmbedExternal.isView(e) ||
194 AppBskyEmbedVideo.isView(e)
195 ) {
196 return e
197 } else if (
198 AppBskyEmbedRecordWithMedia.isView(e) &&
199 (AppBskyEmbedImages.isView(e.media) ||
200 AppBskyEmbedExternal.isView(e.media) ||
201 AppBskyEmbedVideo.isView(e.media))
202 ) {
203 return e.media
204 }
205 }
206 }, [quote.embed, allowNestedQuotes])
207
208 const onBeforePress = React.useCallback(() => {
209 precacheProfile(queryClient, quote.author)
210 onOpen?.()
211 }, [queryClient, quote.author, onOpen])
212
213 const [hover, setHover] = React.useState(false)
214 return (
215 <View
216 onPointerEnter={() => {
217 setHover(true)
218 }}
219 onPointerLeave={() => {
220 setHover(false)
221 }}>
222 <ContentHider
223 modui={moderation?.ui('contentList')}
224 style={[
225 a.rounded_md,
226 a.p_md,
227 a.mt_sm,
228 a.border,
229 t.atoms.border_contrast_low,
230 style,
231 ]}
232 childContainerStyle={[a.pt_sm]}>
233 <SubtleWebHover hover={hover} />
234 <Link
235 hoverStyle={{borderColor: pal.colors.borderLinkHover}}
236 href={itemHref}
237 title={itemTitle}
238 onBeforePress={onBeforePress}>
239 <View pointerEvents="none">
240 <PostMeta
241 author={quote.author}
242 moderation={moderation}
243 showAvatar
244 postHref={itemHref}
245 timestamp={quote.indexedAt}
246 />
247 </View>
248 {moderation ? (
249 <PostAlerts
250 modui={moderation.ui('contentView')}
251 style={[a.py_xs]}
252 />
253 ) : null}
254 {richText ? (
255 <RichText
256 value={richText}
257 style={a.text_md}
258 numberOfLines={20}
259 disableLinks
260 />
261 ) : null}
262 {embed && <PostEmbeds embed={embed} moderation={moderation} />}
263 </Link>
264 </ContentHider>
265 </View>
266 )
267}
268
269export function QuoteX({onRemove}: {onRemove: () => void}) {
270 const {_} = useLingui()
271 return (
272 <TouchableOpacity
273 style={[
274 a.absolute,
275 a.p_xs,
276 a.rounded_full,
277 a.align_center,
278 a.justify_center,
279 {
280 top: 16,
281 right: 10,
282 backgroundColor: 'rgba(0, 0, 0, 0.75)',
283 },
284 ]}
285 onPress={onRemove}
286 accessibilityRole="button"
287 accessibilityLabel={_(msg`Remove quote`)}
288 accessibilityHint={_(msg`Removes quoted post`)}
289 onAccessibilityEscape={onRemove}
290 hitSlop={HITSLOP_20}>
291 <FontAwesomeIcon size={12} icon="xmark" style={s.white} />
292 </TouchableOpacity>
293 )
294}
295
296export function LazyQuoteEmbed({uri}: {uri: string}) {
297 const {data} = useResolveLinkQuery(uri)
298 if (!data || data.type !== 'record' || data.kind !== 'post') {
299 return null
300 }
301 return <QuoteEmbed quote={data.view} />
302}
303
304function viewRecordToPostView(
305 viewRecord: AppBskyEmbedRecord.ViewRecord,
306): AppBskyFeedDefs.PostView {
307 const {value, embeds, ...rest} = viewRecord
308 return {
309 ...rest,
310 $type: 'app.bsky.feed.defs#postView',
311 record: value,
312 embed: embeds?.[0],
313 }
314}
315
316const styles = StyleSheet.create({
317 errorContainer: {
318 flexDirection: 'row',
319 alignItems: 'center',
320 gap: 4,
321 borderRadius: 8,
322 marginTop: 8,
323 paddingVertical: 14,
324 paddingHorizontal: 14,
325 borderWidth: StyleSheet.hairlineWidth,
326 },
327 alert: {
328 marginBottom: 6,
329 },
330})