openvibers/openvibe.live Commits
Chat: PowerChat relay follows the channel, cross-feed stops duplicating
Changes
@@@ -5293,18 +5293,28 @@ function _handleGlobalFeedMessage(msg) {}// Don't duplicate messages from the channel we're currently viewing (its own-- // messages already render in the main timeline). Works live AND offline.-- if (hasStreamChannel) {++ // messages already render in the main timeline). Works live AND offline. The++ // local copy always takes precedence over the cross-feed one.++ // Fallback chain matters: in a popout pinned to an expired stream id,++ // currentStreamData is empty — chatChannel (set by the history load) and the++ // joined channel user id still identify the channel, so compare those too or++ // every own message renders twice (local + cross-feed copy).++ if (hasStreamChannel || msg.channel_user_id) {const currentStreamChannel = (currentStreamData && currentStreamData.username)|| (typeof currentChannelUsername !== 'undefined' && currentChannelUsername)++ || (typeof chatChannel !== 'undefined' && chatChannel)|| null;-- if (currentStreamChannel) {++ if (hasStreamChannel && currentStreamChannel) {const currentChannelNorm = String(currentStreamChannel).trim().toLowerCase();const sourceChannelNorm = String(msg.stream_channel).trim().toLowerCase();if (currentChannelNorm && currentChannelNorm === sourceChannelNorm) {return;}}++ if (msg.channel_user_id && typeof chatChannelUserId !== 'undefined' && chatChannelUserId++ && Number(msg.channel_user_id) === Number(chatChannelUserId)) {++ return;++ }}// Render into the ACTIVE chat container (live sidebar, offline chat, or broadcast).@@@ -906,7 +906,10 @@ class ChatServer {// AI viewers genuinely need a live stream.try {const pc = require('../integrations/powerchat-platform');-- if (client.channelUserId && pc.slotRelayEnabled(client.streamId)) {++ // Relay is per CHANNEL, not per slot: a popout pinned to an expired stream++ // id keeps chatting in the same channel, so the slot check follows the++ // channel's current live stream rather than the stale id.++ if (client.channelUserId && pc.channelRelayEnabled(client.channelUserId, client.streamId)) {let isMod = false, isSub = false;try { isMod = !!(client.user && permissions.canModerateChannel(client.user, client.channelUserId)); } catch { /* */ }try { isSub = !!(client.user && db.isActiveSubscriber(client.user.id, client.channelUserId)); } catch { /* */ }@@@ -106,6 +106,24 @@ function slotRelayEnabled(streamId) {return !ms || ms.slot_powerchat_relay !== 0;} catch { return true; }}++// Channel-scoped relay check for chat: PowerChat's unified chat is per STREAMER, not++// per stream slot, and viewers can be pinned to a long-expired stream id (a popout++// opened during an earlier session keeps chatting in the same channel). A stale id++// must not decide the relay — when the referenced stream isn't the live one, fall++// back to the channel's CURRENT live stream's slot setting (or allow when offline).++function channelRelayEnabled(channelUserId, streamId) {++ try {++ let s = streamId ? db.getStreamById(streamId) : null;++ if (!s || !s.is_live) {++ const live = channelUserId ? (db.getLiveStreamsByUserId(channelUserId) || []) : [];++ s = live[0] || null;++ }++ if (!s || !s.managed_stream_id) return true;++ const ms = db.get('SELECT slot_powerchat_relay FROM managed_streams WHERE id = ?', [s.managed_stream_id]);++ return !ms || ms.slot_powerchat_relay !== 0;++ } catch { return true; }++}++// Destination switch: the Twitch/Kick/YouTube relay bridge for one restream destination.function destRelayEnabled(destId) {if (!destId) return true;@@@ -440,7 +458,7 @@ async function sendCustomAlert(streamerUserId, { actorName, message, amountCents}module.exports = {-- slotRelayEnabled, destRelayEnabled, totalViewersForStream,++ slotRelayEnabled, channelRelayEnabled, destRelayEnabled, totalViewersForStream,CURRENCY_KEY, TIP_CURRENCY_KEY,forwardChat, forwardFollow, forwardSubscription, forwardTip,sendViewCount, startViewerCountSweeper,
@@ -5293,18 +5293,28 @@ function _handleGlobalFeedMessage(msg) {
@@ -5293,18 +5293,28 @@ function _handleGlobalFeedMessage(msg) {
}
}
// Don't duplicate messages from the channel we're currently viewing (its own
// Don't duplicate messages from the channel we're currently viewing (its own
- // messages already render in the main timeline). Works live AND offline.
- if (hasStreamChannel) {
+ // messages already render in the main timeline). Works live AND offline. The
+ // local copy always takes precedence over the cross-feed one.
+ // Fallback chain matters: in a popout pinned to an expired stream id,
+ // currentStreamData is empty — chatChannel (set by the history load) and the
+ // joined channel user id still identify the channel, so compare those too or
+ // every own message renders twice (local + cross-feed copy).
+ if (hasStreamChannel || msg.channel_user_id) {
const currentStreamChannel = (currentStreamData && currentStreamData.username)
const currentStreamChannel = (currentStreamData && currentStreamData.username)
|| (typeof currentChannelUsername !== 'undefined' && currentChannelUsername)
|| (typeof currentChannelUsername !== 'undefined' && currentChannelUsername)
+ || (typeof chatChannel !== 'undefined' && chatChannel)
|| null;
|| null;
- if (currentStreamChannel) {
+ if (hasStreamChannel && currentStreamChannel) {
const currentChannelNorm = String(currentStreamChannel).trim().toLowerCase();
const currentChannelNorm = String(currentStreamChannel).trim().toLowerCase();
const sourceChannelNorm = String(msg.stream_channel).trim().toLowerCase();
const sourceChannelNorm = String(msg.stream_channel).trim().toLowerCase();
if (currentChannelNorm && currentChannelNorm === sourceChannelNorm) {
if (currentChannelNorm && currentChannelNorm === sourceChannelNorm) {
return;
return;
}
}
}
}
+ if (msg.channel_user_id && typeof chatChannelUserId !== 'undefined' && chatChannelUserId
+ && Number(msg.channel_user_id) === Number(chatChannelUserId)) {
+ return;
+ }
}
}
// Render into the ACTIVE chat container (live sidebar, offline chat, or broadcast).
// Render into the ACTIVE chat container (live sidebar, offline chat, or broadcast).
@@ -906,7 +906,10 @@ class ChatServer {
@@ -906,7 +906,10 @@ class ChatServer {
// AI viewers genuinely need a live stream.
// AI viewers genuinely need a live stream.
try {
try {
const pc = require('../integrations/powerchat-platform');
const pc = require('../integrations/powerchat-platform');
- if (client.channelUserId && pc.slotRelayEnabled(client.streamId)) {
+ // Relay is per CHANNEL, not per slot: a popout pinned to an expired stream
+ // id keeps chatting in the same channel, so the slot check follows the
+ // channel's current live stream rather than the stale id.
+ if (client.channelUserId && pc.channelRelayEnabled(client.channelUserId, client.streamId)) {
let isMod = false, isSub = false;
let isMod = false, isSub = false;
try { isMod = !!(client.user && permissions.canModerateChannel(client.user, client.channelUserId)); } catch { /* */ }
try { isMod = !!(client.user && permissions.canModerateChannel(client.user, client.channelUserId)); } catch { /* */ }
try { isSub = !!(client.user && db.isActiveSubscriber(client.user.id, client.channelUserId)); } catch { /* */ }
try { isSub = !!(client.user && db.isActiveSubscriber(client.user.id, client.channelUserId)); } catch { /* */ }
@@ -106,6 +106,24 @@ function slotRelayEnabled(streamId) {
@@ -106,6 +106,24 @@ function slotRelayEnabled(streamId) {
return !ms || ms.slot_powerchat_relay !== 0;
return !ms || ms.slot_powerchat_relay !== 0;
} catch { return true; }
} catch { return true; }
}
}
+// Channel-scoped relay check for chat: PowerChat's unified chat is per STREAMER, not
+// per stream slot, and viewers can be pinned to a long-expired stream id (a popout
+// opened during an earlier session keeps chatting in the same channel). A stale id
+// must not decide the relay — when the referenced stream isn't the live one, fall
+// back to the channel's CURRENT live stream's slot setting (or allow when offline).
+function channelRelayEnabled(channelUserId, streamId) {
+ try {
+ let s = streamId ? db.getStreamById(streamId) : null;
+ if (!s || !s.is_live) {
+ const live = channelUserId ? (db.getLiveStreamsByUserId(channelUserId) || []) : [];
+ s = live[0] || null;
+ }
+ if (!s || !s.managed_stream_id) return true;
+ const ms = db.get('SELECT slot_powerchat_relay FROM managed_streams WHERE id = ?', [s.managed_stream_id]);
+ return !ms || ms.slot_powerchat_relay !== 0;
+ } catch { return true; }
+}
+
// Destination switch: the Twitch/Kick/YouTube relay bridge for one restream destination.
// Destination switch: the Twitch/Kick/YouTube relay bridge for one restream destination.
function destRelayEnabled(destId) {
function destRelayEnabled(destId) {
if (!destId) return true;
if (!destId) return true;
@@ -440,7 +458,7 @@ async function sendCustomAlert(streamerUserId, { actorName, message, amountCents
@@ -440,7 +458,7 @@ async function sendCustomAlert(streamerUserId, { actorName, message, amountCents
}
}
module.exports = {
module.exports = {
- slotRelayEnabled, destRelayEnabled, totalViewersForStream,
+ slotRelayEnabled, channelRelayEnabled, destRelayEnabled, totalViewersForStream,
CURRENCY_KEY, TIP_CURRENCY_KEY,
CURRENCY_KEY, TIP_CURRENCY_KEY,
forwardChat, forwardFollow, forwardSubscription, forwardTip,
forwardChat, forwardFollow, forwardSubscription, forwardTip,
sendViewCount, startViewerCountSweeper,
sendViewCount, startViewerCountSweeper,