Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
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/core/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 {ProfileBadges} from '#/components/ProfileBadges'
21import {Text} from '#/components/Typography'
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 return (
74 <Pressable
75 style={[
76 a.flex_row,
77 a.align_start,
78 a.pt_xs,
79 a.pb_lg,
80 a.mb_md,
81 a.mx_lg,
82 a.border_b,
83 t.atoms.border_contrast_medium,
84 web(a.user_select_text),
85 ]}
86 onPress={onPress}
87 accessibilityRole="button"
88 accessibilityLabel={_(
89 msg`Expand or collapse the full post you are replying to`,
90 )}
91 accessibilityHint="">
92 <PreviewableUserAvatar
93 size={42}
94 profile={replyTo.author}
95 moderation={replyTo.moderation?.ui('avatar')}
96 type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
97 disableNavigation={true}
98 />
99 <View style={[a.flex_1, a.pl_md, a.pr_sm, a.gap_2xs]}>
100 <View style={[a.flex_row, a.align_center, a.pr_xs]}>
101 <Text
102 style={[a.font_semi_bold, a.text_md, a.leading_snug, a.flex_shrink]}
103 numberOfLines={1}
104 emoji>
105 {sanitizeDisplayName(
106 replyTo.author.displayName ||
107 sanitizeHandle(replyTo.author.handle),
108 )}
109 </Text>
110 <ProfileBadges profile={replyTo.author} size="sm" style={[a.pl_xs]} />
111 {replyTo.author?.pronouns && (
112 <Text
113 style={[
114 t.atoms.text_contrast_low,
115 a.text_md,
116 a.leading_snug,
117 a.pl_sm,
118 ]}
119 numberOfLines={1}
120 emoji>
121 {sanitizePronouns(replyTo.author.pronouns, true)}
122 </Text>
123 )}
124 </View>
125 <View style={[a.flex_row, a.gap_md]}>
126 <View style={[a.flex_1, a.flex_grow]}>
127 <Text
128 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}
129 numberOfLines={!showFull ? 6 : undefined}
130 emoji>
131 {replyTo.text}
132 </Text>
133 </View>
134 {images && !replyTo.moderation?.ui('contentMedia').blur && (
135 <ComposerReplyToImages images={images} showFull={showFull} />
136 )}
137 </View>
138 {showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && (
139 <QuoteEmbed
140 embed={parsedQuoteEmbed}
141 showPronouns={true}
142 linkDisabled
143 />
144 )}
145 </View>
146 </Pressable>
147 )
148}
149
150function ComposerReplyToImages({
151 images,
152}: {
153 images: AppBskyEmbedImages.ViewImage[]
154 showFull: boolean
155}) {
156 return (
157 <View
158 style={[
159 a.rounded_xs,
160 a.overflow_hidden,
161 a.mt_2xs,
162 a.mx_xs,
163 {
164 height: 64,
165 width: 64,
166 },
167 ]}>
168 {(images.length === 1 && (
169 <Image
170 source={{uri: images[0].thumb}}
171 style={[a.flex_1]}
172 cachePolicy="memory-disk"
173 accessibilityIgnoresInvertColors
174 />
175 )) ||
176 (images.length === 2 && (
177 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
178 <Image
179 source={{uri: images[0].thumb}}
180 style={[a.flex_1]}
181 cachePolicy="memory-disk"
182 accessibilityIgnoresInvertColors
183 />
184 <Image
185 source={{uri: images[1].thumb}}
186 style={[a.flex_1]}
187 cachePolicy="memory-disk"
188 accessibilityIgnoresInvertColors
189 />
190 </View>
191 )) ||
192 (images.length === 3 && (
193 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
194 <Image
195 source={{uri: images[0].thumb}}
196 style={[a.flex_1]}
197 cachePolicy="memory-disk"
198 accessibilityIgnoresInvertColors
199 />
200 <View style={[a.flex_1, a.gap_2xs]}>
201 <Image
202 source={{uri: images[1].thumb}}
203 style={[a.flex_1]}
204 cachePolicy="memory-disk"
205 accessibilityIgnoresInvertColors
206 />
207 <Image
208 source={{uri: images[2].thumb}}
209 style={[a.flex_1]}
210 cachePolicy="memory-disk"
211 accessibilityIgnoresInvertColors
212 />
213 </View>
214 </View>
215 )) ||
216 (images.length === 4 && (
217 <View style={[a.flex_1, a.gap_2xs]}>
218 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
219 <Image
220 source={{uri: images[0].thumb}}
221 style={[a.flex_1]}
222 cachePolicy="memory-disk"
223 accessibilityIgnoresInvertColors
224 />
225 <Image
226 source={{uri: images[1].thumb}}
227 style={[a.flex_1]}
228 cachePolicy="memory-disk"
229 accessibilityIgnoresInvertColors
230 />
231 </View>
232 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
233 <Image
234 source={{uri: images[2].thumb}}
235 style={[a.flex_1]}
236 cachePolicy="memory-disk"
237 accessibilityIgnoresInvertColors
238 />
239 <Image
240 source={{uri: images[3].thumb}}
241 style={[a.flex_1]}
242 cachePolicy="memory-disk"
243 accessibilityIgnoresInvertColors
244 />
245 </View>
246 </View>
247 ))}
248 </View>
249 )
250}