Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions indra/newview/llblocklist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ LLBlockList::~LLBlockList()
LLMuteList::getInstance()->removeObserver(this);
}

void LLBlockList::onChange()
{
const U32 current_size = static_cast<U32>(LLMuteList::getInstance()->getMutes().size());
if (current_size == mMuteListSize)
{
return;
}
if (mMuteListSize == 0 && current_size > 0)
{
mShouldAddAll = true;
mActionType = NONE;
setDirty(true);
}
Comment on lines +90 to +93
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

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

The onChange() implementation only handles the case where mMuteListSize transitions from 0 to a positive value. If the mute list size changes in any other way through a batch operation (e.g., from 10 to 15 items when cache loads), the UI will not update because the condition on line 88 will be false. Consider updating mMuteListSize at the end of this method or triggering a refresh whenever current_size != mMuteListSize, not just when transitioning from zero.

Suggested change
mShouldAddAll = true;
mActionType = NONE;
setDirty(true);
}
// Initial population of the mute list: add all existing entries.
mShouldAddAll = true;
mActionType = NONE;
}
// The mute list size has changed; mark the UI as needing refresh
// and update our cached size so future comparisons are correct.
setDirty(true);
mMuteListSize = current_size;

Copilot uses AI. Check for mistakes.
}

void LLBlockList::createList()
{
std::vector<LLMute> mutes = LLMuteList::instance().getMutes();
Expand Down
8 changes: 4 additions & 4 deletions indra/newview/llblocklist.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ class LLBlockList: public LLFlatListViewEx, public LLMuteListObserver
LLBlockList(const Params& p);
virtual ~LLBlockList();

virtual bool handleRightMouseDown(S32 x, S32 y, MASK mask);
bool handleRightMouseDown(S32 x, S32 y, MASK mask) override;
LLToggleableMenu* getContextMenu() const { return mContextMenu.get(); }
LLBlockedListItem* getBlockedItem() const;

virtual void onChange() { }
virtual void onChangeDetailed(const LLMute& );
virtual void draw();
void onChange() override;
void onChangeDetailed(const LLMute& ) override;
void draw() override;

void setNameFilter(const std::string& filter);
void sortByName();
Expand Down
117 changes: 109 additions & 8 deletions indra/newview/llmutelist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,28 @@
#include "llworld.h" //for particle system banning
#include "llimview.h"
#include "llnotifications.h"
#include "llnotificationsutil.h"
#include "llviewercontrol.h"
#include "llviewerobjectlist.h"
#include "lltrans.h"
#include "lleventtimer.h"

namespace
{
constexpr F32 MUTE_LIST_REQUEST_TIMEOUT_SECONDS = 30.f;
constexpr F32 MUTE_LIST_REQUEST_COOLDOWN_SECONDS = 5.f;

void notify_mute_list_cache_used()
{
static bool s_notified = false;
if (s_notified)
{
return;
}
s_notified = true;
LLNotificationsUtil::add("MuteListFallbackCache");
Copy link
Contributor

@akleshchev akleshchev Jan 14, 2026

Choose a reason for hiding this comment

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

I don't think this should be shown to a user. At least in some cases it's a bug that needs to be fixed server side in others viewer should rerequest.

If viewer timeouts, got an empty list or errors getting the list, you probably can do something about it via MuteCRC field. But I agree that if something got wrong we at least should get the data from cache.

Or read cache first (issues with this approach if outdated?), mark as 'no send', apply server response on top...

P.S. Related: #4267

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think this should be shown to a user.

I added a notification because I felt the user impact of potentially missing recent uncached mutes that the server could be aware of that we are not could be highly disruptive to the user. In a perfect world they should never see the notification because the replies to MuteListRequest are expected to arrive, but the user complaints about this I have seen are describing a great level of distress over harassment the mute list should prevent.

The notification will absolutely be removed if it's decided to not be acceptable. I felt it was appropriate at the time given the potential impact of missing mutes.

If viewer timeouts, got an empty list or errors getting the list, you probably can do something about it via MuteCRC field. But I agree that if something got wrong we at least should get the data from cache.

That is my thought, because for some unknown reason these messages are being lost or remain unsent at random for some users.

Or read cache first (issues with this approach if outdated?), mark as 'no send', apply server response on top...

The UpdateMuteListEntry and RemoveMuteListEntry messages both refer to mutes by their ID or name, so as far as I can tell there should be no negative impact on the server mute list if the user moves forward with a session of adding/removing mutes based on their cached copy. The client would just be missing the local record of what is on the server.

What I considered when deciding not to go for the cached data first, then layer on the server once (if?) we get it was the merging logic. If we were to load from cache first and later learn of a newer server version, it is not guaranteed to be reconcilable with what we have in our cache.

For merging the server list with the cached list, the assumptions I would have to make could potentially result in unintended re-mutes or unintended unmutes based on which we determine to be the correct state of a mute that is present vs not or different in one list compared to the other.


I was very anxious to repeatedly request a mute list from the region because I do not want to generate undue load with loops. There are two timeouts/cooldowns in place within my PR

  1. The existing conceptual 30 second timeout, after which for the duration of the session I completely rely on the cached mute list and stop caring about what the server may ultimately come back with to avoid merging or recreating the mute list, and stop any effort to re-request an update
  2. The 5 second cooldown between MuteListRequest dispatch attempts, which can be triggered by region change. This was to prevent asking every region an agent may be passing through for a mute list that they may not stay around long enough to receive.

Copy link
Contributor

@akleshchev akleshchev Jan 14, 2026

Choose a reason for hiding this comment

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

because for some unknown reason these messages are being lost or remain unsent at random for some users.

I will create a server ticket for that. I know at least one case with a repro where server isn't responding yet should.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was very anxious to repeatedly request a mute list from the region because I do not want to generate undue load with loops.

Makes sense. But region change can end up requesting indefinetely either way. Better add some kind of retry limit there.

Copy link
Contributor

Choose a reason for hiding this comment

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

because for some unknown reason these messages are being lost

SendReliable supports callbacks. Might be possible to refine this by detecting send failures to log better and to rerequest on failures.

}

// This method is used to return an object to mute given an object id.
// Its used by the LLMute constructor and LLMuteList::isMuted.
LLViewerObject* get_object_to_mute_from_id(LLUUID object_id)
Expand Down Expand Up @@ -155,7 +171,8 @@ std::string LLMute::getDisplayType() const
//-----------------------------------------------------------------------------
LLMuteList::LLMuteList() :
mLoadState(ML_INITIAL),
mRequestStartTime(0.f)
mRequestStartTime(0.f),
mRequestTimeout(nullptr)
{
gGenericDispatcher.addHandler("emptymutelist", &sDispatchEmptyMuteList);

Expand All @@ -178,6 +195,8 @@ LLMuteList::LLMuteList() :
// but this way is just more convinient
onAccountNameChanged(id, av_name.getUserName());
});

mRegionChangedSlot = gAgent.addRegionChangedCallback([this]() { onRegionChanged(); });
}

