openvibers/openvibe.network Commits

/my: a hero in your colour with balances and one-tap actions; On OpenVibe now (#627)

OP OpenVibers committed 6 days ago · dae8e02066 · 5 files, +216 −33

The owner (11-07): "make this page a little better … more clear and nice … front-face the best stuff we have to
offer … awesome design, desktop and mobile". The header is now a hero that wears your profile colour: your picture in
a ring of it, your name large, your OpenCoins and Vibes on frosted glass (the old Your money box), and actions for Go
live (Watch live for a viewer), Messages and Ask Vibe (open in place in the Dock), Your files, Codes, Wallet,
Notifications (with the count) and Settings. After Pick up where you left off, On OpenVibe now shows what the sites
themselves put forward (the frame.menu-card@1 cards from /api/frame: trending videos, new worlds, who is live), https
entries only, a card with none left out. Your numbers is its own box beside Your sites.

Changes

ddiff --git a/public/css/my.css b/public/css/my.css
iindex 7e77bb86..c63c5c61 100644
---- a/public/css/my.css
++++ b/public/css/my.css
@@@ -515,3 +515,91 @@ button.td-stat:hover span { color: var(--accent-light); }
.qr-note { margin-top: 14px; }
.qr-dialog { border-radius: 16px; }
}
++
++/* ── You (11-07; the owner: "front-face the best stuff we have to offer … awesome design, desktop and mobile"). The hero
++ wears your profile colour (--me, set by today.js from your profile): your picture in a ring of it, your balances on
++ frosted glass, and seven one-tap actions. Then On OpenVibe now: what the sites themselves put forward. ── */
++.me { --me: var(--accent); position: relative; isolation: isolate; overflow: hidden; display: grid; gap: 22px; margin: 4px 0 30px; padding: 28px;
++ border-radius: 28px; border: 1px solid color-mix(in srgb, var(--me) 34%, var(--border));
++ background:
++ radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--me) 30%, transparent) 0%, transparent 55%),
++ radial-gradient(90% 120% at 100% 100%, color-mix(in srgb, var(--accent) 18%, transparent) 0%, transparent 60%),
++ var(--bg-card); }
++.me::after { content: ""; position: absolute; z-index: -1; right: -120px; top: -160px; width: 420px; height: 420px; border-radius: 50%; pointer-events: none;
++ background: radial-gradient(closest-side, color-mix(in srgb, var(--me) 38%, transparent), transparent); opacity: .7; }
++.me [hidden] { display: none !important; }
++.me-top { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 20px 28px; }
++.me-who { display: flex; align-items: center; gap: 20px; min-width: 0; flex: 1 1 320px; }
++.me-face { flex: none; padding: 4px; border-radius: 50%; background: conic-gradient(from 210deg, var(--me), color-mix(in srgb, var(--me) 30%, var(--accent)), var(--me)); box-shadow: 0 10px 30px color-mix(in srgb, var(--me) 35%, transparent); }
++.me-face .avatar { display: block; width: 96px; height: 96px; border-radius: 50%; object-fit: cover; border: 4px solid var(--bg-card); background: var(--bg-card); }
++.me .info { min-width: 0; }
++.me .greeting { font-size: 13px; font-weight: 700; letter-spacing: .04em; color: color-mix(in srgb, var(--me) 55%, var(--text-secondary)); margin-bottom: 2px; }
++.me .greeting:empty, .me .badges:empty { display: none; }
++.me h1 { margin: 0; font-size: clamp(28px, 4.2vw, 42px); line-height: 1.08; font-weight: 850; letter-spacing: -.025em; overflow-wrap: anywhere; }
++.me .subtitle { margin-top: 4px; font-size: 14px; color: var(--text-secondary); }
++.me .badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
++.me-money { display: grid; gap: 8px; padding: 14px 18px; border-radius: 20px; min-width: 250px; flex: 0 1 auto;
++ background: color-mix(in srgb, var(--bg-primary, #0b0f1a) 42%, transparent); border: 1px solid color-mix(in srgb, var(--text-primary, #fff) 10%, transparent);
++ -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
++.me-bals { display: grid; gap: 6px; color: var(--text); text-decoration: none; }
++.me-bal { display: flex; align-items: baseline; gap: 8px; }
++.me-bal b { font-size: 26px; font-weight: 850; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
++.me-bal small { font-size: 13px; font-weight: 650; color: var(--text-secondary); }
++.me-bals:hover b { color: var(--accent-light); }
++.me-last { margin: 0; font-size: 12.5px; color: var(--text-muted); }
++.me-tip { justify-self: start; display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 650; color: var(--accent-light); text-decoration: none; }
++.me-tip:hover { text-decoration: underline; }
++.me-acts { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 10px; }
++.me-acts:empty { display: none; }
++.me-act { --a: var(--accent); position: relative; display: grid; justify-items: center; gap: 8px; padding: 14px 6px 12px; border-radius: 18px; min-width: 0;
++ background: color-mix(in srgb, var(--bg-primary, #0b0f1a) 38%, transparent); border: 1px solid color-mix(in srgb, var(--text-primary, #fff) 8%, transparent);
++ color: var(--text); text-decoration: none; transition: transform var(--ov-dur-2, .2s) var(--ov-ease-out, ease), border-color .2s, background .2s; }
++.me-act:hover, .me-act:focus-visible { transform: translateY(-2px); border-color: color-mix(in srgb, var(--a) 60%, transparent); background: color-mix(in srgb, var(--a) 12%, var(--bg-card)); }
++.me-act-ic { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 14px; font-size: 18px; color: var(--a); background: color-mix(in srgb, var(--a) 16%, transparent); }
++.me-act-t { max-width: 100%; font-size: 13px; font-weight: 700; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.me-act-n { position: absolute; top: 8px; right: calc(50% - 34px); min-width: 20px; padding: 1px 6px; border-radius: 999px; background: var(--live-red, #ef4444); color: #fff; font-size: 11px; font-weight: 800; text-align: center; box-shadow: 0 0 0 3px var(--bg-card); }
++.me-act[data-tone="live"] { --a: #ef4444; } .me-act[data-tone="chat"] { --a: #22c55e; } .me-act[data-tone="vibe"] { --a: #a855f7; }
++.me-act[data-tone="files"] { --a: #3b82f6; } .me-act[data-tone="codes"] { --a: #f59e0b; } .me-act[data-tone="bell"] { --a: #06b6d4; }
++.me-act[data-tone="gear"] { --a: #94a3b8; } .me-act[data-tone="wallet"] { --a: #eab308; }
++.me .badges .badge { font-size: 11px; padding: 3px 10px; border-radius: 999px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
++.td-net { align-items: start; }
++
++.td-net { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; }
++.td-net-card { gap: 6px; padding: 14px 14px 10px; }
++.td-net-h { display: flex; align-items: center; gap: 10px; padding-bottom: 6px; }
++.td-net-ht { display: grid; flex: 1; min-width: 0; }
++.td-net-ht b { font-size: 15px; font-weight: 800; }
++.td-net-ht small { font-size: 12px; color: var(--text-muted); }
++.td-net-card[data-site="media-hub"] .td-site-ic { color: #f472b6; background: color-mix(in srgb, #ec4899 16%, transparent); }
++.td-net-card[data-site="games"] .td-site-ic { color: #4ade80; background: color-mix(in srgb, #22c55e 16%, transparent); }
++.td-net-card[data-site="live"] .td-site-ic { color: #f87171; background: color-mix(in srgb, #ef4444 16%, transparent); }
++.td-net-list { list-style: none; margin: 0; padding: 0; display: grid; }
++.td-net-row { display: flex; align-items: center; gap: 12px; padding: 9px 6px; border-radius: 10px; color: var(--text); text-decoration: none; }
++.td-net-row:hover { background: color-mix(in srgb, var(--text-primary, #fff) 6%, transparent); }
++.td-net-list li + li .td-net-row { border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent); border-radius: 0; }
++.td-net-list li + li .td-net-row:hover { border-radius: 10px; }
++.td-net-i { flex: none; width: 22px; font-size: 15px; font-weight: 850; color: var(--text-muted); text-align: center; font-variant-numeric: tabular-nums; }
++.td-net-list li:first-child .td-net-i { color: var(--accent-light); }
++.td-net-t { display: grid; min-width: 0; }
++.td-net-t b { font-size: 14px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.td-net-t small { font-size: 12px; color: var(--text-muted); }
++.td-nums .td-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; }
++.td-nums .td-stat { display: grid; justify-items: start; gap: 2px; padding: 12px 14px; border-radius: 14px; background: color-mix(in srgb, var(--text-primary, #fff) 4%, transparent); text-align: left; }
++.td-nums .td-stat b { font-size: 1.5rem; }
++@media (min-width: 760px) { .td-grid { grid-template-columns: 1.4fr 1fr; } }
++@media (max-width: 860px) { .me-acts { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
++@media (max-width: 600px) {
++ .me { padding: 20px 16px 16px; border-radius: 24px; gap: 18px; margin-bottom: 22px; }
++ .me-who { gap: 14px; flex-basis: 100%; }
++ .me-face .avatar { width: 72px; height: 72px; border-width: 3px; }
++ .me-money { flex: 1 1 100%; min-width: 0; grid-template-columns: 1fr auto; align-items: center; padding: 12px 14px; }
++ .me-bals { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-column: 1 / -1; }
++ .me-bal { flex-wrap: wrap; gap: 0 6px; }
++ .me-bal b { font-size: 21px; }
++ .me-bal small { flex-basis: 100%; margin-left: 22px; }
++ .me-acts { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
++ .me-act { padding: 12px 2px 10px; border-radius: 16px; }
++ .me-act-ic { width: 40px; height: 40px; font-size: 16px; }
++ .me-act-t { font-size: 11.5px; }
++}
++@media (prefers-reduced-motion: reduce) { .me-act { transition: none; } .me-act:hover { transform: none; } }
ddiff --git a/public/js/my/profile.js b/public/js/my/profile.js
iindex 2e250dc0..47684695 100644
---- a/public/js/my/profile.js
++++ b/public/js/my/profile.js
@@@ -145,6 +145,10 @@ function renderHeader(user) {
? `<span class="badge badge-anon">anonymous</span>`
: (user.role && user.role !== 'user' ? `<span class="badge badge-${user.role}">${user.role.replace(/_/g, ' ')}</span>` : '');
if (user.anon_number) badges.innerHTML += `<span class="badge badge-anon">Anon #${user.anon_number}</span>`;
++ // The hero wears your profile colour, and its actions know whether you stream (today.js may have drawn them first).
++ const hero = document.getElementById('page-header');
++ if (hero && /^#[0-9a-f]{6}$/i.test(String(user.profile_color || ''))) hero.style.setProperty('--me', user.profile_color);
++ if (typeof renderActions === 'function') renderActions(globalThis.__tdUnread || null);
}
function renderProfile(user) {
ddiff --git a/public/js/my/today.js b/public/js/my/today.js
iindex d82d188a..779c8025 100644
---- a/public/js/my/today.js
++++ b/public/js/my/today.js
@@@ -18,6 +18,8 @@ const TD_SITES = {
host: ['Host', 'fa-server', 'https://openvibe.host'], deals: ['Deals', 'fa-tags', 'https://openvibe.deals'],
coupons: ['Coupons', 'fa-ticket', 'https://openvibe.coupons'], openre: ['Restream', 'fa-share-nodes', 'https://openre.stream'],
design: ['Design', 'fa-palette', 'https://openvibe.design'], report: ['Report', 'fa-chart-column', 'https://openvibe.report'],
++ 'media-hub': ['Video', 'fa-play', 'https://openvibe.video'], video: ['Video', 'fa-play', 'https://openvibe.video'],
++ download: ['Download', 'fa-folder', 'https://openvibe.download'], cam: ['Cam', 'fa-camera', 'https://openvibe.cam'],
};
// What to try next, most used across the network first, each with the reason it is worth a look.
const TD_TRY = [
@@@ -76,20 +78,69 @@ function renderNow(live, unread) {
const sub = document.getElementById('td-now-sub');
if (sub) sub.textContent = people.length ? `${people.length} of the ${tdNum(live.following)} you follow` : '';
++ // Notifications and Go live are the hero's actions (renderActions); this row says who else is live.
const out = [];
-- const count = unread && Number(unread.count);
-- if (count > 0) {
-- const b = tdEl('button', 'td-chip td-chip-hot', [tdIcon('fa-bell'), ` ${tdNum(count)} new notification${count === 1 ? '' : 's'}`], { type: 'button' });
-- b.addEventListener('click', () => { const bell = document.querySelector('[data-ov-bell], .openvibe-navbar-bell, #openvibe-notif-btn'); if (bell) bell.click(); else showSection('notifications'); });
-- out.push(b);
-- }
-- const role = currentUser && currentUser.role;
-- if (role && role !== 'user') out.push(tdEl('a', 'td-chip', [tdIcon('fa-tower-broadcast'), ' Go live'], { href: 'https://openvibe.live/broadcast' }));
if (!people.length && live && live.anyone_live > 0) out.push(tdEl('a', 'td-chip', [tdIcon('fa-circle-play'), ` ${tdNum(live.anyone_live)} live on OpenVibe now`], { href: 'https://openvibe.live/' }));
chips.replaceChildren(...out);
chips.hidden = out.length === 0;
}
++/**
++ * The hero's actions (11-07): one tap to what you do most, from this page. Messages and Ask Vibe open in place, in the
++ * Dock (navbar.js), and are plain links where it is not there; a count shows on Notifications when you have some.
++ */
++function renderActions(unread) {
++ const nav = document.getElementById('me-acts');
++ if (!nav) return;
++ const hero = document.getElementById('page-header');
++ const color = currentUser && /^#[0-9a-f]{6}$/i.test(String(currentUser.profile_color || '')) ? currentUser.profile_color : null;
++ if (hero && color && hero.style && hero.style.setProperty) hero.style.setProperty('--me', color);
++ const streamer = !!(currentUser && currentUser.role && currentUser.role !== 'user');
++ const count = unread && Number(unread.count) > 0 ? Number(unread.count) : 0;
++ const dock = (tab) => () => { try { const nb = globalThis.OpenVibeNavbar; if (nb && nb.openDock) { nb.openDock(tab); return true; } } catch (e) { /* the link works */ } return false; };
++ const acts = [
++ streamer ? ['Go live', 'fa-tower-broadcast', 'https://openvibe.live/broadcast', 'live'] : ['Watch live', 'fa-circle-play', 'https://openvibe.live/', 'live'],
++ ['Messages', 'fa-comment-dots', 'https://openvibe.chat/messages', 'chat', dock('chats')],
++ ['Ask Vibe', 'fa-wand-magic-sparkles', 'https://openvibe.chat/', 'vibe', dock('vibe')],
++ ['Your files', 'fa-folder', 'https://openvibe.download/files', 'files'],
++ ['Codes', 'fa-code', 'https://openvibe.codes/', 'codes'],
++ ['Wallet', 'fa-wallet', '/wallet', 'wallet'],
++ ['Notifications', 'fa-bell', '#notifications', 'bell', () => { const bell = document.querySelector('[data-ov-bell], .openvibe-navbar-bell, #openvibe-notif-btn'); if (bell) { bell.click(); return true; } showSection('notifications'); return true; }, count],
++ ['Settings', 'fa-gear', '#settings', 'gear'],
++ ];
++ nav.replaceChildren(...acts.map(([label, icon, href, tone, open, n]) => {
++ const a = tdEl('a', 'me-act', [tdEl('span', 'me-act-ic', [tdIcon(icon)]), tdEl('span', 'me-act-t', [label])], { href, 'data-tone': tone });
++ if (n) { a.append(tdEl('b', 'me-act-n', [n > 99 ? '99+' : String(n)])); a.setAttribute('aria-label', `${label}, ${n} new`); }
++ if (open) a.addEventListener('click', (e) => { if (e && (e.metaKey || e.ctrlKey || e.shiftKey)) return; if (open() && e && e.preventDefault) e.preventDefault(); });
++ return a;
++ }));
++}
++
++/**
++ * On OpenVibe now: what the sites themselves put forward (the menu cards, frame.menu-card@1 from /api/frame) — the
++ * week's trending videos, new worlds, who is live. Real entries only; a card with no entries stays out.
++ */
++function renderNet(frame) {
++ const box = document.getElementById('td-net');
++ const wrap = document.getElementById('td-net-cards');
++ if (!box || !wrap) return;
++ const cards = ((frame && frame.cards) || []).filter((c) => c && Array.isArray(c.items) && c.items.some((it) => tdSafeUrl(it.url))).slice(0, 3);
++ wrap.replaceChildren(...cards.map((c) => {
++ const site = TD_SITES[c.site] || [c.site || 'OpenVibe', 'fa-globe', null];
++ const rows = c.items.filter((it) => tdSafeUrl(it.url)).slice(0, 5);
++ const list = tdEl('ol', 'td-net-list', rows.map((it, i) => tdEl('li', '', [tdEl('a', 'td-net-row', [
++ tdEl('span', 'td-net-i', [String(i + 1)]),
++ tdEl('span', 'td-net-t', [tdEl('b', '', [String(it.title || '')]), it.meta ? tdEl('small', '', [String(it.meta)]) : null]),
++ ], { href: tdSafeUrl(it.url) })])));
++ const more = c.more && tdSafeUrl(c.more.url) ? tdEl('a', 'td-more', [String(c.more.label || 'More')], { href: tdSafeUrl(c.more.url) }) : null;
++ return tdEl('section', 'td-box td-net-card', [
++ tdEl('div', 'td-net-h', [tdEl('span', 'td-site-ic', [tdIcon(site[1])]), tdEl('span', 'td-net-ht', [tdEl('b', '', [String(c.title || site[0])]), tdEl('small', '', [site[0]])]), more]),
++ list,
++ ], { 'data-site': String(c.site || '') });
++ }));
++ box.hidden = cards.length === 0;
++}
++
/** Continue: the newest things you opened across the sites (Network history), one card per address. */
function renderContinue(hist) {
const box = document.getElementById('td-continue');
@@@ -165,6 +216,8 @@ function renderStuff(live, hist, nodes) {
add(nodes && nodes.nodes && nodes.nodes.filter((n) => !n.revoked_at).length, 'machines', () => { showSection('machines'); document.getElementById('settings').scrollIntoView({ behavior: 'smooth' }); });
add(hist && hist.total, 'pages in your history', () => { showSection('history'); document.getElementById('settings').scrollIntoView({ behavior: 'smooth' }); });
row.replaceChildren(...cells);
++ const box = document.getElementById('td-nums');
++ if (box) box.hidden = cells.length === 0;
}
async function loadToday() {
@@@ -172,7 +225,8 @@ async function loadToday() {
renderGreeting();
if (typeof isAnonSession === 'function' && isAnonSession()) return; // a guest identity keeps no history, follows or coins
const quiet = (p) => p.catch(() => null);
-- const [live, hist, coins, last, unread, sites, nodes] = await Promise.all([
++ renderActions(null);
++ const [live, hist, coins, last, unread, sites, nodes, frame] = await Promise.all([
quiet(apiFetch('/api/v1/me/live-follows')),
quiet(apiFetch('/api/history?limit=30')),
quiet(apiFetch('/api/coins/me?with=vibes')),
@@@ -180,7 +234,11 @@ async function loadToday() {
quiet(apiFetch('/api/notifications/unread-count')),
quiet(apiFetch('/api/themes/me/sites')),
quiet(apiFetch('/api/v1/me/nodes')),
++ quiet(apiFetch('/api/frame?host=openvibe.network')),
]);
++ globalThis.__tdUnread = unread;
++ renderActions(unread);
++ renderNet(frame);
renderNow(live, unread);
renderContinue(hist);
renderMoney(coins, last);
ddiff --git a/public/my.html b/public/my.html
iindex 6f7e87ad..fb0e2f48 100644
---- a/public/my.html
++++ b/public/my.html
@@@ -52,16 +52,30 @@
<div id="navbar-mount"></div>
<div class="page">
-- <div class="page-header" id="page-header">
-- <img class="avatar" id="user-avatar" src="" alt="">
-- <div class="info">
-- <div class="greeting" id="user-greeting"></div>
-- <h1 id="user-name">Loading...</h1>
-- <div class="subtitle" id="user-subtitle"></div>
-- <div class="badges" id="user-badges"></div>
-- </div>
-- <a class="btn btn-outline td-settings-btn" href="#settings"><i class="fa-solid fa-gear" aria-hidden="true"></i> Settings</a>
-- </div>
++ <!-- You (11-07 redesign; the owner: "front-face the best stuff we have to offer … awesome design"): who you are in
++ your own colour, your balances, and one tap to what you do most, on every site. -->
++ <header class="me" id="page-header">
++ <div class="me-top">
++ <div class="me-who">
++ <span class="me-face"><img class="avatar" id="user-avatar" src="" alt=""></span>
++ <div class="info">
++ <div class="greeting" id="user-greeting"></div>
++ <h1 id="user-name">Loading...</h1>
++ <div class="subtitle" id="user-subtitle"></div>
++ <div class="badges" id="user-badges"></div>
++ </div>
++ </div>
++ <div class="me-money" id="coins" hidden>
++ <a class="me-bals" href="/wallet" aria-label="Your balances: open the wallet">
++ <span class="me-bal"><span class="td-coin" aria-hidden="true"></span><b id="td-coins">0</b><small>OpenCoins</small></span>
++ <span class="me-bal" id="td-vibes-row" hidden><span class="td-coin td-coin-v" aria-hidden="true"></span><b id="td-vibes">0</b><small>Vibes</small></span>
++ </a>
++ <p class="me-last" id="td-money-last" hidden></p>
++ <a class="me-tip" href="https://openvibe.tips"><i class="fa-solid fa-hand-holding-heart" aria-hidden="true"></i> Tip a creator</a>
++ </div>
++ </div>
++ <nav class="me-acts" id="me-acts" aria-label="Quick actions"></nav>
++ </header>
<!-- You, today (plan L43): what is happening for you now, what you were doing, your money, your sites and your
numbers. my/today.js fills each block from its own API; a block with nothing in it stays out. -->
@@@ -75,18 +89,19 @@
<div class="td-h"><h2 id="td-continue-h">Pick up where you left off</h2><a class="td-more" href="#history" onclick="showSection('history');document.getElementById('settings').scrollIntoView({behavior:'smooth'});return false">History</a></div>
<div class="td-rail" id="td-continue-list"></div>
</section>
++ <section class="td-sec" id="td-net" hidden aria-labelledby="td-net-h">
++ <div class="td-h"><h2 id="td-net-h">On OpenVibe now</h2><a class="td-more" href="/sites">Every site</a></div>
++ <div class="td-net" id="td-net-cards"></div>
++ </section>
<div class="td-grid">
<section class="td-box" id="td-sites" hidden aria-labelledby="td-sites-h">
<div class="td-h"><h2 id="td-sites-h">Your sites</h2><a class="td-more" href="/sites">All sites</a></div>
<div class="td-sites" id="td-sites-grid"></div>
<div class="td-tries" id="td-try" hidden></div>
</section>
-- <section class="td-box td-money" id="coins" hidden aria-labelledby="td-money-h">
-- <div class="td-h"><h2 id="td-money-h">Your money</h2><a class="td-more" href="/wallet">Wallet</a></div>
-- <div class="td-bal"><span class="td-coin" aria-hidden="true"></span><b id="td-coins">0</b><span>OpenCoins</span></div>
-- <div class="td-bal" id="td-vibes-row" hidden><span class="td-coin td-coin-v" aria-hidden="true"></span><b id="td-vibes">0</b><span>Vibes</span></div>
-- <p class="td-sub" id="td-money-last" hidden></p>
-- <div class="td-money-acts"><a class="td-chip" href="https://openvibe.tips"><i class="fa-solid fa-hand-holding-heart" aria-hidden="true"></i> Tip a creator</a></div>
++ <section class="td-box td-nums" id="td-nums" hidden aria-labelledby="td-nums-h">
++ <div class="td-h"><h2 id="td-nums-h">Your numbers</h2><a class="td-more" href="https://openvibe.report/you">On openvibe.report</a></div>
++ <div class="td-stats" id="td-stuff-row"></div>
</section>
</div>
<div class="recent-tools" id="my-recent-tools" hidden>
@@@ -94,10 +109,6 @@
<div class="recent-tools-list" id="my-recent-tools-list"></div>
<p class="recent-tools-status" id="my-recent-tools-status" role="status"></p>
</div>
-- <div class="td-stuff">
-- <div class="td-stats" id="td-stuff-row"></div>
-- <a class="td-more" href="https://openvibe.report/you">Your numbers on openvibe.report <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a>
-- </div>
</div>
<h2 class="settings-h" id="settings">Settings</h2>
ddiff --git a/test/my-today.test.js b/test/my-today.test.js
iindex ba163b16..5adf40cd 100644
---- a/test/my-today.test.js
++++ b/test/my-today.test.js
@@@ -39,7 +39,7 @@ class Node {
function page() {
const ids = {};
-- for (const [, id] of HTML.matchAll(/id="((?:td-|user-greeting|today|coins|settings)[a-z-]*)"/g)) ids[id] = new Node('div');
++ for (const [, id] of HTML.matchAll(/id="((?:td-|me-|page-header|user-greeting|today|coins|settings)[a-z-]*)"/g)) ids[id] = new Node('div');
const shown = [];
const sandbox = {
document: { getElementById: (id) => ids[id] || null, createElement: (t) => new Node(t), querySelector: () => null },
@@@ -79,10 +79,31 @@ assert.match(CSS, /@media \(min-width:1000px\)\{[\s\S]*\.settings\{display:grid;
assert.strictEqual(p.ids['td-live'].all((c) => c.tagName === 'IMG').length, 1, 'only an https thumbnail is shown');
assert.strictEqual(p.ids['td-now-sub'].textContent, '2 of the 14 you follow');
const chips = p.ids['td-chips'];
-- assert.match(chips.textContent, /3 new notifications/);
-- assert.match(chips.textContent, /Go live/, 'a streamer can go live from here');
assert.ok(!/live on OpenVibe now/.test(chips.textContent), 'not "who else is live" while people you follow are');
++ // The hero's actions (11-07): one tap to what you do most; Go live for a streamer, the count on Notifications.
++ p.sandbox.renderActions({ count: 3 });
++ const acts = p.ids['me-acts'];
++ assert.deepStrictEqual(acts.children.map((a) => a.children[1].textContent), ['Go live', 'Messages', 'Ask Vibe', 'Your files', 'Codes', 'Wallet', 'Notifications', 'Settings']);
++ assert.deepStrictEqual(links(acts).slice(0, 5), ['https://openvibe.live/broadcast', 'https://openvibe.chat/messages', 'https://openvibe.chat/', 'https://openvibe.download/files', 'https://openvibe.codes/'], 'every action is a real link first');
++ assert.match(acts.textContent, /Notifications3/, 'the unread count on Notifications');
++ p.sandbox.currentUser = { role: 'user' };
++ p.sandbox.renderActions({ count: 0 });
++ assert.strictEqual(acts.children[0].children[1].textContent, 'Watch live', 'a viewer watches');
++ assert.ok(!/Notifications\d/.test(acts.textContent), 'no zero count');
++ p.sandbox.currentUser = { role: 'streamer' };
++
++ // On OpenVibe now: the sites' own cards, https entries only, an empty card left out.
++ p.sandbox.renderNet({ cards: [
++ { id: 'trending', site: 'media-hub', title: 'Trending', items: [{ title: 'A walk', url: 'https://openvibe.video/v/1', meta: '9 views this week' }, { title: 'Bad', url: 'javascript:alert(1)' }], more: { label: 'Watch more', url: 'https://openvibe.video/' } },
++ { id: 'empty', site: 'games', title: 'Nothing', items: [] },
++ ] });
++ assert.strictEqual(p.ids['td-net'].hidden, false);
++ assert.deepStrictEqual(links(p.ids['td-net-cards']), ['https://openvibe.video/', 'https://openvibe.video/v/1']);
++ assert.match(p.ids['td-net-cards'].textContent, /TrendingVideo/);
++ p.sandbox.renderNet(null);
++ assert.strictEqual(p.ids['td-net'].hidden, true, 'no cards: the block stays out');
++
p.sandbox.renderContinue({ items: [
{ title: 'Geese at dawn', url: 'https://openvibe.live/@goosely', service: 'live', service_label: 'Live', updated_at: ago(5) },
{ title: 'Geese at dawn', url: 'https://openvibe.live/@goosely', service: 'live', updated_at: ago(50) },
@@@ -117,7 +138,7 @@ assert.match(CSS, /@media \(min-width:1000px\)\{[\s\S]*\.settings\{display:grid;
p.sandbox.renderNow({ live: [], following: 0, anyone_live: 4 }, { count: 0 });
assert.strictEqual(p.ids['td-now'].hidden, true, 'nobody you follow is live: no Live now row');
assert.match(p.ids['td-chips'].textContent, /4 live on OpenVibe now/, 'but who else is, in one chip');
-- assert.ok(!/Go live|notification/.test(p.ids['td-chips'].textContent), 'no Go live for a viewer, no zero count');
++ assert.ok(!/Go live|notification/.test(p.ids['td-chips'].textContent), 'the chips only say who else is live');
p.sandbox.renderContinue(null);
assert.strictEqual(p.ids['td-continue'].hidden, true);
p.sandbox.renderSites(null, null);
@@@ -126,6 +147,7 @@ assert.match(CSS, /@media \(min-width:1000px\)\{[\s\S]*\.settings\{display:grid;
assert.strictEqual(p.ids.coins.hidden, true, 'no coins answer: no money block');
p.sandbox.renderStuff(null, null, null);
assert.strictEqual(p.ids['td-stuff-row'].children.length, 0);
++ assert.strictEqual(p.ids['td-nums'].hidden, true, 'nothing to count: Your numbers stays out');
// ── Every API down: the page still draws ──
p = page();