Add KnownFollowers component to standard profile header (#4420)
* Add KnownFollowers component to standard profile header * Prep for known followers screen * Add known followers screen * Tighten space * Add pressed state * Edit title * Vertically center * Don't show if no known followers * Bump sdk * Use actual followers.length to show * Updates to show logic, space * Prevent fresh data from applying to cached screens * Tighten space * Better label * Oxford comma * Fix count logic * Add bskyweb route * Useless ternary * Minor spacing tweak --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>zio/stable
parent
7011ac8f72
commit
bb0a6a4b6c
|
@ -207,6 +207,7 @@ func serve(cctx *cli.Context) error {
|
||||||
e.GET("/profile/:handleOrDID", server.WebProfile)
|
e.GET("/profile/:handleOrDID", server.WebProfile)
|
||||||
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
|
||||||
|
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.12.16",
|
"@atproto/api": "^0.12.18",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
|
|
|
@ -41,6 +41,7 @@ import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
||||||
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
||||||
import HashtagScreen from '#/screens/Hashtag'
|
import HashtagScreen from '#/screens/Hashtag'
|
||||||
import {ModerationScreen} from '#/screens/Moderation'
|
import {ModerationScreen} from '#/screens/Moderation'
|
||||||
|
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||||
|
@ -169,6 +170,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||||
title: title(msg`People followed by @${route.params.name}`),
|
title: title(msg`People followed by @${route.params.name}`),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ProfileKnownFollowers"
|
||||||
|
getComponent={() => ProfileKnownFollowersScreen}
|
||||||
|
options={({route}) => ({
|
||||||
|
title: title(msg`Followers of @${route.params.name} that you know`),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ProfileList"
|
name="ProfileList"
|
||||||
getComponent={() => ProfileListScreen}
|
getComponent={() => ProfileListScreen}
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||||
|
import {msg, plural, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Link} from '#/components/Link'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
const AVI_SIZE = 30
|
||||||
|
const AVI_BORDER = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared logic to determine if `KnownFollowers` should be shown.
|
||||||
|
*
|
||||||
|
* Checks the # of actual returned users instead of the `count` value, because
|
||||||
|
* `count` includes blocked users and `followers` does not.
|
||||||
|
*/
|
||||||
|
export function shouldShowKnownFollowers(
|
||||||
|
knownFollowers?: AppBskyActorDefs.KnownFollowers,
|
||||||
|
) {
|
||||||
|
return knownFollowers && knownFollowers.followers.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KnownFollowers({
|
||||||
|
profile,
|
||||||
|
moderationOpts,
|
||||||
|
}: {
|
||||||
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
|
}) {
|
||||||
|
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
||||||
|
new Map(),
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Results for `knownFollowers` are not sorted consistently, so when
|
||||||
|
* revalidating we can see a flash of this data updating. This cache prevents
|
||||||
|
* this happening for screens that remain in memory. When pushing a new
|
||||||
|
* screen, or once this one is popped, this cache is empty, so new data is
|
||||||
|
* displayed.
|
||||||
|
*/
|
||||||
|
if (profile.viewer?.knownFollowers && !cache.current.has(profile.did)) {
|
||||||
|
cache.current.set(profile.did, profile.viewer.knownFollowers)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedKnownFollowers = cache.current.get(profile.did)
|
||||||
|
|
||||||
|
if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) {
|
||||||
|
return (
|
||||||
|
<KnownFollowersInner
|
||||||
|
profile={profile}
|
||||||
|
cachedKnownFollowers={cachedKnownFollowers}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function KnownFollowersInner({
|
||||||
|
profile,
|
||||||
|
moderationOpts,
|
||||||
|
cachedKnownFollowers,
|
||||||
|
}: {
|
||||||
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
|
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const textStyle = [
|
||||||
|
a.flex_1,
|
||||||
|
a.text_sm,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]
|
||||||
|
|
||||||
|
// list of users, minus blocks
|
||||||
|
const returnedCount = cachedKnownFollowers.followers.length
|
||||||
|
// db count, includes blocks
|
||||||
|
const fullCount = cachedKnownFollowers.count
|
||||||
|
// knownFollowers can return up to 5 users, but will exclude blocks
|
||||||
|
// therefore, if we have less 5 users, use whichever count is lower
|
||||||
|
const count =
|
||||||
|
returnedCount < 5 ? Math.min(fullCount, returnedCount) : fullCount
|
||||||
|
|
||||||
|
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
|
||||||
|
const moderation = moderateProfile(f, moderationOpts)
|
||||||
|
return {
|
||||||
|
profile: {
|
||||||
|
...f,
|
||||||
|
displayName: sanitizeDisplayName(
|
||||||
|
f.displayName || f.handle,
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
moderation,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
label={_(
|
||||||
|
msg`Press to view followers of this account that you also follow`,
|
||||||
|
)}
|
||||||
|
to={makeProfileLink(profile, 'known-followers')}
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.flex_row,
|
||||||
|
a.gap_md,
|
||||||
|
a.align_center,
|
||||||
|
{marginLeft: -AVI_BORDER},
|
||||||
|
]}>
|
||||||
|
{({hovered, pressed}) => (
|
||||||
|
<>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
height: AVI_SIZE,
|
||||||
|
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||||
|
},
|
||||||
|
pressed && {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{slice.map(({profile: prof, moderation}, i) => (
|
||||||
|
<View
|
||||||
|
key={prof.did}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
borderWidth: AVI_BORDER,
|
||||||
|
borderColor: t.atoms.bg.backgroundColor,
|
||||||
|
width: AVI_SIZE + AVI_BORDER * 2,
|
||||||
|
height: AVI_SIZE + AVI_BORDER * 2,
|
||||||
|
left: i * a.gap_md.gap,
|
||||||
|
zIndex: AVI_BORDER - i,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<UserAvatar
|
||||||
|
size={AVI_SIZE}
|
||||||
|
avatar={prof.avatar}
|
||||||
|
moderation={moderation.ui('avatar')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
textStyle,
|
||||||
|
hovered && {
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
textDecorationColor: t.atoms.text_contrast_medium.color,
|
||||||
|
},
|
||||||
|
pressed && {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
numberOfLines={2}>
|
||||||
|
<Trans>Followed by</Trans>{' '}
|
||||||
|
{count > 2 ? (
|
||||||
|
<>
|
||||||
|
{slice.slice(0, 2).map(({profile: prof}, i) => (
|
||||||
|
<Text key={prof.did} style={textStyle}>
|
||||||
|
{prof.displayName}
|
||||||
|
{i === 0 && ', '}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
{', '}
|
||||||
|
{plural(count - 2, {
|
||||||
|
one: 'and # other',
|
||||||
|
other: 'and # others',
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
) : count === 2 ? (
|
||||||
|
slice.map(({profile: prof}, i) => (
|
||||||
|
<Text key={prof.did} style={textStyle}>
|
||||||
|
{prof.displayName} {i === 0 ? _(msg`and`) + ' ' : ''}
|
||||||
|
</Text>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text key={slice[0].profile.did} style={textStyle}>
|
||||||
|
{slice[0].profile.displayName}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ export type CommonNavigatorParams = {
|
||||||
Profile: {name: string; hideBackButton?: boolean}
|
Profile: {name: string; hideBackButton?: boolean}
|
||||||
ProfileFollowers: {name: string}
|
ProfileFollowers: {name: string}
|
||||||
ProfileFollows: {name: string}
|
ProfileFollows: {name: string}
|
||||||
|
ProfileKnownFollowers: {name: string}
|
||||||
ProfileList: {name: string; rkey: string}
|
ProfileList: {name: string; rkey: string}
|
||||||
PostThread: {name: string; rkey: string}
|
PostThread: {name: string; rkey: string}
|
||||||
PostLikedBy: {name: string; rkey: string}
|
PostLikedBy: {name: string; rkey: string}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const router = new Router({
|
||||||
Profile: ['/profile/:name', '/profile/:name/rss'],
|
Profile: ['/profile/:name', '/profile/:name/rss'],
|
||||||
ProfileFollowers: '/profile/:name/followers',
|
ProfileFollowers: '/profile/:name/followers',
|
||||||
ProfileFollows: '/profile/:name/follows',
|
ProfileFollows: '/profile/:name/follows',
|
||||||
|
ProfileKnownFollowers: '/profile/:name/known-followers',
|
||||||
ProfileList: '/profile/:name/lists/:rkey',
|
ProfileList: '/profile/:name/lists/:rkey',
|
||||||
PostThread: '/profile/:name/post/:rkey',
|
PostThread: '/profile/:name/post/:rkey',
|
||||||
PostLikedBy: '/profile/:name/post/:rkey/liked-by',
|
PostLikedBy: '/profile/:name/post/:rkey/liked-by',
|
||||||
|
|
|
@ -30,6 +30,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
|
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {
|
||||||
|
KnownFollowers,
|
||||||
|
shouldShowKnownFollowers,
|
||||||
|
} from '#/components/KnownFollowers'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {ProfileHeaderDisplayName} from './DisplayName'
|
import {ProfileHeaderDisplayName} from './DisplayName'
|
||||||
|
@ -268,6 +272,16 @@ let ProfileHeaderStandard = ({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
|
{!isMe &&
|
||||||
|
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm, a.pt_md]}>
|
||||||
|
<KnownFollowers
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -83,7 +83,7 @@ let ProfileHeaderShell = ({
|
||||||
|
|
||||||
{!isPlaceholderProfile && (
|
{!isPlaceholderProfile && (
|
||||||
<View
|
<View
|
||||||
style={[a.px_lg, a.pb_sm]}
|
style={[a.px_lg, a.py_xs]}
|
||||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||||
{isMe ? (
|
{isMe ? (
|
||||||
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
|
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {useProfileKnownFollowersQuery} from '#/state/queries/known-followers'
|
||||||
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
|
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||||
|
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||||
|
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
|
||||||
|
import {List} from '#/view/com/util/List'
|
||||||
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
|
import {
|
||||||
|
ListFooter,
|
||||||
|
ListHeaderDesktop,
|
||||||
|
ListMaybePlaceholder,
|
||||||
|
} from '#/components/Lists'
|
||||||
|
|
||||||
|
function renderItem({item}: {item: AppBskyActorDefs.ProfileViewBasic}) {
|
||||||
|
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
|
||||||
|
return item.did
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<
|
||||||
|
CommonNavigatorParams,
|
||||||
|
'ProfileKnownFollowers'
|
||||||
|
>
|
||||||
|
export const ProfileKnownFollowersScreen = ({route}: Props) => {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
|
const initialNumToRender = useInitialNumToRender()
|
||||||
|
|
||||||
|
const {name} = route.params
|
||||||
|
|
||||||
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
|
const {
|
||||||
|
data: resolvedDid,
|
||||||
|
isLoading: isDidLoading,
|
||||||
|
error: resolveError,
|
||||||
|
} = useResolveDidQuery(route.params.name)
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading: isFollowersLoading,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
} = useProfileKnownFollowersQuery(resolvedDid)
|
||||||
|
|
||||||
|
const onRefresh = React.useCallback(async () => {
|
||||||
|
setIsPTRing(true)
|
||||||
|
try {
|
||||||
|
await refetch()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to refresh followers', {message: err})
|
||||||
|
}
|
||||||
|
setIsPTRing(false)
|
||||||
|
}, [refetch, setIsPTRing])
|
||||||
|
|
||||||
|
const onEndReached = React.useCallback(async () => {
|
||||||
|
if (isFetchingNextPage || !hasNextPage || !!error) return
|
||||||
|
try {
|
||||||
|
await fetchNextPage()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to load more followers', {message: err})
|
||||||
|
}
|
||||||
|
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
|
||||||
|
|
||||||
|
const followers = React.useMemo(() => {
|
||||||
|
if (data?.pages) {
|
||||||
|
return data.pages.flatMap(page => page.followers)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const isError = Boolean(resolveError || error)
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
React.useCallback(() => {
|
||||||
|
setMinimalShellMode(false)
|
||||||
|
}, [setMinimalShellMode]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (followers.length < 1) {
|
||||||
|
return (
|
||||||
|
<ListMaybePlaceholder
|
||||||
|
isLoading={isDidLoading || isFollowersLoading}
|
||||||
|
isError={isError}
|
||||||
|
emptyType="results"
|
||||||
|
emptyMessage={_(msg`You don't follow any users who follow @${name}.`)}
|
||||||
|
errorMessage={cleanError(resolveError || error)}
|
||||||
|
onRetry={isError ? refetch : undefined}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{flex: 1}}>
|
||||||
|
<ViewHeader title={_(msg`Followers you know`)} />
|
||||||
|
<List
|
||||||
|
data={followers}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
refreshing={isPTRing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
onEndReachedThreshold={4}
|
||||||
|
ListHeaderComponent={
|
||||||
|
<ListHeaderDesktop title={_(msg`Followers you know`)} />
|
||||||
|
}
|
||||||
|
ListFooterComponent={
|
||||||
|
<ListFooter
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
error={cleanError(error)}
|
||||||
|
onRetry={fetchNextPage}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
// @ts-ignore our .web version only -prf
|
||||||
|
desktopFixedHeight
|
||||||
|
initialNumToRender={initialNumToRender}
|
||||||
|
windowSize={11}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {AppBskyGraphGetKnownFollowers} from '@atproto/api'
|
||||||
|
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
|
const RQKEY_ROOT = 'profile-known-followers'
|
||||||
|
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||||
|
|
||||||
|
export function useProfileKnownFollowersQuery(did: string | undefined) {
|
||||||
|
const agent = useAgent()
|
||||||
|
return useInfiniteQuery<
|
||||||
|
AppBskyGraphGetKnownFollowers.OutputSchema,
|
||||||
|
Error,
|
||||||
|
InfiniteData<AppBskyGraphGetKnownFollowers.OutputSchema>,
|
||||||
|
QueryKey,
|
||||||
|
RQPageParam
|
||||||
|
>({
|
||||||
|
queryKey: RQKEY(did || ''),
|
||||||
|
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||||
|
const res = await agent.app.bsky.graph.getKnownFollowers({
|
||||||
|
actor: did!,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
cursor: pageParam,
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
initialPageParam: undefined,
|
||||||
|
getNextPageParam: lastPage => lastPage.cursor,
|
||||||
|
enabled: !!did,
|
||||||
|
})
|
||||||
|
}
|
|
@ -34,10 +34,10 @@
|
||||||
jsonpointer "^5.0.0"
|
jsonpointer "^5.0.0"
|
||||||
leven "^3.1.0"
|
leven "^3.1.0"
|
||||||
|
|
||||||
"@atproto/api@^0.12.16":
|
"@atproto/api@^0.12.18":
|
||||||
version "0.12.16"
|
version "0.12.18"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.16.tgz#f5b5e06d75d379dafe79521d727ed8ad5516d3fc"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.18.tgz#490a6f22966a3b605c22154fe7befc78bf640821"
|
||||||
integrity sha512-v3lA/m17nkawDXiqgwXyaUSzJPeXJBMH8QKOoYxcDqN+8yG9LFlGe2ecGarXcbGQjYT0GJTAAW3Y/AaCOEwuLg==
|
integrity sha512-Ii3J/uzmyw1qgnfhnvAsmuXa8ObRSCHelsF8TmQrgMWeXCbfypeS/VESm++1Z9+xHK7bHPOwSek3RmWB0cqEbQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common-web" "^0.3.0"
|
"@atproto/common-web" "^0.3.0"
|
||||||
"@atproto/lexicon" "^0.4.0"
|
"@atproto/lexicon" "^0.4.0"
|
||||||
|
|
Loading…
Reference in New Issue