diff --git a/apps/web/src/app/l/[joinCode]/page.tsx b/apps/web/src/app/l/[joinCode]/page.tsx index 60a325a7..d7a58c22 100644 --- a/apps/web/src/app/l/[joinCode]/page.tsx +++ b/apps/web/src/app/l/[joinCode]/page.tsx @@ -117,6 +117,9 @@ export default async function LiveDetailPage({ params }: PageProps) { const when = session.published_at ?? session.created_at; const nonce = (await headers()).get('x-nonce') ?? undefined; const snippet = iframeSnippet(session.join_code, { title: session.subject }); + // A stream is credited to its channel when it has one — see the name link + // below — so the avatar follows the same precedence. + const avatarUrl = session.channel_avatar_url ?? session.host_avatar_url; return (
@@ -177,13 +180,9 @@ export default async function LiveDetailPage({ params }: PageProps) {
- {session.host_avatar_url ? ( + {avatarUrl ? ( // eslint-disable-next-line @next/next/no-img-element - + ) : (
diff --git a/packages/shared-types/src/database.ts b/packages/shared-types/src/database.ts index 006ec0ff..c88f91a7 100644 --- a/packages/shared-types/src/database.ts +++ b/packages/shared-types/src/database.ts @@ -226,6 +226,7 @@ export interface PublicSessionDetail { liked: boolean; channel_handle: string | null; channel_name: string | null; + channel_avatar_url: string | null; /** Playback URL of the latest finished recording, when available. */ recording_url: string | null; } diff --git a/supabase/migrations/20260829120000_public_session_channel_avatar.sql b/supabase/migrations/20260829120000_public_session_channel_avatar.sql new file mode 100644 index 00000000..11b0e858 --- /dev/null +++ b/supabase/migrations/20260829120000_public_session_channel_avatar.sql @@ -0,0 +1,41 @@ +-- The live page (/l/) credits a stream to its channel — the name it +-- links is ch.name, falling back to the host's profile only when the session +-- has no channel. The avatar beside that name did not follow the same rule: it +-- only ever read profiles.avatar_url, which nothing in the app writes, so every +-- public live rendered the grey placeholder even when the channel had artwork. +-- +-- Expose the channel's avatar alongside the host's so the page can mirror the +-- precedence its name link already uses. + +DROP FUNCTION IF EXISTS public.get_public_session(text); +CREATE OR REPLACE FUNCTION public.get_public_session(p_join_code text) +RETURNS TABLE( + id uuid, join_code text, subject text, description text, banner_url text, + status session_status, is_live boolean, viewer_count bigint, + published_at timestamptz, created_at timestamptz, + host_username text, host_display_name text, host_avatar_url text, + like_count bigint, comment_count bigint, liked boolean, + channel_handle text, channel_name text, channel_avatar_url text, + recording_url text +) +LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public' AS $function$ +DECLARE live_ttl CONSTANT interval := interval '90 seconds'; +BEGIN + RETURN QUERY + SELECT s.id, s.join_code, s.subject, s.description, s.banner_url, s.status, + (s.current_host_id IS NOT NULL AND s.host_last_seen_at > NOW() - live_ttl) AS is_live, + (SELECT COUNT(*) FROM public.session_participants sp WHERE sp.session_id = s.id AND sp.left_at IS NULL), + s.published_at, s.created_at, p.username, p.display_name, p.avatar_url, + (SELECT COUNT(*) FROM public.session_likes sl WHERE sl.session_id = s.id), + (SELECT COUNT(*) FROM public.session_comments sc WHERE sc.session_id = s.id), + (auth.uid() IS NOT NULL AND EXISTS (SELECT 1 FROM public.session_likes sl WHERE sl.session_id = s.id AND sl.user_id = auth.uid())), + ch.handle, ch.name, ch.avatar_url, + (SELECT r.playback_url FROM public.recordings r + WHERE r.session_id = s.id AND r.status = 'ready' AND r.playback_url IS NOT NULL + ORDER BY r.created_at DESC LIMIT 1) + FROM public.sessions s + JOIN public.profiles p ON p.id = s.creator_id + LEFT JOIN public.channels ch ON ch.id = s.channel_id + WHERE s.is_public = TRUE AND lower(s.join_code) = lower(btrim(COALESCE(p_join_code, ''))) LIMIT 1; +END; $function$; +GRANT EXECUTE ON FUNCTION public.get_public_session(text) TO anon, authenticated;