openvibers/openvibe.live Commits

A tip with a hidden amount says 'sent a tip', never 'donated 0 Vibes': Tips sends amount null for privacy.hide_amount, and Live turned it into 0 in the chat event, the saved line and TTS. The delivery route keeps null, the saved line reads '<name> sent a tip: <message>', and chat renders and speaks it without an amount. Test: a hidden-amount delivery broadcasts amount null and saves 'Viewer sent a tip: hi'.

OP OpenVibers committed 29 Sept 2026 · 65849994c0 · 3 files, +18 −7


Changes

ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex 670935be..c8a29d8d 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -4119,24 +4119,26 @@ function addDonationMessage(msg) {
el.className = 'chat-msg donation';
const donorName = esc(msg.username || msg.from || 'Anonymous');
-- const amount = Math.round(msg.amount || 0);
-- const amtStr = amount.toLocaleString();
++ // A supporter can hide the amount (Tips privacy.hide_amount): the amount arrives as null and the line says "sent a tip".
++ const hidden = msg.amount == null;
++ const amount = hidden ? 0 : Math.round(msg.amount || 0);
++ const did = hidden ? 'sent a tip' : `donated <strong>${amount.toLocaleString()} Vibes</strong>`;
const rawDonText = msg.message || '';
const text = rawDonText ? `: ${(typeof parseEmotes === 'function') ? parseEmotes(rawDonText) : esc(rawDonText)}` : '';
-- el.innerHTML = `<i class="fa-solid fa-coins" style="color:var(--accent)"></i> <strong>${donorName}</strong> donated <strong>${amtStr} Vibes</strong>${text}`;
++ el.innerHTML = `<i class="fa-solid fa-coins" style="color:var(--accent)"></i> <strong>${donorName}</strong> ${did}${text}`;
container.appendChild(el);
scrollChat();
if (_chatUserScrolledUp) _onNewChatMessageWhileScrolledUp();
queueFullscreenChatEntry({
kind: 'donation',
-- html: `<div class="fullscreen-chat-meta"><span class="fullscreen-chat-user" style="color:var(--accent)"><i class="fa-solid fa-coins"></i> ${donorName}</span></div><div class="fullscreen-chat-text">donated <strong>${amtStr} Vibes</strong>${text}</div>`,
++ html: `<div class="fullscreen-chat-meta"><span class="fullscreen-chat-user" style="color:var(--accent)"><i class="fa-solid fa-coins"></i> ${donorName}</span></div><div class="fullscreen-chat-text">${did}${text}</div>`,
});
// TTS for donations
if (document.getElementById('tts-checkbox')?.checked) {
-- speakTTS(`${donorName} donated ${amount} Vibes. ${msg.message || ''}`);
++ speakTTS(`${donorName} ${hidden ? 'sent a tip' : `donated ${amount} Vibes`}. ${msg.message || ''}`);
}
}
ddiff --git a/server/tips/delivery-routes.js b/server/tips/delivery-routes.js
iindex 36215ad7..cbd78c90 100644
---- a/server/tips/delivery-routes.js
++++ b/server/tips/delivery-routes.js
@@@ -116,7 +116,8 @@ router.post('/deliveries', guard('live.tips_delivery.write'), express.json({ lim
const chatServer = require('../chat/chat-server');
const streamId = streamIdFor(b.target, userId);
const name = String((b.supporter && b.supporter.name) || 'Someone').slice(0, 80);
-- const amount = Math.max(0, Math.round(Number(i.amount) || 0));
++ // null = the supporter hid the amount (Tips privacy.hide_amount): it stays null all the way to the chat line.
++ const amount = i.amount == null ? null : Math.max(0, Math.round(Number(i.amount) || 0));
const message = String(i.message || '').slice(0, 500);
try {
if (b.effect === 'chat_line' || b.effect === 'paid_message') {
@@@ -128,7 +129,7 @@ router.post('/deliveries', guard('live.tips_delivery.write'), express.json({ lim
try { chatServer.broadcastGlobal({ ...event, global: true, channel_user_id: userId }); } catch { /* non-critical */ }
const saved = db.saveChatMessage({
stream_id: streamId, channel_user_id: userId, user_id: null, username: name,
-- message: String(b.text || `${name} tipped ${amount.toLocaleString()} Vibes${message ? ': ' + message : ''}`).slice(0, 1000),
++ message: String(b.text || `${name} ${amount == null ? 'sent a tip' : `tipped ${amount.toLocaleString()} Vibes`}${message ? ': ' + message : ''}`).slice(0, 1000),
message_type: 'donation',
metadata: { kind: 'donation', amount, message, username: name, source: 'tips', interaction_id: i.id || null, paid_message: event.paid_message, highlight_seconds: event.highlight_seconds, test: !!b.test },
});
ddiff --git a/test/tips-delivery.test.js b/test/tips-delivery.test.js
iindex f05f071e..16a4dcf6 100644
---- a/test/tips-delivery.test.js
++++ b/test/tips-delivery.test.js
@@@ -122,6 +122,14 @@ const token = (cap, aud = 'openvibe.live') => serviceAuth.signServiceToken({ iss
assert.strictEqual(tts[0][2], 'read me');
assert.strictEqual(tts[0][6], 501);
++ // A hidden amount (Tips privacy.hide_amount sends null) stays hidden: no "0 Vibes" anywhere.
++ const h = await post({ ...job, text: undefined, interaction: { ...job.interaction, id: 'tint_h', amount: null, message: 'hi' } }, { key: 'tint_h:chat_line' });
++ assert.strictEqual(h.status, 200);
++ assert.strictEqual(broadcasts[broadcasts.length - 1].ev.amount, null);
++ const hs = d.prepare('SELECT * FROM chat_messages WHERE id = ?').get(h.json.ref.chat_message_id);
++ assert.strictEqual(hs.message, 'Viewer sent a tip: hi');
++ assert.strictEqual(JSON.parse(hs.metadata).amount, null);
++
// Unknown creator: permanent refusal.
const u = await post({ ...job, creator: { type: 'user', id: 'usr_01J0000000000000000000000Y' } }, { key: 'tint_2:chat_line' });
assert.strictEqual(u.status, 404);