Sort follows and following behind gate (#11237)
This commit is contained in:
@@ -1736,16 +1736,6 @@
|
|||||||
"count": 9
|
"count": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/view/com/profile/ProfileFollowers.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/view/com/profile/ProfileFollows.tsx": {
|
|
||||||
"typescript/no-misused-promises": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/view/com/util/EmptyState.tsx": {
|
"src/view/com/util/EmptyState.tsx": {
|
||||||
"typescript/no-explicit-any": {
|
"typescript/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
+1
-1
@@ -96,7 +96,7 @@
|
|||||||
"prettier": "prettier --check ."
|
"prettier": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "0.20.31",
|
"@atproto/api": "0.20.32",
|
||||||
"@atproto/common-web": "0.5.6",
|
"@atproto/common-web": "0.5.6",
|
||||||
"@atproto/syntax": "0.7.2",
|
"@atproto/syntax": "0.7.2",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
|
|||||||
Generated
+5
-5
@@ -242,8 +242,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/api':
|
'@atproto/api':
|
||||||
specifier: 0.20.31
|
specifier: 0.20.32
|
||||||
version: 0.20.31
|
version: 0.20.32
|
||||||
'@atproto/common-web':
|
'@atproto/common-web':
|
||||||
specifier: 0.5.6
|
specifier: 0.5.6
|
||||||
version: 0.5.6
|
version: 0.5.6
|
||||||
@@ -871,8 +871,8 @@ packages:
|
|||||||
graphql:
|
graphql:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@atproto/api@0.20.31':
|
'@atproto/api@0.20.32':
|
||||||
resolution: {integrity: sha512-TovCQLQv5ti1jqh8UH6jJ0EFuWRjGdUtFyFR5xYC/IkIulwHDuyrVaXdCv7VLWiHftp92DevtZnFRm+BZsZZdw==}
|
resolution: {integrity: sha512-P6Lh6+PR+x0/HHdEbmwZ5e2uvrzViEeobHWHoEc7KTaKJ/bQQu39z2wfBrG4dOTEC/OcOhFYhAhepo0VGIOeAQ==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.6':
|
'@atproto/common-web@0.5.6':
|
||||||
@@ -9389,7 +9389,7 @@ snapshots:
|
|||||||
|
|
||||||
'@0no-co/graphql.web@1.2.0': {}
|
'@0no-co/graphql.web@1.2.0': {}
|
||||||
|
|
||||||
'@atproto/api@0.20.31':
|
'@atproto/api@0.20.32':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/common-web': 0.5.6
|
'@atproto/common-web': 0.5.6
|
||||||
'@atproto/lexicon': 0.7.7
|
'@atproto/lexicon': 0.7.7
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export enum Features {
|
|||||||
PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable',
|
PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable',
|
||||||
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
||||||
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
||||||
|
FollowSortEnable = 'follow_sort:enable',
|
||||||
|
|
||||||
AATest = 'aa-test',
|
AATest = 'aa-test',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -475,25 +475,30 @@ export type Events = {
|
|||||||
'profile:followers:view': {
|
'profile:followers:view': {
|
||||||
contextProfileDid: string
|
contextProfileDid: string
|
||||||
isOwnProfile: boolean
|
isOwnProfile: boolean
|
||||||
|
sort?: 'latest' | 'top'
|
||||||
}
|
}
|
||||||
'profile:followers:paginate': {
|
'profile:followers:paginate': {
|
||||||
contextProfileDid: string
|
contextProfileDid: string
|
||||||
itemCount: number
|
itemCount: number
|
||||||
page: number
|
page: number
|
||||||
|
sort?: 'latest' | 'top'
|
||||||
}
|
}
|
||||||
'profile:following:view': {
|
'profile:following:view': {
|
||||||
contextProfileDid: string
|
contextProfileDid: string
|
||||||
isOwnProfile: boolean
|
isOwnProfile: boolean
|
||||||
|
sort?: 'latest' | 'top'
|
||||||
}
|
}
|
||||||
'profile:following:paginate': {
|
'profile:following:paginate': {
|
||||||
contextProfileDid: string
|
contextProfileDid: string
|
||||||
itemCount: number
|
itemCount: number
|
||||||
page: number
|
page: number
|
||||||
|
sort?: 'latest' | 'top'
|
||||||
}
|
}
|
||||||
'profileCard:seen': {
|
'profileCard:seen': {
|
||||||
contextProfileDid?: string
|
contextProfileDid?: string
|
||||||
profileDid: string
|
profileDid: string
|
||||||
position?: number
|
position?: number
|
||||||
|
sort?: 'latest' | 'top'
|
||||||
}
|
}
|
||||||
'profile:mute': {}
|
'profile:mute': {}
|
||||||
'profile:unmute': {}
|
'profile:unmute': {}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
@@ -29,7 +27,7 @@ const TOTAL_AVATARS = 10
|
|||||||
|
|
||||||
export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const {gtPhone} = useBreakpoints()
|
const {gtPhone} = useBreakpoints()
|
||||||
const {rightNavVisible} = useLayoutBreakpoints()
|
const {rightNavVisible} = useLayoutBreakpoints()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -79,7 +77,7 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
|||||||
size="tiny"
|
size="tiny"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
shape="round"
|
shape="round"
|
||||||
label={_(msg`Dismiss getting started guide`)}
|
label={l`Dismiss getting started guide`}
|
||||||
onPress={endProgressGuide}
|
onPress={endProgressGuide}
|
||||||
style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}>
|
style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}>
|
||||||
<ButtonIcon icon={Times} size="xs" />
|
<ButtonIcon icon={Times} size="xs" />
|
||||||
@@ -107,14 +105,14 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
|||||||
<ProgressGuideTask
|
<ProgressGuideTask
|
||||||
current={guide.numLikes + 1}
|
current={guide.numLikes + 1}
|
||||||
total={10 + 1}
|
total={10 + 1}
|
||||||
title={_(msg`Like 10 posts`)}
|
title={l`Like 10 posts`}
|
||||||
subtitle={_(msg`Teach our algorithm what you like`)}
|
subtitle={l`Teach our algorithm what you like`}
|
||||||
/>
|
/>
|
||||||
<ProgressGuideTask
|
<ProgressGuideTask
|
||||||
current={guide.numFollows + 1}
|
current={guide.numFollows + 1}
|
||||||
total={7 + 1}
|
total={7 + 1}
|
||||||
title={_(msg`Follow 7 accounts`)}
|
title={l`Follow 7 accounts`}
|
||||||
subtitle={_(msg`Bluesky is better with friends!`)}
|
subtitle={l`Bluesky is better with friends!`}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,15 +10,33 @@ import {
|
|||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
|
const DEFAULT_SORT = 'latest'
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
const RQKEY_ROOT = 'profile-followers'
|
const RQKEY_ROOT = 'profile-followers'
|
||||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
export const RQKEY = (did: string, sort: 'latest' | 'top' = DEFAULT_SORT) => [
|
||||||
|
RQKEY_ROOT,
|
||||||
|
did,
|
||||||
|
sort,
|
||||||
|
]
|
||||||
|
|
||||||
export function useProfileFollowersQuery(did: string | undefined) {
|
export function useProfileFollowersQuery(
|
||||||
|
did?: string,
|
||||||
|
{
|
||||||
|
sort,
|
||||||
|
}: {
|
||||||
|
sort?: 'latest' | 'top'
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable)
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
|
const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined
|
||||||
|
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyGraphGetFollowers.OutputSchema,
|
AppBskyGraphGetFollowers.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
@@ -26,12 +44,13 @@ export function useProfileFollowersQuery(did: string | undefined) {
|
|||||||
QueryKey,
|
QueryKey,
|
||||||
RQPageParam
|
RQPageParam
|
||||||
>({
|
>({
|
||||||
queryKey: RQKEY(did || ''),
|
queryKey: RQKEY(did || '', sortParam),
|
||||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||||
const res = await agent.app.bsky.graph.getFollowers({
|
const res = await agent.app.bsky.graph.getFollowers({
|
||||||
actor: did || '',
|
actor: did || '',
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
cursor: pageParam,
|
cursor: pageParam,
|
||||||
|
sort: sortParam,
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,25 +8,36 @@ import {
|
|||||||
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
|
||||||
|
const DEFAULT_SORT = 'latest'
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
// TODO refactor invalidate on mutate?
|
// TODO refactor invalidate on mutate?
|
||||||
const RQKEY_ROOT = 'profile-follows'
|
const RQKEY_ROOT = 'profile-follows'
|
||||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
export const RQKEY = (did: string, sort: 'latest' | 'top' = DEFAULT_SORT) => [
|
||||||
|
RQKEY_ROOT,
|
||||||
|
did,
|
||||||
|
sort,
|
||||||
|
]
|
||||||
|
|
||||||
export function useProfileFollowsQuery(
|
export function useProfileFollowsQuery(
|
||||||
did: string | undefined,
|
did: string | undefined,
|
||||||
{
|
{
|
||||||
limit,
|
limit,
|
||||||
|
sort,
|
||||||
}: {
|
}: {
|
||||||
limit?: number
|
limit?: number
|
||||||
} = {
|
sort?: 'latest' | 'top'
|
||||||
limit: PAGE_SIZE,
|
} = {},
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable)
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
|
const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined
|
||||||
|
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyGraphGetFollows.OutputSchema,
|
AppBskyGraphGetFollows.OutputSchema,
|
||||||
Error,
|
Error,
|
||||||
@@ -35,12 +46,13 @@ export function useProfileFollowsQuery(
|
|||||||
RQPageParam
|
RQPageParam
|
||||||
>({
|
>({
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryKey: RQKEY(did || ''),
|
queryKey: RQKEY(did || '', sortParam),
|
||||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||||
const res = await agent.app.bsky.graph.getFollows({
|
const res = await agent.app.bsky.graph.getFollows({
|
||||||
actor: did || '',
|
actor: did || '',
|
||||||
limit: limit || PAGE_SIZE,
|
limit: limit || PAGE_SIZE,
|
||||||
cursor: pageParam,
|
cursor: pageParam,
|
||||||
|
sort: sortParam,
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||||
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
@@ -48,18 +47,22 @@ function keyExtractor(item: ActorDefs.ProfileView) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileFollowers({name}: {name: string}) {
|
export function ProfileFollowers({name}: {name: string}) {
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const initialNumToRender = useInitialNumToRender()
|
const initialNumToRender = useInitialNumToRender()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable)
|
||||||
|
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
const {
|
const {
|
||||||
data: resolvedDid,
|
data: resolvedDid,
|
||||||
isLoading: isDidLoading,
|
isLoading: isDidLoading,
|
||||||
error: resolveError,
|
error: resolveError,
|
||||||
} = useResolveDidQuery(name)
|
} = useResolveDidQuery(name)
|
||||||
|
const isMe = resolvedDid === currentAccount?.did
|
||||||
|
const sort = isMe ? 'latest' : 'top'
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading: isFollowersLoading,
|
isLoading: isFollowersLoading,
|
||||||
@@ -68,10 +71,11 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useProfileFollowersQuery(resolvedDid)
|
} = useProfileFollowersQuery(resolvedDid, {
|
||||||
|
sort,
|
||||||
|
})
|
||||||
|
|
||||||
const isError = !!resolveError || !!error
|
const isError = !!resolveError || !!error
|
||||||
const isMe = resolvedDid === currentAccount?.did
|
|
||||||
|
|
||||||
const followers = useMemo(() => {
|
const followers = useMemo(() => {
|
||||||
if (data?.pages) {
|
if (data?.pages) {
|
||||||
@@ -101,10 +105,18 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
contextProfileDid: resolvedDid,
|
contextProfileDid: resolvedDid,
|
||||||
itemCount: followers.length,
|
itemCount: followers.length,
|
||||||
page: currentPageCount,
|
page: currentPageCount,
|
||||||
|
sort: isSortEnabled ? sort : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
paginationTrackingRef.current.page = currentPageCount
|
paginationTrackingRef.current.page = currentPageCount
|
||||||
}, [ax, data?.pages?.length, resolvedDid, followers.length])
|
}, [
|
||||||
|
ax,
|
||||||
|
data?.pages?.length,
|
||||||
|
resolvedDid,
|
||||||
|
followers.length,
|
||||||
|
sort,
|
||||||
|
isSortEnabled,
|
||||||
|
])
|
||||||
|
|
||||||
const onRefresh = useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
@@ -137,9 +149,10 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
ax.metric('profile:followers:view', {
|
ax.metric('profile:followers:view', {
|
||||||
contextProfileDid: resolvedDid,
|
contextProfileDid: resolvedDid,
|
||||||
isOwnProfile: isMe,
|
isOwnProfile: isMe,
|
||||||
|
sort: isSortEnabled ? sort : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [ax, resolvedDid, isMe])
|
}, [ax, resolvedDid, isMe, sort, isSortEnabled])
|
||||||
|
|
||||||
// track seen items
|
// track seen items
|
||||||
const seenItemsRef = useRef<Set<string>>(new Set())
|
const seenItemsRef = useRef<Set<string>>(new Set())
|
||||||
@@ -160,9 +173,10 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
profileDid: item.did,
|
profileDid: item.did,
|
||||||
position,
|
position,
|
||||||
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
|
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
|
||||||
|
sort: isSortEnabled ? sort : undefined,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[ax, followers, resolvedDid],
|
[ax, followers, resolvedDid, sort, isSortEnabled],
|
||||||
)
|
)
|
||||||
|
|
||||||
const [followersPromoDismissed, setFollowersPromoDismissed] =
|
const [followersPromoDismissed, setFollowersPromoDismissed] =
|
||||||
@@ -199,8 +213,8 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
emptyType="results"
|
emptyType="results"
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
isMe
|
isMe
|
||||||
? _(msg`No followers yet`)
|
? l`No followers yet`
|
||||||
: _(msg`This user doesn't have any followers.`)
|
: l`This user doesn't have any followers.`
|
||||||
}
|
}
|
||||||
errorMessage={cleanError(resolveError || error)}
|
errorMessage={cleanError(resolveError || error)}
|
||||||
onRetry={isError ? refetch : undefined}
|
onRetry={isError ? refetch : undefined}
|
||||||
@@ -208,8 +222,8 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
useEmptyState={true}
|
useEmptyState={true}
|
||||||
emptyStateIcon={PeopleRemoveIcon}
|
emptyStateIcon={PeopleRemoveIcon}
|
||||||
emptyStateButton={{
|
emptyStateButton={{
|
||||||
label: _(msg`Go back`),
|
label: l`Go back`,
|
||||||
text: _(msg`Go back`),
|
text: l`Go back`,
|
||||||
color: 'secondary',
|
color: 'secondary',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
onPress: () => navigation.goBack(),
|
onPress: () => navigation.goBack(),
|
||||||
@@ -221,8 +235,8 @@ export function ProfileFollowers({name}: {name: string}) {
|
|||||||
renderItem={renderItemWithContext}
|
renderItem={renderItemWithContext}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={() => void onRefresh()}
|
||||||
onEndReached={onEndReached}
|
onEndReached={() => void onEndReached()}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
onItemSeen={onItemSeen}
|
onItemSeen={onItemSeen}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||||
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
@@ -44,12 +43,14 @@ function keyExtractor(item: ActorDefs.ProfileView) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileFollows({name}: {name: string}) {
|
export function ProfileFollows({name}: {name: string}) {
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const initialNumToRender = useInitialNumToRender()
|
const initialNumToRender = useInitialNumToRender()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable)
|
||||||
|
|
||||||
const onPressFindAccounts = useCallback(() => {
|
const onPressFindAccounts = useCallback(() => {
|
||||||
if (IS_WEB) {
|
if (IS_WEB) {
|
||||||
navigation.navigate('Search', {})
|
navigation.navigate('Search', {})
|
||||||
@@ -65,6 +66,8 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
isLoading: isDidLoading,
|
isLoading: isDidLoading,
|
||||||
error: resolveError,
|
error: resolveError,
|
||||||
} = useResolveDidQuery(name)
|
} = useResolveDidQuery(name)
|
||||||
|
const isMe = resolvedDid === currentAccount?.did
|
||||||
|
const sort = isMe ? 'latest' : 'top'
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading: isFollowsLoading,
|
isLoading: isFollowsLoading,
|
||||||
@@ -73,10 +76,11 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useProfileFollowsQuery(resolvedDid)
|
} = useProfileFollowsQuery(resolvedDid, {
|
||||||
|
sort,
|
||||||
|
})
|
||||||
|
|
||||||
const isError = !!resolveError || !!error
|
const isError = !!resolveError || !!error
|
||||||
const isMe = resolvedDid === currentAccount?.did
|
|
||||||
|
|
||||||
const follows = useMemo(() => {
|
const follows = useMemo(() => {
|
||||||
if (data?.pages) {
|
if (data?.pages) {
|
||||||
@@ -106,10 +110,18 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
contextProfileDid: resolvedDid,
|
contextProfileDid: resolvedDid,
|
||||||
itemCount: follows.length,
|
itemCount: follows.length,
|
||||||
page: currentPageCount,
|
page: currentPageCount,
|
||||||
|
sort: isSortEnabled ? sort : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
paginationTrackingRef.current.page = currentPageCount
|
paginationTrackingRef.current.page = currentPageCount
|
||||||
}, [ax, data?.pages?.length, resolvedDid, follows.length])
|
}, [
|
||||||
|
ax,
|
||||||
|
data?.pages?.length,
|
||||||
|
resolvedDid,
|
||||||
|
follows.length,
|
||||||
|
sort,
|
||||||
|
isSortEnabled,
|
||||||
|
])
|
||||||
|
|
||||||
const onRefresh = useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
@@ -142,9 +154,10 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
ax.metric('profile:following:view', {
|
ax.metric('profile:following:view', {
|
||||||
contextProfileDid: resolvedDid,
|
contextProfileDid: resolvedDid,
|
||||||
isOwnProfile: isMe,
|
isOwnProfile: isMe,
|
||||||
|
sort: isSortEnabled ? sort : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [ax, resolvedDid, isMe])
|
}, [ax, resolvedDid, isMe, sort, isSortEnabled])
|
||||||
|
|
||||||
// track seen items
|
// track seen items
|
||||||
const seenItemsRef = useRef<Set<string>>(new Set())
|
const seenItemsRef = useRef<Set<string>>(new Set())
|
||||||
@@ -165,9 +178,10 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
profileDid: item.did,
|
profileDid: item.did,
|
||||||
position,
|
position,
|
||||||
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
|
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
|
||||||
|
sort: isSortEnabled ? sort : undefined,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[ax, follows, resolvedDid],
|
[ax, follows, resolvedDid, sort, isSortEnabled],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (follows.length < 1) {
|
if (follows.length < 1) {
|
||||||
@@ -178,8 +192,8 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
emptyType="results"
|
emptyType="results"
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
isMe
|
isMe
|
||||||
? _(msg`You are not following anyone yet`)
|
? l`You are not following anyone yet`
|
||||||
: _(msg`This user isn't following anyone.`)
|
: l`This user isn't following anyone.`
|
||||||
}
|
}
|
||||||
errorMessage={cleanError(resolveError || error)}
|
errorMessage={cleanError(resolveError || error)}
|
||||||
onRetry={isError ? refetch : undefined}
|
onRetry={isError ? refetch : undefined}
|
||||||
@@ -187,8 +201,8 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
useEmptyState={true}
|
useEmptyState={true}
|
||||||
emptyStateIcon={PeopleRemoveIcon}
|
emptyStateIcon={PeopleRemoveIcon}
|
||||||
emptyStateButton={{
|
emptyStateButton={{
|
||||||
label: _(msg`See suggested accounts`),
|
label: l`See suggested accounts`,
|
||||||
text: _(msg`See suggested accounts`),
|
text: l`See suggested accounts`,
|
||||||
onPress: onPressFindAccounts,
|
onPress: onPressFindAccounts,
|
||||||
size: 'tiny',
|
size: 'tiny',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
@@ -203,8 +217,8 @@ export function ProfileFollows({name}: {name: string}) {
|
|||||||
renderItem={renderItemWithContext}
|
renderItem={renderItemWithContext}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
refreshing={isPTRing}
|
refreshing={isPTRing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={() => void onRefresh()}
|
||||||
onEndReached={onEndReached}
|
onEndReached={() => void onEndReached()}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
onItemSeen={onItemSeen}
|
onItemSeen={onItemSeen}
|
||||||
ListHeaderComponent={<FindContactsBannerNUX />}
|
ListHeaderComponent={<FindContactsBannerNUX />}
|
||||||
|
|||||||
Reference in New Issue
Block a user