Merge remote-tracking branch 'upstream/main' into patch-3
This commit is contained in:
		
						commit
						ad43d594c9
					
				
					 174 changed files with 7262 additions and 5065 deletions
				
			
		|  | @ -3,7 +3,8 @@ import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' | |||
| import { | ||||
|   AppBskyActorDefs, | ||||
|   moderateProfile, | ||||
|   ProfileModeration, | ||||
|   ModerationCause, | ||||
|   ModerationDecision, | ||||
| } from '@atproto/api' | ||||
| import {Link} from '../util/Link' | ||||
| import {Text} from '../util/text/Text' | ||||
|  | @ -14,16 +15,13 @@ import {FollowButton} from './FollowButton' | |||
| import {sanitizeDisplayName} from 'lib/strings/display-names' | ||||
| import {sanitizeHandle} from 'lib/strings/handles' | ||||
| import {makeProfileLink} from 'lib/routes/links' | ||||
| import { | ||||
|   describeModerationCause, | ||||
|   getProfileModerationCauses, | ||||
|   getModerationCauseKey, | ||||
| } from 'lib/moderation' | ||||
| import {getModerationCauseKey, isJustAMute} from 'lib/moderation' | ||||
| import {Shadow} from '#/state/cache/types' | ||||
| import {useModerationOpts} from '#/state/queries/preferences' | ||||
| import {useProfileShadow} from '#/state/cache/profile-shadow' | ||||
| import {useSession} from '#/state/session' | ||||
| import {Trans} from '@lingui/macro' | ||||
| import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' | ||||
| 
 | ||||
