openvibers/openvibe.mediahub Commits

Comments: one sign-in line when empty, Top and Newest only with two or more (#408)

OP OpenVibers committed 1 days ago · 3bc5f0e3a2 · 4 files, +31 −14

Signed out with no comments, the composer spot reads "Be the first to comment."
with Sign in beside it, instead of a sign-in line and a second empty box. Top and
Newest show only when there is something to order. The head, the order pills and
the Community link are quieter; the thread sits flush in its panel.

Changes

ddiff --git a/public/css/comments.css b/public/css/comments.css
iindex cbfb011..30855a4 100644
---- a/public/css/comments.css
++++ b/public/css/comments.css
@@@ -1,15 +1,24 @@
++/* The inline Community thread on Video and Pics item pages: a quiet head (the title, then Top and Newest only when
++ there is something to order), the composer or one sign-in line, the comments, and a small link to the discussion. */
.ovc-thread { margin: 24px 0; }
--.ovc-head, .ovc-order, .ovc-comment header, .ovc-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
--.ovc-head { justify-content: space-between; }
++.ovv-panel > .ovc-thread { margin: 0; }
++.ovc-head, .ovc-comment header, .ovc-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
++.ovc-head { justify-content: space-between; min-height: 40px; margin-bottom: 8px; }
++.ovc-head h2 { margin: 0; font-size: 1.125rem; }
++.ovc-signin, .ovc-none { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin: 0; }
++.ovc-none { margin-top: 12px; opacity: .75; }
.ovc-comment { padding: 12px 0; border-bottom: 1px solid var(--border, #5555); }
.ovc-comment header img { border-radius: 50%; object-fit: cover; }
--.ovc-comment time, .ovc-community { font-size: .875em; opacity: .8; }
++.ovc-comment time { font-size: .875em; opacity: .8; }
.ovc-replies { margin-left: 24px; }
.ovc-actions form { display: inline; }
.ovc-attach { display: inline-block; margin: 4px 8px 4px 0; }
.ovc-attach--file { border: 1px solid currentColor; border-radius: 999px; padding: 4px 12px; }
.ovc-attach img { max-width: 120px; max-height: 90px; object-fit: cover; }
--.ovc-order { display: inline-flex; gap: 0; border: 1px solid currentColor; border-radius: 8px; overflow: hidden; }
--.ovc-order a { border: 0; border-radius: 0; }
--.ovc-order a + a { border-left: 1px solid currentColor; }
--.ovc-order .is-active { font-weight: 700; background: color-mix(in srgb, currentColor 12%, transparent); }
++.ovc-order { display: inline-flex; gap: 2px; padding: 2px; border-radius: 999px; background: color-mix(in srgb, currentColor 8%, transparent); }
++.ovc-order a { min-height: 36px; padding: 4px 14px; border: 0; border-radius: 999px; font-size: .875rem; }
++.ovc-order .is-active { font-weight: 700; background: color-mix(in srgb, currentColor 16%, transparent); }
++.ovc-community { margin: 12px 0 0; font-size: .875em; }
++.ovc-community a { color: inherit; opacity: .8; text-decoration: none; }
++.ovc-community a::after { content: ' \2192'; }
++.ovc-community a:hover, .ovc-community a:focus-visible { opacity: 1; text-decoration: underline; }
ddiff --git a/server/comments/inline.js b/server/comments/inline.js
iindex 7d6b3d7..6cc7812 100644
---- a/server/comments/inline.js
++++ b/server/comments/inline.js
@@@ -1,7 +1,6 @@
'use strict';
/** Plain, server-rendered Community thread for the Video and Pics item pages. */
--const kit = require('openvibe-shared/kit');
const thread = require('openvibe-shared/thread');
const { esc } = require('../render/html');
@@@ -34,7 +33,9 @@ function renderThread({ answer, base, post, remove, signedIn, sort = 'top', repl
const threadUrl = safeHttps(answer && answer.thread && answer.thread.url) || (/^cth_[A-Za-z0-9_-]{22}$/.test(accessId) ? `https://openvibe.community/c/${accessId}` : null);
const validReply = ID.test(String(replyId || '')) ? String(replyId) : null;
const validExpanded = ID.test(String(expandedId || '')) ? String(expandedId) : null;
-- const signIn = `<p class="ovc-signin">Sign in to comment. <a class="ovk-btn ovk-btn--primary" href="/auth/login?next=${encodeURIComponent(query(base, sort))}">Sign in</a></p>`;
++ // Signed out, the composer's spot is one line; with no comments yet that line is the whole empty state (no second
++ // box saying the same thing).
++ const signIn = `<p class="ovc-signin">${comments.length ? 'Sign in to comment.' : 'Be the first to comment.'} <a class="ovk-btn ovk-btn--primary" href="/auth/login?next=${encodeURIComponent(query(base, sort))}">Sign in</a></p>`;
const composer = (parent = null) => signedIn ? thread.composer({ action: post, name: 'message', label: 'Comment', placeholder: parent ? 'Write a reply…' : 'Add a comment…', maxlength: 2000,
sendLabel: parent ? 'Reply' : 'Comment', hidden: parent ? { parent_id: parent } : {},
attach: { name: 'files', accept: 'image/jpeg,image/png,image/gif,image/webp,application/pdf,text/plain' },
@@@ -68,9 +69,10 @@ function renderThread({ answer, base, post, remove, signedIn, sort = 'top', repl
return `<article class="ovc-comment" id="comment-${id}"><header>${avatar ? `<img src="${esc(avatar)}" alt="" width="32" height="32" loading="lazy">` : ''}${profile ? `<a href="${profile}">${esc(name(c))}</a>` : `<span>${esc(name(c))}</span>`}${when.short ? `<time datetime="${esc(c.created_at)}" title="${esc(when.full)}">${esc(when.short)}</time>` : ''}</header>${target}${gone ? '<p>Comment removed</p>' : `<p>${esc(c.message || '')}</p>${attachments(c)}`}${controls}${validReply === id ? `<div data-ovc-composer-slot>${composer(id)}</div>` : ''}${replies}</article>`;
};
const hasReplySlot = validReply && comments.some((c) => String(c.id) === validReply || (validExpanded === String(c.id) && Array.isArray(expandedReplies) ? expandedReplies : c.replies || []).some((r) => String(r.id) === validReply));
-- const order = `<nav class="ovc-order" aria-label="Comment order">${[['top', 'Top'], ['new', 'Newest']].map(([key, label]) => `<a class="ovk-btn ovk-btn--quiet${sort === key ? ' is-active' : ''}" href="${query(base, key)}"${sort === key ? ' aria-current="page"' : ''} data-ovc-order>${label}</a>`).join('')}</nav>`;
++ // Top and Newest only when there is something to put in order.
++ const order = comments.length < 2 && !(answer && answer.next_cursor) ? '' : `<nav class="ovc-order" aria-label="Comment order">${[['top', 'Top'], ['new', 'Newest']].map(([key, label]) => `<a class="ovk-btn ovk-btn--quiet${sort === key ? ' is-active' : ''}" href="${query(base, key)}"${sort === key ? ' aria-current="page"' : ''} data-ovc-order>${label}</a>`).join('')}</nav>`;
const next = answer && answer.next_cursor ? `<a class="ovk-btn ovk-btn--quiet ovc-more" href="${query(base, sort, { after: answer.next_cursor })}" data-ovc-order>More comments</a>` : '';
-- return `<section class="ovc-thread" id="comments" data-ovc-thread><div class="ovc-head"><h2>Comments</h2>${order}</div>${error ? `<p role="alert">${esc(error)}</p>` : ''}${!hasReplySlot ? `<div data-ovc-composer-slot>${composer()}</div>` : ''}${comments.length ? `<div class="ovc-list">${comments.map((c) => comment(c)).join('')}</div>` : kit.empty({ title: 'Be the first to comment' })}${next}${threadUrl ? `<p class="ovc-community"><a href="${esc(threadUrl)}">Discuss on OpenVibe.Community</a></p>` : ''}</section>`;
++ return `<section class="ovc-thread" id="comments" data-ovc-thread><div class="ovc-head"><h2>Comments</h2>${order}</div>${error ? `<p role="alert">${esc(error)}</p>` : ''}${!hasReplySlot ? `<div data-ovc-composer-slot>${composer()}</div>` : ''}${comments.length ? `<div class="ovc-list">${comments.map((c) => comment(c)).join('')}</div>` : (signedIn ? '<p class="ovc-none">Be the first to comment.</p>' : '')}${next}${threadUrl ? `<p class="ovc-community"><a href="${esc(threadUrl)}">Discuss on OpenVibe.Community</a></p>` : ''}</section>`;
}
module.exports = { renderThread, MEDIA_ID, ID };
ddiff --git a/test/comments-inline.test.js b/test/comments-inline.test.js
iindex 729046b..5e8dff0 100644
---- a/test/comments-inline.test.js
++++ b/test/comments-inline.test.js
@@@ -17,7 +17,10 @@ const signedOut = renderThread({ ...options, signedIn: false });
assert.match(signedOut, /class="ovc-thread"/);
assert.match(signedOut, /Sign in to comment/);
assert.doesNotMatch(signedOut, /data-ovt-composer/);
--assert.match(signedOut, /sort=new/);
++assert.doesNotMatch(signedOut, /class="ovc-order"/, 'one top-level comment has nothing to put in order');
++const two = renderThread({ ...options, answer: { ...answer, comments: [...answer.comments, { ...answer.comments[0], id: 20, replies: [], reply_count: 0 }] }, signedIn: false });
++assert.match(two, /class="ovc-order"/);
++assert.match(two, /sort=new/);
assert.match(signedOut, /1 more reply/);
assert.match(signedOut, /href="https:\/\/openvibe.network\/@alice"/);
assert.match(signedOut, /class="ovc-target"/);
@@@ -30,5 +33,8 @@ assert.match(signedIn, /data-ovt-composer/);
assert.match(signedIn, /name="parent_id" value="11"/);
assert.match(signedIn, /data-ovt-files/);
assert.match(signedIn, /name="comment_id" value="10"/);
--assert.match(renderThread({ ...options, answer: { ...answer, comments: [] }, signedIn: false }), /Be the first to comment/);
++const emptyOut = renderThread({ ...options, answer: { ...answer, comments: [] }, signedIn: false });
++assert.match(emptyOut, /<p class="ovc-signin">Be the first to comment\. <a /, 'signed out with no comments: one line, sign-in beside it');
++assert.doesNotMatch(emptyOut, /ovk-empty/, 'no second box saying the same thing');
++assert.match(renderThread({ ...options, answer: { ...answer, comments: [] }, signedIn: true }), /<p class="ovc-none">Be the first to comment\.<\/p>/);
process.stdout.write('Inline thread renders replies, targets, attachments, order, composer and signed-out state\n');
ddiff --git a/test/video-pages.test.js b/test/video-pages.test.js
iindex ae1f83f..bd837d6 100644
---- a/test/video-pages.test.js
++++ b/test/video-pages.test.js
@@@ -789,7 +789,7 @@ function countOccurrences(haystack, needle) {
assert.ok(r.text.includes('href="https://openvibe.community/t/vod-101"'));
});
-- await check('a VOD with no comments uses the kit empty state and keeps the Community link', async () => {
++ await check('a VOD with no comments says so on the sign-in line and keeps the Community link', async () => {
const r = await getVideo('/v/102');
assert.strictEqual(r.status, 200);
assert.ok(r.text.includes('Be the first to comment'));