//-----------------------------------------------------------------------------
Expand All @@ -191,6 +210,11 @@ LLMuteList::~LLMuteList()
void LLMuteList::cleanupSingleton()
{
LLAvatarNameCache::getInstance()->setAccountNameChangedCallback(nullptr);
if (mRegionChangedSlot.connected())
{
mRegionChangedSlot.disconnect();
}
cancelRequestTimeout();
}

bool LLMuteList::isLinden(const std::string& name)
Expand Down Expand Up @@ -735,6 +759,17 @@ bool LLMuteList::isMuted(const std::string& username, U32 flags) const
//-----------------------------------------------------------------------------
void LLMuteList::requestFromServer(const LLUUID& agent_id)
{
if (mLoadState == ML_REQUESTED)
{
const F64 now = LLTimer::getElapsedSeconds();
if (now - mRequestStartTime < MUTE_LIST_REQUEST_COOLDOWN_SECONDS)
{
return;
}
}

cancelRequestTimeout();

std::string agent_id_string;
std::string filename;
agent_id.toString(agent_id_string);
Expand Down Expand Up @@ -764,11 +799,64 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id)
}
mLoadState = ML_REQUESTED;
mRequestStartTime = LLTimer::getElapsedSeconds();
const F64 request_start_time = mRequestStartTime;
mRequestTimeout = LLEventTimer::run_after(MUTE_LIST_REQUEST_TIMEOUT_SECONDS, [this, request_start_time]()
{
mRequestTimeout = nullptr;
if (mLoadState != ML_REQUESTED)
{
return;
}
if (mRequestStartTime != request_start_time)
{
return;
}
LL_WARNS() << "Mute list request timed out; loading cached mute list" << LL_ENDL;
if (!loadFromCache())
{
LL_WARNS() << "Failed to load cached mute list after timeout" << LL_ENDL;
mLoadState = ML_FAILED;
}
else
{
notify_mute_list_cache_used();
}
});
// Double amount of retries due to this request happening during busy stage
// Ideally this should be turned into a capability
gMessageSystem->sendReliable(gAgent.getRegionHost(), LL_DEFAULT_RELIABLE_RETRIES * 2, true, LL_PING_BASED_TIMEOUT_DUMMY, NULL, NULL);
}

