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