diff --git a/src/hooks/useProfileSections.ts b/src/hooks/useProfileSections.ts index 4ab7cd9..37faf57 100644 --- a/src/hooks/useProfileSections.ts +++ b/src/hooks/useProfileSections.ts @@ -5,19 +5,33 @@ import { listVouches, type VouchList } from '../lib/tangled/graph' import { getRepoByRepoDid, listRepos, type Repo, type RepoList } from '../lib/tangled/repo' import { listStrings, type StringList } from '../lib/tangled/string' import { useDeferredResource } from './useDeferredResource' +import { useCursorList } from './useCursorList' type StarredRepo = { handle: Handle; repo: Repo } +const PROFILE_LIST_LIMIT = 20 export function useProfileRepos(did: Did, isEnabled: boolean) { - return useDeferredResource(did, isEnabled, loadRepos) + return useCursorList( + isEnabled ? did : null, + (options) => listRepos(did, options), + PROFILE_LIST_LIMIT, + ) } export function useProfileStrings(did: Did, isEnabled: boolean) { - return useDeferredResource(did, isEnabled, listStrings) + return useCursorList( + isEnabled ? did : null, + (options) => listStrings(did, options), + PROFILE_LIST_LIMIT, + ) } export function useProfileVouches(did: Did, isEnabled: boolean) { - return useDeferredResource(did, isEnabled, loadVouches) + return useCursorList( + isEnabled ? did : null, + (options) => listVouches(did, options), + PROFILE_LIST_LIMIT, + ) } export function usePinnedRepos(repoDids: readonly Did[], isEnabled: boolean) { @@ -25,7 +39,11 @@ export function usePinnedRepos(repoDids: readonly Did[], isEnabled: boolean) { } export function useStarredRepos(did: Did, isEnabled: boolean) { - return useDeferredResource(did, isEnabled, loadStarredRepos) + return useCursorList( + isEnabled ? did : null, + (options) => loadStarredRepos(did, options), + PROFILE_LIST_LIMIT, + ) } async function loadPinnedRepos(repoDids: string): Promise { @@ -40,20 +58,13 @@ async function loadPinnedRepos(repoDids: string): Promise { ) } -function loadRepos(did: string): Promise { - return listRepos(did as Did) -} - -function loadVouches(did: string): Promise { - return listVouches(did as Did) -} - -async function loadStarredRepos(did: string): Promise { - const stars = await listStarsBy(did as Did) +async function loadStarredRepos(did: Did, options: { cursor?: string; limit: number }) { + const stars = await listStarsBy(did, options) const results = await Promise.allSettled(getRepoDidsFromStars(stars).map(loadStarredRepo)) - return results.flatMap((result) => + const items = results.flatMap((result) => result.status === 'fulfilled' && result.value !== null ? [result.value] : [], ) + return { items, cursor: stars.cursor } } async function loadStarredRepo(repoDid: Did): Promise { diff --git a/src/lib/tangled/repo/index.ts b/src/lib/tangled/repo/index.ts index 4fa1ff9..1b5c5d9 100644 --- a/src/lib/tangled/repo/index.ts +++ b/src/lib/tangled/repo/index.ts @@ -14,6 +14,7 @@ export type { ListIssuesOptions } from './listIssues' export { listPulls } from './listPulls' export type { ListPullsOptions } from './listPulls' export { listRepos } from './listRepos' +export type { ListReposOptions } from './listRepos' export { getRecordRkey } from './recordIdentity' export { getRepoName, getRepoRkey } from './repoIdentity' export type { diff --git a/src/lib/tangled/repo/listRepos.ts b/src/lib/tangled/repo/listRepos.ts index ddd0e89..a072acc 100644 --- a/src/lib/tangled/repo/listRepos.ts +++ b/src/lib/tangled/repo/listRepos.ts @@ -8,10 +8,16 @@ import { getRepoRkey } from './repoIdentity' import type { Repo, RepoList } from './types' import { validateRepo } from './validators' -export async function listRepos(did: Did): Promise { +export type ListReposOptions = { + cursor?: string + limit?: number + order?: 'asc' | 'desc' +} + +export async function listRepos(did: Did, options: ListReposOptions = {}): Promise { const response = await ok( rpc.get('sh.tangled.repo.listRepos', { - params: { subject: did }, + params: { subject: did, cursor: options.cursor, limit: options.limit, order: options.order }, }), ) diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 6a63c62..01a3f74 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -7,6 +7,7 @@ import { ProfileSection } from '../components/profile/ProfileSection' import { ProfileTabs } from '../components/profile/ProfileTabs' import { RepoListItem } from '../components/repo/RepoListItem' import { ErrorPage } from '../components/shared/ErrorPage' +import { LoadMoreButton } from '../components/shared/LoadMoreButton' import { StringListItem } from '../components/string/StringListItem' import { VouchList } from '../components/vouch/VouchList' import { useProfilePage } from '../hooks/useProfilePage' @@ -66,22 +67,34 @@ function ProfileContent({ activeSection, pageData }: ProfileContentProps) { (profile.value.pinnedRepositories ?? []).map((did) => did as `did:${string}:${string}`), isOverview, ) - const { data: repos, error: reposError } = useProfileRepos( - identity.did, - activeSection === 'repos', - ) - const { data: strings, error: stringsError } = useProfileStrings( - identity.did, - activeSection === 'strings', - ) - const { data: starredRepos, error: starredReposError } = useStarredRepos( - identity.did, - activeSection === 'stars', - ) - const { data: vouches, error: vouchesError } = useProfileVouches( - identity.did, - isOverview || activeSection === 'vouches', - ) + const { + data: repos, + error: reposError, + hasMore: hasMoreRepos, + isLoadingMore: isLoadingMoreRepos, + loadMore: loadMoreRepos, + } = useProfileRepos(identity.did, activeSection === 'repos') + const { + data: strings, + error: stringsError, + hasMore: hasMoreStrings, + isLoadingMore: isLoadingMoreStrings, + loadMore: loadMoreStrings, + } = useProfileStrings(identity.did, activeSection === 'strings') + const { + data: starredRepos, + error: starredReposError, + hasMore: hasMoreStars, + isLoadingMore: isLoadingMoreStars, + loadMore: loadMoreStars, + } = useStarredRepos(identity.did, activeSection === 'stars') + const { + data: vouches, + error: vouchesError, + hasMore: hasMoreVouches, + isLoadingMore: isLoadingMoreVouches, + loadMore: loadMoreVouches, + } = useProfileVouches(identity.did, isOverview || activeSection === 'vouches') const recentVouches = vouches?.items.slice(0, MAX_RECENT_VOUCHES) ?? [] return ( @@ -135,6 +148,13 @@ function ProfileContent({ activeSection, pageData }: ProfileContentProps) { ))} )} + {hasMoreRepos && ( + void loadMoreRepos()} + /> + )} )} @@ -154,6 +174,13 @@ function ProfileContent({ activeSection, pageData }: ProfileContentProps) { ))} )} + {hasMoreStrings && ( + void loadMoreStrings()} + /> + )} )} @@ -161,15 +188,22 @@ function ProfileContent({ activeSection, pageData }: ProfileContentProps) { {starredReposError && } {starredRepos === null && starredReposError === null &&

Loading stars...

} - {starredRepos?.length === 0 &&

No stars found.

} + {starredRepos?.items.length === 0 &&

No stars found.

} - {starredRepos && starredRepos.length > 0 && ( + {starredRepos && starredRepos.items.length > 0 && (
- {starredRepos.map(({ repo, handle }) => ( + {starredRepos.items.map(({ repo, handle }) => ( ))}
)} + {hasMoreStars && ( + void loadMoreStars()} + /> + )}
)} @@ -179,6 +213,13 @@ function ProfileContent({ activeSection, pageData }: ProfileContentProps) { {vouches === null && vouchesError === null &&

Loading vouches...

} {vouches?.items.length === 0 &&

No vouches yet.

} {vouches && vouches.items.length > 0 && } + {hasMoreVouches && ( + void loadMoreVouches()} + /> + )} )}