/** * Renders a profile's "follow" tab, which shows the users the user follows at * `/@name/follow` and the playlists at `/@name/follow/playlists`. * * @module */ import { useCallback } from "react"; import { useNavigate } from "react-router"; import { Trans } from "@lingui/react/macro"; import { API_URL } from "../../config/api.ts"; import type { Wire } from "../../../shared/api.ts"; import type { Playlist } from "../../../shared/playlist.ts"; import type { PublicUser } from "../../../shared/user.ts"; import { type FollowView, userPath } from "../../../shared/paths.ts"; import { canLoadMore, pageUrl, readPage } from "../../utils/feed/pagedList.ts"; import { deserializePlaylist, deserializePublicUser, } from "../../utils/shared/deserialize.ts"; import { usePagedList } from "../../hooks/feed/usePagedList.ts"; import { useInfiniteScroll } from "../../hooks/feed/useInfiniteScroll.ts"; import { usePlaylistListSync } from "../../hooks/playlist/usePlaylistListSync.ts"; import { ListState } from "../shared/ListState.tsx"; import { TabBar } from "../shared/TabBar.tsx"; import { FeedFooter } from "../feed/FeedFooter.tsx"; import { UserCard } from "../social/UserCard.tsx"; import { PlaylistCardList } from "../playlist/PlaylistCardList.tsx"; import { ProfileSection } from "./ProfileSection.tsx"; /** Passed to {@link ProfileFollowTab} by `UserPublicProfile`. */ export interface ProfileFollowTabProps { user: PublicUser; /** The view the path names, after `/follow`. */ view: FollowView; } /** * Returns page after page of what `userId` follows, from * `GET /api/users/:userId/follow/`, and the sentinel that loads the * next. */ function useFollowList( userId: string, view: FollowView, deserialize: (wire: Wire) => T, ) { const url = `${API_URL}/api/users/${userId}/follow/${view}`; const paged = usePagedList( url, useCallback( (page: number, signal?: AbortSignal) => fetch(pageUrl(url, page), { signal }) .then((res) => readPage(res, deserialize)), [url, deserialize], ), ); const loaded = paged.state.status === "loaded" ? paged.state.data : null; const sentinelRef = useInfiniteScroll(paged.loadMore, canLoadMore(loaded)); return { ...paged, sentinelRef }; } /** Renders the users `userId` follows, the latest followed first. */ function FollowedUsers({ userId }: { userId: string }) { const { state, sentinelRef } = useFollowList( userId, "users", deserializePublicUser, ); return ( data.items.length === 0} empty={

Not following anyone yet.

} > {({ data }) => ( <>
    {data.items.map((u) => )}
)}
); } /** * Renders the public playlists `userId` follows, which the socket keeps * current but never adds to. */ function FollowedPlaylists({ userId }: { userId: string }) { const { state, setItems, sentinelRef } = useFollowList( userId, "playlists", deserializePlaylist, ); usePlaylistListSync(setItems, { noNewEntries: true }); return ( data.items.length === 0} empty={

No followed playlists yet.

} > {({ data }) => ( <> )}
); } /** * Renders the two views under a row of tabs that switch between their paths, * each view loading its list when first shown. */ export function ProfileFollowTab( { user, view }: ProfileFollowTabProps, ): React.JSX.Element { const navigate = useNavigate(); return ( Followed}> Users }, { key: "playlists" as FollowView, label: Playlists }, ]} activeTab={view} onChange={(v) => navigate(userPath(user.username, "follow", v), { replace: true })} innerClassName="followed-sub-nav" /> {view === "users" ? : } ); }