Port Profile Followers/Follows to RQ (#1893)
* Port user followers to RQ * Port user follows to RQ * Start porting FollowButton to RQ * Fix RQ key * Check pending * Fix shadow and pending states * Rm unused * Remove last usage of useFollowProfilezio/stable
parent
d1cb74febe
commit
e699df21c6
|
@ -1,55 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {FollowState} from 'state/models/cache/my-follows'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
|
|
||||||
export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) {
|
|
||||||
const store = useStores()
|
|
||||||
const state = store.me.follows.getFollowState(profile.did)
|
|
||||||
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
following: state === FollowState.Following,
|
|
||||||
toggle: React.useCallback(async () => {
|
|
||||||
if (state === FollowState.Following) {
|
|
||||||
try {
|
|
||||||
await store.agent.deleteFollow(
|
|
||||||
store.me.follows.getFollowUri(profile.did),
|
|
||||||
)
|
|
||||||
store.me.follows.removeFollow(profile.did)
|
|
||||||
return {
|
|
||||||
state: FollowState.NotFollowing,
|
|
||||||
following: false,
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
logger.error('Failed to delete follow', {error: e})
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
} else if (state === FollowState.NotFollowing) {
|
|
||||||
try {
|
|
||||||
const res = await store.agent.follow(profile.did)
|
|
||||||
store.me.follows.addFollow(profile.did, {
|
|
||||||
followRecordUri: res.uri,
|
|
||||||
did: profile.did,
|
|
||||||
handle: profile.handle,
|
|
||||||
displayName: profile.displayName,
|
|
||||||
avatar: profile.avatar,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
state: FollowState.Following,
|
|
||||||
following: true,
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
logger.error('Failed to create follow', {error: e})
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
state: FollowState.Unknown,
|
|
||||||
following: false,
|
|
||||||
}
|
|
||||||
}, [store, profile, state]),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
|
||||||
import {
|
|
||||||
AppBskyGraphGetFollowers as GetFollowers,
|
|
||||||
AppBskyActorDefs as ActorDefs,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {RootStoreModel} from '../root-store'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
|
||||||
|
|
||||||
export type FollowerItem = ActorDefs.ProfileViewBasic
|
|
||||||
|
|
||||||
export class UserFollowersModel {
|
|
||||||
// state
|
|
||||||
isLoading = false
|
|
||||||
isRefreshing = false
|
|
||||||
hasLoaded = false
|
|
||||||
error = ''
|
|
||||||
params: GetFollowers.QueryParams
|
|
||||||
hasMore = true
|
|
||||||
loadMoreCursor?: string
|
|
||||||
|
|
||||||
// data
|
|
||||||
subject: ActorDefs.ProfileViewBasic = {
|
|
||||||
did: '',
|
|
||||||
handle: '',
|
|
||||||
}
|
|
||||||
followers: FollowerItem[] = []
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public rootStore: RootStoreModel,
|
|
||||||
params: GetFollowers.QueryParams,
|
|
||||||
) {
|
|
||||||
makeAutoObservable(
|
|
||||||
this,
|
|
||||||
{
|
|
||||||
rootStore: false,
|
|
||||||
params: false,
|
|
||||||
},
|
|
||||||
{autoBind: true},
|
|
||||||
)
|
|
||||||
this.params = params
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasContent() {
|
|
||||||
return this.subject.did !== ''
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasError() {
|
|
||||||
return this.error !== ''
|
|
||||||
}
|
|
||||||
|
|
||||||
get isEmpty() {
|
|
||||||
return this.hasLoaded && !this.hasContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// public api
|
|
||||||
// =
|
|
||||||
|
|
||||||
async refresh() {
|
|
||||||
return this.loadMore(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
|
||||||
if (!replace && !this.hasMore) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._xLoading(replace)
|
|
||||||
try {
|
|
||||||
const params = Object.assign({}, this.params, {
|
|
||||||
limit: PAGE_SIZE,
|
|
||||||
cursor: replace ? undefined : this.loadMoreCursor,
|
|
||||||
})
|
|
||||||
const res = await this.rootStore.agent.getFollowers(params)
|
|
||||||
if (replace) {
|
|
||||||
this._replaceAll(res)
|
|
||||||
} else {
|
|
||||||
this._appendAll(res)
|
|
||||||
}
|
|
||||||
this._xIdle()
|
|
||||||
} catch (e: any) {
|
|
||||||
this._xIdle(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// state transitions
|
|
||||||
// =
|
|
||||||
|
|
||||||
_xLoading(isRefreshing = false) {
|
|
||||||
this.isLoading = true
|
|
||||||
this.isRefreshing = isRefreshing
|
|
||||||
this.error = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
_xIdle(err?: any) {
|
|
||||||
this.isLoading = false
|
|
||||||
this.isRefreshing = false
|
|
||||||
this.hasLoaded = true
|
|
||||||
this.error = cleanError(err)
|
|
||||||
if (err) {
|
|
||||||
logger.error('Failed to fetch user followers', {error: err})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper functions
|
|
||||||
// =
|
|
||||||
|
|
||||||
_replaceAll(res: GetFollowers.Response) {
|
|
||||||
this.followers = []
|
|
||||||
this._appendAll(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
_appendAll(res: GetFollowers.Response) {
|
|
||||||
this.loadMoreCursor = res.data.cursor
|
|
||||||
this.hasMore = !!this.loadMoreCursor
|
|
||||||
this.followers = this.followers.concat(res.data.followers)
|
|
||||||
this.rootStore.me.follows.hydrateMany(res.data.followers)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
|
||||||
import {
|
|
||||||
AppBskyGraphGetFollows as GetFollows,
|
|
||||||
AppBskyActorDefs as ActorDefs,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {RootStoreModel} from '../root-store'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
|
||||||
|
|
||||||
export type FollowItem = ActorDefs.ProfileViewBasic
|
|
||||||
|
|
||||||
export class UserFollowsModel {
|
|
||||||
// state
|
|
||||||
isLoading = false
|
|
||||||
isRefreshing = false
|
|
||||||
hasLoaded = false
|
|
||||||
error = ''
|
|
||||||
params: GetFollows.QueryParams
|
|
||||||
hasMore = true
|
|
||||||
loadMoreCursor?: string
|
|
||||||
|
|
||||||
// data
|
|
||||||
subject: ActorDefs.ProfileViewBasic = {
|
|
||||||
did: '',
|
|
||||||
handle: '',
|
|
||||||
}
|
|
||||||
follows: FollowItem[] = []
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public rootStore: RootStoreModel,
|
|
||||||
params: GetFollows.QueryParams,
|
|
||||||
) {
|
|
||||||
makeAutoObservable(
|
|
||||||
this,
|
|
||||||
{
|
|
||||||
rootStore: false,
|
|
||||||
params: false,
|
|
||||||
},
|
|
||||||
{autoBind: true},
|
|
||||||
)
|
|
||||||
this.params = params
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasContent() {
|
|
||||||
return this.subject.did !== ''
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasError() {
|
|
||||||
return this.error !== ''
|
|
||||||
}
|
|
||||||
|
|
||||||
get isEmpty() {
|
|
||||||
return this.hasLoaded && !this.hasContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// public api
|
|
||||||
// =
|
|
||||||
|
|
||||||
async refresh() {
|
|
||||||
return this.loadMore(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
|
||||||
if (!replace && !this.hasMore) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._xLoading(replace)
|
|
||||||
try {
|
|
||||||
const params = Object.assign({}, this.params, {
|
|
||||||
limit: PAGE_SIZE,
|
|
||||||
cursor: replace ? undefined : this.loadMoreCursor,
|
|
||||||
})
|
|
||||||
const res = await this.rootStore.agent.getFollows(params)
|
|
||||||
if (replace) {
|
|
||||||
this._replaceAll(res)
|
|
||||||
} else {
|
|
||||||
this._appendAll(res)
|
|
||||||
}
|
|
||||||
this._xIdle()
|
|
||||||
} catch (e: any) {
|
|
||||||
this._xIdle(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// state transitions
|
|
||||||
// =
|
|
||||||
|
|
||||||
_xLoading(isRefreshing = false) {
|
|
||||||
this.isLoading = true
|
|
||||||
this.isRefreshing = isRefreshing
|
|
||||||
this.error = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
_xIdle(err?: any) {
|
|
||||||
this.isLoading = false
|
|
||||||
this.isRefreshing = false
|
|
||||||
this.hasLoaded = true
|
|
||||||
this.error = cleanError(err)
|
|
||||||
if (err) {
|
|
||||||
logger.error('Failed to fetch user follows', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper functions
|
|
||||||
// =
|
|
||||||
|
|
||||||
_replaceAll(res: GetFollows.Response) {
|
|
||||||
this.follows = []
|
|
||||||
this._appendAll(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
_appendAll(res: GetFollows.Response) {
|
|
||||||
this.loadMoreCursor = res.data.cursor
|
|
||||||
this.hasMore = !!this.loadMoreCursor
|
|
||||||
this.follows = this.follows.concat(res.data.follows)
|
|
||||||
this.rootStore.me.follows.hydrateMany(res.data.follows)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import {AppBskyGraphGetFollowers} from '@atproto/api'
|
||||||
|
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
|
||||||
|
import {useSession} from '../session'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 30
|
||||||
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
|
export const RQKEY = (did: string) => ['profile-followers', did]
|
||||||
|
|
||||||
|
export function useProfileFollowersQuery(did: string | undefined) {
|
||||||
|
const {agent} = useSession()
|
||||||
|
return useInfiniteQuery<
|
||||||
|
AppBskyGraphGetFollowers.OutputSchema,
|
||||||
|
Error,
|
||||||
|
InfiniteData<AppBskyGraphGetFollowers.OutputSchema>,
|
||||||
|
QueryKey,
|
||||||
|
RQPageParam
|
||||||
|
>({
|
||||||
|
queryKey: RQKEY(did || ''),
|
||||||
|
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||||
|
const res = await agent.app.bsky.graph.getFollowers({
|
||||||
|
actor: did || '',
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
cursor: pageParam,
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
initialPageParam: undefined,
|
||||||
|
getNextPageParam: lastPage => lastPage.cursor,
|
||||||
|
enabled: !!did,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import {AppBskyGraphGetFollows} from '@atproto/api'
|
||||||
|
import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
|
||||||
|
import {useSession} from '../session'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 30
|
||||||
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
|
export const RQKEY = (did: string) => ['profile-follows', did]
|
||||||
|
|
||||||
|
export function useProfileFollowsQuery(did: string | undefined) {
|
||||||
|
const {agent} = useSession()
|
||||||
|
return useInfiniteQuery<
|
||||||
|
AppBskyGraphGetFollows.OutputSchema,
|
||||||
|
Error,
|
||||||
|
InfiniteData<AppBskyGraphGetFollows.OutputSchema>,
|
||||||
|
QueryKey,
|
||||||
|
RQPageParam
|
||||||
|
>({
|
||||||
|
queryKey: RQKEY(did || ''),
|
||||||
|
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||||
|
const res = await agent.app.bsky.graph.getFollows({
|
||||||
|
actor: did || '',
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
cursor: pageParam,
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
initialPageParam: undefined,
|
||||||
|
getNextPageParam: lastPage => lastPage.cursor,
|
||||||
|
enabled: !!did,
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,7 +1,12 @@
|
||||||
import {AppBskyActorGetSuggestions, moderateProfile} from '@atproto/api'
|
import {
|
||||||
|
AppBskyActorGetSuggestions,
|
||||||
|
AppBskyGraphGetSuggestedFollowsByActor,
|
||||||
|
moderateProfile,
|
||||||
|
} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useMutation,
|
useMutation,
|
||||||
|
useQuery,
|
||||||
InfiniteData,
|
InfiniteData,
|
||||||
QueryKey,
|
QueryKey,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
@ -9,7 +14,11 @@ import {
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useModerationOpts} from '#/state/queries/preferences'
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
|
|
||||||
export const suggestedFollowsQueryKey = ['suggested-follows']
|
const suggestedFollowsQueryKey = ['suggested-follows']
|
||||||
|
const suggestedFollowsByActorQuery = (did: string) => [
|
||||||
|
'suggested-follows-by-actor',
|
||||||
|
did,
|
||||||
|
]
|
||||||
|
|
||||||
export function useSuggestedFollowsQuery() {
|
export function useSuggestedFollowsQuery() {
|
||||||
const {agent, currentAccount} = useSession()
|
const {agent, currentAccount} = useSession()
|
||||||
|
@ -60,6 +69,21 @@ export function useSuggestedFollowsQuery() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
|
||||||
|
const {agent} = useSession()
|
||||||
|
|
||||||
|
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
|
||||||
|
queryKey: suggestedFollowsByActorQuery(did),
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
|
||||||
|
actor: did,
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Delete and replace usages with the one above.
|
||||||
export function useGetSuggestedFollowersByActor() {
|
export function useGetSuggestedFollowersByActor() {
|
||||||
const {agent} = useSession()
|
const {agent} = useSession()
|
||||||
|
|
||||||
|
|
|
@ -1,47 +1,76 @@
|
||||||
import React from 'react'
|
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 {AppBskyActorDefs} from '@atproto/api'
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import {Button, ButtonType} from '../util/forms/Button'
|
import {Button, ButtonType} from '../util/forms/Button'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {FollowState} from 'state/models/cache/my-follows'
|
import {
|
||||||
import {useFollowProfile} from 'lib/hooks/useFollowProfile'
|
useProfileFollowMutation,
|
||||||
|
useProfileUnfollowMutation,
|
||||||
|
} from '#/state/queries/profile'
|
||||||
|
import {Shadow} from '#/state/cache/types'
|
||||||
|
|
||||||
export const FollowButton = observer(function FollowButtonImpl({
|
export function FollowButton({
|
||||||
unfollowedType = 'inverted',
|
unfollowedType = 'inverted',
|
||||||
followedType = 'default',
|
followedType = 'default',
|
||||||
profile,
|
profile,
|
||||||
onToggleFollow,
|
|
||||||
labelStyle,
|
labelStyle,
|
||||||
}: {
|
}: {
|
||||||
unfollowedType?: ButtonType
|
unfollowedType?: ButtonType
|
||||||
followedType?: ButtonType
|
followedType?: ButtonType
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||||
onToggleFollow?: (v: boolean) => void
|
|
||||||
labelStyle?: StyleProp<TextStyle>
|
labelStyle?: StyleProp<TextStyle>
|
||||||
}) {
|
}) {
|
||||||
const {state, following, toggle} = useFollowProfile(profile)
|
const followMutation = useProfileFollowMutation()
|
||||||
|
const unfollowMutation = useProfileUnfollowMutation()
|
||||||
|
|
||||||
const onPress = React.useCallback(async () => {
|
const onPressFollow = async () => {
|
||||||
try {
|
if (profile.viewer?.following) {
|
||||||
const {following} = await toggle()
|
return
|
||||||
onToggleFollow?.(following)
|
}
|
||||||
} catch (e: any) {
|
try {
|
||||||
Toast.show('An issue occurred, please try again.')
|
await followMutation.mutateAsync({did: profile.did})
|
||||||
|
} catch (e: any) {
|
||||||
|
Toast.show(`An issue occurred, please try again.`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [toggle, onToggleFollow])
|
|
||||||
|
|
||||||
if (state === FollowState.Unknown) {
|
const onPressUnfollow = async () => {
|
||||||
|
if (!profile.viewer?.following) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await unfollowMutation.mutateAsync({
|
||||||
|
did: profile.did,
|
||||||
|
followUri: profile.viewer?.following,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
Toast.show(`An issue occurred, please try again.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile.viewer) {
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile.viewer.following) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type={following ? followedType : unfollowedType}
|
type={followedType}
|
||||||
labelStyle={labelStyle}
|
labelStyle={labelStyle}
|
||||||
onPress={onPress}
|
onPress={onPressUnfollow}
|
||||||
label={following ? 'Unfollow' : 'Follow'}
|
label="Unfollow"
|
||||||
withLoading={true}
|
withLoading={true}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
} else {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={unfollowedType}
|
||||||
|
labelStyle={labelStyle}
|
||||||
|
onPress={onPressFollow}
|
||||||
|
label="Follow"
|
||||||
|
withLoading={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,8 +21,10 @@ import {
|
||||||
getProfileModerationCauses,
|
getProfileModerationCauses,
|
||||||
getModerationCauseKey,
|
getModerationCauseKey,
|
||||||
} from 'lib/moderation'
|
} from 'lib/moderation'
|
||||||
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {useModerationOpts} from '#/state/queries/preferences'
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
export function ProfileCard({
|
export function ProfileCard({
|
||||||
testID,
|
testID,
|
||||||
|
@ -40,7 +42,9 @@ export function ProfileCard({
|
||||||
noBg?: boolean
|
noBg?: boolean
|
||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||||
renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => React.ReactNode
|
renderButton?: (
|
||||||
|
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
|
||||||
|
) => React.ReactNode
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -188,20 +192,21 @@ const FollowersList = observer(function FollowersListImpl({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ProfileCardWithFollowBtn = observer(
|
export function ProfileCardWithFollowBtn({
|
||||||
function ProfileCardWithFollowBtnImpl({
|
|
||||||
profile,
|
profile,
|
||||||
noBg,
|
noBg,
|
||||||
noBorder,
|
noBorder,
|
||||||
followers,
|
followers,
|
||||||
}: {
|
dataUpdatedAt,
|
||||||
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
noBg?: boolean
|
noBg?: boolean
|
||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||||
}) {
|
dataUpdatedAt: number
|
||||||
const store = useStores()
|
}) {
|
||||||
const isMe = store.me.did === profile.did
|
const {currentAccount} = useSession()
|
||||||
|
const isMe = profile.did === currentAccount?.did
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
|
@ -210,12 +215,14 @@ export const ProfileCardWithFollowBtn = observer(
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
followers={followers}
|
followers={followers}
|
||||||
renderButton={
|
renderButton={
|
||||||
isMe ? undefined : () => <FollowButton profile={profile} />
|
isMe
|
||||||
|
? undefined
|
||||||
|
: profileShadow => <FollowButton profile={profileShadow} />
|
||||||
}
|
}
|
||||||
|
dataUpdatedAt={dataUpdatedAt}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
|
|
|
@ -1,49 +1,73 @@
|
||||||
import React, {useEffect} from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
||||||
import {
|
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||||
UserFollowersModel,
|
|
||||||
FollowerItem,
|
|
||||||
} from 'state/models/lists/user-followers'
|
|
||||||
import {CenteredView, FlatList} from '../util/Views'
|
import {CenteredView, FlatList} from '../util/Views'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
|
||||||
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
|
||||||
export const ProfileFollowers = observer(function ProfileFollowers({
|
export function ProfileFollowers({name}: {name: string}) {
|
||||||
name,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
const view = React.useMemo(
|
const {
|
||||||
() => new UserFollowersModel(store, {actor: name}),
|
data: resolvedDid,
|
||||||
[store, name],
|
error: resolveError,
|
||||||
)
|
isFetching: isFetchingDid,
|
||||||
|
} = useResolveDidQuery(name)
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
dataUpdatedAt,
|
||||||
|
isFetching,
|
||||||
|
isFetched,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
} = useProfileFollowersQuery(resolvedDid?.did)
|
||||||
|
|
||||||
useEffect(() => {
|
const followers = React.useMemo(() => {
|
||||||
view
|
if (data?.pages) {
|
||||||
.loadMore()
|
return data.pages.flatMap(page => page.followers)
|
||||||
.catch(err =>
|
}
|
||||||
logger.error('Failed to fetch user followers', {error: err}),
|
}, [data])
|
||||||
)
|
|
||||||
}, [view])
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
setIsPTRing(true)
|
||||||
const onRefresh = () => {
|
try {
|
||||||
view.refresh()
|
await refetch()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to refresh followers', {error: err})
|
||||||
|
}
|
||||||
|
setIsPTRing(false)
|
||||||
|
}, [refetch, setIsPTRing])
|
||||||
|
|
||||||
|
const onEndReached = async () => {
|
||||||
|
if (isFetching || !hasNextPage || isError) return
|
||||||
|
try {
|
||||||
|
await fetchNextPage()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to load more followers', {error: err})
|
||||||
}
|
}
|
||||||
const onEndReached = () => {
|
|
||||||
view.loadMore().catch(err =>
|
|
||||||
logger.error('Failed to load more followers', {
|
|
||||||
error: err,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!view.hasLoaded) {
|
const renderItem = React.useCallback(
|
||||||
|
({item}: {item: ActorDefs.ProfileViewBasic}) => (
|
||||||
|
<ProfileCardWithFollowBtn
|
||||||
|
key={item.did}
|
||||||
|
profile={item}
|
||||||
|
dataUpdatedAt={dataUpdatedAt}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[dataUpdatedAt],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isFetchingDid || !isFetched) {
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
|
@ -53,26 +77,26 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
||||||
|
|
||||||
// error
|
// error
|
||||||
// =
|
// =
|
||||||
if (view.hasError) {
|
if (resolveError || isError) {
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
|
<ErrorMessage
|
||||||
|
message={cleanError(resolveError || error)}
|
||||||
|
onPressTryAgain={onRefresh}
|
||||||
|
/>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: FollowerItem}) => (
|
|
||||||
<ProfileCardWithFollowBtn key={item.did} profile={item} />
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={view.followers}
|
data={followers}
|
||||||
keyExtractor={item => item.did}
|
keyExtractor={item => item.did}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={view.isRefreshing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
tintColor={pal.colors.text}
|
||||||
titleColor={pal.colors.text}
|
titleColor={pal.colors.text}
|
||||||
|
@ -85,15 +109,14 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
ListFooterComponent={() => (
|
ListFooterComponent={() => (
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
{view.isLoading && <ActivityIndicator />}
|
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
extraData={view.isLoading}
|
|
||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
footer: {
|
footer: {
|
||||||
|
|
|
@ -1,42 +1,73 @@
|
||||||
import React, {useEffect} from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
||||||
|
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||||
import {CenteredView, FlatList} from '../util/Views'
|
import {CenteredView, FlatList} from '../util/Views'
|
||||||
import {UserFollowsModel, FollowItem} from 'state/models/lists/user-follows'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||||
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
|
||||||
export const ProfileFollows = observer(function ProfileFollows({
|
export function ProfileFollows({name}: {name: string}) {
|
||||||
name,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
const view = React.useMemo(
|
const {
|
||||||
() => new UserFollowsModel(store, {actor: name}),
|
data: resolvedDid,
|
||||||
[store, name],
|
error: resolveError,
|
||||||
|
isFetching: isFetchingDid,
|
||||||
|
} = useResolveDidQuery(name)
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
dataUpdatedAt,
|
||||||
|
isFetching,
|
||||||
|
isFetched,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
} = useProfileFollowsQuery(resolvedDid?.did)
|
||||||
|
|
||||||
|
const follows = React.useMemo(() => {
|
||||||
|
if (data?.pages) {
|
||||||
|
return data.pages.flatMap(page => page.follows)
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
setIsPTRing(true)
|
||||||
|
try {
|
||||||
|
await refetch()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to refresh follows', {error: err})
|
||||||
|
}
|
||||||
|
setIsPTRing(false)
|
||||||
|
}, [refetch, setIsPTRing])
|
||||||
|
|
||||||
|
const onEndReached = async () => {
|
||||||
|
if (isFetching || !hasNextPage || isError) return
|
||||||
|
try {
|
||||||
|
await fetchNextPage()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to load more follows', {error: err})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderItem = React.useCallback(
|
||||||
|
({item}: {item: ActorDefs.ProfileViewBasic}) => (
|
||||||
|
<ProfileCardWithFollowBtn
|
||||||
|
key={item.did}
|
||||||
|
profile={item}
|
||||||
|
dataUpdatedAt={dataUpdatedAt}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[dataUpdatedAt],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
if (isFetchingDid || !isFetched) {
|
||||||
view
|
|
||||||
.loadMore()
|
|
||||||
.catch(err => logger.error('Failed to fetch user follows', err))
|
|
||||||
}, [view])
|
|
||||||
|
|
||||||
const onRefresh = () => {
|
|
||||||
view.refresh()
|
|
||||||
}
|
|
||||||
const onEndReached = () => {
|
|
||||||
view
|
|
||||||
.loadMore()
|
|
||||||
.catch(err => logger.error('Failed to load more follows', err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!view.hasLoaded) {
|
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
|
@ -46,26 +77,26 @@ export const ProfileFollows = observer(function ProfileFollows({
|
||||||
|
|
||||||
// error
|
// error
|
||||||
// =
|
// =
|
||||||
if (view.hasError) {
|
if (resolveError || isError) {
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
|
<ErrorMessage
|
||||||
|
message={cleanError(resolveError || error)}
|
||||||
|
onPressTryAgain={onRefresh}
|
||||||
|
/>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: FollowItem}) => (
|
|
||||||
<ProfileCardWithFollowBtn key={item.did} profile={item} />
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={view.follows}
|
data={follows}
|
||||||
keyExtractor={item => item.did}
|
keyExtractor={item => item.did}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={view.isRefreshing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
tintColor={pal.colors.text}
|
tintColor={pal.colors.text}
|
||||||
titleColor={pal.colors.text}
|
titleColor={pal.colors.text}
|
||||||
|
@ -78,15 +109,14 @@ export const ProfileFollows = observer(function ProfileFollows({
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
ListFooterComponent={() => (
|
ListFooterComponent={() => (
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
{view.isLoading && <ActivityIndicator />}
|
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
extraData={view.isLoading}
|
|
||||||
// @ts-ignore our .web version only -prf
|
// @ts-ignore our .web version only -prf
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
footer: {
|
footer: {
|
||||||
|
|
|
@ -6,20 +6,16 @@ import Animated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
Easing,
|
Easing,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useQuery} from '@tanstack/react-query'
|
|
||||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {useFollowProfile} from 'lib/hooks/useFollowProfile'
|
|
||||||
import {Button} from 'view/com/util/forms/Button'
|
import {Button} from 'view/com/util/forms/Button'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
|
@ -27,6 +23,13 @@ import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {Link} from 'view/com/util/Link'
|
import {Link} from 'view/com/util/Link'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {useModerationOpts} from '#/state/queries/preferences'
|
||||||
|
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||||
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
|
import {
|
||||||
|
useProfileFollowMutation,
|
||||||
|
useProfileUnfollowMutation,
|
||||||
|
} from '#/state/queries/profile'
|
||||||
|
|
||||||
const OUTER_PADDING = 10
|
const OUTER_PADDING = 10
|
||||||
const INNER_PADDING = 14
|
const INNER_PADDING = 14
|
||||||
|
@ -43,7 +46,6 @@ export function ProfileHeaderSuggestedFollows({
|
||||||
}) {
|
}) {
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
|
||||||
const animatedHeight = useSharedValue(0)
|
const animatedHeight = useSharedValue(0)
|
||||||
const animatedStyles = useAnimatedStyle(() => ({
|
const animatedStyles = useAnimatedStyle(() => ({
|
||||||
opacity: animatedHeight.value / TOTAL_HEIGHT,
|
opacity: animatedHeight.value / TOTAL_HEIGHT,
|
||||||
|
@ -66,31 +68,8 @@ export function ProfileHeaderSuggestedFollows({
|
||||||
}
|
}
|
||||||
}, [active, animatedHeight, track])
|
}, [active, animatedHeight, track])
|
||||||
|
|
||||||
const {isLoading, data: suggestedFollows} = useQuery({
|
const {isLoading, data, dataUpdatedAt} = useSuggestedFollowsByActorQuery({
|
||||||
enabled: active,
|
did: actorDid,
|
||||||
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.hydrateMany(suggestions)
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
} catch (e) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -149,9 +128,13 @@ export function ProfileHeaderSuggestedFollows({
|
||||||
<SuggestedFollowSkeleton />
|
<SuggestedFollowSkeleton />
|
||||||
<SuggestedFollowSkeleton />
|
<SuggestedFollowSkeleton />
|
||||||
</>
|
</>
|
||||||
) : suggestedFollows ? (
|
) : data ? (
|
||||||
suggestedFollows.map(profile => (
|
data.suggestions.map(profile => (
|
||||||
<SuggestedFollow key={profile.did} profile={profile} />
|
<SuggestedFollow
|
||||||
|
key={profile.did}
|
||||||
|
profile={profile}
|
||||||
|
dataUpdatedAt={dataUpdatedAt}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<View />
|
<View />
|
||||||
|
@ -214,29 +197,51 @@ function SuggestedFollowSkeleton() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SuggestedFollow = observer(function SuggestedFollowImpl({
|
function SuggestedFollow({
|
||||||
profile,
|
profile: profileUnshadowed,
|
||||||
|
dataUpdatedAt,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileView
|
profile: AppBskyActorDefs.ProfileView
|
||||||
|
dataUpdatedAt: number
|
||||||
}) {
|
}) {
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const moderationOpts = useModerationOpts()
|
||||||
const {following, toggle} = useFollowProfile(profile)
|
const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt)
|
||||||
const moderation = moderateProfile(profile, store.preferences.moderationOpts)
|
const followMutation = useProfileFollowMutation()
|
||||||
|
const unfollowMutation = useProfileUnfollowMutation()
|
||||||
|
|
||||||
const onPress = React.useCallback(async () => {
|
const onPressFollow = React.useCallback(async () => {
|
||||||
try {
|
if (profile.viewer?.following) {
|
||||||
const {following: isFollowing} = await toggle()
|
return
|
||||||
|
|
||||||
if (isFollowing) {
|
|
||||||
track('ProfileHeader:SuggestedFollowFollowed')
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
track('ProfileHeader:SuggestedFollowFollowed')
|
||||||
|
await followMutation.mutateAsync({did: profile.did})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Toast.show('An issue occurred, please try again.')
|
Toast.show('An issue occurred, please try again.')
|
||||||
}
|
}
|
||||||
}, [toggle, track])
|
}, [followMutation, profile, track])
|
||||||
|
|
||||||
|
const onPressUnfollow = React.useCallback(async () => {
|
||||||
|
if (!profile.viewer?.following) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await unfollowMutation.mutateAsync({
|
||||||
|
did: profile.did,
|
||||||
|
followUri: profile.viewer?.following,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
Toast.show('An issue occurred, please try again.')
|
||||||
|
}
|
||||||
|
}, [unfollowMutation, profile])
|
||||||
|
|
||||||
|
if (!moderationOpts) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
|
const following = profile.viewer?.following
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={makeProfileLink(profile)}
|
href={makeProfileLink(profile)}
|
||||||
|
@ -278,13 +283,13 @@ const SuggestedFollow = observer(function SuggestedFollowImpl({
|
||||||
label={following ? 'Unfollow' : 'Follow'}
|
label={following ? 'Unfollow' : 'Follow'}
|
||||||
type="inverted"
|
type="inverted"
|
||||||
labelStyle={{textAlign: 'center'}}
|
labelStyle={{textAlign: 'center'}}
|
||||||
onPress={onPress}
|
onPress={following ? onPressUnfollow : onPressFollow}
|
||||||
withLoading
|
withLoading
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
suggestedFollowCardOuter: {
|
suggestedFollowCardOuter: {
|
||||||
|
|
Loading…
Reference in New Issue