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
46
src/lib/hooks/useFollowDid.ts
Normal file
46
src/lib/hooks/useFollowDid.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
import {FollowState} from 'state/models/cache/my-follows'
|
||||||
|
|
||||||
|
export function useFollowDid({did}: {did: string}) {
|
||||||
|
const store = useStores()
|
||||||
|
const state = store.me.follows.getFollowState(did)
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
following: state === FollowState.Following,
|
||||||
|
toggle: React.useCallback(async () => {
|
||||||
|
if (state === FollowState.Following) {
|
||||||
|
try {
|
||||||
|
await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
|
||||||
|
store.me.follows.removeFollow(did)
|
||||||
|
return {
|
||||||
|
state: FollowState.NotFollowing,
|
||||||
|
following: false,
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
store.log.error('Failed to delete follow', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else if (state === FollowState.NotFollowing) {
|
||||||
|
try {
|
||||||
|
const res = await store.agent.follow(did)
|
||||||
|
store.me.follows.addFollow(did, res.uri)
|
||||||
|
return {
|
||||||
|
state: FollowState.Following,
|
||||||
|
following: true,
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
store.log.error('Failed to create follow', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: FollowState.Unknown,
|
||||||
|
following: false,
|
||||||
|
}
|
||||||
|
}, [store, did, state]),
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,7 +41,12 @@ export const Component = observer(function ProfilePreviewImpl({
|
||||||
styles.headerWrapper,
|
styles.headerWrapper,
|
||||||
isLoading && isIOS && styles.headerPositionAdjust,
|
isLoading && isIOS && styles.headerPositionAdjust,
|
||||||
]}>
|
]}>
|
||||||
<ProfileHeader view={model} hideBackButton onRefreshAll={() => {}} />
|
<ProfileHeader
|
||||||
|
view={model}
|
||||||
|
hideBackButton
|
||||||
|
onRefreshAll={() => {}}
|
||||||
|
isProfilePreview
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.hintWrapper, pal.view]}>
|
<View style={[styles.hintWrapper, pal.view]}>
|
||||||
<View style={styles.hint}>
|
<View style={styles.hint}>
|
||||||
|
|
|
@ -2,9 +2,9 @@ import React from 'react'
|
||||||
import {StyleProp, TextStyle, View} from 'react-native'
|
import {StyleProp, TextStyle, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Button, ButtonType} from '../util/forms/Button'
|
import {Button, ButtonType} from '../util/forms/Button'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {FollowState} from 'state/models/cache/my-follows'
|
import {FollowState} from 'state/models/cache/my-follows'
|
||||||
|
import {useFollowDid} from 'lib/hooks/useFollowDid'
|
||||||
|
|
||||||
export const FollowButton = observer(function FollowButtonImpl({
|
export const FollowButton = observer(function FollowButtonImpl({
|
||||||
unfollowedType = 'inverted',
|
unfollowedType = 'inverted',
|
||||||
|
@ -19,44 +19,27 @@ export const FollowButton = observer(function FollowButtonImpl({
|
||||||
onToggleFollow?: (v: boolean) => void
|
onToggleFollow?: (v: boolean) => void
|
||||||
labelStyle?: StyleProp<TextStyle>
|
labelStyle?: StyleProp<TextStyle>
|
||||||
}) {
|
}) {
|
||||||
const store = useStores()
|
const {state, following, toggle} = useFollowDid({did})
|
||||||
const followState = store.me.follows.getFollowState(did)
|
|
||||||
|
|
||||||
if (followState === FollowState.Unknown) {
|
const onPress = React.useCallback(async () => {
|
||||||
return <View />
|
try {
|
||||||
}
|
const {following} = await toggle()
|
||||||
|
onToggleFollow?.(following)
|
||||||
const onToggleFollowInner = async () => {
|
} catch (e: any) {
|
||||||
const updatedFollowState = await store.me.follows.fetchFollowState(did)
|
Toast.show('An issue occurred, please try again.')
|
||||||
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.')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [toggle, onToggleFollow])
|
||||||
|
|
||||||
|
if (state === FollowState.Unknown) {
|
||||||
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type={
|
type={following ? followedType : unfollowedType}
|
||||||
followState === FollowState.Following ? followedType : unfollowedType
|
|
||||||
}
|
|
||||||
labelStyle={labelStyle}
|
labelStyle={labelStyle}
|
||||||
onPress={onToggleFollowInner}
|
onPress={onPress}
|
||||||
label={followState === FollowState.Following ? 'Unfollow' : 'Follow'}
|
label={following ? 'Unfollow' : 'Follow'}
|
||||||
withLoading={true}
|
withLoading={true}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,17 +38,20 @@ import {BACK_HITSLOP} from 'lib/constants'
|
||||||
import {isInvalidHandle} from 'lib/strings/handles'
|
import {isInvalidHandle} from 'lib/strings/handles'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
|
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
view: ProfileModel
|
view: ProfileModel
|
||||||
onRefreshAll: () => void
|
onRefreshAll: () => void
|
||||||
hideBackButton?: boolean
|
hideBackButton?: boolean
|
||||||
|
isProfilePreview?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileHeader = observer(function ProfileHeaderImpl({
|
export const ProfileHeader = observer(function ProfileHeaderImpl({
|
||||||
view,
|
view,
|
||||||
onRefreshAll,
|
onRefreshAll,
|
||||||
hideBackButton = false,
|
hideBackButton = false,
|
||||||
|
isProfilePreview,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
|
@ -95,6 +98,7 @@ export const ProfileHeader = observer(function ProfileHeaderImpl({
|
||||||
view={view}
|
view={view}
|
||||||
onRefreshAll={onRefreshAll}
|
onRefreshAll={onRefreshAll}
|
||||||
hideBackButton={hideBackButton}
|
hideBackButton={hideBackButton}
|
||||||
|
isProfilePreview={isProfilePreview}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -103,6 +107,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
view,
|
view,
|
||||||
onRefreshAll,
|
onRefreshAll,
|
||||||
hideBackButton = false,
|
hideBackButton = false,
|
||||||
|
isProfilePreview,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const palInverted = usePalette('inverted')
|
const palInverted = usePalette('inverted')
|
||||||
|
@ -111,6 +116,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const invalidHandle = isInvalidHandle(view.handle)
|
const invalidHandle = isInvalidHandle(view.handle)
|
||||||
const {isDesktop} = useWebMediaQueries()
|
const {isDesktop} = useWebMediaQueries()
|
||||||
|
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
const onPressBack = React.useCallback(() => {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
|
@ -133,6 +139,8 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
)
|
)
|
||||||
view?.toggleFollowing().then(
|
view?.toggleFollowing().then(
|
||||||
() => {
|
() => {
|
||||||
|
setShowSuggestedFollows(Boolean(view.viewer.following))
|
||||||
|
|
||||||
Toast.show(
|
Toast.show(
|
||||||
`${
|
`${
|
||||||
view.viewer.following ? 'Following' : 'No longer following'
|
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),
|
err => store.log.error('Failed to toggle follow', err),
|
||||||
)
|
)
|
||||||
}, [track, view, store.log])
|
}, [track, view, store.log, setShowSuggestedFollows])
|
||||||
|
|
||||||
const onPressEditProfile = React.useCallback(() => {
|
const onPressEditProfile = React.useCallback(() => {
|
||||||
track('ProfileHeader:EditProfileButtonClicked')
|
track('ProfileHeader:EditProfileButtonClicked')
|
||||||
|
@ -373,6 +381,39 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : !view.viewer.blockedBy ? (
|
) : !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) ===
|
{store.me.follows.getFollowState(view.did) ===
|
||||||
FollowState.Following ? (
|
FollowState.Following ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
@ -504,6 +545,15 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
)}
|
)}
|
||||||
<ProfileHeaderAlerts moderation={view.moderation} />
|
<ProfileHeaderAlerts moderation={view.moderation} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{!isProfilePreview && (
|
||||||
|
<ProfileHeaderSuggestedFollows
|
||||||
|
actorDid={view.did}
|
||||||
|
active={showSuggestedFollows}
|
||||||
|
requestDismiss={() => setShowSuggestedFollows(!showSuggestedFollows)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isDesktop && !hideBackButton && (
|
{!isDesktop && !hideBackButton && (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={onPressBack}
|
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