mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at samuel/exp-cli 302 lines 8.8 kB view raw
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}