jevstrudel.git / website / src / jev / ListenTogether.jsx
ListenTogether.jsx73 lines · 3.1 KB · raw
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}