import { graphql, useLazyLoadQuery, usePaginationFragment, useSubscription, } from "react-relay"; import { useEffect, useRef, useMemo } from "react"; import type { AppQuery } from "./__generated__/AppQuery.graphql"; import type { App_plays$key } from "./__generated__/App_plays.graphql"; import type { AppSubscription } from "./__generated__/AppSubscription.graphql"; import TrackItem from "./TrackItem"; import Layout from "./Layout"; import ScrobbleChart from "./ScrobbleChart"; import { ConnectionHandler, type GraphQLSubscriptionConfig, } from "relay-runtime"; export default function App() { const queryVariables = useMemo(() => { // Round to start of day to keep timestamp stable const now = new Date(); now.setHours(0, 0, 0, 0); const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); return { chartWhere: { playedTime: { gte: ninetyDaysAgo.toISOString(), }, }, }; }, []); const queryData = useLazyLoadQuery( graphql` query AppQuery($chartWhere: FmTealAlphaFeedPlayWhereInput!) { ...App_plays ...ScrobbleChart_data } `, queryVariables ); const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment< AppQuery, App_plays$key >( graphql` fragment App_plays on Query @refetchable(queryName: "AppPaginationQuery") @argumentDefinitions( cursor: { type: "String" } count: { type: "Int", defaultValue: 20 } ) { fmTealAlphaFeedPlays( first: $count after: $cursor sortBy: [{ field: playedTime, direction: desc }] ) @connection(key: "App_fmTealAlphaFeedPlays", filters: ["sortBy"]) { totalCount edges { node { playedTime ...TrackItem_play } } } } `, queryData ); const loadMoreRef = useRef(null); // Subscribe to new plays const subscriptionConfig: GraphQLSubscriptionConfig = { subscription: graphql` subscription AppSubscription { fmTealAlphaFeedPlayCreated { uri playedTime ...TrackItem_play } } `, variables: {}, updater: (store) => { const newPlay = store.getRootField("fmTealAlphaFeedPlayCreated"); if (!newPlay) return; // Only add plays from the last 24 hours const playedTime = newPlay.getValue("playedTime") as string | null; if (!playedTime) return; const playDate = new Date(playedTime); const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); if (playDate < cutoff) { // Play is too old, don't add it to the feed return; } const root = store.getRoot(); const connection = ConnectionHandler.getConnection( root, "App_fmTealAlphaFeedPlays", { sortBy: [{ field: "playedTime", direction: "desc" }] } ); if (!connection) return; const edge = ConnectionHandler.createEdge( store, connection, newPlay, "FmTealAlphaFeedPlayEdge" ); ConnectionHandler.insertEdgeBefore(connection, edge); // Update totalCount const totalCountRecord = root.getLinkedRecord("fmTealAlphaFeedPlays", { sortBy: [{ field: "playedTime", direction: "desc" }], }); if (totalCountRecord) { const currentCount = totalCountRecord.getValue("totalCount") as number; if (typeof currentCount === "number") { totalCountRecord.setValue(currentCount + 1, "totalCount"); } } }, }; useSubscription(subscriptionConfig); useEffect(() => { window.scrollTo(0, 0); }, []); const plays = data?.fmTealAlphaFeedPlays?.edges ?.map((edge) => edge.node) .filter((n) => n != null) || []; useEffect(() => { if (!loadMoreRef.current || !hasNext) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNext && !isLoadingNext) { loadNext(20); } }, { threshold: 0.1 } ); observer.observe(loadMoreRef.current); return () => observer.disconnect(); }, [hasNext, isLoadingNext, loadNext]); // Group plays by date const groupedPlays: { date: string; plays: typeof plays }[] = []; let currentDate = ""; plays.forEach((play) => { if (!play?.playedTime) return; const playDate = new Date(play.playedTime).toLocaleDateString("en-US", { weekday: "long", day: "numeric", month: "long", year: "numeric", }); if (playDate !== currentDate) { currentDate = playDate; groupedPlays.push({ date: playDate, plays: [play] }); } else { groupedPlays[groupedPlays.length - 1].plays.push(play); } }); return ( }>

{data?.fmTealAlphaFeedPlays?.totalCount?.toLocaleString()} scrobbles

{groupedPlays.map((group, groupIndex) => (

{group.date}

{group.plays.map((play, index) => ( ))}
))}
{hasNext && (
{isLoadingNext ? (

Loading...

) : (

ยท

)}
)}
); }