openvibers/openvibe.shared Commits
Shared 2.19.0: items.js draws community badges (network.badge.image@1) — the Workshop badge's image before the name, straight from the equipped read (#42)
Changes
@@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking 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).++## 2.19.0 — 2026-10-09++++- `items.js` draws **community badges** (`network.badge.image@1`, OpenVibe.Inventory's Workshop): the badge's++ reviewed image, from OpenVibe.Media, in an `img.ov-badge` just before the name. The image comes straight from the++ equipped read's `media_id`, with no extra request, and only ids of the `med_…` shape become a URL. items.css sizes it++ to the text.++## 2.18.0 — 2026-10-09- `items.js` draws two more renderers. **Hats** (`live.hat.glyph@1`): the emoji just before the name, with Live's@@@ -668,7 +668,8 @@ no credentials, each answer kept 60 s) and gives each name its renderers' classeand only the new ones are read. Renderers: `live.name_effect.css@1` → `ov-fx` + the item's token (Live's 13 nameeffects, `name-fx-rainbow` … `name-fx-divine`); `live.hat.glyph@1` → the hat's emoji just before the name(`ov-hat`, with its motion); `live.particle.css@1` → `ov-px` + the token, and a burst of the item's characters when--someone points at or focuses the name. Hats and particles read Live's definitions once per page. A server that already knows the token writes the class itself. The++someone points at or focuses the name. Hats and particles read Live's definitions once per page. `network.badge.image@1` → a++community badge's image (`img.ov-badge`) before the name, from the equipped read's `media_id`. A server that already knows the token writes the class itself. TheCalm setting and `prefers-reduced-motion` stop the animation. `window.OpenVibeItemsConfig = { api }` points it atanother Inventory (development).@@@ -5,6 +5,7 @@* live.name_effect.css@1 the item's art.token is a class on the name: <span class="ov-fx name-fx-rainbow">Ana</span>* live.hat.glyph@1 the hat's emoji before the name: <span class="ov-hat ov-hat-pulse">👑</span>* live.particle.css@1 `ov-px` + the token on the name; items.js spawns `.ov-px-p` characters in a burst++ * network.badge.image@1 <img class="ov-badge"> before the name (a Workshop badge, from OpenVibe.Media)* items.js adds the classes from the person's equipped set; a page that already knows the token (a server-rendered* profile) writes the class itself. These are Live's own name effects (OpenVibe.Live public/css/cosmetics.css),* unchanged, so a name looks the same in Live's chat and everywhere else. The network's Calm setting@@@ -180,6 +181,9 @@50% { background-position: 300% 50%; }}++/* Community badges (network.badge.image@1): the reviewed image, a little larger than the text, just before the name. */++.ov-badge { display: inline-block; width: 1.15em; height: 1.15em; margin-right: 3px; vertical-align: -0.2em; border-radius: 3px; object-fit: contain; }++/* Hats (live.hat.glyph@1): the emoji just before the name, Live's float/pulse/warp motions. */.ov-hat { display: inline-block; margin-right: 2px; font-size: 0.9em; line-height: 1; vertical-align: -0.05em; }.ov-hat-float { animation: ovHatFloat 2s ease-in-out infinite; }@@@ -12,6 +12,8 @@* live.name_effect.css@1 `ov-fx` + the token (name-fx-rainbow, …) on the name: items.css draws it* live.hat.glyph@1 the hat's emoji just before the name (`ov-hat`, with its float/pulse/warp animation)* live.particle.css@1 a short burst of the item's characters when you point at or focus the name (`ov-px`)++ * network.badge.image@1 a community badge (the Workshop): its reviewed image just before the name (`ov-badge`),++ * straight from the equipped read's media_id, served by OpenVibe.Media* Hats and particles need their definitions (emoji, animation, characters): one public read of Live's definitions per* page, only when someone on it wears one. Anything else is left alone. Inventory unreachable: names stay as they* are. Only tokens of the known shape become class names; emoji and characters are text, never markup. A burst never@@@ -22,6 +24,7 @@const SUBJECT = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;const NAME_FX = /^name-fx-[a-z]{2,24}$/;const PX = /^px-[a-z]{2,24}$/;++ const MEDIA_ID = /^med_[0-9A-HJKMNP-TV-Z]{26}$/;const HAT_MOTION = ['float', 'pulse', 'warp'];const BATCH = 100;const TTL_MS = 60 * 1000;@@@ -100,8 +103,7 @@/** Give one element what a set of slots draws (removing what an earlier set drew). defs: Map from definitions(). */function apply(el, slots, defs) {for (const c of [...el.classList]) if (c === 'ov-fx' || NAME_FX.test(c) || c === 'ov-px' || PX.test(c)) el.classList.remove(c);-- const prev = el.previousElementSibling;-- if (prev && prev.classList && prev.classList.contains('ov-hat')) prev.remove();++ for (let prev = el.previousElementSibling; prev && prev.classList && (prev.classList.contains('ov-hat') || prev.classList.contains('ov-badge')); prev = el.previousElementSibling) prev.remove();el.removeEventListener('mouseenter', onBurst);el.removeEventListener('focusin', onBurst);delete el.dataset.ovPxChars;@@@ -109,6 +111,18 @@const fx = slotOf(slots, 'live.name_effect');if (fx && NAME_FX.test(String(fx.token || ''))) el.classList.add('ov-fx', fx.token);++ const badge = slotOf(slots, 'network.badge');++ if (badge && MEDIA_ID.test(String(badge.media_id || '')) && el.parentNode) {++ const b = el.ownerDocument.createElement('img');++ b.className = 'ov-badge';++ b.src = `${String((root.OpenVibeItemsConfig && root.OpenVibeItemsConfig.media) || 'https://openvibe.media').replace(/\/+$/, '')}/o/${badge.media_id}`;++ b.alt = '';++ b.width = 16; b.height = 16;++ b.setAttribute('loading', 'lazy');++ b.setAttribute('aria-hidden', 'true');++ el.parentNode.insertBefore(b, el);++ }++const hat = slotOf(slots, 'live.hat');const hatDef = hat && defs && defs.get(hat.definition_id);const glyph = hatDef && String(hatDef.art.emoji || hatDef.attributes.hat_char || '').slice(0, 8);@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "2.18.0",++ "version": "2.19.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "2.18.0",++ "version": "2.19.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "2.18.0",++ "version": "2.19.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -12,6 +12,7 @@ const { parseHTML } = require('linkedom');const A = 'usr_01JZ0000000000000000000AAA';const B = 'usr_01JZ0000000000000000000BBB';const C = 'usr_01JZ0000000000000000000CCC';++const D = 'usr_01JZ0000000000000000000DDD';const DEFS = [{ id: 'itd_2', kind: 'live.hat', art: { emoji: '👑' }, attributes: { hat_char: '👑', animated: 'pulse' } },{ id: 'itd_4', kind: 'live.particle', art: { emoji: '✨', token: 'px-sparkle' }, attributes: { chars: '✦✧⋆' } },@@@ -33,7 +34,8 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;subject: s,slots: s === A ? { 'live.name_effect:name_effect': { instance_id: 'inv_1', definition_id: 'itd_1', token: 'name-fx-rainbow' }, 'live.hat:hat': { instance_id: 'inv_2', definition_id: 'itd_2', token: 'hat-crown' } }: s === B ? { 'live.name_effect:name_effect': { instance_id: 'inv_3', definition_id: 'itd_3', token: 'evil" onload="x' } }-- : s === C ? { 'live.particle:particle': { instance_id: 'inv_4', definition_id: 'itd_4', token: 'px-sparkle' } } : {},++ : s === C ? { 'live.particle:particle': { instance_id: 'inv_4', definition_id: 'itd_4', token: 'px-sparkle' } }++ : s === D ? { 'network.badge:badge': { instance_id: 'inv_5', definition_id: 'itd_5', media_id: 'med_01JZ00000000000000000000B1' } } : {},})),}),};@@@ -42,12 +44,12 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;const items = global.OpenVibeItems;assert.ok(items && typeof items.decorate === 'function');-- const { document } = parseHTML(`<main><a id="a" data-ov-subject="${A}">Ana</a><span id="b" data-ov-subject="${B}">Bob</span><span id="c" data-ov-subject="nope">C</span><b><span id="p" data-ov-subject="${C}">Cleo</span></b>++ const { document } = parseHTML(`<main><a id="a" data-ov-subject="${A}">Ana</a><span id="b" data-ov-subject="${B}">Bob</span><span id="c" data-ov-subject="nope">C</span><b><span id="p" data-ov-subject="${C}">Cleo</span></b><i><span id="d" data-ov-subject="${D}">Dee</span></i>${Array.from({ length: 150 }, (_, i) => `<span class="many" data-ov-subject="${subjectOf(i)}">p${i}</span>`).join('')}</main>`);-- assert.strictEqual(await items.decorate(document), 153, 'every valid subject, the invalid one skipped');++ assert.strictEqual(await items.decorate(document), 154, 'every valid subject, the invalid one skipped');const sets = calls.filter((c) => c.url.includes('/equipped?'));-- assert.strictEqual(sets.length, 2, '153 people in two requests (100 + 53)');++ assert.strictEqual(sets.length, 2, '154 people in two requests (100 + 54)');assert.strictEqual(calls.filter((c) => c.url.includes('/definitions?')).length, 1, 'definitions once, because someone wears a hat or particles');assert.ok(calls.every((c) => c.url.startsWith('https://inventory.openvibe.network/api/v1/') && c.opts.credentials === 'omit'));assert.deepStrictEqual([...document.getElementById('a').classList].sort(), ['name-fx-rainbow', 'ov-fx'], 'Ana wears her rainbow');@@@ -56,6 +58,11 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;const cleo = document.getElementById('p');assert.deepStrictEqual([...cleo.classList].sort(), ['ov-px', 'px-sparkle'], 'Cleo wears particles');assert.strictEqual(cleo.dataset.ovPxChars, '✦✧⋆');++ const dee = document.getElementById('d');++ const b = dee.previousElementSibling;++ assert.ok(b && b.tagName === 'IMG' && b.className === 'ov-badge' && b.getAttribute('src') === 'https://openvibe.media/o/med_01JZ00000000000000000000B1' && b.getAttribute('alt') === '', 'Dee\'s community badge before her name');++ items.apply(dee, { 'network.badge:badge': { media_id: 'javascript:alert(1)' } });++ assert.ok(!(dee.previousElementSibling && dee.previousElementSibling.classList.contains('ov-badge')), 'a bad media id draws nothing, and the old badge is gone');items.burst(cleo);assert.strictEqual(cleo.querySelectorAll('.ov-px-p').length, 6, 'a burst: six characters');assert.ok([...cleo.querySelectorAll('.ov-px-p')].every((p) => '✦✧⋆'.includes(p.textContent)));@@@ -94,6 +101,7 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;assert.match(css, /\.ov-fx \{ display: inline-block; \}/);assert.match(css, /prefers-reduced-motion: reduce[\s\S]*animation: none !important/);assert.match(css, /\.ov-hat-pulse \{ animation: ovHatPulse/);++ assert.match(css, /\.ov-badge \{ display: inline-block;/);assert.match(css, /\.px-sparkle \.ov-px-p \{ color: #ffd700; \}/);assert.match(css, /prefers-reduced-motion: reduce[\s\S]*\.ov-px-p \{ display: none; \}/);console.log('items: all checks passed');
@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,13 @@ 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.
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).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
+## 2.19.0 — 2026-10-09
+
+- `items.js` draws **community badges** (`network.badge.image@1`, OpenVibe.Inventory's Workshop): the badge's
+ reviewed image, from OpenVibe.Media, in an `img.ov-badge` just before the name. The image comes straight from the
+ equipped read's `media_id`, with no extra request, and only ids of the `med_…` shape become a URL. items.css sizes it
+ to the text.
+
## 2.18.0 — 2026-10-09
## 2.18.0 — 2026-10-09
- `items.js` draws two more renderers. **Hats** (`live.hat.glyph@1`): the emoji just before the name, with Live's
- `items.js` draws two more renderers. **Hats** (`live.hat.glyph@1`): the emoji just before the name, with Live's
@@ -668,7 +668,8 @@ no credentials, each answer kept 60 s) and gives each name its renderers' classe
@@ -668,7 +668,8 @@ no credentials, each answer kept 60 s) and gives each name its renderers' classe
and only the new ones are read. Renderers: `live.name_effect.css@1` → `ov-fx` + the item's token (Live's 13 name
and only the new ones are read. Renderers: `live.name_effect.css@1` → `ov-fx` + the item's token (Live's 13 name
effects, `name-fx-rainbow` … `name-fx-divine`); `live.hat.glyph@1` → the hat's emoji just before the name
effects, `name-fx-rainbow` … `name-fx-divine`); `live.hat.glyph@1` → the hat's emoji just before the name
(`ov-hat`, with its motion); `live.particle.css@1` → `ov-px` + the token, and a burst of the item's characters when
(`ov-hat`, with its motion); `live.particle.css@1` → `ov-px` + the token, and a burst of the item's characters when
-someone points at or focuses the name. Hats and particles read Live's definitions once per page. A server that already knows the token writes the class itself. The
+someone points at or focuses the name. Hats and particles read Live's definitions once per page. `network.badge.image@1` → a
+community badge's image (`img.ov-badge`) before the name, from the equipped read's `media_id`. A server that already knows the token writes the class itself. The
Calm setting and `prefers-reduced-motion` stop the animation. `window.OpenVibeItemsConfig = { api }` points it at
Calm setting and `prefers-reduced-motion` stop the animation. `window.OpenVibeItemsConfig = { api }` points it at
another Inventory (development).
another Inventory (development).
@@ -5,6 +5,7 @@
@@ -5,6 +5,7 @@
* live.name_effect.css@1 the item's art.token is a class on the name: <span class="ov-fx name-fx-rainbow">Ana</span>
* live.name_effect.css@1 the item's art.token is a class on the name: <span class="ov-fx name-fx-rainbow">Ana</span>
* live.hat.glyph@1 the hat's emoji before the name: <span class="ov-hat ov-hat-pulse">👑</span>
* live.hat.glyph@1 the hat's emoji before the name: <span class="ov-hat ov-hat-pulse">👑</span>
* live.particle.css@1 `ov-px` + the token on the name; items.js spawns `.ov-px-p` characters in a burst
* live.particle.css@1 `ov-px` + the token on the name; items.js spawns `.ov-px-p` characters in a burst
+ * network.badge.image@1 <img class="ov-badge"> before the name (a Workshop badge, from OpenVibe.Media)
* items.js adds the classes from the person's equipped set; a page that already knows the token (a server-rendered
* items.js adds the classes from the person's equipped set; a page that already knows the token (a server-rendered
* profile) writes the class itself. These are Live's own name effects (OpenVibe.Live public/css/cosmetics.css),
* profile) writes the class itself. These are Live's own name effects (OpenVibe.Live public/css/cosmetics.css),
* unchanged, so a name looks the same in Live's chat and everywhere else. The network's Calm setting
* unchanged, so a name looks the same in Live's chat and everywhere else. The network's Calm setting
@@ -180,6 +181,9 @@
@@ -180,6 +181,9 @@
50% { background-position: 300% 50%; }
50% { background-position: 300% 50%; }
}
}
+/* Community badges (network.badge.image@1): the reviewed image, a little larger than the text, just before the name. */
+.ov-badge { display: inline-block; width: 1.15em; height: 1.15em; margin-right: 3px; vertical-align: -0.2em; border-radius: 3px; object-fit: contain; }
+
/* Hats (live.hat.glyph@1): the emoji just before the name, Live's float/pulse/warp motions. */
/* Hats (live.hat.glyph@1): the emoji just before the name, Live's float/pulse/warp motions. */
.ov-hat { display: inline-block; margin-right: 2px; font-size: 0.9em; line-height: 1; vertical-align: -0.05em; }
.ov-hat { display: inline-block; margin-right: 2px; font-size: 0.9em; line-height: 1; vertical-align: -0.05em; }
.ov-hat-float { animation: ovHatFloat 2s ease-in-out infinite; }
.ov-hat-float { animation: ovHatFloat 2s ease-in-out infinite; }
@@ -12,6 +12,8 @@
@@ -12,6 +12,8 @@
* live.name_effect.css@1 `ov-fx` + the token (name-fx-rainbow, …) on the name: items.css draws it
* live.name_effect.css@1 `ov-fx` + the token (name-fx-rainbow, …) on the name: items.css draws it
* live.hat.glyph@1 the hat's emoji just before the name (`ov-hat`, with its float/pulse/warp animation)
* live.hat.glyph@1 the hat's emoji just before the name (`ov-hat`, with its float/pulse/warp animation)
* live.particle.css@1 a short burst of the item's characters when you point at or focus the name (`ov-px`)
* live.particle.css@1 a short burst of the item's characters when you point at or focus the name (`ov-px`)
+ * network.badge.image@1 a community badge (the Workshop): its reviewed image just before the name (`ov-badge`),
+ * straight from the equipped read's media_id, served by OpenVibe.Media
* Hats and particles need their definitions (emoji, animation, characters): one public read of Live's definitions per
* Hats and particles need their definitions (emoji, animation, characters): one public read of Live's definitions per
* page, only when someone on it wears one. Anything else is left alone. Inventory unreachable: names stay as they
* page, only when someone on it wears one. Anything else is left alone. Inventory unreachable: names stay as they
* are. Only tokens of the known shape become class names; emoji and characters are text, never markup. A burst never
* are. Only tokens of the known shape become class names; emoji and characters are text, never markup. A burst never
@@ -22,6 +24,7 @@
@@ -22,6 +24,7 @@
const SUBJECT = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const SUBJECT = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const NAME_FX = /^name-fx-[a-z]{2,24}$/;
const NAME_FX = /^name-fx-[a-z]{2,24}$/;
const PX = /^px-[a-z]{2,24}$/;
const PX = /^px-[a-z]{2,24}$/;
+ const MEDIA_ID = /^med_[0-9A-HJKMNP-TV-Z]{26}$/;
const HAT_MOTION = ['float', 'pulse', 'warp'];
const HAT_MOTION = ['float', 'pulse', 'warp'];
const BATCH = 100;
const BATCH = 100;
const TTL_MS = 60 * 1000;
const TTL_MS = 60 * 1000;
@@ -100,8 +103,7 @@
@@ -100,8 +103,7 @@
/** Give one element what a set of slots draws (removing what an earlier set drew). defs: Map from definitions(). */
/** Give one element what a set of slots draws (removing what an earlier set drew). defs: Map from definitions(). */
function apply(el, slots, defs) {
function apply(el, slots, defs) {
for (const c of [...el.classList]) if (c === 'ov-fx' || NAME_FX.test(c) || c === 'ov-px' || PX.test(c)) el.classList.remove(c);
for (const c of [...el.classList]) if (c === 'ov-fx' || NAME_FX.test(c) || c === 'ov-px' || PX.test(c)) el.classList.remove(c);
- const prev = el.previousElementSibling;
- if (prev && prev.classList && prev.classList.contains('ov-hat')) prev.remove();
+ for (let prev = el.previousElementSibling; prev && prev.classList && (prev.classList.contains('ov-hat') || prev.classList.contains('ov-badge')); prev = el.previousElementSibling) prev.remove();
el.removeEventListener('mouseenter', onBurst);
el.removeEventListener('mouseenter', onBurst);
el.removeEventListener('focusin', onBurst);
el.removeEventListener('focusin', onBurst);
delete el.dataset.ovPxChars;
delete el.dataset.ovPxChars;
@@ -109,6 +111,18 @@
@@ -109,6 +111,18 @@
const fx = slotOf(slots, 'live.name_effect');
const fx = slotOf(slots, 'live.name_effect');
if (fx && NAME_FX.test(String(fx.token || ''))) el.classList.add('ov-fx', fx.token);
if (fx && NAME_FX.test(String(fx.token || ''))) el.classList.add('ov-fx', fx.token);
+ const badge = slotOf(slots, 'network.badge');
+ if (badge && MEDIA_ID.test(String(badge.media_id || '')) && el.parentNode) {
+ const b = el.ownerDocument.createElement('img');
+ b.className = 'ov-badge';
+ b.src = `${String((root.OpenVibeItemsConfig && root.OpenVibeItemsConfig.media) || 'https://openvibe.media').replace(/\/+$/, '')}/o/${badge.media_id}`;
+ b.alt = '';
+ b.width = 16; b.height = 16;
+ b.setAttribute('loading', 'lazy');
+ b.setAttribute('aria-hidden', 'true');
+ el.parentNode.insertBefore(b, el);
+ }
+
const hat = slotOf(slots, 'live.hat');
const hat = slotOf(slots, 'live.hat');
const hatDef = hat && defs && defs.get(hat.definition_id);
const hatDef = hat && defs && defs.get(hat.definition_id);
const glyph = hatDef && String(hatDef.art.emoji || hatDef.attributes.hat_char || '').slice(0, 8);
const glyph = hatDef && String(hatDef.art.emoji || hatDef.attributes.hat_char || '').slice(0, 8);
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.18.0",
+ "version": "2.19.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.18.0",
+ "version": "2.19.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.18.0",
+ "version": "2.19.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -12,6 +12,7 @@ const { parseHTML } = require('linkedom');
@@ -12,6 +12,7 @@ const { parseHTML } = require('linkedom');
const A = 'usr_01JZ0000000000000000000AAA';
const A = 'usr_01JZ0000000000000000000AAA';
const B = 'usr_01JZ0000000000000000000BBB';
const B = 'usr_01JZ0000000000000000000BBB';
const C = 'usr_01JZ0000000000000000000CCC';
const C = 'usr_01JZ0000000000000000000CCC';
+const D = 'usr_01JZ0000000000000000000DDD';
const DEFS = [
const DEFS = [
{ id: 'itd_2', kind: 'live.hat', art: { emoji: '👑' }, attributes: { hat_char: '👑', animated: 'pulse' } },
{ id: 'itd_2', kind: 'live.hat', art: { emoji: '👑' }, attributes: { hat_char: '👑', animated: 'pulse' } },
{ id: 'itd_4', kind: 'live.particle', art: { emoji: '✨', token: 'px-sparkle' }, attributes: { chars: '✦✧⋆' } },
{ id: 'itd_4', kind: 'live.particle', art: { emoji: '✨', token: 'px-sparkle' }, attributes: { chars: '✦✧⋆' } },
@@ -33,7 +34,8 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;
@@ -33,7 +34,8 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;
subject: s,
subject: s,
slots: s === A ? { 'live.name_effect:name_effect': { instance_id: 'inv_1', definition_id: 'itd_1', token: 'name-fx-rainbow' }, 'live.hat:hat': { instance_id: 'inv_2', definition_id: 'itd_2', token: 'hat-crown' } }
slots: s === A ? { 'live.name_effect:name_effect': { instance_id: 'inv_1', definition_id: 'itd_1', token: 'name-fx-rainbow' }, 'live.hat:hat': { instance_id: 'inv_2', definition_id: 'itd_2', token: 'hat-crown' } }
: s === B ? { 'live.name_effect:name_effect': { instance_id: 'inv_3', definition_id: 'itd_3', token: 'evil" onload="x' } }
: s === B ? { 'live.name_effect:name_effect': { instance_id: 'inv_3', definition_id: 'itd_3', token: 'evil" onload="x' } }
- : s === C ? { 'live.particle:particle': { instance_id: 'inv_4', definition_id: 'itd_4', token: 'px-sparkle' } } : {},
+ : s === C ? { 'live.particle:particle': { instance_id: 'inv_4', definition_id: 'itd_4', token: 'px-sparkle' } }
+ : s === D ? { 'network.badge:badge': { instance_id: 'inv_5', definition_id: 'itd_5', media_id: 'med_01JZ00000000000000000000B1' } } : {},
})),
})),
}),
}),
};
};
@@ -42,12 +44,12 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;
@@ -42,12 +44,12 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;
const items = global.OpenVibeItems;
const items = global.OpenVibeItems;
assert.ok(items && typeof items.decorate === 'function');
assert.ok(items && typeof items.decorate === 'function');
- const { document } = parseHTML(`<main><a id="a" data-ov-subject="${A}">Ana</a><span id="b" data-ov-subject="${B}">Bob</span><span id="c" data-ov-subject="nope">C</span><b><span id="p" data-ov-subject="${C}">Cleo</span></b>
+ const { document } = parseHTML(`<main><a id="a" data-ov-subject="${A}">Ana</a><span id="b" data-ov-subject="${B}">Bob</span><span id="c" data-ov-subject="nope">C</span><b><span id="p" data-ov-subject="${C}">Cleo</span></b><i><span id="d" data-ov-subject="${D}">Dee</span></i>
${Array.from({ length: 150 }, (_, i) => `<span class="many" data-ov-subject="${subjectOf(i)}">p${i}</span>`).join('')}</main>`);
${Array.from({ length: 150 }, (_, i) => `<span class="many" data-ov-subject="${subjectOf(i)}">p${i}</span>`).join('')}</main>`);
- assert.strictEqual(await items.decorate(document), 153, 'every valid subject, the invalid one skipped');
+ assert.strictEqual(await items.decorate(document), 154, 'every valid subject, the invalid one skipped');
const sets = calls.filter((c) => c.url.includes('/equipped?'));
const sets = calls.filter((c) => c.url.includes('/equipped?'));
- assert.strictEqual(sets.length, 2, '153 people in two requests (100 + 53)');
+ assert.strictEqual(sets.length, 2, '154 people in two requests (100 + 54)');
assert.strictEqual(calls.filter((c) => c.url.includes('/definitions?')).length, 1, 'definitions once, because someone wears a hat or particles');
assert.strictEqual(calls.filter((c) => c.url.includes('/definitions?')).length, 1, 'definitions once, because someone wears a hat or particles');
assert.ok(calls.every((c) => c.url.startsWith('https://inventory.openvibe.network/api/v1/') && c.opts.credentials === 'omit'));
assert.ok(calls.every((c) => c.url.startsWith('https://inventory.openvibe.network/api/v1/') && c.opts.credentials === 'omit'));
assert.deepStrictEqual([...document.getElementById('a').classList].sort(), ['name-fx-rainbow', 'ov-fx'], 'Ana wears her rainbow');
assert.deepStrictEqual([...document.getElementById('a').classList].sort(), ['name-fx-rainbow', 'ov-fx'], 'Ana wears her rainbow');
@@ -56,6 +58,11 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;
@@ -56,6 +58,11 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;
const cleo = document.getElementById('p');
const cleo = document.getElementById('p');
assert.deepStrictEqual([...cleo.classList].sort(), ['ov-px', 'px-sparkle'], 'Cleo wears particles');
assert.deepStrictEqual([...cleo.classList].sort(), ['ov-px', 'px-sparkle'], 'Cleo wears particles');
assert.strictEqual(cleo.dataset.ovPxChars, '✦✧⋆');
assert.strictEqual(cleo.dataset.ovPxChars, '✦✧⋆');
+ const dee = document.getElementById('d');
+ const b = dee.previousElementSibling;
+ assert.ok(b && b.tagName === 'IMG' && b.className === 'ov-badge' && b.getAttribute('src') === 'https://openvibe.media/o/med_01JZ00000000000000000000B1' && b.getAttribute('alt') === '', 'Dee\'s community badge before her name');
+ items.apply(dee, { 'network.badge:badge': { media_id: 'javascript:alert(1)' } });
+ assert.ok(!(dee.previousElementSibling && dee.previousElementSibling.classList.contains('ov-badge')), 'a bad media id draws nothing, and the old badge is gone');
items.burst(cleo);
items.burst(cleo);
assert.strictEqual(cleo.querySelectorAll('.ov-px-p').length, 6, 'a burst: six characters');
assert.strictEqual(cleo.querySelectorAll('.ov-px-p').length, 6, 'a burst: six characters');
assert.ok([...cleo.querySelectorAll('.ov-px-p')].every((p) => '✦✧⋆'.includes(p.textContent)));
assert.ok([...cleo.querySelectorAll('.ov-px-p')].every((p) => '✦✧⋆'.includes(p.textContent)));
@@ -94,6 +101,7 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;
@@ -94,6 +101,7 @@ const subjectOf = (i) => `usr_01JZ${String(i).padStart(22, '0')}`;
assert.match(css, /\.ov-fx \{ display: inline-block; \}/);
assert.match(css, /\.ov-fx \{ display: inline-block; \}/);
assert.match(css, /prefers-reduced-motion: reduce[\s\S]*animation: none !important/);
assert.match(css, /prefers-reduced-motion: reduce[\s\S]*animation: none !important/);
assert.match(css, /\.ov-hat-pulse \{ animation: ovHatPulse/);
assert.match(css, /\.ov-hat-pulse \{ animation: ovHatPulse/);
+ assert.match(css, /\.ov-badge \{ display: inline-block;/);
assert.match(css, /\.px-sparkle \.ov-px-p \{ color: #ffd700; \}/);
assert.match(css, /\.px-sparkle \.ov-px-p \{ color: #ffd700; \}/);
assert.match(css, /prefers-reduced-motion: reduce[\s\S]*\.ov-px-p \{ display: none; \}/);
assert.match(css, /prefers-reduced-motion: reduce[\s\S]*\.ov-px-p \{ display: none; \}/);
console.log('items: all checks passed');
console.log('items: all checks passed');