| export function ProfileCard({ | ||||
|   testID, | ||||
|  | @ -33,6 +31,7 @@ export function ProfileCard({ | |||
|   noBorder, | ||||
|   followers, | ||||
|   renderButton, | ||||
|   onPress, | ||||
|   style, | ||||
| }: { | ||||
|   testID?: string | ||||
|  | @ -44,6 +43,7 @@ export function ProfileCard({ | |||
|   renderButton?: ( | ||||
|     profile: Shadow<AppBskyActorDefs.ProfileViewBasic>, | ||||
|   ) => React.ReactNode | ||||
|   onPress?: () => void | ||||
|   style?: StyleProp<ViewStyle> | ||||
| }) { | ||||
|   const pal = usePalette('default') | ||||
|  | @ -53,11 +53,8 @@ export function ProfileCard({ | |||
|     return null | ||||
|   } | ||||
|   const moderation = moderateProfile(profile, moderationOpts) | ||||
|   if ( | ||||
|     !noModFilter && | ||||
|     moderation.account.filter && | ||||
|     moderation.account.cause?.type !== 'muted' | ||||
|   ) { | ||||
|   const modui = moderation.ui('profileList') | ||||
|   if (!noModFilter && modui.filter && !isJustAMute(modui)) { | ||||
|     return null | ||||
|   } | ||||
| 
 | ||||
|  | @ -73,6 +70,7 @@ export function ProfileCard({ | |||
|       ]} | ||||
|       href={makeProfileLink(profile)} | ||||
|       title={profile.handle} | ||||
|       onBeforePress={onPress} | ||||
|       asAnchor | ||||
|       anchorNoUnderline> | ||||
|       <View style={styles.layout}> | ||||
|  | @ -80,7 +78,7 @@ export function ProfileCard({ | |||
|           <UserAvatar | ||||
|             size={40} | ||||
|             avatar={profile.avatar} | ||||
|             moderation={moderation.avatar} | ||||
|             moderation={moderation.ui('avatar')} | ||||
|           /> | ||||
|         </View> | ||||
|         <View style={styles.layoutContent}> | ||||
|  | @ -91,7 +89,7 @@ export function ProfileCard({ | |||
|             lineHeight={1.2}> | ||||
|             {sanitizeDisplayName( | ||||
|               profile.displayName || sanitizeHandle(profile.handle), | ||||
|               moderation.profile, | ||||
|               moderation.ui('displayName'), | ||||
|             )} | ||||
|           </Text> | ||||
|           <Text type="md" style={[pal.textLight]} numberOfLines={1}> | ||||
|  | @ -119,17 +117,17 @@ export function ProfileCard({ | |||
|   ) | ||||
| } | ||||
| 
 | ||||
| function ProfileCardPills({ | ||||
| export function ProfileCardPills({ | ||||
|   followedBy, | ||||
|   moderation, | ||||
| }: { | ||||
|   followedBy: boolean | ||||
|   moderation: ProfileModeration | ||||
|   moderation: ModerationDecision | ||||
| }) { | ||||
|   const pal = usePalette('default') | ||||
| 
 | ||||
|   const causes = getProfileModerationCauses(moderation) | ||||
|   if (!followedBy && !causes.length) { | ||||
|   const modui = moderation.ui('profileList') | ||||
|   if (!followedBy && !modui.inform && !modui.alert) { | ||||
|     return null | ||||
|   } | ||||
| 
 | ||||
|  | @ -142,19 +140,41 @@ function ProfileCardPills({ | |||
|           </Text> | ||||
|         </View> | ||||
|       )} | ||||
|       {causes.map(cause => { | ||||
|         const desc = describeModerationCause(cause, 'account') | ||||
|         return ( | ||||
|           <View | ||||
|             style={[s.mt5, pal.btn, styles.pill]} | ||||
|             key={getModerationCauseKey(cause)}> | ||||
|             <Text type="xs" style={pal.text}> | ||||
|               {cause?.type === 'label' ? '⚠' : ''} | ||||
|               {desc.name} | ||||
|             </Text> | ||||
|           </View> | ||||
|         ) | ||||
|       })} | ||||
|       {modui.alerts.map(alert => ( | ||||
|         <ProfileCardPillModerationCause | ||||
|           key={getModerationCauseKey(alert)} | ||||
|           cause={alert} | ||||
|           severity="alert" | ||||
|         /> | ||||
|       ))} | ||||
|       {modui.informs.map(inform => ( | ||||
|         <ProfileCardPillModerationCause | ||||
|           key={getModerationCauseKey(inform)} | ||||
|           cause={inform} | ||||
|           severity="inform" | ||||
|         /> | ||||
|       ))} | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function ProfileCardPillModerationCause({ | ||||
|   cause, | ||||
|   severity, | ||||
| }: { | ||||
|   cause: ModerationCause | ||||
|   severity: 'alert' | 'inform' | ||||
| }) { | ||||
|   const pal = usePalette('default') | ||||
|   const {name} = useModerationCauseDescription(cause) | ||||
|   return ( | ||||
|     <View | ||||
|       style={[s.mt5, pal.btn, styles.pill]} | ||||
|       key={getModerationCauseKey(cause)}> | ||||
|       <Text type="xs" style={pal.text}> | ||||
|         {severity === 'alert' ? '⚠ ' : ''} | ||||
|         {name} | ||||
|       </Text> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|  | @ -177,7 +197,7 @@ function FollowersList({ | |||
|         f, | ||||
|         mod: moderateProfile(f, moderationOpts), | ||||
|       })) | ||||
|       .filter(({mod}) => !mod.account.filter) | ||||
|       .filter(({mod}) => !mod.ui('profileList').filter) | ||||
|   }, [followers, moderationOpts]) | ||||
| 
 | ||||
|   if (!followersWithMods?.length) { | ||||
|  | @ -199,7 +219,11 @@ function FollowersList({ | |||
|       {followersWithMods.slice(0, 3).map(({f, mod}) => ( | ||||
|         <View key={f.did} style={styles.followedByAviContainer}> | ||||
|           <View style={[styles.followedByAvi, pal.view]}> | ||||
|             <UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} /> | ||||
|             <UserAvatar | ||||
|               avatar={f.avatar} | ||||
|               size={32} | ||||
|               moderation={mod.ui('avatar')} | ||||
|             /> | ||||
|           </View> | ||||
|         </View> | ||||
|       ))} | ||||
|  | @ -212,11 +236,13 @@ export function ProfileCardWithFollowBtn({ | |||
|   noBg, | ||||
|   noBorder, | ||||
|   followers, | ||||
|   onPress, | ||||
| }: { | ||||
|   profile: AppBskyActorDefs.ProfileViewBasic | ||||
|   noBg?: boolean | ||||
|   noBorder?: boolean | ||||
|   followers?: AppBskyActorDefs.ProfileView[] | undefined | ||||
|   onPress?: () => void | ||||
| }) { | ||||
|   const {currentAccount} = useSession() | ||||
|   const isMe = profile.did === currentAccount?.did | ||||
|  | @ -234,6 +260,7 @@ export function ProfileCardWithFollowBtn({ | |||
|               <FollowButton profile={profileShadow} logContext="ProfileCard" /> | ||||
|             ) | ||||
|       } | ||||
|       onPress={onPress} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -1,598 +0,0 @@ | |||
| import React, {memo, useMemo} from 'react' | ||||
| import { | ||||
|   StyleSheet, | ||||
|   TouchableOpacity, | ||||
|   TouchableWithoutFeedback, | ||||
|   View, | ||||
| } from 'react-native' | ||||
| import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' | ||||
| import {useNavigation} from '@react-navigation/native' | ||||
| import { | ||||
|   AppBskyActorDefs, | ||||
|   ModerationOpts, | ||||
|   moderateProfile, | ||||
|   RichText as RichTextAPI, | ||||
| } from '@atproto/api' | ||||
| import {Trans, msg} from '@lingui/macro' | ||||
| import {useLingui} from '@lingui/react' | ||||
| import {NavigationProp} from 'lib/routes/types' | ||||
| import {isNative} from 'platform/detection' | ||||
| import {BlurView} from '../util/BlurView' | ||||
| import * as Toast from '../util/Toast' | ||||
| import {LoadingPlaceholder} from '../util/LoadingPlaceholder' | ||||
| import {Text} from '../util/text/Text' | ||||
| import {ThemedText} from '../util/text/ThemedText' | ||||
| import {RichText} from '#/components/RichText' | ||||
| import {UserAvatar} from '../util/UserAvatar' | ||||
| import {UserBanner} from '../util/UserBanner' | ||||
| import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts' | ||||
| import {formatCount} from '../util/numeric/format' | ||||
| import {Link} from '../util/Link' | ||||
| import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows' | ||||
| import {useModalControls} from '#/state/modals' | ||||
| import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox' | ||||
| import { | ||||
|   useProfileBlockMutationQueue, | ||||
|   useProfileFollowMutationQueue, | ||||
| } from '#/state/queries/profile' | ||||
| import {usePalette} from 'lib/hooks/usePalette' | ||||
| import {useAnalytics} from 'lib/analytics/analytics' | ||||
| import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' | ||||
| import {BACK_HITSLOP} from 'lib/constants' | ||||
| import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles' | ||||
| import {makeProfileLink} from 'lib/routes/links' | ||||
| import {pluralize} from 'lib/strings/helpers' | ||||
| import {sanitizeDisplayName} from 'lib/strings/display-names' | ||||
| import {s, colors} from 'lib/styles' | ||||
| import {logger} from '#/logger' | ||||
| import {useSession} from '#/state/session' | ||||
| import {Shadow} from '#/state/cache/types' | ||||
| import {useRequireAuth} from '#/state/session' | ||||
| import {LabelInfo} from '../util/moderation/LabelInfo' | ||||
| import {useProfileShadow} from 'state/cache/profile-shadow' | ||||
| import {atoms as a} from '#/alf' | ||||
| import {ProfileMenu} from 'view/com/profile/ProfileMenu' | ||||
| import * as Prompt from '#/components/Prompt' | ||||
| 
 | ||||
| let ProfileHeaderLoading = (_props: {}): React.ReactNode => { | ||||
|   const pal = usePalette('default') | ||||
|   return ( | ||||
|     <View style={pal.view}> | ||||
|       <LoadingPlaceholder width="100%" height={150} style={{borderRadius: 0}} /> | ||||
|       <View | ||||
|         style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> | ||||
|         <LoadingPlaceholder width={80} height={80} style={styles.br40} /> | ||||
|       </View> | ||||
|       <View style={styles.content}> | ||||
|         <View style={[styles.buttonsLine]}> | ||||
|           <LoadingPlaceholder width={167} height={31} style={styles.br50} /> | ||||
|         </View> | ||||
|       </View> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
| ProfileHeaderLoading = memo(ProfileHeaderLoading) | ||||
| export {ProfileHeaderLoading} | ||||
| 
 | ||||
| interface Props { | ||||
|   profile: AppBskyActorDefs.ProfileViewDetailed | ||||
|   descriptionRT: RichTextAPI | null | ||||
|   moderationOpts: ModerationOpts | ||||
|   hideBackButton?: boolean | ||||
|   isPlaceholderProfile?: boolean | ||||
| } | ||||
| 
 | ||||
| let ProfileHeader = ({ | ||||
|   profile: profileUnshadowed, | ||||
|   descriptionRT, | ||||
|   moderationOpts, | ||||
|   hideBackButton = false, | ||||
|   isPlaceholderProfile, | ||||
| }: Props): React.ReactNode => { | ||||
|   const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> = | ||||
|     useProfileShadow(profileUnshadowed) | ||||
|   const pal = usePalette('default') | ||||
|   const palInverted = usePalette('inverted') | ||||
|   const {currentAccount, hasSession} = useSession() | ||||
|   const requireAuth = useRequireAuth() | ||||
|   const {_} = useLingui() | ||||
|   const {openModal} = useModalControls() | ||||
|   const {openLightbox} = useLightboxControls() | ||||
|   const navigation = useNavigation<NavigationProp>() | ||||
|   const {track} = useAnalytics() | ||||
|   const invalidHandle = isInvalidHandle(profile.handle) | ||||
|   const {isDesktop} = useWebMediaQueries() | ||||
|   const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) | ||||
|   const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( | ||||
|     profile, | ||||
|     'ProfileHeader', | ||||
|   ) | ||||
|   const [__, queueUnblock] = useProfileBlockMutationQueue(profile) | ||||
|   const unblockPromptControl = Prompt.usePromptControl() | ||||
|   const moderation = useMemo( | ||||
|     () => moderateProfile(profile, moderationOpts), | ||||
|     [profile, moderationOpts], | ||||
|   ) | ||||
| 
 | ||||
|   const onPressBack = React.useCallback(() => { | ||||
|     if (navigation.canGoBack()) { | ||||
|       navigation.goBack() | ||||
|     } else { | ||||
|       navigation.navigate('Home') | ||||
|     } | ||||
|   }, [navigation]) | ||||
| 
 | ||||
|   const onPressAvi = React.useCallback(() => { | ||||
|     if ( | ||||
|       profile.avatar && | ||||
|       !(moderation.avatar.blur && moderation.avatar.noOverride) | ||||
|     ) { | ||||
|       openLightbox(new ProfileImageLightbox(profile)) | ||||
|     } | ||||
|   }, [openLightbox, profile, moderation]) | ||||
| 
 | ||||
|   const onPressFollow = () => { | ||||
|     requireAuth(async () => { | ||||
|       try { | ||||
|         track('ProfileHeader:FollowButtonClicked') | ||||
|         await queueFollow() | ||||
|         Toast.show( | ||||
|           _( | ||||
|             msg`Following ${sanitizeDisplayName( | ||||
|               profile.displayName || profile.handle, | ||||
|             )}`,
 | ||||
|           ), | ||||
|         ) | ||||
|       } catch (e: any) { | ||||
|         if (e?.name !== 'AbortError') { | ||||
|           logger.error('Failed to follow', {message: String(e)}) | ||||
|           Toast.show(_(msg`There was an issue! ${e.toString()}`)) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   const onPressUnfollow = () => { | ||||
|     requireAuth(async () => { | ||||
|       try { | ||||
|         track('ProfileHeader:UnfollowButtonClicked') | ||||
|         await queueUnfollow() | ||||
|         Toast.show( | ||||
|           _( | ||||
|             msg`No longer following ${sanitizeDisplayName( | ||||
|               profile.displayName || profile.handle, | ||||
|             )}`,
 | ||||
|           ), | ||||
|         ) | ||||
|       } catch (e: any) { | ||||
|         if (e?.name !== 'AbortError') { | ||||
|           logger.error('Failed to unfollow', {message: String(e)}) | ||||
|           Toast.show(_(msg`There was an issue! ${e.toString()}`)) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   const onPressEditProfile = React.useCallback(() => { | ||||
|     track('ProfileHeader:EditProfileButtonClicked') | ||||
|     openModal({ | ||||
|       name: 'edit-profile', | ||||
|       profile, | ||||
|     }) | ||||
|   }, [track, openModal, profile]) | ||||
| 
 | ||||
|   const unblockAccount = React.useCallback(async () => { | ||||
|     track('ProfileHeader:UnblockAccountButtonClicked') | ||||
|     try { | ||||
|       await queueUnblock() | ||||
|       Toast.show(_(msg`Account unblocked`)) | ||||
|     } catch (e: any) { | ||||
|       if (e?.name !== 'AbortError') { | ||||
|         logger.error('Failed to unblock account', {message: e}) | ||||
|         Toast.show(_(msg`There was an issue! ${e.toString()}`)) | ||||
|       } | ||||
|     } | ||||
|   }, [_, queueUnblock, track]) | ||||
| 
 | ||||
|   const isMe = React.useMemo( | ||||
|     () => currentAccount?.did === profile.did, | ||||
|     [currentAccount, profile], | ||||
|   ) | ||||
| 
 | ||||
|   const blockHide = | ||||
|     !isMe && (profile.viewer?.blocking || profile.viewer?.blockedBy) | ||||
|   const following = formatCount(profile.followsCount || 0) | ||||
|   const followers = formatCount(profile.followersCount || 0) | ||||
|   const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') | ||||
| 
 | ||||
|   return ( | ||||
|     <View style={[pal.view]} pointerEvents="box-none"> | ||||
|       <View pointerEvents="none"> | ||||
|         {isPlaceholderProfile ? ( | ||||
|           <LoadingPlaceholder | ||||
|             width="100%" | ||||
|             height={150} | ||||
|             style={{borderRadius: 0}} | ||||
|           /> | ||||
|         ) : ( | ||||
|           <UserBanner banner={profile.banner} moderation={moderation.avatar} /> | ||||
|         )} | ||||
|       </View> | ||||
|       <View style={styles.content} pointerEvents="box-none"> | ||||
|         <View style={[styles.buttonsLine]} pointerEvents="box-none"> | ||||
|           {isMe ? ( | ||||
|             <TouchableOpacity | ||||
|               testID="profileHeaderEditProfileButton" | ||||
|               onPress={onPressEditProfile} | ||||
|               style={[styles.btn, styles.mainBtn, pal.btn]} | ||||
|               accessibilityRole="button" | ||||
|               accessibilityLabel={_(msg`Edit profile`)} | ||||
|               accessibilityHint={_( | ||||
|                 msg`Opens editor for profile display name, avatar, background image, and description`, | ||||
|               )}> | ||||
|               <Text type="button" style={pal.text}> | ||||
|                 <Trans>Edit Profile</Trans> | ||||
|               </Text> | ||||
|             </TouchableOpacity> | ||||
|           ) : profile.viewer?.blocking ? ( | ||||
|             profile.viewer?.blockingByList ? null : ( | ||||
|               <TouchableOpacity | ||||
|                 testID="unblockBtn" | ||||
|                 onPress={() => unblockPromptControl.open()} | ||||
|                 style={[styles.btn, styles.mainBtn, pal.btn]} | ||||
|                 accessibilityRole="button" | ||||
|                 accessibilityLabel={_(msg`Unblock`)} | ||||
|                 accessibilityHint=""> | ||||
|                 <Text type="button" style={[pal.text, s.bold]}> | ||||
|                   <Trans context="action">Unblock</Trans> | ||||
|                 </Text> | ||||
|               </TouchableOpacity> | ||||
|             ) | ||||
|           ) : !profile.viewer?.blockedBy ? ( | ||||
|             <> | ||||
|               {hasSession && ( | ||||
|                 <TouchableOpacity | ||||
|                   testID="suggestedFollowsBtn" | ||||
|                   onPress={() => setShowSuggestedFollows(!showSuggestedFollows)} | ||||
|                   style={[ | ||||
|                     styles.btn, | ||||
|                     styles.mainBtn, | ||||
|                     pal.btn, | ||||
|                     { | ||||
|                       paddingHorizontal: 10, | ||||
|                       backgroundColor: showSuggestedFollows | ||||
|                         ? pal.colors.text | ||||
|                         : pal.colors.backgroundLight, | ||||
|                     }, | ||||
|                   ]} | ||||
|                   accessibilityRole="button" | ||||
|                   accessibilityLabel={_( | ||||
|                     msg`Show follows similar to ${profile.handle}`, | ||||
|                   )} | ||||
|                   accessibilityHint={_( | ||||
|                     msg`Shows a list of users similar to this user.`, | ||||
|                   )}> | ||||
|                   <FontAwesomeIcon | ||||
|                     icon="user-plus" | ||||
|                     style={[ | ||||
|                       pal.text, | ||||
|                       { | ||||
|                         color: showSuggestedFollows | ||||
|                           ? pal.textInverted.color | ||||
|                           : pal.text.color, | ||||
|                       }, | ||||
|                     ]} | ||||
|                     size={14} | ||||
|                   /> | ||||
|                 </TouchableOpacity> | ||||
|               )} | ||||
| 
 | ||||
|               {profile.viewer?.following ? ( | ||||
|                 <TouchableOpacity | ||||
|                   testID="unfollowBtn" | ||||
|                   onPress={onPressUnfollow} | ||||
|                   style={[styles.btn, styles.mainBtn, pal.btn]} | ||||
|                   accessibilityRole="button" | ||||
|                   accessibilityLabel={_(msg`Unfollow ${profile.handle}`)} | ||||
|                   accessibilityHint={_( | ||||
|                     msg`Hides posts from ${profile.handle} in your feed`, | ||||
|                   )}> | ||||
|                   <FontAwesomeIcon | ||||
|                     icon="check" | ||||
|                     style={[pal.text, s.mr5]} | ||||
|                     size={14} | ||||
|                   /> | ||||
|                   <Text type="button" style={pal.text}> | ||||
|                     <Trans>Following</Trans> | ||||
|                   </Text> | ||||
|                 </TouchableOpacity> | ||||
|               ) : ( | ||||
|                 <TouchableOpacity | ||||
|                   testID="followBtn" | ||||
|                   onPress={onPressFollow} | ||||
|                   style={[styles.btn, styles.mainBtn, palInverted.view]} | ||||
|                   accessibilityRole="button" | ||||
|                   accessibilityLabel={_(msg`Follow ${profile.handle}`)} | ||||
|                   accessibilityHint={_( | ||||
|                     msg`Shows posts from ${profile.handle} in your feed`, | ||||
|                   )}> | ||||
|                   <FontAwesomeIcon | ||||
|                     icon="plus" | ||||
|                     style={[palInverted.text, s.mr5]} | ||||
|                   /> | ||||
|                   <Text type="button" style={[palInverted.text, s.bold]}> | ||||
|                     <Trans>Follow</Trans> | ||||
|                   </Text> | ||||
|                 </TouchableOpacity> | ||||
|               )} | ||||
|             </> | ||||
|           ) : null} | ||||
|           <ProfileMenu profile={profile} /> | ||||
|         </View> | ||||
|         <View pointerEvents="none"> | ||||
|           <Text | ||||
|             testID="profileHeaderDisplayName" | ||||
|             type="title-2xl" | ||||
|             style={[pal.text, styles.title]}> | ||||
|             {sanitizeDisplayName( | ||||
|               profile.displayName || sanitizeHandle(profile.handle), | ||||
|               moderation.profile, | ||||
|             )} | ||||
|           </Text> | ||||
|         </View> | ||||
|         <View style={styles.handleLine} pointerEvents="none"> | ||||
|           {profile.viewer?.followedBy && !blockHide ? ( | ||||
|             <View style={[styles.pill, pal.btn, s.mr5]}> | ||||
|               <Text type="xs" style={[pal.text]}> | ||||
|                 <Trans>Follows you</Trans> | ||||
|               </Text> | ||||
|             </View> | ||||
|           ) : undefined} | ||||
|           <ThemedText | ||||
|             type={invalidHandle ? 'xs' : 'md'} | ||||
|             fg={invalidHandle ? 'error' : 'light'} | ||||
|             border={invalidHandle ? 'error' : undefined} | ||||
|             style={[ | ||||
|               invalidHandle ? styles.invalidHandle : undefined, | ||||
|               styles.handle, | ||||
|             ]}> | ||||
|             {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} | ||||
|           </ThemedText> | ||||
|         </View> | ||||
|         {!isPlaceholderProfile && !blockHide && ( | ||||
|           <> | ||||
|             <View style={styles.metricsLine} pointerEvents="box-none"> | ||||
|               <Link | ||||
|                 testID="profileHeaderFollowersButton" | ||||
|                 style={[s.flexRow, s.mr10]} | ||||
|                 href={makeProfileLink(profile, 'followers')} | ||||
|                 onPressOut={() => | ||||
|                   track(`ProfileHeader:FollowersButtonClicked`, { | ||||
|                     handle: profile.handle, | ||||
|                   }) | ||||
|                 } | ||||
|                 asAnchor | ||||
|                 accessibilityLabel={`${followers} ${pluralizedFollowers}`} | ||||
|                 accessibilityHint={_(msg`Opens followers list`)}> | ||||
|                 <Text type="md" style={[s.bold, pal.text]}> | ||||
|                   {followers}{' '} | ||||
|                 </Text> | ||||
|                 <Text type="md" style={[pal.textLight]}> | ||||
|                   {pluralizedFollowers} | ||||
|                 </Text> | ||||
|               </Link> | ||||
|               <Link | ||||
|                 testID="profileHeaderFollowsButton" | ||||
|                 style={[s.flexRow, s.mr10]} | ||||
|                 href={makeProfileLink(profile, 'follows')} | ||||
|                 onPressOut={() => | ||||
|                   track(`ProfileHeader:FollowsButtonClicked`, { | ||||
|                     handle: profile.handle, | ||||
|                   }) | ||||
|                 } | ||||
|                 asAnchor | ||||
|                 accessibilityLabel={_(msg`${following} following`)} | ||||
|                 accessibilityHint={_(msg`Opens following list`)}> | ||||
|                 <Trans> | ||||
|                   <Text type="md" style={[s.bold, pal.text]}> | ||||
|                     {following}{' '} | ||||
|                   </Text> | ||||
|                   <Text type="md" style={[pal.textLight]}> | ||||
|                     following | ||||
|                   </Text> | ||||
|                 </Trans> | ||||
|               </Link> | ||||
|               <Text type="md" style={[s.bold, pal.text]}> | ||||
|                 {formatCount(profile.postsCount || 0)}{' '} | ||||
|                 <Text type="md" style={[pal.textLight]}> | ||||
|                   {pluralize(profile.postsCount || 0, 'post')} | ||||
|                 </Text> | ||||
|               </Text> | ||||
|             </View> | ||||
|             {descriptionRT && !moderation.profile.blur ? ( | ||||
|               <View pointerEvents="auto" style={[styles.description]}> | ||||
|                 <RichText | ||||
|                   testID="profileHeaderDescription" | ||||
|                   style={[a.text_md]} | ||||
|                   numberOfLines={15} | ||||
|                   value={descriptionRT} | ||||
|                 /> | ||||
|               </View> | ||||
|             ) : undefined} | ||||
|           </> | ||||
|         )} | ||||
|         <ProfileHeaderAlerts moderation={moderation} /> | ||||
|         {isMe && ( | ||||
|           <LabelInfo details={{did: profile.did}} labels={profile.labels} /> | ||||
|         )} | ||||
|       </View> | ||||
| 
 | ||||
|       {showSuggestedFollows && ( | ||||
|         <ProfileHeaderSuggestedFollows | ||||
|           actorDid={profile.did} | ||||
|           requestDismiss={() => { | ||||
|             if (showSuggestedFollows) { | ||||
|               setShowSuggestedFollows(false) | ||||
|             } else { | ||||
|               track('ProfileHeader:SuggestedFollowsOpened') | ||||
|               setShowSuggestedFollows(true) | ||||
|             } | ||||
|           }} | ||||
|         /> | ||||
|       )} | ||||
| 
 | ||||
|       {!isDesktop && !hideBackButton && ( | ||||
|         <TouchableWithoutFeedback | ||||
|           testID="profileHeaderBackBtn" | ||||
|           onPress={onPressBack} | ||||
|           hitSlop={BACK_HITSLOP} | ||||
|           accessibilityRole="button" | ||||
|           accessibilityLabel={_(msg`Back`)} | ||||
|           accessibilityHint=""> | ||||
|           <View style={styles.backBtnWrapper}> | ||||
|             <BlurView style={styles.backBtn} blurType="dark"> | ||||
|               <FontAwesomeIcon size={18} icon="angle-left" style={s.white} /> | ||||
|             </BlurView> | ||||
|           </View> | ||||
|         </TouchableWithoutFeedback> | ||||
|       )} | ||||
|       <TouchableWithoutFeedback | ||||
|         testID="profileHeaderAviButton" | ||||
|         onPress={onPressAvi} | ||||
|         accessibilityRole="image" | ||||
|         accessibilityLabel={_(msg`View ${profile.handle}'s avatar`)} | ||||
|         accessibilityHint=""> | ||||
|         <View | ||||
|           style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> | ||||
|           <UserAvatar | ||||
|             size={80} | ||||
|             avatar={profile.avatar} | ||||
|             moderation={moderation.avatar} | ||||
|           /> | ||||
|         </View> | ||||
|       </TouchableWithoutFeedback> | ||||
|       <Prompt.Basic | ||||
|         control={unblockPromptControl} | ||||
|         title={_(msg`Unblock Account?`)} | ||||
|         description={_( | ||||
|           msg`The account will be able to interact with you after unblocking.`, | ||||
|         )} | ||||
|         onConfirm={unblockAccount} | ||||
|         confirmButtonCta={ | ||||
|           profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) | ||||
|         } | ||||
|         confirmButtonColor="negative" | ||||
|       /> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
| ProfileHeader = memo(ProfileHeader) | ||||
| export {ProfileHeader} | ||||
| 
 | ||||
| const styles = StyleSheet.create({ | ||||
|   banner: { | ||||
|     width: '100%', | ||||
|     height: 120, | ||||
|   }, | ||||
|   backBtnWrapper: { | ||||
|     position: 'absolute', | ||||
|     top: 10, | ||||
|     left: 10, | ||||
|     width: 30, | ||||
|     height: 30, | ||||
|     overflow: 'hidden', | ||||
|     borderRadius: 15, | ||||
|     // @ts-ignore web only
 | ||||
|     cursor: 'pointer', | ||||
|   }, | ||||
|   backBtn: { | ||||
|     width: 30, | ||||
|     height: 30, | ||||
|     borderRadius: 15, | ||||
|     alignItems: 'center', | ||||
|     justifyContent: 'center', | ||||
|   }, | ||||
|   avi: { | ||||
|     position: 'absolute', | ||||
|     top: 110, | ||||
|     left: 10, | ||||
|     width: 84, | ||||
|     height: 84, | ||||
|     borderRadius: 42, | ||||
|     borderWidth: 2, | ||||
|   }, | ||||
|   content: { | ||||
|     paddingTop: 8, | ||||
|     paddingHorizontal: 14, | ||||
|     paddingBottom: 4, | ||||
|   }, | ||||
| 
 | ||||
|   buttonsLine: { | ||||
|     flexDirection: 'row', | ||||
|     marginLeft: 'auto', | ||||
|     marginBottom: 12, | ||||
|   }, | ||||
|   primaryBtn: { | ||||
|     backgroundColor: colors.blue3, | ||||
|     paddingHorizontal: 24, | ||||
|     paddingVertical: 6, | ||||
|   }, | ||||
|   mainBtn: { | ||||
|     paddingHorizontal: 24, | ||||
|   }, | ||||
|   secondaryBtn: { | ||||
|     paddingHorizontal: 14, | ||||
|   }, | ||||
|   btn: { | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center', | ||||
|     justifyContent: 'center', | ||||
|     paddingVertical: 7, | ||||
|     borderRadius: 50, | ||||
|     marginLeft: 6, | ||||
|   }, | ||||
|   title: {lineHeight: 38}, | ||||
| 
 | ||||
|   // Word wrapping appears fine on
 | ||||
|   // mobile but overflows on desktop
 | ||||
|   handle: isNative | ||||
|     ? {} | ||||
|     : { | ||||
|         // @ts-ignore web only -prf
 | ||||
|         wordBreak: 'break-all', | ||||
|       }, | ||||
|   invalidHandle: { | ||||
|     borderWidth: 1, | ||||
|     borderRadius: 4, | ||||
|     paddingHorizontal: 4, | ||||
|   }, | ||||
| 
 | ||||
|   handleLine: { | ||||
|     flexDirection: 'row', | ||||
|     marginBottom: 8, | ||||
|   }, | ||||
| 
 | ||||
|   metricsLine: { | ||||
|     flexDirection: 'row', | ||||
|     marginBottom: 8, | ||||
|   }, | ||||
| 
 | ||||
|   description: { | ||||
|     marginBottom: 8, | ||||
|   }, | ||||
| 
 | ||||
|   detailLine: { | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center', | ||||
|     marginBottom: 5, | ||||
|   }, | ||||
| 
 | ||||
|   pill: { | ||||
|     borderRadius: 4, | ||||
|     paddingHorizontal: 6, | ||||
|     paddingVertical: 2, | ||||
|   }, | ||||
| 
 | ||||
|   br40: {borderRadius: 40}, | ||||
|   br50: {borderRadius: 50}, | ||||
| }) | ||||
|  | @ -219,7 +219,7 @@ function SuggestedFollow({ | |||
|         <UserAvatar | ||||
|           size={60} | ||||
|           avatar={profile.avatar} | ||||
|           moderation={moderation.avatar} | ||||
|           moderation={moderation.ui('avatar')} | ||||
|         /> | ||||
| 
 | ||||
|         <View style={{width: '100%', paddingVertical: 12}}> | ||||
|  | @ -229,7 +229,7 @@ function SuggestedFollow({ | |||
|             numberOfLines={1}> | ||||
|             {sanitizeDisplayName( | ||||
|               profile.displayName || sanitizeHandle(profile.handle), | ||||
|               moderation.profile, | ||||
|               moderation.ui('displayName'), | ||||
|             )} | ||||
|           </Text> | ||||
|           <Text | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import {toShareUrl} from 'lib/strings/url-helpers' | |||
| import {makeProfileLink} from 'lib/routes/links' | ||||
| import {useAnalytics} from 'lib/analytics/analytics' | ||||
| import {useModalControls} from 'state/modals' | ||||
| import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' | ||||
| import { | ||||
|   RQKEY as profileQueryKey, | ||||
|   useProfileBlockMutationQueue, | ||||
|  | @ -31,6 +32,7 @@ import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' | |||
| import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck' | ||||
| import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX' | ||||
| import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' | ||||
| import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' | ||||
| import {logger} from '#/logger' | ||||
| import {Shadow} from 'state/cache/types' | ||||
| import * as Prompt from '#/components/Prompt' | ||||
|  | @ -47,12 +49,17 @@ let ProfileMenu = ({ | |||
|   const pal = usePalette('default') | ||||
|   const {track} = useAnalytics() | ||||
|   const {openModal} = useModalControls() | ||||
|   const reportDialogControl = useReportDialogControl() | ||||
|   const queryClient = useQueryClient() | ||||
|   const isSelf = currentAccount?.did === profile.did | ||||
|   const isFollowing = profile.viewer?.following | ||||
|   const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy | ||||
|   const isFollowingBlockedAccount = isFollowing && isBlocked | ||||
|   const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked | ||||
| 
 | ||||
|   const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) | ||||
|   const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) | ||||
|   const [, queueUnfollow] = useProfileFollowMutationQueue( | ||||
|   const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( | ||||
|     profile, | ||||
|     'ProfileMenu', | ||||
|   ) | ||||
|  | @ -139,6 +146,19 @@ let ProfileMenu = ({ | |||
|     } | ||||
|   }, [profile.viewer?.blocking, track, _, queueUnblock, queueBlock]) | ||||
| 
 | ||||
|   const onPressFollowAccount = React.useCallback(async () => { | ||||
|     track('ProfileHeader:FollowButtonClicked') | ||||
|     try { | ||||
|       await queueFollow() | ||||
|       Toast.show(_(msg`Account followed`)) | ||||
|     } catch (e: any) { | ||||
|       if (e?.name !== 'AbortError') { | ||||
|         logger.error('Failed to follow account', {message: e}) | ||||
|         Toast.show(_(msg`There was an issue! ${e.toString()}`)) | ||||
|       } | ||||
|     } | ||||
|   }, [_, queueFollow, track]) | ||||
| 
 | ||||
|   const onPressUnfollowAccount = React.useCallback(async () => { | ||||
|     track('ProfileHeader:UnfollowButtonClicked') | ||||
|     try { | ||||
|  | @ -154,11 +174,8 @@ let ProfileMenu = ({ | |||
| 
 | ||||
|   const onPressReportAccount = React.useCallback(() => { | ||||
|     track('ProfileHeader:ReportAccountButtonClicked') | ||||
|     openModal({ | ||||
|       name: 'report', | ||||
|       did: profile.did, | ||||
|     }) | ||||
|   }, [track, openModal, profile]) | ||||
|     reportDialogControl.open() | ||||
|   }, [track, reportDialogControl]) | ||||
| 
 | ||||
|   return ( | ||||
|     <EventStopper onKeyDown={false}> | ||||
|  | @ -175,10 +192,9 @@ let ProfileMenu = ({ | |||
|                     flexDirection: 'row', | ||||
|                     alignItems: 'center', | ||||
|                     justifyContent: 'center', | ||||
|                     paddingVertical: 7, | ||||
|                     paddingVertical: 10, | ||||
|                     borderRadius: 50, | ||||
|                     marginLeft: 6, | ||||
|                     paddingHorizontal: 14, | ||||
|                     paddingHorizontal: 16, | ||||
|                   }, | ||||
|                   pal.btn, | ||||
|                 ]}> | ||||
|  | @ -210,10 +226,38 @@ let ProfileMenu = ({ | |||
|               <Menu.ItemIcon icon={Share} /> | ||||
|             </Menu.Item> | ||||
|           </Menu.Group> | ||||
| 
 | ||||
|           {hasSession && ( | ||||
|             <> | ||||
|               <Menu.Divider /> | ||||
|               <Menu.Group> | ||||
|                 {!isSelf && ( | ||||
|                   <> | ||||
|                     {(isLabelerAndNotBlocked || isFollowingBlockedAccount) && ( | ||||
|                       <Menu.Item | ||||
|                         testID="profileHeaderDropdownFollowBtn" | ||||
|                         label={ | ||||
|                           isFollowing | ||||
|                             ? _(msg`Unfollow Account`) | ||||
|                             : _(msg`Follow Account`) | ||||
|                         } | ||||
|                         onPress={ | ||||
|                           isFollowing | ||||
|                             ? onPressUnfollowAccount | ||||
|                             : onPressFollowAccount | ||||
|                         }> | ||||
|                         <Menu.ItemText> | ||||
|                           {isFollowing ? ( | ||||
|                             <Trans>Unfollow Account</Trans> | ||||
|                           ) : ( | ||||
|                             <Trans>Follow Account</Trans> | ||||
|                           )} | ||||
|                         </Menu.ItemText> | ||||
|                         <Menu.ItemIcon icon={isFollowing ? UserMinus : Plus} /> | ||||
|                       </Menu.Item> | ||||
|                     )} | ||||
|                   </> | ||||
|                 )} | ||||
|                 <Menu.Item | ||||
|                   testID="profileHeaderDropdownListAddRemoveBtn" | ||||
|                   label={_(msg`Add to Lists`)} | ||||
|  | @ -225,18 +269,6 @@ let ProfileMenu = ({ | |||
|                 </Menu.Item> | ||||
|                 {!isSelf && ( | ||||
|                   <> | ||||
|                     {profile.viewer?.following && | ||||
|                       (profile.viewer.blocking || profile.viewer.blockedBy) && ( | ||||
|                         <Menu.Item | ||||
|                           testID="profileHeaderDropdownUnfollowBtn" | ||||
|                           label={_(msg`Unfollow Account`)} | ||||
|                           onPress={onPressUnfollowAccount}> | ||||
|                           <Menu.ItemText> | ||||
|                             <Trans>Unfollow Account</Trans> | ||||
|                           </Menu.ItemText> | ||||
|                           <Menu.ItemIcon icon={UserMinus} /> | ||||
|                         </Menu.Item> | ||||
|                       )} | ||||
|                     {!profile.viewer?.blocking && | ||||
|                       !profile.viewer?.mutedByList && ( | ||||
|                         <Menu.Item | ||||
|  | @ -299,6 +331,11 @@ let ProfileMenu = ({ | |||
|         </Menu.Outer> | ||||
|       </Menu.Root> | ||||
| 
 | ||||
|       <ReportDialog | ||||
|         control={reportDialogControl} | ||||
|         params={{type: 'account', did: profile.did}} | ||||
|       /> | ||||
| 
 | ||||
|       <Prompt.Basic | ||||
|         control={blockPromptControl} | ||||
|         title={ | ||||
|  | @ -311,6 +348,10 @@ let ProfileMenu = ({ | |||
|             ? _( | ||||
|                 msg`The account will be able to interact with you after unblocking.`, | ||||
|               ) | ||||
|             : profile.associated?.labeler | ||||
|             ? _( | ||||
|                 msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`, | ||||
|               ) | ||||
|             : _( | ||||
|                 msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, | ||||
|               ) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue