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

Configure Feed

Select the types of activity you want to include in your feed.

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);