openvibers/openvibe.live Commits
Live: a community badge (the Workshop's network.badge) shows before a name in chat — Live's decor carries it as badgeFX (#61)
Changes
@@@ -210,6 +210,17 @@HAT EFFECTS — shown before username in chat══════════════════════════════════════════════════════════════ */++/* A community badge (the Workshop, network.badge): its reviewed image before the name. */++.chat-ws-badge {++ display: inline-block;++ width: 1.15em;++ height: 1.15em;++ margin-right: 3px;++ vertical-align: -0.2em;++ border-radius: 3px;++ object-fit: contain;++}++.chat-hat {display: inline-block;margin-right: 2px;@@@ -3749,9 +3749,13 @@ function buildChatMessageEl(msg, opts = {}) {// ── Cosmetic rendering ───────────────────────────────// Hat emoji before namelet hatHtml = '';++ // A community badge (the Workshop): its reviewed image on OpenVibe.Media, before the hat and the name.++ if (msg.badgeFX && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(msg.badgeFX.mediaId || ''))) {++ hatHtml += `<img class="chat-ws-badge" src="https://openvibe.media/o/${msg.badgeFX.mediaId}" alt="" width="16" height="16" loading="lazy" aria-hidden="true">`;++ }if (msg.hatFX && msg.hatFX.hatChar) {const animClass = msg.hatFX.animated ? ` hat-${esc(msg.hatFX.animated)}` : '';-- hatHtml = `<span class="chat-hat${animClass}">${esc(msg.hatFX.hatChar)}</span>`;++ hatHtml += `<span class="chat-hat${animClass}">${esc(msg.hatFX.hatChar)}</span>`;}// Name effect CSS class on username@@@ -4707,6 +4711,7 @@ function _renderHistoryMainMsg(m) {nameFX: m.nameFX || undefined,particleFX: m.particleFX || undefined,hatFX: m.hatFX || undefined,++ badgeFX: m.badgeFX || undefined,tag: m.tag || undefined,// Source-stream context (for the per-message origin/channel badge)stream_id: m.stream_id,@@@ -4817,6 +4822,7 @@ function _renderGlobalHistoryMessages(msgs) {source_managed_id: m.source_managed_id || null,source_is_live: m.source_is_live ? 1 : 0,hatFX: m.hatFX || null,++ badgeFX: m.badgeFX || null,nameFX: m.nameFX || null,particleFX: m.particleFX || null,reply_to: m.reply_to || null,@@@ -127,7 +127,7 @@ async function getEquipped(userId) {}, {});}--/** What a chat line carries for one { slot → item id } set: { nameFX, particleFX, hatFX, voiceFX } (each optional). */++/** What a chat line carries for one { slot → item id } set: { nameFX, particleFX, hatFX, badgeFX, voiceFX } (each optional). */function profileOf(equipped) {const result = {};if (equipped.name_effect && COSMETICS[equipped.name_effect]) {@@@ -142,6 +142,8 @@ function profileOf(equipped) {const c = COSMETICS[equipped.hat];result.hatFX = { itemId: equipped.hat, hatChar: c.hatChar, cssClass: c.hatChar, animated: c.animated };}++ // A community badge from the Workshop (network.badge): its reviewed image on OpenVibe.Media, before the name.++ if (/^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(equipped._badge || ''))) result.badgeFX = { mediaId: equipped._badge };if (equipped.voice && COSMETICS[equipped.voice]) {const c = COSMETICS[equipped.voice];result.voiceFX = { itemId: equipped.voice, pitch: c.pitch, rate: c.rate };@@@ -5,7 +5,8 @@* catalog() Live's definitions (alias = Live's item id ↔ itd_ id), cached 10 minutes* subjectOf(userId) a Live user's Network subject (linked_accounts, server/auth/identity-sync.js), cached* owned(subject) { alias → instance_id } of the live.* items the person owns-- * equipped(subject) { slot → alias } of what they wear, cached 30 s (chat and calls read it per message)++ * equipped(subject) { slot → alias } of what they wear (and `_badge`: a worn community badge's media id),++ * cached 30 s (chat and calls read it per message)* equippedMany(subjects) Map(subject → { slot → alias }), 100 people per read (OpenVibe.Chat's decor lookups)* grant / revoke / equip the writes, acting for the person with Live's own token*@@@ -119,6 +120,8 @@ function remember(subject, slots, byId) {const value = {};for (const [key, v] of Object.entries(slots || {})) {const [kind, slot] = key.split(':');++ // A community badge (OpenVibe.Inventory's Workshop, network.badge): its image, for the chat line.++ if (kind === 'network.badge' && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(v.media_id || ''))) { value._badge = v.media_id; continue; }if (!kind.startsWith('live.')) continue;const alias = byId.get(v.definition_id);if (alias) value[slot] = alias;@@@ -103,6 +103,15 @@ function stub() {assert.strictEqual((await cosmetics.revokeCosmetic(70, 'hat_crown')).success, true);assert.ok(!(await cosmetics.getUnlocked(70)).some((u) => u.item_id === 'hat_crown'));++ // A community badge from the Workshop rides along as badgeFX (its Media image), alone and in a batch.++ state.slots['network.badge:badge'] = { instance_id: 'inv_01JZ000000000000000000BAD1', definition_id: 'itd_01JZ000000000000000000BAD1', media_id: 'med_01JZ00000000000000000000B1' };++ inventory.forget(A);++ assert.deepStrictEqual((await cosmetics.getCosmeticProfile(70)).badgeFX, { mediaId: 'med_01JZ00000000000000000000B1' });++ inventory.forget(A);++ assert.deepStrictEqual((await cosmetics.getCosmeticProfiles([70]))[70].badgeFX, { mediaId: 'med_01JZ00000000000000000000B1' });++ delete state.slots['network.badge:badge'];++ inventory.forget(A);++// The shop says where an item is earned: Quest gives the Rainbow (Inventory's grantors).assert.deepStrictEqual(await cosmetics.earnable(), { fx_rainbow: 'quest' });
@@ -210,6 +210,17 @@
@@ -210,6 +210,17 @@
HAT EFFECTS — shown before username in chat
HAT EFFECTS — shown before username in chat
══════════════════════════════════════════════════════════════ */
══════════════════════════════════════════════════════════════ */
+/* A community badge (the Workshop, network.badge): its reviewed image before the name. */
+.chat-ws-badge {
+ display: inline-block;
+ width: 1.15em;
+ height: 1.15em;
+ margin-right: 3px;
+ vertical-align: -0.2em;
+ border-radius: 3px;
+ object-fit: contain;
+}
+
.chat-hat {
.chat-hat {
display: inline-block;
display: inline-block;
margin-right: 2px;
margin-right: 2px;
@@ -3749,9 +3749,13 @@ function buildChatMessageEl(msg, opts = {}) {
@@ -3749,9 +3749,13 @@ function buildChatMessageEl(msg, opts = {}) {
// ── Cosmetic rendering ───────────────────────────────
// ── Cosmetic rendering ───────────────────────────────
// Hat emoji before name
// Hat emoji before name
let hatHtml = '';
let hatHtml = '';
+ // A community badge (the Workshop): its reviewed image on OpenVibe.Media, before the hat and the name.
+ if (msg.badgeFX && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(msg.badgeFX.mediaId || ''))) {
+ hatHtml += `<img class="chat-ws-badge" src="https://openvibe.media/o/${msg.badgeFX.mediaId}" alt="" width="16" height="16" loading="lazy" aria-hidden="true">`;
+ }
if (msg.hatFX && msg.hatFX.hatChar) {
if (msg.hatFX && msg.hatFX.hatChar) {
const animClass = msg.hatFX.animated ? ` hat-${esc(msg.hatFX.animated)}` : '';
const animClass = msg.hatFX.animated ? ` hat-${esc(msg.hatFX.animated)}` : '';
- hatHtml = `<span class="chat-hat${animClass}">${esc(msg.hatFX.hatChar)}</span>`;
+ hatHtml += `<span class="chat-hat${animClass}">${esc(msg.hatFX.hatChar)}</span>`;
}
}
// Name effect CSS class on username
// Name effect CSS class on username
@@ -4707,6 +4711,7 @@ function _renderHistoryMainMsg(m) {
@@ -4707,6 +4711,7 @@ function _renderHistoryMainMsg(m) {
nameFX: m.nameFX || undefined,
nameFX: m.nameFX || undefined,
particleFX: m.particleFX || undefined,
particleFX: m.particleFX || undefined,
hatFX: m.hatFX || undefined,
hatFX: m.hatFX || undefined,
+ badgeFX: m.badgeFX || undefined,
tag: m.tag || undefined,
tag: m.tag || undefined,
// Source-stream context (for the per-message origin/channel badge)
// Source-stream context (for the per-message origin/channel badge)
stream_id: m.stream_id,
stream_id: m.stream_id,
@@ -4817,6 +4822,7 @@ function _renderGlobalHistoryMessages(msgs) {
@@ -4817,6 +4822,7 @@ function _renderGlobalHistoryMessages(msgs) {
source_managed_id: m.source_managed_id || null,
source_managed_id: m.source_managed_id || null,
source_is_live: m.source_is_live ? 1 : 0,
source_is_live: m.source_is_live ? 1 : 0,
hatFX: m.hatFX || null,
hatFX: m.hatFX || null,
+ badgeFX: m.badgeFX || null,
nameFX: m.nameFX || null,
nameFX: m.nameFX || null,
particleFX: m.particleFX || null,
particleFX: m.particleFX || null,
reply_to: m.reply_to || null,
reply_to: m.reply_to || null,
@@ -127,7 +127,7 @@ async function getEquipped(userId) {
@@ -127,7 +127,7 @@ async function getEquipped(userId) {
}, {});
}, {});
}
}
-/** What a chat line carries for one { slot → item id } set: { nameFX, particleFX, hatFX, voiceFX } (each optional). */
+/** What a chat line carries for one { slot → item id } set: { nameFX, particleFX, hatFX, badgeFX, voiceFX } (each optional). */
function profileOf(equipped) {
function profileOf(equipped) {
const result = {};
const result = {};
if (equipped.name_effect && COSMETICS[equipped.name_effect]) {
if (equipped.name_effect && COSMETICS[equipped.name_effect]) {
@@ -142,6 +142,8 @@ function profileOf(equipped) {
@@ -142,6 +142,8 @@ function profileOf(equipped) {
const c = COSMETICS[equipped.hat];
const c = COSMETICS[equipped.hat];
result.hatFX = { itemId: equipped.hat, hatChar: c.hatChar, cssClass: c.hatChar, animated: c.animated };
result.hatFX = { itemId: equipped.hat, hatChar: c.hatChar, cssClass: c.hatChar, animated: c.animated };
}
}
+ // A community badge from the Workshop (network.badge): its reviewed image on OpenVibe.Media, before the name.
+ if (/^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(equipped._badge || ''))) result.badgeFX = { mediaId: equipped._badge };
if (equipped.voice && COSMETICS[equipped.voice]) {
if (equipped.voice && COSMETICS[equipped.voice]) {
const c = COSMETICS[equipped.voice];
const c = COSMETICS[equipped.voice];
result.voiceFX = { itemId: equipped.voice, pitch: c.pitch, rate: c.rate };
result.voiceFX = { itemId: equipped.voice, pitch: c.pitch, rate: c.rate };
@@ -5,7 +5,8 @@
@@ -5,7 +5,8 @@
* catalog() Live's definitions (alias = Live's item id ↔ itd_ id), cached 10 minutes
* catalog() Live's definitions (alias = Live's item id ↔ itd_ id), cached 10 minutes
* subjectOf(userId) a Live user's Network subject (linked_accounts, server/auth/identity-sync.js), cached
* subjectOf(userId) a Live user's Network subject (linked_accounts, server/auth/identity-sync.js), cached
* owned(subject) { alias → instance_id } of the live.* items the person owns
* owned(subject) { alias → instance_id } of the live.* items the person owns
- * equipped(subject) { slot → alias } of what they wear, cached 30 s (chat and calls read it per message)
+ * equipped(subject) { slot → alias } of what they wear (and `_badge`: a worn community badge's media id),
+ * cached 30 s (chat and calls read it per message)
* equippedMany(subjects) Map(subject → { slot → alias }), 100 people per read (OpenVibe.Chat's decor lookups)
* equippedMany(subjects) Map(subject → { slot → alias }), 100 people per read (OpenVibe.Chat's decor lookups)
* grant / revoke / equip the writes, acting for the person with Live's own token
* grant / revoke / equip the writes, acting for the person with Live's own token
*
*
@@ -119,6 +120,8 @@ function remember(subject, slots, byId) {
@@ -119,6 +120,8 @@ function remember(subject, slots, byId) {
const value = {};
const value = {};
for (const [key, v] of Object.entries(slots || {})) {
for (const [key, v] of Object.entries(slots || {})) {
const [kind, slot] = key.split(':');
const [kind, slot] = key.split(':');
+ // A community badge (OpenVibe.Inventory's Workshop, network.badge): its image, for the chat line.
+ if (kind === 'network.badge' && /^med_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(v.media_id || ''))) { value._badge = v.media_id; continue; }
if (!kind.startsWith('live.')) continue;
if (!kind.startsWith('live.')) continue;
const alias = byId.get(v.definition_id);
const alias = byId.get(v.definition_id);
if (alias) value[slot] = alias;
if (alias) value[slot] = alias;
@@ -103,6 +103,15 @@ function stub() {
@@ -103,6 +103,15 @@ function stub() {
assert.strictEqual((await cosmetics.revokeCosmetic(70, 'hat_crown')).success, true);
assert.strictEqual((await cosmetics.revokeCosmetic(70, 'hat_crown')).success, true);
assert.ok(!(await cosmetics.getUnlocked(70)).some((u) => u.item_id === 'hat_crown'));
assert.ok(!(await cosmetics.getUnlocked(70)).some((u) => u.item_id === 'hat_crown'));
+ // A community badge from the Workshop rides along as badgeFX (its Media image), alone and in a batch.
+ state.slots['network.badge:badge'] = { instance_id: 'inv_01JZ000000000000000000BAD1', definition_id: 'itd_01JZ000000000000000000BAD1', media_id: 'med_01JZ00000000000000000000B1' };
+ inventory.forget(A);
+ assert.deepStrictEqual((await cosmetics.getCosmeticProfile(70)).badgeFX, { mediaId: 'med_01JZ00000000000000000000B1' });
+ inventory.forget(A);
+ assert.deepStrictEqual((await cosmetics.getCosmeticProfiles([70]))[70].badgeFX, { mediaId: 'med_01JZ00000000000000000000B1' });
+ delete state.slots['network.badge:badge'];
+ inventory.forget(A);
+
// The shop says where an item is earned: Quest gives the Rainbow (Inventory's grantors).
// The shop says where an item is earned: Quest gives the Rainbow (Inventory's grantors).
assert.deepStrictEqual(await cosmetics.earnable(), { fx_rainbow: 'quest' });
assert.deepStrictEqual(await cosmetics.earnable(), { fx_rainbow: 'quest' });