openvibers/openvibe.chat Commits

Chat: the front page says what OpenVibe.Chat does, under the global room (#88)

OP OpenVibers committed 15 Oct 2026 · d2240dad95 · 4 files, +29 −1

**openvibe.chat's front page says what Chat is.**

**Before:** the front page was the global room and a sign-in button, and nothing else. A visitor, or a search engine, could not tell that Chat also runs Live's stream chat, rooms, direct messages, calls and network-wide preferences.

**Now:** under the room, a "What OpenVibe.Chat does" section (showcase kit) has six cards. Each one links somewhere real, and each claim is true today:
- **One room for the network:** global chat across the sites, readable without an account (links to the room).
- **Stream chat on Live:** each channel's room, with the streamer's moderators, timeouts, bans, emotes and text to speech (links to openvibe.live).
- **Rooms of your own:** public rooms, or private ones that stay invisible to everyone else (`/rooms`).
- **Direct messages:** one-to-one and group conversations, with the person's own controls over who may start one and whether groups may add them (`/messages`).
- **Voice and video calls:** call a person, a call room, or a stream's call, with up to eight people and mute, camera and kick controls. Calls are on in production (`CHAT_CALLS=1`).
- **Your preferences everywhere:** compact mode, timestamps, text size, badges and emotes, from the Network module (`/settings`).

A guest's "messages" and "settings" cards go through sign-in first. The layout links `showcase.css` only on this page (`o.showcase`), and the section sits in the page column without a second gutter.

**Checked** locally at 1280 and 390 px: no horizontal overflow, two rows of three cards on desktop.

**Test:** `web.test.js` checks for six cards, the rooms link, the sign-in links for a guest, the direct links when signed in, and the stylesheet.

Changes

ddiff --git a/public/web/chat.css b/public/web/chat.css
iindex 0dd4087..7bda95b 100644
---- a/public/web/chat.css
++++ b/public/web/chat.css
@@@ -28,6 +28,8 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
.oc-tabs a[aria-current="page"], .oc-tabs a:hover { color: var(--oc-text); background: var(--oc-panel); }
.oc-account { font-size: 14px; color: var(--oc-muted); margin-left: auto; }
.oc-main { max-width: 960px; margin: 0 auto; padding: 16px 16px 48px; overflow-wrap: anywhere; }
++/* The front page's showcase section under the room: the page column already has the gutter. */
++.oc-main .sc-sec { padding-left: 0; padding-right: 0; margin-top: 40px; }
h1 { font-size: 1.6rem; line-height: 1.25; margin: .3em 0 .2em; }
.oc-muted { color: var(--oc-muted); }
.oc-live { color: var(--oc-ok); font-size: .85em; margin-left: 6px; }
ddiff --git a/server/web/layout.js b/server/web/layout.js
iindex a252141..82baf31 100644
---- a/server/web/layout.js
++++ b/server/web/layout.js
@@@ -91,6 +91,7 @@ ${head}
${appIcon.headTags({ site: 'network' })}
<link rel="stylesheet" href="${asset('chat.css')}">
<link rel="stylesheet" href="${ovServe.url('items.css')}">
++${o.showcase ? `<link rel="stylesheet" href="${ovServe.url('showcase.css')}">` : ''}
<script src="${ovServe.url('theme-loader.js')}" defer></script>
<script src="${ovServe.url('navbar.js')}" defer></script>
<script src="${ovServe.url('footer.js')}" defer></script>
ddiff --git a/server/web/pages.js b/server/web/pages.js
iindex d50997d..2e0a4a5 100644
---- a/server/web/pages.js
++++ b/server/web/pages.js
@@@ -26,6 +26,7 @@
* tables; the only openvibe.live URLs are links to people's profiles (navigation, never fetched).
* test/live-down.test.js renders every page and uses the core APIs with Live refusing and hanging.
*/
++const showcase = require('openvibe-shared/showcase');
const path = require('path');
const express = require('express');
const rateLimit = require('express-rate-limit');
@@@ -143,6 +144,21 @@ function createWebRoutes({ config }) {
const n = async (fn) => { try { return Number(await fn()) || 0; } catch { return 0; } };
return { messages: await n(async () => await dm.getTotalUnread(actor.user.id)), rooms: await n(async () => await require('../rooms/rooms').unreadTotal(actor.user)) };
};
++ // Under the global room on the front page: what openvibe.chat is for (openvibe-shared/showcase), each card a real
++ // place to go. Every line is true today (calls run since CHAT_CALLS=1; rooms, messages and settings are pages here).
++ const whatChatDoes = (signedIn) => showcase.features({
++ id: 'what-chat-does', title: 'What OpenVibe.Chat does',
++ lede: 'The chat behind every OpenVibe site, with one account and one set of preferences everywhere.',
++ items: [
++ { icon: 'ov:chat', title: 'One room for the network', text: 'Global chat reaches everyone on Live, Tools, Games and the rest, and reads without an account.', href: '#oc-h' },
++ { icon: 'ov:live', title: 'Stream chat on Live', text: 'Every channel on OpenVibe.Live has its room, with the streamer\'s moderators, timeouts, bans, emotes and text to speech.', href: 'https://openvibe.live/' },
++ { icon: 'ov:community', title: 'Rooms of your own', text: 'Start a public room anyone can read, or a private one that stays invisible to everyone else.', href: '/rooms' },
++ { icon: 'ov:account', title: 'Direct messages', text: 'One-to-one and group conversations. You choose who may start one, and whether groups may add you.', href: signedIn ? '/messages' : '/auth/login?next=%2Fmessages' },
++ { icon: 'ov:stream', title: 'Voice and video calls', text: 'Call a person, open a call room, or join a stream\'s call: up to eight people, with mute, camera and kick controls.', href: '/rooms' },
++ { icon: 'ov:theme', title: 'Your preferences everywhere', text: 'Compact mode, timestamps, text size, badges and emotes follow your account to every site.', href: signedIn ? '/settings' : '/auth/login?next=%2Fsettings' },
++ ],
++ });
++
const page = async (req, res, status, o) => {
const actor = o.actor || await viewerOf(req);
res.status(status).set('Cache-Control', o.cache || cache.htmlHeaders({ private: true })).type('html')
@@@ -208,7 +224,9 @@ function createWebRoutes({ config }) {
${error}
<ol class="oc-feed" id="oc-feed" aria-live="polite" aria-label="Messages">${shown.map(globalMessage).join('\n') || '<li class="oc-empty oc-muted">No messages yet. Say hello.</li>'}</ol>
${composer}
--</section>`,
++</section>
++${whatChatDoes(actor.kind === 'user')}`,
++ showcase: true,
});
});
ddiff --git a/test/web.test.js b/test/web.test.js
iindex 3de497b..b0f0175 100644
---- a/test/web.test.js
++++ b/test/web.test.js
@@@ -43,6 +43,13 @@ t('global chat reads without JavaScript; signing in shows the composer', async (
assert.ok(!/id="oc-compose"/.test(r.text), 'no composer for a guest');
const me = await req('GET', '/', { headers: cookie(alice) });
assert.match(me.text, /id="oc-compose"/);
++ // Under the room, what openvibe.chat does: six cards, each a place to go, with the showcase stylesheet. A guest's
++ // messages and settings cards go through sign-in; a signed-in person's go straight there.
++ const what = (/<section class="sc-sec" id="what-chat-does"[\s\S]*?<\/section>/.exec(r.text) || [''])[0];
++ assert.strictEqual((what.match(/class="sc-card"/g) || []).length, 6, 'six cards');
++ assert.ok(what.includes('href="/rooms"') && what.includes('href="/auth/login?next=%2Fmessages"'), what.slice(0, 300));
++ assert.match(r.text, /<link rel="stylesheet" href="\/shared\/showcase\.css\?v=/);
++ assert.ok(me.text.includes('href="/messages"') && me.text.includes('href="/settings"'), 'signed in: straight to messages and settings');
});
t('the composer posts without JavaScript, through the API\'s checks; other origins are refused', async () => {