Live video on the AT Protocol
79
fork

Configure Feed

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

at natb/command-errors 275 lines 8.4 kB view raw
1import { useCallback, useEffect } from "react"; 2import storage from "../storage"; 3import { 4 getStreamplaceStoreFromContext, 5 useStreamplaceStore, 6} from "./streamplace-store"; 7import { usePossiblyUnauthedPDSAgent } from "./xrpc"; 8 9export interface BrandingAsset { 10 key: string; 11 mimeType: string; 12 url?: string; // URL for images 13 data?: string; // inline data for text, or base64 for images 14 width?: number; // image width in pixels 15 height?: number; // image height in pixels 16} 17 18// helper to convert blob to base64 19const blobToBase64 = (blob: Blob): Promise<string> => { 20 return new Promise((resolve, reject) => { 21 const reader = new FileReader(); 22 reader.onloadend = () => resolve(reader.result as string); 23 reader.onerror = reject; 24 reader.readAsDataURL(blob); 25 }); 26}; 27 28const PropsInHeader = [ 29 "siteTitle", 30 "siteDescription", 31 "primaryColor", 32 "accentColor", 33 "defaultStreamer", 34 "mainLogo", 35 "favicon", 36 "sidebarBg", 37 "legalLinks", 38]; 39 40function getMetaContent(key: string): BrandingAsset | null { 41 if (typeof window === "undefined" || !window.document) return null; 42 const meta = document.querySelector(`meta[name="internal-brand:${key}`); 43 if (meta && meta.getAttribute("content")) { 44 let content = meta.getAttribute("content"); 45 if (content) return JSON.parse(content) as BrandingAsset; 46 } 47 48 return null; 49} 50 51// hook to fetch broadcaster DID (unauthenticated) 52export function useFetchBroadcasterDID() { 53 const streamplaceAgent = usePossiblyUnauthedPDSAgent(); 54 const store = getStreamplaceStoreFromContext(); 55 56 // prefetch from meta records, if on web 57 useEffect(() => { 58 if (typeof window !== "undefined" && window.document) { 59 try { 60 const metaRecords = PropsInHeader.reduce( 61 (acc, key) => { 62 const meta = document.querySelector( 63 `meta[name="internal-brand:${key}`, 64 ); 65 // hrmmmmmmmmmmmm 66 if (meta && meta.getAttribute("content")) { 67 let content = meta.getAttribute("content"); 68 if (content) acc[key] = JSON.parse(content) as BrandingAsset; 69 } 70 return acc; 71 }, 72 {} as Record<string, BrandingAsset>, 73 ); 74 75 console.log("Found meta records for broadcaster DID:", metaRecords); 76 // filter out all non-text values, can get on second fetch? 77 for (const key of Object.keys(metaRecords)) { 78 if (metaRecords[key].mimeType != "text/plain") { 79 delete metaRecords[key]; 80 } 81 } 82 } catch (e) { 83 console.warn("Failed to parse broadcaster DID from meta tags", e); 84 } 85 } 86 }, []); 87 88 return useCallback(async () => { 89 try { 90 if (!streamplaceAgent) { 91 throw new Error("Streamplace agent not available"); 92 } 93 const result = 94 await streamplaceAgent.place.stream.broadcast.getBroadcaster(); 95 store.setState({ broadcasterDID: result.data.broadcaster }); 96 if (result.data.server) { 97 store.setState({ serverDID: result.data.server }); 98 } 99 if (result.data.admins) { 100 store.setState({ adminDIDs: result.data.admins }); 101 } 102 } catch (err) { 103 console.error("Failed to fetch broadcaster DID:", err); 104 } 105 }, [streamplaceAgent, store]); 106} 107 108// hook to fetch branding data from the server 109export function useFetchBranding() { 110 const streamplaceAgent = usePossiblyUnauthedPDSAgent(); 111 const broadcasterDID = useStreamplaceStore((state) => state.broadcasterDID); 112 const url = useStreamplaceStore((state) => state.url); 113 const store = getStreamplaceStoreFromContext(); 114 115 return useCallback( 116 async ({ force = true } = {}) => { 117 if (!broadcasterDID) return; 118 119 try { 120 store.setState({ brandingLoading: true }); 121 122 // check localStorage first 123 const cacheKey = `branding:${broadcasterDID}`; 124 const cached = await storage.getItem(cacheKey); 125 if (!force && cached) { 126 try { 127 const parsed = JSON.parse(cached); 128 // check if cache is less than 1 hour old 129 if (Date.now() - parsed.timestamp < 60 * 60 * 1000) { 130 store.setState({ 131 branding: parsed.data, 132 brandingLoading: false, 133 brandingError: null, 134 }); 135 return; 136 } 137 } catch (e) { 138 // invalid cache, continue to fetch 139 console.warn("Invalid branding cache, refetching", e); 140 } 141 } 142 143 // fetch branding metadata from server 144 if (!streamplaceAgent) { 145 throw new Error("Streamplace agent not available"); 146 } 147 const res = await streamplaceAgent.place.stream.branding.getBranding({ 148 broadcaster: broadcasterDID, 149 }); 150 const assets = res.data.assets; 151 152 // convert assets array to keyed object and fetch blob data 153 const brandingMap: Record<string, BrandingAsset> = {}; 154 155 for (const asset of assets) { 156 brandingMap[asset.key] = { ...asset }; 157 158 // if data is already inline (text assets), use it directly 159 if (asset.data) { 160 brandingMap[asset.key].data = asset.data; 161 } else if (asset.url) { 162 // for images, construct full URL and fetch blob 163 const fullUrl = `${url}${asset.url}`; 164 const blobRes = await fetch(fullUrl); 165 const blob = await blobRes.blob(); 166 brandingMap[asset.key].data = await blobToBase64(blob); 167 } 168 } 169 170 // cache in localStorage 171 storage.setItem( 172 cacheKey, 173 JSON.stringify({ 174 timestamp: Date.now(), 175 data: brandingMap, 176 }), 177 ); 178 179 store.setState({ 180 branding: brandingMap, 181 brandingLoading: false, 182 brandingError: null, 183 }); 184 } catch (err: any) { 185 console.error("Failed to fetch branding:", err); 186 store.setState({ 187 brandingLoading: false, 188 brandingError: err.message || "Failed to fetch branding", 189 }); 190 } 191 }, 192 [broadcasterDID, streamplaceAgent, url, store], 193 ); 194} 195 196// hook to get a specific branding asset by key 197export function useBrandingAsset(key: string): BrandingAsset | undefined { 198 return ( 199 useStreamplaceStore((state) => state.branding?.[key]) || 200 getMetaContent(key) || 201 undefined 202 ); 203} 204 205// convenience hook for main logo 206export function useMainLogo(): string | undefined { 207 const asset = useBrandingAsset("mainLogo"); 208 return asset?.data; 209} 210 211// convenience hook for favicon 212export function useFavicon(): string | undefined { 213 const asset = useBrandingAsset("favicon"); 214 return asset?.data; 215} 216 217// convenience hook for site title 218export function useSiteTitle(): string { 219 const asset = useBrandingAsset("siteTitle"); 220 return asset?.data || "My Streamplace Station"; 221} 222 223// convenience hook for site description 224export function useSiteDescription(): string { 225 const asset = useBrandingAsset("siteDescription"); 226 return asset?.data || "Live streaming platform"; 227} 228 229// convenience hook for primary color 230export function usePrimaryColor(): string { 231 const asset = useBrandingAsset("primaryColor"); 232 return asset?.data || "#6366f1"; 233} 234 235// convenience hook for accent color 236export function useAccentColor(): string { 237 const asset = useBrandingAsset("accentColor"); 238 return asset?.data || "#8b5cf6"; 239} 240 241// convenience hook for default streamer 242export function useDefaultStreamer(): string | undefined { 243 const asset = useBrandingAsset("defaultStreamer"); 244 return asset?.data || undefined; 245} 246 247// convenience hook for sidebar background image 248export function useSidebarBackgroundImage(): BrandingAsset | undefined { 249 return useBrandingAsset("sidebarBackgroundImage"); 250} 251 252// convenience hook for legal links 253export function useLegalLinks(): { text: string; url: string }[] { 254 const asset = useBrandingAsset("legalLinks"); 255 if (!asset?.data) { 256 return []; 257 } 258 try { 259 return JSON.parse(asset.data); 260 } catch { 261 return []; 262 } 263} 264 265// hook to auto-fetch branding when broadcaster changes 266export function useBrandingAutoFetch() { 267 const fetchBranding = useFetchBranding(); 268 const broadcasterDID = useStreamplaceStore((state) => state.broadcasterDID); 269 270 useEffect(() => { 271 if (broadcasterDID) { 272 fetchBranding(); 273 } 274 }, [broadcasterDID, fetchBranding]); 275}