Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0a14318
First commit for a second experiment, investigating how nearby voice/…
callumlinden Oct 9, 2025
219da2a
Remove separator bar for Nearby Voice
callumlinden Oct 9, 2025
53d8310
Improve robustness of when moderator options appear and add some init…
callumlinden Oct 13, 2025
c64c16a
Merge branch 'develop' of https://github.com/secondlife/viewer into c…
callumlinden Oct 20, 2025
21e9b38
Merge branch 'develop' of https://github.com/secondlife/viewer into c…
callumlinden Oct 23, 2025
cf048cf
#4013 Update voice moderator options; show notifications when muted; …
maxim-productengine Oct 30, 2025
c39135c
#4013 add simple voice moderation permission check
maxim-productengine Nov 7, 2025
74a64d2
Merge branch 'develop' into maxim/voice-moderation
maxim-productengine Nov 7, 2025
a4d01ed
Show moderator options only on webrtc region
maxim-productengine Nov 11, 2025
d9ec89a
Ignore muted flags from non-primary voice server
maxim-productengine Nov 12, 2025
8111052
#4994 remove redundant moderator_id key
maxim-productengine Nov 13, 2025
bee23b4
#4995 change muted/unmuted alerts to non-modal toast
maxim-productengine Nov 13, 2025
e740bd2
Toggle off 'Speak' button when muted by moderator
maxim-productengine Nov 14, 2025
6ee41d6
#5018 add webrtc connection statistics
maxim-productengine Nov 26, 2025
ec149b5
#5018 mac build fix
maxim-productengine Nov 26, 2025
a9e8676
#5055 don't show moderate menu if the user is not parcel owner within…
maxim-productengine Nov 27, 2025
1565e46
Merge branch 'develop' into maxim/voice-moderation
maxim-productengine Dec 1, 2025
8b1e44e
Merge branch 'develop' into maxim/voice-moderation
maxim-productengine Dec 2, 2025
88a3d95
#5088 Hide 'Moderation options' menu when disconnected from spatial v…
maxim-productengine Dec 2, 2025
3647e95
Merge pull request #5096 from secondlife/develop
Geenz Dec 3, 2025
c4ec3d8
Merge pull request #5100 from secondlife/develop
Geenz Dec 3, 2025
9f82b90
#3612 "Copy SLURL" from Favorites bar not working
akleshchev Dec 5, 2025
4f22c12
#5109 LLExperienceCache crashes on a coroutine
akleshchev Dec 5, 2025
3fd68bc
#4931 Fix missed name cache connection #2
akleshchev Dec 5, 2025
49c73ac
#3612 "Copy SLURL" from Favorites bar not working #2
akleshchev Dec 8, 2025
cbe606d
Merge branch 'release/2026.01' into maxim/voice-moderation
maxim-productengine Dec 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions indra/newview/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ set(viewer_SOURCE_FILES
llfloaterimnearbychat.cpp
llfloaterimnearbychathandler.cpp
llfloaterimnearbychatlistener.cpp
llnearbyvoicemoderation.cpp
llnetmap.cpp
llnotificationalerthandler.cpp
llnotificationgrouphandler.cpp
Expand Down Expand Up @@ -1103,6 +1104,7 @@ set(viewer_HEADER_FILES
llnameeditor.h
llnamelistctrl.h
llnavigationbar.h
llnearbyvoicemoderation.h
llnetmap.h
llnotificationhandler.h
llnotificationlistitem.h
Expand Down
101 changes: 98 additions & 3 deletions indra/newview/llfloaterimcontainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,6 +92,7 @@ LLFloaterIMContainer::LLFloaterIMContainer(const LLSD& seed, const Params& param

mAutoResize = false;
LLTransientFloaterMgr::getInstance()->addControlView(LLTransientFloaterMgr::IM, this);
LLNearbyVoiceModeration::getInstance();
}

LLFloaterIMContainer::~LLFloaterIMContainer()
Expand Down Expand Up @@ -530,6 +533,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<LLConversationItemParticipant*>((*current_participant_model).get());
if (participant_model)
{
participant_model->setModeratorOptionsVisible(LLNearbyVoiceModeration::getInstance()->isNearbyChatModerator());
}

current_participant_model++;
}
}
}

mParticipantRefreshTimer.setTimerExpirySec(1.0f);
Expand Down Expand Up @@ -1685,6 +1705,10 @@ bool LLFloaterIMContainer::visibleContextMenuItem(const LLSD& userdata)
{
return isMuted(conversation_item->getUUID());
}
else if ("can_allow_text_chat" == item)
{
return !isNearbyChatSpeakerSelected();
}

