openvibers/openvibe.community Commits

Pulse on Events realtime (WS-F task 1): on the first page, public activity from here and the other sites (new pastes, threads, replies, go-lives, blog, wiki and news posts) arrives over events.openvibe.network/realtime/stream and a bar offers to show it; CSP connect-src allows events.openvibe.network

OP OpenVibers committed 25 Sept 2026 · d401ef42c3 · 4 files, +26 −2


Changes

ddiff --git a/public/css/community.css b/public/css/community.css
iindex 7383e64..4e6024f 100644
---- a/public/css/community.css
++++ b/public/css/community.css
@@@ -326,3 +326,7 @@ button.rx:hover { border-color: var(--accent); }
.search-meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 4px 0; font-size: .8rem; color: var(--text-muted); }
.search-snippet { margin: 4px 0 0; color: var(--text-secondary); overflow-wrap: anywhere; }
.search-snippet mark { background: rgba(var(--accent-rgb), .22); color: inherit; border-radius: 3px; padding: 0 2px; }
++
++/* Pulse: new activity from Events realtime */
++.pulse-new { position: sticky; top: 8px; z-index: 5; display: flex; justify-content: center; margin: 0 0 10px; }
++.pulse-new[hidden] { display: none; }
ddiff --git a/public/js/community.js b/public/js/community.js
iindex ca5856b..6535891 100644
---- a/public/js/community.js
++++ b/public/js/community.js
@@@ -138,4 +138,23 @@
});
});
}
++
++ // Pulse follows OpenVibe.Events realtime (roadmap WS-F task 1): on the first page, public activity
++ // from here and the other sites arrives over events.openvibe.network/realtime/stream, and a bar
++ // offers to show it (the page itself stays server-rendered, so a reload is the whole update).
++ var pulse = document.querySelector('[data-pulse-live]');
++ if (pulse && typeof EventSource !== 'undefined') {
++ var topics = ['community.paste.created', 'community.thread.created', 'community.post.created', 'live.stream.started', 'blog.post.published', 'wiki.page.published', 'news.story.published'];
++ var bar = pulse.querySelector('.pulse-new'), show = pulse.querySelector('[data-pulse-show]'), fresh = 0;
++ var es = new EventSource('https://events.openvibe.network/realtime/stream?topics=' + encodeURIComponent(topics.join(',')));
++ es.onmessage = function (m) {
++ var d; try { d = JSON.parse(m.data); } catch (e) { return; }
++ if (!d || !d.event || topics.indexOf(d.event.event_type) < 0) return;
++ fresh++;
++ show.textContent = fresh === 1 ? 'Show 1 new item' : 'Show ' + fresh + ' new items';
++ bar.hidden = false;
++ };
++ show.addEventListener('click', function () { es.close(); location.reload(); });
++ window.addEventListener('pagehide', function () { es.close(); });
++ }
})();
ddiff --git a/server/app.js b/server/app.js
iindex 322f249..248b457 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -84,7 +84,7 @@ function createApp(opts = {}) {
fontSrc: ["'self'", 'https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com', 'data:'],
// Screenshots serve from openvibe.media (which may 302 to object storage); avatars from Live/Network.
imgSrc: ["'self'", 'data:', 'blob:', 'https:'],
-- connectSrc: ["'self'", 'https://openvibe.network', 'https://openvibe.live', 'https://openvibe.media'],
++ connectSrc: ["'self'", 'https://openvibe.network', 'https://openvibe.live', 'https://openvibe.media', 'https://events.openvibe.network'],
// The Network's hidden /sso/check frame: how a visitor who is signed in elsewhere gets signed in here.
frameSrc: ["'self'", 'https://openvibe.network'],
frameAncestors: ["'self'"],
ddiff --git a/server/render/pulse.js b/server/render/pulse.js
iindex 50d377e..5dce113 100644
---- a/server/render/pulse.js
++++ b/server/render/pulse.js
@@@ -47,7 +47,8 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null }) {
<p class="muted">Public activity from across OpenVibe as it happens: new pastes, threads and replies here, and what the other sites share. Only public things appear, and anything made by AI is labelled as AI.</p>
</header>
<nav class="tabs sort-tabs" aria-label="Filter by origin">${tabs}</nav>
--<section data-results>
++<section data-results${after ? '' : ' data-pulse-live'}>
++ <p class="pulse-new" hidden><button type="button" class="btn btn-sm" data-pulse-show></button></p>
${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : '<p class="empty">Nothing here yet.</p>'}
${after || nextCursor ? `<nav class="pager" aria-label="Pages">${after ? `<a href="${esc(href(origin))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Latest</a>` : '<span></span>'}<span></span>${nextCursor ? `<a rel="next" href="${esc(href(origin, nextCursor))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}
</section>`;