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 verify-intent 399 lines 9.8 kB view raw
1import React from 'react' 2import {GestureResponderEvent, View} from 'react-native' 3import { 4 AppBskyActorDefs, 5 moderateProfile, 6 ModerationOpts, 7 RichText as RichTextApi, 8} from '@atproto/api' 9import {msg} from '@lingui/macro' 10import {useLingui} from '@lingui/react' 11 12import {LogEvents} from '#/lib/statsig/statsig' 13import {sanitizeDisplayName} from '#/lib/strings/display-names' 14import {sanitizeHandle} from '#/lib/strings/handles' 15import {useProfileShadow} from '#/state/cache/profile-shadow' 16import {useProfileFollowMutationQueue} from '#/state/queries/profile' 17import {useSession} from '#/state/session' 18import {ProfileCardPills} from '#/view/com/profile/ProfileCard' 19import * as Toast from '#/view/com/util/Toast' 20import {UserAvatar} from '#/view/com/util/UserAvatar' 21import {atoms as a, useTheme} from '#/alf' 22import {Button, ButtonIcon, ButtonProps, ButtonText} from '#/components/Button' 23import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 24import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 25import {Link as InternalLink, LinkProps} from '#/components/Link' 26import {RichText} from '#/components/RichText' 27import {Text} from '#/components/Typography' 28 29export function Default({ 30 profile, 31 moderationOpts, 32 logContext = 'ProfileCard', 33}: { 34 profile: AppBskyActorDefs.ProfileViewDetailed 35 moderationOpts: ModerationOpts 36 logContext?: 'ProfileCard' | 'StarterPackProfilesList' 37}) { 38 return ( 39 <Link profile={profile}> 40 <Card 41 profile={profile} 42 moderationOpts={moderationOpts} 43 logContext={logContext} 44 /> 45 </Link> 46 ) 47} 48 49export function Card({ 50 profile, 51 moderationOpts, 52 logContext = 'ProfileCard', 53}: { 54 profile: AppBskyActorDefs.ProfileViewDetailed 55 moderationOpts: ModerationOpts 56 logContext?: 'ProfileCard' | 'StarterPackProfilesList' 57}) { 58 const moderation = moderateProfile(profile, moderationOpts) 59 60 return ( 61 <Outer> 62 <Header> 63 <Avatar profile={profile} moderationOpts={moderationOpts} /> 64 <NameAndHandle profile={profile} moderationOpts={moderationOpts} /> 65 <FollowButton 66 profile={profile} 67 moderationOpts={moderationOpts} 68 logContext={logContext} 69 /> 70 </Header> 71 72 <ProfileCardPills 73 followedBy={Boolean(profile.viewer?.followedBy)} 74 moderation={moderation} 75 /> 76 77 <Description profile={profile} /> 78 </Outer> 79 ) 80} 81 82export function Outer({ 83 children, 84}: { 85 children: React.ReactElement | React.ReactElement[] 86}) { 87 return <View style={[a.w_full, a.flex_1, a.gap_xs]}>{children}</View> 88} 89 90export function Header({ 91 children, 92}: { 93 children: React.ReactElement | React.ReactElement[] 94}) { 95 return <View style={[a.flex_row, a.align_center, a.gap_sm]}>{children}</View> 96} 97 98export function Link({ 99 profile, 100 children, 101 style, 102 ...rest 103}: { 104 profile: AppBskyActorDefs.ProfileViewDetailed 105} & Omit<LinkProps, 'to' | 'label'>) { 106 const {_} = useLingui() 107 return ( 108 <InternalLink 109 label={_( 110 msg`View ${ 111 profile.displayName || sanitizeHandle(profile.handle) 112 }'s profile`, 113 )} 114 to={{ 115 screen: 'Profile', 116 params: {name: profile.did}, 117 }} 118 style={[a.flex_col, style]} 119 {...rest}> 120 {children} 121 </InternalLink> 122 ) 123} 124 125export function Avatar({ 126 profile, 127 moderationOpts, 128}: { 129 profile: AppBskyActorDefs.ProfileViewDetailed 130 moderationOpts: ModerationOpts 131}) { 132 const moderation = moderateProfile(profile, moderationOpts) 133 134 return ( 135 <UserAvatar 136 size={42} 137 avatar={profile.avatar} 138 type={profile.associated?.labeler ? 'labeler' : 'user'} 139 moderation={moderation.ui('avatar')} 140 /> 141 ) 142} 143 144export function AvatarPlaceholder() { 145 const t = useTheme() 146 return ( 147 <View 148 style={[ 149 a.rounded_full, 150 t.atoms.bg_contrast_50, 151 { 152 width: 42, 153 height: 42, 154 }, 155 ]} 156 /> 157 ) 158} 159 160export function NameAndHandle({ 161 profile, 162 moderationOpts, 163}: { 164 profile: AppBskyActorDefs.ProfileViewDetailed 165 moderationOpts: ModerationOpts 166}) { 167 const t = useTheme() 168 const moderation = moderateProfile(profile, moderationOpts) 169 const name = sanitizeDisplayName( 170 profile.displayName || sanitizeHandle(profile.handle), 171 moderation.ui('displayName'), 172 ) 173 const handle = sanitizeHandle(profile.handle, '@') 174 175 return ( 176 <View style={[a.flex_1]}> 177 <Text 178 emoji 179 style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]} 180 numberOfLines={1}> 181 {name} 182 </Text> 183 <Text 184 emoji 185 style={[a.leading_snug, t.atoms.text_contrast_medium]} 186 numberOfLines={1}> 187 {handle} 188 </Text> 189 </View> 190 ) 191} 192 193export function NameAndHandlePlaceholder() { 194 const t = useTheme() 195 196 return ( 197 <View style={[a.flex_1, a.gap_xs]}> 198 <View 199 style={[ 200 a.rounded_xs, 201 t.atoms.bg_contrast_50, 202 { 203 width: '60%', 204 height: 14, 205 }, 206 ]} 207 /> 208 209 <View 210 style={[ 211 a.rounded_xs, 212 t.atoms.bg_contrast_50, 213 { 214 width: '40%', 215 height: 10, 216 }, 217 ]} 218 /> 219 </View> 220 ) 221} 222 223export function Description({ 224 profile: profileUnshadowed, 225 numberOfLines = 3, 226}: { 227 profile: AppBskyActorDefs.ProfileViewDetailed 228 numberOfLines?: number 229}) { 230 const profile = useProfileShadow(profileUnshadowed) 231 const {description} = profile 232 const rt = React.useMemo(() => { 233 if (!description) return 234 const rt = new RichTextApi({text: description || ''}) 235 rt.detectFacetsWithoutResolution() 236 return rt 237 }, [description]) 238 if (!rt) return null 239 if ( 240 profile.viewer && 241 (profile.viewer.blockedBy || 242 profile.viewer.blocking || 243 profile.viewer.blockingByList) 244 ) 245 return null 246 return ( 247 <View style={[a.pt_xs]}> 248 <RichText 249 value={rt} 250 style={[a.leading_snug]} 251 numberOfLines={numberOfLines} 252 disableLinks 253 /> 254 </View> 255 ) 256} 257 258export function DescriptionPlaceholder({ 259 numberOfLines = 3, 260}: { 261 numberOfLines?: number 262}) { 263 const t = useTheme() 264 return ( 265 <View style={[{gap: 8}]}> 266 {Array(numberOfLines) 267 .fill(0) 268 .map((_, i) => ( 269 <View 270 key={i} 271 style={[ 272 a.rounded_xs, 273 a.w_full, 274 t.atoms.bg_contrast_50, 275 {height: 12, width: i + 1 === numberOfLines ? '60%' : '100%'}, 276 ]} 277 /> 278 ))} 279 </View> 280 ) 281} 282 283export type FollowButtonProps = { 284 profile: AppBskyActorDefs.ProfileViewBasic 285 moderationOpts: ModerationOpts 286 logContext: LogEvents['profile:follow:sampled']['logContext'] & 287 LogEvents['profile:unfollow:sampled']['logContext'] 288} & Partial<ButtonProps> 289 290export function FollowButton(props: FollowButtonProps) { 291 const {currentAccount, hasSession} = useSession() 292 const isMe = props.profile.did === currentAccount?.did 293 return hasSession && !isMe ? <FollowButtonInner {...props} /> : null 294} 295 296export function FollowButtonInner({ 297 profile: profileUnshadowed, 298 moderationOpts, 299 logContext, 300 ...rest 301}: FollowButtonProps) { 302 const {_} = useLingui() 303 const profile = useProfileShadow(profileUnshadowed) 304 const moderation = moderateProfile(profile, moderationOpts) 305 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 306 profile, 307 logContext, 308 ) 309 const isRound = Boolean(rest.shape && rest.shape === 'round') 310 311 const onPressFollow = async (e: GestureResponderEvent) => { 312 e.preventDefault() 313 e.stopPropagation() 314 try { 315 await queueFollow() 316 Toast.show( 317 _( 318 msg`Following ${sanitizeDisplayName( 319 profile.displayName || profile.handle, 320 moderation.ui('displayName'), 321 )}`, 322 ), 323 ) 324 } catch (err: any) { 325 if (err?.name !== 'AbortError') { 326 Toast.show(_(msg`An issue occurred, please try again.`), 'xmark') 327 } 328 } 329 } 330 331 const onPressUnfollow = async (e: GestureResponderEvent) => { 332 e.preventDefault() 333 e.stopPropagation() 334 try { 335 await queueUnfollow() 336 Toast.show( 337 _( 338 msg`No longer following ${sanitizeDisplayName( 339 profile.displayName || profile.handle, 340 moderation.ui('displayName'), 341 )}`, 342 ), 343 ) 344 } catch (err: any) { 345 if (err?.name !== 'AbortError') { 346 Toast.show(_(msg`An issue occurred, please try again.`), 'xmark') 347 } 348 } 349 } 350 351 const unfollowLabel = _( 352 msg({ 353 message: 'Following', 354 comment: 'User is following this account, click to unfollow', 355 }), 356 ) 357 const followLabel = _( 358 msg({ 359 message: 'Follow', 360 comment: 'User is not following this account, click to follow', 361 }), 362 ) 363 364 if (!profile.viewer) return null 365 if ( 366 profile.viewer.blockedBy || 367 profile.viewer.blocking || 368 profile.viewer.blockingByList 369 ) 370 return null 371 372 return ( 373 <View> 374 {profile.viewer.following ? ( 375 <Button 376 label={unfollowLabel} 377 size="small" 378 variant="solid" 379 color="secondary" 380 {...rest} 381 onPress={onPressUnfollow}> 382 <ButtonIcon icon={Check} position={isRound ? undefined : 'left'} /> 383 {isRound ? null : <ButtonText>{unfollowLabel}</ButtonText>} 384 </Button> 385 ) : ( 386 <Button 387 label={followLabel} 388 size="small" 389 variant="solid" 390 color="primary" 391 {...rest} 392 onPress={onPressFollow}> 393 <ButtonIcon icon={Plus} position={isRound ? undefined : 'left'} /> 394 {isRound ? null : <ButtonText>{followLabel}</ButtonText>} 395 </Button> 396 )} 397 </View> 398 ) 399}