mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at vouch-impl 407 lines 12 kB view raw
1import React, {memo, useCallback} from 'react' 2import { 3 Pressable, 4 type PressableStateCallbackType, 5 type StyleProp, 6 View, 7 type ViewStyle, 8} from 'react-native' 9import * as Clipboard from 'expo-clipboard' 10import { 11 AppBskyFeedDefs, 12 AppBskyFeedPost, 13 AppBskyFeedThreadgate, 14 AtUri, 15 RichText as RichTextAPI, 16} from '@atproto/api' 17import {msg, plural} from '@lingui/macro' 18import {useLingui} from '@lingui/react' 19 20import {IS_INTERNAL} from '#/lib/app-info' 21import {DISCOVER_DEBUG_DIDS, POST_CTRL_HITSLOP} from '#/lib/constants' 22import {CountWheel} from '#/lib/custom-animations/CountWheel' 23import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' 24import {useHaptics} from '#/lib/haptics' 25import {makeProfileLink} from '#/lib/routes/links' 26import {shareUrl} from '#/lib/sharing' 27import {toShareUrl} from '#/lib/strings/url-helpers' 28import {Shadow} from '#/state/cache/types' 29import {useFeedFeedbackContext} from '#/state/feed-feedback' 30import { 31 usePostLikeMutationQueue, 32 usePostRepostMutationQueue, 33} from '#/state/queries/post' 34import {useRequireAuth, useSession} from '#/state/session' 35import {useComposerControls} from '#/state/shell/composer' 36import { 37 ProgressGuideAction, 38 useProgressGuideControls, 39} from '#/state/shell/progress-guide' 40import {atoms as a, useTheme} from '#/alf' 41import {useDialogControl} from '#/components/Dialog' 42import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox' 43import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble' 44import * as Prompt from '#/components/Prompt' 45import {PostDropdownBtn} from '../forms/PostDropdownBtn' 46import {formatCount} from '../numeric/format' 47import {Text} from '../text/Text' 48import * as Toast from '../Toast' 49import {RepostButton} from './RepostButton' 50 51let PostCtrls = ({ 52 big, 53 post, 54 record, 55 richText, 56 feedContext, 57 style, 58 onPressReply, 59 onPostReply, 60 logContext, 61 threadgateRecord, 62}: { 63 big?: boolean 64 post: Shadow<AppBskyFeedDefs.PostView> 65 record: AppBskyFeedPost.Record 66 richText: RichTextAPI 67 feedContext?: string | undefined 68 style?: StyleProp<ViewStyle> 69 onPressReply: () => void 70 onPostReply?: (postUri: string | undefined) => void 71 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' 72 threadgateRecord?: AppBskyFeedThreadgate.Record 73}): React.ReactNode => { 74 const t = useTheme() 75 const {_, i18n} = useLingui() 76 const {openComposer} = useComposerControls() 77 const {currentAccount} = useSession() 78 const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) 79 const [queueRepost, queueUnrepost] = usePostRepostMutationQueue( 80 post, 81 logContext, 82 ) 83 const requireAuth = useRequireAuth() 84 const loggedOutWarningPromptControl = useDialogControl() 85 const {sendInteraction} = useFeedFeedbackContext() 86 const {captureAction} = useProgressGuideControls() 87 const playHaptic = useHaptics() 88 const isDiscoverDebugUser = 89 IS_INTERNAL || DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] 90 const isBlocked = Boolean( 91 post.author.viewer?.blocking || 92 post.author.viewer?.blockedBy || 93 post.author.viewer?.blockingByList, 94 ) 95 96 const shouldShowLoggedOutWarning = React.useMemo(() => { 97 return ( 98 post.author.did !== currentAccount?.did && 99 !!post.author.labels?.find(label => label.val === '!no-unauthenticated') 100 ) 101 }, [currentAccount, post]) 102 103 const defaultCtrlColor = React.useMemo( 104 () => ({ 105 color: t.palette.contrast_500, 106 }), 107 [t], 108 ) as StyleProp<ViewStyle> 109 110 const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] = 111 React.useState(false) 112 113 const onPressToggleLike = React.useCallback(async () => { 114 if (isBlocked) { 115 Toast.show( 116 _(msg`Cannot interact with a blocked user`), 117 'exclamation-circle', 118 ) 119 return 120 } 121 122 try { 123 setHasLikeIconBeenToggled(true) 124 if (!post.viewer?.like) { 125 playHaptic('Light') 126 sendInteraction({ 127 item: post.uri, 128 event: 'app.bsky.feed.defs#interactionLike', 129 feedContext, 130 }) 131 captureAction(ProgressGuideAction.Like) 132 await queueLike() 133 } else { 134 await queueUnlike() 135 } 136 } catch (e: any) { 137 if (e?.name !== 'AbortError') { 138 throw e 139 } 140 } 141 }, [ 142 _, 143 playHaptic, 144 post.uri, 145 post.viewer?.like, 146 queueLike, 147 queueUnlike, 148 sendInteraction, 149 captureAction, 150 feedContext, 151 isBlocked, 152 ]) 153 154 const onRepost = useCallback(async () => { 155 if (isBlocked) { 156 Toast.show( 157 _(msg`Cannot interact with a blocked user`), 158 'exclamation-circle', 159 ) 160 return 161 } 162 163 try { 164 if (!post.viewer?.repost) { 165 sendInteraction({ 166 item: post.uri, 167 event: 'app.bsky.feed.defs#interactionRepost', 168 feedContext, 169 }) 170 await queueRepost() 171 } else { 172 await queueUnrepost() 173 } 174 } catch (e: any) { 175 if (e?.name !== 'AbortError') { 176 throw e 177 } 178 } 179 }, [ 180 _, 181 post.uri, 182 post.viewer?.repost, 183 queueRepost, 184 queueUnrepost, 185 sendInteraction, 186 feedContext, 187 isBlocked, 188 ]) 189 190 const onQuote = useCallback(() => { 191 if (isBlocked) { 192 Toast.show( 193 _(msg`Cannot interact with a blocked user`), 194 'exclamation-circle', 195 ) 196 return 197 } 198 199 sendInteraction({ 200 item: post.uri, 201 event: 'app.bsky.feed.defs#interactionQuote', 202 feedContext, 203 }) 204 openComposer({ 205 quote: post, 206 onPost: onPostReply, 207 }) 208 }, [ 209 _, 210 sendInteraction, 211 post, 212 feedContext, 213 openComposer, 214 onPostReply, 215 isBlocked, 216 ]) 217 218 const onShare = useCallback(() => { 219 const urip = new AtUri(post.uri) 220 const href = makeProfileLink(post.author, 'post', urip.rkey) 221 const url = toShareUrl(href) 222 shareUrl(url) 223 sendInteraction({ 224 item: post.uri, 225 event: 'app.bsky.feed.defs#interactionShare', 226 feedContext, 227 }) 228 }, [post.uri, post.author, sendInteraction, feedContext]) 229 230 const btnStyle = React.useCallback( 231 ({pressed, hovered}: PressableStateCallbackType) => [ 232 a.gap_xs, 233 a.rounded_full, 234 a.flex_row, 235 a.justify_center, 236 a.align_center, 237 a.overflow_hidden, 238 {padding: 5}, 239 (pressed || hovered) && t.atoms.bg_contrast_25, 240 ], 241 [t.atoms.bg_contrast_25], 242 ) 243 244 return ( 245 <View style={[a.flex_row, a.justify_between, a.align_center, style]}> 246 <View 247 style={[ 248 big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}], 249 post.viewer?.replyDisabled ? {opacity: 0.5} : undefined, 250 ]}> 251 <Pressable 252 testID="replyBtn" 253 style={btnStyle} 254 onPress={() => { 255 if (!post.viewer?.replyDisabled) { 256 playHaptic('Light') 257 requireAuth(() => onPressReply()) 258 } 259 }} 260 accessibilityRole="button" 261 accessibilityLabel={_( 262 msg`Reply (${plural(post.replyCount || 0, { 263 one: '# reply', 264 other: '# replies', 265 })})`, 266 )} 267 accessibilityHint="" 268 hitSlop={POST_CTRL_HITSLOP}> 269 <Bubble 270 style={[defaultCtrlColor, {pointerEvents: 'none'}]} 271 width={big ? 22 : 18} 272 /> 273 {typeof post.replyCount !== 'undefined' && post.replyCount > 0 ? ( 274 <Text 275 style={[ 276 defaultCtrlColor, 277 big ? a.text_md : {fontSize: 15}, 278 a.user_select_none, 279 ]}> 280 {formatCount(i18n, post.replyCount)} 281 </Text> 282 ) : undefined} 283 </Pressable> 284 </View> 285 <View style={big ? a.align_center : [a.flex_1, a.align_start]}> 286 <RepostButton 287 isReposted={!!post.viewer?.repost} 288 repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)} 289 onRepost={onRepost} 290 onQuote={onQuote} 291 big={big} 292 embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)} 293 /> 294 </View> 295 <View style={big ? a.align_center : [a.flex_1, a.align_start]}> 296 <Pressable 297 testID="likeBtn" 298 style={btnStyle} 299 onPress={() => requireAuth(() => onPressToggleLike())} 300 accessibilityRole="button" 301 accessibilityLabel={ 302 post.viewer?.like 303 ? _( 304 msg`Unlike (${plural(post.likeCount || 0, { 305 one: '# like', 306 other: '# likes', 307 })})`, 308 ) 309 : _( 310 msg`Like (${plural(post.likeCount || 0, { 311 one: '# like', 312 other: '# likes', 313 })})`, 314 ) 315 } 316 accessibilityHint="" 317 hitSlop={POST_CTRL_HITSLOP}> 318 <AnimatedLikeIcon 319 isLiked={Boolean(post.viewer?.like)} 320 big={big} 321 hasBeenToggled={hasLikeIconBeenToggled} 322 /> 323 <CountWheel 324 likeCount={post.likeCount ?? 0} 325 big={big} 326 isLiked={Boolean(post.viewer?.like)} 327 hasBeenToggled={hasLikeIconBeenToggled} 328 /> 329 </Pressable> 330 </View> 331 {big && ( 332 <> 333 <View style={a.align_center}> 334 <Pressable 335 testID="shareBtn" 336 style={btnStyle} 337 onPress={() => { 338 if (shouldShowLoggedOutWarning) { 339 loggedOutWarningPromptControl.open() 340 } else { 341 onShare() 342 } 343 }} 344 accessibilityRole="button" 345 accessibilityLabel={_(msg`Share`)} 346 accessibilityHint="" 347 hitSlop={POST_CTRL_HITSLOP}> 348 <ArrowOutOfBox 349 style={[defaultCtrlColor, {pointerEvents: 'none'}]} 350 width={22} 351 /> 352 </Pressable> 353 </View> 354 <Prompt.Basic 355 control={loggedOutWarningPromptControl} 356 title={_(msg`Note about sharing`)} 357 description={_( 358 msg`This post is only visible to logged-in users. It won't be visible to people who aren't logged in.`, 359 )} 360 onConfirm={onShare} 361 confirmButtonCta={_(msg`Share anyway`)} 362 /> 363 </> 364 )} 365 <View style={big ? a.align_center : [a.flex_1, a.align_start]}> 366 <PostDropdownBtn 367 testID="postDropdownBtn" 368 post={post} 369 postFeedContext={feedContext} 370 record={record} 371 richText={richText} 372 style={{padding: 5}} 373 hitSlop={POST_CTRL_HITSLOP} 374 timestamp={post.indexedAt} 375 threadgateRecord={threadgateRecord} 376 /> 377 </View> 378 {isDiscoverDebugUser && feedContext && ( 379 <Pressable 380 accessible={false} 381 style={{ 382 position: 'absolute', 383 top: 0, 384 bottom: 0, 385 right: 0, 386 display: 'flex', 387 justifyContent: 'center', 388 }} 389 onPress={e => { 390 e.stopPropagation() 391 Clipboard.setStringAsync(feedContext) 392 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 393 }}> 394 <Text 395 style={{ 396 color: t.palette.contrast_400, 397 fontSize: 7, 398 }}> 399 {feedContext} 400 </Text> 401 </Pressable> 402 )} 403 </View> 404 ) 405} 406PostCtrls = memo(PostCtrls) 407export {PostCtrls}