forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useMemo, useState} from 'react'
2import {LayoutAnimation, Pressable, View} from 'react-native'
3import {Image} from 'expo-image'
4import {
5 AppBskyEmbedImages,
6 AppBskyEmbedRecord,
7 AppBskyEmbedRecordWithMedia,
8 AppBskyFeedPost,
9} from '@atproto/api'
10import {msg} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12
13import {sanitizeDisplayName} from '#/lib/strings/display-names'
14import {sanitizeHandle} from '#/lib/strings/handles'
15import {sanitizePronouns} from '#/lib/strings/pronouns'
16import {type ComposerOptsPostRef} from '#/state/shell/composer'
17import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
18import {atoms as a, useTheme, web} from '#/alf'
19import {QuoteEmbed} from '#/components/Post/Embed'
20import {Text} from '#/components/Typography'
21import {useSimpleVerificationState} from '#/components/verification'
22import {VerificationCheck} from '#/components/verification/VerificationCheck'
23import {parseEmbed} from '#/types/bsky/post'
24
25export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
26 const t = useTheme()
27 const {_} = useLingui()
28 const {embed} = replyTo
29
30 const [showFull, setShowFull] = useState(false)
31
32 const onPress = useCallback(() => {
33 setShowFull(prev => !prev)
34 LayoutAnimation.configureNext({
35 duration: 350,
36 update: {type: 'spring', springDamping: 0.7},
37 })
38 }, [])
39
40 const quoteEmbed = useMemo(() => {
41 if (
42 AppBskyEmbedRecord.isView(embed) &&
43 AppBskyEmbedRecord.isViewRecord(embed.record) &&
44 AppBskyFeedPost.isRecord(embed.record.value)
45 ) {
46 return embed
47 } else if (
48 AppBskyEmbedRecordWithMedia.isView(embed) &&
49 AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
50 AppBskyFeedPost.isRecord(embed.record.record.value)
51 ) {
52 return embed.record
53 }
54 return null
55 }, [embed])
56 const parsedQuoteEmbed = quoteEmbed
57 ? parseEmbed({
58 $type: 'app.bsky.embed.record#view',
59 ...quoteEmbed,
60 })
61 : null
62
63 const images = useMemo(() => {
64 if (AppBskyEmbedImages.isView(embed)) {
65 return embed.images
66 } else if (
67 AppBskyEmbedRecordWithMedia.isView(embed) &&
68 AppBskyEmbedImages.isView(embed.media)
69 ) {
70 return embed.media.images
71 }
72 }, [embed])
73
74 const verification = useSimpleVerificationState({profile: replyTo.author})
75
76 return (
77 <Pressable
78 style={[
79 a.flex_row,
80 a.align_start,
81 a.pt_xs,
82 a.pb_lg,
83 a.mb_md,
84 a.mx_lg,
85 a.border_b,
86 t.atoms.border_contrast_medium,
87 web(a.user_select_text),
88 ]}
89 onPress={onPress}
90 accessibilityRole="button"
91 accessibilityLabel={_(
92 msg`Expand or collapse the full post you are replying to`,
93 )}
94 accessibilityHint="">
95 <PreviewableUserAvatar
96 size={42}
97 profile={replyTo.author}
98 moderation={replyTo.moderation?.ui('avatar')}
99 type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
100 disableNavigation={true}
101 />
102 <View style={[a.flex_1, a.pl_md, a.pr_sm, a.gap_2xs]}>
103 <View style={[a.flex_row, a.align_center, a.pr_xs]}>
104 <View style={[a.flex_row, a.align_center]}>
105 <Text
106 style={[
107 a.font_semi_bold,
108 a.text_md,
109 a.leading_snug,
110 a.flex_shrink,
111 ]}
112 numberOfLines={1}
113 emoji>
114 {sanitizeDisplayName(
115 replyTo.author.displayName ||
116 sanitizeHandle(replyTo.author.handle),
117 )}
118 </Text>
119 {verification.showBadge && (
120 <View style={[a.pl_xs]}>
121 <VerificationCheck
122 width={14}
123 verifier={verification.role === 'verifier'}
124 />
125 </View>
126 )}
127 </View>
128 {replyTo.author?.pronouns && (
129 <Text
130 style={[
131 t.atoms.text_contrast_low,
132 a.text_md,
133 a.leading_snug,
134 a.pl_sm,
135 ]}
136 numberOfLines={1}
137 emoji>
138 {sanitizePronouns(replyTo.author.pronouns, true)}
139 </Text>
140 )}
141 </View>
142 <View style={[a.flex_row, a.gap_md]}>
143 <View style={[a.flex_1, a.flex_grow]}>
144 <Text
145 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}
146 numberOfLines={!showFull ? 6 : undefined}
147 emoji>
148 {replyTo.text}
149 </Text>
150 </View>
151 {images && !replyTo.moderation?.ui('contentMedia').blur && (
152 <ComposerReplyToImages images={images} showFull={showFull} />
153 )}
154 </View>
155 {showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && (
156 <QuoteEmbed embed={parsedQuoteEmbed} showPronouns={true} />
157 )}
158 </View>
159 </Pressable>
160 )
161}
162
163function ComposerReplyToImages({
164 images,
165}: {
166 images: AppBskyEmbedImages.ViewImage[]
167 showFull: boolean
168}) {
169 return (
170 <View
171 style={[
172 a.rounded_xs,
173 a.overflow_hidden,
174 a.mt_2xs,
175 a.mx_xs,
176 {
177 height: 64,
178 width: 64,
179 },
180 ]}>
181 {(images.length === 1 && (
182 <Image
183 source={{uri: images[0].thumb}}
184 style={[a.flex_1]}
185 cachePolicy="memory-disk"
186 accessibilityIgnoresInvertColors
187 />
188 )) ||
189 (images.length === 2 && (
190 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
191 <Image
192 source={{uri: images[0].thumb}}
193 style={[a.flex_1]}
194 cachePolicy="memory-disk"
195 accessibilityIgnoresInvertColors
196 />
197 <Image
198 source={{uri: images[1].thumb}}
199 style={[a.flex_1]}
200 cachePolicy="memory-disk"
201 accessibilityIgnoresInvertColors
202 />
203 </View>
204 )) ||
205 (images.length === 3 && (
206 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
207 <Image
208 source={{uri: images[0].thumb}}
209 style={[a.flex_1]}
210 cachePolicy="memory-disk"
211 accessibilityIgnoresInvertColors
212 />
213 <View style={[a.flex_1, a.gap_2xs]}>
214 <Image
215 source={{uri: images[1].thumb}}
216 style={[a.flex_1]}
217 cachePolicy="memory-disk"
218 accessibilityIgnoresInvertColors
219 />
220 <Image
221 source={{uri: images[2].thumb}}
222 style={[a.flex_1]}
223 cachePolicy="memory-disk"
224 accessibilityIgnoresInvertColors
225 />
226 </View>
227 </View>
228 )) ||
229 (images.length === 4 && (
230 <View style={[a.flex_1, a.gap_2xs]}>
231 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
232 <Image
233 source={{uri: images[0].thumb}}
234 style={[a.flex_1]}
235 cachePolicy="memory-disk"
236 accessibilityIgnoresInvertColors
237 />
238 <Image
239 source={{uri: images[1].thumb}}
240 style={[a.flex_1]}
241 cachePolicy="memory-disk"
242 accessibilityIgnoresInvertColors
243 />
244 </View>
245 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
246 <Image
247 source={{uri: images[2].thumb}}
248 style={[a.flex_1]}
249 cachePolicy="memory-disk"
250 accessibilityIgnoresInvertColors
251 />
252 <Image
253 source={{uri: images[3].thumb}}
254 style={[a.flex_1]}
255 cachePolicy="memory-disk"
256 accessibilityIgnoresInvertColors
257 />
258 </View>
259 </View>
260 ))}
261 </View>
262 )
263}