A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import React from "react"; 2import { AtProtoRecord } from "../core/AtProtoRecord"; 3import { CurrentlyPlayingRenderer } from "../renderers/CurrentlyPlayingRenderer"; 4import { useDidResolution } from "../hooks/useDidResolution"; 5import type { TealActorStatusRecord } from "../types/teal"; 6 7/** 8 * Props for rendering teal.fm currently playing status. 9 */ 10export interface CurrentlyPlayingProps { 11 /** DID of the user whose currently playing status to display. */ 12 did: string; 13 /** Record key within the `fm.teal.alpha.actor.status` collection (usually "self"). */ 14 rkey?: string; 15 /** Prefetched teal.fm status record. When provided, skips fetching from the network. */ 16 record?: TealActorStatusRecord; 17 /** Optional renderer override for custom presentation. */ 18 renderer?: React.ComponentType<CurrentlyPlayingRendererInjectedProps>; 19 /** Fallback node displayed before loading begins. */ 20 fallback?: React.ReactNode; 21 /** Indicator node shown while data is loading. */ 22 loadingIndicator?: React.ReactNode; 23 /** Preferred color scheme for theming. */ 24 colorScheme?: "light" | "dark" | "system"; 25 /** Auto-refresh music data and album art. When true, refreshes every 15 seconds. Defaults to true. */ 26 autoRefresh?: boolean; 27 /** Refresh interval in milliseconds. Defaults to 15000 (15 seconds). Only used when autoRefresh is true. */ 28 refreshInterval?: number; 29} 30 31/** 32 * Values injected into custom currently playing renderer implementations. 33 */ 34export type CurrentlyPlayingRendererInjectedProps = { 35 /** Loaded teal.fm status record value. */ 36 record: TealActorStatusRecord; 37 /** Indicates whether the record is currently loading. */ 38 loading: boolean; 39 /** Fetch error, if any. */ 40 error?: Error; 41 /** Preferred color scheme for downstream components. */ 42 colorScheme?: "light" | "dark" | "system"; 43 /** DID associated with the record. */ 44 did: string; 45 /** Record key for the status. */ 46 rkey: string; 47 /** Label to display. */ 48 label?: string; 49 /** Handle to display in not listening state */ 50 handle?: string; 51}; 52 53/** NSID for teal.fm actor status records. */ 54export const CURRENTLY_PLAYING_COLLECTION = "fm.teal.alpha.actor.status"; 55 56/** 57 * Compares two teal.fm status records to determine if the track has changed. 58 * Used to prevent unnecessary re-renders during auto-refresh when the same track is still playing. 59 */ 60const compareTealRecords = ( 61 prev: TealActorStatusRecord | undefined, 62 next: TealActorStatusRecord | undefined 63): boolean => { 64 if (!prev || !next) return prev === next; 65 66 const prevTrack = prev.item.trackName; 67 const nextTrack = next.item.trackName; 68 const prevArtist = prev.item.artists[0]?.artistName; 69 const nextArtist = next.item.artists[0]?.artistName; 70 71 return prevTrack === nextTrack && prevArtist === nextArtist; 72}; 73 74/** 75 * Displays the currently playing track from teal.fm with auto-refresh. 76 * 77 * @param did - DID whose currently playing status should be fetched. 78 * @param rkey - Record key within the teal.fm status collection (defaults to "self"). 79 * @param renderer - Optional component override that will receive injected props. 80 * @param fallback - Node rendered before the first load begins. 81 * @param loadingIndicator - Node rendered while the status is loading. 82 * @param colorScheme - Preferred color scheme for theming the renderer. 83 * @param autoRefresh - When true (default), refreshes the record every 15 seconds (or custom interval). 84 * @param refreshInterval - Custom refresh interval in milliseconds. Defaults to 15000 (15 seconds). 85 * @returns A JSX subtree representing the currently playing track with loading states handled. 86 */ 87export const CurrentlyPlaying: React.FC<CurrentlyPlayingProps> = React.memo(({ 88 did, 89 rkey = "self", 90 record, 91 renderer, 92 fallback, 93 loadingIndicator, 94 colorScheme, 95 autoRefresh = true, 96 refreshInterval = 15000, 97}) => { 98 // Resolve handle from DID 99 const { handle } = useDidResolution(did); 100 101 const Comp: React.ComponentType<CurrentlyPlayingRendererInjectedProps> = 102 renderer ?? ((props) => <CurrentlyPlayingRenderer {...props} />); 103 const Wrapped: React.FC<{ 104 record: TealActorStatusRecord; 105 loading: boolean; 106 error?: Error; 107 }> = (props) => ( 108 <Comp 109 {...props} 110 colorScheme={colorScheme} 111 did={did} 112 rkey={rkey} 113 label="CURRENTLY PLAYING" 114 handle={handle} 115 /> 116 ); 117 118 if (record !== undefined) { 119 return ( 120 <AtProtoRecord<TealActorStatusRecord> 121 record={record} 122 renderer={Wrapped} 123 fallback={fallback} 124 loadingIndicator={loadingIndicator} 125 /> 126 ); 127 } 128 129 return ( 130 <AtProtoRecord<TealActorStatusRecord> 131 did={did} 132 collection={CURRENTLY_PLAYING_COLLECTION} 133 rkey={rkey} 134 renderer={Wrapped} 135 fallback={fallback} 136 loadingIndicator={loadingIndicator} 137 refreshInterval={autoRefresh ? refreshInterval : undefined} 138 compareRecords={compareTealRecords} 139 /> 140 ); 141}); 142 143export default CurrentlyPlaying;