void LLMuteList::onRegionChanged()
{
if (isLoaded())
{
return;
}
if (mLoadState == ML_REQUESTED)
{
const F64 now = LLTimer::getElapsedSeconds();
if (now - mRequestStartTime < MUTE_LIST_REQUEST_COOLDOWN_SECONDS)
{
return;
}
}
if (gDisconnected || !gAgent.getRegion() || gAgent.getID().isNull())
{
return;
}
requestFromServer(gAgent.getID());
}

void LLMuteList::cancelRequestTimeout()
{
if (mRequestTimeout)
{
delete mRequestTimeout;
}
mRequestTimeout = nullptr;
}

//-----------------------------------------------------------------------------
// cache()
//-----------------------------------------------------------------------------
Expand All @@ -786,6 +874,14 @@ void LLMuteList::cache(const LLUUID& agent_id)
}
}

bool LLMuteList::loadFromCache()
{
std::string agent_id_string;
gAgent.getID().toString(agent_id_string);
std::string filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE, agent_id_string) + ".cached_mute";
return loadFromFile(filename);
}

//-----------------------------------------------------------------------------
// Static message handlers
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -825,12 +921,7 @@ void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**)
void LLMuteList::processUseCachedMuteList(LLMessageSystem* msg, void**)
{
LL_INFOS() << "LLMuteList::processUseCachedMuteList()" << LL_ENDL;

std::string agent_id_string;
gAgent.getID().toString(agent_id_string);
std::string filename;
filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string) + ".cached_mute";
LLMuteList::getInstance()->loadFromFile(filename);
LLMuteList::getInstance()->loadFromCache();
}

void LLMuteList::onFileMuteList(void** user_data, S32 error_code, LLExtStat ext_status)
Expand All @@ -845,7 +936,16 @@ void LLMuteList::onFileMuteList(void** user_data, S32 error_code, LLExtStat ext_
}
else
{
LLMuteList::getInstance()->mLoadState = ML_FAILED;
LL_WARNS() << "Mute list transfer failed; falling back to cached mute list" << LL_ENDL;
LLMuteList* mute_list = LLMuteList::getInstance();
if (!mute_list->loadFromCache())
{
mute_list->mLoadState = ML_FAILED;
}
else
{
notify_mute_list_cache_used();
}
}
delete local_filename_and_path;
}
Expand Down Expand Up @@ -905,6 +1005,7 @@ void LLMuteList::removeObserver(LLMuteListObserver* observer)
void LLMuteList::setLoaded()
{
mLoadState = ML_LOADED;
cancelRequestTimeout();
notifyObservers();
}

Expand Down
8 changes: 8 additions & 0 deletions indra/newview/llmutelist.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
#include "lluuid.h"
#include "llextendedstatus.h"

#include <boost/signals2/connection.hpp>

class LLViewerObject;
class LLEventTimer;
class LLMessageSystem;
class LLMuteListObserver;

Expand Down Expand Up @@ -127,6 +130,9 @@ class LLMuteList : public LLSingleton<LLMuteList>
void cache(const LLUUID& agent_id);

private:
void onRegionChanged();
void cancelRequestTimeout();
bool loadFromCache();
bool loadFromFile(const std::string& filename);
bool saveToFile(const std::string& filename);

Expand Down Expand Up @@ -178,6 +184,8 @@ class LLMuteList : public LLSingleton<LLMuteList>

EMuteListState mLoadState;
F64 mRequestStartTime;
LLEventTimer* mRequestTimeout;
boost::signals2::connection mRegionChangedSlot;

friend class LLDispatchEmptyMuteList;
};
Expand Down
9 changes: 9 additions & 0 deletions indra/newview/skins/default/xui/en/notifications.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,15 @@ Unable to add new entry to block list because you reached the limit of [MUTE_LIM
<tag>fail</tag>
</notification>

<notification
icon="alertmodal.tga"
name="MuteListFallbackCache"
persist="false"
type="notify">
Could not update the block list from the region. Using the local cached copy. Log out and back in to refresh the block list if recent changes are missing.
<tag>fail</tag>
</notification>

<notification
icon="alertmodal.tga"
name="UnableToLinkObjects"
Expand Down