From 0a14318dd6cb9e2eae142956126cfd000c3027d9 Mon Sep 17 00:00:00 2001 From: Callum Prentice Date: Wed, 8 Oct 2025 18:58:27 -0700 Subject: 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 ++++++++ .../skins/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 c727d5ae57..239193001c 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 d821d9a4a5..9a016f6286 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 30eed8be36..c91093c107 100644 --- a/indra/newview/llfloaterimcontainer.h +++ b/indra/newview/llfloaterimcontainer.h @@ -178,6 +178,8 @@ private: 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 0000000000..d714fc36b4 --- /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 0000000000..4275754dbf --- /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 62cdaa5886..cf9b8959ce 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 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - -- cgit v1.3 From 219da2a16b3ae6e1d2e02b05159b37fa1aca1f47 Mon Sep 17 00:00:00 2001 From: Callum Prentice Date: Thu, 9 Oct 2025 10:44:32 -0700 Subject: 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 cf9b8959ce..5a28f0dde5 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: 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 9a016f6286..2b9fb9dd37 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 d714fc36b4..5ae8feba08 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 4275754dbf..8a1ca5af6b 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); }; -- cgit v1.3 From cf048cf9c3ebd9ca8ee1e606b69bc459b62ed1c5 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 30 Oct 2025 16:59:51 +0200 Subject: #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 +- .../newview/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 c25fbdcc51..ac8234bf24 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 c91093c107..9f1690a9b9 100644 --- a/indra/newview/llfloaterimcontainer.h +++ b/indra/newview/llfloaterimcontainer.h @@ -179,7 +179,6 @@ private: 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 5ae8feba08..ec0e1c0431 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 8a1ca5af6b..21d185db54 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 a085bc4d91..fa60c36834 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 71a9e71a9f..5a2ef248ba 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 93f8f0d14a..1d78ca002a 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 dbd513afe8..3f9a2ba13e 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: #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 ec0e1c0431..2b4fd26afc 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 21d185db54..95bae9ca2a 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 432da2e990..452c666652 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 1925cd23ed..8863bc7a06 100644 --- a/indra/newview/llviewerparcelmgr.h +++ b/indra/newview/llviewerparcelmgr.h @@ -173,6 +173,12 @@ public: 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; -- cgit v1.3 From a4d01edef6288528647968ea8d97fee8a553891d Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Tue, 11 Nov 2025 23:34:05 +0200 Subject: 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 2b4fd26afc..10f92502a5 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 fa60c36834..98440d96ae 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 244e2b7835..bde0fa13ce 100644 --- a/indra/newview/llviewerregion.h +++ b/indra/newview/llviewerregion.h @@ -424,6 +424,8 @@ public: 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 c57557079e..be2e840f63 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(); -- cgit v1.3 From d9ec89ac0a0ae2e44f00f524678d18cf66c52b13 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Wed, 12 Nov 2025 20:24:42 +0200 Subject: 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 be2e840f63..e4fdf85d12 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. -- cgit v1.3 From 811105270d2cede0a99efbb501792d35ef61a530 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 13 Nov 2025 18:48:24 +0200 Subject: #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 10f92502a5..5a6d7e1c6b 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; -- cgit v1.3 From bee23b4956b424f99765fb099906d3cb8250ce63 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 13 Nov 2025 19:02:49 +0200 Subject: #4995 change muted/unmuted alerts to non-modal toast --- indra/newview/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 3f9a2ba13e..14e9211b9c 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: 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 ac8234bf24..00b9c0b052 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 5a6d7e1c6b..d3e540e838 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 95bae9ca2a..619f169883 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 5a2ef248ba..2d732d0a71 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) { -- cgit v1.3 From 6ee41d60753d31d6826994731570f0ab64b92bd9 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Wed, 26 Nov 2025 18:41:30 +0200 Subject: #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 ++ .../newview/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 b9f126e511..62eadecd6f 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 7d06b7d2b4..e76e708f0c 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 01cfb17ced..2a00e066bd 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 aca9910253..eac6893504 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 d39d466205..db6d83db5f 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 011269d7ee..92e15bb74b 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 e4fdf85d12..5c13b849d7 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 2ce575852a..6786b049c2 100644 --- a/indra/newview/llvoicewebrtc.h +++ b/indra/newview/llvoicewebrtc.h @@ -540,6 +540,8 @@ private: 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 1600c422c3..191db4f854 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 @@ + + + + + + + + + + + + + + + -- cgit v1.3 From ec149b515c26cec64d1666df5aeb441ea052cf19 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Wed, 26 Nov 2025 19:44:20 +0200 Subject: #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 2a00e066bd..c1e909df72 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: -- cgit v1.3 From a9e8676611bf0ffda077a6d7561e0d57f9e3fcf7 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Thu, 27 Nov 2025 20:51:35 +0200 Subject: #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 d3e540e838..668365d266 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(); + } +} -- cgit v1.3 From 88a3d951fd15863bfd77b9b7137e9c02c21a4a10 Mon Sep 17 00:00:00 2001 From: Mnikolenko Productengine Date: Tue, 2 Dec 2025 19:43:01 +0200 Subject: #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 668365d266..a5ee0e9423 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 -- cgit v1.3 From 17e72f98aba92e8aa690d835f1532d54c526d6d9 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 2 Dec 2025 14:57:12 -0800 Subject: Handle new privileged land permission. Allow caution dialog to contain multiple messages. --- indra/newview/llfloaternotificationsconsole.cpp | 2 +- indra/newview/llscriptruntimeperms.h | 13 +++++-------- indra/newview/llviewermessage.cpp | 17 ++++++++++++++--- indra/newview/skins/default/xui/en/notifications.xml | 3 ++- .../newview/skins/default/xui/en/panel_notification.xml | 8 ++++---- indra/newview/skins/default/xui/en/strings.xml | 3 +++ 6 files changed, 29 insertions(+), 17 deletions(-) diff --git a/indra/newview/llfloaternotificationsconsole.cpp b/indra/newview/llfloaternotificationsconsole.cpp index a819b30e30..d3ec79cb0d 100644 --- a/indra/newview/llfloaternotificationsconsole.cpp +++ b/indra/newview/llfloaternotificationsconsole.cpp @@ -34,7 +34,7 @@ #include "llpanel.h" #include "llcombobox.h" -const S32 NOTIFICATION_PANEL_HEADER_HEIGHT = 20; +const S32 NOTIFICATION_PANEL_HEADER_HEIGHT = 40; const S32 HEADER_PADDING = 38; class LLNotificationChannelPanel : public LLLayoutPanel diff --git a/indra/newview/llscriptruntimeperms.h b/indra/newview/llscriptruntimeperms.h index 300304c21b..6350b61d08 100644 --- a/indra/newview/llscriptruntimeperms.h +++ b/indra/newview/llscriptruntimeperms.h @@ -24,10 +24,7 @@ * $/LicenseInfo$ */ -#ifndef LL_LLSCRIPTRUNTIME_PERMS_H -#define LL_LLSCRIPTRUNTIME_PERMS_H - -#include +#pragma once typedef struct _script_perm { std::string question; @@ -37,12 +34,12 @@ typedef struct _script_perm { question(q), permbit(b), caution(c) {} } script_perm_t; -const U32 NUM_SCRIPT_PERMISSIONS = 18; +const U32 NUM_SCRIPT_PERMISSIONS = 19; const S32 SCRIPT_PERMISSION_DEBIT = 0; const S32 SCRIPT_PERMISSION_TRIGGER_ANIMATION = 3; const S32 SCRIPT_PERMISSION_OVERRIDE_ANIMATIONS = 14; -static const boost::array SCRIPT_PERMISSIONS = {{ +static const std::array SCRIPT_PERMISSIONS = {{ _script_perm("ScriptTakeMoney", (0x1 << 1), true), _script_perm("ActOnControlInputs", (0x1 << 2), false), _script_perm("RemapControlInputs", (0x1 << 3), false), @@ -60,7 +57,7 @@ static const boost::array SCRIPT_PERMISSI _script_perm("OverrideYourAnimations", (0x1 << 15), false), _script_perm("ScriptReturnObjects", (0x1 << 16), false), _script_perm("ForceSitAvatar", (0x1 << 17), false), - _script_perm("ChangeEnvSettings", (0x1 << 18), false) + _script_perm("ChangeEnvSettings", (0x1 << 18), false), + _script_perm("PrivilegedLandAccess",(0x1 << 19), true) } }; -#endif // LL_LLSCRIPTRUNTIME_PERMS_H diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 36e8e27800..731e2b68bb 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -5749,7 +5749,9 @@ void process_script_question(LLMessageSystem *msg, void **user_data) args["NAME"] = clean_owner_name; S32 known_questions = 0; bool has_not_only_debit = questions ^ SCRIPT_PERMISSIONS[SCRIPT_PERMISSION_DEBIT].permbit; + bool caution_enabled = gSavedSettings.getBOOL("PermissionsCautionEnabled"); // check the received permission flags against each permission + std::string warning_msg; for (const script_perm_t& script_perm : SCRIPT_PERMISSIONS) { if (questions & script_perm.permbit) @@ -5759,8 +5761,11 @@ void process_script_question(LLMessageSystem *msg, void **user_data) // check whether permission question should cause special caution dialog caution |= (script_perm.caution); - if (("ScriptTakeMoney" == script_perm.question) && has_not_only_debit) + if (caution_enabled && script_perm.caution) + { + warning_msg += "\n" + LLTrans::getString(script_perm.question + "Caution") + "\n"; continue; + } if (LLTrans::getString(script_perm.question).empty()) { @@ -5771,6 +5776,12 @@ void process_script_question(LLMessageSystem *msg, void **user_data) } } + if (!warning_msg.empty()) + { + LLStringUtil::format(warning_msg, args); + args["WARNINGS"] = warning_msg; + } + args["QUESTIONS"] = script_question; if (known_questions != questions) @@ -5795,12 +5806,12 @@ void process_script_question(LLMessageSystem *msg, void **user_data) // check whether cautions are even enabled or not const char* notification = "ScriptQuestion"; - if(caution && gSavedSettings.getBOOL("PermissionsCautionEnabled")) + if (caution && caution_enabled) { args["FOOTERTEXT"] = (count > 1) ? LLTrans::getString("AdditionalPermissionsRequestHeader") + "\n\n" + script_question : ""; notification = "ScriptQuestionCaution"; } - else if(experienceid.notNull()) + else if (experienceid.notNull()) { payload["experience"]=experienceid; LLExperienceCache::instance().get(experienceid, boost::bind(process_script_experience_details, _1, args, payload)); diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index dbd513afe8..5088426502 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -8585,7 +8585,8 @@ Scripts associated with this experience will be able to do the following on regi priority="critical" persist="true" type="notify"> -Warning: The object '<nolink>[OBJECTNAME]</nolink>' wants total access to your Linden Dollars account. If you allow access, it can remove funds from your account at any time, or empty your account completely, on an ongoing basis with no additional warnings. +Warning: +[WARNINGS] Do not allow access if you do not fully understand why it wants access to your account. diff --git a/indra/newview/skins/default/xui/en/panel_notification.xml b/indra/newview/skins/default/xui/en/panel_notification.xml index bfe738f472..eaf638d35f 100644 --- a/indra/newview/skins/default/xui/en/panel_notification.xml +++ b/indra/newview/skins/default/xui/en/panel_notification.xml @@ -49,7 +49,7 @@ embedded_items="false" enabled="false" follows="left|right|top|bottom" - height="85" + height="85" layout="topleft" left="10" mouse_opaque="false" @@ -67,16 +67,16 @@ - diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index 8e0eea97d1..bcda160f21 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -518,6 +518,8 @@ http://secondlife.com/support for help fixing this problem. You will now be reconnected to Nearby Voice Chat '[OBJECTNAME]', an object owned by '[OWNERNAME]', located in [REGIONNAME] at [REGIONPOS], has been granted permission to: [PERMISSIONS]. '[OBJECTNAME]', an object owned by '[OWNERNAME]', located in [REGIONNAME] at [REGIONPOS], has been denied permission to: [PERMISSIONS]. + The object '<nolink>[OBJECTNAME]</nolink>' wants total access to your Linden Dollars account. If you allow access, it can remove funds from your account at any time, or empty your account completely, on an ongoing basis with no additional warnings. + The object '<nolink>[OBJECTNAME]</nolink>' wants privileged access to your land. If you allow access it may sell your land with no additional warnings. If you allow access to your account, you will also be allowing the object to: Take Linden dollars (L$) from you Act on your control inputs @@ -535,6 +537,7 @@ http://secondlife.com/support for help fixing this problem. Change your default animations Force your avatar to sit Change your environment settings + Sell land on your behalf. Not Connected (You) -- cgit v1.3 From 20ddc736730a9f9c62ef71a808e33ed5587ae19e Mon Sep 17 00:00:00 2001 From: Brad Linden Date: Wed, 3 Dec 2025 12:58:43 -0800 Subject: update CLA exceptions allowlist --- .github/workflows/cla.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml index 5b31c584d5..800f3c42d1 100644 --- a/.github/workflows/cla.yaml +++ b/.github/workflows/cla.yaml @@ -23,4 +23,4 @@ jobs: path-to-signatures: signatures.json remote-organization-name: secondlife remote-repository-name: cla-signatures - allowlist: callum@mbp.localdomain,rye@lindenlab.com,rye,bot* + allowlist: callum@mbp.localdomain,rye@lindenlab.com,rye,signal@lindenlab.com,dependabot*,bot* -- cgit v1.3 From 63ad735fef0dfca0c0e7daab142b5fb8343fc55f Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:24:42 +0200 Subject: #4628 New land access permission --- indra/newview/llfloaternotificationsconsole.cpp | 2 +- indra/newview/lltoastscriptquestion.cpp | 73 +++++++++++++++++++--- indra/newview/llviewermessage.cpp | 9 +-- .../newview/skins/default/xui/en/notifications.xml | 9 +-- .../default/xui/en/panel_script_question_toast.xml | 6 +- indra/newview/skins/default/xui/en/strings.xml | 4 +- 6 files changed, 77 insertions(+), 26 deletions(-) diff --git a/indra/newview/llfloaternotificationsconsole.cpp b/indra/newview/llfloaternotificationsconsole.cpp index d3ec79cb0d..a819b30e30 100644 --- a/indra/newview/llfloaternotificationsconsole.cpp +++ b/indra/newview/llfloaternotificationsconsole.cpp @@ -34,7 +34,7 @@ #include "llpanel.h" #include "llcombobox.h" -const S32 NOTIFICATION_PANEL_HEADER_HEIGHT = 40; +const S32 NOTIFICATION_PANEL_HEADER_HEIGHT = 20; const S32 HEADER_PADDING = 38; class LLNotificationChannelPanel : public LLLayoutPanel diff --git a/indra/newview/lltoastscriptquestion.cpp b/indra/newview/lltoastscriptquestion.cpp index 25dc0982b8..5d99efe6a4 100644 --- a/indra/newview/lltoastscriptquestion.cpp +++ b/indra/newview/lltoastscriptquestion.cpp @@ -48,7 +48,12 @@ bool LLToastScriptQuestion::postBuild() LLTextBox* mFooter = getChild("bottom_info_message"); mMessage->setValue(mNotification->getMessage()); - mFooter->setValue(mNotification->getFooter()); + std::string footer = mNotification->getFooter(); + mFooter->setValue(footer); + if (footer.empty()) + { + mFooter->setVisible(false); + } snapToMessageHeight(); @@ -78,21 +83,69 @@ void LLToastScriptQuestion::snapToMessageHeight() if (mMessage->getVisible() && mFooter->getVisible()) { - S32 heightDelta = 0; - S32 maxTextHeight = (mMessage->getFont()->getLineHeight() * MAX_LINES_COUNT) + S32 height_delta = 0; + S32 max_text_height = (mMessage->getFont()->getLineHeight() * MAX_LINES_COUNT) + (mFooter->getFont()->getLineHeight() * MAX_LINES_COUNT); - LLRect messageRect = mMessage->getRect(); - LLRect footerRect = mFooter->getRect(); + LLRect message_rect = mMessage->getRect(); + + S32 old_message_height = message_rect.getHeight(); + S32 new_message_height = mMessage->getTextBoundingRect().getHeight(); + S32 new_footer_height = mFooter->getTextBoundingRect().getHeight(); + + constexpr S32 FOOTER_PADDING = 8; // new height should include padding for newly added footer + S32 required_text_height = new_message_height + new_footer_height + FOOTER_PADDING; + S32 new_text_height = llmin(required_text_height, max_text_height); + + // Footer was invisibe, so use old_message_height for old height + height_delta = new_text_height - old_message_height; + + reshape( getRect().getWidth(), llmax(getRect().getHeight() + height_delta, MIN_PANEL_HEIGHT)); + + // Floater was resized, now resize and shift children + // Message follows top, so it's top is in a correct position, but needs to be resized down + S32 message_delta = new_message_height - old_message_height; + message_rect = mMessage->getRect(); // refresh since it might have changed after reshape + message_rect.mBottom = message_rect.mBottom - message_delta; + mMessage->setRect(message_rect); + mMessage->needsReflow(); + // Button panel should stay the same size, just translate it + LLPanel* panel = getChild("buttons_panel"); + panel->translate(0, -message_delta); + // Footer should be both moved and resized + LLRect footer_rect = mFooter->getRect(); + footer_rect.mTop = footer_rect.mTop - message_delta; + footer_rect.mBottom = footer_rect.mTop - new_footer_height; + mFooter->setRect(footer_rect); + mFooter->needsReflow(); + } + else if (mMessage->getVisible()) + { + S32 height_delta = 0; + S32 max_text_height = (mMessage->getFont()->getLineHeight() * MAX_LINES_COUNT); + + LLRect message_rect = mMessage->getRect(); + + S32 old_message_height = message_rect.getHeight(); + S32 new_message_height = mMessage->getTextBoundingRect().getHeight(); - S32 oldTextHeight = messageRect.getHeight() + footerRect.getHeight(); + S32 new_text_height = llmin(new_message_height, max_text_height); - S32 requiredTextHeight = mMessage->getTextBoundingRect().getHeight() + mFooter->getTextBoundingRect().getHeight(); - S32 newTextHeight = llmin(requiredTextHeight, maxTextHeight); + // Footer was invisibe, so use old_message_height for old height + height_delta = new_text_height - old_message_height; - heightDelta = newTextHeight - oldTextHeight - heightDelta; + reshape(getRect().getWidth(), llmax(getRect().getHeight() + height_delta, MIN_PANEL_HEIGHT)); - reshape( getRect().getWidth(), llmax(getRect().getHeight() + heightDelta, MIN_PANEL_HEIGHT)); + // Floater was resized, now resize and shift children + // Message follows top, so it's top is in a correct position, but needs to be resized down + S32 message_delta = new_message_height - old_message_height; + message_rect = mMessage->getRect(); // refresh since it might have changed after reshape + message_rect.mBottom = message_rect.mBottom - message_delta; + mMessage->setRect(message_rect); + mMessage->needsReflow(); + // Button panel should stay the same size, just translate it + LLPanel* panel = getChild("buttons_panel"); + panel->translate(0, -message_delta); } } diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 731e2b68bb..6f2b00a862 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -5756,11 +5756,11 @@ void process_script_question(LLMessageSystem *msg, void **user_data) { if (questions & script_perm.permbit) { - count++; known_questions |= script_perm.permbit; // check whether permission question should cause special caution dialog caution |= (script_perm.caution); + // Caustions go into top part of the dialog, questions go into the footer if (caution_enabled && script_perm.caution) { warning_msg += "\n" + LLTrans::getString(script_perm.question + "Caution") + "\n"; @@ -5772,7 +5772,8 @@ void process_script_question(LLMessageSystem *msg, void **user_data) continue; } - script_question += " " + LLTrans::getString(script_perm.question) + "\n"; + count++; + script_question += "\n " + LLTrans::getString(script_perm.question); } } @@ -5806,9 +5807,9 @@ void process_script_question(LLMessageSystem *msg, void **user_data) // check whether cautions are even enabled or not const char* notification = "ScriptQuestion"; - if (caution && caution_enabled) + if(caution && caution_enabled) { - args["FOOTERTEXT"] = (count > 1) ? LLTrans::getString("AdditionalPermissionsRequestHeader") + "\n\n" + script_question : ""; + args["FOOTERTEXT"] = (count > 0) ? LLTrans::getString("AdditionalPermissionsRequestHeader") + "\n" + script_question : ""; notification = "ScriptQuestionCaution"; } else if (experienceid.notNull()) diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index 5088426502..46c1d3abe9 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -8585,11 +8585,10 @@ Scripts associated with this experience will be able to do the following on regi priority="critical" persist="true" type="notify"> -Warning: +Warning: [WARNINGS] - -Do not allow access if you do not fully understand why it wants access to your account. +Do not allow access if you do not fully understand why it wants access to your account. confirm
- - - [[URL] Subscribe Now] - - - - - - - diff --git a/indra/newview/skins/default/xui/en/panel_voice_effect.xml b/indra/newview/skins/default/xui/en/panel_voice_effect.xml deleted file mode 100644 index 42cd510efd..0000000000 --- a/indra/newview/skins/default/xui/en/panel_voice_effect.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - Voice Morphing Off - - - Preview Voice Morphing ▶ - - - Get Voice Morphing ▶ - - - - - - diff --git a/indra/newview/skins/default/xui/es/floater_big_preview.xml b/indra/newview/skins/default/xui/es/floater_big_preview.xml deleted file mode 100644 index b112243d7a..0000000000 --- a/indra/newview/skins/default/xui/es/floater_big_preview.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/indra/newview/skins/default/xui/es/floater_post_process.xml b/indra/newview/skins/default/xui/es/floater_post_process.xml deleted file mode 100644 index 5c62ccde36..0000000000 --- a/indra/newview/skins/default/xui/es/floater_post_process.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - Brillo - - - Saturación - - - Contraste - - - Color base del contraste - - - - - - - - - - Amplificación de luz - - - Cantidad de ruido - - - Intensidad del ruido - - - - - - Extracción de la luminosidad - - - Bloom: cantidad - - - Bloom: intensidad - - - -