add suggested follow section to profile header (#1481)
* add suggested follow section to profile header * fix button overflow * don't even render on preview * fix useFollowDid and FollowButton race condition * add section header, close button, active state * lighten icon
This commit is contained in:
		
							parent
							
								
									498c3e2c27
								
							
						
					
					
						commit
						6df1bcad31
					
				
					 5 changed files with 406 additions and 34 deletions
				
			
		| 
						 | 
				
			
			@ -2,9 +2,9 @@ import React from 'react'
 | 
			
		|||
import {StyleProp, TextStyle, View} from 'react-native'
 | 
			
		||||
import {observer} from 'mobx-react-lite'
 | 
			
		||||
import {Button, ButtonType} from '../util/forms/Button'
 | 
			
		||||
import {useStores} from 'state/index'
 | 
			
		||||
import * as Toast from '../util/Toast'
 | 
			
		||||
import {FollowState} from 'state/models/cache/my-follows'
 | 
			
		||||
import {useFollowDid} from 'lib/hooks/useFollowDid'
 | 
			
		||||
 | 
			
		||||
export const FollowButton = observer(function FollowButtonImpl({
 | 
			
		||||
  unfollowedType = 'inverted',
 | 
			
		||||
| 
						 | 
				
			
			@ -19,44 +19,27 @@ export const FollowButton = observer(function FollowButtonImpl({
 | 
			
		|||
  onToggleFollow?: (v: boolean) => void
 | 
			
		||||
  labelStyle?: StyleProp<TextStyle>
 | 
			
		||||
}) {
 | 
			
		||||
  const store = useStores()
 | 
			
		||||
  const followState = store.me.follows.getFollowState(did)
 | 
			
		||||
  const {state, following, toggle} = useFollowDid({did})
 | 
			
		||||
 | 
			
		||||
  if (followState === FollowState.Unknown) {
 | 
			
		||||
    return <View />
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onToggleFollowInner = async () => {
 | 
			
		||||
    const updatedFollowState = await store.me.follows.fetchFollowState(did)
 | 
			
		||||
    if (updatedFollowState === FollowState.Following) {
 | 
			
		||||
      try {
 | 
			
		||||
        onToggleFollow?.(false)
 | 
			
		||||
        await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
 | 
			
		||||
        store.me.follows.removeFollow(did)
 | 
			
		||||
      } catch (e: any) {
 | 
			
		||||
        store.log.error('Failed to delete follow', e)
 | 
			
		||||
        Toast.show('An issue occurred, please try again.')
 | 
			
		||||
      }
 | 
			
		||||
    } else if (updatedFollowState === FollowState.NotFollowing) {
 | 
			
		||||
      try {
 | 
			
		||||
        onToggleFollow?.(true)
 | 
			
		||||
        const res = await store.agent.follow(did)
 | 
			
		||||
        store.me.follows.addFollow(did, res.uri)
 | 
			
		||||
      } catch (e: any) {
 | 
			
		||||
        store.log.error('Failed to create follow', e)
 | 
			
		||||
        Toast.show('An issue occurred, please try again.')
 | 
			
		||||
      }
 | 
			
		||||
  const onPress = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const {following} = await toggle()
 | 
			
		||||
      onToggleFollow?.(following)
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      Toast.show('An issue occurred, please try again.')
 | 
			
		||||
    }
 | 
			
		||||
  }, [toggle, onToggleFollow])
 | 
			
		||||
 | 
			
		||||
  if (state === FollowState.Unknown) {
 | 
			
		||||
    return <View />
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      type={
 | 
			
		||||
        followState === FollowState.Following ? followedType : unfollowedType
 | 
			
		||||
      }
 | 
			
		||||
      type={following ? followedType : unfollowedType}
 | 
			
		||||
      labelStyle={labelStyle}
 | 
			
		||||
      onPress={onToggleFollowInner}
 | 
			
		||||
      label={followState === FollowState.Following ? 'Unfollow' : 'Follow'}
 | 
			
		||||
      onPress={onPress}
 | 
			
		||||
      label={following ? 'Unfollow' : 'Follow'}
 | 
			
		||||
      withLoading={true}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,17 +38,20 @@ import {BACK_HITSLOP} from 'lib/constants'
 | 
			
		|||
import {isInvalidHandle} from 'lib/strings/handles'
 | 
			
		||||
import {makeProfileLink} from 'lib/routes/links'
 | 
			
		||||
import {Link} from '../util/Link'
 | 
			
		||||
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  view: ProfileModel
 | 
			
		||||
  onRefreshAll: () => void
 | 
			
		||||
  hideBackButton?: boolean
 | 
			
		||||
  isProfilePreview?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ProfileHeader = observer(function ProfileHeaderImpl({
 | 
			
		||||
  view,
 | 
			
		||||
  onRefreshAll,
 | 
			
		||||
  hideBackButton = false,
 | 
			
		||||
  isProfilePreview,
 | 
			
		||||
}: Props) {
 | 
			
		||||
  const pal = usePalette('default')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -95,6 +98,7 @@ export const ProfileHeader = observer(function ProfileHeaderImpl({
 | 
			
		|||
      view={view}
 | 
			
		||||
      onRefreshAll={onRefreshAll}
 | 
			
		||||
      hideBackButton={hideBackButton}
 | 
			
		||||
      isProfilePreview={isProfilePreview}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +107,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 | 
			
		|||
  view,
 | 
			
		||||
  onRefreshAll,
 | 
			
		||||
  hideBackButton = false,
 | 
			
		||||
  isProfilePreview,
 | 
			
		||||
}: Props) {
 | 
			
		||||
  const pal = usePalette('default')
 | 
			
		||||
  const palInverted = usePalette('inverted')
 | 
			
		||||
| 
						 | 
				
			
			@ -111,6 +116,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 | 
			
		|||
  const {track} = useAnalytics()
 | 
			
		||||
  const invalidHandle = isInvalidHandle(view.handle)
 | 
			
		||||
  const {isDesktop} = useWebMediaQueries()
 | 
			
		||||
  const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
 | 
			
		||||
 | 
			
		||||
  const onPressBack = React.useCallback(() => {
 | 
			
		||||
    navigation.goBack()
 | 
			
		||||
| 
						 | 
				
			
			@ -133,6 +139,8 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 | 
			
		|||
    )
 | 
			
		||||
    view?.toggleFollowing().then(
 | 
			
		||||
      () => {
 | 
			
		||||
        setShowSuggestedFollows(Boolean(view.viewer.following))
 | 
			
		||||
 | 
			
		||||
        Toast.show(
 | 
			
		||||
          `${
 | 
			
		||||
            view.viewer.following ? 'Following' : 'No longer following'
 | 
			
		||||
| 
						 | 
				
			
			@ -141,7 +149,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 | 
			
		|||
      },
 | 
			
		||||
      err => store.log.error('Failed to toggle follow', err),
 | 
			
		||||
    )
 | 
			
		||||
  }, [track, view, store.log])
 | 
			
		||||
  }, [track, view, store.log, setShowSuggestedFollows])
 | 
			
		||||
 | 
			
		||||
  const onPressEditProfile = React.useCallback(() => {
 | 
			
		||||
    track('ProfileHeader:EditProfileButtonClicked')
 | 
			
		||||
| 
						 | 
				
			
			@ -373,6 +381,39 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 | 
			
		|||
            </TouchableOpacity>
 | 
			
		||||
          ) : !view.viewer.blockedBy ? (
 | 
			
		||||
            <>
 | 
			
		||||
              {!isProfilePreview && (
 | 
			
		||||
                <TouchableOpacity
 | 
			
		||||
                  testID="suggestedFollowsBtn"
 | 
			
		||||
                  onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
 | 
			
		||||
                  style={[
 | 
			
		||||
                    styles.btn,
 | 
			
		||||
                    styles.mainBtn,
 | 
			
		||||
                    pal.btn,
 | 
			
		||||
                    {
 | 
			
		||||
                      paddingHorizontal: 10,
 | 
			
		||||
                      backgroundColor: showSuggestedFollows
 | 
			
		||||
                        ? colors.blue3
 | 
			
		||||
                        : pal.viewLight.backgroundColor,
 | 
			
		||||
                    },
 | 
			
		||||
                  ]}
 | 
			
		||||
                  accessibilityRole="button"
 | 
			
		||||
                  accessibilityLabel={`Show follows similar to ${view.handle}`}
 | 
			
		||||
                  accessibilityHint={`Shows a list of users similar to this user.`}>
 | 
			
		||||
                  <FontAwesomeIcon
 | 
			
		||||
                    icon="user-plus"
 | 
			
		||||
                    style={[
 | 
			
		||||
                      pal.text,
 | 
			
		||||
                      {
 | 
			
		||||
                        color: showSuggestedFollows
 | 
			
		||||
                          ? colors.white
 | 
			
		||||
                          : pal.text.color,
 | 
			
		||||
                      },
 | 
			
		||||
                    ]}
 | 
			
		||||
                    size={14}
 | 
			
		||||
                  />
 | 
			
		||||
                </TouchableOpacity>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              {store.me.follows.getFollowState(view.did) ===
 | 
			
		||||
              FollowState.Following ? (
 | 
			
		||||
                <TouchableOpacity
 | 
			
		||||
| 
						 | 
				
			
			@ -504,6 +545,15 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
 | 
			
		|||
        )}
 | 
			
		||||
        <ProfileHeaderAlerts moderation={view.moderation} />
 | 
			
		||||
      </View>
 | 
			
		||||
 | 
			
		||||
      {!isProfilePreview && (
 | 
			
		||||
        <ProfileHeaderSuggestedFollows
 | 
			
		||||
          actorDid={view.did}
 | 
			
		||||
          active={showSuggestedFollows}
 | 
			
		||||
          requestDismiss={() => setShowSuggestedFollows(!showSuggestedFollows)}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {!isDesktop && !hideBackButton && (
 | 
			
		||||
        <TouchableWithoutFeedback
 | 
			
		||||
          onPress={onPressBack}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										288
									
								
								src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,288 @@
 | 
			
		|||
import React from 'react'
 | 
			
		||||
import {View, StyleSheet, ScrollView, Pressable} from 'react-native'
 | 
			
		||||
import Animated, {
 | 
			
		||||
  useSharedValue,
 | 
			
		||||
  withTiming,
 | 
			
		||||
  useAnimatedStyle,
 | 
			
		||||
  Easing,
 | 
			
		||||
} from 'react-native-reanimated'
 | 
			
		||||
import {useQuery} from '@tanstack/react-query'
 | 
			
		||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
 | 
			
		||||
import {observer} from 'mobx-react-lite'
 | 
			
		||||
import {
 | 
			
		||||
  FontAwesomeIcon,
 | 
			
		||||
  FontAwesomeIconStyle,
 | 
			
		||||
} from '@fortawesome/react-native-fontawesome'
 | 
			
		||||
 | 
			
		||||
import * as Toast from '../util/Toast'
 | 
			
		||||
import {useStores} from 'state/index'
 | 
			
		||||
import {usePalette} from 'lib/hooks/usePalette'
 | 
			
		||||
import {Text} from 'view/com/util/text/Text'
 | 
			
		||||
import {UserAvatar} from 'view/com/util/UserAvatar'
 | 
			
		||||
import {useFollowDid} from 'lib/hooks/useFollowDid'
 | 
			
		||||
import {Button} from 'view/com/util/forms/Button'
 | 
			
		||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
 | 
			
		||||
import {sanitizeHandle} from 'lib/strings/handles'
 | 
			
		||||
import {makeProfileLink} from 'lib/routes/links'
 | 
			
		||||
import {Link} from 'view/com/util/Link'
 | 
			
		||||
 | 
			
		||||
const OUTER_PADDING = 10
 | 
			
		||||
const INNER_PADDING = 14
 | 
			
		||||
const TOTAL_HEIGHT = 250
 | 
			
		||||
 | 
			
		||||
export function ProfileHeaderSuggestedFollows({
 | 
			
		||||
  actorDid,
 | 
			
		||||
  active,
 | 
			
		||||
  requestDismiss,
 | 
			
		||||
}: {
 | 
			
		||||
  actorDid: string
 | 
			
		||||
  active: boolean
 | 
			
		||||
  requestDismiss: () => void
 | 
			
		||||
}) {
 | 
			
		||||
  const pal = usePalette('default')
 | 
			
		||||
  const store = useStores()
 | 
			
		||||
  const animatedHeight = useSharedValue(0)
 | 
			
		||||
  const animatedStyles = useAnimatedStyle(() => ({
 | 
			
		||||
    opacity: animatedHeight.value / TOTAL_HEIGHT,
 | 
			
		||||
    height: animatedHeight.value,
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (active) {
 | 
			
		||||
      animatedHeight.value = withTiming(TOTAL_HEIGHT, {
 | 
			
		||||
        duration: 500,
 | 
			
		||||
        easing: Easing.inOut(Easing.exp),
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      animatedHeight.value = withTiming(0, {
 | 
			
		||||
        duration: 500,
 | 
			
		||||
        easing: Easing.inOut(Easing.exp),
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }, [active, animatedHeight])
 | 
			
		||||
 | 
			
		||||
  const {isLoading, data: suggestedFollows} = useQuery({
 | 
			
		||||
    enabled: active,
 | 
			
		||||
    cacheTime: 0,
 | 
			
		||||
    staleTime: 0,
 | 
			
		||||
    queryKey: ['suggested_follows_by_actor', actorDid],
 | 
			
		||||
    async queryFn() {
 | 
			
		||||
      try {
 | 
			
		||||
        const {
 | 
			
		||||
          data: {suggestions},
 | 
			
		||||
          success,
 | 
			
		||||
        } = await store.agent.app.bsky.graph.getSuggestedFollowsByActor({
 | 
			
		||||
          actor: actorDid,
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (!success) {
 | 
			
		||||
          return []
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        store.me.follows.hydrateProfiles(suggestions)
 | 
			
		||||
 | 
			
		||||
        return suggestions
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        return []
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Animated.View style={[{overflow: 'hidden', opacity: 0}, animatedStyles]}>
 | 
			
		||||
      <View style={{paddingVertical: OUTER_PADDING}}>
 | 
			
		||||
        <View
 | 
			
		||||
          style={{
 | 
			
		||||
            backgroundColor: pal.viewLight.backgroundColor,
 | 
			
		||||
            height: '100%',
 | 
			
		||||
            paddingTop: INNER_PADDING / 2,
 | 
			
		||||
            paddingBottom: INNER_PADDING,
 | 
			
		||||
          }}>
 | 
			
		||||
          <View
 | 
			
		||||
            style={{
 | 
			
		||||
              flexDirection: 'row',
 | 
			
		||||
              justifyContent: 'space-between',
 | 
			
		||||
              alignItems: 'center',
 | 
			
		||||
              paddingTop: 4,
 | 
			
		||||
              paddingBottom: INNER_PADDING / 2,
 | 
			
		||||
              paddingLeft: INNER_PADDING,
 | 
			
		||||
              paddingRight: INNER_PADDING / 2,
 | 
			
		||||
            }}>
 | 
			
		||||
            <Text type="sm-bold" style={[pal.textLight]}>
 | 
			
		||||
              Suggested for you
 | 
			
		||||
            </Text>
 | 
			
		||||
 | 
			
		||||
            <Pressable
 | 
			
		||||
              accessibilityRole="button"
 | 
			
		||||
              onPress={requestDismiss}
 | 
			
		||||
              hitSlop={10}
 | 
			
		||||
              style={{padding: INNER_PADDING / 2}}>
 | 
			
		||||
              <FontAwesomeIcon
 | 
			
		||||
                icon="x"
 | 
			
		||||
                size={12}
 | 
			
		||||
                style={pal.textLight as FontAwesomeIconStyle}
 | 
			
		||||
              />
 | 
			
		||||
            </Pressable>
 | 
			
		||||
          </View>
 | 
			
		||||
 | 
			
		||||
          <ScrollView
 | 
			
		||||
            horizontal
 | 
			
		||||
            showsHorizontalScrollIndicator={false}
 | 
			
		||||
            contentContainerStyle={{
 | 
			
		||||
              alignItems: 'flex-start',
 | 
			
		||||
              paddingLeft: INNER_PADDING / 2,
 | 
			
		||||
            }}>
 | 
			
		||||
            {isLoading ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <SuggestedFollowSkeleton />
 | 
			
		||||
                <SuggestedFollowSkeleton />
 | 
			
		||||
                <SuggestedFollowSkeleton />
 | 
			
		||||
                <SuggestedFollowSkeleton />
 | 
			
		||||
                <SuggestedFollowSkeleton />
 | 
			
		||||
                <SuggestedFollowSkeleton />
 | 
			
		||||
              </>
 | 
			
		||||
            ) : suggestedFollows ? (
 | 
			
		||||
              suggestedFollows.map(profile => (
 | 
			
		||||
                <SuggestedFollow key={profile.did} profile={profile} />
 | 
			
		||||
              ))
 | 
			
		||||
            ) : (
 | 
			
		||||
              <View />
 | 
			
		||||
            )}
 | 
			
		||||
          </ScrollView>
 | 
			
		||||
        </View>
 | 
			
		||||
      </View>
 | 
			
		||||
    </Animated.View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SuggestedFollowSkeleton() {
 | 
			
		||||
  const pal = usePalette('default')
 | 
			
		||||
  return (
 | 
			
		||||
    <View
 | 
			
		||||
      style={[
 | 
			
		||||
        styles.suggestedFollowCardOuter,
 | 
			
		||||
        {
 | 
			
		||||
          backgroundColor: pal.view.backgroundColor,
 | 
			
		||||
        },
 | 
			
		||||
      ]}>
 | 
			
		||||
      <View
 | 
			
		||||
        style={{
 | 
			
		||||
          height: 60,
 | 
			
		||||
          width: 60,
 | 
			
		||||
          borderRadius: 60,
 | 
			
		||||
          backgroundColor: pal.viewLight.backgroundColor,
 | 
			
		||||
          opacity: 0.6,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <View
 | 
			
		||||
        style={{
 | 
			
		||||
          height: 17,
 | 
			
		||||
          width: 70,
 | 
			
		||||
          borderRadius: 4,
 | 
			
		||||
          backgroundColor: pal.viewLight.backgroundColor,
 | 
			
		||||
          marginTop: 12,
 | 
			
		||||
          marginBottom: 4,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <View
 | 
			
		||||
        style={{
 | 
			
		||||
          height: 12,
 | 
			
		||||
          width: 70,
 | 
			
		||||
          borderRadius: 4,
 | 
			
		||||
          backgroundColor: pal.viewLight.backgroundColor,
 | 
			
		||||
          marginBottom: 12,
 | 
			
		||||
          opacity: 0.6,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <View
 | 
			
		||||
        style={{
 | 
			
		||||
          height: 32,
 | 
			
		||||
          borderRadius: 32,
 | 
			
		||||
          width: '100%',
 | 
			
		||||
          backgroundColor: pal.viewLight.backgroundColor,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SuggestedFollow = observer(function SuggestedFollowImpl({
 | 
			
		||||
  profile,
 | 
			
		||||
}: {
 | 
			
		||||
  profile: AppBskyActorDefs.ProfileView
 | 
			
		||||
}) {
 | 
			
		||||
  const pal = usePalette('default')
 | 
			
		||||
  const store = useStores()
 | 
			
		||||
  const {following, toggle} = useFollowDid({did: profile.did})
 | 
			
		||||
  const moderation = moderateProfile(profile, store.preferences.moderationOpts)
 | 
			
		||||
 | 
			
		||||
  const onPress = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await toggle()
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      Toast.show('An issue occurred, please try again.')
 | 
			
		||||
    }
 | 
			
		||||
  }, [toggle])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Link
 | 
			
		||||
      href={makeProfileLink(profile)}
 | 
			
		||||
      title={profile.handle}
 | 
			
		||||
      asAnchor
 | 
			
		||||
      anchorNoUnderline>
 | 
			
		||||
      <View
 | 
			
		||||
        style={[
 | 
			
		||||
          styles.suggestedFollowCardOuter,
 | 
			
		||||
          {
 | 
			
		||||
            backgroundColor: pal.view.backgroundColor,
 | 
			
		||||
          },
 | 
			
		||||
        ]}>
 | 
			
		||||
        <UserAvatar
 | 
			
		||||
          size={60}
 | 
			
		||||
          avatar={profile.avatar}
 | 
			
		||||
          moderation={moderation.avatar}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <View style={{width: '100%', paddingVertical: 12}}>
 | 
			
		||||
          <Text
 | 
			
		||||
            type="xs-medium"
 | 
			
		||||
            style={[pal.text, {textAlign: 'center'}]}
 | 
			
		||||
            numberOfLines={1}>
 | 
			
		||||
            {sanitizeDisplayName(
 | 
			
		||||
              profile.displayName || sanitizeHandle(profile.handle),
 | 
			
		||||
              moderation.profile,
 | 
			
		||||
            )}
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Text
 | 
			
		||||
            type="xs-medium"
 | 
			
		||||
            style={[pal.textLight, {textAlign: 'center'}]}
 | 
			
		||||
            numberOfLines={1}>
 | 
			
		||||
            {sanitizeHandle(profile.handle, '@')}
 | 
			
		||||
          </Text>
 | 
			
		||||
        </View>
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
          label={following ? 'Unfollow' : 'Follow'}
 | 
			
		||||
          type="inverted"
 | 
			
		||||
          labelStyle={{textAlign: 'center'}}
 | 
			
		||||
          onPress={onPress}
 | 
			
		||||
          withLoading
 | 
			
		||||
        />
 | 
			
		||||
      </View>
 | 
			
		||||
    </Link>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  suggestedFollowCardOuter: {
 | 
			
		||||
    marginHorizontal: INNER_PADDING / 2,
 | 
			
		||||
    paddingTop: 10,
 | 
			
		||||
    paddingBottom: 12,
 | 
			
		||||
    paddingHorizontal: 10,
 | 
			
		||||
    borderRadius: 8,
 | 
			
		||||
    width: 130,
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    overflow: 'hidden',
 | 
			
		||||
    flexShrink: 1,
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue