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