mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {memo, useState} from 'react'
2import {type StyleProp, View, type ViewStyle} from 'react-native'
3import {
4 type AppBskyFeedDefs,
5 type AppBskyFeedPost,
6 type AppBskyFeedThreadgate,
7 type RichText as RichTextAPI,
8} from '@atproto/api'
9import {msg, plural} from '@lingui/macro'
10import {useLingui} from '@lingui/react'
11
12import {CountWheel} from '#/lib/custom-animations/CountWheel'
13import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
14import {useHaptics} from '#/lib/haptics'
15import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
16import {type Shadow} from '#/state/cache/types'
17import {useFeedFeedbackContext} from '#/state/feed-feedback'
18import {
19 usePostLikeMutationQueue,
20 usePostRepostMutationQueue,
21} from '#/state/queries/post'
22import {useRequireAuth} from '#/state/session'
23import {
24 ProgressGuideAction,
25 useProgressGuideControls,
26} from '#/state/shell/progress-guide'
27import {formatCount} from '#/view/com/util/numeric/format'
28import * as Toast from '#/view/com/util/Toast'
29import {atoms as a, useBreakpoints} from '#/alf'
30import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
31import {
32 PostControlButton,
33 PostControlButtonIcon,
34 PostControlButtonText,
35} from './PostControlButton'
36import {PostMenuButton} from './PostMenu'
37import {RepostButton} from './RepostButton'
38import {ShareMenuButton} from './ShareMenu'
39
40let PostControls = ({
41 big,
42 post,
43 record,
44 richText,
45 feedContext,
46 reqId,
47 style,
48 onPressReply,
49 onPostReply,
50 logContext,
51 threadgateRecord,
52 onShowLess,
53 viaRepost,
54}: {
55 big?: boolean
56 post: Shadow<AppBskyFeedDefs.PostView>
57 record: AppBskyFeedPost.Record
58 richText: RichTextAPI
59 feedContext?: string | undefined
60 reqId?: string | undefined
61 style?: StyleProp<ViewStyle>
62 onPressReply: () => void
63 onPostReply?: (postUri: string | undefined) => void
64 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
65 threadgateRecord?: AppBskyFeedThreadgate.Record
66 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
67 viaRepost?: {uri: string; cid: string}
68}): React.ReactNode => {
69 const {_, i18n} = useLingui()
70 const {gtMobile} = useBreakpoints()
71 const {openComposer} = useOpenComposer()
72 const {feedDescriptor} = useFeedFeedbackContext()
73 const [queueLike, queueUnlike] = usePostLikeMutationQueue(
74 post,
75 viaRepost,
76 feedDescriptor,
77 logContext,
78 )
79 const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
80 post,
81 viaRepost,
82 feedDescriptor,
83 logContext,
84 )
85 const requireAuth = useRequireAuth()
86 const {sendInteraction} = useFeedFeedbackContext()
87 const {captureAction} = useProgressGuideControls()
88 const playHaptic = useHaptics()
89 const isBlocked = Boolean(
90 post.author.viewer?.blocking ||
91 post.author.viewer?.blockedBy ||
92 post.author.viewer?.blockingByList,
93 )
94 const replyDisabled = post.viewer?.replyDisabled
95
96 const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] = useState(false)
97
98 const onPressToggleLike = async () => {
99 if (isBlocked) {
100 Toast.show(
101 _(msg`Cannot interact with a blocked user`),
102 'exclamation-circle',
103 )
104 return
105 }
106
107 try {
108 setHasLikeIconBeenToggled(true)
109 if (!post.viewer?.like) {
110 playHaptic('Light')
111 sendInteraction({
112 item: post.uri,
113 event: 'app.bsky.feed.defs#interactionLike',
114 feedContext,
115 reqId,
116 })
117 captureAction(ProgressGuideAction.Like)
118 await queueLike()
119 } else {
120 await queueUnlike()
121 }
122 } catch (e: any) {
123 if (e?.name !== 'AbortError') {
124 throw e
125 }
126 }
127 }
128
129 const onRepost = async () => {
130 if (isBlocked) {
131 Toast.show(
132 _(msg`Cannot interact with a blocked user`),
133 'exclamation-circle',
134 )
135 return
136 }
137
138 try {
139 if (!post.viewer?.repost) {
140 sendInteraction({
141 item: post.uri,
142 event: 'app.bsky.feed.defs#interactionRepost',
143 feedContext,
144 reqId,
145 })
146 await queueRepost()
147 } else {
148 await queueUnrepost()
149 }
150 } catch (e: any) {
151 if (e?.name !== 'AbortError') {
152 throw e
153 }
154 }
155 }
156
157 const onQuote = () => {
158 if (isBlocked) {
159 Toast.show(
160 _(msg`Cannot interact with a blocked user`),
161 'exclamation-circle',
162 )
163 return
164 }
165
166 sendInteraction({
167 item: post.uri,
168 event: 'app.bsky.feed.defs#interactionQuote',
169 feedContext,
170 reqId,
171 })
172 openComposer({
173 quote: post,
174 onPost: onPostReply,
175 })
176 }
177
178 const onShare = () => {
179 sendInteraction({
180 item: post.uri,
181 event: 'app.bsky.feed.defs#interactionShare',
182 feedContext,
183 reqId,
184 })
185 }
186
187 return (
188 <View style={[a.flex_row, a.justify_between, a.align_center, style]}>
189 <View
190 style={[
191 big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}],
192 replyDisabled ? {opacity: 0.5} : undefined,
193 ]}>
194 <PostControlButton
195 testID="replyBtn"
196 onPress={
197 !replyDisabled ? () => requireAuth(() => onPressReply()) : undefined
198 }
199 label={_(
200 msg({
201 message: `Reply (${plural(post.replyCount || 0, {
202 one: '# reply',
203 other: '# replies',
204 })})`,
205 comment:
206 'Accessibility label for the reply button, verb form followed by number of replies and noun form',
207 }),
208 )}
209 big={big}>
210 <PostControlButtonIcon icon={Bubble} />
211 {typeof post.replyCount !== 'undefined' && post.replyCount > 0 && (
212 <PostControlButtonText>
213 {formatCount(i18n, post.replyCount)}
214 </PostControlButtonText>
215 )}
216 </PostControlButton>
217 </View>
218 <View style={big ? a.align_center : [a.flex_1, a.align_start]}>
219 <RepostButton
220 isReposted={!!post.viewer?.repost}
221 repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)}
222 onRepost={onRepost}
223 onQuote={onQuote}
224 big={big}
225 embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)}
226 />
227 </View>
228 <View style={big ? a.align_center : [a.flex_1, a.align_start]}>
229 <PostControlButton
230 testID="likeBtn"
231 big={big}
232 onPress={() => requireAuth(() => onPressToggleLike())}
233 label={
234 post.viewer?.like
235 ? _(
236 msg({
237 message: `Unlike (${plural(post.likeCount || 0, {
238 one: '# like',
239 other: '# likes',
240 })})`,
241 comment:
242 'Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun',
243 }),
244 )
245 : _(
246 msg({
247 message: `Like (${plural(post.likeCount || 0, {
248 one: '# like',
249 other: '# likes',
250 })})`,
251 comment:
252 'Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form',
253 }),
254 )
255 }>
256 <AnimatedLikeIcon
257 isLiked={Boolean(post.viewer?.like)}
258 big={big}
259 hasBeenToggled={hasLikeIconBeenToggled}
260 />
261 <CountWheel
262 likeCount={post.likeCount ?? 0}
263 big={big}
264 isLiked={Boolean(post.viewer?.like)}
265 hasBeenToggled={hasLikeIconBeenToggled}
266 />
267 </PostControlButton>
268 </View>
269 <View style={big ? a.align_center : [a.flex_1, a.align_start]}>
270 <View style={[!big && a.ml_sm]}>
271 <ShareMenuButton
272 testID="postShareBtn"
273 post={post}
274 big={big}
275 record={record}
276 richText={richText}
277 timestamp={post.indexedAt}
278 threadgateRecord={threadgateRecord}
279 onShare={onShare}
280 />
281 </View>
282 </View>
283 <View
284 style={big ? a.align_center : [gtMobile && a.flex_1, a.align_start]}>
285 <PostMenuButton
286 testID="postDropdownBtn"
287 post={post}
288 postFeedContext={feedContext}
289 postReqId={reqId}
290 big={big}
291 record={record}
292 richText={richText}
293 timestamp={post.indexedAt}
294 threadgateRecord={threadgateRecord}
295 onShowLess={onShowLess}
296 />
297 </View>
298 </View>
299 )
300}
301PostControls = memo(PostControls)
302export {PostControls}