openvibers/openvibe.chat Commits

Empty states: sidebars, the inbox list and a server's forum use the kit (#535)

OP OpenVibers committed 2 days ago · 9e9c90d056 · 5 files, +32 −8

Changes

ddiff --git a/server/web/inbox-pages.js b/server/web/inbox-pages.js
iindex 6d5a116..f3d5e76 100644
---- a/server/web/inbox-pages.js
++++ b/server/web/inbox-pages.js
@@@ -103,7 +103,7 @@ function mountInboxPages(router, { page, viewerOf, needUser, sameSite, formLimit
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>
<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, {
ddiff --git a/server/web/layout.js b/server/web/layout.js
iindex 1d1f0e3..cf3afd1 100644
---- a/server/web/layout.js
++++ b/server/web/layout.js
@@@ -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')}">` : ''}
ddiff --git a/server/web/server-pages.js b/server/web/server-pages.js
iindex ba7b050..10a3a8e 100644
---- a/server/web/server-pages.js
++++ b/server/web/server-pages.js
@@@ -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({
ddiff --git a/server/web/shell.js b/server/web/shell.js
iindex 4d35fea..bd30300 100644
---- a/server/web/shell.js
++++ b/server/web/shell.js
@@@ -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 || {})}`;
}
ddiff --git a/test/empty-states.test.js b/test/empty-states.test.js
nnew file mode 100644
iindex 0000000..dafe9a1
---- /dev/null
++++ b/test/empty-states.test.js
@@@ -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');