openvibers/openvibe.live Commits
Chat: persist test tips, and show donations in global chat
Changes
@@@ -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>@@@ -3982,6 +3982,19 @@ async function loadGlobalChatHistory() {if (m.message_type === 'system') {// Render system messages (update announcements, etc.) with system stylingaddRichSystemMessage(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,@@@ -469,7 +469,7 @@ router.get('/global/history', optionalAuth, (req, res) => {LEFT JOIN users su ON s.user_id = su.idLEFT JOIN users cu ON cm.channel_user_id = cu.idLEFT 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 = [];@@@ -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', messaglet 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).@@@ -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,
@@ -4975,7 +4975,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
@@ -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/chat-shared.js?v=3" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
<script src="/js/gif-picker.js?v=1" 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/chat-autocomplete.js?v=1" defer></script>
<script src="/js/channel-uploads.js?v=12" defer></script>
<script src="/js/channel-uploads.js?v=12" defer></script>
<script src="/js/controls.js?v=8" defer></script>
<script src="/js/controls.js?v=8" defer></script>
@@ -3982,6 +3982,19 @@ async function loadGlobalChatHistory() {
@@ -3982,6 +3982,19 @@ async function loadGlobalChatHistory() {
if (m.message_type === 'system') {
if (m.message_type === 'system') {
// Render system messages (update announcements, etc.) with system styling
// Render system messages (update announcements, etc.) with system styling
addRichSystemMessage(esc(m.message), 'update');
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 {
} else {
addChatMessage({
addChatMessage({
id: m.id,
id: m.id,
@@ -469,7 +469,7 @@ router.get('/global/history', optionalAuth, (req, res) => {
@@ -469,7 +469,7 @@ router.get('/global/history', optionalAuth, (req, res) => {
LEFT JOIN users su ON s.user_id = su.id
LEFT JOIN users su ON s.user_id = su.id
LEFT JOIN users cu ON cm.channel_user_id = cu.id
LEFT JOIN users cu ON cm.channel_user_id = cu.id
LEFT JOIN managed_streams ms ON s.managed_stream_id = ms.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)`;
AND (cm.auto_delete_at IS NULL OR datetime(cm.auto_delete_at) > CURRENT_TIMESTAMP)`;
const params = [];
const params = [];
@@ -98,10 +98,12 @@ function _handleDonation(userId, data) {
@@ -98,10 +98,12 @@ function _handleDonation(userId, data) {
try { goalResult = openvibeBucks.applyDonationToGoal(userId, amount, goalId); } catch { /* */ }
try { goalResult = openvibeBucks.applyDonationToGoal(userId, amount, goalId); } catch { /* */ }
// 1) Donation chat event — live + persisted to channel history.
// 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,
type: 'donation', username: donor, user_id: null, avatar_url: null,
amount, message, source: 'powerchat', timestamp: ts,
amount, message, source: 'powerchat', timestamp: ts,
- });
+ };
+ chatServer.broadcastToChannelRoom(userId, streamId, donationEvent);
+ try { chatServer.broadcastGlobal({ ...donationEvent, global: true, channel_user_id: userId }); } catch { /* */ }
try {
try {
db.saveChatMessage({
db.saveChatMessage({
stream_id: streamId, channel_user_id: userId, user_id: null, username: donor,
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
@@ -212,10 +214,23 @@ function simulateDonation(userId, { amountUsd = 5, donor = 'Test Tipper', messag
let streamId = null;
let streamId = null;
try { const live = db.getLiveStreamsByUserId(userId) || []; if (live.length) streamId = live[0].id; } catch { /* */ }
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,
type: 'donation', username: donor, user_id: null, avatar_url: null,
amount, message, source: 'powerchat-test', timestamp: ts,
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 { /* */ }
try { alerts.playAlertSound(chatServer, userId, streamId, 'donation'); } catch { /* */ }
// Transient goal-progress preview (does NOT persist — reload restores the real number).
// Transient goal-progress preview (does NOT persist — reload restores the real number).
@@ -79,11 +79,15 @@ router.post('/donate', requireAuth, (req, res) => {
@@ -79,11 +79,15 @@ router.post('/donate', requireAuth, (req, res) => {
// 1) Donation chat message — broadcast live AND persist to channel history so
// 1) Donation chat message — broadcast live AND persist to channel history so
// late-joiners see it. Channel-room broadcast reaches all slots + offline.
// 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,
type: 'donation', username: donor, user_id: req.user.id,
avatar_url: donorUser?.avatar_url || null,
avatar_url: donorUser?.avatar_url || null,
amount: result.amount, message: message || '', timestamp: ts,
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 {
try {
db.saveChatMessage({
db.saveChatMessage({
stream_id: stream_id || null, channel_user_id: streamer_id, user_id: req.user.id,
stream_id: stream_id || null, channel_user_id: streamer_id, user_id: req.user.id,