openvibers/openvibe.chat Commits
Empty states: sidebars, the inbox list and a server's forum use the kit (#535)
Changes
@@@ -103,7 +103,7 @@ function mountInboxPages(router, { page, viewerOf, needUser, sameSite, formLimitconst list = mine.length? `<ul class="ib-list">${mine.map((i) => `<li><a href="/inbox/${esc(i.id)}"><span class="ib-dot" style="background:${esc(i.color)}"></span><b>${esc(i.name)}</b><span class="oc-muted">${i.waiting ? `${i.waiting} waiting · ` : ''}${i.open} open · ${i.role === 'owner' ? 'owner' : 'agent'}</span></a></li>`).join('')}</ul>`-- : '<p class="oc-muted">No inboxes yet. Make one below, paste its snippet into your site, and visitors can write to you: Vibe answers first, you take over when it matters.</p>';++ : kit.empty({ icon: 'mail', title: 'No inboxes yet', text: 'Make one below, paste its snippet into your site, and visitors can write to you: Vibe answers first, you take over when it matters.', action: { href: '#oc-new-inbox', label: 'Make an inbox', icon: 'plus' } });const mineServers = (await require('../rooms/servers').list(actor.user)).mine;const dmUnread = await require('../chat/dm').getTotalUnread(actor.user.id);await page(req, res, 200, {@@@ -167,7 +167,8 @@ ${o.shell ? `<link rel="stylesheet" href="${asset('chat-shell.css')}">` : ''}${o.showcase ? `<link rel="stylesheet" href="${ovServe.url('showcase.css')}">` : ''}${o.upload ? `<link rel="stylesheet" href="${ovServe.url('ov-upload.css')}">` : ''}${(o.inlineStyles || []).map((css) => `<style>${css}</style>`).join('\n')}--${o.kit ? `<link rel="stylesheet" href="${ovServe.url('kit.css')}">\n<link rel="stylesheet" href="${ovServe.url('thread.css')}">` : ''}++${o.kit || o.shell ? `<link rel="stylesheet" href="${ovServe.url('kit.css')}">` : ''}++${o.kit ? `<link rel="stylesheet" href="${ovServe.url('thread.css')}">` : ''}${o.app || o.appStyle ? `<link rel="stylesheet" href="${asset(o.appStyle || 'vibe.css')}">` : ''}${o.feedView ? `<link rel="stylesheet" href="${asset('feed-view.css')}">\n<script src="${asset('feed-view.js')}" defer></script>` : ''}${o.partyHome ? `<link rel="stylesheet" href="${asset('party-home.css')}">` : ''}@@@ -16,6 +16,7 @@const { esc } = require('./layout');const ui = require('./ui');const { ICONS: IC } = require('./icons');++const kit = require('openvibe-shared/kit');const rooms = require('../rooms/rooms');const shell = require('./shell');const { insightBlock } = require('./insight-block');@@@ -212,9 +213,9 @@ ${sheet}`,const sortLinks = `<nav aria-label="Forum order"><a href="?sort=active">Latest activity</a> · <a href="?sort=new">Newest</a></nav>`;const tags = categories.length ? `<nav aria-label="Tags"><a href="?sort=active">All tags</a>${categories.map((tag) => ` · <a href="?category=${encodeURIComponent(tag.slug)}">${esc(tag.name)}</a>`).join('')}</nav>` : '';const searchHref = `${require('../config').community.publicUrl}/search?type=thread&q=${encodeURIComponent(data.space && data.space.name || '')}`;-- const listMarkup = (data.threads || []).map((t) => `<article><h3><a href="${itemHref(t.slug)}">${esc(t.title)}</a></h3><p>${esc(t.author && (t.author.display_name || t.author.username) || 'Member')} · ${Number(t.reply_count) || 0} replies · ${esc(t.last_activity_at || t.created_at || '')}${t.pinned ? ' · Pinned' : ''}${t.locked ? ' · Locked' : ''}</p>${t.category ? `<p>Tag: ${esc(t.category.name)}</p>` : ''}</article>`).join('') || '<p>No posts yet.</p>';++ const listMarkup = (data.threads || []).map((t) => `<article><h3><a href="${itemHref(t.slug)}">${esc(t.title)}</a></h3><p>${esc(t.author && (t.author.display_name || t.author.username) || 'Member')} · ${Number(t.reply_count) || 0} replies · ${esc(t.last_activity_at || t.created_at || '')}${t.pinned ? ' · Pinned' : ''}${t.locked ? ' · Locked' : ''}</p>${t.category ? `<p>Tag: ${esc(t.category.name)}</p>` : ''}</article>`).join('') || kit.empty({ icon: 'chat', title: 'No posts yet', text: 'Nothing has been posted in this channel yet.', action: me && !post ? { href: '#oc-forum-new', label: 'Start a post', icon: 'plus' } : null });const postMarkup = post ? `<article><h2>${esc(post.title)}</h2><p>${esc(post.author && (post.author.display_name || post.author.username) || 'Member')} · ${esc(post.last_activity_at || post.created_at || '')}${post.pinned ? ' · Pinned' : ''}${post.locked ? ' · Locked' : ''}</p>${(data.posts || []).map((p) => `<section><p>${esc(p.author && (p.author.display_name || p.author.username) || 'Member')} · ${esc(p.created_at || '')}</p><div>${p.body_html || ''}</div></section>`).join('')}<p><a href="${esc(`${require('../config').community.publicUrl}/s/${encodeURIComponent(room.forum_space)}/t/${encodeURIComponent(post.slug)}`)}">Reply or edit in Community</a></p></article>` : `<section aria-label="Forum posts">${listMarkup}</section>`;-- const forumForm = me && !post ? `<form class="oc-form" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum"><h3>Start a post</h3><label>Title <input name="title" maxlength="200" required></label><label>Post <textarea name="body" maxlength="40000" rows="5" required></textarea></label>${categories.length ? `<label>Tag <select name="category"><option value="">No tag</option>${categories.map((c) => `<option value="${esc(c.slug)}">${esc(c.name)}</option>`).join('')}</select></label>` : ''}<button class="ovk-btn ovk-btn--primary" type="submit">Post</button></form>` : '';++ const forumForm = me && !post ? `<form class="oc-form" id="oc-forum-new" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum"><h3>Start a post</h3><label>Title <input name="title" maxlength="200" required></label><label>Post <textarea name="body" maxlength="40000" rows="5" required></textarea></label>${categories.length ? `<label>Tag <select name="category"><option value="">No tag</option>${categories.map((c) => `<option value="${esc(c.slug)}">${esc(c.name)}</option>`).join('')}</select></label>` : ''}<button class="ovk-btn ovk-btn--primary" type="submit">Post</button></form>` : '';const replyForm = me && post && data.viewer && data.viewer.can_reply ? `<form class="oc-form" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum/${esc(post.slug)}/replies"><label>Reply <textarea name="body" maxlength="40000" rows="4" required></textarea></label><button class="ovk-btn ovk-btn--primary" type="submit">Reply</button></form>` : '';const modForm = me && post && data.viewer && data.viewer.can_moderate ? `<form class="oc-form" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum/${esc(post.slug)}/state"><label><input type="checkbox" name="pinned" value="true"${post.pinned ? ' checked' : ''}> Pinned</label><label><input type="checkbox" name="locked" value="true"${post.locked ? ' checked' : ''}> Locked</label><button class="ovk-btn ovk-btn--quiet" type="submit">Save moderation</button></form>` : '';const shellBody = shell.shell({@@@ -24,6 +24,7 @@const { esc } = require('./layout');const ui = require('./ui');const { ICONS: IC } = require('./icons');++const kit = require('openvibe-shared/kit');/** The five places of the phone's bottom bar: Shared's app bar draws them (appbar.js). */const APP_BAR = [@@@ -116,18 +117,18 @@ function sideHome(o = {}) {</div><section class="oc-side-sec" aria-labelledby="oc-side-friends"><h2 class="oc-side-group" id="oc-side-friends">Friends <span class="oc-muted">${friends.length}</span></h2>${friends.length ? `<ul class="oc-side-list">${friends.map((f) => `<li class="oc-row"><form method="post" action="/messages/new"><input type="hidden" name="username" value="${esc(f.username || '')}"><button type="submit" class="oc-side-person">${ui.initials(f.display_name || f.username || '?', f.presence, { avatarUrl: f.avatar_url })}<span class="oc-row-main"><span class="oc-row-top"><strong>${esc(f.display_name || f.username || '?')}</strong></span><span class="oc-row-sub">@${esc(f.username || '')}</span></span></button></form></li>`).join('')}</ul>`-- : `<p class="oc-side-empty oc-muted">No friends yet. Start a conversation and add the people you talk to.</p>`}++ : `<div class="oc-side-empty">${kit.empty({ icon: 'user', title: 'No friends yet', text: 'Start a conversation and add the people you talk to.' })}</div>`}<form class="oc-side-add" method="post" action="/messages/contacts"><label class="oc-sr" for="oc-add-friend">Add a friend by username</label><input id="oc-add-friend" name="username" required pattern="[A-Za-z0-9_]{3,24}" placeholder="Add a friend by username" autocomplete="off"><button type="submit">Add</button></form></section><section class="oc-side-sec" aria-labelledby="oc-side-requests"><h2 class="oc-side-group" id="oc-side-requests">Requests <span class="oc-muted">${requests.length}</span></h2>${requests.length ? `<ul class="oc-side-list">${requests.map((r) => personRow(r, { extra: `<form class="oc-inline" method="post" action="/messages/contacts/${Number(r.id)}/accept"><button type="submit" class="oc-link">Accept</button></form>` })).join('')}</ul>`-- : `<p class="oc-side-empty oc-muted">No requests.</p>`}++ : `<div class="oc-side-empty">${kit.empty({ icon: 'user', title: 'No requests', text: 'Friend requests appear here.' })}</div>`}</section><section class="oc-side-sec" aria-labelledby="oc-side-vibe"><h2 class="oc-side-group" id="oc-side-vibe">Vibe</h2><ul class="oc-side-list"><li class="oc-row"><a href="/vibe">${IC.spark}<span class="oc-row-main"><span class="oc-row-top"><strong>Vibe</strong></span><span class="oc-row-sub">OpenVibe's assistant</span></span></a></li></ul></section><section class="oc-side-sec" aria-labelledby="oc-side-dms"><h2 class="oc-side-group" id="oc-side-dms">Direct messages</h2>--${dms.length ? `<ul class="oc-side-list">${dms.map(dmRow).join('')}</ul>` : `<p class="oc-side-empty oc-muted">No conversations yet.</p>`}++${dms.length ? `<ul class="oc-side-list">${dms.map(dmRow).join('')}</ul>` : `<div class="oc-side-empty">${kit.empty({ icon: 'chat', title: 'No conversations yet', text: 'Find someone above to start one.' })}</div>`}</section>${youBar(o.you || {})}`;}@@@ -215,7 +216,7 @@ function sideInbox(o = {}) {const row = (i) => `<li class="oc-row${i.waiting ? ' oc-unread' : ''}"><a href="/inbox/${esc(i.id)}"${i.current ? ' aria-current="page"' : ''}><span class="oc-dot" style="background:${esc(i.color || 'var(--oc-accent)')}" aria-hidden="true"></span><span class="oc-row-main"><span class="oc-row-top"><strong>${esc(i.name)}</strong></span><span class="oc-row-sub">${i.waiting ? `${i.waiting} waiting · ` : ''}${i.open} open · ${i.role === 'owner' ? 'owner' : 'agent'}</span></span></a></li>`;return `<div class="oc-side-top"><a class="oc-side-find" href="/inbox#oc-new-inbox">${IC.plus}<span>Make an inbox</span></a></div><section class="oc-side-sec" aria-labelledby="oc-side-inboxes"><h2 class="oc-side-group" id="oc-side-inboxes">Your inboxes</h2>--${inboxes.length ? `<ul class="oc-side-list">${inboxes.map(row).join('')}</ul>` : '<p class="oc-side-empty oc-muted">No inboxes yet.</p>'}</section>++${inboxes.length ? `<ul class="oc-side-list">${inboxes.map(row).join('')}</ul>` : `<div class="oc-side-empty">${kit.empty({ icon: 'mail', title: 'No inboxes yet', text: 'Make one to answer visitors.', action: { href: '/inbox#oc-new-inbox', label: 'Make an inbox', icon: 'plus' } })}</div>`}</section>${youBar(o.you || {})}`;}nnew file mode 100644@@@ -0,0 +1,21 @@++'use strict';++/** Owner ask 81: a sidebar with nothing uses the shared kit's empty state, not a bare "No X yet" line. */++const assert = require('assert');++const shell = require('../server/web/shell');++++const has = (html, s) => assert.ok(html.includes(s), `expected to find: ${s}`);++++const home = shell.sideHome({ dms: [], friends: [], requests: [] });++has(home, 'class="ovk-empty"');++has(home, 'No friends yet');++has(home, 'No requests');++has(home, 'No conversations yet');++has(home, 'class="oc-side-empty"');++++const inbox = shell.sideInbox({ inboxes: [] });++has(inbox, 'class="ovk-empty"');++has(inbox, 'No inboxes yet');++has(inbox, 'Make an inbox');++has(inbox, 'ovk-btn');++++console.log('ok - Chat sidebars draw the kit empty state');
@@ -103,7 +103,7 @@ function mountInboxPages(router, { page, viewerOf, needUser, sameSite, formLimit
@@ -103,7 +103,7 @@ function mountInboxPages(router, { page, viewerOf, needUser, sameSite, formLimit
const list = mine.length
const list = mine.length
? `<ul class="ib-list">${mine.map((i) => `<li><a href="/inbox/${esc(i.id)}"><span class="ib-dot" style="background:${esc(i.color)}"></span><b>${esc(i.name)}</b>
? `<ul class="ib-list">${mine.map((i) => `<li><a href="/inbox/${esc(i.id)}"><span class="ib-dot" style="background:${esc(i.color)}"></span><b>${esc(i.name)}</b>
<span class="oc-muted">${i.waiting ? `${i.waiting} waiting · ` : ''}${i.open} open · ${i.role === 'owner' ? 'owner' : 'agent'}</span></a></li>`).join('')}</ul>`
<span class="oc-muted">${i.waiting ? `${i.waiting} waiting · ` : ''}${i.open} open · ${i.role === 'owner' ? 'owner' : 'agent'}</span></a></li>`).join('')}</ul>`
- : '<p class="oc-muted">No inboxes yet. Make one below, paste its snippet into your site, and visitors can write to you: Vibe answers first, you take over when it matters.</p>';
+ : kit.empty({ icon: 'mail', title: 'No inboxes yet', text: 'Make one below, paste its snippet into your site, and visitors can write to you: Vibe answers first, you take over when it matters.', action: { href: '#oc-new-inbox', label: 'Make an inbox', icon: 'plus' } });
const mineServers = (await require('../rooms/servers').list(actor.user)).mine;
const mineServers = (await require('../rooms/servers').list(actor.user)).mine;
const dmUnread = await require('../chat/dm').getTotalUnread(actor.user.id);
const dmUnread = await require('../chat/dm').getTotalUnread(actor.user.id);
await page(req, res, 200, {
await page(req, res, 200, {
@@ -167,7 +167,8 @@ ${o.shell ? `<link rel="stylesheet" href="${asset('chat-shell.css')}">` : ''}
@@ -167,7 +167,8 @@ ${o.shell ? `<link rel="stylesheet" href="${asset('chat-shell.css')}">` : ''}
${o.showcase ? `<link rel="stylesheet" href="${ovServe.url('showcase.css')}">` : ''}
${o.showcase ? `<link rel="stylesheet" href="${ovServe.url('showcase.css')}">` : ''}
${o.upload ? `<link rel="stylesheet" href="${ovServe.url('ov-upload.css')}">` : ''}
${o.upload ? `<link rel="stylesheet" href="${ovServe.url('ov-upload.css')}">` : ''}
${(o.inlineStyles || []).map((css) => `<style>${css}</style>`).join('\n')}
${(o.inlineStyles || []).map((css) => `<style>${css}</style>`).join('\n')}
-${o.kit ? `<link rel="stylesheet" href="${ovServe.url('kit.css')}">\n<link rel="stylesheet" href="${ovServe.url('thread.css')}">` : ''}
+${o.kit || o.shell ? `<link rel="stylesheet" href="${ovServe.url('kit.css')}">` : ''}
+${o.kit ? `<link rel="stylesheet" href="${ovServe.url('thread.css')}">` : ''}
${o.app || o.appStyle ? `<link rel="stylesheet" href="${asset(o.appStyle || 'vibe.css')}">` : ''}
${o.app || o.appStyle ? `<link rel="stylesheet" href="${asset(o.appStyle || 'vibe.css')}">` : ''}
${o.feedView ? `<link rel="stylesheet" href="${asset('feed-view.css')}">\n<script src="${asset('feed-view.js')}" defer></script>` : ''}
${o.feedView ? `<link rel="stylesheet" href="${asset('feed-view.css')}">\n<script src="${asset('feed-view.js')}" defer></script>` : ''}
${o.partyHome ? `<link rel="stylesheet" href="${asset('party-home.css')}">` : ''}
${o.partyHome ? `<link rel="stylesheet" href="${asset('party-home.css')}">` : ''}
@@ -16,6 +16,7 @@
@@ -16,6 +16,7 @@
const { esc } = require('./layout');
const { esc } = require('./layout');
const ui = require('./ui');
const ui = require('./ui');
const { ICONS: IC } = require('./icons');
const { ICONS: IC } = require('./icons');
+const kit = require('openvibe-shared/kit');
const rooms = require('../rooms/rooms');
const rooms = require('../rooms/rooms');
const shell = require('./shell');
const shell = require('./shell');
const { insightBlock } = require('./insight-block');
const { insightBlock } = require('./insight-block');
@@ -212,9 +213,9 @@ ${sheet}`,
@@ -212,9 +213,9 @@ ${sheet}`,
const sortLinks = `<nav aria-label="Forum order"><a href="?sort=active">Latest activity</a> · <a href="?sort=new">Newest</a></nav>`;
const sortLinks = `<nav aria-label="Forum order"><a href="?sort=active">Latest activity</a> · <a href="?sort=new">Newest</a></nav>`;
const tags = categories.length ? `<nav aria-label="Tags"><a href="?sort=active">All tags</a>${categories.map((tag) => ` · <a href="?category=${encodeURIComponent(tag.slug)}">${esc(tag.name)}</a>`).join('')}</nav>` : '';
const tags = categories.length ? `<nav aria-label="Tags"><a href="?sort=active">All tags</a>${categories.map((tag) => ` · <a href="?category=${encodeURIComponent(tag.slug)}">${esc(tag.name)}</a>`).join('')}</nav>` : '';
const searchHref = `${require('../config').community.publicUrl}/search?type=thread&q=${encodeURIComponent(data.space && data.space.name || '')}`;
const searchHref = `${require('../config').community.publicUrl}/search?type=thread&q=${encodeURIComponent(data.space && data.space.name || '')}`;
- const listMarkup = (data.threads || []).map((t) => `<article><h3><a href="${itemHref(t.slug)}">${esc(t.title)}</a></h3><p>${esc(t.author && (t.author.display_name || t.author.username) || 'Member')} · ${Number(t.reply_count) || 0} replies · ${esc(t.last_activity_at || t.created_at || '')}${t.pinned ? ' · Pinned' : ''}${t.locked ? ' · Locked' : ''}</p>${t.category ? `<p>Tag: ${esc(t.category.name)}</p>` : ''}</article>`).join('') || '<p>No posts yet.</p>';
+ const listMarkup = (data.threads || []).map((t) => `<article><h3><a href="${itemHref(t.slug)}">${esc(t.title)}</a></h3><p>${esc(t.author && (t.author.display_name || t.author.username) || 'Member')} · ${Number(t.reply_count) || 0} replies · ${esc(t.last_activity_at || t.created_at || '')}${t.pinned ? ' · Pinned' : ''}${t.locked ? ' · Locked' : ''}</p>${t.category ? `<p>Tag: ${esc(t.category.name)}</p>` : ''}</article>`).join('') || kit.empty({ icon: 'chat', title: 'No posts yet', text: 'Nothing has been posted in this channel yet.', action: me && !post ? { href: '#oc-forum-new', label: 'Start a post', icon: 'plus' } : null });
const postMarkup = post ? `<article><h2>${esc(post.title)}</h2><p>${esc(post.author && (post.author.display_name || post.author.username) || 'Member')} · ${esc(post.last_activity_at || post.created_at || '')}${post.pinned ? ' · Pinned' : ''}${post.locked ? ' · Locked' : ''}</p>${(data.posts || []).map((p) => `<section><p>${esc(p.author && (p.author.display_name || p.author.username) || 'Member')} · ${esc(p.created_at || '')}</p><div>${p.body_html || ''}</div></section>`).join('')}<p><a href="${esc(`${require('../config').community.publicUrl}/s/${encodeURIComponent(room.forum_space)}/t/${encodeURIComponent(post.slug)}`)}">Reply or edit in Community</a></p></article>` : `<section aria-label="Forum posts">${listMarkup}</section>`;
const postMarkup = post ? `<article><h2>${esc(post.title)}</h2><p>${esc(post.author && (post.author.display_name || post.author.username) || 'Member')} · ${esc(post.last_activity_at || post.created_at || '')}${post.pinned ? ' · Pinned' : ''}${post.locked ? ' · Locked' : ''}</p>${(data.posts || []).map((p) => `<section><p>${esc(p.author && (p.author.display_name || p.author.username) || 'Member')} · ${esc(p.created_at || '')}</p><div>${p.body_html || ''}</div></section>`).join('')}<p><a href="${esc(`${require('../config').community.publicUrl}/s/${encodeURIComponent(room.forum_space)}/t/${encodeURIComponent(post.slug)}`)}">Reply or edit in Community</a></p></article>` : `<section aria-label="Forum posts">${listMarkup}</section>`;
- const forumForm = me && !post ? `<form class="oc-form" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum"><h3>Start a post</h3><label>Title <input name="title" maxlength="200" required></label><label>Post <textarea name="body" maxlength="40000" rows="5" required></textarea></label>${categories.length ? `<label>Tag <select name="category"><option value="">No tag</option>${categories.map((c) => `<option value="${esc(c.slug)}">${esc(c.name)}</option>`).join('')}</select></label>` : ''}<button class="ovk-btn ovk-btn--primary" type="submit">Post</button></form>` : '';
+ const forumForm = me && !post ? `<form class="oc-form" id="oc-forum-new" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum"><h3>Start a post</h3><label>Title <input name="title" maxlength="200" required></label><label>Post <textarea name="body" maxlength="40000" rows="5" required></textarea></label>${categories.length ? `<label>Tag <select name="category"><option value="">No tag</option>${categories.map((c) => `<option value="${esc(c.slug)}">${esc(c.name)}</option>`).join('')}</select></label>` : ''}<button class="ovk-btn ovk-btn--primary" type="submit">Post</button></form>` : '';
const replyForm = me && post && data.viewer && data.viewer.can_reply ? `<form class="oc-form" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum/${esc(post.slug)}/replies"><label>Reply <textarea name="body" maxlength="40000" rows="4" required></textarea></label><button class="ovk-btn ovk-btn--primary" type="submit">Reply</button></form>` : '';
const replyForm = me && post && data.viewer && data.viewer.can_reply ? `<form class="oc-form" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum/${esc(post.slug)}/replies"><label>Reply <textarea name="body" maxlength="40000" rows="4" required></textarea></label><button class="ovk-btn ovk-btn--primary" type="submit">Reply</button></form>` : '';
const modForm = me && post && data.viewer && data.viewer.can_moderate ? `<form class="oc-form" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum/${esc(post.slug)}/state"><label><input type="checkbox" name="pinned" value="true"${post.pinned ? ' checked' : ''}> Pinned</label><label><input type="checkbox" name="locked" value="true"${post.locked ? ' checked' : ''}> Locked</label><button class="ovk-btn ovk-btn--quiet" type="submit">Save moderation</button></form>` : '';
const modForm = me && post && data.viewer && data.viewer.can_moderate ? `<form class="oc-form" method="post" action="/s/${esc(server.slug)}/${esc(pick.slug)}/forum/${esc(post.slug)}/state"><label><input type="checkbox" name="pinned" value="true"${post.pinned ? ' checked' : ''}> Pinned</label><label><input type="checkbox" name="locked" value="true"${post.locked ? ' checked' : ''}> Locked</label><button class="ovk-btn ovk-btn--quiet" type="submit">Save moderation</button></form>` : '';
const shellBody = shell.shell({
const shellBody = shell.shell({
@@ -24,6 +24,7 @@
@@ -24,6 +24,7 @@
const { esc } = require('./layout');
const { esc } = require('./layout');
const ui = require('./ui');
const ui = require('./ui');
const { ICONS: IC } = require('./icons');
const { ICONS: IC } = require('./icons');
+const kit = require('openvibe-shared/kit');
/** The five places of the phone's bottom bar: Shared's app bar draws them (appbar.js). */
/** The five places of the phone's bottom bar: Shared's app bar draws them (appbar.js). */
const APP_BAR = [
const APP_BAR = [
@@ -116,18 +117,18 @@ function sideHome(o = {}) {
@@ -116,18 +117,18 @@ function sideHome(o = {}) {
</div>
</div>
<section class="oc-side-sec" aria-labelledby="oc-side-friends"><h2 class="oc-side-group" id="oc-side-friends">Friends <span class="oc-muted">${friends.length}</span></h2>
<section class="oc-side-sec" aria-labelledby="oc-side-friends"><h2 class="oc-side-group" id="oc-side-friends">Friends <span class="oc-muted">${friends.length}</span></h2>
${friends.length ? `<ul class="oc-side-list">${friends.map((f) => `<li class="oc-row"><form method="post" action="/messages/new"><input type="hidden" name="username" value="${esc(f.username || '')}"><button type="submit" class="oc-side-person">${ui.initials(f.display_name || f.username || '?', f.presence, { avatarUrl: f.avatar_url })}<span class="oc-row-main"><span class="oc-row-top"><strong>${esc(f.display_name || f.username || '?')}</strong></span><span class="oc-row-sub">@${esc(f.username || '')}</span></span></button></form></li>`).join('')}</ul>`
${friends.length ? `<ul class="oc-side-list">${friends.map((f) => `<li class="oc-row"><form method="post" action="/messages/new"><input type="hidden" name="username" value="${esc(f.username || '')}"><button type="submit" class="oc-side-person">${ui.initials(f.display_name || f.username || '?', f.presence, { avatarUrl: f.avatar_url })}<span class="oc-row-main"><span class="oc-row-top"><strong>${esc(f.display_name || f.username || '?')}</strong></span><span class="oc-row-sub">@${esc(f.username || '')}</span></span></button></form></li>`).join('')}</ul>`
- : `<p class="oc-side-empty oc-muted">No friends yet. Start a conversation and add the people you talk to.</p>`}
+ : `<div class="oc-side-empty">${kit.empty({ icon: 'user', title: 'No friends yet', text: 'Start a conversation and add the people you talk to.' })}</div>`}
<form class="oc-side-add" method="post" action="/messages/contacts"><label class="oc-sr" for="oc-add-friend">Add a friend by username</label><input id="oc-add-friend" name="username" required pattern="[A-Za-z0-9_]{3,24}" placeholder="Add a friend by username" autocomplete="off"><button type="submit">Add</button></form>
<form class="oc-side-add" method="post" action="/messages/contacts"><label class="oc-sr" for="oc-add-friend">Add a friend by username</label><input id="oc-add-friend" name="username" required pattern="[A-Za-z0-9_]{3,24}" placeholder="Add a friend by username" autocomplete="off"><button type="submit">Add</button></form>
</section>
</section>
<section class="oc-side-sec" aria-labelledby="oc-side-requests"><h2 class="oc-side-group" id="oc-side-requests">Requests <span class="oc-muted">${requests.length}</span></h2>
<section class="oc-side-sec" aria-labelledby="oc-side-requests"><h2 class="oc-side-group" id="oc-side-requests">Requests <span class="oc-muted">${requests.length}</span></h2>
${requests.length ? `<ul class="oc-side-list">${requests.map((r) => personRow(r, { extra: `<form class="oc-inline" method="post" action="/messages/contacts/${Number(r.id)}/accept"><button type="submit" class="oc-link">Accept</button></form>` })).join('')}</ul>`
${requests.length ? `<ul class="oc-side-list">${requests.map((r) => personRow(r, { extra: `<form class="oc-inline" method="post" action="/messages/contacts/${Number(r.id)}/accept"><button type="submit" class="oc-link">Accept</button></form>` })).join('')}</ul>`
- : `<p class="oc-side-empty oc-muted">No requests.</p>`}
+ : `<div class="oc-side-empty">${kit.empty({ icon: 'user', title: 'No requests', text: 'Friend requests appear here.' })}</div>`}
</section>
</section>
<section class="oc-side-sec" aria-labelledby="oc-side-vibe"><h2 class="oc-side-group" id="oc-side-vibe">Vibe</h2>
<section class="oc-side-sec" aria-labelledby="oc-side-vibe"><h2 class="oc-side-group" id="oc-side-vibe">Vibe</h2>
<ul class="oc-side-list"><li class="oc-row"><a href="/vibe">${IC.spark}<span class="oc-row-main"><span class="oc-row-top"><strong>Vibe</strong></span><span class="oc-row-sub">OpenVibe's assistant</span></span></a></li></ul>
<ul class="oc-side-list"><li class="oc-row"><a href="/vibe">${IC.spark}<span class="oc-row-main"><span class="oc-row-top"><strong>Vibe</strong></span><span class="oc-row-sub">OpenVibe's assistant</span></span></a></li></ul>
</section>
</section>
<section class="oc-side-sec" aria-labelledby="oc-side-dms"><h2 class="oc-side-group" id="oc-side-dms">Direct messages</h2>
<section class="oc-side-sec" aria-labelledby="oc-side-dms"><h2 class="oc-side-group" id="oc-side-dms">Direct messages</h2>
-${dms.length ? `<ul class="oc-side-list">${dms.map(dmRow).join('')}</ul>` : `<p class="oc-side-empty oc-muted">No conversations yet.</p>`}
+${dms.length ? `<ul class="oc-side-list">${dms.map(dmRow).join('')}</ul>` : `<div class="oc-side-empty">${kit.empty({ icon: 'chat', title: 'No conversations yet', text: 'Find someone above to start one.' })}</div>`}
</section>
</section>
${youBar(o.you || {})}`;
${youBar(o.you || {})}`;
}
}
@@ -215,7 +216,7 @@ function sideInbox(o = {}) {
@@ -215,7 +216,7 @@ function sideInbox(o = {}) {
const row = (i) => `<li class="oc-row${i.waiting ? ' oc-unread' : ''}"><a href="/inbox/${esc(i.id)}"${i.current ? ' aria-current="page"' : ''}><span class="oc-dot" style="background:${esc(i.color || 'var(--oc-accent)')}" aria-hidden="true"></span><span class="oc-row-main"><span class="oc-row-top"><strong>${esc(i.name)}</strong></span><span class="oc-row-sub">${i.waiting ? `${i.waiting} waiting · ` : ''}${i.open} open · ${i.role === 'owner' ? 'owner' : 'agent'}</span></span></a></li>`;
const row = (i) => `<li class="oc-row${i.waiting ? ' oc-unread' : ''}"><a href="/inbox/${esc(i.id)}"${i.current ? ' aria-current="page"' : ''}><span class="oc-dot" style="background:${esc(i.color || 'var(--oc-accent)')}" aria-hidden="true"></span><span class="oc-row-main"><span class="oc-row-top"><strong>${esc(i.name)}</strong></span><span class="oc-row-sub">${i.waiting ? `${i.waiting} waiting · ` : ''}${i.open} open · ${i.role === 'owner' ? 'owner' : 'agent'}</span></span></a></li>`;
return `<div class="oc-side-top"><a class="oc-side-find" href="/inbox#oc-new-inbox">${IC.plus}<span>Make an inbox</span></a></div>
return `<div class="oc-side-top"><a class="oc-side-find" href="/inbox#oc-new-inbox">${IC.plus}<span>Make an inbox</span></a></div>
<section class="oc-side-sec" aria-labelledby="oc-side-inboxes"><h2 class="oc-side-group" id="oc-side-inboxes">Your inboxes</h2>
<section class="oc-side-sec" aria-labelledby="oc-side-inboxes"><h2 class="oc-side-group" id="oc-side-inboxes">Your inboxes</h2>
-${inboxes.length ? `<ul class="oc-side-list">${inboxes.map(row).join('')}</ul>` : '<p class="oc-side-empty oc-muted">No inboxes yet.</p>'}</section>
+${inboxes.length ? `<ul class="oc-side-list">${inboxes.map(row).join('')}</ul>` : `<div class="oc-side-empty">${kit.empty({ icon: 'mail', title: 'No inboxes yet', text: 'Make one to answer visitors.', action: { href: '/inbox#oc-new-inbox', label: 'Make an inbox', icon: 'plus' } })}</div>`}</section>
${youBar(o.you || {})}`;
${youBar(o.you || {})}`;
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+'use strict';
+/** Owner ask 81: a sidebar with nothing uses the shared kit's empty state, not a bare "No X yet" line. */
+const assert = require('assert');
+const shell = require('../server/web/shell');
+
+const has = (html, s) => assert.ok(html.includes(s), `expected to find: ${s}`);
+
+const home = shell.sideHome({ dms: [], friends: [], requests: [] });
+has(home, 'class="ovk-empty"');
+has(home, 'No friends yet');
+has(home, 'No requests');
+has(home, 'No conversations yet');
+has(home, 'class="oc-side-empty"');
+
+const inbox = shell.sideInbox({ inboxes: [] });
+has(inbox, 'class="ovk-empty"');
+has(inbox, 'No inboxes yet');
+has(inbox, 'Make an inbox');
+has(inbox, 'ovk-btn');
+
+console.log('ok - Chat sidebars draw the kit empty state');