mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 326 lines 11 kB view raw
1import React, {memo, useMemo} from 'react' 2import {View} from 'react-native' 3import { 4 AppBskyActorDefs, 5 moderateProfile, 6 ModerationOpts, 7 RichText as RichTextAPI, 8} from '@atproto/api' 9import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 10import {msg, Trans} from '@lingui/macro' 11import {useLingui} from '@lingui/react' 12 13import {useGate} from '#/lib/statsig/statsig' 14import {logger} from '#/logger' 15import {isIOS} from '#/platform/detection' 16import {Shadow} from '#/state/cache/types' 17import {useModalControls} from '#/state/modals' 18import { 19 useProfileBlockMutationQueue, 20 useProfileFollowMutationQueue, 21} from '#/state/queries/profile' 22import {useRequireAuth, useSession} from '#/state/session' 23import {useAnalytics} from 'lib/analytics/analytics' 24import {sanitizeDisplayName} from 'lib/strings/display-names' 25import {useProfileShadow} from 'state/cache/profile-shadow' 26import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows' 27import {ProfileMenu} from '#/view/com/profile/ProfileMenu' 28import * as Toast from '#/view/com/util/Toast' 29import {atoms as a, useTheme} from '#/alf' 30import {Button, ButtonIcon, ButtonText} from '#/components/Button' 31import {MessageProfileButton} from '#/components/dms/MessageProfileButton' 32import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 33import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 34import { 35 KnownFollowers, 36 shouldShowKnownFollowers, 37} from '#/components/KnownFollowers' 38import * as Prompt from '#/components/Prompt' 39import {RichText} from '#/components/RichText' 40import {ProfileHeaderDisplayName} from './DisplayName' 41import {ProfileHeaderHandle} from './Handle' 42import {ProfileHeaderMetrics} from './Metrics' 43import {ProfileHeaderShell} from './Shell' 44 45interface Props { 46 profile: AppBskyActorDefs.ProfileViewDetailed 47 descriptionRT: RichTextAPI | null 48 moderationOpts: ModerationOpts 49 hideBackButton?: boolean 50 isPlaceholderProfile?: boolean 51} 52 53let ProfileHeaderStandard = ({ 54 profile: profileUnshadowed, 55 descriptionRT, 56 moderationOpts, 57 hideBackButton = false, 58 isPlaceholderProfile, 59}: Props): React.ReactNode => { 60 const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> = 61 useProfileShadow(profileUnshadowed) 62 const t = useTheme() 63 const gate = useGate() 64 const {currentAccount, hasSession} = useSession() 65 const {_} = useLingui() 66 const {openModal} = useModalControls() 67 const {track} = useAnalytics() 68 const moderation = useMemo( 69 () => moderateProfile(profile, moderationOpts), 70 [profile, moderationOpts], 71 ) 72 const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) 73 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 74 profile, 75 'ProfileHeader', 76 ) 77 const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) 78 const unblockPromptControl = Prompt.usePromptControl() 79 const requireAuth = useRequireAuth() 80 const isBlockedUser = 81 profile.viewer?.blocking || 82 profile.viewer?.blockedBy || 83 profile.viewer?.blockingByList 84 85 const onPressEditProfile = React.useCallback(() => { 86 track('ProfileHeader:EditProfileButtonClicked') 87 openModal({ 88 name: 'edit-profile', 89 profile, 90 }) 91 }, [track, openModal, profile]) 92 93 const onPressFollow = () => { 94 requireAuth(async () => { 95 try { 96 track('ProfileHeader:FollowButtonClicked') 97 await queueFollow() 98 Toast.show( 99 _( 100 msg`Following ${sanitizeDisplayName( 101 profile.displayName || profile.handle, 102 moderation.ui('displayName'), 103 )}`, 104 ), 105 ) 106 } catch (e: any) { 107 if (e?.name !== 'AbortError') { 108 logger.error('Failed to follow', {message: String(e)}) 109 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') 110 } 111 } 112 }) 113 } 114 115 const onPressUnfollow = () => { 116 requireAuth(async () => { 117 try { 118 track('ProfileHeader:UnfollowButtonClicked') 119 await queueUnfollow() 120 Toast.show( 121 _( 122 msg`No longer following ${sanitizeDisplayName( 123 profile.displayName || profile.handle, 124 moderation.ui('displayName'), 125 )}`, 126 ), 127 ) 128 } catch (e: any) { 129 if (e?.name !== 'AbortError') { 130 logger.error('Failed to unfollow', {message: String(e)}) 131 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') 132 } 133 } 134 }) 135 } 136 137 const unblockAccount = React.useCallback(async () => { 138 track('ProfileHeader:UnblockAccountButtonClicked') 139 try { 140 await queueUnblock() 141 Toast.show(_(msg`Account unblocked`)) 142 } catch (e: any) { 143 if (e?.name !== 'AbortError') { 144 logger.error('Failed to unblock account', {message: e}) 145 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') 146 } 147 } 148 }, [_, queueUnblock, track]) 149 150 const isMe = React.useMemo( 151 () => currentAccount?.did === profile.did, 152 [currentAccount, profile], 153 ) 154 155 return ( 156 <ProfileHeaderShell 157 profile={profile} 158 moderation={moderation} 159 hideBackButton={hideBackButton} 160 isPlaceholderProfile={isPlaceholderProfile}> 161 <View 162 style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]} 163 pointerEvents={isIOS ? 'auto' : 'box-none'}> 164 <View 165 style={[ 166 {paddingLeft: 90}, 167 a.flex_row, 168 a.justify_end, 169 a.gap_sm, 170 a.pb_sm, 171 a.flex_wrap, 172 ]} 173 pointerEvents={isIOS ? 'auto' : 'box-none'}> 174 {isMe ? ( 175 <Button 176 testID="profileHeaderEditProfileButton" 177 size="small" 178 color="secondary" 179 variant="solid" 180 onPress={onPressEditProfile} 181 label={_(msg`Edit profile`)} 182 style={[a.rounded_full, a.py_sm]}> 183 <ButtonText> 184 <Trans>Edit Profile</Trans> 185 </ButtonText> 186 </Button> 187 ) : profile.viewer?.blocking ? ( 188 profile.viewer?.blockingByList ? null : ( 189 <Button 190 testID="unblockBtn" 191 size="small" 192 color="secondary" 193 variant="solid" 194 label={_(msg`Unblock`)} 195 disabled={!hasSession} 196 onPress={() => unblockPromptControl.open()} 197 style={[a.rounded_full, a.py_sm]}> 198 <ButtonText> 199 <Trans context="action">Unblock</Trans> 200 </ButtonText> 201 </Button> 202 ) 203 ) : !profile.viewer?.blockedBy ? ( 204 <> 205 {hasSession && ( 206 <> 207 <MessageProfileButton profile={profile} /> 208 {!gate('show_follow_suggestions_in_profile') && ( 209 <Button 210 testID="suggestedFollowsBtn" 211 size="small" 212 color={showSuggestedFollows ? 'primary' : 'secondary'} 213 variant="solid" 214 shape="round" 215 onPress={() => 216 setShowSuggestedFollows(!showSuggestedFollows) 217 } 218 label={_(msg`Show follows similar to ${profile.handle}`)} 219 style={{width: 36, height: 36}}> 220 <FontAwesomeIcon 221 icon="user-plus" 222 style={ 223 showSuggestedFollows 224 ? {color: t.palette.white} 225 : t.atoms.text 226 } 227 size={14} 228 /> 229 </Button> 230 )} 231 </> 232 )} 233 234 <Button 235 testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'} 236 size="small" 237 color={profile.viewer?.following ? 'secondary' : 'primary'} 238 variant="solid" 239 label={ 240 profile.viewer?.following 241 ? _(msg`Unfollow ${profile.handle}`) 242 : _(msg`Follow ${profile.handle}`) 243 } 244 onPress={ 245 profile.viewer?.following ? onPressUnfollow : onPressFollow 246 } 247 style={[a.rounded_full, a.gap_xs, a.py_sm]}> 248 <ButtonIcon 249 position="left" 250 icon={profile.viewer?.following ? Check : Plus} 251 /> 252 <ButtonText> 253 {profile.viewer?.following ? ( 254 <Trans>Following</Trans> 255 ) : ( 256 <Trans>Follow</Trans> 257 )} 258 </ButtonText> 259 </Button> 260 </> 261 ) : null} 262 <ProfileMenu profile={profile} /> 263 </View> 264 <View style={[a.flex_col, a.gap_xs, a.pb_sm]}> 265 <ProfileHeaderDisplayName profile={profile} moderation={moderation} /> 266 <ProfileHeaderHandle profile={profile} /> 267 </View> 268 {!isPlaceholderProfile && !isBlockedUser && ( 269 <> 270 <ProfileHeaderMetrics profile={profile} /> 271 {descriptionRT && !moderation.ui('profileView').blur ? ( 272 <View pointerEvents="auto"> 273 <RichText 274 testID="profileHeaderDescription" 275 style={[a.text_md]} 276 numberOfLines={15} 277 value={descriptionRT} 278 enableTags 279 authorHandle={profile.handle} 280 /> 281 </View> 282 ) : undefined} 283 284 {!isMe && 285 !isBlockedUser && 286 shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( 287 <View style={[a.flex_row, a.align_center, a.gap_sm, a.pt_md]}> 288 <KnownFollowers 289 profile={profile} 290 moderationOpts={moderationOpts} 291 /> 292 </View> 293 )} 294 </> 295 )} 296 </View> 297 {showSuggestedFollows && ( 298 <ProfileHeaderSuggestedFollows 299 actorDid={profile.did} 300 requestDismiss={() => { 301 if (showSuggestedFollows) { 302 setShowSuggestedFollows(false) 303 } else { 304 track('ProfileHeader:SuggestedFollowsOpened') 305 setShowSuggestedFollows(true) 306 } 307 }} 308 /> 309 )} 310 <Prompt.Basic 311 control={unblockPromptControl} 312 title={_(msg`Unblock Account?`)} 313 description={_( 314 msg`The account will be able to interact with you after unblocking.`, 315 )} 316 onConfirm={unblockAccount} 317 confirmButtonCta={ 318 profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) 319 } 320 confirmButtonColor="negative" 321 /> 322 </ProfileHeaderShell> 323 ) 324} 325ProfileHeaderStandard = memo(ProfileHeaderStandard) 326export {ProfileHeaderStandard}