openvibers/openvibe.live Commits

Chat: persist test tips, and show donations in global chat

OP OpenVibers committed 18 Aug 2026 · 908aac21c5 · 5 files, +40 −8

Two separate bugs behind "vibes alerts vanish on reload".

1. The PowerChat TEST tip was the only donation path that broadcast without
   saving — simulateDonation() called broadcastToChannelRoom and never
   saveChatMessage, so it disappeared on refresh. Real Vibes tips and real
   PowerChat tips both persisted correctly, which is why the database held 8
   donation rows and none of them were the test message. A test that vanishes on
   reload does not test what the streamer is actually checking. It now saves the
   same row a real tip does, and logs if that fails rather than swallowing it.

2. Global chat filtered donations out at both ends: the history query listed
   message_type IN ('chat','system','channel-sound','soundboard') with no
   'donation', and the global renderer went straight to addChatMessage rather
   than the rich-card path used for channel history. Donations are now included
   in the query, rendered as proper donation cards there, and broadcast live via
   broadcastGlobal from all three emitters — so a tip shows up site-wide as it
   happens, not only after a reload.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Xw6Sf5zzSALw8m93XNu5JE

Changes

ddiff --git a/public/index.html b/public/index.html
iindex c8a6c1b2..c63ebdf1 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -4975,7 +4975,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/chat-shared.js?v=3" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
<script src="/js/gif-picker.js?v=1" defer></script>
--<script src="/js/chat.js?v=70" defer></script>
++<script src="/js/chat.js?v=71" defer></script>
<script src="/js/chat-autocomplete.js?v=1" defer></script>
<script src="/js/channel-uploads.js?v=12" defer></script>
<script src="/js/controls.js?v=8" defer></script>
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex def7de48..36da5792 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -3982,6 +3982,19 @@ async function loadGlobalChatHistory() {
if (m.message_type === 'system') {
// Render system messages (update announcements, etc.) with system styling
addRichSystemMessage(esc(m.message), 'update');
++ } else if (m.message_type === 'donation') {
++ // Donations carry their payload in metadata — render the rich card here
++ // too, not a plain text line. Global history renders through addChatMessage
++ // directly rather than _renderHistoryMainMsg, so it needs its own hook.
++ let meta = null;
++ try { meta = typeof m.metadata === 'string' ? JSON.parse(m.metadata) : m.metadata; } catch { meta = null; }
++ if (meta && meta.kind === 'goal-reached') {
++ addDonationGoalMessage({ goal: { title: meta.title, target_amount: meta.target, image_url: meta.image, media_type: meta.media_type }, by: meta.by });
++ } else if (meta && meta.kind === 'donation') {
++ addDonationMessage({ username: meta.username, amount: meta.amount, message: meta.message, avatar_url: meta.avatar_url, user_id: meta.user_id });
++ } else {
++ addRichSystemMessage(esc(m.message), 'update');
++ }
} else {
addChatMessage({
id: m.id,
ddiff --git a/server/chat/routes.js b/server/chat/routes.js
iindex 5bf4205f..44637b77 100644
---- a/server/chat/routes.js
++++ b/server/chat/routes.js
@@@ -469,7 +469,7 @@ router.get('/global/history', optionalAuth, (req, res) => {
LEFT JOIN users su ON s.user_id = su.id
LEFT JOIN users cu ON cm.channel_user_id = cu.id
LEFT JOIN managed_streams ms ON s.managed_stream_id = ms.id
-- WHERE cm.is_deleted = 0 AND cm.message_type IN ('chat', 'system', 'channel-sound', 'soundboard')
++ WHERE cm.is_deleted = 0 AND cm.message_type IN ('chat', 'system', 'channel-sound', 'soundboard', 'donation')
AND (cm.auto_delete_at IS NULL OR datetime(cm.auto_delete_at) > CURRENT_TIMESTAMP)`;
const params = [];
ddiff --git a/server/integrations/powerchat-webhook.js b/server/integrations/powerchat-webhook.js
iindex 0ea579e2..cbc474fc 100644
---- a/server/integrations/powerchat-webhook.js
++++ b/server/integrations/powerchat-webhook.js
@@@ -98,10 +98,12 @@ function _handleDonation(userId, data) {
try { goalResult = openvibeBucks.applyDonationToGoal(userId, amount, goalId); } catch { /* */ }
// 1) Donation chat event — live + persisted to channel history.
-- chatServer.broadcastToChannelRoom(userId, streamId, {
++ const donationEvent = {
type: 'donation', username: donor, user_id: null, avatar_url: null,
amount, message, source: 'powerchat', timestamp: ts,
-- });
++ };
++ chatServer.broadcastToChannelRoom(userId, streamId, donationEvent);
++ try { chatServer.broadcastGlobal({ ...donationEvent, global: true, channel_user_id: userId }); } catch { /* */ }
try {
db.saveChatMessage({
stream_id: streamId, channel_user_id: userId, user_id: null, username: donor,
@@@ -212,10 +214,23 @@ function simulateDonation(userId, { amountUsd = 5, donor = 'Test Tipper', messag
let streamId = null;
try { const live = db.getLiveStreamsByUserId(userId) || []; if (live.length) streamId = live[0].id; } catch { /* */ }
-- chatServer.broadcastToChannelRoom(userId, streamId, {
++ const testEvent = {
type: 'donation', username: donor, user_id: null, avatar_url: null,
amount, message, source: 'powerchat-test', timestamp: ts,
-- });
++ };
++ chatServer.broadcastToChannelRoom(userId, streamId, testEvent);
++ try { chatServer.broadcastGlobal({ ...testEvent, global: true, channel_user_id: userId }); } catch { /* */ }
++ // Persist it, exactly like a real tip. A test that vanishes on reload does not
++ // actually test what the streamer is checking — that the alert lands in chat AND
++ // survives a refresh. This was the only donation path that broadcast without saving.
++ try {
++ db.saveChatMessage({
++ stream_id: streamId, channel_user_id: userId, user_id: null, username: donor,
++ message: `${donor} donated ${amount.toLocaleString()} Vibes${message ? ': ' + message : ''}`,
++ message_type: 'donation',
++ metadata: { kind: 'donation', amount, message, username: donor, source: 'powerchat-test' },
++ });
++ } catch (e) { console.warn('[PowerChat] test tip not saved to history:', e.message); }
try { alerts.playAlertSound(chatServer, userId, streamId, 'donation'); } catch { /* */ }
// Transient goal-progress preview (does NOT persist — reload restores the real number).
ddiff --git a/server/monetization/routes.js b/server/monetization/routes.js
iindex e8cdbb8b..21d6792e 100644
---- a/server/monetization/routes.js
++++ b/server/monetization/routes.js
@@@ -79,11 +79,15 @@ router.post('/donate', requireAuth, (req, res) => {
// 1) Donation chat message — broadcast live AND persist to channel history so
// late-joiners see it. Channel-room broadcast reaches all slots + offline.
-- chatServer.broadcastToChannelRoom(streamer_id, stream_id || null, {
++ const donationEvent = {
type: 'donation', username: donor, user_id: req.user.id,
avatar_url: donorUser?.avatar_url || null,
amount: result.amount, message: message || '', timestamp: ts,
-- });
++ };
++ chatServer.broadcastToChannelRoom(streamer_id, stream_id || null, donationEvent);
++ // Tips are a site-wide event worth celebrating, so mirror them into global chat
++ // instead of confining them to the channel that received them.
++ try { chatServer.broadcastGlobal({ ...donationEvent, global: true, channel_user_id: streamer_id }); } catch { /* */ }
try {
db.saveChatMessage({
stream_id: stream_id || null, channel_user_id: streamer_id, user_id: req.user.id,