From e3abe0d12ca381d37b61720e3f70b47d3dc993bf Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Wed, 24 Jun 2026 16:47:30 -0500 Subject: [PATCH] fix(recommendations): fall back to anon PDS agent for logged-out viewers getRecommendations was throwing 'no pdsAgent' for logged-out users because the action only checked pdsAgent, which is null until the user logs in. The endpoint is a public read on the Streamplace server (no auth check), so use the anonPDSAgent as a fallback. Also fixed a race in PlayerOffline: the recommendation effect depended only on profile.did, so if the WebSocket delivered the profile before loadOAuthClient finished setting up the anon agent, the effect ran once with no agent and never re-ran. Added pdsAgent and anonPDSAgent to the dep list, plus an early return when neither is available, so the effect retries once the OAuth client finishes initializing. Removed the temporary console.logs from the previous commit. --- .../src/components/stream/player-offline.tsx | 33 ++++++++++--------- .../src/lib/store/slices/streamplaceSlice.ts | 13 ++++++-- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/js/web/src/components/stream/player-offline.tsx b/js/web/src/components/stream/player-offline.tsx index de50a796..6f23ecd2 100644 --- a/js/web/src/components/stream/player-offline.tsx +++ b/js/web/src/components/stream/player-offline.tsx @@ -3,6 +3,7 @@ import { Link } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useStore as useZustandStore } from "zustand"; +import { useShallow } from "zustand/react/shallow"; import useAvatars from "../../hooks/use-avatars"; import { useStore } from "../../lib/store"; @@ -26,6 +27,16 @@ export function PlayerOffline({ }) { const { t } = useTranslation("common"); const profile = useZustandStore(store, (s) => s.profile); + // Subscribe to the app's agent fields so the effect re-runs when + // loadOAuthClient finishes setting up the anon (or authed) agent. + // Without this, a fast-firing WebSocket profile delivery can race + // the OAuth client init and we end up stuck with no agent. + const { pdsAgent, anonPDSAgent } = useStore( + useShallow((s) => ({ + pdsAgent: s.pdsAgent, + anonPDSAgent: s.anonPDSAgent, + })), + ); const avatars = useAvatars(profile?.did ? [profile.did] : []); const detailed = profile?.did ? avatars[profile.did] : null; const banner = detailed?.banner; @@ -38,43 +49,33 @@ export function PlayerOffline({ } | null>(null); useEffect(() => { - if (!profile?.did) { - console.log("[PlayerOffline] no profile.did yet, skipping fetch"); - return; - } + if (!profile?.did) return; + if (!pdsAgent && !anonPDSAgent) return; const getRecommendations = useStore.getState().getRecommendations; let mounted = true; const fetchRec = async () => { try { - console.log( - "[PlayerOffline] fetching recommendations for", - profile.did, - ); const result = await getRecommendations(profile.did); if (!mounted) return; - console.log( - "[PlayerOffline] got recommendations:", - result.recommendations?.length, - ); const first = result.recommendations?.find( (r) => r.$type === "place.stream.live.getRecommendations#livestreamRecommendation" && (r as { did?: string }).did, ) as { did?: string; source?: string } | undefined; - console.log("[PlayerOffline] first recommendation:", first); if (first?.did) { setRecommendation({ did: first.did, source: first.source ?? "" }); } - } catch (err) { - console.error("[PlayerOffline] recommendations fetch failed:", err); + } catch { + // Silent: recommendations are best-effort. The OFFLINE state + // is still useful on its own. } }; fetchRec(); return () => { mounted = false; }; - }, [profile?.did]); + }, [profile?.did, pdsAgent, anonPDSAgent]); // Look up the recommended streamer's profile so the card can // show their avatar and handle. diff --git a/js/web/src/lib/store/slices/streamplaceSlice.ts b/js/web/src/lib/store/slices/streamplaceSlice.ts index 0450fbbc..4c1ff8c7 100644 --- a/js/web/src/lib/store/slices/streamplaceSlice.ts +++ b/js/web/src/lib/store/slices/streamplaceSlice.ts @@ -112,11 +112,18 @@ export const createStreamplaceSlice: StateCreator< set({ chatWarned: warned }); }, getRecommendations: async (userDID: string) => { - const { pdsAgent } = get(); - if (!pdsAgent) { + // Fall back to the anonymous PDS agent when the user isn't logged + // in. The getRecommendations endpoint is a public read on the + // Streamplace server — it returns the streamer's own + // recommendations list filtered to currently-live streamers, or + // falls back to the streamer's follows that are live. No auth + // required, so a logged-out viewer can still see suggestions. + const { pdsAgent, anonPDSAgent } = get(); + const agent = pdsAgent ?? anonPDSAgent; + if (!agent) { throw new Error("no pdsAgent"); } - const result = await pdsAgent.place.stream.live.getRecommendations({ + const result = await agent.place.stream.live.getRecommendations({ userDID, }); return result.data; -- 2.51.2