jevstrudel.git / website / src / jev / ActivityTab.jsx
ActivityTab.jsx99 lines · 4.0 KB · raw
1// The jev panel's activity tab (activity.mjs): what is happening on the
2// site, newest first, with more on request. Names open profiles; a
3// listener song plays in the sandbox as its card does, and a take (a site
4// song's or a listener's) replays from its link (performance.mjs). Text only.
5import { useEffect, useState } from 'react';
6import { loadListenerSong as showListenerSong, readSongs } from './client.mjs';
7import { appendPage, describe, loadActivity } from './activity.mjs';
8import { loadListenerSong } from './listeners.mjs';
9import { NameLink } from './NameLink.jsx';
10import { performancePath } from './performance.mjs';
11
12const when = (ms) => new Date(ms).toLocaleString([], { dateStyle: 'short', timeStyle: 'short' });
13
14export function ActivityTab({ context }) {
15  const [items, setItems] = useState([]);
16  const [next, setNext] = useState(null);
17  const [state, setState] = useState('loading'); // loading | idle | error
18  const [error, setError] = useState(null);
19  const songs = readSongs();
20  const title = (id) => songs.find((s) => s.id === id)?.title ?? id;
21
22  const load = async (before, fresh = false) => {
23    setState('loading');
24    try {
25      const page = await loadActivity(before);
26      setItems((old) => (fresh ? page.items : appendPage(old, page.items)));
27      setNext(page.next);
28      setState('idle');
29    } catch (e) {
30      setError(e.message);
31      setState('error');
32    }
33  };
34  useEffect(() => void load(null, true), []);
35
36  const editor = context.editorRef.current;
37  const openListener = async (id) => {
38    try {
39      await showListenerSong(editor, await loadListenerSong(id), { play: true });
40    } catch (e) {
41      setError(`that song could not be loaded: ${e.message}`);
42    }
43  };
44
45  return (
46    <div className="px-4 py-3 text-sm font-mono" data-testid="jev-activity">
47      <div className="flex items-center gap-2 mb-2">
48        <span className="opacity-70 grow">What is happening on the site, newest first.</span>
49        <button className="underline hover:opacity-60 text-xs" onClick={() => load(null, true)}>
50          refresh
51        </button>
52      </div>
53      {error && <p className="text-red-400 text-xs">{error}</p>}
54      {state !== 'loading' && !items.length && !error && <p className="opacity-60">Nothing yet.</p>}
55      <ol className="space-y-2">
56        {items.map((i) => (
57          <li key={`${i.kind}:${i.id ?? i.song}:${i.rev ?? ''}:${i.at}`} className="leading-snug">
58            <span className="opacity-50 text-xs">{when(i.at)} </span>
59            {i.author && (
60              <>
61                <NameLink id={i.author.id} name={i.author.name} />{' '}
62              </>
63            )}
64            {i.kind === 'take' && (
65              <>
66                <NameLink id={i.player?.id} name={i.player?.name} />{' '}
67              </>
68            )}
69            <span style={{ color: i.screen === 'spam' || i.screen === 'abuse' ? '#f87171' : undefined }}>
70              {describe(i, title)}
71            </span>
72            {/* a public revision's song plays its newest public revision */}
73            {((i.kind === 'revision' && i.screen === 'fine') || i.kind === 'scored') && (
74              <button className="ml-1 text-xs underline opacity-70 hover:opacity-100" onClick={() => openListener(i.song)}>
75                play
76              </button>
77            )}
78            {i.kind === 'take' && (
79              <a
80                className="ml-1 text-xs underline opacity-70 hover:opacity-100"
81                href={performancePath(i.song, i.id)}
82                title="replay this take: Jev's decisions as they were, with no Jev calls"
83              >
84                ▶ replay
85              </a>
86            )}
87            {i.body && <p className="whitespace-pre-wrap break-words opacity-90 mt-0.5">{i.body}</p>}
88          </li>
89        ))}
90      </ol>
91      {state === 'loading' && <p className="opacity-60 mt-2">reading…</p>}
92      {state === 'idle' && next !== null && (
93        <button className="mt-3 underline hover:opacity-60" onClick={() => load(next)}>
94          older
95        </button>
96      )}
97    </div>
98  );
99}