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