From 0a14318dd6cb9e2eae142956126cfd000c3027d9 Mon Sep 17 00:00:00 2001 From: Callum Prentice Date: Wed, 8 Oct 2025 18:58:27 -0700 Subject: [PATCH 01/14] First commit for a second experiment, investigating how nearby voice/chat moderation might work --- indra/newview/CMakeLists.txt | 2 + indra/newview/llfloaterimcontainer.cpp | 96 +++++++++++++++- indra/newview/llfloaterimcontainer.h | 2 + indra/newview/llnearbyvoicemoderation.cpp | 102 +++++++++++++++++ indra/newview/llnearbyvoicemoderation.h | 41 +++++++ .../default/xui/en/menu_conversation.xml | 105 +++++++++--------- 6 files changed, 291 insertions(+), 57 deletions(-) create mode 100644 indra/newview/llnearbyvoicemoderation.cpp create mode 100644 indra/newview/llnearbyvoicemoderation.h diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index c727d5ae572..239193001cf 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -414,6 +414,7 @@ set(viewer_SOURCE_FILES llfloaterimnearbychat.cpp llfloaterimnearbychathandler.cpp llfloaterimnearbychatlistener.cpp + llnearbyvoicemoderation.cpp llnetmap.cpp llnotificationalerthandler.cpp llnotificationgrouphandler.cpp @@ -1087,6 +1088,7 @@ set(viewer_HEADER_FILES llnameeditor.h llnamelistctrl.h llnavigationbar.h + llnearbyvoicemoderation.h llnetmap.h llnotificationhandler.h llnotificationlistitem.h diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index d821d9a4a56..9a016f6286a 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -57,6 +57,8 @@ #include "llsdserialize.h" #include "llviewermenu.h" // is_agent_mappable #include "llviewerobjectlist.h" +#include "llvoavatar.h" +#include "llnearbyvoicemoderation.h" const S32 EVENTS_PER_IDLE_LOOP_CURRENT_SESSION = 80; @@ -502,12 +504,13 @@ void LLFloaterIMContainer::idleUpdate() const LLConversationItem *current_session = getCurSelectedViewModelItem(); if (current_session) { - if (current_session->getType() == LLConversationItem::CONV_SESSION_GROUP) + bool is_nearby_chat = current_session->getType() == LLConversationItem::CONV_SESSION_NEARBY; + if (current_session->getType() == LLConversationItem::CONV_SESSION_GROUP || is_nearby_chat) { // Update moderator options visibility LLFolderViewModelItemCommon::child_list_t::const_iterator current_participant_model = current_session->getChildrenBegin(); LLFolderViewModelItemCommon::child_list_t::const_iterator end_participant_model = current_session->getChildrenEnd(); - bool is_moderator = isGroupModerator(); + bool is_moderator = isGroupModerator() || (is_nearby_chat && isNearbyChatModerator()); bool can_ban = haveAbilityToBan(); while (current_participant_model != end_participant_model) { @@ -1685,6 +1688,10 @@ bool LLFloaterIMContainer::visibleContextMenuItem(const LLSD& userdata) { return isMuted(conversation_item->getUUID()); } + else if ("can_allow_text_chat" == item) + { + return !isNearbyChatSpeakerSelected(); + } return true; } @@ -2009,9 +2016,27 @@ LLConversationViewParticipant* LLFloaterIMContainer::createConversationViewParti bool LLFloaterIMContainer::enableModerateContextMenuItem(const std::string& userdata, bool is_self) { - // only group moderators can perform actions related to this "enable callback" - if (!isGroupModerator()) + if (isNearbyChatModerator() && isNearbyChatSpeakerSelected()) + { + // Determine here which actions are allowed + if ("can_moderate_voice" == userdata) + { + return true; + } + else if (("can_mute" == userdata)) + { + return true; + } + else if ("can_unmute" == userdata) + { + return true; + } + + return false; + } + else if (!isGroupModerator()) { + // only group moderators can perform actions related to this "enable callback" return false; } @@ -2144,7 +2169,37 @@ void LLFloaterIMContainer::banSelectedMember(const LLUUID& participant_uuid) void LLFloaterIMContainer::moderateVoice(const std::string& command, const LLUUID& userID) { - if (!gAgent.getRegion()) return; + if (!gAgent.getRegion()) + { + return; + } + + if (isNearbyChatSpeakerSelected()) + { + if ("selected" == command) + { + // Toggle the voice icon display + LLAvatarActions::toggleMuteVoice(userID); + + // Request a mute/unmute using a capability request via the simulator + const bool mute_state = LLAvatarActions::isVoiceMuted(userID); + LLNearbyVoiceModeration::getInstance()->requestMuteChange(userID, mute_state); + } + else + if ("mute_all" == command) + { + // TODO: the SpatialVoiceModerationRequest has an mute_all/unmute_all + // verb but we do not have an equivalent of LLAvatarActions::toggleMuteVoice(userID); + // to visually mute all the speaker icons in the conversation floater + } + else + if ("unmute_all" == command) + { + // TODO: same idea as "mute_all" above + } + + return; + } if (command.compare("selected")) { @@ -2262,6 +2317,37 @@ LLSpeaker * LLFloaterIMContainer::getSpeakerOfSelectedParticipant(LLSpeakerMgr * return speaker_managerp->findSpeaker(participant_itemp->getUUID()); } +bool LLFloaterIMContainer::isNearbyChatSpeakerSelected() +{ + LLFolderViewItem *selectedItem = mConversationsRoot->getCurSelectedItem(); + if (NULL == selectedItem) + { + LL_WARNS() << "Current selected item is null" << LL_ENDL; + return NULL; + } + + conversations_widgets_map::const_iterator iter = mConversationsWidgets.begin(); + conversations_widgets_map::const_iterator end = mConversationsWidgets.end(); + const LLUUID * conversation_uuidp = NULL; + while(iter != end) + { + if (iter->second == selectedItem || iter->second == selectedItem->getParentFolder()) + { + conversation_uuidp = &iter->first; + break; + } + ++iter; + } + // Nearby chat ID is LLUUID::null + return conversation_uuidp->isNull(); +} + +bool LLFloaterIMContainer::isNearbyChatModerator() +{ + // TODO: Need a better heurestic for determining if this person is a moderator :) + return true; +} + void LLFloaterIMContainer::toggleAllowTextChat(const LLUUID& participant_uuid) { LLIMSpeakerMgr * speaker_managerp = dynamic_cast(getSpeakerMgrForSelectedParticipant()); diff --git a/indra/newview/llfloaterimcontainer.h b/indra/newview/llfloaterimcontainer.h index 30eed8be365..c91093c1073 100644 --- a/indra/newview/llfloaterimcontainer.h +++ b/indra/newview/llfloaterimcontainer.h @@ -178,6 +178,8 @@ class LLFloaterIMContainer void banSelectedMember(const LLUUID& participant_uuid); void openNearbyChat(); bool isParticipantListExpanded(); + bool isNearbyChatSpeakerSelected(); + bool isNearbyChatModerator(); void idleUpdate(); // for convenience (self) from static idle void idleProcessEvents(); diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp new file mode 100644 index 00000000000..d714fc36b4f --- /dev/null +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -0,0 +1,102 @@ +/** + * @file llnearbyvoicemoderation.cpp + * + * $LicenseInfo:firstyear=2008&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llagent.h" +#include "llviewerregion.h" +#include "llvoavatar.h" +#include "llviewerobjectlist.h" + +#include "llnearbyvoicemoderation.h" + +LLVOAvatar* LLNearbyVoiceModeration::getVOAvatarFromId(const LLUUID& agent_id) +{ + LLViewerObject *obj = gObjectList.findObject(agent_id); + while (obj && obj->isAttachment()) + { + obj = (LLViewerObject*)obj->getParent(); + } + + if (obj && obj->isAvatar()) + { + return (LLVOAvatar*)obj; + } + else + { + return NULL; + } +} + +void LLNearbyVoiceModeration::requestMuteChange(const LLUUID& agent_id, bool mute) +{ + LLVOAvatar* avatar = getVOAvatarFromId(agent_id); + if (avatar) + { + LLViewerRegion* region = avatar->getRegion(); + if (! region || ! region->capabilitiesReceived()) + { + // TODO: Retry if fails since the capabilities may not have been received + // if this is called early into a region entry + LL_INFOS() << "Region or region capabilities unavailable" << LL_ENDL; + return; + } + LL_INFOS() << "Region name is " << region->getName() << LL_ENDL; + + std::string url = region->getCapability("SpatialVoiceModerationRequest"); + if (url.empty()) + { + // TODO: Retry if fails since URL may not have not be available + // if this is called early into a region entry + LL_INFOS() << "Capability URL is empty" << LL_ENDL; + return; + } + LL_INFOS() << "Capability URL is " << url << LL_ENDL; + + const std::string agent_name = avatar->getFullname(); + + const std::string operand = mute ? "mute" : "unmute"; + + LLSD body; + body["operand"] = operand; + body["agent_id"] = agent_id; + body["moderator_id"] = gAgent.getID(); + + LL_INFOS() << "Resident " << agent_name + << " (" << agent_id << ")" << " applying " << operand << LL_ENDL; + + std::string success_msg = + STRINGIZE("Resident " << agent_name + << " (" << agent_id << ")" << " nearby voice was set to " << operand); + + std::string failure_msg = + STRINGIZE("Unable to change voice muting for resident " + << agent_name << " (" << agent_id << ")"); + + LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost(url, body, + success_msg, + failure_msg); + } +} diff --git a/indra/newview/llnearbyvoicemoderation.h b/indra/newview/llnearbyvoicemoderation.h new file mode 100644 index 00000000000..4275754dbff --- /dev/null +++ b/indra/newview/llnearbyvoicemoderation.h @@ -0,0 +1,41 @@ +/** + * @file llnearbyvoicemoderation.h + * + * $LicenseInfo:firstyear=2008&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +class LLVOAvatar; + +class LLNearbyVoiceModeration : + public LLSingleton { + LLSINGLETON(LLNearbyVoiceModeration) { + }; + + ~LLNearbyVoiceModeration() { + }; + + public: + LLVOAvatar* getVOAvatarFromId(const LLUUID& id); + void requestMuteChange(const LLUUID& userID, bool mute); +}; diff --git a/indra/newview/skins/default/xui/en/menu_conversation.xml b/indra/newview/skins/default/xui/en/menu_conversation.xml index 62cdaa5886d..cf9b8959ce4 100644 --- a/indra/newview/skins/default/xui/en/menu_conversation.xml +++ b/indra/newview/skins/default/xui/en/menu_conversation.xml @@ -176,57 +176,58 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - From 219da2a16b3ae6e1d2e02b05159b37fa1aca1f47 Mon Sep 17 00:00:00 2001 From: Callum Prentice Date: Thu, 9 Oct 2025 10:44:32 -0700 Subject: [PATCH 02/14] Remove separator bar for Nearby Voice --- indra/newview/skins/default/xui/en/menu_conversation.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/indra/newview/skins/default/xui/en/menu_conversation.xml b/indra/newview/skins/default/xui/en/menu_conversation.xml index cf9b8959ce4..5a28f0dde57 100644 --- a/indra/newview/skins/default/xui/en/menu_conversation.xml +++ b/indra/newview/skins/default/xui/en/menu_conversation.xml @@ -190,7 +190,9 @@ - + + + Date: Mon, 13 Oct 2025 14:57:01 -0700 Subject: [PATCH 03/14] Improve robustness of when moderator options appear and add some initial code for muting indivudual / everyone via the capability --- indra/newview/llfloaterimcontainer.cpp | 42 +++++++++- indra/newview/llnearbyvoicemoderation.cpp | 97 ++++++++++++++++------- indra/newview/llnearbyvoicemoderation.h | 6 +- 3 files changed, 111 insertions(+), 34 deletions(-) diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index 9a016f6286a..2b9fb9dd375 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -504,13 +504,12 @@ void LLFloaterIMContainer::idleUpdate() const LLConversationItem *current_session = getCurSelectedViewModelItem(); if (current_session) { - bool is_nearby_chat = current_session->getType() == LLConversationItem::CONV_SESSION_NEARBY; - if (current_session->getType() == LLConversationItem::CONV_SESSION_GROUP || is_nearby_chat) + if (current_session->getType() == LLConversationItem::CONV_SESSION_GROUP) { // Update moderator options visibility LLFolderViewModelItemCommon::child_list_t::const_iterator current_participant_model = current_session->getChildrenBegin(); LLFolderViewModelItemCommon::child_list_t::const_iterator end_participant_model = current_session->getChildrenEnd(); - bool is_moderator = isGroupModerator() || (is_nearby_chat && isNearbyChatModerator()); + bool is_moderator = isGroupModerator(); bool can_ban = haveAbilityToBan(); while (current_participant_model != end_participant_model) { @@ -533,6 +532,23 @@ void LLFloaterIMContainer::idleUpdate() mGeneralTitleInUse = !needs_override; setTitle(needs_override ? conversation_floaterp->getTitle() : mGeneralTitle); } + const LLConversationItem* nearby_session = getSessionModel(LLUUID()); + if (nearby_session) + { + LLFolderViewModelItemCommon::child_list_t::const_iterator current_participant_model = nearby_session->getChildrenBegin(); + LLFolderViewModelItemCommon::child_list_t::const_iterator end_participant_model = nearby_session->getChildrenEnd(); + while (current_participant_model != end_participant_model) + { + LLConversationItemParticipant* participant_model = + dynamic_cast((*current_participant_model).get()); + if (participant_model) + { + participant_model->setModeratorOptionsVisible(isNearbyChatModerator()); + } + + current_participant_model++; + } + } } mParticipantRefreshTimer.setTimerExpirySec(1.0f); @@ -2183,7 +2199,7 @@ void LLFloaterIMContainer::moderateVoice(const std::string& command, const LLUUI // Request a mute/unmute using a capability request via the simulator const bool mute_state = LLAvatarActions::isVoiceMuted(userID); - LLNearbyVoiceModeration::getInstance()->requestMuteChange(userID, mute_state); + LLNearbyVoiceModeration::getInstance()->requestMuteIndividual(userID, mute_state); } else if ("mute_all" == command) @@ -2191,11 +2207,29 @@ void LLFloaterIMContainer::moderateVoice(const std::string& command, const LLUUI // TODO: the SpatialVoiceModerationRequest has an mute_all/unmute_all // verb but we do not have an equivalent of LLAvatarActions::toggleMuteVoice(userID); // to visually mute all the speaker icons in the conversation floater + + // Mute visually too + conversations_widgets_map::const_iterator iter = mConversationsWidgets.begin(); + conversations_widgets_map::const_iterator end = mConversationsWidgets.end(); + const LLUUID * conversation_uuidp = NULL; + while(iter != end) + { + const LLUUID id = (*iter).first; + ++iter; + } + + // Send the mute_all request to the server + const bool mute_state = true; + LLNearbyVoiceModeration::getInstance()->requestMuteAll(mute_state); } else if ("unmute_all" == command) { // TODO: same idea as "mute_all" above + + // Send the unmute_all request to the server + const bool mute_state = false; + LLNearbyVoiceModeration::getInstance()->requestMuteAll(mute_state); } return; diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp index d714fc36b4f..5ae8feba08c 100644 --- a/indra/newview/llnearbyvoicemoderation.cpp +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -50,53 +50,92 @@ LLVOAvatar* LLNearbyVoiceModeration::getVOAvatarFromId(const LLUUID& agent_id) } } -void LLNearbyVoiceModeration::requestMuteChange(const LLUUID& agent_id, bool mute) +const std::string LLNearbyVoiceModeration::getCapUrlFromRegion(LLViewerRegion* region) +{ + if (! region || ! region->capabilitiesReceived()) + { + // TODO: Retry if fails since the capabilities may not have been received + // if this is called early into a region entry + LL_INFOS() << "Region or region capabilities unavailable." << LL_ENDL; + return std::string(); + } + LL_INFOS() << "Capabilities for region " << region->getName() << " received." << LL_ENDL; + + std::string url = region->getCapability("SpatialVoiceModerationRequest"); + if (url.empty()) + { + // TODO: Retry if fails since URL may not have not be available + // if this is called early into a region entry + LL_INFOS() << "Capability URL for region " << region->getName() << " is empty" << LL_ENDL; + return std::string(); + } + LL_INFOS() << "Capability URL for region " << region->getName() << " is " << url << LL_ENDL; + + return url; +} + +void LLNearbyVoiceModeration::requestMuteIndividual(const LLUUID& agent_id, bool mute) { LLVOAvatar* avatar = getVOAvatarFromId(agent_id); if (avatar) { - LLViewerRegion* region = avatar->getRegion(); - if (! region || ! region->capabilitiesReceived()) + const std::string cap_url = getCapUrlFromRegion(avatar->getRegion()); + if (cap_url.length()) { - // TODO: Retry if fails since the capabilities may not have been received - // if this is called early into a region entry - LL_INFOS() << "Region or region capabilities unavailable" << LL_ENDL; - return; - } - LL_INFOS() << "Region name is " << region->getName() << LL_ENDL; + const std::string operand = mute ? "mute" : "unmute"; - std::string url = region->getCapability("SpatialVoiceModerationRequest"); - if (url.empty()) - { - // TODO: Retry if fails since URL may not have not be available - // if this is called early into a region entry - LL_INFOS() << "Capability URL is empty" << LL_ENDL; - return; + LLSD body; + body["operand"] = operand; + body["agent_id"] = agent_id; + body["moderator_id"] = gAgent.getID(); + + const std::string agent_name = avatar->getFullname(); + LL_INFOS() << "Resident " << agent_name + << " (" << agent_id << ")" << " applying " << operand << LL_ENDL; + + std::string success_msg = + STRINGIZE("Resident " << agent_name + << " (" << agent_id << ")" << " nearby voice was set to " << operand); + + std::string failure_msg = + STRINGIZE("Unable to change voice muting for resident " + << agent_name << " (" << agent_id << ")"); + + LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost( + cap_url, + body, + success_msg, + failure_msg); } - LL_INFOS() << "Capability URL is " << url << LL_ENDL; + } +} - const std::string agent_name = avatar->getFullname(); +void LLNearbyVoiceModeration::requestMuteAll(bool mute) +{ + // Use our own avatar to get the region name + LLViewerRegion* region = gAgent.getRegion(); - const std::string operand = mute ? "mute" : "unmute"; + const std::string cap_url = getCapUrlFromRegion(region); + if (cap_url.length()) + { + const std::string operand = mute ? "mute_all" : "unmute_all"; LLSD body; body["operand"] = operand; - body["agent_id"] = agent_id; body["moderator_id"] = gAgent.getID(); - LL_INFOS() << "Resident " << agent_name - << " (" << agent_id << ")" << " applying " << operand << LL_ENDL; + LL_INFOS() << "For all residents in this region, applying: " << operand << LL_ENDL; std::string success_msg = - STRINGIZE("Resident " << agent_name - << " (" << agent_id << ")" << " nearby voice was set to " << operand); + STRINGIZE("Nearby voice for all residents was set to: " << operand); std::string failure_msg = - STRINGIZE("Unable to change voice muting for resident " - << agent_name << " (" << agent_id << ")"); + STRINGIZE("Unable to set nearby voice for all residents to: " << operand); - LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost(url, body, - success_msg, - failure_msg); + LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost( + cap_url, + body, + success_msg, + failure_msg); } } diff --git a/indra/newview/llnearbyvoicemoderation.h b/indra/newview/llnearbyvoicemoderation.h index 4275754dbff..8a1ca5af6bd 100644 --- a/indra/newview/llnearbyvoicemoderation.h +++ b/indra/newview/llnearbyvoicemoderation.h @@ -37,5 +37,9 @@ class LLNearbyVoiceModeration : public: LLVOAvatar* getVOAvatarFromId(const LLUUID& id); - void requestMuteChange(const LLUUID& userID, bool mute); + void requestMuteIndividual(const LLUUID& userID, bool mute); + void requestMuteAll(bool mute); + + private: + const std::string getCapUrlFromRegion(LLViewerRegion* region); }; From cf048cf9c3ebd9ca8ee1e606b69bc459b62ed1c5 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 30 Oct 2025 16:59:51 +0200 Subject: [PATCH 04/14] #4013 Update voice moderator options; show notifications when muted; add stub code for info request --- indra/newview/llfloaterimcontainer.cpp | 35 +----- indra/newview/llfloaterimcontainer.h | 1 - indra/newview/llnearbyvoicemoderation.cpp | 113 +++++++++++++++++- indra/newview/llnearbyvoicemoderation.h | 26 ++-- indra/newview/llviewerregion.cpp | 1 + indra/newview/llvoiceclient.cpp | 4 + indra/newview/llvoicewebrtc.cpp | 8 +- .../skins/default/xui/en/notifications.xml | 23 ++++ 8 files changed, 165 insertions(+), 46 deletions(-) diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index c25fbdcc51e..ac8234bf24c 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -92,6 +92,7 @@ LLFloaterIMContainer::LLFloaterIMContainer(const LLSD& seed, const Params& param mAutoResize = false; LLTransientFloaterMgr::getInstance()->addControlView(LLTransientFloaterMgr::IM, this); + LLNearbyVoiceModeration::getInstance(); } LLFloaterIMContainer::~LLFloaterIMContainer() @@ -543,7 +544,7 @@ void LLFloaterIMContainer::idleUpdate() dynamic_cast((*current_participant_model).get()); if (participant_model) { - participant_model->setModeratorOptionsVisible(isNearbyChatModerator()); + participant_model->setModeratorOptionsVisible(LLNearbyVoiceModeration::getInstance()->isNearbyChatModerator()); } current_participant_model++; @@ -2037,7 +2038,7 @@ LLConversationViewParticipant* LLFloaterIMContainer::createConversationViewParti bool LLFloaterIMContainer::enableModerateContextMenuItem(const std::string& userdata, bool is_self) { - if (isNearbyChatModerator() && isNearbyChatSpeakerSelected()) + if (LLNearbyVoiceModeration::getInstance()->isNearbyChatModerator() && isNearbyChatSpeakerSelected()) { // Determine here which actions are allowed if ("can_moderate_voice" == userdata) @@ -2046,7 +2047,7 @@ bool LLFloaterIMContainer::enableModerateContextMenuItem(const std::string& user } else if (("can_mute" == userdata)) { - return true; + return !is_self; } else if ("can_unmute" == userdata) { @@ -2199,30 +2200,12 @@ void LLFloaterIMContainer::moderateVoice(const std::string& command, const LLUUI { if ("selected" == command) { - // Toggle the voice icon display - LLAvatarActions::toggleMuteVoice(userID); - // Request a mute/unmute using a capability request via the simulator - const bool mute_state = LLAvatarActions::isVoiceMuted(userID); - LLNearbyVoiceModeration::getInstance()->requestMuteIndividual(userID, mute_state); + LLNearbyVoiceModeration::getInstance()->requestMuteIndividual(userID, !isMuted(userID)); } else if ("mute_all" == command) { - // TODO: the SpatialVoiceModerationRequest has an mute_all/unmute_all - // verb but we do not have an equivalent of LLAvatarActions::toggleMuteVoice(userID); - // to visually mute all the speaker icons in the conversation floater - - // Mute visually too - conversations_widgets_map::const_iterator iter = mConversationsWidgets.begin(); - conversations_widgets_map::const_iterator end = mConversationsWidgets.end(); - const LLUUID * conversation_uuidp = NULL; - while(iter != end) - { - const LLUUID id = (*iter).first; - ++iter; - } - // Send the mute_all request to the server const bool mute_state = true; LLNearbyVoiceModeration::getInstance()->requestMuteAll(mute_state); @@ -2230,8 +2213,6 @@ void LLFloaterIMContainer::moderateVoice(const std::string& command, const LLUUI else if ("unmute_all" == command) { - // TODO: same idea as "mute_all" above - // Send the unmute_all request to the server const bool mute_state = false; LLNearbyVoiceModeration::getInstance()->requestMuteAll(mute_state); @@ -2381,12 +2362,6 @@ bool LLFloaterIMContainer::isNearbyChatSpeakerSelected() return conversation_uuidp->isNull(); } -bool LLFloaterIMContainer::isNearbyChatModerator() -{ - // TODO: Need a better heurestic for determining if this person is a moderator :) - return true; -} - void LLFloaterIMContainer::toggleAllowTextChat(const LLUUID& participant_uuid) { LLIMSpeakerMgr * speaker_managerp = dynamic_cast(getSpeakerMgrForSelectedParticipant()); diff --git a/indra/newview/llfloaterimcontainer.h b/indra/newview/llfloaterimcontainer.h index c91093c1073..9f1690a9b9c 100644 --- a/indra/newview/llfloaterimcontainer.h +++ b/indra/newview/llfloaterimcontainer.h @@ -179,7 +179,6 @@ class LLFloaterIMContainer void openNearbyChat(); bool isParticipantListExpanded(); bool isNearbyChatSpeakerSelected(); - bool isNearbyChatModerator(); void idleUpdate(); // for convenience (self) from static idle void idleProcessEvents(); diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp index 5ae8feba08c..ec0e1c04312 100644 --- a/indra/newview/llnearbyvoicemoderation.cpp +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -26,12 +26,29 @@ #include "llviewerprecompiledheaders.h" #include "llagent.h" +#include "llnotificationsutil.h" #include "llviewerregion.h" #include "llvoavatar.h" +#include "llvoiceclient.h" #include "llviewerobjectlist.h" #include "llnearbyvoicemoderation.h" +LLNearbyVoiceModeration::LLNearbyVoiceModeration() +{ + // TODO: default to false, when appropriate info cap is added + mIsNearbyChatModerator = true; + mParcelCallbackConnection = gAgent.addParcelChangedCallback([this]() { updateModeratorStatus(); }); +} + +LLNearbyVoiceModeration::~LLNearbyVoiceModeration() +{ + if (mParcelCallbackConnection.connected()) + { + mParcelCallbackConnection.disconnect(); + } +} + LLVOAvatar* LLNearbyVoiceModeration::getVOAvatarFromId(const LLUUID& agent_id) { LLViewerObject *obj = gObjectList.findObject(agent_id); @@ -54,18 +71,12 @@ const std::string LLNearbyVoiceModeration::getCapUrlFromRegion(LLViewerRegion* r { if (! region || ! region->capabilitiesReceived()) { - // TODO: Retry if fails since the capabilities may not have been received - // if this is called early into a region entry - LL_INFOS() << "Region or region capabilities unavailable." << LL_ENDL; return std::string(); } - LL_INFOS() << "Capabilities for region " << region->getName() << " received." << LL_ENDL; std::string url = region->getCapability("SpatialVoiceModerationRequest"); if (url.empty()) { - // TODO: Retry if fails since URL may not have not be available - // if this is called early into a region entry LL_INFOS() << "Capability URL for region " << region->getName() << " is empty" << LL_ENDL; return std::string(); } @@ -139,3 +150,93 @@ void LLNearbyVoiceModeration::requestMuteAll(bool mute) failure_msg); } } + +void LLNearbyVoiceModeration::setMutedInfo(const std::string& channelID, bool mute) +{ + auto it = mChannelMuteMap.find(channelID); + if (it == mChannelMuteMap.end()) + { + if (mute) + { + // Channel is new and being muted + showMutedNotification(true); + } + mChannelMuteMap[channelID] = mute; + } + else + { + if (it->second != mute) + { + // Flag changed + showMutedNotification(mute); + it->second = mute; + } + } +} + +void LLNearbyVoiceModeration::showNotificationIfNeeded() +{ + if (LLVoiceClient::getInstance()->inProximalChannel() && + LLVoiceClient::getInstance()->getIsModeratorMuted(gAgentID)) + { + showMutedNotification(true); + } +} + +void LLNearbyVoiceModeration::showMutedNotification(bool is_muted) +{ + // Check if the current voice channel is nearby chat + if (LLVoiceClient::getInstance()->inProximalChannel()) + { + LLNotificationsUtil::add(is_muted ? "NearbyVoiceMutedByModerator" : "NearbyVoiceUnmutedByModerator"); + } +} + +void LLNearbyVoiceModeration::updateModeratorStatus() +{ + LL_WARNS() << "Request moderator status info" << LL_ENDL; + // TODO: Uncomment and set correct capability name, when appropriate info cap is added + /* + if (LLViewerRegion* region = gAgent.getRegion()) + { + + std::string url = region->getCapability("SpatialVoiceModerationInfoRequest"); + if (!url.empty()) + { + LLCoros::instance().launch("getModeratorStatusCoro", [url]() { getModeratorStatusCoro(url); }); + } + } + */ +} + +void LLNearbyVoiceModeration::getModeratorStatusCoro(std::string cap_url) +{ + LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); + LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("getModeratorStatusCoro", httpPolicy)); + LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest); + LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions); + LLCore::HttpHeaders::ptr_t httpHeaders; + + httpOpts->setFollowRedirects(true); + + LLSD result = httpAdapter->getAndSuspend(httpRequest, cap_url, httpOpts, httpHeaders); + + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); + + if (!status) + { + LL_WARNS() << "Failed to get nearby voice moderator info" << LL_ENDL; + return; + } + else if (!result["success"].asBoolean()) + { + LL_WARNS() << "Failed to get nearby voice moderator info: " << result["message"] << LL_ENDL; + return; + } + + // TODO: update the field, when appropriate info cap is added + bool is_moderator = result["moderator"].asBoolean(); + LLNearbyVoiceModeration::getInstance()->setNearbyChatModerator(is_moderator); +} + diff --git a/indra/newview/llnearbyvoicemoderation.h b/indra/newview/llnearbyvoicemoderation.h index 8a1ca5af6bd..21d185db54b 100644 --- a/indra/newview/llnearbyvoicemoderation.h +++ b/indra/newview/llnearbyvoicemoderation.h @@ -27,19 +27,29 @@ class LLVOAvatar; -class LLNearbyVoiceModeration : - public LLSingleton { - LLSINGLETON(LLNearbyVoiceModeration) { - }; - - ~LLNearbyVoiceModeration() { - }; +class LLNearbyVoiceModeration : public LLSingleton { + LLSINGLETON(LLNearbyVoiceModeration); + ~LLNearbyVoiceModeration(); public: - LLVOAvatar* getVOAvatarFromId(const LLUUID& id); void requestMuteIndividual(const LLUUID& userID, bool mute); void requestMuteAll(bool mute); + void setMutedInfo(const std::string& channelID, bool mute); + void showMutedNotification(bool is_muted); + void showNotificationIfNeeded(); + + void updateModeratorStatus(); + static void getModeratorStatusCoro(std::string cap_url); + + bool isNearbyChatModerator() { return mIsNearbyChatModerator; }; + void setNearbyChatModerator(bool moderator) { mIsNearbyChatModerator = moderator; } + private: + LLVOAvatar* getVOAvatarFromId(const LLUUID& id); const std::string getCapUrlFromRegion(LLViewerRegion* region); + + boost::signals2::connection mParcelCallbackConnection; + std::map mChannelMuteMap; + bool mIsNearbyChatModerator; }; diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index a085bc4d914..fa60c368341 100755 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -3300,6 +3300,7 @@ void LLViewerRegionImpl::buildCapabilityNames(LLSD& capabilityNames) capabilityNames.append("SetDisplayName"); capabilityNames.append("SimConsoleAsync"); capabilityNames.append("SimulatorFeatures"); + capabilityNames.append("SpatialVoiceModerationRequest"); capabilityNames.append("StartGroupProposal"); capabilityNames.append("TerrainNavMeshProperties"); capabilityNames.append("TextureStats"); diff --git a/indra/newview/llvoiceclient.cpp b/indra/newview/llvoiceclient.cpp index 71a9e71a9fd..5a2ef248ba3 100644 --- a/indra/newview/llvoiceclient.cpp +++ b/indra/newview/llvoiceclient.cpp @@ -39,6 +39,7 @@ #include "llagent.h" #include "lltrans.h" #include "lluiusage.h" +#include "llnearbyvoicemoderation.h" const F32 LLVoiceClient::OVERDRIVEN_POWER_LEVEL = 0.7f; @@ -712,6 +713,9 @@ bool LLVoiceClient::getPTTIsToggle() void LLVoiceClient::inputUserControlState(bool down) { + if (down && !getUserPTTState()) + LLNearbyVoiceModeration::getInstance()->showNotificationIfNeeded(); + if(mPTTIsToggle) { if(down) // toggle open-mic state on 'down' diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index 93f8f0d14aa..1d78ca002ad 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -65,6 +65,7 @@ #include "llviewernetwork.h" #include "llnotificationsutil.h" +#include "llnearbyvoicemoderation.h" #include "llcorehttputil.h" #include "lleventfilter.h" @@ -3194,14 +3195,19 @@ void LLVoiceWebRTCConnection::OnDataReceivedImpl(const std::string &data, bool b participant->mIsSpeaking = participant_obj["v"].as_bool(); } + // Currently, viewer doesn't receive this info when the user is muted in Nearby chat, + // but it *does* receive it when muted in Group chat. A server-side change is required. if (participant_obj.contains("m") && participant_obj["m"].is_bool()) { participant->mIsModeratorMuted = participant_obj["m"].as_bool(); + if (isSpatial() && (gAgentID == agent_id)) + { + LLNearbyVoiceModeration::getInstance()->setMutedInfo(mChannelID, participant->mIsModeratorMuted); + } } } } } - // tell the simulator to set the mute and volume data for this // participant, if there are any updates. boost::json::object root; diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index dbd513afe8b..3f9a2ba13e2 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -9192,6 +9192,29 @@ Your voice has been muted by moderator. yestext="OK"/> + + The moderator has muted your voice chat. +People in this location will not hear you if you speak. + voice + + + + + Your voice chat can now be heard by people in this location. + voice + + + Date: Fri, 7 Nov 2025 17:19:56 +0200 Subject: [PATCH 05/14] #4013 add simple voice moderation permission check --- indra/newview/llnearbyvoicemoderation.cpp | 56 ++--------------------- indra/newview/llnearbyvoicemoderation.h | 7 +-- indra/newview/llviewerparcelmgr.cpp | 10 ++++ indra/newview/llviewerparcelmgr.h | 6 +++ 4 files changed, 21 insertions(+), 58 deletions(-) diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp index ec0e1c04312..2b4fd26afc0 100644 --- a/indra/newview/llnearbyvoicemoderation.cpp +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -31,22 +31,17 @@ #include "llvoavatar.h" #include "llvoiceclient.h" #include "llviewerobjectlist.h" +#include "llviewerparcelmgr.h" +#include "roles_constants.h" #include "llnearbyvoicemoderation.h" LLNearbyVoiceModeration::LLNearbyVoiceModeration() { - // TODO: default to false, when appropriate info cap is added - mIsNearbyChatModerator = true; - mParcelCallbackConnection = gAgent.addParcelChangedCallback([this]() { updateModeratorStatus(); }); } LLNearbyVoiceModeration::~LLNearbyVoiceModeration() { - if (mParcelCallbackConnection.connected()) - { - mParcelCallbackConnection.disconnect(); - } } LLVOAvatar* LLNearbyVoiceModeration::getVOAvatarFromId(const LLUUID& agent_id) @@ -192,51 +187,8 @@ void LLNearbyVoiceModeration::showMutedNotification(bool is_muted) } } -void LLNearbyVoiceModeration::updateModeratorStatus() -{ - LL_WARNS() << "Request moderator status info" << LL_ENDL; - // TODO: Uncomment and set correct capability name, when appropriate info cap is added - /* - if (LLViewerRegion* region = gAgent.getRegion()) - { - - std::string url = region->getCapability("SpatialVoiceModerationInfoRequest"); - if (!url.empty()) - { - LLCoros::instance().launch("getModeratorStatusCoro", [url]() { getModeratorStatusCoro(url); }); - } - } - */ -} - -void LLNearbyVoiceModeration::getModeratorStatusCoro(std::string cap_url) +bool LLNearbyVoiceModeration::isNearbyChatModerator() { - LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); - LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("getModeratorStatusCoro", httpPolicy)); - LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest); - LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions); - LLCore::HttpHeaders::ptr_t httpHeaders; - - httpOpts->setFollowRedirects(true); - - LLSD result = httpAdapter->getAndSuspend(httpRequest, cap_url, httpOpts, httpHeaders); - - LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; - LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); - - if (!status) - { - LL_WARNS() << "Failed to get nearby voice moderator info" << LL_ENDL; - return; - } - else if (!result["success"].asBoolean()) - { - LL_WARNS() << "Failed to get nearby voice moderator info: " << result["message"] << LL_ENDL; - return; - } - - // TODO: update the field, when appropriate info cap is added - bool is_moderator = result["moderator"].asBoolean(); - LLNearbyVoiceModeration::getInstance()->setNearbyChatModerator(is_moderator); + return gAgent.canManageEstate() || LLViewerParcelMgr::getInstance()->allowVoiceModeration(); } diff --git a/indra/newview/llnearbyvoicemoderation.h b/indra/newview/llnearbyvoicemoderation.h index 21d185db54b..95bae9ca2ad 100644 --- a/indra/newview/llnearbyvoicemoderation.h +++ b/indra/newview/llnearbyvoicemoderation.h @@ -39,11 +39,7 @@ class LLNearbyVoiceModeration : public LLSingleton { void showMutedNotification(bool is_muted); void showNotificationIfNeeded(); - void updateModeratorStatus(); - static void getModeratorStatusCoro(std::string cap_url); - - bool isNearbyChatModerator() { return mIsNearbyChatModerator; }; - void setNearbyChatModerator(bool moderator) { mIsNearbyChatModerator = moderator; } + bool isNearbyChatModerator(); private: LLVOAvatar* getVOAvatarFromId(const LLUUID& id); @@ -51,5 +47,4 @@ class LLNearbyVoiceModeration : public LLSingleton { boost::signals2::connection mParcelCallbackConnection; std::map mChannelMuteMap; - bool mIsNearbyChatModerator; }; diff --git a/indra/newview/llviewerparcelmgr.cpp b/indra/newview/llviewerparcelmgr.cpp index 432da2e9905..452c666652e 100644 --- a/indra/newview/llviewerparcelmgr.cpp +++ b/indra/newview/llviewerparcelmgr.cpp @@ -702,6 +702,16 @@ bool LLViewerParcelMgr::allowAgentVoice() const return allowAgentVoice(gAgent.getRegion(), mAgentParcel); } +bool LLViewerParcelMgr::isVoiceRestricted() const +{ + return mAgentParcel && !mAgentParcel->getParcelFlagUseEstateVoiceChannel(); +} + +bool LLViewerParcelMgr::allowVoiceModeration() const +{ + return isVoiceRestricted() && isParcelOwnedByAgent(mAgentParcel, GP_SESSION_MODERATOR); +} + bool LLViewerParcelMgr::allowAgentVoice(const LLViewerRegion* region, const LLParcel* parcel) const { return region && region->isVoiceEnabled() diff --git a/indra/newview/llviewerparcelmgr.h b/indra/newview/llviewerparcelmgr.h index 1925cd23ed2..8863bc7a062 100644 --- a/indra/newview/llviewerparcelmgr.h +++ b/indra/newview/llviewerparcelmgr.h @@ -173,6 +173,12 @@ class LLViewerParcelMgr : public LLSingleton bool allowAgentVoice() const; bool allowAgentVoice(const LLViewerRegion* region, const LLParcel* parcel) const; + // Returns true if this parcel is using private voice channel + bool isVoiceRestricted() const; + + // Can this agent moderate Nearby voice chat on this parcel? + bool allowVoiceModeration() const; + // Can this agent start flying on this parcel? // Used for parcel property icons in nav bar. bool allowAgentFly(const LLViewerRegion* region, const LLParcel* parcel) const; From a4d01edef6288528647968ea8d97fee8a553891d Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Tue, 11 Nov 2025 23:34:05 +0200 Subject: [PATCH 06/14] Show moderator options only on webrtc region --- indra/newview/llnearbyvoicemoderation.cpp | 3 ++- indra/newview/llviewerregion.cpp | 10 ++++++++++ indra/newview/llviewerregion.h | 2 ++ indra/newview/llvoicewebrtc.cpp | 2 -- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp index 2b4fd26afc0..10f92502a51 100644 --- a/indra/newview/llnearbyvoicemoderation.cpp +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -189,6 +189,7 @@ void LLNearbyVoiceModeration::showMutedNotification(bool is_muted) bool LLNearbyVoiceModeration::isNearbyChatModerator() { - return gAgent.canManageEstate() || LLViewerParcelMgr::getInstance()->allowVoiceModeration(); + return gAgent.getRegion() && gAgent.getRegion()->isRegionWebRTCEnabled() && + (gAgent.canManageEstate() || LLViewerParcelMgr::getInstance()->allowVoiceModeration()); } diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index fa60c368341..98440d96ae5 100755 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -3798,6 +3798,16 @@ std::string LLViewerRegion::getSimHostName() return std::string("..."); } + +bool LLViewerRegion::isRegionWebRTCEnabled() +{ + if (mSimulatorFeaturesReceived && mSimulatorFeatures.has("VoiceServerType")) + { + return mSimulatorFeatures["VoiceServerType"].asString() == "webrtc"; + } + return false; +} + void LLViewerRegion::applyCacheMiscExtras(LLViewerObject* obj) { LL_PROFILE_ZONE_SCOPED_CATEGORY_DISPLAY; diff --git a/indra/newview/llviewerregion.h b/indra/newview/llviewerregion.h index 244e2b7835c..bde0fa13ce5 100644 --- a/indra/newview/llviewerregion.h +++ b/indra/newview/llviewerregion.h @@ -424,6 +424,8 @@ class LLViewerRegion: public LLCapabilityProvider // implements this interface std::string getSimHostName(); + bool isRegionWebRTCEnabled(); + static bool isNewObjectCreationThrottleDisabled() {return sNewObjectCreationThrottle < 0;} // rebuild reflection probe list diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index c57557079e2..be2e840f630 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -3168,8 +3168,6 @@ void LLVoiceWebRTCConnection::OnDataReceivedImpl(const std::string &data, bool b participant->mIsSpeaking = participant_obj["v"].as_bool(); } - // Currently, viewer doesn't receive this info when the user is muted in Nearby chat, - // but it *does* receive it when muted in Group chat. A server-side change is required. if (participant_obj.contains("m") && participant_obj["m"].is_bool()) { participant->mIsModeratorMuted = participant_obj["m"].as_bool(); From d9ec89ac0a0ae2e44f00f524678d18cf66c52b13 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Wed, 12 Nov 2025 20:24:42 +0200 Subject: [PATCH 07/14] Ignore muted flags from non-primary voice server --- indra/newview/llvoicewebrtc.cpp | 45 ++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index be2e840f630..e4fdf85d12d 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -3170,14 +3170,53 @@ void LLVoiceWebRTCConnection::OnDataReceivedImpl(const std::string &data, bool b if (participant_obj.contains("m") && participant_obj["m"].is_bool()) { - participant->mIsModeratorMuted = participant_obj["m"].as_bool(); - if (isSpatial() && (gAgentID == agent_id)) + bool is_moderator_muted = participant_obj["m"].as_bool(); + if (isSpatial()) { - LLNearbyVoiceModeration::getInstance()->setMutedInfo(mChannelID, participant->mIsModeratorMuted); + // ignore muted flags from non-primary server + if (mPrimary || primary) + { + participant->mIsModeratorMuted = is_moderator_muted; + if (gAgentID == agent_id) + { + LLNearbyVoiceModeration::getInstance()->setMutedInfo(mChannelID, is_moderator_muted); + } + } + } + else + { + participant->mIsModeratorMuted = is_moderator_muted; } } } } + else + { + if (isSpatial() && (mPrimary || primary)) + { + // mute info message can be received before join message, so try to mute again later + if (participant_obj.contains("m") && participant_obj["m"].is_bool()) + { + bool is_moderator_muted = participant_obj["m"].as_bool(); + std::string channel_id = mChannelID; + F32 delay { 1.5f }; + doAfterInterval( + [channel_id, agent_id, is_moderator_muted]() + { + LLWebRTCVoiceClient::participantStatePtr_t participant = + LLWebRTCVoiceClient::getInstance()->findParticipantByID(channel_id, agent_id); + if (participant) + { + participant->mIsModeratorMuted = is_moderator_muted; + if (gAgentID == agent_id) + { + LLNearbyVoiceModeration::getInstance()->setMutedInfo(channel_id, is_moderator_muted); + } + } + }, delay); + } + } + } } // tell the simulator to set the mute and volume data for this // participant, if there are any updates. From 811105270d2cede0a99efbb501792d35ef61a530 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 13 Nov 2025 18:48:24 +0200 Subject: [PATCH 08/14] #4994 remove redundant moderator_id key --- indra/newview/llnearbyvoicemoderation.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp index 10f92502a51..5a6d7e1c6bb 100644 --- a/indra/newview/llnearbyvoicemoderation.cpp +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -93,7 +93,6 @@ void LLNearbyVoiceModeration::requestMuteIndividual(const LLUUID& agent_id, bool LLSD body; body["operand"] = operand; body["agent_id"] = agent_id; - body["moderator_id"] = gAgent.getID(); const std::string agent_name = avatar->getFullname(); LL_INFOS() << "Resident " << agent_name @@ -128,7 +127,6 @@ void LLNearbyVoiceModeration::requestMuteAll(bool mute) LLSD body; body["operand"] = operand; - body["moderator_id"] = gAgent.getID(); LL_INFOS() << "For all residents in this region, applying: " << operand << LL_ENDL; From bee23b4956b424f99765fb099906d3cb8250ce63 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 13 Nov 2025 19:02:49 +0200 Subject: [PATCH 09/14] #4995 change muted/unmuted alerts to non-modal toast --- .../skins/default/xui/en/notifications.xml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index 3f9a2ba13e2..14e9211b9c4 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -9193,26 +9193,20 @@ Your voice has been muted by moderator. + type="notifytip"> The moderator has muted your voice chat. People in this location will not hear you if you speak. - voice - + voice + type="notifytip"> Your voice chat can now be heard by people in this location. - voice - + voice Date: Fri, 14 Nov 2025 16:19:57 +0200 Subject: [PATCH 10/14] Toggle off 'Speak' button when muted by moderator --- indra/newview/llfloaterimcontainer.cpp | 2 +- indra/newview/llnearbyvoicemoderation.cpp | 13 ++++++++++--- indra/newview/llnearbyvoicemoderation.h | 4 ++-- indra/newview/llvoiceclient.cpp | 6 +++++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index ac8234bf24c..00b9c0b0529 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -2340,7 +2340,7 @@ LLSpeaker * LLFloaterIMContainer::getSpeakerOfSelectedParticipant(LLSpeakerMgr * bool LLFloaterIMContainer::isNearbyChatSpeakerSelected() { LLFolderViewItem *selectedItem = mConversationsRoot->getCurSelectedItem(); - if (NULL == selectedItem) + if (!selectedItem) { LL_WARNS() << "Current selected item is null" << LL_ENDL; return NULL; diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp index 5a6d7e1c6bb..d3e540e838f 100644 --- a/indra/newview/llnearbyvoicemoderation.cpp +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -165,24 +165,31 @@ void LLNearbyVoiceModeration::setMutedInfo(const std::string& channelID, bool mu it->second = mute; } } + if (mute && LLVoiceClient::getInstance()->getUserPTTState()) + { + LLVoiceClient::getInstance()->setUserPTTState(false); + } } -void LLNearbyVoiceModeration::showNotificationIfNeeded() +bool LLNearbyVoiceModeration::showNotificationIfNeeded() { if (LLVoiceClient::getInstance()->inProximalChannel() && LLVoiceClient::getInstance()->getIsModeratorMuted(gAgentID)) { - showMutedNotification(true); + return showMutedNotification(true); } + return false; } -void LLNearbyVoiceModeration::showMutedNotification(bool is_muted) +bool LLNearbyVoiceModeration::showMutedNotification(bool is_muted) { // Check if the current voice channel is nearby chat if (LLVoiceClient::getInstance()->inProximalChannel()) { LLNotificationsUtil::add(is_muted ? "NearbyVoiceMutedByModerator" : "NearbyVoiceUnmutedByModerator"); + return true; } + return false; } bool LLNearbyVoiceModeration::isNearbyChatModerator() diff --git a/indra/newview/llnearbyvoicemoderation.h b/indra/newview/llnearbyvoicemoderation.h index 95bae9ca2ad..619f1698839 100644 --- a/indra/newview/llnearbyvoicemoderation.h +++ b/indra/newview/llnearbyvoicemoderation.h @@ -36,8 +36,8 @@ class LLNearbyVoiceModeration : public LLSingleton { void requestMuteAll(bool mute); void setMutedInfo(const std::string& channelID, bool mute); - void showMutedNotification(bool is_muted); - void showNotificationIfNeeded(); + bool showMutedNotification(bool is_muted); + bool showNotificationIfNeeded(); bool isNearbyChatModerator(); diff --git a/indra/newview/llvoiceclient.cpp b/indra/newview/llvoiceclient.cpp index 5a2ef248ba3..2d732d0a71e 100644 --- a/indra/newview/llvoiceclient.cpp +++ b/indra/newview/llvoiceclient.cpp @@ -714,7 +714,11 @@ bool LLVoiceClient::getPTTIsToggle() void LLVoiceClient::inputUserControlState(bool down) { if (down && !getUserPTTState()) - LLNearbyVoiceModeration::getInstance()->showNotificationIfNeeded(); + { + // Nearby chat is muted by moderator, don't toggle PTT + if (LLNearbyVoiceModeration::getInstance()->showNotificationIfNeeded()) + return; + } if(mPTTIsToggle) { From 6ee41d60753d31d6826994731570f0ab64b92bd9 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Wed, 26 Nov 2025 18:41:30 +0200 Subject: [PATCH 11/14] #5018 add webrtc connection statistics --- indra/llwebrtc/llwebrtc.cpp | 51 ++++++++ indra/llwebrtc/llwebrtc.h | 6 + indra/llwebrtc/llwebrtc_impl.h | 2 + indra/newview/app_settings/settings.xml | 33 +++++ indra/newview/llviewerstats.cpp | 14 +++ indra/newview/llviewerstats.h | 2 + indra/newview/llvoicewebrtc.cpp | 117 ++++++++++++++++++ indra/newview/llvoicewebrtc.h | 6 + .../skins/default/xui/en/floater_stats.xml | 58 +++++++++ 9 files changed, 289 insertions(+) diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index b9f126e5119..62eadecd6f0 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -1521,6 +1521,57 @@ void LLWebRTCPeerConnectionImpl::unsetDataObserver(LLWebRTCDataObserver* observe } } +class LLStatsCollectorCallback : public webrtc::RTCStatsCollectorCallback +{ +public: + typedef std::function StatsCallback; + + LLStatsCollectorCallback(StatsCallback callback) : callback_(callback) {} + + void OnStatsDelivered(const webrtc::scoped_refptr& report) override + { + if (callback_) + { + // Transform RTCStatsReport stats to simple map + LLWebRTCStatsMap stats_map; + for (const auto& stats : *report) + { + std::map stat_attributes; + + // Convert each attribute to string format + for (const auto& attribute : stats.Attributes()) + { + stat_attributes[attribute.name()] = attribute.ToString(); + } + stats_map[stats.id()] = stat_attributes; + } + callback_(stats_map); + } + } + +private: + StatsCallback callback_; +}; + +void LLWebRTCPeerConnectionImpl::gatherConnectionStats() +{ + if (!mPeerConnection) + { + return; + } + + auto stats_callback = webrtc::make_ref_counted( + [this](const LLWebRTCStatsMap& generic_stats) + { + for (auto& observer : mSignalingObserverList) + { + observer->OnStatsDelivered(generic_stats); + } + }); + + mPeerConnection->GetStats(stats_callback.get()); +} + LLWebRTCImpl * gWebRTCImpl = nullptr; LLWebRTCDeviceInterface * getDeviceInterface() { diff --git a/indra/llwebrtc/llwebrtc.h b/indra/llwebrtc/llwebrtc.h index 7d06b7d2b40..e76e708f0ce 100644 --- a/indra/llwebrtc/llwebrtc.h +++ b/indra/llwebrtc/llwebrtc.h @@ -38,6 +38,7 @@ #ifndef LLWEBRTC_H #define LLWEBRTC_H +#include #include #include @@ -55,6 +56,7 @@ namespace llwebrtc { +typedef std::map> LLWebRTCStatsMap; class LLWebRTCLogCallback { @@ -240,6 +242,8 @@ class LLWebRTCSignalingObserver // Called when the data channel has been established and data // transfer can begin. virtual void OnDataChannelReady(LLWebRTCDataInterface *data_interface) = 0; + + virtual void OnStatsDelivered(const LLWebRTCStatsMap& stats_data) {} }; // LLWebRTCPeerConnectionInterface representsd a connection to a peer, @@ -273,6 +277,8 @@ class LLWebRTCPeerConnectionInterface virtual void unsetSignalingObserver(LLWebRTCSignalingObserver* observer) = 0; virtual void AnswerAvailable(const std::string &sdp) = 0; + + virtual void gatherConnectionStats() = 0; }; // The following define the dynamic linked library diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index 01cfb17ced2..2a00e066bde 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -648,6 +648,8 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, void enableSenderTracks(bool enable); void enableReceiverTracks(bool enable); + void gatherConnectionStats(); + protected: LLWebRTCImpl * mWebRTCImpl; diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index aca9910253f..eac6893504f 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -6060,6 +6060,39 @@ Value 0 + OpenDebugStatVoice + + Comment + Expand Voice (WebRTC) stats display + Persist + 1 + Type + Boolean + Value + 1 + + OpenDebugStatVoiceOutgoing + + Comment + Expand Outgoing audio (Voice) stats display + Persist + 1 + Type + Boolean + Value + 1 + + OpenDebugStatVoiceIncoming + + Comment + Expand Incoming audio (Voice) stats display + Persist + 1 + Type + Boolean + Value + 1 + OutBandwidth Comment diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp index d39d4662053..db6d83db5f4 100644 --- a/indra/newview/llviewerstats.cpp +++ b/indra/newview/llviewerstats.cpp @@ -263,6 +263,20 @@ LLTrace::SampleStatHandle > HUDS_FRAME_PCT("huds_ LLTrace::SampleStatHandle > UI_FRAME_PCT("ui_frame_pct"); LLTrace::SampleStatHandle > SWAP_FRAME_PCT("swap_frame_pct"); LLTrace::SampleStatHandle > IDLE_FRAME_PCT("idle_frame_pct"); + + + +LLTrace::SampleStatHandle WEBRTC_PACKETS_IN_LOST("webrtc_packets_in_lost", "Lost incoming packets"), + WEBRTC_PACKETS_IN_RECEIVED("webrtc_packets_in_recv", "Incoming packets received"), + WEBRTC_PACKETS_OUT_SENT("webrtc_packets_out_sent", "Outgoing packets sent"), + WEBRTC_PACKETS_OUT_LOST("webrtc_packets_out_lost", "Lost outgoing packets"); + +LLTrace::SampleStatHandle WEBRTC_JITTER_OUT("webrtc_jitter_out", "Timing variation of outgoing audio"), + WEBRTC_JITTER_IN("webrtc_jitter_in", "Timing variation of incoming audio"), + WEBRTC_LATENCY("webrtc_latency", "Round-trip audio delay"), + WEBRTC_UPLOAD_BANDWIDTH("webrtc_upload_bandwidth", "Estimated upload bandwidth"), + WEBRTC_JITTER_BUFFER("webrtc_jitter_buffer", "Average delay added to smooth incoming audio"); + } LLViewerStats::LLViewerStats() diff --git a/indra/newview/llviewerstats.h b/indra/newview/llviewerstats.h index 011269d7ee6..92e15bb74b1 100644 --- a/indra/newview/llviewerstats.h +++ b/indra/newview/llviewerstats.h @@ -229,6 +229,8 @@ extern LLTrace::EventStatHandle AVATAR_EDIT_TIME, extern LLTrace::EventStatHandle > OBJECT_CACHE_HIT_RATE; +extern LLTrace::SampleStatHandle WEBRTC_PACKETS_IN_LOST, WEBRTC_PACKETS_IN_RECEIVED, WEBRTC_PACKETS_OUT_SENT, WEBRTC_PACKETS_OUT_LOST; +extern LLTrace::SampleStatHandle WEBRTC_JITTER_OUT, WEBRTC_JITTER_IN, WEBRTC_LATENCY, WEBRTC_UPLOAD_BANDWIDTH, WEBRTC_JITTER_BUFFER; } class LLViewerStats : public LLSingleton diff --git a/indra/newview/llvoicewebrtc.cpp b/indra/newview/llvoicewebrtc.cpp index e4fdf85d12d..5c13b849d74 100644 --- a/indra/newview/llvoicewebrtc.cpp +++ b/indra/newview/llvoicewebrtc.cpp @@ -61,6 +61,7 @@ #include "llrand.h" #include "llviewerwindow.h" #include "llviewercamera.h" +#include "llviewerstats.h" #include "llversioninfo.h" #include "llviewernetwork.h" @@ -81,6 +82,8 @@ const std::string WEBRTC_VOICE_SERVER_TYPE = "webrtc"; +const F32 STATS_TIMER_DELAY = 2.0; + namespace { const F32 MAX_AUDIO_DIST = 50.0f; @@ -2905,6 +2908,7 @@ bool LLVoiceWebRTCConnection::connectionStateMachine() } mWebRTCAudioInterface->setReceiveVolume(mSpeakerVolume); LLWebRTCVoiceClient::getInstance()->OnConnectionEstablished(mChannelID, mRegionID); + resetConnectionStats(); setVoiceConnectionState(VOICE_STATE_WAIT_FOR_DATA_CHANNEL); break; } @@ -2954,6 +2958,13 @@ bool LLVoiceWebRTCConnection::connectionStateMachine() sendJoin(); } } + + static LLTimer stats_timer; + if (stats_timer.getElapsedTimeF32() > STATS_TIMER_DELAY) + { + mWebRTCPeerConnectionInterface->gatherConnectionStats(); + stats_timer.reset(); + } } break; } @@ -3288,6 +3299,112 @@ void LLVoiceWebRTCConnection::sendJoin() mWebRTCDataInterface->sendData(json_data, false); } +void LLVoiceWebRTCConnection::OnStatsDelivered(const llwebrtc::LLWebRTCStatsMap& stats_data) +{ + LL::WorkQueue::postMaybe(mMainQueue, [=, this] + { + if (mShutDown) + { + return; + } + for (const auto& [stats_id, attributes] : stats_data) + { + if (attributes.contains("currentRoundTripTime")) + { + F32 rtt_seconds = 0.0f; + LLStringUtil::convertToF32(attributes.at("currentRoundTripTime"), rtt_seconds); + sample(LLStatViewer::WEBRTC_LATENCY, rtt_seconds * 1000.0f); + } + if (attributes.contains("availableOutgoingBitrate")) + { + F32 bitrate_bps = 0.0f; + LLStringUtil::convertToF32(attributes.at("availableOutgoingBitrate"), bitrate_bps); + sample(LLStatViewer::WEBRTC_UPLOAD_BANDWIDTH, bitrate_bps / 1000.0f); + } + + // Stat type detection below is heuristic-based. + // It's relied on specific fields to distinguish outbound-rtp, remote-inbound-rtp, and inbound-rtp. + // This approach works with current WebRTC stats but may need updating later. + + // Outbound RTP + if (attributes.contains("mediaSourceId")) + { + U32 out_packets_sent = 0; + LLStringUtil::convertToU32(attributes.at("packetsSent"), out_packets_sent); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_SENT, out_packets_sent); + } + // Remote-Inbound RTP + else if (attributes.contains("localId")) + { + if (attributes.contains("packetsLost")) + { + U32 out_packets_lost = 0; + LLStringUtil::convertToU32(attributes.at("packetsLost"), out_packets_lost); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_LOST, out_packets_lost); + } + if (attributes.contains("jitter")) + { + F32 jitter_seconds = 0.0f; + LLStringUtil::convertToF32(attributes.at("jitter"), jitter_seconds); + sample(LLStatViewer::WEBRTC_JITTER_OUT, jitter_seconds * 1000.0f); + } + } + // Inbound RTP + else if (attributes.contains("jitterBufferDelay")) + { + if (attributes.contains("packetsLost")) + { + U32 in_packets_lost = 0; + LLStringUtil::convertToU32(attributes.at("packetsLost"), in_packets_lost); + sample(LLStatViewer::WEBRTC_PACKETS_IN_LOST, in_packets_lost); + } + if (attributes.contains("packetsReceived")) + { + U32 in_packets_recv = 0; + LLStringUtil::convertToU32(attributes.at("packetsReceived"), in_packets_recv); + sample(LLStatViewer::WEBRTC_PACKETS_IN_RECEIVED, in_packets_recv); + } + if (attributes.contains("jitter")) + { + F32 jitter_seconds = 0.0f; + LLStringUtil::convertToF32(attributes.at("jitter"), jitter_seconds); + sample(LLStatViewer::WEBRTC_JITTER_IN, jitter_seconds * 1000.0f); + } + if (attributes.contains("jitterBufferDelay") && attributes.contains("jitterBufferEmittedCount")) + { + F32 total_delay_seconds = 0.0f; + F32 emitted_count_f = 0.0f; + + // total delay in seconds + LLStringUtil::convertToF32(attributes.at("jitterBufferDelay"), total_delay_seconds); + + // number of packets played out + LLStringUtil::convertToF32(attributes.at("jitterBufferEmittedCount"), emitted_count_f); + if (emitted_count_f > 0.0f) + { + F32 avg_delay_seconds = total_delay_seconds / emitted_count_f; + F32 avg_delay_ms = avg_delay_seconds * 1000.0f; + sample(LLStatViewer::WEBRTC_JITTER_BUFFER, avg_delay_seconds * 1000.0f); + } + } + } + } + }); +} + +void LLVoiceWebRTCConnection::resetConnectionStats() +{ + sample(LLStatViewer::WEBRTC_JITTER_BUFFER, 0); + sample(LLStatViewer::WEBRTC_JITTER_IN, 0); + sample(LLStatViewer::WEBRTC_JITTER_OUT, 0); + sample(LLStatViewer::WEBRTC_LATENCY, 0); + sample(LLStatViewer::WEBRTC_PACKETS_IN_LOST, 0); + sample(LLStatViewer::WEBRTC_PACKETS_IN_RECEIVED, 0); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_SENT, 0); + sample(LLStatViewer::WEBRTC_PACKETS_OUT_LOST, 0); + sample(LLStatViewer::WEBRTC_UPLOAD_BANDWIDTH, 0); +} + ///////////////////////////// // WebRTC Spatial Connection diff --git a/indra/newview/llvoicewebrtc.h b/indra/newview/llvoicewebrtc.h index 2ce575852ab..6786b049c2d 100644 --- a/indra/newview/llvoicewebrtc.h +++ b/indra/newview/llvoicewebrtc.h @@ -540,6 +540,8 @@ class LLWebRTCVoiceClient : public LLSingleton, static bool sShuttingDown; LLEventMailDrop mWebRTCPump; + + LLSD mLastWebRTCStats; }; @@ -603,6 +605,8 @@ class LLVoiceWebRTCConnection : //@{ void OnDataReceived(const std::string &data, bool binary) override; void OnDataChannelReady(llwebrtc::LLWebRTCDataInterface *data_interface) override; + + void OnStatsDelivered(const llwebrtc::LLWebRTCStatsMap& stats_data) override; //@} void OnDataReceivedImpl(const std::string &data, bool binary); @@ -638,6 +642,8 @@ class LLVoiceWebRTCConnection : void OnVoiceConnectionRequestSuccess(const LLSD &body); + void resetConnectionStats(); + protected: typedef enum e_voice_connection_state { diff --git a/indra/newview/skins/default/xui/en/floater_stats.xml b/indra/newview/skins/default/xui/en/floater_stats.xml index 1600c422c37..191db4f854c 100644 --- a/indra/newview/skins/default/xui/en/floater_stats.xml +++ b/indra/newview/skins/default/xui/en/floater_stats.xml @@ -418,6 +418,64 @@ + + + + + + + + + + + + + + + From ec149b515c26cec64d1666df5aeb441ea052cf19 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Wed, 26 Nov 2025 19:44:20 +0200 Subject: [PATCH 12/14] #5018 mac build fix --- indra/llwebrtc/llwebrtc_impl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index 2a00e066bde..c1e909df72b 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -648,7 +648,7 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, void enableSenderTracks(bool enable); void enableReceiverTracks(bool enable); - void gatherConnectionStats(); + void gatherConnectionStats() override; protected: From a9e8676611bf0ffda077a6d7561e0d57f9e3fcf7 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 27 Nov 2025 20:51:35 +0200 Subject: [PATCH 13/14] #5055 don't show moderate menu if the user is not parcel owner within a parcel with restricted voice --- indra/newview/llnearbyvoicemoderation.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp index d3e540e838f..668365d2661 100644 --- a/indra/newview/llnearbyvoicemoderation.cpp +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -194,7 +194,19 @@ bool LLNearbyVoiceModeration::showMutedNotification(bool is_muted) bool LLNearbyVoiceModeration::isNearbyChatModerator() { - return gAgent.getRegion() && gAgent.getRegion()->isRegionWebRTCEnabled() && - (gAgent.canManageEstate() || LLViewerParcelMgr::getInstance()->allowVoiceModeration()); -} + // Region doesn't support WebRTC voice + if (!gAgent.getRegion() || !gAgent.getRegion()->isRegionWebRTCEnabled()) + { + return false; + } + if (LLViewerParcelMgr::getInstance()->isVoiceRestricted()) + { + // Only the parcel owner should have access to moderate parcel voice space + return LLViewerParcelMgr::getInstance()->allowVoiceModeration(); + } + else + { + return gAgent.canManageEstate(); + } +} From 88a3d951fd15863bfd77b9b7137e9c02c21a4a10 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Tue, 2 Dec 2025 19:43:01 +0200 Subject: [PATCH 14/14] #5088 Hide 'Moderation options' menu when disconnected from spatial voice --- indra/newview/llnearbyvoicemoderation.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/indra/newview/llnearbyvoicemoderation.cpp b/indra/newview/llnearbyvoicemoderation.cpp index 668365d2661..a5ee0e9423a 100644 --- a/indra/newview/llnearbyvoicemoderation.cpp +++ b/indra/newview/llnearbyvoicemoderation.cpp @@ -29,6 +29,7 @@ #include "llnotificationsutil.h" #include "llviewerregion.h" #include "llvoavatar.h" +#include "llvoicechannel.h" #include "llvoiceclient.h" #include "llviewerobjectlist.h" #include "llviewerparcelmgr.h" @@ -200,6 +201,13 @@ bool LLNearbyVoiceModeration::isNearbyChatModerator() return false; } + // Only show moderator options when connected to spatial voice chat + LLVoiceChannel* channel = LLVoiceChannel::getCurrentVoiceChannel(); + if (!channel || channel->getSessionID().notNull() || !LLAgent::isActionAllowed("speak")) + { + return false; + } + if (LLViewerParcelMgr::getInstance()->isVoiceRestricted()) { // Only the parcel owner should have access to moderate parcel voice space