1// "listen together", on a song's view in the jev panel: start a listening 2// party (party.mjs) and share its link, or, opened from one, see who is in 3// it and whether the host is playing. 4import { useState, useSyncExternalStore } from 'react'; 5import { leaveParty, linkFor, partyStore, startParty } from './party.mjs'; 6import { themeColor as ACCENT } from './site.mjs'; 7 8export const useParty = () => useSyncExternalStore(partyStore.subscribe, partyStore.get, partyStore.get); 9 10const people = (n) => `${n} ${n === 1 ? 'person' : 'people'} here`; 11 12export function ListenTogether({ song }) { 13 const party = useParty(); 14 const [copied, setCopied] = useState(false); 15 if (!song?.code) return null; 16 17 if (party.status === 'off') { 18 return ( 19 <div className="not-prose my-3 text-sm"> 20 <button 21 className="rounded border px-2 py-1 hover:opacity-80" 22 style={{ borderColor: ACCENT }} 23 onClick={() => startParty()} 24 title="start a listening party: everyone with the link hears this song, with the same Jev, in sync" 25 > 26 👥 listen together 27 </button> 28 </div> 29 ); 30 } 31 32 const link = party.song || song ? linkFor(party.room, party.song ?? song.id) : ''; 33 const copy = async () => { 34 try { 35 await navigator.clipboard.writeText(link); 36 setCopied(true); 37 setTimeout(() => setCopied(false), 2000); 38 } catch { 39 setCopied(false); 40 } 41 }; 42 43 let status; 44 if (party.status === 'ended') status = party.note ?? 'this party has ended'; 45 else if (party.status !== 'open') status = party.status === 'connecting' ? 'joining…' : 'reconnecting…'; 46 else if (party.role === 'host') status = `${people(party.people)} · you are the host: your Jev decides for everyone`; 47 else if (!party.hostHere) status = `${people(party.people)} · the host has left; the song plays its fallbacks`; 48 else if (party.waiting) status = `${people(party.people)} · waiting for the host to press play`; 49 else if (party.joinedAt !== null) 50 status = `${people(party.people)} · in sync with the host${party.joinedAt >= 1 ? `, joined at cycle ${Math.floor(party.joinedAt) + 1}` : ''}`; 51 else status = `${people(party.people)} · press play to listen in sync with the host`; 52 53 return ( 54 <div className="not-prose my-3 text-sm rounded border px-3 py-2 space-y-1" style={{ borderColor: ACCENT }}> 55 <div className="flex items-center gap-2"> 56 <b>👥 listening party</b> 57 <button className="ml-auto opacity-60 hover:opacity-100" onClick={leaveParty}> 58 {party.role === 'host' ? 'end' : 'leave'} 59 </button> 60 </div> 61 <div data-party-status>{status}</div> 62 {party.role === 'host' && party.status !== 'ended' && ( 63 <div className="flex items-center gap-2"> 64 <input readOnly value={link} className="grow bg-background border border-muted rounded px-1 text-xs" /> 65 <button className="rounded border border-muted px-2 hover:opacity-80" onClick={copy}> 66 {copied ? 'copied' : 'copy'} 67 </button> 68 </div> 69 )} 70 {party.note && party.status !== 'ended' && <div className="opacity-70">{party.note}</div>} 71 </div> 72 ); 73}