openvibers/openvibe.shared Commits

Shared 4.1.9: avatars are asked for in the size they are shown (Media's /o/:id?w=&fmt=) (#77)

OP OpenVibers committed 29 days ago · ec10e82913 · 5 files, +31 −3

**Avatars are asked for in the size they are shown (openvibe-shared 4.1.9).**

**Why:** the navbar and the footer load a person's picture at full size. One sampled avatar is a 162 × 152 PNG of 18.6 KB, shown 32 px wide in the navbar and 24 px in the footer. Media's thumbnail variants (Media#118, Fabric F4), followed by the object route, resize a public image object on request and cache the result. At 96 px as WebP that avatar is 606 bytes.

**Change:**
- **`navbar.js` `avatarSrc(user, size)`:** an `https://openvibe.media/o/med_…` picture becomes `?w=<size>&fmt=webp`. The sizes are 64 for the chip, 72 for the menu and 48 for the account switcher, already twice the shown size. Any other address is used as it is. The code is compact to keep `navbar.js` under its 30 KB brotli budget (30,696 bytes).
- **`footer.js`:** the 24 px signed-in avatar asks for 64 px.

Until Media serves object variants, it ignores the query and answers the original.

**Test:** `navbar-auth.test.js` signs in with an object avatar (the chip's `src` carries `?w=64&fmt=webp`) and with a `/f/` file (unchanged). It also checks that the footer sizes an object avatar. The suite passes (58 of 59 files; floating-promises skips without acorn).

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex f666040..337c197 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,14 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 4.1.9 — 2026-10-15
++
++- **Avatars are asked for in the size they are shown.** When a person's picture is an openvibe.media object
++ (`/o/med_…`), the navbar asks for `?w=<size>&fmt=webp` (64 for the chip, 72 for the menu, 48 for the account
++ switcher, already twice the shown size). The footer's 24 px avatar asks for 64. Media answers a resized WebP (Fabric
++ F4, Media's `/o/:id?w=&fmt=`): a sampled 18.6 KB avatar is under 1 KB at 96 px. Any other picture address is used as
++ it is.
++
## 4.1.8 — 2026-10-15
- **Four showcase steps sit two by two below four columns.** In a section narrower than four steps across (under
ddiff --git a/footer.js b/footer.js
iindex 1a6ad6f..e09efa7 100644
---- a/footer.js
++++ b/footer.js
@@@ -276,8 +276,10 @@
// ── Signed-in row: the same account, wherever you are ──────
function accountHTML(user, service) {
const name = esc(user.display_name || user.username || 'you');
++ // An openvibe.media object picture comes in sizes (Media's /o/:id?w=&fmt=): 64 px as WebP for the 24 px avatar.
++ const sized = /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]{10,40}$/.test(user.avatar_url || '') ? `${user.avatar_url}?w=64&fmt=webp` : user.avatar_url;
const avatar = user.avatar_url
-- ? `<img src="${esc(user.avatar_url)}" alt="">`
++ ? `<img src="${esc(sized)}" alt="">`
: `<span>${esc(String(name).charAt(0).toUpperCase())}</span>`;
const quick = [
service !== 'live' ? { name: 'My channel', url: `https://openvibe.live/@${esc(user.username || '')}`, icon: 'channel' } : { name: 'My dashboard', url: '/dashboard', icon: 'dashboard' },
ddiff --git a/navbar.js b/navbar.js
iindex 573b240..e72a873 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -1287,7 +1287,8 @@
}
function avatarSrc(user, size = 64) {
-- return user?.avatar_url || makeAvatarPlaceholder(user, size);
++ const url = user?.avatar_url;
++ return /^https:\/\/openvibe\.media\/o\/med_\w+$/.test(url || '') ? `${url}?w=${size}&fmt=webp` : url || makeAvatarPlaceholder(user, size);
}
function avatarImg(user, size = 64, className = 'openvibe-navbar-avatar', id = '') {
ddiff --git a/package.json b/package.json
iindex 7749c29..8d6529b 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.1.8",
++ "version": "4.1.9",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/navbar-auth.test.js b/test/navbar-auth.test.js
iindex efe94f4..68fabdc 100644
---- a/test/navbar-auth.test.js
++++ b/test/navbar-auth.test.js
@@@ -162,6 +162,23 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
assert.strictEqual(calls.length, 1, 'exactly one auth call');
}
++ // ── 5b. An openvibe.media object avatar is asked for in the chip's size, as WebP (Media's /o/:id?w=&fmt=) ─
++ for (const [avatar, want] of [
++ ['https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN', 'src="https://openvibe.media/o/med_01KZK5GRQ088Y3CW1TMEF4Z4RN?w=64&amp;fmt=webp"'],
++ ['https://openvibe.media/f/b.png', 'src="https://openvibe.media/f/b.png"'],
++ ]) {
++ const { navbar } = loadNavbar({
++ hostname: 'net.openvibe.tools', href: 'https://net.openvibe.tools/', cookie: 'ov_token=valid-jwt',
++ fetch: async (url) => (url === 'https://openvibe.network/api/auth/me' ? jsonResponse(200, { user: { ...USER, avatar_url: avatar }, preferences: {} }) : jsonResponse(404, {})),
++ });
++ navbar.init({ service: 'net', apiBase: 'https://openvibe.network', token: 'valid-jwt' });
++ await navbar.refreshAuth();
++ assert.ok(navbar.getElement().innerHTML.includes(want), `${avatar}: ${want}`);
++ }
++
++ // The footer's 24 px signed-in avatar asks for 64 px WebP of an object picture too.
++ assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'footer.js'), 'utf8'), /\? `\$\{user\.avatar_url\}\?w=64&fmt=webp` : user\.avatar_url;/, 'the footer sizes an object avatar');
++
// ── 6. No token, no sessionUrl → signed out, zero fetches ─
{
const { navbar, calls } = loadNavbar({ hostname: 'maps.openvibe.tools' });