/** * @file llconversationmodel.cpp * @brief Implementation of conversations list * * $LicenseInfo:firstyear=2009&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 "llavatarnamecache.h" #include "llavataractions.h" #include "llevents.h" #include "llfloaterimsession.h" #include "llsdutil.h" #include "llconversationmodel.h" #include "llimview.h" //For LLIMModel #include "lltrans.h" // // Conversation items : common behaviors // LLConversationItem::LLConversationItem(std::string display_name, const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) : LLFolderViewModelItemCommon(root_view_model), mName(display_name), mUUID(uuid), mNeedsRefresh(true), mConvType(CONV_UNKNOWN), mLastActiveTime(0.0), mDisplayModeratorOptions(false), mDisplayGroupBanOptions(false), mAvatarNameCacheConnection() { } LLConversationItem::LLConversationItem(const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) : LLFolderViewModelItemCommon(root_view_model), mName(""), mUUID(uuid), mNeedsRefresh(true), mConvType(CONV_UNKNOWN), mLastActiveTime(0.0), mDisplayModeratorOptions(false), mDisplayGroupBanOptions(false), mAvatarNameCacheConnection() { } LLConversationItem::LLConversationItem(LLFolderViewModelInterface& root_view_model) : LLFolderViewModelItemCommon(root_view_model), mName(""), mUUID(), mNeedsRefresh(true), mConvType(CONV_UNKNOWN), mLastActiveTime(0.0), mDisplayModeratorOptions(false), mDisplayGroupBanOptions(false), mAvatarNameCacheConnection() { } LLConversationItem::~LLConversationItem() { // Disconnect any previous avatar name cache connection to ensure // that the callback method is not called after destruction if (mAvatarNameCacheConnection.connected()) { mAvatarNameCacheConnection.disconnect(); } } //virtual void LLConversationItem::addChild(LLFolderViewModelItem* child) { // Avoid duplicates: bail out if that child is already present in the list // Note: this happens when models are created and 'parented' before views // This is performance unfriendly, but conversation can addToFolder multiple times child_list_t::const_iterator iter; for (iter = mChildren.begin(); iter != mChildren.end(); iter++) { if (child == *iter) { return; } } LLFolderViewModelItemCommon::addChild(child); } void LLConversationItem::postEvent(const std::string& event_type, LLConversationItemSession* session, LLConversationItemParticipant* participant) { LLUUID session_id = (session ? session->getUUID() : LLUUID()); LLUUID participant_id = (participant ? participant->getUUID() : LLUUID()); LLSD event(LLSDMap("type", event_type)("session_uuid", session_id)("participant_uuid", participant_id)); LLEventPumps::instance().obtain("ConversationsEvents").post(event); } // Virtual action callbacks void LLConversationItem::performAction(LLInventoryModel* model, std::string action) { } void LLConversationItem::openItem( void ) { } void LLConversationItem::closeItem( void ) { } void LLConversationItem::previewItem( void ) { } void LLConversationItem::showProperties(void) { } void LLConversationItem::buildParticipantMenuOptions(menuentry_vec_t& items, U32 flags) { if (flags & ITEM_IN_MULTI_SELECTION) { items.push_back(std::string("im")); items.push_back(std::string("offer_teleport")); items.push_back(std::string("voice_call")); items.push_back(std::string("remove_friends")); } else { items.push_back(std::string("view_profile")); items.push_back(std::string("im")); items.push_back(std::string("offer_teleport")); items.push_back(std::string("request_teleport")); if (getType() != CONV_SESSION_1_ON_1) { items.push_back(std::string("voice_call")); } else { LLVoiceChannel* voice_channel = LLIMModel::getInstance() ? LLIMModel::getInstance()->getVoiceChannel(this->getUUID()) : NULL; if(voice_channel != LLVoiceChannel::getCurrentVoiceChannel()) { items.push_back(std::string("voice_call")); } else { items.push_back(std::string("disconnect_from_voice")); } } items.push_back(std::string("chat_history")); items.push_back(std::string("separator_chat_history")); items.push_back(std::string("add_friend")); items.push_back(std::string("remove_friend")); items.push_back(std::string("invite_to_group")); items.push_back(std::string("separator_invite_to_group")); if (static_cast(mParent)->getType() == CONV_SESSION_NEARBY) items.push_back(std::string("zoom_in")); items.push_back(std::string("map")); items.push_back(std::string("share")); items.push_back(std::string("pay")); items.push_back(std::string("report_abuse")); items.push_back(std::string("block_unblock")); items.push_back(std::string("MuteText")); if ((getType() != CONV_SESSION_1_ON_1) && mDisplayModeratorOptions) { items.push_back(std::string("Moderator Options Separator")); items.push_back(std::string("Moderator Options")); items.push_back(std::string("AllowTextChat")); items.push_back(std::string("moderate_voice_separator")); items.push_back(std::string("ModerateVoiceMuteSelected")); items.push_back(std::string("ModerateVoiceUnMuteSelected")); items.push_back(std::string("ModerateVoiceMute")); items.push_back(std::string("ModerateVoiceUnmute")); } if ((getType() != CONV_SESSION_1_ON_1) && mDisplayGroupBanOptions) { items.push_back(std::string("Group Ban Separator")); items.push_back(std::string("BanMember")); } } } // method does subscription to changes in avatar name cache for current session/participant conversation item. void LLConversationItem::fetchAvatarName(bool isParticipant /*= true*/) { LLUUID item_id = getUUID(); // item should not be null for participants if (isParticipant) { llassert(item_id.notNull()); } // disconnect any previous avatar name cache connection if (mAvatarNameCacheConnection.connected()) { mAvatarNameCacheConnection.disconnect(); } // exclude nearby chat item if (item_id.notNull()) { // for P2P session item, override it as item of called agent if (CONV_SESSION_1_ON_1 == getType()) { item_id = LLIMModel::getInstance()->getOtherParticipantID(item_id); } // subscribe on avatar name cache changes for participant and session items mAvatarNameCacheConnection = LLAvatarNameCache::get(item_id, boost::bind(&LLConversationItem::onAvatarNameCache, this, _2)); } } // // LLConversationItemSession // LLConversationItemSession::LLConversationItemSession(std::string display_name, const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) : LLConversationItem(display_name,uuid,root_view_model), mIsLoaded(false) { mConvType = CONV_SESSION_UNKNOWN; } LLConversationItemSession::LLConversationItemSession(const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) : LLConversationItem(uuid,root_view_model) { mConvType = CONV_SESSION_UNKNOWN; } bool LLConversationItemSession::hasChildren() const { return getChildrenCount() > 0; } void LLConversationItemSession::addParticipant(LLConversationItemParticipant* participant) { addChild(participant); mIsLoaded = true; mNeedsRefresh = true; updateName(participant); postEvent("add_participant", this, participant); } void LLConversationItemSession::updateName(LLConversationItemParticipant* participant) { EConversationType conversation_type = getType(); // We modify the session name only in the case of an ad-hoc session or P2P session, exit otherwise (nothing to do) if ((conversation_type != CONV_SESSION_AD_HOC) && (conversation_type != CONV_SESSION_1_ON_1)) { return; } // Avoid changing the default name if no participant present yet if (mChildren.size() == 0) { return; } uuid_vec_t temp_uuids; // uuids vector for building the added participants' names string if (conversation_type == CONV_SESSION_AD_HOC || conversation_type == CONV_SESSION_1_ON_1) { // Build a string containing the participants UUIDs (minus own agent) and check if ready for display (we don't want "(waiting)" in there) // Note: we don't bind ourselves to the LLAvatarNameCache event as updateParticipantName() is called by // onAvatarNameCache() which is itself attached to the same event. // In the case of a P2P conversation, we need to grab the name of the other participant in the session instance itself // as we do not create participants for such a session. for (auto itemp : mChildren) { LLConversationItem* current_participant = dynamic_cast(itemp); // Add the avatar uuid to the list (except if it's the own agent uuid) if (current_participant->getUUID() != gAgentID) { LLAvatarName av_name; if (LLAvatarNameCache::get(current_participant->getUUID(), &av_name)) { temp_uuids.push_back(current_participant->getUUID()); if (conversation_type == CONV_SESSION_1_ON_1) { break; } } } } } if (temp_uuids.size() != 0) { std::string new_session_name; LLAvatarActions::buildResidentsString(temp_uuids, new_session_name); renameItem(new_session_name); postEvent("update_session", this, NULL); } } void LLConversationItemSession::removeParticipant(LLConversationItemParticipant* participant) { removeChild(participant); mNeedsRefresh = true; updateName(participant); postEvent("remove_participant", this, participant); } void LLConversationItemSession::removeParticipant(const LLUUID& participant_id) { LLConversationItemParticipant* participant = findParticipant(participant_id); if (participant) { removeParticipant(participant); } } void LLConversationItemSession::clearParticipants() { // clearParticipants function potentially is malfunctioning since it only cleans children of models, // it does nothing to views that own those models (listeners) // probably needs to post some kind of 'remove all participants' event clearChildren(); mIsLoaded = false; mNeedsRefresh = true; } void LLConversationItemSession::clearAndDeparentModels() { std::for_each(mChildren.begin(), mChildren.end(), [](LLFolderViewModelItem* c) { if (c->getNumRefs() == 0) { // LLConversationItemParticipant can be created but not assigned to any view, // it was waiting for an "add_participant" event to be processed delete c; } else { // Model is still assigned to some view/widget c->setParent(NULL); } } ); mChildren.clear(); } LLConversationItemParticipant* LLConversationItemSession::findParticipant(const LLUUID& participant_id) { // This is *not* a general tree parsing algorithm. It assumes that a session contains only // items (LLConversationItemParticipant) that have themselve no children. LLConversationItemParticipant* participant = NULL; child_list_t::iterator iter; for (iter = mChildren.begin(); iter != mChildren.end(); iter++) { participant = dynamic_cast(*iter); if (participant && participant->hasSameValue(participant_id)) { break; } } return (iter == mChildren.end() ? NULL : participant); } void LLConversationItemSession::setParticipantIsMuted(const LLUUID& participant_id, bool is_muted) { LLConversationItemParticipant* participant = findParticipant(participant_id); if (participant) { participant->moderateVoice(is_muted); } } void LLConversationItemSession::setParticipantIsModerator(const LLUUID& participant_id, bool is_moderator) { LLConversationItemParticipant* participant = findParticipant(participant_id); if (participant) { participant->setIsModerator(is_moderator); } } void LLConversationItemSession::setTimeNow(const LLUUID& participant_id) { mLastActiveTime = LLFrameTimer::getElapsedSeconds(); mNeedsRefresh = true; LLConversationItemParticipant* participant = findParticipant(participant_id); if (participant) { participant->setTimeNow(); } } void LLConversationItemSession::setDistance(const LLUUID& participant_id, F64 dist) { LLConversationItemParticipant* participant = findParticipant(participant_id); if (participant) { participant->setDistance(dist); mNeedsRefresh = true; } } void LLConversationItemSession::buildContextMenu(LLMenuGL& menu, U32 flags) { LL_DEBUGS() << "LLConversationItemParticipant::buildContextMenu()" << LL_ENDL; menuentry_vec_t items; menuentry_vec_t disabled_items; if((flags & ITEM_IN_MULTI_SELECTION) && (this->getType() != CONV_SESSION_NEARBY)) { items.push_back(std::string("close_selected_conversations")); } if(this->getType() == CONV_SESSION_1_ON_1) { items.push_back(std::string("close_conversation")); items.push_back(std::string("separator_disconnect_from_voice")); buildParticipantMenuOptions(items, flags); } else if(this->getType() == CONV_SESSION_GROUP) { items.push_back(std::string("close_conversation")); addVoiceOptions(items); items.push_back(std::string("chat_history")); items.push_back(std::string("separator_chat_history")); items.push_back(std::string("group_profile")); items.push_back(std::string("activate_group")); items.push_back(std::string("leave_group")); } else if(this->getType() == CONV_SESSION_AD_HOC) { items.push_back(std::string("close_conversation")); addVoiceOptions(items); items.push_back(std::string("chat_history")); } else if(this->getType() == CONV_SESSION_NEARBY) { items.push_back(std::string("chat_history")); } hide_context_entries(menu, items, disabled_items); } void LLConversationItemSession::addVoiceOptions(menuentry_vec_t& items) { LLVoiceChannel* voice_channel = LLIMModel::getInstance() ? LLIMModel::getInstance()->getVoiceChannel(this->getUUID()) : NULL; if(voice_channel != LLVoiceChannel::getCurrentVoiceChannel()) { items.push_back(std::string("open_voice_conversation")); } else { items.push_back(std::string("disconnect_from_voice")); } } // The time of activity of a session is the time of the most recent activity, session and participants included const bool LLConversationItemSession::getTime(F64& time) const { F64 most_recent_time = mLastActiveTime; bool has_time = (most_recent_time > 0.1); LLConversationItemParticipant* participant = NULL; child_list_t::const_iterator iter; for (iter = mChildren.begin(); iter != mChildren.end(); iter++) { participant = dynamic_cast(*iter); F64 participant_time; if (participant && participant->getTime(participant_time)) { has_time = true; most_recent_time = llmax(most_recent_time,participant_time); } } if (has_time) { time = most_recent_time; } return has_time; } void LLConversationItemSession::dumpDebugData(bool dump_children) { // Session info LL_INFOS() << "Merov debug : session " << this << ", uuid = " << mUUID << ", name = " << mName << ", is loaded = " << mIsLoaded << LL_ENDL; // Children info if (dump_children) { for (child_list_t::iterator iter = mChildren.begin(); iter != mChildren.end(); iter++) { LLConversationItemParticipant* participant = dynamic_cast(*iter); if (participant) { participant->dumpDebugData(); } } } } // should be invoked only for P2P sessions void LLConversationItemSession::onAvatarNameCache(const LLAvatarName& av_name) { if (mAvatarNameCacheConnection.connected()) { mAvatarNameCacheConnection.disconnect(); } renameItem(av_name.getDisplayName()); postEvent("update_session", this, NULL); } // // LLConversationItemParticipant // LLConversationItemParticipant::LLConversationItemParticipant(std::string display_name, const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) : LLConversationItem(display_name,uuid,root_view_model), mIsModeratorMuted(false), mIsModerator(false), mDisplayModeratorLabel(false), mDistToAgent(-1.0) { mDisplayName = display_name; mConvType = CONV_PARTICIPANT; } LLConversationItemParticipant::LLConversationItemParticipant(const LLUUID& uuid, LLFolderViewModelInterface& root_view_model) : LLConversationItem(uuid,root_view_model), mIsModeratorMuted(false), mIsModerator(false), mDisplayModeratorLabel(false), mDistToAgent(-1.0) { mConvType = CONV_PARTICIPANT; } void LLConversationItemParticipant::updateName() { llassert(getUUID().notNull()); if (getUUID().notNull()) { LLAvatarName av_name; if (LLAvatarNameCache::get(getUUID(),&av_name)) { updateName(av_name); } } } void LLConversationItemParticipant::onAvatarNameCache(const LLAvatarName& av_name) { if (mAvatarNameCacheConnection.connected()) { mAvatarNameCacheConnection.disconnect(); } updateName(av_name); } void LLConversationItemParticipant::updateName(const LLAvatarName& av_name) { mName = av_name.getUserName(); mDisplayName = av_name.getDisplayName(); if (mDisplayModeratorLabel) { mDisplayName += " " + LLTrans::getString("IM_moderator_label"); } renameItem(mDisplayName); if (mParent != NULL) { LLConversationItemSession* parent_session = dynamic_cast(mParent); if (parent_session != NULL) { parent_session->requestSort(); parent_session->updateName(this); postEvent("update_participant", parent_session, this); } } } void LLConversationItemParticipant::buildContextMenu(LLMenuGL& menu, U32 flags) { menuentry_vec_t items; menuentry_vec_t disabled_items; buildParticipantMenuOptions(items, flags); hide_context_entries(menu, items, disabled_items); } LLConversationItemSession* LLConversationItemParticipant::getParentSession() { LLConversationItemSession* parent_session = NULL; if (hasParent()) { parent_session = dynamic_cast(mParent); } return parent_session; } void LLConversationItemParticipant::dumpDebugData() { LL_INFOS() << "Merov debug : participant, uuid = " << mUUID << ", name = " << mName << ", display name = " << mDisplayName << ", muted = " << isVoiceMuted() << ", moderator = " << mIsModerator << LL_ENDL; } void LLConversationItemParticipant::setDisplayModeratorRole(bool displayRole) { if (displayRole != mDisplayModeratorLabel) { mDisplayModeratorLabel = displayRole; updateName(); } } bool LLConversationItemParticipant::isVoiceMuted() { return mIsModeratorMuted || LLMuteList::getInstance()->isMuted(mUUID, LLMute::flagVoiceChat); } // // LLConversationSort // // Comparison operator: returns "true" is a comes before b, "false" otherwise bool LLConversationSort::operator()(const LLConversationItem* const& a, const LLConversationItem* const& b) const { LLConversationItem::EConversationType type_a = a->getType(); LLConversationItem::EConversationType type_b = b->getType(); if ((type_a == LLConversationItem::CONV_PARTICIPANT) && (type_b == LLConversationItem::CONV_PARTICIPANT)) { // If both items are participants U32 sort_order = getSortOrderParticipants(); if (sort_order == LLConversationFilter::SO_DATE) { F64 time_a = 0.0; F64 time_b = 0.0; bool has_time_a = a->getTime(time_a); bool has_time_b = b->getTime(time_b); if (has_time_a && has_time_b) { // Most recent comes first return (time_a > time_b); } else if (has_time_a || has_time_b) { // If we have only one time available, the element with time must come first return has_time_a; } // If no time available, we'll default to sort by name at the end of this method } else if (sort_order == LLConversationFilter::SO_DISTANCE) { F64 dist_a = 0.0; F64 dist_b = 0.0; bool has_dist_a = a->getDistanceToAgent(dist_a); bool has_dist_b = b->getDistanceToAgent(dist_b); if (has_dist_a && has_dist_b) { // Closest comes first return (dist_a < dist_b); } else if (has_dist_a || has_dist_b) { // If we have only one distance available, the element with it must come first return has_dist_a; } // If no distance available, we'll default to sort by name at the end of this method } } else if ((type_a > LLConversationItem::CONV_PARTICIPANT) && (type_b > LLConversationItem::CONV_PARTICIPANT)) { // If both are sessions U32 sort_order = getSortOrderSessions(); if (sort_order == LLConversationFilter::SO_DATE) { // Sort by time F64 time_a = 0.0; F64 time_b = 0.0; bool has_time_a = a->getTime(time_a); bool has_time_b = b->getTime(time_b); if (has_time_a && has_time_b) { // Most recent comes first return (time_a > time_b); } else if (has_time_a || has_time_b) { // If we have only one time available, the element with time must come first return has_time_a; } // If no time available, we'll default to sort by name at the end of this method } else { if ((type_a == LLConversationItem::CONV_SESSION_NEARBY) || (type_b == LLConversationItem::CONV_SESSION_NEARBY)) { // If one is the nearby session, put nearby session *always* last return (type_b == LLConversationItem::CONV_SESSION_NEARBY); } else if (sort_order == LLConversationFilter::SO_SESSION_TYPE) { if (type_a != type_b) { // Lowest types come first. See LLConversationItem definition of types return (type_a < type_b); } // If types are identical, we'll default to sort by name at the end of this method } } } else { // If one item is a participant and the other a session, the session comes before the participant // so we simply compare the type // Notes: as a consequence, CONV_UNKNOWN (which should never get created...) always come first return (type_a > type_b); } // By default, in all other possible cases (including sort order type LLConversationFilter::SO_NAME of course), // we sort by name S32 compare = LLStringUtil::compareDict(a->getName(), b->getName()); return (compare < 0); } // // LLConversationViewModel // void LLConversationViewModel::sort(LLFolderViewFolder* folder) { base_t::sort(folder); } // EOF