return true;
}
Expand Down Expand Up @@ -2014,9 +2038,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 (LLNearbyVoiceModeration::getInstance()->isNearbyChatModerator() && isNearbyChatSpeakerSelected())
{
// Determine here which actions are allowed
if ("can_moderate_voice" == userdata)
{
return true;
}
else if (("can_mute" == userdata))
{
return !is_self;
}
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;
}

Expand Down Expand Up @@ -2149,7 +2191,35 @@ 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)
{
// Request a mute/unmute using a capability request via the simulator
LLNearbyVoiceModeration::getInstance()->requestMuteIndividual(userID, !isMuted(userID));
}
else
if ("mute_all" == command)
{
// Send the mute_all request to the server
const bool mute_state = true;
LLNearbyVoiceModeration::getInstance()->requestMuteAll(mute_state);
}
else
if ("unmute_all" == command)
{
// Send the unmute_all request to the server
const bool mute_state = false;
LLNearbyVoiceModeration::getInstance()->requestMuteAll(mute_state);
}

return;
}

if (command.compare("selected"))
{
Expand Down Expand Up @@ -2267,6 +2337,31 @@ LLSpeaker * LLFloaterIMContainer::getSpeakerOfSelectedParticipant(LLSpeakerMgr *
return speaker_managerp->findSpeaker(participant_itemp->getUUID());
}

bool LLFloaterIMContainer::isNearbyChatSpeakerSelected()
{
LLFolderViewItem *selectedItem = mConversationsRoot->getCurSelectedItem();
if (NULL == selectedItem)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has the viewer switched yet to using nullptr (c++-ism) instead of NULL (c-ism)?

Copy link
Contributor Author

@maxim-productengine maxim-productengine Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case it was probably just copy-paste from another place. It's more common just to do if (!selectedItem) check.
Speaking of NULL vs nullptr - there we no switch, but it makes sense to use nullptr at least in the new code.

{
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();
}

void LLFloaterIMContainer::toggleAllowTextChat(const LLUUID& participant_uuid)
{
LLIMSpeakerMgr * speaker_managerp = dynamic_cast<LLIMSpeakerMgr*>(getSpeakerMgrForSelectedParticipant());
Expand Down
1 change: 1 addition & 0 deletions indra/newview/llfloaterimcontainer.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ class LLFloaterIMContainer
void banSelectedMember(const LLUUID& participant_uuid);
void openNearbyChat();
bool isParticipantListExpanded();
bool isNearbyChatSpeakerSelected();

void idleUpdate(); // for convenience (self) from static idle
void idleProcessEvents();
Expand Down
195 changes: 195 additions & 0 deletions indra/newview/llnearbyvoicemoderation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* @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 "llnotificationsutil.h"
#include "llviewerregion.h"
#include "llvoavatar.h"
#include "llvoiceclient.h"
#include "llviewerobjectlist.h"
#include "llviewerparcelmgr.h"
#include "roles_constants.h"

#include "llnearbyvoicemoderation.h"

LLNearbyVoiceModeration::LLNearbyVoiceModeration()
{
}

LLNearbyVoiceModeration::~LLNearbyVoiceModeration()
{
}

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;
}
}

const std::string LLNearbyVoiceModeration::getCapUrlFromRegion(LLViewerRegion* region)
{
if (! region || ! region->capabilitiesReceived())
{
return std::string();
}

std::string url = region->getCapability("SpatialVoiceModerationRequest");
if (url.empty())
{
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)
{
const std::string cap_url = getCapUrlFromRegion(avatar->getRegion());
if (cap_url.length())
{
const std::string operand = mute ? "mute" : "unmute";

LLSD body;
body["operand"] = operand;
body["agent_id"] = agent_id;
body["moderator_id"] = gAgent.getID();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to send the moderator_id: remove it. The server ignores that field and instead grabs the agent_id directly from the connection context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.


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);
}
}
}

void LLNearbyVoiceModeration::requestMuteAll(bool mute)
{
// Use our own avatar to get the region name
LLViewerRegion* region = gAgent.getRegion();

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["moderator_id"] = gAgent.getID();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the moderator_id field.


LL_INFOS() << "For all residents in this region, applying: " << operand << LL_ENDL;

std::string success_msg =
STRINGIZE("Nearby voice for all residents was set to: " << operand);

std::string failure_msg =
STRINGIZE("Unable to set nearby voice for all residents to: " << operand);

LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost(
cap_url,
body,
success_msg,
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");
}
}

bool LLNearbyVoiceModeration::isNearbyChatModerator()
{
return gAgent.getRegion() && gAgent.getRegion()->isRegionWebRTCEnabled() &&
(gAgent.canManageEstate() || LLViewerParcelMgr::getInstance()->allowVoiceModeration());
}

Loading
Loading