openvibers/openvibe.network Commits
/my: a hero in your colour with balances and one-tap actions; On OpenVibe now (#627)
Changes
@@@ -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; } }@@@ -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) {@@@ -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 coinsconst 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);@@@ -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 yournumbers. 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>@@@ -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();
@@ -515,3 +515,91 @@ button.td-stat:hover span { color: var(--accent-light); }
@@ -515,3 +515,91 @@ button.td-stat:hover span { color: var(--accent-light); }
.qr-note { margin-top: 14px; }
.qr-note { margin-top: 14px; }
.qr-dialog { border-radius: 16px; }
.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; } }
@@ -145,6 +145,10 @@ function renderHeader(user) {
@@ -145,6 +145,10 @@ function renderHeader(user) {
? `<span class="badge badge-anon">anonymous</span>`
? `<span class="badge badge-anon">anonymous</span>`
: (user.role && user.role !== 'user' ? `<span class="badge badge-${user.role}">${user.role.replace(/_/g, ' ')}</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>`;
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) {
function renderProfile(user) {
@@ -18,6 +18,8 @@ const TD_SITES = {
@@ -18,6 +18,8 @@ const TD_SITES = {
host: ['Host', 'fa-server', 'https://openvibe.host'], deals: ['Deals', 'fa-tags', 'https://openvibe.deals'],
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'],
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'],
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.
// What to try next, most used across the network first, each with the reason it is worth a look.
const TD_TRY = [
const TD_TRY = [
@@ -76,20 +78,69 @@ function renderNow(live, unread) {
@@ -76,20 +78,69 @@ function renderNow(live, unread) {
const sub = document.getElementById('td-now-sub');
const sub = document.getElementById('td-now-sub');
if (sub) sub.textContent = people.length ? `${people.length} of the ${tdNum(live.following)} you follow` : '';
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 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/' }));
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.replaceChildren(...out);
chips.hidden = out.length === 0;
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. */
/** Continue: the newest things you opened across the sites (Network history), one card per address. */
function renderContinue(hist) {
function renderContinue(hist) {
const box = document.getElementById('td-continue');
const box = document.getElementById('td-continue');
@@ -165,6 +216,8 @@ function renderStuff(live, hist, nodes) {
@@ -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(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' }); });
add(hist && hist.total, 'pages in your history', () => { showSection('history'); document.getElementById('settings').scrollIntoView({ behavior: 'smooth' }); });
row.replaceChildren(...cells);
row.replaceChildren(...cells);
+ const box = document.getElementById('td-nums');
+ if (box) box.hidden = cells.length === 0;
}
}
async function loadToday() {
async function loadToday() {
@@ -172,7 +225,8 @@ async function loadToday() {
@@ -172,7 +225,8 @@ async function loadToday() {
renderGreeting();
renderGreeting();
if (typeof isAnonSession === 'function' && isAnonSession()) return; // a guest identity keeps no history, follows or coins
if (typeof isAnonSession === 'function' && isAnonSession()) return; // a guest identity keeps no history, follows or coins
const quiet = (p) => p.catch(() => null);
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/v1/me/live-follows')),
quiet(apiFetch('/api/history?limit=30')),
quiet(apiFetch('/api/history?limit=30')),
quiet(apiFetch('/api/coins/me?with=vibes')),
quiet(apiFetch('/api/coins/me?with=vibes')),
@@ -180,7 +234,11 @@ async function loadToday() {
@@ -180,7 +234,11 @@ async function loadToday() {
quiet(apiFetch('/api/notifications/unread-count')),
quiet(apiFetch('/api/notifications/unread-count')),
quiet(apiFetch('/api/themes/me/sites')),
quiet(apiFetch('/api/themes/me/sites')),
quiet(apiFetch('/api/v1/me/nodes')),
quiet(apiFetch('/api/v1/me/nodes')),
+ quiet(apiFetch('/api/frame?host=openvibe.network')),
]);
]);
+ globalThis.__tdUnread = unread;
+ renderActions(unread);
+ renderNet(frame);
renderNow(live, unread);
renderNow(live, unread);
renderContinue(hist);
renderContinue(hist);
renderMoney(coins, last);
renderMoney(coins, last);
@@ -52,16 +52,30 @@
@@ -52,16 +52,30 @@
<div id="navbar-mount"></div>
<div id="navbar-mount"></div>
<div class="page">
<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
<!-- 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. -->
numbers. my/today.js fills each block from its own API; a block with nothing in it stays out. -->
@@ -75,18 +89,19 @@
@@ -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-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>
<div class="td-rail" id="td-continue-list"></div>
</section>
</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">
<div class="td-grid">
<section class="td-box" id="td-sites" hidden aria-labelledby="td-sites-h">
<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-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-sites" id="td-sites-grid"></div>
<div class="td-tries" id="td-try" hidden></div>
<div class="td-tries" id="td-try" hidden></div>
</section>
</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>
</section>
</div>
</div>
<div class="recent-tools" id="my-recent-tools" hidden>
<div class="recent-tools" id="my-recent-tools" hidden>
@@ -94,10 +109,6 @@
@@ -94,10 +109,6 @@
<div class="recent-tools-list" id="my-recent-tools-list"></div>
<div class="recent-tools-list" id="my-recent-tools-list"></div>
<p class="recent-tools-status" id="my-recent-tools-status" role="status"></p>
<p class="recent-tools-status" id="my-recent-tools-status" role="status"></p>
</div>
</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>
</div>
<h2 class="settings-h" id="settings">Settings</h2>
<h2 class="settings-h" id="settings">Settings</h2>
@@ -39,7 +39,7 @@ class Node {
@@ -39,7 +39,7 @@ class Node {
function page() {
function page() {
const ids = {};
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 shown = [];
const sandbox = {
const sandbox = {
document: { getElementById: (id) => ids[id] || null, createElement: (t) => new Node(t), querySelector: () => null },
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;
@@ -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-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');
assert.strictEqual(p.ids['td-now-sub'].textContent, '2 of the 14 you follow');
const chips = p.ids['td-chips'];
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');
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: [
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', service_label: 'Live', updated_at: ago(5) },
{ title: 'Geese at dawn', url: 'https://openvibe.live/@goosely', service: 'live', updated_at: ago(50) },
{ 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;
@@ -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 });
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.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.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);
p.sandbox.renderContinue(null);
assert.strictEqual(p.ids['td-continue'].hidden, true);
assert.strictEqual(p.ids['td-continue'].hidden, true);
p.sandbox.renderSites(null, null);
p.sandbox.renderSites(null, null);
@@ -126,6 +147,7 @@ assert.match(CSS, /@media \(min-width:1000px\)\{[\s\S]*\.settings\{display:grid;
@@ -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');
assert.strictEqual(p.ids.coins.hidden, true, 'no coins answer: no money block');
p.sandbox.renderStuff(null, null, null);
p.sandbox.renderStuff(null, null, null);
assert.strictEqual(p.ids['td-stuff-row'].children.length, 0);
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 ──
// ── Every API down: the page still draws ──
p = page();
p = page();