openvibers/openvibe.community Commits
People's pastes lead the home and /pastes; the moment pastes from streams have their own shelf and filter (#121)
Changes
@@@ -96,7 +96,7 @@results.classList.remove('loading');}).catch(function () { location.href = url; });}-- browseForm.addEventListener('submit', function (e) { e.preventDefault(); var qs = new URLSearchParams(new FormData(browseForm)); ['sort', 'lang', 'q'].forEach(function (k) { if (!qs.get(k) || (k === 'sort' && qs.get(k) === 'new')) qs.delete(k); }); var s = qs.toString(); load('/pastes' + (s ? '?' + s : ''), true); }, { signal });++ browseForm.addEventListener('submit', function (e) { e.preventDefault(); var qs = new URLSearchParams(new FormData(browseForm)); ['sort', 'lang', 'q', 'from'].forEach(function (k) { if (!qs.get(k) || (k === 'sort' && qs.get(k) === 'new') || (k === 'from' && qs.get(k) === 'people')) qs.delete(k); }); var s = qs.toString(); load('/pastes' + (s ? '?' + s : ''), true); }, { signal });$$('select', browseForm).forEach(function (sel) { sel.addEventListener('change', function () { browseForm.requestSubmit ? browseForm.requestSubmit() : browseForm.dispatchEvent(new Event('submit', { cancelable: true })); }, { signal }); });results.addEventListener('click', function (e) { var a = e.target.closest ? e.target.closest('.pager a') : null; if (!a) return; e.preventDefault(); load(a.getAttribute('href'), true); window.scrollTo({ top: results.offsetTop - 80, behavior: 'smooth' }); }, { signal });window.addEventListener('popstate', function (e) { if (e.state && e.state.browse) load(e.state.browse, false); else if (location.pathname === '/pastes') load(location.pathname + location.search, false); }, { signal });@@@ -326,8 +326,9 @@ async function createApp(opts = {}) {}app.get('/', withUser, wrap(async (req, res) => {-- const [latest, trending, languages, extras] = await Promise.all([catalog.latest(12), catalog.trending(8), catalog.languages(), readHomeExtras()]);-- html(res, pages.homePage({ latest, trending, languages, user: req.user, ...extras }));++ // People's pastes lead; OpenVibe.AI's moment pastes from streams have their own shelf.++ const [latest, trending, fromStreams, languages, extras] = await Promise.all([catalog.latest(12, { from: 'people' }), catalog.trending(8, { from: 'people' }), catalog.latest(8, { from: 'streams' }), catalog.languages(), readHomeExtras()]);++ html(res, pages.homePage({ latest, trending, fromStreams, languages, user: req.user, ...extras }));}));app.get('/updates', (req, res) => html(res, pages.updatesPage()));@@@ -21,42 +21,41 @@ const WINDOW = 200; // page cap for one window queryconst LOCAL_TTL_MS = 2000;const PER_PAGE = 24;--let _recent = { at: 0, promise: null, pastes: [] };--const _byType = new Map(); // 'paste' | 'screenshot' → { at, promise, pastes }++// One cached window per (type, from): type 'paste' | 'screenshot' | null (both), from 'people' | 'streams' | 'all'.++const _windows = new Map(); // key → { at, promise, pastes }++// Where a paste comes from: people (written or imported by someone) or streams (OpenVibe.AI's moment pastes from Live).++const FROM = { people: 'people', streams: 'ai', all: undefined };function _textOnlyPublic(rows) {return (rows || []).filter((p) => p && p.slug && (p.visibility === 'public' || p.visibility == null));}--/** The latest public pastes (cached). Never throws — an unavailable store yields the stale list or []. */--async function recent() {++/** The latest public pastes of one type and origin (cached). Never throws: an unavailable store yields the stale list or []. */++async function windowOf({ type = null, from = 'all' } = {}) {++ const key = `${type || 'any'}:${from}`;const now = Date.now();-- if (_recent.promise) return _recent.promise;-- if (now - _recent.at < LOCAL_TTL_MS) return _recent.pastes;-- _recent.promise = source.listPastes({ limit: WINDOW, offset: 0 })-- .then((out) => { _recent = { at: Date.now(), promise: null, pastes: _textOnlyPublic(out && out.pastes) }; return _recent.pastes; })-- .catch((err) => { console.warn('[Catalog] recent pastes unavailable:', err.message); _recent.promise = null; _recent.at = Date.now() - LOCAL_TTL_MS + 5000; return _recent.pastes; });-- return _recent.promise;--}----/** The latest public pastes of one type, 'paste' (text) or 'screenshot' (cached like recent()). Never throws. */--async function recentOf(type) {-- const now = Date.now();-- const cur = _byType.get(type) || { at: 0, promise: null, pastes: [] };++ const cur = _windows.get(key) || { at: 0, promise: null, pastes: [] };if (cur.promise) return cur.promise;if (now - cur.at < LOCAL_TTL_MS) return cur.pastes;-- cur.promise = source.listPastes({ limit: WINDOW, offset: 0, type })-- .then((out) => { _byType.set(type, { at: Date.now(), promise: null, pastes: _textOnlyPublic(out && out.pastes) }); return _byType.get(type).pastes; })-- .catch((err) => { console.warn(`[Catalog] recent ${type}s unavailable:`, err.message); _byType.set(type, { at: Date.now() - LOCAL_TTL_MS + 5000, promise: null, pastes: cur.pastes }); return cur.pastes; });-- _byType.set(type, cur);++ const query = { limit: WINDOW, offset: 0, ...(type ? { type } : {}), ...(FROM[from] ? { origin: FROM[from] } : {}) };++ cur.promise = source.listPastes(query)++ .then((out) => { _windows.set(key, { at: Date.now(), promise: null, pastes: _textOnlyPublic(out && out.pastes) }); return _windows.get(key).pastes; })++ .catch((err) => { console.warn(`[Catalog] recent pastes (${key}) unavailable:`, err.message); _windows.set(key, { at: Date.now() - LOCAL_TTL_MS + 5000, promise: null, pastes: cur.pastes }); return cur.pastes; });++ _windows.set(key, cur);return cur.promise;}++/** The latest public pastes, every type and origin (cached). Never throws. */++const recent = () => windowOf();++/** The latest public pastes of one type, 'paste' (text) or 'screenshot' (cached like recent()). Never throws. */++const recentOf = (type) => windowOf({ type });++function byViews(a, b) { return (b.views || 0) - (a.views || 0) || (b.likes || 0) - (a.likes || 0) || String(b.created_at).localeCompare(String(a.created_at)); }function byNewest(a, b) { return String(b.created_at || '').localeCompare(String(a.created_at || '')); }--async function latest(n = 12) { return (await recent()).slice().sort(byNewest).slice(0, n); }--async function trending(n = 8) { return (await recent()).slice().sort(byViews).slice(0, n); }++const fromOf = (v) => (Object.prototype.hasOwnProperty.call(FROM, v) ? v : 'all');++async function latest(n = 12, { from = 'all' } = {}) { return (await windowOf({ from: fromOf(from) })).slice().sort(byNewest).slice(0, n); }++async function trending(n = 8, { from = 'all' } = {}) { return (await windowOf({ from: fromOf(from) })).slice().sort(byViews).slice(0, n); }/** Pastes in the same language first, then the newest, never the paste itself. */async function related(paste, n = 6) {@@@ -85,7 +84,9 @@ function normalizeBrowseQuery(query = {}) {const page = Math.max(1, Math.min(parseInt(query.page, 10) || 1, 500));// What to list: text pastes (default), images (screenshots) or all of them.const type = ['all', 'pastes', 'images'].includes(query.type) ? query.type : (lang === 'image' ? 'images' : 'pastes');-- return { q, sort, lang: type === 'images' && lang === 'image' ? '' : lang, page, type };++ // Whose: people's pastes (the default), the moment pastes from streams, or both.++ const from = ['people', 'streams', 'all'].includes(query.from) ? query.from : 'people';++ return { q, sort, lang: type === 'images' && lang === 'image' ? '' : lang, page, type, from };}const _ofType = (type) => (p) => (type === 'all' ? true : type === 'images' ? p.type === 'screenshot' : p.type !== 'screenshot');@@@ -95,24 +96,25 @@ const _ofType = (type) => (p) => (type === 'all' ? true : type === 'images' ? p.* sort and the language filter are answered from the recent window.*/async function browse(query = {}, ctx = {}) {-- const { q, sort, lang, page, type } = normalizeBrowseQuery(query);++ const { q, sort, lang, page, type, from } = normalizeBrowseQuery(query);++ const origin = FROM[from] ? { origin: FROM[from] } : {};const offset = (page - 1) * PER_PAGE;let pastes, total, windowed = false;if (sort === 'new' && !lang && type === 'pastes') {-- const out = await source.listPastes({ limit: PER_PAGE, offset, search: q || undefined, type: 'paste' }, ctx);++ const out = await source.listPastes({ limit: PER_PAGE, offset, search: q || undefined, type: 'paste', ...origin }, ctx);pastes = _textOnlyPublic(out && out.pastes);total = Number(out && out.total) || pastes.length;} else {let rows;if (q) {-- const out = await source.listPastes({ limit: WINDOW, offset: 0, search: q, type: 'paste' }, ctx);++ const out = await source.listPastes({ limit: WINDOW, offset: 0, search: q, type: 'paste', ...origin }, ctx);rows = _textOnlyPublic(out && out.pastes);} else if (type === 'all') {-- const [text, images] = await Promise.all([recentOf('paste'), recentOf('screenshot')]);++ const [text, images] = await Promise.all([windowOf({ type: 'paste', from }), windowOf({ type: 'screenshot', from })]);rows = [...text, ...images];} else {-- rows = await recentOf(type === 'images' ? 'screenshot' : 'paste');++ rows = await windowOf({ type: type === 'images' ? 'screenshot' : 'paste', from });}rows = rows.filter(_ofType(type));if (lang) rows = rows.filter((p) => (p.language || 'text') === lang);@@@ -122,10 +124,10 @@ async function browse(query = {}, ctx = {}) {windowed = true;}const pages = Math.max(1, Math.ceil(total / PER_PAGE));-- return { pastes, total, page, pages, perPage: PER_PAGE, q, sort, lang, type, windowed, windowSize: WINDOW };++ return { pastes, total, page, pages, perPage: PER_PAGE, q, sort, lang, type, from, windowed, windowSize: WINDOW };}/** For tests and a hot reload after a write. */--function reset() { _recent = { at: 0, promise: null, pastes: [] }; _byType.clear(); }++function reset() { _windows.clear(); }module.exports = { recent, recentOf, latest, trending, related, languages, browse, normalizeBrowseQuery, reset, PER_PAGE, WINDOW };@@@ -212,6 +212,9 @@ async function listPastes(db, opts = {}) {// hit) would give their content away without the read that burns them.else if (!opts.includeHidden) where.push("visibility = 'public' AND burn_after_read = 0");if (opts.origin === 'user' || opts.origin === 'ai' || opts.origin === 'imported') { where.push('origin = ?'); params.push(opts.origin); }++ // 'people': everything but AI pastes (what people wrote or imported), the shelves' default since AI moment pastes++ // outnumbered them three to one (2026-10-22: 890 AI, 307 by people).++ else if (opts.origin === 'people') where.push("origin <> 'ai'");if (opts.ownerSubject !== undefined) { where.push('owner_subject = ?'); params.push(opts.ownerSubject); }if (opts.type === 'paste' || opts.type === 'screenshot') { where.push('type = ?'); params.push(opts.type); }if (opts.search) {@@@ -88,6 +88,7 @@ function pagerHref(state, page) {if (state.sort && state.sort !== 'new') q.set('sort', state.sort);if (state.lang) q.set('lang', state.lang);if (state.type && state.type !== 'pastes') q.set('type', state.type);++ if (state.from && state.from !== 'people') q.set('from', state.from);if (page > 1) q.set('page', String(page));const s = q.toString();return `/pastes${s ? `?${s}` : ''}`;@@@ -182,7 +183,7 @@ function homeFaq() {</section>`;}--function homePage({ latest, trending, languages, user, spaces = [], roadmap = [], activity = [], counts = {} }) {++function homePage({ latest, trending, fromStreams = [], languages, user, spaces = [], roadmap = [], activity = [], counts = {} }) {const body = showcase.hero({eyebrow: 'OpenVibe.Community',title: 'Share it with a link.', accent: 'Talk about it here.',@@@ -205,6 +206,12 @@ function homePage({ latest, trending, languages, user, spaces = [], roadmap = []<div class="section-head"><h2>Most viewed</h2><a class="more" href="/pastes?sort=views">By views <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>${cardGrid(trending, 'Nothing trending yet.')}</section>++${fromStreams.length ? `++<section class="section" id="from-streams">++ <div class="section-head"><h2>From streams</h2><a class="more" href="/pastes?from=streams">All from streams <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>++ <p class="muted">Moments from streams on OpenVibe.Live, written up by OpenVibe.AI, each linking back to the stream.</p>++ ${cardGrid(fromStreams, '')}++</section>` : ''}${forumShelf(spaces)}${roadmapShelf(roadmap)}${pulseShelf(activity)}@@@ -266,35 +273,36 @@ function updatesPage() {// ── Browse ───────────────────────────────────────────────────function browsePage(result, languages) {-- const { pastes, total, page, pages, q, sort, lang, type = 'pastes', windowed, windowSize } = result;++ const { pastes, total, page, pages, q, sort, lang, type = 'pastes', from = 'people', windowed, windowSize } = result;const titleBits = [];if (q) titleBits.push(`“${q}”`);if (lang) titleBits.push(languageLabel(lang));titleBits.push(sort === 'views' ? 'most viewed' : 'newest');const title = `Pastes — ${titleBits.join(', ')}${page > 1 ? ` (page ${page})` : ''}`;-- const canonical = pagerHref({ q, sort, lang, type }, page);++ const canonical = pagerHref({ q, sort, lang, type, from }, page);const opt = (id, label) => `<option value="${esc(id)}"${id === (lang || '') ? ' selected' : ''}>${esc(label)}</option>`;const langOptions = [opt('', 'All languages')].concat((languages || []).map((l) => opt(l.language, `${languageLabel(l.language)} (${l.count})`)));if (lang && !(languages || []).some((l) => l.language === lang)) langOptions.push(opt(lang, languageLabel(lang)));const pager = pages > 1 ? `<nav class="pager" aria-label="Pages">-- ${page > 1 ? `<a rel="prev" href="${esc(pagerHref({ q, sort, lang, type }, page - 1))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newer</a>` : '<span></span>'}++ ${page > 1 ? `<a rel="prev" href="${esc(pagerHref({ q, sort, lang, type, from }, page - 1))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newer</a>` : '<span></span>'}<span class="pager-info">Page ${page} of ${pages}</span>-- ${page < pages ? `<a rel="next" href="${esc(pagerHref({ q, sort, lang, type }, page + 1))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}++ ${page < pages ? `<a rel="next" href="${esc(pagerHref({ q, sort, lang, type, from }, page + 1))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : '';const body = `<header class="page-head"><h1>Pastes</h1>-- <p class="muted">${num(total)} ${total === 1 ? 'paste' : 'pastes'}${windowed ? ` in the latest ${windowSize}` : ''}${q ? ` matching “${esc(q)}”` : ''}</p>++ <p class="muted">${num(total)} ${total === 1 ? 'paste' : 'pastes'}${from === 'streams' ? ' from streams, written up by OpenVibe.AI' : from === 'people' ? ' by people' : ''}${windowed ? ` in the latest ${windowSize}` : ''}${q ? ` matching “${esc(q)}”` : ''}</p></header><form class="filters" method="get" action="/pastes" role="search" data-browse><label class="search"><i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i><input type="search" name="q" value="${esc(q)}" placeholder="Search titles and content" aria-label="Search pastes"></label>++ <label>From <select name="from"><option value="people"${from === 'people' ? ' selected' : ''}>People</option><option value="streams"${from === 'streams' ? ' selected' : ''}>Streams (AI)</option><option value="all"${from === 'all' ? ' selected' : ''}>Everyone</option></select></label><label>Show <select name="type"><option value="pastes"${type === 'pastes' ? ' selected' : ''}>Pastes</option><option value="images"${type === 'images' ? ' selected' : ''}>Images</option><option value="all"${type === 'all' ? ' selected' : ''}>All</option></select></label><label>Sort <select name="sort"><option value="new"${sort === 'new' ? ' selected' : ''}>Newest</option><option value="views"${sort === 'views' ? ' selected' : ''}>Most viewed</option></select></label><label>Language <select name="lang">${langOptions.join('')}</select></label><button class="btn" type="submit">Apply</button>-- ${(q || lang || sort !== 'new') ? `<a class="btn btn-ghost" href="/pastes">Clear</a>` : ''}++ ${(q || lang || sort !== 'new' || from !== 'people') ? `<a class="btn btn-ghost" href="/pastes">Clear</a>` : ''}</form><section id="results" data-results>${cardGrid(pastes, q ? 'No pastes match that search.' : 'No pastes yet.')}@@@ -115,6 +115,8 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual(bad.status, 400);const onlyAi = (await call('/api/pastes?origin=ai')).json();assert.ok(onlyAi.pastes.length >= 1 && onlyAi.pastes.every((x) => x.origin === 'ai'));++ const people = (await call('/api/pastes?origin=people&limit=200')).json();++ assert.ok(people.pastes.length >= 1 && people.pastes.every((x) => x.origin !== 'ai'), 'origin=people: every paste but the AI ones');const browserMeta = await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { content: 'meta?', metadata: { ai_moment: true } } });assert.strictEqual(browserMeta.json().paste.metadata, null, 'browsers cannot set metadata');});nnew file mode 100644@@@ -0,0 +1,62 @@++'use strict';++// People's pastes and the moment pastes from streams, apart (plan 10-22: 890 AI pastes beside 307 by people buried what++// people wrote). The home's latest and most viewed are people's, with a "From streams" shelf of its own; /pastes lists++// people's by default, from=streams the AI ones and from=all both; the store's 'people' is every origin but AI.++const assert = require('assert');++const source = require('../server/pastes/source');++const catalog = require('../server/pastes/catalog');++const pages = require('../server/render/pages');++++const at = (i) => new Date(Date.UTC(2026, 9, 1) + i * 60_000).toISOString();++const rows = [++ { slug: 'ai-1', origin: 'ai', type: 'paste', title: 'Moment one', visibility: 'public', views: 500, created_at: at(30) },++ { slug: 'ai-2', origin: 'ai', type: 'paste', title: 'Moment two', visibility: 'public', views: 400, created_at: at(31) },++ { slug: 'me-1', origin: 'user', type: 'paste', title: 'My script', visibility: 'public', views: 5, created_at: at(10) },++ { slug: 'imp-1', origin: 'imported', type: 'paste', title: 'Imported log', visibility: 'public', views: 9, created_at: at(11) },++].sort((a, b) => b.created_at.localeCompare(a.created_at));++const asked = [];++source.use({++ list: async (_viewer, q) => {++ asked.push(q.origin || 'any');++ const keep = (p) => (!q.type || p.type === q.type) && (!q.origin || (q.origin === 'people' ? p.origin !== 'ai' : p.origin === q.origin));++ const found = rows.filter(keep);++ return { pastes: found.slice(q.offset || 0, (q.offset || 0) + q.limit), total: found.length };++ },++});++++let passed = 0;++const check = async (name, fn) => { catalog.reset(); await fn(); passed++; console.log(' ✓', name); };++++(async () => {++ await check('latest and most viewed by people leave AI pastes out; from streams is only them; all is everything', async () => {++ assert.deepStrictEqual((await catalog.latest(12, { from: 'people' })).map((p) => p.slug), ['imp-1', 'me-1']);++ assert.deepStrictEqual((await catalog.trending(8, { from: 'people' })).map((p) => p.slug), ['imp-1', 'me-1']);++ assert.deepStrictEqual((await catalog.latest(8, { from: 'streams' })).map((p) => p.slug), ['ai-2', 'ai-1']);++ assert.strictEqual((await catalog.latest(12)).length, 4, 'the default (the sitemap and feeds) is every public paste');++ assert.deepStrictEqual((await catalog.latest(3, { from: 'nonsense' })).length, 3, 'an unknown origin is everything');++ });++++ await check('/pastes lists people\'s by default, and from=streams or from=all on request, in the store and the window', async () => {++ const people = await catalog.browse({});++ assert.deepStrictEqual([people.from, people.pastes.map((p) => p.slug)], ['people', ['imp-1', 'me-1']]);++ assert.deepStrictEqual((await catalog.browse({ from: 'streams' })).pastes.map((p) => p.slug), ['ai-2', 'ai-1']);++ assert.strictEqual((await catalog.browse({ from: 'all' })).total, 4);++ assert.deepStrictEqual((await catalog.browse({ from: 'streams', sort: 'views' })).pastes.map((p) => p.slug), ['ai-1', 'ai-2'], 'the windowed path keeps the origin too');++ assert.ok(asked.includes('people') && asked.includes('ai'));++ });++++ await check('the home has a From streams shelf; the browse page says whose pastes and keeps it in its links', async () => {++ const home = pages.homePage({ latest: [], trending: [], fromStreams: [rows[0]], languages: [] });++ assert.match(home, /id="from-streams"/);++ assert.match(home, /href="\/pastes\?from=streams"/);++ assert.doesNotMatch(pages.homePage({ latest: [], trending: [], languages: [] }), /id="from-streams"/, 'no shelf without any');++ const out = await catalog.browse({ from: 'streams' });++ const page = pages.browsePage({ ...out, pages: 3 }, []);++ assert.match(page, /from streams, written up by OpenVibe\.AI/);++ assert.match(page, /<option value="streams" selected>/);++ assert.match(page, /href="\/pastes\?from=streams&page=2"/, 'the pager keeps whose pastes');++ assert.match(pages.browsePage({ ...(await catalog.browse({})), pages: 1 }, []), /pastes by people/);++ });++++ console.log(`pastes from streams: ${passed} checks passed`);++})().catch((e) => { console.error(e); process.exit(1); });
@@ -96,7 +96,7 @@
@@ -96,7 +96,7 @@
results.classList.remove('loading');
results.classList.remove('loading');
}).catch(function () { location.href = url; });
}).catch(function () { location.href = url; });
}
}
- browseForm.addEventListener('submit', function (e) { e.preventDefault(); var qs = new URLSearchParams(new FormData(browseForm)); ['sort', 'lang', 'q'].forEach(function (k) { if (!qs.get(k) || (k === 'sort' && qs.get(k) === 'new')) qs.delete(k); }); var s = qs.toString(); load('/pastes' + (s ? '?' + s : ''), true); }, { signal });
+ browseForm.addEventListener('submit', function (e) { e.preventDefault(); var qs = new URLSearchParams(new FormData(browseForm)); ['sort', 'lang', 'q', 'from'].forEach(function (k) { if (!qs.get(k) || (k === 'sort' && qs.get(k) === 'new') || (k === 'from' && qs.get(k) === 'people')) qs.delete(k); }); var s = qs.toString(); load('/pastes' + (s ? '?' + s : ''), true); }, { signal });
$$('select', browseForm).forEach(function (sel) { sel.addEventListener('change', function () { browseForm.requestSubmit ? browseForm.requestSubmit() : browseForm.dispatchEvent(new Event('submit', { cancelable: true })); }, { signal }); });
$$('select', browseForm).forEach(function (sel) { sel.addEventListener('change', function () { browseForm.requestSubmit ? browseForm.requestSubmit() : browseForm.dispatchEvent(new Event('submit', { cancelable: true })); }, { signal }); });
results.addEventListener('click', function (e) { var a = e.target.closest ? e.target.closest('.pager a') : null; if (!a) return; e.preventDefault(); load(a.getAttribute('href'), true); window.scrollTo({ top: results.offsetTop - 80, behavior: 'smooth' }); }, { signal });
results.addEventListener('click', function (e) { var a = e.target.closest ? e.target.closest('.pager a') : null; if (!a) return; e.preventDefault(); load(a.getAttribute('href'), true); window.scrollTo({ top: results.offsetTop - 80, behavior: 'smooth' }); }, { signal });
window.addEventListener('popstate', function (e) { if (e.state && e.state.browse) load(e.state.browse, false); else if (location.pathname === '/pastes') load(location.pathname + location.search, false); }, { signal });
window.addEventListener('popstate', function (e) { if (e.state && e.state.browse) load(e.state.browse, false); else if (location.pathname === '/pastes') load(location.pathname + location.search, false); }, { signal });
@@ -326,8 +326,9 @@ async function createApp(opts = {}) {
@@ -326,8 +326,9 @@ async function createApp(opts = {}) {
}
}
app.get('/', withUser, wrap(async (req, res) => {
app.get('/', withUser, wrap(async (req, res) => {
- const [latest, trending, languages, extras] = await Promise.all([catalog.latest(12), catalog.trending(8), catalog.languages(), readHomeExtras()]);
- html(res, pages.homePage({ latest, trending, languages, user: req.user, ...extras }));
+ // People's pastes lead; OpenVibe.AI's moment pastes from streams have their own shelf.
+ const [latest, trending, fromStreams, languages, extras] = await Promise.all([catalog.latest(12, { from: 'people' }), catalog.trending(8, { from: 'people' }), catalog.latest(8, { from: 'streams' }), catalog.languages(), readHomeExtras()]);
+ html(res, pages.homePage({ latest, trending, fromStreams, languages, user: req.user, ...extras }));
}));
}));
app.get('/updates', (req, res) => html(res, pages.updatesPage()));
app.get('/updates', (req, res) => html(res, pages.updatesPage()));
@@ -21,42 +21,41 @@ const WINDOW = 200; // page cap for one window query
@@ -21,42 +21,41 @@ const WINDOW = 200; // page cap for one window query
const LOCAL_TTL_MS = 2000;
const LOCAL_TTL_MS = 2000;
const PER_PAGE = 24;
const PER_PAGE = 24;
-let _recent = { at: 0, promise: null, pastes: [] };
-const _byType = new Map(); // 'paste' | 'screenshot' → { at, promise, pastes }
+// One cached window per (type, from): type 'paste' | 'screenshot' | null (both), from 'people' | 'streams' | 'all'.
+const _windows = new Map(); // key → { at, promise, pastes }
+// Where a paste comes from: people (written or imported by someone) or streams (OpenVibe.AI's moment pastes from Live).
+const FROM = { people: 'people', streams: 'ai', all: undefined };
function _textOnlyPublic(rows) {
function _textOnlyPublic(rows) {
return (rows || []).filter((p) => p && p.slug && (p.visibility === 'public' || p.visibility == null));
return (rows || []).filter((p) => p && p.slug && (p.visibility === 'public' || p.visibility == null));
}
}
-/** The latest public pastes (cached). Never throws — an unavailable store yields the stale list or []. */
-async function recent() {
+/** The latest public pastes of one type and origin (cached). Never throws: an unavailable store yields the stale list or []. */
+async function windowOf({ type = null, from = 'all' } = {}) {
+ const key = `${type || 'any'}:${from}`;
const now = Date.now();
const now = Date.now();
- if (_recent.promise) return _recent.promise;
- if (now - _recent.at < LOCAL_TTL_MS) return _recent.pastes;
- _recent.promise = source.listPastes({ limit: WINDOW, offset: 0 })
- .then((out) => { _recent = { at: Date.now(), promise: null, pastes: _textOnlyPublic(out && out.pastes) }; return _recent.pastes; })
- .catch((err) => { console.warn('[Catalog] recent pastes unavailable:', err.message); _recent.promise = null; _recent.at = Date.now() - LOCAL_TTL_MS + 5000; return _recent.pastes; });
- return _recent.promise;
-}
-
-/** The latest public pastes of one type, 'paste' (text) or 'screenshot' (cached like recent()). Never throws. */
-async function recentOf(type) {
- const now = Date.now();
- const cur = _byType.get(type) || { at: 0, promise: null, pastes: [] };
+ const cur = _windows.get(key) || { at: 0, promise: null, pastes: [] };
if (cur.promise) return cur.promise;
if (cur.promise) return cur.promise;
if (now - cur.at < LOCAL_TTL_MS) return cur.pastes;
if (now - cur.at < LOCAL_TTL_MS) return cur.pastes;
- cur.promise = source.listPastes({ limit: WINDOW, offset: 0, type })
- .then((out) => { _byType.set(type, { at: Date.now(), promise: null, pastes: _textOnlyPublic(out && out.pastes) }); return _byType.get(type).pastes; })
- .catch((err) => { console.warn(`[Catalog] recent ${type}s unavailable:`, err.message); _byType.set(type, { at: Date.now() - LOCAL_TTL_MS + 5000, promise: null, pastes: cur.pastes }); return cur.pastes; });
- _byType.set(type, cur);
+ const query = { limit: WINDOW, offset: 0, ...(type ? { type } : {}), ...(FROM[from] ? { origin: FROM[from] } : {}) };
+ cur.promise = source.listPastes(query)
+ .then((out) => { _windows.set(key, { at: Date.now(), promise: null, pastes: _textOnlyPublic(out && out.pastes) }); return _windows.get(key).pastes; })
+ .catch((err) => { console.warn(`[Catalog] recent pastes (${key}) unavailable:`, err.message); _windows.set(key, { at: Date.now() - LOCAL_TTL_MS + 5000, promise: null, pastes: cur.pastes }); return cur.pastes; });
+ _windows.set(key, cur);
return cur.promise;
return cur.promise;
}
}
+/** The latest public pastes, every type and origin (cached). Never throws. */
+const recent = () => windowOf();
+/** The latest public pastes of one type, 'paste' (text) or 'screenshot' (cached like recent()). Never throws. */
+const recentOf = (type) => windowOf({ type });
+
function byViews(a, b) { return (b.views || 0) - (a.views || 0) || (b.likes || 0) - (a.likes || 0) || String(b.created_at).localeCompare(String(a.created_at)); }
function byViews(a, b) { return (b.views || 0) - (a.views || 0) || (b.likes || 0) - (a.likes || 0) || String(b.created_at).localeCompare(String(a.created_at)); }
function byNewest(a, b) { return String(b.created_at || '').localeCompare(String(a.created_at || '')); }
function byNewest(a, b) { return String(b.created_at || '').localeCompare(String(a.created_at || '')); }
-async function latest(n = 12) { return (await recent()).slice().sort(byNewest).slice(0, n); }
-async function trending(n = 8) { return (await recent()).slice().sort(byViews).slice(0, n); }
+const fromOf = (v) => (Object.prototype.hasOwnProperty.call(FROM, v) ? v : 'all');
+async function latest(n = 12, { from = 'all' } = {}) { return (await windowOf({ from: fromOf(from) })).slice().sort(byNewest).slice(0, n); }
+async function trending(n = 8, { from = 'all' } = {}) { return (await windowOf({ from: fromOf(from) })).slice().sort(byViews).slice(0, n); }
/** Pastes in the same language first, then the newest, never the paste itself. */
/** Pastes in the same language first, then the newest, never the paste itself. */
async function related(paste, n = 6) {
async function related(paste, n = 6) {
@@ -85,7 +84,9 @@ function normalizeBrowseQuery(query = {}) {
@@ -85,7 +84,9 @@ function normalizeBrowseQuery(query = {}) {
const page = Math.max(1, Math.min(parseInt(query.page, 10) || 1, 500));
const page = Math.max(1, Math.min(parseInt(query.page, 10) || 1, 500));
// What to list: text pastes (default), images (screenshots) or all of them.
// What to list: text pastes (default), images (screenshots) or all of them.
const type = ['all', 'pastes', 'images'].includes(query.type) ? query.type : (lang === 'image' ? 'images' : 'pastes');
const type = ['all', 'pastes', 'images'].includes(query.type) ? query.type : (lang === 'image' ? 'images' : 'pastes');
- return { q, sort, lang: type === 'images' && lang === 'image' ? '' : lang, page, type };
+ // Whose: people's pastes (the default), the moment pastes from streams, or both.
+ const from = ['people', 'streams', 'all'].includes(query.from) ? query.from : 'people';
+ return { q, sort, lang: type === 'images' && lang === 'image' ? '' : lang, page, type, from };
}
}
const _ofType = (type) => (p) => (type === 'all' ? true : type === 'images' ? p.type === 'screenshot' : p.type !== 'screenshot');
const _ofType = (type) => (p) => (type === 'all' ? true : type === 'images' ? p.type === 'screenshot' : p.type !== 'screenshot');
@@ -95,24 +96,25 @@ const _ofType = (type) => (p) => (type === 'all' ? true : type === 'images' ? p.
@@ -95,24 +96,25 @@ const _ofType = (type) => (p) => (type === 'all' ? true : type === 'images' ? p.
* sort and the language filter are answered from the recent window.
* sort and the language filter are answered from the recent window.
*/
*/
async function browse(query = {}, ctx = {}) {
async function browse(query = {}, ctx = {}) {
- const { q, sort, lang, page, type } = normalizeBrowseQuery(query);
+ const { q, sort, lang, page, type, from } = normalizeBrowseQuery(query);
+ const origin = FROM[from] ? { origin: FROM[from] } : {};
const offset = (page - 1) * PER_PAGE;
const offset = (page - 1) * PER_PAGE;
let pastes, total, windowed = false;
let pastes, total, windowed = false;
if (sort === 'new' && !lang && type === 'pastes') {
if (sort === 'new' && !lang && type === 'pastes') {
- const out = await source.listPastes({ limit: PER_PAGE, offset, search: q || undefined, type: 'paste' }, ctx);
+ const out = await source.listPastes({ limit: PER_PAGE, offset, search: q || undefined, type: 'paste', ...origin }, ctx);
pastes = _textOnlyPublic(out && out.pastes);
pastes = _textOnlyPublic(out && out.pastes);
total = Number(out && out.total) || pastes.length;
total = Number(out && out.total) || pastes.length;
} else {
} else {
let rows;
let rows;
if (q) {
if (q) {
- const out = await source.listPastes({ limit: WINDOW, offset: 0, search: q, type: 'paste' }, ctx);
+ const out = await source.listPastes({ limit: WINDOW, offset: 0, search: q, type: 'paste', ...origin }, ctx);
rows = _textOnlyPublic(out && out.pastes);
rows = _textOnlyPublic(out && out.pastes);
} else if (type === 'all') {
} else if (type === 'all') {
- const [text, images] = await Promise.all([recentOf('paste'), recentOf('screenshot')]);
+ const [text, images] = await Promise.all([windowOf({ type: 'paste', from }), windowOf({ type: 'screenshot', from })]);
rows = [...text, ...images];
rows = [...text, ...images];
} else {
} else {
- rows = await recentOf(type === 'images' ? 'screenshot' : 'paste');
+ rows = await windowOf({ type: type === 'images' ? 'screenshot' : 'paste', from });
}
}
rows = rows.filter(_ofType(type));
rows = rows.filter(_ofType(type));
if (lang) rows = rows.filter((p) => (p.language || 'text') === lang);
if (lang) rows = rows.filter((p) => (p.language || 'text') === lang);
@@ -122,10 +124,10 @@ async function browse(query = {}, ctx = {}) {
@@ -122,10 +124,10 @@ async function browse(query = {}, ctx = {}) {
windowed = true;
windowed = true;
}
}
const pages = Math.max(1, Math.ceil(total / PER_PAGE));
const pages = Math.max(1, Math.ceil(total / PER_PAGE));
- return { pastes, total, page, pages, perPage: PER_PAGE, q, sort, lang, type, windowed, windowSize: WINDOW };
+ return { pastes, total, page, pages, perPage: PER_PAGE, q, sort, lang, type, from, windowed, windowSize: WINDOW };
}
}
/** For tests and a hot reload after a write. */
/** For tests and a hot reload after a write. */
-function reset() { _recent = { at: 0, promise: null, pastes: [] }; _byType.clear(); }
+function reset() { _windows.clear(); }
module.exports = { recent, recentOf, latest, trending, related, languages, browse, normalizeBrowseQuery, reset, PER_PAGE, WINDOW };
module.exports = { recent, recentOf, latest, trending, related, languages, browse, normalizeBrowseQuery, reset, PER_PAGE, WINDOW };
@@ -212,6 +212,9 @@ async function listPastes(db, opts = {}) {
@@ -212,6 +212,9 @@ async function listPastes(db, opts = {}) {
// hit) would give their content away without the read that burns them.
// hit) would give their content away without the read that burns them.
else if (!opts.includeHidden) where.push("visibility = 'public' AND burn_after_read = 0");
else if (!opts.includeHidden) where.push("visibility = 'public' AND burn_after_read = 0");
if (opts.origin === 'user' || opts.origin === 'ai' || opts.origin === 'imported') { where.push('origin = ?'); params.push(opts.origin); }
if (opts.origin === 'user' || opts.origin === 'ai' || opts.origin === 'imported') { where.push('origin = ?'); params.push(opts.origin); }
+ // 'people': everything but AI pastes (what people wrote or imported), the shelves' default since AI moment pastes
+ // outnumbered them three to one (2026-10-22: 890 AI, 307 by people).
+ else if (opts.origin === 'people') where.push("origin <> 'ai'");
if (opts.ownerSubject !== undefined) { where.push('owner_subject = ?'); params.push(opts.ownerSubject); }
if (opts.ownerSubject !== undefined) { where.push('owner_subject = ?'); params.push(opts.ownerSubject); }
if (opts.type === 'paste' || opts.type === 'screenshot') { where.push('type = ?'); params.push(opts.type); }
if (opts.type === 'paste' || opts.type === 'screenshot') { where.push('type = ?'); params.push(opts.type); }
if (opts.search) {
if (opts.search) {
@@ -88,6 +88,7 @@ function pagerHref(state, page) {
@@ -88,6 +88,7 @@ function pagerHref(state, page) {
if (state.sort && state.sort !== 'new') q.set('sort', state.sort);
if (state.sort && state.sort !== 'new') q.set('sort', state.sort);
if (state.lang) q.set('lang', state.lang);
if (state.lang) q.set('lang', state.lang);
if (state.type && state.type !== 'pastes') q.set('type', state.type);
if (state.type && state.type !== 'pastes') q.set('type', state.type);
+ if (state.from && state.from !== 'people') q.set('from', state.from);
if (page > 1) q.set('page', String(page));
if (page > 1) q.set('page', String(page));
const s = q.toString();
const s = q.toString();
return `/pastes${s ? `?${s}` : ''}`;
return `/pastes${s ? `?${s}` : ''}`;
@@ -182,7 +183,7 @@ function homeFaq() {
@@ -182,7 +183,7 @@ function homeFaq() {
</section>`;
</section>`;
}
}
-function homePage({ latest, trending, languages, user, spaces = [], roadmap = [], activity = [], counts = {} }) {
+function homePage({ latest, trending, fromStreams = [], languages, user, spaces = [], roadmap = [], activity = [], counts = {} }) {
const body = showcase.hero({
const body = showcase.hero({
eyebrow: 'OpenVibe.Community',
eyebrow: 'OpenVibe.Community',
title: 'Share it with a link.', accent: 'Talk about it here.',
title: 'Share it with a link.', accent: 'Talk about it here.',
@@ -205,6 +206,12 @@ function homePage({ latest, trending, languages, user, spaces = [], roadmap = []
@@ -205,6 +206,12 @@ function homePage({ latest, trending, languages, user, spaces = [], roadmap = []
<div class="section-head"><h2>Most viewed</h2><a class="more" href="/pastes?sort=views">By views <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
<div class="section-head"><h2>Most viewed</h2><a class="more" href="/pastes?sort=views">By views <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
${cardGrid(trending, 'Nothing trending yet.')}
${cardGrid(trending, 'Nothing trending yet.')}
</section>
</section>
+${fromStreams.length ? `
+<section class="section" id="from-streams">
+ <div class="section-head"><h2>From streams</h2><a class="more" href="/pastes?from=streams">All from streams <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
+ <p class="muted">Moments from streams on OpenVibe.Live, written up by OpenVibe.AI, each linking back to the stream.</p>
+ ${cardGrid(fromStreams, '')}
+</section>` : ''}
${forumShelf(spaces)}
${forumShelf(spaces)}
${roadmapShelf(roadmap)}
${roadmapShelf(roadmap)}
${pulseShelf(activity)}
${pulseShelf(activity)}
@@ -266,35 +273,36 @@ function updatesPage() {
@@ -266,35 +273,36 @@ function updatesPage() {
// ── Browse ───────────────────────────────────────────────────
// ── Browse ───────────────────────────────────────────────────
function browsePage(result, languages) {
function browsePage(result, languages) {
- const { pastes, total, page, pages, q, sort, lang, type = 'pastes', windowed, windowSize } = result;
+ const { pastes, total, page, pages, q, sort, lang, type = 'pastes', from = 'people', windowed, windowSize } = result;
const titleBits = [];
const titleBits = [];
if (q) titleBits.push(`“${q}”`);
if (q) titleBits.push(`“${q}”`);
if (lang) titleBits.push(languageLabel(lang));
if (lang) titleBits.push(languageLabel(lang));
titleBits.push(sort === 'views' ? 'most viewed' : 'newest');
titleBits.push(sort === 'views' ? 'most viewed' : 'newest');
const title = `Pastes — ${titleBits.join(', ')}${page > 1 ? ` (page ${page})` : ''}`;
const title = `Pastes — ${titleBits.join(', ')}${page > 1 ? ` (page ${page})` : ''}`;
- const canonical = pagerHref({ q, sort, lang, type }, page);
+ const canonical = pagerHref({ q, sort, lang, type, from }, page);
const opt = (id, label) => `<option value="${esc(id)}"${id === (lang || '') ? ' selected' : ''}>${esc(label)}</option>`;
const opt = (id, label) => `<option value="${esc(id)}"${id === (lang || '') ? ' selected' : ''}>${esc(label)}</option>`;
const langOptions = [opt('', 'All languages')].concat((languages || []).map((l) => opt(l.language, `${languageLabel(l.language)} (${l.count})`)));
const langOptions = [opt('', 'All languages')].concat((languages || []).map((l) => opt(l.language, `${languageLabel(l.language)} (${l.count})`)));
if (lang && !(languages || []).some((l) => l.language === lang)) langOptions.push(opt(lang, languageLabel(lang)));
if (lang && !(languages || []).some((l) => l.language === lang)) langOptions.push(opt(lang, languageLabel(lang)));
const pager = pages > 1 ? `<nav class="pager" aria-label="Pages">
const pager = pages > 1 ? `<nav class="pager" aria-label="Pages">
- ${page > 1 ? `<a rel="prev" href="${esc(pagerHref({ q, sort, lang, type }, page - 1))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newer</a>` : '<span></span>'}
+ ${page > 1 ? `<a rel="prev" href="${esc(pagerHref({ q, sort, lang, type, from }, page - 1))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newer</a>` : '<span></span>'}
<span class="pager-info">Page ${page} of ${pages}</span>
<span class="pager-info">Page ${page} of ${pages}</span>
- ${page < pages ? `<a rel="next" href="${esc(pagerHref({ q, sort, lang, type }, page + 1))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}
+ ${page < pages ? `<a rel="next" href="${esc(pagerHref({ q, sort, lang, type, from }, page + 1))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}
</nav>` : '';
</nav>` : '';
const body = `
const body = `
<header class="page-head">
<header class="page-head">
<h1>Pastes</h1>
<h1>Pastes</h1>
- <p class="muted">${num(total)} ${total === 1 ? 'paste' : 'pastes'}${windowed ? ` in the latest ${windowSize}` : ''}${q ? ` matching “${esc(q)}”` : ''}</p>
+ <p class="muted">${num(total)} ${total === 1 ? 'paste' : 'pastes'}${from === 'streams' ? ' from streams, written up by OpenVibe.AI' : from === 'people' ? ' by people' : ''}${windowed ? ` in the latest ${windowSize}` : ''}${q ? ` matching “${esc(q)}”` : ''}</p>
</header>
</header>
<form class="filters" method="get" action="/pastes" role="search" data-browse>
<form class="filters" method="get" action="/pastes" role="search" data-browse>
<label class="search"><i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i><input type="search" name="q" value="${esc(q)}" placeholder="Search titles and content" aria-label="Search pastes"></label>
<label class="search"><i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i><input type="search" name="q" value="${esc(q)}" placeholder="Search titles and content" aria-label="Search pastes"></label>
+ <label>From <select name="from"><option value="people"${from === 'people' ? ' selected' : ''}>People</option><option value="streams"${from === 'streams' ? ' selected' : ''}>Streams (AI)</option><option value="all"${from === 'all' ? ' selected' : ''}>Everyone</option></select></label>
<label>Show <select name="type"><option value="pastes"${type === 'pastes' ? ' selected' : ''}>Pastes</option><option value="images"${type === 'images' ? ' selected' : ''}>Images</option><option value="all"${type === 'all' ? ' selected' : ''}>All</option></select></label>
<label>Show <select name="type"><option value="pastes"${type === 'pastes' ? ' selected' : ''}>Pastes</option><option value="images"${type === 'images' ? ' selected' : ''}>Images</option><option value="all"${type === 'all' ? ' selected' : ''}>All</option></select></label>
<label>Sort <select name="sort"><option value="new"${sort === 'new' ? ' selected' : ''}>Newest</option><option value="views"${sort === 'views' ? ' selected' : ''}>Most viewed</option></select></label>
<label>Sort <select name="sort"><option value="new"${sort === 'new' ? ' selected' : ''}>Newest</option><option value="views"${sort === 'views' ? ' selected' : ''}>Most viewed</option></select></label>
<label>Language <select name="lang">${langOptions.join('')}</select></label>
<label>Language <select name="lang">${langOptions.join('')}</select></label>
<button class="btn" type="submit">Apply</button>
<button class="btn" type="submit">Apply</button>
- ${(q || lang || sort !== 'new') ? `<a class="btn btn-ghost" href="/pastes">Clear</a>` : ''}
+ ${(q || lang || sort !== 'new' || from !== 'people') ? `<a class="btn btn-ghost" href="/pastes">Clear</a>` : ''}
</form>
</form>
<section id="results" data-results>
<section id="results" data-results>
${cardGrid(pastes, q ? 'No pastes match that search.' : 'No pastes yet.')}
${cardGrid(pastes, q ? 'No pastes match that search.' : 'No pastes yet.')}
@@ -115,6 +115,8 @@ const { boot, check, done } = require('./helpers/app');
@@ -115,6 +115,8 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(bad.status, 400);
assert.strictEqual(bad.status, 400);
const onlyAi = (await call('/api/pastes?origin=ai')).json();
const onlyAi = (await call('/api/pastes?origin=ai')).json();
assert.ok(onlyAi.pastes.length >= 1 && onlyAi.pastes.every((x) => x.origin === 'ai'));
assert.ok(onlyAi.pastes.length >= 1 && onlyAi.pastes.every((x) => x.origin === 'ai'));
+ const people = (await call('/api/pastes?origin=people&limit=200')).json();
+ assert.ok(people.pastes.length >= 1 && people.pastes.every((x) => x.origin !== 'ai'), 'origin=people: every paste but the AI ones');
const browserMeta = await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { content: 'meta?', metadata: { ai_moment: true } } });
const browserMeta = await call('/api/pastes', { method: 'POST', cookie: alexJwt, json: { content: 'meta?', metadata: { ai_moment: true } } });
assert.strictEqual(browserMeta.json().paste.metadata, null, 'browsers cannot set metadata');
assert.strictEqual(browserMeta.json().paste.metadata, null, 'browsers cannot set metadata');
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
+'use strict';
+// People's pastes and the moment pastes from streams, apart (plan 10-22: 890 AI pastes beside 307 by people buried what
+// people wrote). The home's latest and most viewed are people's, with a "From streams" shelf of its own; /pastes lists
+// people's by default, from=streams the AI ones and from=all both; the store's 'people' is every origin but AI.
+const assert = require('assert');
+const source = require('../server/pastes/source');
+const catalog = require('../server/pastes/catalog');
+const pages = require('../server/render/pages');
+
+const at = (i) => new Date(Date.UTC(2026, 9, 1) + i * 60_000).toISOString();
+const rows = [
+ { slug: 'ai-1', origin: 'ai', type: 'paste', title: 'Moment one', visibility: 'public', views: 500, created_at: at(30) },
+ { slug: 'ai-2', origin: 'ai', type: 'paste', title: 'Moment two', visibility: 'public', views: 400, created_at: at(31) },
+ { slug: 'me-1', origin: 'user', type: 'paste', title: 'My script', visibility: 'public', views: 5, created_at: at(10) },
+ { slug: 'imp-1', origin: 'imported', type: 'paste', title: 'Imported log', visibility: 'public', views: 9, created_at: at(11) },
+].sort((a, b) => b.created_at.localeCompare(a.created_at));
+const asked = [];
+source.use({
+ list: async (_viewer, q) => {
+ asked.push(q.origin || 'any');
+ const keep = (p) => (!q.type || p.type === q.type) && (!q.origin || (q.origin === 'people' ? p.origin !== 'ai' : p.origin === q.origin));
+ const found = rows.filter(keep);
+ return { pastes: found.slice(q.offset || 0, (q.offset || 0) + q.limit), total: found.length };
+ },
+});
+
+let passed = 0;
+const check = async (name, fn) => { catalog.reset(); await fn(); passed++; console.log(' ✓', name); };
+
+(async () => {
+ await check('latest and most viewed by people leave AI pastes out; from streams is only them; all is everything', async () => {
+ assert.deepStrictEqual((await catalog.latest(12, { from: 'people' })).map((p) => p.slug), ['imp-1', 'me-1']);
+ assert.deepStrictEqual((await catalog.trending(8, { from: 'people' })).map((p) => p.slug), ['imp-1', 'me-1']);
+ assert.deepStrictEqual((await catalog.latest(8, { from: 'streams' })).map((p) => p.slug), ['ai-2', 'ai-1']);
+ assert.strictEqual((await catalog.latest(12)).length, 4, 'the default (the sitemap and feeds) is every public paste');
+ assert.deepStrictEqual((await catalog.latest(3, { from: 'nonsense' })).length, 3, 'an unknown origin is everything');
+ });
+
+ await check('/pastes lists people\'s by default, and from=streams or from=all on request, in the store and the window', async () => {
+ const people = await catalog.browse({});
+ assert.deepStrictEqual([people.from, people.pastes.map((p) => p.slug)], ['people', ['imp-1', 'me-1']]);
+ assert.deepStrictEqual((await catalog.browse({ from: 'streams' })).pastes.map((p) => p.slug), ['ai-2', 'ai-1']);
+ assert.strictEqual((await catalog.browse({ from: 'all' })).total, 4);
+ assert.deepStrictEqual((await catalog.browse({ from: 'streams', sort: 'views' })).pastes.map((p) => p.slug), ['ai-1', 'ai-2'], 'the windowed path keeps the origin too');
+ assert.ok(asked.includes('people') && asked.includes('ai'));
+ });
+
+ await check('the home has a From streams shelf; the browse page says whose pastes and keeps it in its links', async () => {
+ const home = pages.homePage({ latest: [], trending: [], fromStreams: [rows[0]], languages: [] });
+ assert.match(home, /id="from-streams"/);
+ assert.match(home, /href="\/pastes\?from=streams"/);
+ assert.doesNotMatch(pages.homePage({ latest: [], trending: [], languages: [] }), /id="from-streams"/, 'no shelf without any');
+ const out = await catalog.browse({ from: 'streams' });
+ const page = pages.browsePage({ ...out, pages: 3 }, []);
+ assert.match(page, /from streams, written up by OpenVibe\.AI/);
+ assert.match(page, /<option value="streams" selected>/);
+ assert.match(page, /href="\/pastes\?from=streams&page=2"/, 'the pager keeps whose pastes');
+ assert.match(pages.browsePage({ ...(await catalog.browse({})), pages: 1 }, []), /pastes by people/);
+ });
+
+ console.log(`pastes from streams: ${passed} checks passed`);
+})().catch((e) => { console.error(e); process.exit(1); });