openvibers/openvibe.community Commits
Author avatars load at the size the page draws them (#222)
Changes
@@@ -12,7 +12,7 @@ const ld = require('./jsonld');const { renderPage } = require('./layout');const { escapeHtml: esc } = require('./highlight');const { markdownToText } = require('./markdown');--const { timeTag, fmtDate, num, avatarUrl, authorHtml, wearName } = require('./pages');++const { timeTag, fmtDate, num, avatarUrl, mediaW, authorHtml, wearName } = require('./pages');const f = require('./forum');const STYLE_LABELS = { forum: 'Classic', feed: 'Feed' }; // the two styles, as the Forum page and the settings name them@@@ -176,7 +176,7 @@ ${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moderfunction authorPanel(p) {const a = p.author;if (!a) return '<aside class="postbit-author"><span class="postbit-name">Anonymous</span></aside>';-- const pic = avatarUrl(a, 96);++ const pic = mediaW(avatarUrl(a, 96), 160);const name = a.display_name || a.username || (a.is_ai ? 'AI' : a.is_system ? 'OpenVibe' : 'Someone');const initial = esc(String(name).trim()[0] || '?').toUpperCase();const st = p.author_stats;@@@ -28,6 +28,12 @@ function mediaAbs(u) {if (/^https?:\/\//i.test(u)) return u;return `${config.mediaUrl}${u.startsWith('/') ? '' : '/'}${u}`;}++// A Media object is stored at full size: ask its resize ladder for the width drawn here (96 for the 22 px author++// avatar, 160 for the 64 px postbit one). Any other picture address — the network's avatar endpoint, a picture that++// already carries a query — is used unchanged.++function mediaW(u, w) {++ return /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(String(u || '')) ? `${u}?w=${w}` : u;++}/** A screenshot's Media address when the stored row has no screenshot_url. */function screenshotFallback(slug) { return `${config.mediaUrl}/p/${encodeURIComponent(slug)}/screenshot`; }@@@ -76,7 +82,7 @@ function authorHtml(p, { link = true } = {}) {// picture, or a generated initial), so an author looks the same here as on every other OpenVibe site.const netAvatar = !avatarSrc && p.username ? `${config.networkUrl}/avatar/${encodeURIComponent(p.username)}?s=44` : null;const avatar = (avatarSrc || netAvatar)-- ? `<img class="avatar" src="${esc(avatarSrc || netAvatar)}" alt="" loading="lazy" width="22" height="22">`++ ? `<img class="avatar" src="${esc(mediaW(avatarSrc || netAvatar, 96))}" alt="" loading="lazy" width="22" height="22">`: `<span class="avatar avatar-letter" aria-hidden="true">${initial}</span>`;const wearer = wearerOf(p);const inner = `${avatar}${wearer ? wearName(p, name) : `<span>${esc(name)}</span>`}`;@@@ -597,4 +603,4 @@ function errorPage({ status = 500, title = 'Something went wrong', message = '',return renderPage({ title, description: message || title, canonicalPath: '/', robots: 'noindex,nofollow', footerVariant: 'compact', body });}--module.exports = { homePage, updatesPage, browsePage, pastePage, newPage, myPage, errorPage, pasteCard, cardGrid, timeAgo, timeTag, fmtDate, num, authorHtml, avatarUrl, wearName };++module.exports = { homePage, updatesPage, browsePage, pastePage, newPage, myPage, errorPage, pasteCard, cardGrid, timeAgo, timeTag, fmtDate, num, authorHtml, avatarUrl, mediaW, wearName };@@@ -50,6 +50,14 @@ const { boot, check, done } = require('./helpers/app');assert.ok(!page.includes('class="thread-score"'), 'no score column');});++ await check('the postbit avatar asks Media for the width it draws (the 64 px picture: 160)', async () => {++ const obj = 'https://openvibe.media/o/med_01M644DY8BFX4H7MTRKPGG052R';++ await t.db.prepare('UPDATE subject_projection SET avatar_url = ? WHERE subject_id = ?').run(obj, alex.subject_id);++ const html = (await call('/s/general/t/welcome-thread')).text;++ assert.ok(html.includes(`<img class="postbit-avatar" src="${obj}?w=160"`), 'the 64 px postbit avatar asks for 160');++ await t.db.prepare('UPDATE subject_projection SET avatar_url = NULL WHERE subject_id = ?').run(alex.subject_id);++ });++await check('the board index: groups in order, every space with topics, posts and the last post', async () => {const r = (await call('/api/v1/spaces')).json();const general = r.groups[0].spaces.find((s) => s.slug === 'general');@@@ -188,6 +188,16 @@ const { boot, check, done } = require('./helpers/app');assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');});++ await check('an author avatar on a Media object asks for the width the page draws; other addresses are left alone', () => {++ const { authorHtml } = require('../server/render/pages');++ const obj = 'https://openvibe.media/o/med_01M644DY8BFX4H7MTRKPGG052R';++ assert.ok(authorHtml({ display_name: 'Alex', username: 'alex', avatar_url: obj }).includes(`<img class="avatar" src="${obj}?w=96"`), 'the 22 px author avatar asks for 96');++ const sized = authorHtml({ display_name: 'Alex', username: 'alex', avatar_url: `${obj}?w=64` });++ assert.ok(sized.includes(`src="${obj}?w=64"`) && !sized.includes('?w=64?w='), 'an already sized URL is used as stored, with no second query');++ const relative = authorHtml({ display_name: 'Alex', username: 'alex', avatar_url: '/data/avatars/alex.png' });++ assert.ok(relative.includes('/data/avatars/alex.png"') && !relative.includes('/data/avatars/alex.png?w='), 'a non-Media avatar is used unchanged');++ });++await t.close();done();})();
@@ -12,7 +12,7 @@ const ld = require('./jsonld');
@@ -12,7 +12,7 @@ const ld = require('./jsonld');
const { renderPage } = require('./layout');
const { renderPage } = require('./layout');
const { escapeHtml: esc } = require('./highlight');
const { escapeHtml: esc } = require('./highlight');
const { markdownToText } = require('./markdown');
const { markdownToText } = require('./markdown');
-const { timeTag, fmtDate, num, avatarUrl, authorHtml, wearName } = require('./pages');
+const { timeTag, fmtDate, num, avatarUrl, mediaW, authorHtml, wearName } = require('./pages');
const f = require('./forum');
const f = require('./forum');
const STYLE_LABELS = { forum: 'Classic', feed: 'Feed' }; // the two styles, as the Forum page and the settings name them
const STYLE_LABELS = { forum: 'Classic', feed: 'Feed' }; // the two styles, as the Forum page and the settings name them
@@ -176,7 +176,7 @@ ${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moder
@@ -176,7 +176,7 @@ ${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moder
function authorPanel(p) {
function authorPanel(p) {
const a = p.author;
const a = p.author;
if (!a) return '<aside class="postbit-author"><span class="postbit-name">Anonymous</span></aside>';
if (!a) return '<aside class="postbit-author"><span class="postbit-name">Anonymous</span></aside>';
- const pic = avatarUrl(a, 96);
+ const pic = mediaW(avatarUrl(a, 96), 160);
const name = a.display_name || a.username || (a.is_ai ? 'AI' : a.is_system ? 'OpenVibe' : 'Someone');
const name = a.display_name || a.username || (a.is_ai ? 'AI' : a.is_system ? 'OpenVibe' : 'Someone');
const initial = esc(String(name).trim()[0] || '?').toUpperCase();
const initial = esc(String(name).trim()[0] || '?').toUpperCase();
const st = p.author_stats;
const st = p.author_stats;
@@ -28,6 +28,12 @@ function mediaAbs(u) {
@@ -28,6 +28,12 @@ function mediaAbs(u) {
if (/^https?:\/\//i.test(u)) return u;
if (/^https?:\/\//i.test(u)) return u;
return `${config.mediaUrl}${u.startsWith('/') ? '' : '/'}${u}`;
return `${config.mediaUrl}${u.startsWith('/') ? '' : '/'}${u}`;
}
}
+// A Media object is stored at full size: ask its resize ladder for the width drawn here (96 for the 22 px author
+// avatar, 160 for the 64 px postbit one). Any other picture address — the network's avatar endpoint, a picture that
+// already carries a query — is used unchanged.
+function mediaW(u, w) {
+ return /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(String(u || '')) ? `${u}?w=${w}` : u;
+}
/** A screenshot's Media address when the stored row has no screenshot_url. */
/** A screenshot's Media address when the stored row has no screenshot_url. */
function screenshotFallback(slug) { return `${config.mediaUrl}/p/${encodeURIComponent(slug)}/screenshot`; }
function screenshotFallback(slug) { return `${config.mediaUrl}/p/${encodeURIComponent(slug)}/screenshot`; }
@@ -76,7 +82,7 @@ function authorHtml(p, { link = true } = {}) {
@@ -76,7 +82,7 @@ function authorHtml(p, { link = true } = {}) {
// picture, or a generated initial), so an author looks the same here as on every other OpenVibe site.
// picture, or a generated initial), so an author looks the same here as on every other OpenVibe site.
const netAvatar = !avatarSrc && p.username ? `${config.networkUrl}/avatar/${encodeURIComponent(p.username)}?s=44` : null;
const netAvatar = !avatarSrc && p.username ? `${config.networkUrl}/avatar/${encodeURIComponent(p.username)}?s=44` : null;
const avatar = (avatarSrc || netAvatar)
const avatar = (avatarSrc || netAvatar)
- ? `<img class="avatar" src="${esc(avatarSrc || netAvatar)}" alt="" loading="lazy" width="22" height="22">`
+ ? `<img class="avatar" src="${esc(mediaW(avatarSrc || netAvatar, 96))}" alt="" loading="lazy" width="22" height="22">`
: `<span class="avatar avatar-letter" aria-hidden="true">${initial}</span>`;
: `<span class="avatar avatar-letter" aria-hidden="true">${initial}</span>`;
const wearer = wearerOf(p);
const wearer = wearerOf(p);
const inner = `${avatar}${wearer ? wearName(p, name) : `<span>${esc(name)}</span>`}`;
const inner = `${avatar}${wearer ? wearName(p, name) : `<span>${esc(name)}</span>`}`;
@@ -597,4 +603,4 @@ function errorPage({ status = 500, title = 'Something went wrong', message = '',
@@ -597,4 +603,4 @@ function errorPage({ status = 500, title = 'Something went wrong', message = '',
return renderPage({ title, description: message || title, canonicalPath: '/', robots: 'noindex,nofollow', footerVariant: 'compact', body });
return renderPage({ title, description: message || title, canonicalPath: '/', robots: 'noindex,nofollow', footerVariant: 'compact', body });
}
}
-module.exports = { homePage, updatesPage, browsePage, pastePage, newPage, myPage, errorPage, pasteCard, cardGrid, timeAgo, timeTag, fmtDate, num, authorHtml, avatarUrl, wearName };
+module.exports = { homePage, updatesPage, browsePage, pastePage, newPage, myPage, errorPage, pasteCard, cardGrid, timeAgo, timeTag, fmtDate, num, authorHtml, avatarUrl, mediaW, wearName };
@@ -50,6 +50,14 @@ const { boot, check, done } = require('./helpers/app');
@@ -50,6 +50,14 @@ const { boot, check, done } = require('./helpers/app');
assert.ok(!page.includes('class="thread-score"'), 'no score column');
assert.ok(!page.includes('class="thread-score"'), 'no score column');
});
});
+ await check('the postbit avatar asks Media for the width it draws (the 64 px picture: 160)', async () => {
+ const obj = 'https://openvibe.media/o/med_01M644DY8BFX4H7MTRKPGG052R';
+ await t.db.prepare('UPDATE subject_projection SET avatar_url = ? WHERE subject_id = ?').run(obj, alex.subject_id);
+ const html = (await call('/s/general/t/welcome-thread')).text;
+ assert.ok(html.includes(`<img class="postbit-avatar" src="${obj}?w=160"`), 'the 64 px postbit avatar asks for 160');
+ await t.db.prepare('UPDATE subject_projection SET avatar_url = NULL WHERE subject_id = ?').run(alex.subject_id);
+ });
+
await check('the board index: groups in order, every space with topics, posts and the last post', async () => {
await check('the board index: groups in order, every space with topics, posts and the last post', async () => {
const r = (await call('/api/v1/spaces')).json();
const r = (await call('/api/v1/spaces')).json();
const general = r.groups[0].spaces.find((s) => s.slug === 'general');
const general = r.groups[0].spaces.find((s) => s.slug === 'general');
@@ -188,6 +188,16 @@ const { boot, check, done } = require('./helpers/app');
@@ -188,6 +188,16 @@ const { boot, check, done } = require('./helpers/app');
assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');
assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');
});
});
+ await check('an author avatar on a Media object asks for the width the page draws; other addresses are left alone', () => {
+ const { authorHtml } = require('../server/render/pages');
+ const obj = 'https://openvibe.media/o/med_01M644DY8BFX4H7MTRKPGG052R';
+ assert.ok(authorHtml({ display_name: 'Alex', username: 'alex', avatar_url: obj }).includes(`<img class="avatar" src="${obj}?w=96"`), 'the 22 px author avatar asks for 96');
+ const sized = authorHtml({ display_name: 'Alex', username: 'alex', avatar_url: `${obj}?w=64` });
+ assert.ok(sized.includes(`src="${obj}?w=64"`) && !sized.includes('?w=64?w='), 'an already sized URL is used as stored, with no second query');
+ const relative = authorHtml({ display_name: 'Alex', username: 'alex', avatar_url: '/data/avatars/alex.png' });
+ assert.ok(relative.includes('/data/avatars/alex.png"') && !relative.includes('/data/avatars/alex.png?w='), 'a non-Media avatar is used unchanged');
+ });
+
await t.close();
await t.close();
done();
done();
})();
})();