openvibers/openvibe.chat Commits
Chat: community badges (the Workshop's badgeFX from Live's decor) travel with chat lines and show on openvibe.chat (#40)
Changes
@@@ -60,6 +60,13 @@// Their hat (m.hatFX) goes just before the name, outside the effect so a gradient cannot hide it.function wearing(m, text) {var frag = document.createDocumentFragment();++ var bf = m && m.badgeFX;++ if (bf && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(bf.mediaId || ''))) {++ var img = document.createElement('img');++ img.className = 'ov-badge'; img.src = 'https://openvibe.media/o/' + bf.mediaId; img.alt = ''; img.width = 16; img.height = 16;++ img.setAttribute('loading', 'lazy'); img.setAttribute('aria-hidden', 'true');++ frag.appendChild(img);++ }var h = m && m.hatFX;var glyph = h && typeof h.hatChar === 'string' ? h.hatChar.slice(0, 8) : '';if (glyph) {@@@ -292,7 +292,7 @@ class CallServer {muted: info.muted, cameraOff: info.cameraOff,forceMuted: info.forceMuted || false, forceCameraOff: info.forceCameraOff || false,speaking: info.speaking || false,-- nameFX: cosmeticProfile.nameFX || null, particleFX: cosmeticProfile.particleFX || null, hatFX: cosmeticProfile.hatFX || null,++ nameFX: cosmeticProfile.nameFX || null, particleFX: cosmeticProfile.particleFX || null, hatFX: cosmeticProfile.hatFX || null, badgeFX: cosmeticProfile.badgeFX || null,...(info.inRoom ? { roomRole: info.roomRole || null, canTalk: !info.listenOnly } : {}),};}@@@ -897,6 +897,7 @@ class ChatServer {if (cosmeticProfile.nameFX) chatMsg.nameFX = cosmeticProfile.nameFX;if (cosmeticProfile.particleFX) chatMsg.particleFX = cosmeticProfile.particleFX;if (cosmeticProfile.hatFX) chatMsg.hatFX = cosmeticProfile.hatFX;++ if (cosmeticProfile.badgeFX) chatMsg.badgeFX = cosmeticProfile.badgeFX;if (cosmeticProfile.voiceFX) chatMsg.voiceFX = cosmeticProfile.voiceFX;} catch { /* non-critical */ }@@@ -1256,6 +1257,7 @@ class ChatServer {if (cosmeticProfile.nameFX) gottiMsg.nameFX = cosmeticProfile.nameFX;if (cosmeticProfile.particleFX) gottiMsg.particleFX = cosmeticProfile.particleFX;if (cosmeticProfile.hatFX) gottiMsg.hatFX = cosmeticProfile.hatFX;++ if (cosmeticProfile.badgeFX) gottiMsg.badgeFX = cosmeticProfile.badgeFX;} catch { /* non-critical */ }try {@@@ -1540,6 +1542,7 @@ class ChatServer {if (cp.nameFX) soundMsg.nameFX = cp.nameFX;if (cp.particleFX) soundMsg.particleFX = cp.particleFX;if (cp.hatFX) soundMsg.hatFX = cp.hatFX;++ if (cp.badgeFX) soundMsg.badgeFX = cp.badgeFX;} catch { /* non-critical */ }try {const tagProfile = tags.getTagProfile(client.user.id);@@@ -106,6 +106,7 @@ function enrichMessagesWithCosmetics(messages) {if (prof.nameFX) m.nameFX = prof.nameFX;if (prof.particleFX) m.particleFX = prof.particleFX;if (prof.hatFX) m.hatFX = prof.hatFX;++ if (prof.badgeFX) m.badgeFX = prof.badgeFX; // a community badge (the Workshop): its Media imageif (prof.tag) m.tag = prof.tag;}return messages;@@@ -577,7 +577,7 @@ async function ensureDecor(ids) {} catch (err) { console.warn('[LiveContext] decor:', err.message); break; }}}--/** cosmetics.getCosmeticProfile(userId): { nameFX, particleFX, hatFX, voiceFX } (empty until warm). */++/** cosmetics.getCosmeticProfile(userId): { nameFX, particleFX, hatFX, badgeFX, voiceFX } (empty until warm). */function getCosmeticProfile(userId) { const d = _decor.peek(Number(userId)); return (d && d.cosmetic) || {}; }/** tags.getTagProfile(userId): the equipped tag or null. */function getTagProfile(userId) { const d = _decor.peek(Number(userId)); return (d && d.tag) || null; }@@@ -82,6 +82,11 @@ const nameFxClass = (m) => (m && m.nameFX && NAME_FX_RE.test(String(m.nameFX.css// The hat they wear (m.hatFX: its emoji and Live's float/pulse/warp motion), just before the name (items.css .ov-hat).const HAT_MOTION = ['float', 'pulse', 'warp'];function hatHtml(m) {++ // A community badge (the Workshop, m.badgeFX): its reviewed image on OpenVibe.Media, first.++ const b = m && m.badgeFX && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(m.badgeFX.mediaId || '')) ? `<img class="ov-badge" src="https://openvibe.media/o/${m.badgeFX.mediaId}" alt="" width="16" height="16" loading="lazy" aria-hidden="true">` : '';++ return b + hatOnly(m);++}++function hatOnly(m) {const h = m && m.hatFX;const glyph = h && typeof h.hatChar === 'string' ? h.hatChar.slice(0, 8) : '';if (!glyph) return '';@@@ -181,6 +186,7 @@ function createWebRoutes({ config }) {const prof = ctx.getCosmeticProfile(m.user_id);if (!m.nameFX && prof.nameFX) m.nameFX = prof.nameFX;if (!m.hatFX && prof.hatFX) m.hatFX = prof.hatFX;++ if (!m.badgeFX && prof.badgeFX) m.badgeFX = prof.badgeFX;}} catch { /* the names stay plain */ }const composer = actor.kind === 'user'@@@ -223,12 +223,12 @@ t('what people wear: a name effect from Live\'s decor is drawn on the name, withconst dave = h.addUser('dave', { subject: ids.newId('user') });const erin = h.addUser('erin', { subject: ids.newId('user') });for (const u of [dave, erin]) { await h.ctx.upsertUser(h.live.users.get(u.id)); h.netModules.subjects.add(u.subject_id); }-- h.live.decor.set(Number(dave.id), { cosmetic: { nameFX: { itemId: 'fx_rainbow', cssClass: 'name-fx-rainbow' }, hatFX: { itemId: 'hat_crown', hatChar: '👑', animated: 'pulse' } }, tag: null });++ h.live.decor.set(Number(dave.id), { cosmetic: { nameFX: { itemId: 'fx_rainbow', cssClass: 'name-fx-rainbow' }, hatFX: { itemId: 'hat_crown', hatChar: '👑', animated: 'pulse' }, badgeFX: { mediaId: 'med_01JZ00000000000000000000B1' } }, tag: null });h.live.decor.set(Number(erin.id), { cosmetic: { nameFX: { itemId: 'x', cssClass: 'evil" onload="x' }, hatFX: { itemId: 'y', hatChar: '<b>', animated: 'spin" x="' } }, tag: null });assert.strictEqual((await req('POST', '/send', form(dave, { message: 'wearing my rainbow' }))).status, 303);assert.strictEqual((await req('POST', '/send', form(erin, { message: 'and mine' }))).status, 303);const r = await req('GET', '/');-- assert.match(r.text, /<span class="ov-hat ov-hat-pulse" aria-hidden="true">👑<\/span><span class="ov-fx name-fx-rainbow">Dave<\/span>/, 'dave\'s crown, then his name in its effect');++ assert.match(r.text, /<img class="ov-badge" src="https:\/\/openvibe\.media\/o\/med_01JZ00000000000000000000B1"[^>]*><span class="ov-hat ov-hat-pulse" aria-hidden="true">👑<\/span><span class="ov-fx name-fx-rainbow">Dave<\/span>/, 'dave\'s community badge, his crown, then his name in its effect');assert.match(r.text, /<span class="ov-hat" aria-hidden="true"><b><\/span>Erin/, 'a hat is text, and an unknown motion is no class');assert.ok(!/onload=/.test(r.text) && !/ov-fx[^"]*">Erin/i.test(r.text) && /Erin/.test(r.text), 'a token of the wrong shape is never a class');assert.match(r.text, /<link rel="stylesheet" href="\/shared\/items\.css\?v=[A-Za-z0-9_-]+">/);
@@ -60,6 +60,13 @@
@@ -60,6 +60,13 @@
// Their hat (m.hatFX) goes just before the name, outside the effect so a gradient cannot hide it.
// Their hat (m.hatFX) goes just before the name, outside the effect so a gradient cannot hide it.
function wearing(m, text) {
function wearing(m, text) {
var frag = document.createDocumentFragment();
var frag = document.createDocumentFragment();
+ var bf = m && m.badgeFX;
+ if (bf && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(bf.mediaId || ''))) {
+ var img = document.createElement('img');
+ img.className = 'ov-badge'; img.src = 'https://openvibe.media/o/' + bf.mediaId; img.alt = ''; img.width = 16; img.height = 16;
+ img.setAttribute('loading', 'lazy'); img.setAttribute('aria-hidden', 'true');
+ frag.appendChild(img);
+ }
var h = m && m.hatFX;
var h = m && m.hatFX;
var glyph = h && typeof h.hatChar === 'string' ? h.hatChar.slice(0, 8) : '';
var glyph = h && typeof h.hatChar === 'string' ? h.hatChar.slice(0, 8) : '';
if (glyph) {
if (glyph) {
@@ -292,7 +292,7 @@ class CallServer {
@@ -292,7 +292,7 @@ class CallServer {
muted: info.muted, cameraOff: info.cameraOff,
muted: info.muted, cameraOff: info.cameraOff,
forceMuted: info.forceMuted || false, forceCameraOff: info.forceCameraOff || false,
forceMuted: info.forceMuted || false, forceCameraOff: info.forceCameraOff || false,
speaking: info.speaking || false,
speaking: info.speaking || false,
- nameFX: cosmeticProfile.nameFX || null, particleFX: cosmeticProfile.particleFX || null, hatFX: cosmeticProfile.hatFX || null,
+ nameFX: cosmeticProfile.nameFX || null, particleFX: cosmeticProfile.particleFX || null, hatFX: cosmeticProfile.hatFX || null, badgeFX: cosmeticProfile.badgeFX || null,
...(info.inRoom ? { roomRole: info.roomRole || null, canTalk: !info.listenOnly } : {}),
...(info.inRoom ? { roomRole: info.roomRole || null, canTalk: !info.listenOnly } : {}),
};
};
}
}
@@ -897,6 +897,7 @@ class ChatServer {
@@ -897,6 +897,7 @@ class ChatServer {
if (cosmeticProfile.nameFX) chatMsg.nameFX = cosmeticProfile.nameFX;
if (cosmeticProfile.nameFX) chatMsg.nameFX = cosmeticProfile.nameFX;
if (cosmeticProfile.particleFX) chatMsg.particleFX = cosmeticProfile.particleFX;
if (cosmeticProfile.particleFX) chatMsg.particleFX = cosmeticProfile.particleFX;
if (cosmeticProfile.hatFX) chatMsg.hatFX = cosmeticProfile.hatFX;
if (cosmeticProfile.hatFX) chatMsg.hatFX = cosmeticProfile.hatFX;
+ if (cosmeticProfile.badgeFX) chatMsg.badgeFX = cosmeticProfile.badgeFX;
if (cosmeticProfile.voiceFX) chatMsg.voiceFX = cosmeticProfile.voiceFX;
if (cosmeticProfile.voiceFX) chatMsg.voiceFX = cosmeticProfile.voiceFX;
} catch { /* non-critical */ }
} catch { /* non-critical */ }
@@ -1256,6 +1257,7 @@ class ChatServer {
@@ -1256,6 +1257,7 @@ class ChatServer {
if (cosmeticProfile.nameFX) gottiMsg.nameFX = cosmeticProfile.nameFX;
if (cosmeticProfile.nameFX) gottiMsg.nameFX = cosmeticProfile.nameFX;
if (cosmeticProfile.particleFX) gottiMsg.particleFX = cosmeticProfile.particleFX;
if (cosmeticProfile.particleFX) gottiMsg.particleFX = cosmeticProfile.particleFX;
if (cosmeticProfile.hatFX) gottiMsg.hatFX = cosmeticProfile.hatFX;
if (cosmeticProfile.hatFX) gottiMsg.hatFX = cosmeticProfile.hatFX;
+ if (cosmeticProfile.badgeFX) gottiMsg.badgeFX = cosmeticProfile.badgeFX;
} catch { /* non-critical */ }
} catch { /* non-critical */ }
try {
try {
@@ -1540,6 +1542,7 @@ class ChatServer {
@@ -1540,6 +1542,7 @@ class ChatServer {
if (cp.nameFX) soundMsg.nameFX = cp.nameFX;
if (cp.nameFX) soundMsg.nameFX = cp.nameFX;
if (cp.particleFX) soundMsg.particleFX = cp.particleFX;
if (cp.particleFX) soundMsg.particleFX = cp.particleFX;
if (cp.hatFX) soundMsg.hatFX = cp.hatFX;
if (cp.hatFX) soundMsg.hatFX = cp.hatFX;
+ if (cp.badgeFX) soundMsg.badgeFX = cp.badgeFX;
} catch { /* non-critical */ }
} catch { /* non-critical */ }
try {
try {
const tagProfile = tags.getTagProfile(client.user.id);
const tagProfile = tags.getTagProfile(client.user.id);
@@ -106,6 +106,7 @@ function enrichMessagesWithCosmetics(messages) {
@@ -106,6 +106,7 @@ function enrichMessagesWithCosmetics(messages) {
if (prof.nameFX) m.nameFX = prof.nameFX;
if (prof.nameFX) m.nameFX = prof.nameFX;
if (prof.particleFX) m.particleFX = prof.particleFX;
if (prof.particleFX) m.particleFX = prof.particleFX;
if (prof.hatFX) m.hatFX = prof.hatFX;
if (prof.hatFX) m.hatFX = prof.hatFX;
+ if (prof.badgeFX) m.badgeFX = prof.badgeFX; // a community badge (the Workshop): its Media image
if (prof.tag) m.tag = prof.tag;
if (prof.tag) m.tag = prof.tag;
}
}
return messages;
return messages;
@@ -577,7 +577,7 @@ async function ensureDecor(ids) {
@@ -577,7 +577,7 @@ async function ensureDecor(ids) {
} catch (err) { console.warn('[LiveContext] decor:', err.message); break; }
} catch (err) { console.warn('[LiveContext] decor:', err.message); break; }
}
}
}
}
-/** cosmetics.getCosmeticProfile(userId): { nameFX, particleFX, hatFX, voiceFX } (empty until warm). */
+/** cosmetics.getCosmeticProfile(userId): { nameFX, particleFX, hatFX, badgeFX, voiceFX } (empty until warm). */
function getCosmeticProfile(userId) { const d = _decor.peek(Number(userId)); return (d && d.cosmetic) || {}; }
function getCosmeticProfile(userId) { const d = _decor.peek(Number(userId)); return (d && d.cosmetic) || {}; }
/** tags.getTagProfile(userId): the equipped tag or null. */
/** tags.getTagProfile(userId): the equipped tag or null. */
function getTagProfile(userId) { const d = _decor.peek(Number(userId)); return (d && d.tag) || null; }
function getTagProfile(userId) { const d = _decor.peek(Number(userId)); return (d && d.tag) || null; }
@@ -82,6 +82,11 @@ const nameFxClass = (m) => (m && m.nameFX && NAME_FX_RE.test(String(m.nameFX.css
@@ -82,6 +82,11 @@ const nameFxClass = (m) => (m && m.nameFX && NAME_FX_RE.test(String(m.nameFX.css
// The hat they wear (m.hatFX: its emoji and Live's float/pulse/warp motion), just before the name (items.css .ov-hat).
// The hat they wear (m.hatFX: its emoji and Live's float/pulse/warp motion), just before the name (items.css .ov-hat).
const HAT_MOTION = ['float', 'pulse', 'warp'];
const HAT_MOTION = ['float', 'pulse', 'warp'];
function hatHtml(m) {
function hatHtml(m) {
+ // A community badge (the Workshop, m.badgeFX): its reviewed image on OpenVibe.Media, first.
+ const b = m && m.badgeFX && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(m.badgeFX.mediaId || '')) ? `<img class="ov-badge" src="https://openvibe.media/o/${m.badgeFX.mediaId}" alt="" width="16" height="16" loading="lazy" aria-hidden="true">` : '';
+ return b + hatOnly(m);
+}
+function hatOnly(m) {
const h = m && m.hatFX;
const h = m && m.hatFX;
const glyph = h && typeof h.hatChar === 'string' ? h.hatChar.slice(0, 8) : '';
const glyph = h && typeof h.hatChar === 'string' ? h.hatChar.slice(0, 8) : '';
if (!glyph) return '';
if (!glyph) return '';
@@ -181,6 +186,7 @@ function createWebRoutes({ config }) {
@@ -181,6 +186,7 @@ function createWebRoutes({ config }) {
const prof = ctx.getCosmeticProfile(m.user_id);
const prof = ctx.getCosmeticProfile(m.user_id);
if (!m.nameFX && prof.nameFX) m.nameFX = prof.nameFX;
if (!m.nameFX && prof.nameFX) m.nameFX = prof.nameFX;
if (!m.hatFX && prof.hatFX) m.hatFX = prof.hatFX;
if (!m.hatFX && prof.hatFX) m.hatFX = prof.hatFX;
+ if (!m.badgeFX && prof.badgeFX) m.badgeFX = prof.badgeFX;
}
}
} catch { /* the names stay plain */ }
} catch { /* the names stay plain */ }
const composer = actor.kind === 'user'
const composer = actor.kind === 'user'
@@ -223,12 +223,12 @@ t('what people wear: a name effect from Live\'s decor is drawn on the name, with
@@ -223,12 +223,12 @@ t('what people wear: a name effect from Live\'s decor is drawn on the name, with
const dave = h.addUser('dave', { subject: ids.newId('user') });
const dave = h.addUser('dave', { subject: ids.newId('user') });
const erin = h.addUser('erin', { subject: ids.newId('user') });
const erin = h.addUser('erin', { subject: ids.newId('user') });
for (const u of [dave, erin]) { await h.ctx.upsertUser(h.live.users.get(u.id)); h.netModules.subjects.add(u.subject_id); }
for (const u of [dave, erin]) { await h.ctx.upsertUser(h.live.users.get(u.id)); h.netModules.subjects.add(u.subject_id); }
- h.live.decor.set(Number(dave.id), { cosmetic: { nameFX: { itemId: 'fx_rainbow', cssClass: 'name-fx-rainbow' }, hatFX: { itemId: 'hat_crown', hatChar: '👑', animated: 'pulse' } }, tag: null });
+ h.live.decor.set(Number(dave.id), { cosmetic: { nameFX: { itemId: 'fx_rainbow', cssClass: 'name-fx-rainbow' }, hatFX: { itemId: 'hat_crown', hatChar: '👑', animated: 'pulse' }, badgeFX: { mediaId: 'med_01JZ00000000000000000000B1' } }, tag: null });
h.live.decor.set(Number(erin.id), { cosmetic: { nameFX: { itemId: 'x', cssClass: 'evil" onload="x' }, hatFX: { itemId: 'y', hatChar: '<b>', animated: 'spin" x="' } }, tag: null });
h.live.decor.set(Number(erin.id), { cosmetic: { nameFX: { itemId: 'x', cssClass: 'evil" onload="x' }, hatFX: { itemId: 'y', hatChar: '<b>', animated: 'spin" x="' } }, tag: null });
assert.strictEqual((await req('POST', '/send', form(dave, { message: 'wearing my rainbow' }))).status, 303);
assert.strictEqual((await req('POST', '/send', form(dave, { message: 'wearing my rainbow' }))).status, 303);
assert.strictEqual((await req('POST', '/send', form(erin, { message: 'and mine' }))).status, 303);
assert.strictEqual((await req('POST', '/send', form(erin, { message: 'and mine' }))).status, 303);
const r = await req('GET', '/');
const r = await req('GET', '/');
- assert.match(r.text, /<span class="ov-hat ov-hat-pulse" aria-hidden="true">👑<\/span><span class="ov-fx name-fx-rainbow">Dave<\/span>/, 'dave\'s crown, then his name in its effect');
+ assert.match(r.text, /<img class="ov-badge" src="https:\/\/openvibe\.media\/o\/med_01JZ00000000000000000000B1"[^>]*><span class="ov-hat ov-hat-pulse" aria-hidden="true">👑<\/span><span class="ov-fx name-fx-rainbow">Dave<\/span>/, 'dave\'s community badge, his crown, then his name in its effect');
assert.match(r.text, /<span class="ov-hat" aria-hidden="true"><b><\/span>Erin/, 'a hat is text, and an unknown motion is no class');
assert.match(r.text, /<span class="ov-hat" aria-hidden="true"><b><\/span>Erin/, 'a hat is text, and an unknown motion is no class');
assert.ok(!/onload=/.test(r.text) && !/ov-fx[^"]*">Erin/i.test(r.text) && /Erin/.test(r.text), 'a token of the wrong shape is never a class');
assert.ok(!/onload=/.test(r.text) && !/ov-fx[^"]*">Erin/i.test(r.text) && /Erin/.test(r.text), 'a token of the wrong shape is never a class');
assert.match(r.text, /<link rel="stylesheet" href="\/shared\/items\.css\?v=[A-Za-z0-9_-]+">/);
assert.match(r.text, /<link rel="stylesheet" href="\/shared\/items\.css\?v=[A-Za-z0-9_-]+">/);