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