diff --git a/apps/web/src/api/profile.ts b/apps/web/src/api/profile.ts index 7ca41a10..2ab213a0 100644 --- a/apps/web/src/api/profile.ts +++ b/apps/web/src/api/profile.ts @@ -1,4 +1,5 @@ import { client } from "."; +import { Neighbour } from "../types/neighbour"; import { Scrobble } from "../types/scrobble"; export const getProfileByDid = async (did: string) => { @@ -28,3 +29,13 @@ export const getRecentTracksByDid = async ( ); return response.data.scrobbles || []; }; + +export const getActorNeighbours = async (did: string) => { + const response = await client.get<{ neighbours: Neighbour[] }>( + "/xrpc/app.rocksky.actor.getActorNeighbours", + { + params: { did }, + }, + ); + return response.data; +}; diff --git a/apps/web/src/hooks/useProfile.tsx b/apps/web/src/hooks/useProfile.tsx index 385e4c89..563114f3 100644 --- a/apps/web/src/hooks/useProfile.tsx +++ b/apps/web/src/hooks/useProfile.tsx @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useSetAtom } from "jotai"; import { useEffect, useState } from "react"; import { + getActorNeighbours, getProfileByDid, getProfileStatsByDid, getRecentTracksByDid, @@ -30,6 +31,13 @@ export const useRecentTracksByDidQuery = (did: string, offset = 0, size = 10) => enabled: !!did, }); +export const useActorNeighboursQuery = (did: string) => + useQuery({ + queryKey: ["profile", "neighbours", did], + queryFn: () => getActorNeighbours(did), + enabled: !!did, + }); + function useProfile(token?: string | null) { const setProfile = useSetAtom(profileAtom); const [data, setData] = useState(null); diff --git a/apps/web/src/pages/profile/Profile.tsx b/apps/web/src/pages/profile/Profile.tsx index a54ee8c8..56717d66 100644 --- a/apps/web/src/pages/profile/Profile.tsx +++ b/apps/web/src/pages/profile/Profile.tsx @@ -29,6 +29,7 @@ import { import Follows from "./follows"; import Followers from "./followers"; import { activeTabAtom } from "../../atoms/tab"; +import Circles from "./circles"; const Group = styled.div` display: flex; @@ -91,10 +92,11 @@ function Profile(props: ProfileProps) { useEffect(() => { if (!props.activeKey) { + setActiveKey("0"); return; } setActiveKey(_.get(props, "activeKey", "0").split("/")[0]); - }, [props.activeKey]); + }, [props.activeKey, setActiveKey, props]); useEffect(() => { if (!data || isLoading) { @@ -113,7 +115,7 @@ function Profile(props: ProfileProps) { } return newSet; }); - }, [data, isLoading]); + }, [data, isLoading, currentDid, setFollows, profile.data?.did]); useEffect(() => { if (tab === undefined) { @@ -121,7 +123,7 @@ function Profile(props: ProfileProps) { } setActiveKey(1); - }, [tab]); + }, [tab, setActiveKey]); // biome-ignore lint/correctness/useExhaustiveDependencies: want to run only on profile.data changes useEffect(() => { @@ -339,6 +341,19 @@ function Profile(props: ProfileProps) { > + + + { + if (!localStorage.getItem("token")) { + setIsSignInOpen(true); + return; + } + setFollows((prev) => new Set(prev).add(followerDid)); + followAccount(followerDid); + }; + + const onUnfollow = (followerDid: string) => { + if (!localStorage.getItem("token")) { + setIsSignInOpen(true); + return; + } + setFollows((prev) => { + const newSet = new Set(prev); + newSet.delete(followerDid); + return newSet; + }); + unfollowAccount(followerDid); + }; + + return ( + <> + + Circles + +

+ People on Rocksky with similar music taste to @{profile.data?.handle} +

+ {!isLoading && ( +
+ {data?.neighbours.map((neighbour: Neighbour) => ( +
+
+ setActiveKey(0)} + > + + +
+
+ setActiveKey(0)} + > + + {neighbour.displayName} + + + + + @{neighbour.handle} + + +
+

+ They both listen to{" "} + {neighbour.topSharedArtistsDetails.map((artist, index) => ( +

+ + + {artist.name} + + + {index !== + neighbour.topSharedArtistsDetails.length - 1 && + (index === + neighbour.topSharedArtistsDetails.length - 2 + ? " and " + : ", ")} +
+ ))} +

+
+
+ {(neighbour.did !== localStorage.getItem("did") || + !localStorage.getItem("did")) && ( +
+ {!follows.has(neighbour.did) && ( + + )} + {follows.has(neighbour.did) && ( + + )} +
+ )} +
+ ))} +
+ )} + setIsSignInOpen(false)} + follow + /> + + ); +} + +export default Circles; diff --git a/apps/web/src/pages/profile/circles/index.tsx b/apps/web/src/pages/profile/circles/index.tsx new file mode 100644 index 00000000..c0770ec3 --- /dev/null +++ b/apps/web/src/pages/profile/circles/index.tsx @@ -0,0 +1,3 @@ +import Circles from "./Circles"; + +export default Circles; diff --git a/apps/web/src/pages/profile/followers/Followers.tsx b/apps/web/src/pages/profile/followers/Followers.tsx index ef44131e..63d3bc67 100644 --- a/apps/web/src/pages/profile/followers/Followers.tsx +++ b/apps/web/src/pages/profile/followers/Followers.tsx @@ -24,7 +24,7 @@ function Followers() { const { did } = useParams({ strict: false }); const profile = useProfileByDidQuery(did!); const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = - useFollowersInfiniteQuery(profile.data?.did!, 20); + useFollowersInfiniteQuery(profile.data?.did, 20); const { mutate: followAccount } = useFollowAccountMutation(); const { mutate: unfollowAccount } = useUnfollowAccountMutation(); diff --git a/apps/web/src/types/neighbour.ts b/apps/web/src/types/neighbour.ts new file mode 100644 index 00000000..423645cb --- /dev/null +++ b/apps/web/src/types/neighbour.ts @@ -0,0 +1,17 @@ +export type Neighbour = { + id: string; + avatar: string; + did: string; + displayName: string; + handle: string; + sharedArtistsCount: number; + similarityScore: number; + topSharedArtistNames: string[]; + topSharedArtistsDetails: { + id: string; + name: string; + picture: string; + uri: string; + }[]; + userId: string; +};