A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz

Add feed fetching with pagination and feed atoms

Implement getFeed API, pass limit/cursor to backend, add feed atoms, and
update hooks and components to use the selected feed generator.

+68 -15
+2
apps/api/src/xrpc/app/rocksky/feed/getFeed.ts
··· 54 54 }>(`${feedUrl}/xrpc/app.rocksky.feed.getFeedSkeleton`, { 55 55 params: { 56 56 feed: feed.uri, 57 + limit: params.limit, 58 + cursor: params.cursor, 57 59 }, 58 60 }); 59 61 return { uris: response.data.feed.map(({ scrobble }) => scrobble), ctx };
+49 -4
apps/web/src/api/feed.ts
··· 1 1 import { client } from "."; 2 2 3 - export const getFeed = () => { 4 - return []; 5 - }; 6 - 7 3 export const getFeedByUri = async (uri: string) => { 8 4 if (uri.includes("app.rocksky.song")) { 9 5 return null; ··· 58 54 } 59 55 return response.data; 60 56 }; 57 + 58 + export const getFeed = async (uri: string, limit?: number, cursor?: string) => { 59 + const response = await client.get<{ 60 + feed: { 61 + scrobble: { 62 + title: string; 63 + artist: string; 64 + albumArtist: string; 65 + album: string; 66 + trackNumber: number; 67 + duration: number; 68 + mbId: string | null; 69 + youtubeLink: string | null; 70 + spotifyLink: string | null; 71 + appleMusicLink: string | null; 72 + tidalLink: string | null; 73 + sha256: string; 74 + discNumber: number; 75 + composer: string | null; 76 + genre: string | null; 77 + label: string | null; 78 + copyrightMessage: string | null; 79 + uri: string; 80 + albumUri: string; 81 + artistUri: string; 82 + xataVersion: number; 83 + cover: string; 84 + date: string; 85 + user: string; 86 + userDisplayName: string; 87 + userAvatar: string; 88 + tags: string[]; 89 + id: string; 90 + }; 91 + }[]; 92 + }>("/xrpc/app.rocksky.feed.getFeed", { 93 + params: { 94 + feed: uri, 95 + limit, 96 + cursor, 97 + }, 98 + }); 99 + 100 + if (response.status !== 200) { 101 + return []; 102 + } 103 + 104 + return response.data.feed.map(({ scrobble }) => scrobble); 105 + };
+7
apps/web/src/atoms/feed.ts
··· 1 + import { atom } from "jotai"; 2 + 3 + export const feedAtom = atom<string>("all"); 4 + 5 + export const feedGeneratorUriAtom = atom<string>( 6 + "at://did:plc:vegqomyce4ssoqs7zwqvgqty/app.rocksky.feed.generator/all", 7 + );
+3 -8
apps/web/src/hooks/useFeed.tsx
··· 1 1 import { useQuery } from "@tanstack/react-query"; 2 - import { client } from "../api"; 3 - import { getFeedByUri, getFeedGenerators } from "../api/feed"; 2 + import { getFeedByUri, getFeedGenerators, getFeed } from "../api/feed"; 4 3 5 - export const useFeedQuery = (limit = 114) => 4 + export const useFeedQuery = (feed: string, limit = 114, cursor?: string) => 6 5 useQuery({ 7 6 queryKey: ["feed"], 8 - queryFn: () => 9 - client.get("/xrpc/app.rocksky.scrobble.getScrobbles", { 10 - params: { limit }, 11 - }), 12 - select: (res) => res.data.scrobbles || [], 7 + queryFn: () => getFeed(feed, limit, cursor), 13 8 }); 14 9 15 10 export const useFeedByUriQuery = (uri: string) =>
+4 -2
apps/web/src/pages/home/feed/Feed.tsx
··· 15 15 import { WS_URL } from "../../../consts"; 16 16 import { useQueryClient } from "@tanstack/react-query"; 17 17 import FeedGenerators from "./FeedGenerators"; 18 + import { useAtomValue } from "jotai"; 19 + import { feedGeneratorUriAtom } from "../../../atoms/feed"; 18 20 19 21 dayjs.extend(relativeTime); 20 22 ··· 35 37 const queryClient = useQueryClient(); 36 38 const socketRef = useRef<WebSocket | null>(null); 37 39 const heartbeatInterval = useRef<number | null>(null); 38 - const { data, isLoading } = useFeedQuery(); 40 + const feedUri = useAtomValue(feedGeneratorUriAtom); 41 + const { data, isLoading } = useFeedQuery(feedUri); 39 42 40 43 useEffect(() => { 41 44 const ws = new WebSocket(`${WS_URL.replace("http", "ws")}`); ··· 53 56 } 54 57 55 58 const message = JSON.parse(event.data); 56 - queryClient.setQueryData(["feed"], () => message.scrobbles); 57 59 queryClient.setQueryData(["now-playings"], () => message.nowPlayings); 58 60 queryClient.setQueryData( 59 61 ["scrobblesChart"],
+3 -1
apps/web/src/pages/home/feed/FeedGenerators/FeedGenerators.tsx
··· 1 1 import { useState, useRef, useEffect } from "react"; 2 2 import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; 3 3 import { categories } from "./constants"; 4 + import { useAtom } from "jotai"; 5 + import { feedAtom } from "../../../../atoms/feed"; 4 6 5 7 function FeedGenerators() { 6 8 const jwt = localStorage.getItem("token"); 7 - const [activeCategory, setActiveCategory] = useState("All"); 9 + const [activeCategory, setActiveCategory] = useAtom(feedAtom); 8 10 const [showLeftChevron, setShowLeftChevron] = useState(false); 9 11 const [showRightChevron, setShowRightChevron] = useState(true); 10 12 const [hasOverflow, setHasOverflow] = useState(false);