openvibers/openvibe.live Commits
Live Now is newest first, never by crowd, and says so (market outcome) (#506)
Changes
@@@ -76,7 +76,7 @@<div class="home-featured" id="home-featured" hidden></div><!-- Live Streams --><div class="section-header" style="--sc:#ef4444">-- <h2><i class="fa-solid fa-circle live-dot"></i> Live Now<small class="section-kicker">on air</small></h2>++ <h2><i class="fa-solid fa-circle live-dot"></i> Live Now<small class="section-kicker" title="Sorted by when each stream started, so a channel that just went live is seen">newest first, never by crowd</small></h2><span id="live-count" class="badge">0</span><button type="button" class="home-featured-toggle" id="home-featured-toggle" hidden><i class="fa-solid fa-satellite-dish"></i> Show featured stream</button></div>@@@ -1388,7 +1388,9 @@ async function loadHome() {// copy is at most 20 seconds old; anything older falls through to a normal load and the// placeholders cover the gap.await apiSWR('/streams', (liveData) => {-- const streams = liveData.streams || [];++ // Newest first, never by crowd (market research, Live outcome: a channel that just went live is seen,++ // instead of the biggest staying on top). The server's own order stays for its other readers.++ const streams = (liveData.streams || []).slice().sort((a, b) => (Date.parse(b.started_at) || 0) - (Date.parse(a.started_at) || 0));document.getElementById('live-count').textContent = streams.length;const noLiveEl = document.getElementById('no-live-streams');if (noLiveEl) noLiveEl.style.display = streams.length ? 'none' : '';nnew file mode 100644@@@ -0,0 +1,26 @@++'use strict';++/**++ * The home's Live Now grid is newest first, never by crowd (market research, Live's outcome: a channel that just went++ * live is seen instead of the biggest staying on top), and the section says so. The server's /api/streams keeps its++ * own order for its other readers; the home sorts what it draws.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const fragment = fs.readFileSync(path.join(__dirname, '..', 'public', 'fragments', 'home.html'), 'utf8');++const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app-home.js'), 'utf8');++++assert.match(fragment, /Live Now<small class="section-kicker"[^>]*>newest first, never by crowd<\/small>/, 'the section says how it is sorted');++const sorter = /const streams = \(liveData\.streams \|\| \[\]\)\.slice\(\)\.sort\(\(a, b\) => (.+?)\);\n/.exec(js);++assert.ok(sorter, 'the home sorts the live streams it draws');++// The comparator the home uses, run on three streams: the one that started last comes first, whatever its crowd.++const cmp = new Function('a', 'b', `return ${sorter[1]};`);++const list = [++ { username: 'big', viewer_count: 900, started_at: '2026-11-01T08:00:00Z' },++ { username: 'new', viewer_count: 1, started_at: '2026-11-01T10:30:00Z' },++ { username: 'mid', viewer_count: 40, started_at: '2026-11-01T09:00:00Z' },++].sort(cmp);++assert.deepStrictEqual(list.map((s) => s.username), ['new', 'mid', 'big']);++assert.ok(js.indexOf(sorter[0]) < js.indexOf("renderStreamGrid('stream-grid-live', streams, true)"), 'sorted before it is drawn');++console.log('home-live-order: ok');
@@ -76,7 +76,7 @@
@@ -76,7 +76,7 @@
<div class="home-featured" id="home-featured" hidden></div>
<div class="home-featured" id="home-featured" hidden></div>
<!-- Live Streams -->
<!-- Live Streams -->
<div class="section-header" style="--sc:#ef4444">
<div class="section-header" style="--sc:#ef4444">
- <h2><i class="fa-solid fa-circle live-dot"></i> Live Now<small class="section-kicker">on air</small></h2>
+ <h2><i class="fa-solid fa-circle live-dot"></i> Live Now<small class="section-kicker" title="Sorted by when each stream started, so a channel that just went live is seen">newest first, never by crowd</small></h2>
<span id="live-count" class="badge">0</span>
<span id="live-count" class="badge">0</span>
<button type="button" class="home-featured-toggle" id="home-featured-toggle" hidden><i class="fa-solid fa-satellite-dish"></i> Show featured stream</button>
<button type="button" class="home-featured-toggle" id="home-featured-toggle" hidden><i class="fa-solid fa-satellite-dish"></i> Show featured stream</button>
</div>
</div>
@@ -1388,7 +1388,9 @@ async function loadHome() {
@@ -1388,7 +1388,9 @@ async function loadHome() {
// copy is at most 20 seconds old; anything older falls through to a normal load and the
// copy is at most 20 seconds old; anything older falls through to a normal load and the
// placeholders cover the gap.
// placeholders cover the gap.
await apiSWR('/streams', (liveData) => {
await apiSWR('/streams', (liveData) => {
- const streams = liveData.streams || [];
+ // Newest first, never by crowd (market research, Live outcome: a channel that just went live is seen,
+ // instead of the biggest staying on top). The server's own order stays for its other readers.
+ const streams = (liveData.streams || []).slice().sort((a, b) => (Date.parse(b.started_at) || 0) - (Date.parse(a.started_at) || 0));
document.getElementById('live-count').textContent = streams.length;
document.getElementById('live-count').textContent = streams.length;
const noLiveEl = document.getElementById('no-live-streams');
const noLiveEl = document.getElementById('no-live-streams');
if (noLiveEl) noLiveEl.style.display = streams.length ? 'none' : '';
if (noLiveEl) noLiveEl.style.display = streams.length ? 'none' : '';
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+'use strict';
+/**
+ * The home's Live Now grid is newest first, never by crowd (market research, Live's outcome: a channel that just went
+ * live is seen instead of the biggest staying on top), and the section says so. The server's /api/streams keeps its
+ * own order for its other readers; the home sorts what it draws.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const fragment = fs.readFileSync(path.join(__dirname, '..', 'public', 'fragments', 'home.html'), 'utf8');
+const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app-home.js'), 'utf8');
+
+assert.match(fragment, /Live Now<small class="section-kicker"[^>]*>newest first, never by crowd<\/small>/, 'the section says how it is sorted');
+const sorter = /const streams = \(liveData\.streams \|\| \[\]\)\.slice\(\)\.sort\(\(a, b\) => (.+?)\);\n/.exec(js);
+assert.ok(sorter, 'the home sorts the live streams it draws');
+// The comparator the home uses, run on three streams: the one that started last comes first, whatever its crowd.
+const cmp = new Function('a', 'b', `return ${sorter[1]};`);
+const list = [
+ { username: 'big', viewer_count: 900, started_at: '2026-11-01T08:00:00Z' },
+ { username: 'new', viewer_count: 1, started_at: '2026-11-01T10:30:00Z' },
+ { username: 'mid', viewer_count: 40, started_at: '2026-11-01T09:00:00Z' },
+].sort(cmp);
+assert.deepStrictEqual(list.map((s) => s.username), ['new', 'mid', 'big']);
+assert.ok(js.indexOf(sorter[0]) < js.indexOf("renderStreamGrid('stream-grid-live', streams, true)"), 'sorted before it is drawn');
+console.log('home-live-order: ok');