diff options
Diffstat (limited to 'indra/newview/llimview.cpp')
-rw-r--r-- | indra/newview/llimview.cpp | 3289 |
1 files changed, 2454 insertions, 835 deletions
diff --git a/indra/newview/llimview.cpp b/indra/newview/llimview.cpp index 4dc5bfddec..c865dcf9a3 100644 --- a/indra/newview/llimview.cpp +++ b/indra/newview/llimview.cpp @@ -2,30 +2,25 @@ * @file LLIMMgr.cpp * @brief Container for Instant Messaging * - * $LicenseInfo:firstyear=2001&license=viewergpl$ - * - * Copyright (c) 2001-2007, Linden Research, Inc. - * + * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code - * The source code in this file ("Source Code") is provided by Linden Lab - * to you under the terms of the GNU General Public License, version 2.0 - * ("GPL"), unless you have obtained a separate licensing agreement - * ("Other License"), formally executed by you and Linden Lab. Terms of - * the GPL can be found in doc/GPL-license.txt in this distribution, or - * online at http://secondlife.com/developers/opensource/gplv2 + * 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. * - * There are special exceptions to the terms and conditions of the GPL as - * it is applied to this Source Code. View the full text of the exception - * in the file doc/FLOSS-exception.txt in this software distribution, or - * online at http://secondlife.com/developers/opensource/flossexception + * 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. * - * By copying, modifying or distributing this software, you acknowledge - * that you have read and understood your obligations described above, - * and agree to abide by those obligations. + * 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 * - * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO - * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, - * COMPLETENESS OR PERFORMANCE. + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ @@ -33,771 +28,1241 @@ #include "llimview.h" +#include "llavatarnamecache.h" // IDEVO +#include "llfloaterreg.h" #include "llfontgl.h" +#include "llgl.h" #include "llrect.h" #include "llerror.h" #include "llbutton.h" #include "llhttpclient.h" -#include "llsdutil.h" +#include "llsdutil_math.h" #include "llstring.h" -#include "llvieweruictrlfactory.h" +#include "lltextutil.h" +#include "lltrans.h" +#include "lluictrlfactory.h" #include "llagent.h" +#include "llagentui.h" +#include "llappviewer.h" +#include "llavatariconctrl.h" +#include "llbottomtray.h" #include "llcallingcard.h" #include "llchat.h" -#include "llresmgr.h" -#include "llfloaterchat.h" -#include "llfloaterchatterbox.h" -#include "llfloaternewim.h" -#include "llhttpnode.h" -#include "llimpanel.h" -#include "llresizebar.h" -#include "lltabcontainer.h" -#include "llviewercontrol.h" -#include "llfloater.h" +#include "llimfloater.h" +#include "llgroupiconctrl.h" +#include "llmd5.h" #include "llmutelist.h" -#include "llresizehandle.h" -#include "llkeyboard.h" -#include "llui.h" -#include "llviewermenu.h" -#include "llcallingcard.h" -#include "lltoolbar.h" +#include "llrecentpeople.h" #include "llviewermessage.h" #include "llviewerwindow.h" -#include "llnotify.h" -#include "llviewerregion.h" - -#include "llfirstuse.h" +#include "llnotifications.h" +#include "llnotificationsutil.h" +#include "llnearbychat.h" +#include "llspeakers.h" //for LLIMSpeakerMgr +#include "lltextbox.h" +#include "llviewercontrol.h" +#include "llviewerparcelmgr.h" -const EInstantMessage GROUP_DIALOG = IM_SESSION_GROUP_START; -const EInstantMessage DEFAULT_DIALOG = IM_NOTHING_SPECIAL; -// -// Globals -// -LLIMMgr* gIMMgr = NULL; +const static std::string ADHOC_NAME_SUFFIX(" Conference"); -// -// Statics -// -//*FIXME: make these all either UIStrings or Strings -static LLString sOnlyUserMessage; -static LLUIString sOfflineMessage; -static LLUIString sInviteMessage; +const static std::string NEARBY_P2P_BY_OTHER("nearby_P2P_by_other"); +const static std::string NEARBY_P2P_BY_AGENT("nearby_P2P_by_agent"); -std::map<std::string,LLString> LLFloaterIM::sEventStringsMap; -std::map<std::string,LLString> LLFloaterIM::sErrorStringsMap; -std::map<std::string,LLString> LLFloaterIM::sForceCloseSessionMap; +/** Timeout of outgoing session initialization (in seconds) */ +const static U32 SESSION_INITIALIZATION_TIMEOUT = 30; +std::string LLCallDialogManager::sPreviousSessionlName = ""; +LLIMModel::LLIMSession::SType LLCallDialogManager::sPreviousSessionType = LLIMModel::LLIMSession::P2P_SESSION; +std::string LLCallDialogManager::sCurrentSessionlName = ""; +LLIMModel::LLIMSession* LLCallDialogManager::sSession = NULL; +LLVoiceChannel::EState LLCallDialogManager::sOldState = LLVoiceChannel::STATE_READY; +const LLUUID LLOutgoingCallDialog::OCD_KEY = LLUUID("7CF78E11-0CFE-498D-ADB9-1417BF03DDB4"); // -// Helper Functions +// Globals // +LLIMMgr* gIMMgr = NULL; -// returns true if a should appear before b -//static BOOL group_dictionary_sort( LLGroupData* a, LLGroupData* b ) -//{ -// return (LLString::compareDict( a->mName, b->mName ) < 0); -//} +BOOL LLSessionTimeoutTimer::tick() +{ + if (mSessionId.isNull()) return TRUE; -// the other_participant_id is either an agent_id, a group_id, or an inventory -// folder item_id (collection of calling cards) + LLIMModel::LLIMSession* session = LLIMModel::getInstance()->findIMSession(mSessionId); + if (session && !session->mSessionInitialized) + { + gIMMgr->showSessionStartError("session_initialization_timed_out_error", mSessionId); + } + return TRUE; +} -// static -LLUUID LLIMMgr::computeSessionID( - EInstantMessage dialog, - const LLUUID& other_participant_id) +static void on_avatar_name_cache_toast(const LLUUID& agent_id, + const LLAvatarName& av_name, + LLSD msg) { - LLUUID session_id; - if (IM_SESSION_GROUP_START == dialog) + LLSD args; + args["MESSAGE"] = msg["message"]; + args["TIME"] = msg["time"]; + // *TODO: Can this ever be an object name or group name? + args["FROM"] = av_name.getCompleteName(); + args["FROM_ID"] = msg["from_id"]; + args["SESSION_ID"] = msg["session_id"]; + LLNotificationsUtil::add("IMToast", args, LLSD(), boost::bind(&LLIMFloater::show, msg["session_id"].asUUID())); +} + +void toast_callback(const LLSD& msg){ + // do not show toast in busy mode or it goes from agent + if (gAgent.getBusy() || gAgent.getID() == msg["from_id"]) { - // slam group session_id to the group_id (other_participant_id) - session_id = other_participant_id; + return; } - else if (IM_SESSION_CONFERENCE_START == dialog) + + // check whether incoming IM belongs to an active session or not + if (LLIMModel::getInstance()->getActiveSessionID().notNull() + && LLIMModel::getInstance()->getActiveSessionID() == msg["session_id"]) { - session_id.generate(); + return; } - else if (IM_SESSION_INVITE == dialog) + + // Skip toasting for system messages + if (msg["from_id"].asUUID() == LLUUID::null) { - // use provided session id for invites - session_id = other_participant_id; + return; } - else + + // Skip toasting if we have open window of IM with this session id + LLIMFloater* open_im_floater = LLIMFloater::findInstance(msg["session_id"]); + if (open_im_floater && open_im_floater->getVisible()) { - LLUUID agent_id = gAgent.getID(); - if (other_participant_id == agent_id) - { - // if we try to send an IM to ourselves then the XOR would be null - // so we just make the session_id the same as the agent_id - session_id = agent_id; - } - else - { - // peer-to-peer or peer-to-asset session_id is the XOR - session_id = other_participant_id ^ agent_id; - } + return; } - return session_id; -} -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// LLFloaterIM -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + LLAvatarNameCache::get(msg["from_id"].asUUID(), + boost::bind(&on_avatar_name_cache_toast, + _1, _2, msg)); +} -LLFloaterIM::LLFloaterIM() +void LLIMModel::setActiveSessionID(const LLUUID& session_id) { - // autoresize=false is necessary to avoid resizing of the IM window whenever - // a session is opened or closed (it would otherwise resize the window to match - // the size of the im-sesssion when they were created. This happens in - // LLMultiFloater::resizeToContents() when called through LLMultiFloater::addFloater()) - this->mAutoResize = FALSE; - gUICtrlFactory->buildFloater(this, "floater_im.xml"); + // check if such an ID really exists + if (!findIMSession(session_id)) + { + llwarns << "Trying to set as active a non-existent session!" << llendl; + return; + } + + mActiveSessionID = session_id; } -BOOL LLFloaterIM::postBuild() +LLIMModel::LLIMModel() { - sOnlyUserMessage = getString("only_user_message"); - sOfflineMessage = getUIString("offline_message"); + addNewMsgCallback(LLIMFloater::newIMCallback); + addNewMsgCallback(toast_callback); +} - sInviteMessage = getUIString("invite_message"); +LLIMModel::LLIMSession::LLIMSession(const LLUUID& session_id, const std::string& name, const EInstantMessage& type, const LLUUID& other_participant_id, const uuid_vec_t& ids, bool voice) +: mSessionID(session_id), + mName(name), + mType(type), + mParticipantUnreadMessageCount(0), + mNumUnread(0), + mOtherParticipantID(other_participant_id), + mInitialTargetIDs(ids), + mVoiceChannel(NULL), + mSpeakers(NULL), + mSessionInitialized(false), + mCallBackEnabled(true), + mTextIMPossible(true), + mOtherParticipantIsAvatar(true), + mStartCallOnInitialize(false), + mStartedAsIMCall(voice) +{ + // set P2P type by default + mSessionType = P2P_SESSION; - if ( sErrorStringsMap.find("generic") == sErrorStringsMap.end() ) + if (IM_NOTHING_SPECIAL == type || IM_SESSION_P2P_INVITE == type) { - sErrorStringsMap["generic"] = - getString("generic_request_error"); - } + mVoiceChannel = new LLVoiceChannelP2P(session_id, name, other_participant_id); + mOtherParticipantIsAvatar = LLVoiceClient::getInstance()->isParticipantAvatar(mSessionID); - if ( sErrorStringsMap.find("unverified") == - sErrorStringsMap.end() ) + // check if it was AVALINE call + if (!mOtherParticipantIsAvatar) + { + mSessionType = AVALINE_SESSION; + } + } + else { - sErrorStringsMap["unverified"] = - getString("insufficient_perms_error"); + mVoiceChannel = new LLVoiceChannelGroup(session_id, name); + + // determine whether it is group or conference session + if (gAgent.isInGroup(mSessionID)) + { + mSessionType = GROUP_SESSION; + } + else + { + mSessionType = ADHOC_SESSION; + } } - if ( sErrorStringsMap.end() == - sErrorStringsMap.find("no_ability") ) + if(mVoiceChannel) { - sErrorStringsMap["no_ability"] = - getString("no_ability_error"); + mVoiceChannelStateChangeConnection = mVoiceChannel->setStateChangedCallback(boost::bind(&LLIMSession::onVoiceChannelStateChanged, this, _1, _2, _3)); } + + mSpeakers = new LLIMSpeakerMgr(mVoiceChannel); + + // All participants will be added to the list of people we've recently interacted with. - if ( sErrorStringsMap.end() == - sErrorStringsMap.find("muted") ) + // we need to add only _active_ speakers...so comment this. + // may delete this later on cleanup + //mSpeakers->addListener(&LLRecentPeople::instance(), "add"); + + //we need to wait for session initialization for outgoing ad-hoc and group chat session + //correct session id for initiated ad-hoc chat will be received from the server + if (!LLIMModel::getInstance()->sendStartSession(mSessionID, mOtherParticipantID, + mInitialTargetIDs, mType)) { - sErrorStringsMap["muted"] = - getString("muted_error"); + //we don't need to wait for any responses + //so we're already initialized + mSessionInitialized = true; } - - if ( sErrorStringsMap.end() == - sErrorStringsMap.find("not_a_moderator") ) + else { - sErrorStringsMap["not_a_moderator"] = - getString("not_a_mod_error"); + //tick returns TRUE - timer will be deleted after the tick + new LLSessionTimeoutTimer(mSessionID, SESSION_INITIALIZATION_TIMEOUT); } - if ( sErrorStringsMap.end() == - sErrorStringsMap.find("does not exist") ) + if (IM_NOTHING_SPECIAL == type) { - sErrorStringsMap["does not exist"] = - getString("session_does_not_exist_error"); + mCallBackEnabled = LLVoiceClient::getInstance()->isSessionCallBackPossible(mSessionID); + mTextIMPossible = LLVoiceClient::getInstance()->isSessionTextIMPossible(mSessionID); } - if ( sEventStringsMap.end() == sEventStringsMap.find("add") ) + buildHistoryFileName(); + + if ( gSavedPerAccountSettings.getBOOL("LogShowHistory") ) { - sEventStringsMap["add"] = - getString("add_session_event"); + std::list<LLSD> chat_history; + + //involves parsing of a chat history + LLLogChat::loadAllHistory(mHistoryFileName, chat_history); + addMessagesFromHistory(chat_history); } - if ( sEventStringsMap.end() == sEventStringsMap.find("message") ) + // Localizing name of ad-hoc session. STORM-153 + // Changing name should happen here- after the history file was created, so that + // history files have consistent (English) names in different locales. + if (isAdHocSessionType() && IM_SESSION_INVITE == type) { - sEventStringsMap["message"] = - getString("message_session_event"); + // Name here has a form of "<Avatar's name> Conference" + // Lets update it to localize the "Conference" word. See EXT-8429. + S32 separator_index = mName.rfind(" "); + std::string name = mName.substr(0, separator_index); + ++separator_index; + std::string conference_word = mName.substr(separator_index, mName.length()); + + // additional check that session name is what we expected + if ("Conference" == conference_word) + { + LLStringUtil::format_map_t args; + args["[AGENT_NAME]"] = name; + LLTrans::findString(mName, "conference-title-incoming", args); + } } +} +void LLIMModel::LLIMSession::onVoiceChannelStateChanged(const LLVoiceChannel::EState& old_state, const LLVoiceChannel::EState& new_state, const LLVoiceChannel::EDirection& direction) +{ + std::string you_joined_call = LLTrans::getString("you_joined_call"); + std::string you_started_call = LLTrans::getString("you_started_call"); + std::string other_avatar_name = ""; - if ( sEventStringsMap.end() == sEventStringsMap.find("mute") ) - { - sEventStringsMap["mute"] = - getString("mute_agent_event"); - } + std::string message; - if ( sForceCloseSessionMap.end() == - sForceCloseSessionMap.find("removed") ) + switch(mSessionType) { - sForceCloseSessionMap["removed"] = - getString("removed_from_group"); - } + case AVALINE_SESSION: + // no text notifications + break; + case P2P_SESSION: + gCacheName->getFullName(mOtherParticipantID, other_avatar_name); // voice - if ( sForceCloseSessionMap.end() == - sForceCloseSessionMap.find("no ability") ) + if(direction == LLVoiceChannel::INCOMING_CALL) + { + switch(new_state) + { + case LLVoiceChannel::STATE_CALL_STARTED : + { + LLStringUtil::format_map_t string_args; + string_args["[NAME]"] = other_avatar_name; + message = LLTrans::getString("name_started_call", string_args); + LLIMModel::getInstance()->addMessage(mSessionID, SYSTEM_FROM, LLUUID::null, message); + break; + } + case LLVoiceChannel::STATE_CONNECTED : + LLIMModel::getInstance()->addMessage(mSessionID, SYSTEM_FROM, LLUUID::null, you_joined_call); + default: + break; + } + } + else // outgoing call + { + switch(new_state) + { + case LLVoiceChannel::STATE_CALL_STARTED : + LLIMModel::getInstance()->addMessage(mSessionID, SYSTEM_FROM, LLUUID::null, you_started_call); + break; + case LLVoiceChannel::STATE_CONNECTED : + message = LLTrans::getString("answered_call"); + LLIMModel::getInstance()->addMessage(mSessionID, SYSTEM_FROM, LLUUID::null, message); + default: + break; + } + } + break; + + case GROUP_SESSION: + case ADHOC_SESSION: + if(direction == LLVoiceChannel::INCOMING_CALL) + { + switch(new_state) + { + case LLVoiceChannel::STATE_CONNECTED : + LLIMModel::getInstance()->addMessage(mSessionID, SYSTEM_FROM, LLUUID::null, you_joined_call); + default: + break; + } + } + else // outgoing call + { + switch(new_state) + { + case LLVoiceChannel::STATE_CALL_STARTED : + LLIMModel::getInstance()->addMessage(mSessionID, SYSTEM_FROM, LLUUID::null, you_started_call); + break; + default: + break; + } + } + } + // Update speakers list when connected + if (LLVoiceChannel::STATE_CONNECTED == new_state) { - sForceCloseSessionMap["no ability"] = - getString("close_on_no_ability"); + mSpeakers->update(true); } - - return TRUE; } -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// Class LLIMViewFriendObserver -// -// Bridge to suport knowing when the inventory has changed. -//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -class LLIMViewFriendObserver : public LLFriendObserver +LLIMModel::LLIMSession::~LLIMSession() { -public: - LLIMViewFriendObserver(LLIMMgr* tv) : mTV(tv) {} - virtual ~LLIMViewFriendObserver() {} - virtual void changed(U32 mask) + delete mSpeakers; + mSpeakers = NULL; + + // End the text IM session if necessary + if(LLVoiceClient::getInstance() && mOtherParticipantID.notNull()) { - if(mask & (LLFriendObserver::ADD | LLFriendObserver::REMOVE | LLFriendObserver::ONLINE)) + switch(mType) { - mTV->refresh(); + case IM_NOTHING_SPECIAL: + case IM_SESSION_P2P_INVITE: + LLVoiceClient::getInstance()->endUserIMSession(mOtherParticipantID); + break; + + default: + // Appease the linux compiler + break; } } -protected: - LLIMMgr* mTV; -}; + mVoiceChannelStateChangeConnection.disconnect(); + + // HAVE to do this here -- if it happens in the LLVoiceChannel destructor it will call the wrong version (since the object's partially deconstructed at that point). + mVoiceChannel->deactivate(); -class LLIMMgr::LLIMSessionInvite + delete mVoiceChannel; + mVoiceChannel = NULL; +} + +void LLIMModel::LLIMSession::sessionInitReplyReceived(const LLUUID& new_session_id) { -public: - LLIMSessionInvite( - const LLUUID& session_id, - const LLString& session_name, - const LLUUID& caller_id, - const LLString& caller_name, - EInstantMessage type, - EInvitationType inv_type, - const LLString& session_handle, - const LLString& notify_box) : - mSessionID(session_id), - mSessionName(session_name), - mCallerID(caller_id), - mCallerName(caller_name), - mType(type), - mInvType(inv_type), - mSessionHandle(session_handle), - mNotifyBox(notify_box) - {}; - - LLUUID mSessionID; - LLString mSessionName; - LLUUID mCallerID; - LLString mCallerName; - EInstantMessage mType; - EInvitationType mInvType; - LLString mSessionHandle; - LLString mNotifyBox; -}; + mSessionInitialized = true; + + if (new_session_id != mSessionID) + { + mSessionID = new_session_id; + mVoiceChannel->updateSessionID(new_session_id); + } +} +void LLIMModel::LLIMSession::addMessage(const std::string& from, const LLUUID& from_id, const std::string& utf8_text, const std::string& time, const bool is_history) +{ + LLSD message; + message["from"] = from; + message["from_id"] = from_id; + message["message"] = utf8_text; + message["time"] = time; + message["index"] = (LLSD::Integer)mMsgs.size(); + message["is_history"] = is_history; -// -// Public Static Member Functions -// + mMsgs.push_front(message); -// This is a helper function to determine what kind of im session -// should be used for the given agent. -// static -EInstantMessage LLIMMgr::defaultIMTypeForAgent(const LLUUID& agent_id) + if (mSpeakers && from_id.notNull()) + { + mSpeakers->speakerChatted(from_id); + mSpeakers->setSpeakerTyping(from_id, FALSE); + } +} + +void LLIMModel::LLIMSession::addMessagesFromHistory(const std::list<LLSD>& history) { - EInstantMessage type = IM_NOTHING_SPECIAL; - if(is_agent_friend(agent_id)) + std::list<LLSD>::const_iterator it = history.begin(); + while (it != history.end()) { - if(LLAvatarTracker::instance().isBuddyOnline(agent_id)) + const LLSD& msg = *it; + + std::string from = msg[IM_FROM]; + LLUUID from_id; + if (msg[IM_FROM_ID].isDefined()) { - type = IM_SESSION_CONFERENCE_START; + from_id = msg[IM_FROM_ID].asUUID(); } + else + { + // Legacy chat logs only wrote the legacy name, not the agent_id + gCacheName->getUUID(from, from_id); + } + + std::string timestamp = msg[IM_TIME]; + std::string text = msg[IM_TEXT]; + + addMessage(from, from_id, text, timestamp, true); + + it++; } - return type; } -// static -//void LLIMMgr::onPinButton(void*) -//{ -// BOOL state = gSavedSettings.getBOOL( "PinTalkViewOpen" ); -// gSavedSettings.setBOOL( "PinTalkViewOpen", !state ); -//} - -// static -void LLIMMgr::toggle(void*) +void LLIMModel::LLIMSession::chatFromLogFile(LLLogChat::ELogLineType type, const LLSD& msg, void* userdata) { - static BOOL return_to_mouselook = FALSE; + if (!userdata) return; - // Hide the button and show the floater or vice versa. - llassert( gIMMgr ); - BOOL old_state = gIMMgr->getFloaterOpen(); - - // If we're in mouselook and we triggered the Talk View, we want to talk. - if( gAgent.cameraMouselook() && old_state ) + LLIMSession* self = (LLIMSession*) userdata; + + if (type == LLLogChat::LOG_LINE) { - return_to_mouselook = TRUE; - gAgent.changeCameraToDefault(); - return; + self->addMessage("", LLSD(), msg["message"].asString(), "", true); + } + else if (type == LLLogChat::LOG_LLSD) + { + self->addMessage(msg["from"].asString(), msg["from_id"].asUUID(), msg["message"].asString(), msg["time"].asString(), true); } +} + +LLIMModel::LLIMSession* LLIMModel::findIMSession(const LLUUID& session_id) const +{ + return get_if_there(mId2SessionMap, session_id, + (LLIMModel::LLIMSession*) NULL); +} + +//*TODO consider switching to using std::set instead of std::list for holding LLUUIDs across the whole code +LLIMModel::LLIMSession* LLIMModel::findAdHocIMSession(const uuid_vec_t& ids) +{ + S32 num = ids.size(); + if (!num) return NULL; - BOOL new_state = !old_state; + if (mId2SessionMap.empty()) return NULL; - if (new_state) + std::map<LLUUID, LLIMSession*>::const_iterator it = mId2SessionMap.begin(); + for (; it != mId2SessionMap.end(); ++it) { - // ...making visible - if ( gAgent.cameraMouselook() ) + LLIMSession* session = (*it).second; + + if (!session->isAdHoc()) continue; + if (session->mInitialTargetIDs.size() != num) continue; + + std::list<LLUUID> tmp_list(session->mInitialTargetIDs.begin(), session->mInitialTargetIDs.end()); + + uuid_vec_t::const_iterator iter = ids.begin(); + while (iter != ids.end()) { - return_to_mouselook = TRUE; - gAgent.changeCameraToDefault(); + tmp_list.remove(*iter); + ++iter; + + if (tmp_list.empty()) + { + break; + } } - } - else - { - // ...hiding - if ( gAgent.cameraThirdPerson() && return_to_mouselook ) + + if (tmp_list.empty() && iter == ids.end()) { - gAgent.changeCameraToMouselook(); + return session; } - return_to_mouselook = FALSE; } - gIMMgr->setFloaterOpen( new_state ); + return NULL; } -// -// Member Functions -// - -LLIMMgr::LLIMMgr() : - mFriendObserver(NULL), - mIMReceived(FALSE) +bool LLIMModel::LLIMSession::isOutgoingAdHoc() { - mFriendObserver = new LLIMViewFriendObserver(this); - LLAvatarTracker::instance().addObserver(mFriendObserver); + return IM_SESSION_CONFERENCE_START == mType; +} - //*HACK: use floater to initialize string constants from xml file - // then delete it right away - LLFloaterIM* dummy_floater = new LLFloaterIM(); - delete dummy_floater; +bool LLIMModel::LLIMSession::isAdHoc() +{ + return IM_SESSION_CONFERENCE_START == mType || (IM_SESSION_INVITE == mType && !gAgent.isInGroup(mSessionID)); +} - mPendingInvitations = LLSD::emptyMap(); - mPendingAgentListUpdates = LLSD::emptyMap(); +bool LLIMModel::LLIMSession::isP2P() +{ + return IM_NOTHING_SPECIAL == mType; } -LLIMMgr::~LLIMMgr() +bool LLIMModel::LLIMSession::isOtherParticipantAvaline() { - LLAvatarTracker::instance().removeObserver(mFriendObserver); - delete mFriendObserver; - // Children all cleaned up by default view destructor. + return !mOtherParticipantIsAvatar; } -// Add a message to a session. -void LLIMMgr::addMessage( - const LLUUID& session_id, - const LLUUID& target_id, - const char* from, - const char* msg, - const char* session_name, - EInstantMessage dialog, - U32 parent_estate_id, - const LLUUID& region_id, - const LLVector3& position) +void LLIMModel::LLIMSession::onAvatarNameCache(const LLUUID& avatar_id, const LLAvatarName& av_name) { - LLUUID other_participant_id = target_id; - bool is_from_system = target_id.isNull(); + // if username is empty, display names isn't enabled, use the display name + mHistoryFileName = av_name.mUsername.empty() ? av_name.mDisplayName : av_name.mUsername; +} - // don't process muted IMs - if (gMuteListp->isMuted( - other_participant_id, - LLMute::flagTextChat) && !gMuteListp->isLinden(from)) +void LLIMModel::LLIMSession::buildHistoryFileName() +{ + mHistoryFileName = mName; + + //ad-hoc requires sophisticated chat history saving schemes + if (isAdHoc()) { - return; + //in case of outgoing ad-hoc sessions + if (mInitialTargetIDs.size()) + { + std::set<LLUUID> sorted_uuids(mInitialTargetIDs.begin(), mInitialTargetIDs.end()); + mHistoryFileName = mName + " hash" + generateHash(sorted_uuids); + return; + } + + //in case of incoming ad-hoc sessions + mHistoryFileName = mName + " " + LLLogChat::timestamp(true) + " " + mSessionID.asString().substr(0, 4); } - //not sure why...but if it is from ourselves we set the target_id - //to be NULL - if( other_participant_id == gAgent.getID() ) + // look up username to use as the log name + if (isP2P()) { - other_participant_id = LLUUID::null; + LLAvatarNameCache::get(mOtherParticipantID, boost::bind(&LLIMModel::LLIMSession::onAvatarNameCache, this, _1, _2)); } +} - LLFloaterIMPanel* floater; - LLUUID new_session_id = session_id; - if (new_session_id.isNull()) +//static +std::string LLIMModel::LLIMSession::generateHash(const std::set<LLUUID>& sorted_uuids) +{ + LLMD5 md5_uuid; + + std::set<LLUUID>::const_iterator it = sorted_uuids.begin(); + while (it != sorted_uuids.end()) { - //no session ID...compute new one - new_session_id = computeSessionID(dialog, other_participant_id); + md5_uuid.update((unsigned char*)(*it).mData, 16); + it++; } - floater = findFloaterBySession(new_session_id); - if (!floater) + md5_uuid.finalize(); + + LLUUID participants_md5_hash; + md5_uuid.raw_digest((unsigned char*) participants_md5_hash.mData); + return participants_md5_hash.asString(); +} + + +void LLIMModel::processSessionInitializedReply(const LLUUID& old_session_id, const LLUUID& new_session_id) +{ + LLIMSession* session = findIMSession(old_session_id); + if (session) { - floater = findFloaterBySession(other_participant_id); - if (floater) + session->sessionInitReplyReceived(new_session_id); + + if (old_session_id != new_session_id) { - llinfos << "found the IM session " << session_id - << " by participant " << other_participant_id << llendl; + mId2SessionMap.erase(old_session_id); + mId2SessionMap[new_session_id] = session; + + gIMMgr->notifyObserverSessionIDUpdated(old_session_id, new_session_id); } - } - // create IM window as necessary - if(!floater) - { - const char* name = from; - if(session_name && (strlen(session_name)>1)) + LLIMFloater* im_floater = LLIMFloater::findInstance(old_session_id); + if (im_floater) { - name = session_name; + im_floater->sessionInitReplyReceived(new_session_id); } - - floater = createFloater( - new_session_id, - other_participant_id, - name, - dialog, - FALSE); - - // When we get a new IM, and if you are a god, display a bit - // of information about the source. This is to help liaisons - // when answering questions. - if(gAgent.isGodlike()) + // auto-start the call on session initialization? + if (session->mStartCallOnInitialize) { - // *TODO:translate (low priority, god ability) - std::ostringstream bonus_info; - bonus_info << "*** parent estate: " - << parent_estate_id - << ((parent_estate_id == 1) ? ", mainland" : "") - << ((parent_estate_id == 5) ? ", teen" : ""); + gIMMgr->startCall(new_session_id); + } + } +} - // once we have web-services (or something) which returns - // information about a region id, we can print this out - // and even have it link to map-teleport or something. - //<< "*** region_id: " << region_id << std::endl - //<< "*** position: " << position << std::endl; +void LLIMModel::testMessages() +{ + LLUUID bot1_id("d0426ec6-6535-4c11-a5d9-526bb0c654d9"); + LLUUID bot1_session_id; + std::string from = "IM Tester"; - floater->addHistoryLine(bonus_info.str(), gSavedSettings.getColor4("SystemChatColor")); - } + bot1_session_id = LLIMMgr::computeSessionID(IM_NOTHING_SPECIAL, bot1_id); + newSession(bot1_session_id, from, IM_NOTHING_SPECIAL, bot1_id); + addMessage(bot1_session_id, from, bot1_id, "Test Message: Hi from testerbot land!"); - make_ui_sound("UISndNewIncomingIMSession"); - } + LLUUID bot2_id; + std::string firstname[] = {"Roflcopter", "Joe"}; + std::string lastname[] = {"Linden", "Tester", "Resident", "Schmoe"}; - // now add message to floater - if ( is_from_system ) // chat came from system + S32 rand1 = ll_rand(sizeof firstname)/(sizeof firstname[0]); + S32 rand2 = ll_rand(sizeof lastname)/(sizeof lastname[0]); + + from = firstname[rand1] + " " + lastname[rand2]; + bot2_id.generate(from); + LLUUID bot2_session_id = LLIMMgr::computeSessionID(IM_NOTHING_SPECIAL, bot2_id); + newSession(bot2_session_id, from, IM_NOTHING_SPECIAL, bot2_id); + addMessage(bot2_session_id, from, bot2_id, "Test Message: Hello there, I have a question. Can I bother you for a second? "); + addMessage(bot2_session_id, from, bot2_id, "Test Message: OMGWTFBBQ."); +} + +//session name should not be empty +bool LLIMModel::newSession(const LLUUID& session_id, const std::string& name, const EInstantMessage& type, + const LLUUID& other_participant_id, const uuid_vec_t& ids, bool voice) +{ + if (name.empty()) { - floater->addHistoryLine( - msg, - gSavedSettings.getColor4("SystemChatColor")); + llwarns << "Attempt to create a new session with empty name; id = " << session_id << llendl; + return false; } - else + + if (findIMSession(session_id)) { - floater->addHistoryLine(other_participant_id, msg); + llwarns << "IM Session " << session_id << " already exists" << llendl; + return false; } - LLFloaterChatterBox* chat_floater = LLFloaterChatterBox::getInstance(LLSD()); + LLIMSession* session = new LLIMSession(session_id, name, type, other_participant_id, ids, voice); + mId2SessionMap[session_id] = session; - if( !chat_floater->getVisible() && !floater->getVisible()) - { - //if the IM window is not open and the floater is not visible (i.e. not torn off) - LLFloater* previouslyActiveFloater = chat_floater->getActiveFloater(); + // When notifying observer, name of session is used instead of "name", because they may not be the + // same if it is an adhoc session (in this case name is localized in LLIMSession constructor). + std::string session_name = LLIMModel::getInstance()->getName(session_id); + LLIMMgr::getInstance()->notifyObserverSessionAdded(session_id, session_name, other_participant_id); - // select the newly added floater (or the floater with the new line added to it). - // it should be there. - chat_floater->selectFloater(floater); + return true; - //there was a previously unseen IM, make that old tab flashing - //it is assumed that the most recently unseen IM tab is the one current selected/active - if ( previouslyActiveFloater && getIMReceived() ) - { - chat_floater->setFloaterFlashing(previouslyActiveFloater, TRUE); - } +} - //notify of a new IM - notifyNewIM(); - } +bool LLIMModel::newSession(const LLUUID& session_id, const std::string& name, const EInstantMessage& type, const LLUUID& other_participant_id, bool voice) +{ + uuid_vec_t no_ids; + return newSession(session_id, name, type, other_participant_id, no_ids, voice); } -void LLIMMgr::addSystemMessage(const LLUUID& session_id, const LLString& message_name, const LLString::format_map_t& args) +bool LLIMModel::clearSession(const LLUUID& session_id) { - LLUIString message; - - // null session id means near me (chat history) - if (session_id.isNull()) + if (mId2SessionMap.find(session_id) == mId2SessionMap.end()) return false; + delete (mId2SessionMap[session_id]); + mId2SessionMap.erase(session_id); + return true; +} + +void LLIMModel::getMessagesSilently(const LLUUID& session_id, std::list<LLSD>& messages, int start_index) +{ + LLIMSession* session = findIMSession(session_id); + if (!session) { - LLFloaterChat* floaterp = LLFloaterChat::getInstance(); + llwarns << "session " << session_id << "does not exist " << llendl; + return; + } - message = floaterp->getUIString(message_name); - message.setArgList(args); + int i = session->mMsgs.size() - start_index; - LLChat chat(message); - chat.mSourceType = CHAT_SOURCE_SYSTEM; - LLFloaterChat::getInstance()->addChatHistory(chat); - } - else // going to IM session + for (std::list<LLSD>::iterator iter = session->mMsgs.begin(); + iter != session->mMsgs.end() && i > 0; + iter++) { - LLFloaterIMPanel* floaterp = findFloaterBySession(session_id); - if (floaterp) - { - message = floaterp->getUIString(message_name); - message.setArgList(args); + LLSD msg; + msg = *iter; + messages.push_back(*iter); + i--; + } +} - gIMMgr->addMessage(session_id, LLUUID::null, SYSTEM_FROM, message.getString().c_str()); - } +void LLIMModel::sendNoUnreadMessages(const LLUUID& session_id) +{ + LLIMSession* session = findIMSession(session_id); + if (!session) + { + llwarns << "session " << session_id << "does not exist " << llendl; + return; } + + session->mNumUnread = 0; + session->mParticipantUnreadMessageCount = 0; + + LLSD arg; + arg["session_id"] = session_id; + arg["num_unread"] = 0; + arg["participant_unread"] = session->mParticipantUnreadMessageCount; + mNoUnreadMsgsSignal(arg); } -void LLIMMgr::notifyNewIM() +void LLIMModel::getMessages(const LLUUID& session_id, std::list<LLSD>& messages, int start_index) { - if(!gIMMgr->getFloaterOpen()) + getMessagesSilently(session_id, messages, start_index); + + sendNoUnreadMessages(session_id); +} + +bool LLIMModel::addToHistory(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text) { + + LLIMSession* session = findIMSession(session_id); + + if (!session) { - mIMReceived = TRUE; + llwarns << "session " << session_id << "does not exist " << llendl; + return false; } + + session->addMessage(from, from_id, utf8_text, LLLogChat::timestamp(false)); //might want to add date separately + + return true; } -void LLIMMgr::clearNewIMNotification() +bool LLIMModel::logToFile(const std::string& file_name, const std::string& from, const LLUUID& from_id, const std::string& utf8_text) { - mIMReceived = FALSE; + if (gSavedPerAccountSettings.getBOOL("LogInstantMessages")) + { + LLLogChat::saveHistory(file_name, from, from_id, utf8_text); + return true; + } + else + { + return false; + } } -BOOL LLIMMgr::getIMReceived() const +bool LLIMModel::logToFile(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text) { - return mIMReceived; + return logToFile(LLIMModel::getInstance()->getHistoryFileName(session_id), from, from_id, utf8_text); } -// This method returns TRUE if the local viewer has a session -// currently open keyed to the uuid. -BOOL LLIMMgr::isIMSessionOpen(const LLUUID& uuid) +bool LLIMModel::proccessOnlineOfflineNotification( + const LLUUID& session_id, + const std::string& utf8_text) { - LLFloaterIMPanel* floater = findFloaterBySession(uuid); - if(floater) return TRUE; - return FALSE; + // Add system message to history + return addMessage(session_id, SYSTEM_FROM, LLUUID::null, utf8_text); } -LLUUID LLIMMgr::addP2PSession(const std::string& name, - const LLUUID& other_participant_id, - const LLString& voice_session_handle) +bool LLIMModel::addMessage(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, + const std::string& utf8_text, bool log2file /* = true */) { + + LLIMSession* session = addMessageSilently(session_id, from, from_id, utf8_text, log2file); + if (!session) return false; + + //good place to add some1 to recent list + //other places may be called from message history. + if( !from_id.isNull() && + ( session->isP2PSessionType() || session->isAdHocSessionType() ) ) + LLRecentPeople::instance().add(from_id); + + // notify listeners + LLSD arg; + arg["session_id"] = session_id; + arg["num_unread"] = session->mNumUnread; + arg["participant_unread"] = session->mParticipantUnreadMessageCount; + arg["message"] = utf8_text; + arg["from"] = from; + arg["from_id"] = from_id; + arg["time"] = LLLogChat::timestamp(false); + mNewMsgSignal(arg); + + return true; +} + +LLIMModel::LLIMSession* LLIMModel::addMessageSilently(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, + const std::string& utf8_text, bool log2file /* = true */) { - LLUUID session_id = addSession(name, IM_NOTHING_SPECIAL, other_participant_id); + LLIMSession* session = findIMSession(session_id); - LLFloaterIMPanel* floater = findFloaterBySession(session_id); - if(floater) + if (!session) { - LLVoiceChannelP2P* voice_channelp = (LLVoiceChannelP2P*)floater->getVoiceChannel(); - voice_channelp->setSessionHandle(voice_session_handle); + llwarns << "session " << session_id << "does not exist " << llendl; + return NULL; } - return session_id; + // replace interactive system message marker with correct from string value + std::string from_name = from; + if (INTERACTIVE_SYSTEM_FROM == from) + { + from_name = SYSTEM_FROM; + } + + addToHistory(session_id, from_name, from_id, utf8_text); + if (log2file) logToFile(session_id, from_name, from_id, utf8_text); + + session->mNumUnread++; + + //update count of unread messages from real participant + if (!(from_id.isNull() || from_id == gAgentID || SYSTEM_FROM == from) + // we should increment counter for interactive system messages() + || INTERACTIVE_SYSTEM_FROM == from) + { + ++(session->mParticipantUnreadMessageCount); + } + + return session; } -// This adds a session to the talk view. The name is the local name of -// the session, dialog specifies the type of session. If the session -// exists, it is brought forward. Specifying id = NULL results in an -// im session to everyone. Returns the uuid of the session. -LLUUID LLIMMgr::addSession( - const std::string& name, - EInstantMessage dialog, - const LLUUID& other_participant_id) + +const std::string LLIMModel::getName(const LLUUID& session_id) const { - LLUUID session_id = computeSessionID(dialog, other_participant_id); + LLIMSession* session = findIMSession(session_id); - LLFloaterIMPanel* floater = findFloaterBySession(session_id); - if(!floater) + if (!session) { - LLDynamicArray<LLUUID> ids; - ids.put(other_participant_id); + llwarns << "session " << session_id << "does not exist " << llendl; + return LLTrans::getString("no_session_message"); + } - floater = createFloater( - session_id, - other_participant_id, - name, - ids, - dialog, - TRUE); + return session->mName; +} - noteOfflineUsers(floater, ids); - LLFloaterChatterBox::showInstance(session_id); - } - else +const S32 LLIMModel::getNumUnread(const LLUUID& session_id) const +{ + LLIMSession* session = findIMSession(session_id); + if (!session) { - floater->open(); + llwarns << "session " << session_id << "does not exist " << llendl; + return -1; } - //mTabContainer->selectTabPanel(panel); - floater->setInputFocus(TRUE); - return floater->getSessionID(); + + return session->mNumUnread; } -// Adds a session using the given session_id. If the session already exists -// the dialog type is assumed correct. Returns the uuid of the session. -LLUUID LLIMMgr::addSession( - const std::string& name, - EInstantMessage dialog, - const LLUUID& other_participant_id, - const LLDynamicArray<LLUUID>& ids) +const LLUUID& LLIMModel::getOtherParticipantID(const LLUUID& session_id) const { - if (0 == ids.getLength()) + LLIMSession* session = findIMSession(session_id); + if (!session) { + llwarns << "session " << session_id << "does not exist " << llendl; return LLUUID::null; } - LLUUID session_id = computeSessionID( - dialog, - other_participant_id); + return session->mOtherParticipantID; +} - LLFloaterIMPanel* floater = findFloaterBySession(session_id); - if(!floater) +EInstantMessage LLIMModel::getType(const LLUUID& session_id) const +{ + LLIMSession* session = findIMSession(session_id); + if (!session) { - // On creation, use the first element of ids as the - // "other_participant_id" - floater = createFloater( - session_id, - other_participant_id, - name, - ids, - dialog, - TRUE); + llwarns << "session " << session_id << "does not exist " << llendl; + return IM_COUNT; + } - if ( !floater ) return LLUUID::null; + return session->mType; +} - noteOfflineUsers(floater, ids); - LLFloaterChatterBox::showInstance(session_id); - } - else +LLVoiceChannel* LLIMModel::getVoiceChannel( const LLUUID& session_id ) const +{ + LLIMSession* session = findIMSession(session_id); + if (!session) { - floater->open(); + llwarns << "session " << session_id << "does not exist " << llendl; + return NULL; } - //mTabContainer->selectTabPanel(panel); - floater->setInputFocus(TRUE); - return floater->getSessionID(); + + return session->mVoiceChannel; } -// This removes the panel referenced by the uuid, and then restores -// internal consistency. The internal pointer is not deleted. -void LLIMMgr::removeSession(const LLUUID& session_id) +LLIMSpeakerMgr* LLIMModel::getSpeakerManager( const LLUUID& session_id ) const { - LLFloaterIMPanel* floater = findFloaterBySession(session_id); - if(floater) + LLIMSession* session = findIMSession(session_id); + if (!session) { - mFloaters.erase(floater->getHandle()); - LLFloaterChatterBox::getInstance(LLSD())->removeFloater(floater); - //mTabContainer->removeTabPanel(floater); - - clearPendingInviation(session_id); - clearPendingAgentListUpdates(session_id); + llwarns << "session " << session_id << " does not exist " << llendl; + return NULL; } + + return session->mSpeakers; } -void LLIMMgr::inviteToSession( - const LLUUID& session_id, - const LLString& session_name, - const LLUUID& caller_id, - const LLString& caller_name, - EInstantMessage type, - EInvitationType inv_type, - const LLString& session_handle) +const std::string& LLIMModel::getHistoryFileName(const LLUUID& session_id) const { - //ignore invites from muted residents - if (gMuteListp->isMuted(caller_id)) + LLIMSession* session = findIMSession(session_id); + if (!session) { - return; + llwarns << "session " << session_id << " does not exist " << llendl; + return LLStringUtil::null; } - LLString notify_box_type; + return session->mHistoryFileName; +} - BOOL ad_hoc_invite = FALSE; - if(type == IM_SESSION_P2P_INVITE) - { - //P2P is different...they only have voice invitations - notify_box_type = "VoiceInviteP2P"; - } - else if ( gAgent.isInGroup(session_id) ) - { - //only really old school groups have voice invitations - notify_box_type = "VoiceInviteGroup"; + +// TODO get rid of other participant ID +void LLIMModel::sendTypingState(LLUUID session_id, LLUUID other_participant_id, BOOL typing) +{ + std::string name; + LLAgentUI::buildFullname(name); + + pack_instant_message( + gMessageSystem, + gAgent.getID(), + FALSE, + gAgent.getSessionID(), + other_participant_id, + name, + std::string("typing"), + IM_ONLINE, + (typing ? IM_TYPING_START : IM_TYPING_STOP), + session_id); + gAgent.sendReliableMessage(); +} + +void LLIMModel::sendLeaveSession(const LLUUID& session_id, const LLUUID& other_participant_id) +{ + if(session_id.notNull()) + { + std::string name; + LLAgentUI::buildFullname(name); + pack_instant_message( + gMessageSystem, + gAgent.getID(), + FALSE, + gAgent.getSessionID(), + other_participant_id, + name, + LLStringUtil::null, + IM_ONLINE, + IM_SESSION_LEAVE, + session_id); + gAgent.sendReliableMessage(); } - else if ( inv_type == INVITATION_TYPE_VOICE ) +} + +//*TODO this method is better be moved to the LLIMMgr +void LLIMModel::sendMessage(const std::string& utf8_text, + const LLUUID& im_session_id, + const LLUUID& other_participant_id, + EInstantMessage dialog) +{ + std::string name; + bool sent = false; + LLAgentUI::buildFullname(name); + + const LLRelationship* info = NULL; + info = LLAvatarTracker::instance().getBuddyInfo(other_participant_id); + + U8 offline = (!info || info->isOnline()) ? IM_ONLINE : IM_OFFLINE; + + if((offline == IM_OFFLINE) && (LLVoiceClient::getInstance()->isOnlineSIP(other_participant_id))) { - //else it's an ad-hoc - //and a voice ad-hoc - notify_box_type = "VoiceInviteAdHoc"; - ad_hoc_invite = TRUE; + // User is online through the OOW connector, but not with a regular viewer. Try to send the message via SLVoice. + sent = LLVoiceClient::getInstance()->sendTextMessage(other_participant_id, utf8_text); } - else if ( inv_type == INVITATION_TYPE_IMMEDIATE ) + + if(!sent) { - notify_box_type = "InviteAdHoc"; - ad_hoc_invite = TRUE; + // Send message normally. + + // default to IM_SESSION_SEND unless it's nothing special - in + // which case it's probably an IM to everyone. + U8 new_dialog = dialog; + + if ( dialog != IM_NOTHING_SPECIAL ) + { + new_dialog = IM_SESSION_SEND; + } + pack_instant_message( + gMessageSystem, + gAgent.getID(), + FALSE, + gAgent.getSessionID(), + other_participant_id, + name.c_str(), + utf8_text.c_str(), + offline, + (EInstantMessage)new_dialog, + im_session_id); + gAgent.sendReliableMessage(); } - LLIMSessionInvite* invite = new LLIMSessionInvite( - session_id, - session_name, - caller_id, - caller_name, - type, - inv_type, - session_handle, - notify_box_type); - - LLVoiceChannel* channelp = LLVoiceChannel::getChannelByID(session_id); - if (channelp && channelp->callStarted()) + // If there is a mute list and this is not a group chat... + if ( LLMuteList::getInstance() ) { - // you have already started a call to the other user, so just accept the invite - inviteUserResponse(0, invite); - return; + // ... the target should not be in our mute list for some message types. + // Auto-remove them if present. + switch( dialog ) + { + case IM_NOTHING_SPECIAL: + case IM_GROUP_INVITATION: + case IM_INVENTORY_OFFERED: + case IM_SESSION_INVITE: + case IM_SESSION_P2P_INVITE: + case IM_SESSION_CONFERENCE_START: + case IM_SESSION_SEND: // This one is marginal - erring on the side of hearing. + case IM_LURE_USER: + case IM_GODLIKE_LURE_USER: + case IM_FRIENDSHIP_OFFERED: + LLMuteList::getInstance()->autoRemove(other_participant_id, LLMuteList::AR_IM); + break; + default: ; // do nothing + } } - if (type == IM_SESSION_P2P_INVITE || ad_hoc_invite) + if((dialog == IM_NOTHING_SPECIAL) && + (other_participant_id.notNull())) { - // is the inviter a friend? - if (LLAvatarTracker::instance().getBuddyInfo(caller_id) == NULL) + // Do we have to replace the /me's here? + std::string from; + LLAgentUI::buildFullname(from); + LLIMModel::getInstance()->addMessage(im_session_id, from, gAgentID, utf8_text); + + //local echo for the legacy communicate panel + std::string history_echo; + LLAgentUI::buildFullname(history_echo); + + history_echo += ": " + utf8_text; + + LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(im_session_id); + if (speaker_mgr) { - // if not, and we are ignoring voice invites from non-friends - // then silently decline - if (gSavedSettings.getBOOL("VoiceCallsFriendsOnly")) - { - // invite not from a friend, so decline - inviteUserResponse(1, invite); - return; - } + speaker_mgr->speakerChatted(gAgentID); + speaker_mgr->setSpeakerTyping(gAgentID, FALSE); } } - if ( !mPendingInvitations.has(session_id.asString()) ) + // Add the recipient to the recent people list. + bool is_not_group_id = LLGroupMgr::getInstance()->getGroupData(other_participant_id) == NULL; + + if (is_not_group_id) { - if (caller_name.empty()) + LLIMModel::LLIMSession* session = LLIMModel::getInstance()->findIMSession(im_session_id); + if( session == 0)//??? shouldn't really happen { - gCacheName->getName(caller_id, onInviteNameLookup, invite); + LLRecentPeople::instance().add(other_participant_id); } else { - LLString::format_map_t args; - args["[NAME]"] = caller_name; - args["[GROUP]"] = session_name; + // IM_SESSION_INVITE means that this is an Ad-hoc incoming chat + // (it can be also Group chat but it is checked above) + // In this case mInitialTargetIDs contains Ad-hoc session ID and it should not be added + // to Recent People to prevent showing of an item with (???)(???). See EXT-8246. + // Concrete participants will be added into this list once they sent message in chat. + if (IM_SESSION_INVITE == dialog) return; + // Add only online members to recent (EXT-8658) + addSpeakersToRecent(im_session_id); + } + } +} - LLNotifyBox::showXml(notify_box_type, - args, - inviteUserResponse, - (void*)invite); +void LLIMModel::addSpeakersToRecent(const LLUUID& im_session_id) +{ + LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(im_session_id); + LLSpeakerMgr::speaker_list_t speaker_list; + if(speaker_mgr != NULL) + { + speaker_mgr->getSpeakerList(&speaker_list, true); + } + for(LLSpeakerMgr::speaker_list_t::iterator it = speaker_list.begin(); it != speaker_list.end(); it++) + { + const LLPointer<LLSpeaker>& speakerp = *it; + LLRecentPeople::instance().add(speakerp->mID); + } +} - } - mPendingInvitations[session_id.asString()] = LLSD(); +void session_starter_helper( + const LLUUID& temp_session_id, + const LLUUID& other_participant_id, + EInstantMessage im_type) +{ + LLMessageSystem *msg = gMessageSystem; + + msg->newMessageFast(_PREHASH_ImprovedInstantMessage); + msg->nextBlockFast(_PREHASH_AgentData); + msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID()); + msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID()); + + msg->nextBlockFast(_PREHASH_MessageBlock); + msg->addBOOLFast(_PREHASH_FromGroup, FALSE); + msg->addUUIDFast(_PREHASH_ToAgentID, other_participant_id); + msg->addU8Fast(_PREHASH_Offline, IM_ONLINE); + msg->addU8Fast(_PREHASH_Dialog, im_type); + msg->addUUIDFast(_PREHASH_ID, temp_session_id); + msg->addU32Fast(_PREHASH_Timestamp, NO_TIMESTAMP); // no timestamp necessary + + std::string name; + LLAgentUI::buildFullname(name); + + msg->addStringFast(_PREHASH_FromAgentName, name); + msg->addStringFast(_PREHASH_Message, LLStringUtil::null); + msg->addU32Fast(_PREHASH_ParentEstateID, 0); + msg->addUUIDFast(_PREHASH_RegionID, LLUUID::null); + msg->addVector3Fast(_PREHASH_Position, gAgent.getPositionAgent()); +} + +void start_deprecated_conference_chat( + const LLUUID& temp_session_id, + const LLUUID& creator_id, + const LLUUID& other_participant_id, + const LLSD& agents_to_invite) +{ + U8* bucket; + U8* pos; + S32 count; + S32 bucket_size; + + // *FIX: this could suffer from endian issues + count = agents_to_invite.size(); + bucket_size = UUID_BYTES * count; + bucket = new U8[bucket_size]; + pos = bucket; + + for(S32 i = 0; i < count; ++i) + { + LLUUID agent_id = agents_to_invite[i].asUUID(); + + memcpy(pos, &agent_id, UUID_BYTES); + pos += UUID_BYTES; } + + session_starter_helper( + temp_session_id, + other_participant_id, + IM_SESSION_CONFERENCE_START); + + gMessageSystem->addBinaryDataFast( + _PREHASH_BinaryBucket, + bucket, + bucket_size); + + gAgent.sendReliableMessage(); + + delete[] bucket; } -//static -void LLIMMgr::onInviteNameLookup(const LLUUID& id, const char* first, const char* last, BOOL is_group, void* userdata) +class LLStartConferenceChatResponder : public LLHTTPClient::Responder +{ +public: + LLStartConferenceChatResponder( + const LLUUID& temp_session_id, + const LLUUID& creator_id, + const LLUUID& other_participant_id, + const LLSD& agents_to_invite) + { + mTempSessionID = temp_session_id; + mCreatorID = creator_id; + mOtherParticipantID = other_participant_id; + mAgents = agents_to_invite; + } + + virtual void error(U32 statusNum, const std::string& reason) + { + //try an "old school" way. + if ( statusNum == 400 ) + { + start_deprecated_conference_chat( + mTempSessionID, + mCreatorID, + mOtherParticipantID, + mAgents); + } + + //else throw an error back to the client? + //in theory we should have just have these error strings + //etc. set up in this file as opposed to the IMMgr, + //but the error string were unneeded here previously + //and it is not worth the effort switching over all + //the possible different language translations + } + +private: + LLUUID mTempSessionID; + LLUUID mCreatorID; + LLUUID mOtherParticipantID; + + LLSD mAgents; +}; + +// Returns true if any messages were sent, false otherwise. +// Is sort of equivalent to "does the server need to do anything?" +bool LLIMModel::sendStartSession( + const LLUUID& temp_session_id, + const LLUUID& other_participant_id, + const uuid_vec_t& ids, + EInstantMessage dialog) { - LLIMSessionInvite* invite = (LLIMSessionInvite*)userdata; + if ( dialog == IM_SESSION_GROUP_START ) + { + session_starter_helper( + temp_session_id, + other_participant_id, + dialog); + gMessageSystem->addBinaryDataFast( + _PREHASH_BinaryBucket, + EMPTY_BINARY_BUCKET, + EMPTY_BINARY_BUCKET_SIZE); + gAgent.sendReliableMessage(); + + return true; + } + else if ( dialog == IM_SESSION_CONFERENCE_START ) + { + LLSD agents; + for (int i = 0; i < (S32) ids.size(); i++) + { + agents.append(ids[i]); + } + + //we have a new way of starting conference calls now + LLViewerRegion* region = gAgent.getRegion(); + if (region) + { + std::string url = region->getCapability( + "ChatSessionRequest"); + LLSD data; + data["method"] = "start conference"; + data["session-id"] = temp_session_id; + + data["params"] = agents; - invite->mCallerName = llformat("%s %s", first, last); - invite->mSessionName = invite->mCallerName; + LLHTTPClient::post( + url, + data, + new LLStartConferenceChatResponder( + temp_session_id, + gAgent.getID(), + other_participant_id, + data["params"])); + } + else + { + start_deprecated_conference_chat( + temp_session_id, + gAgent.getID(), + other_participant_id, + agents); + } - LLString::format_map_t args; - args["[NAME]"] = invite->mCallerName; + //we also need to wait for reply from the server in case of ad-hoc chat (we'll get new session id) + return true; + } - LLNotifyBox::showXml( - invite->mNotifyBox, - args, - inviteUserResponse, - (void*)invite); + return false; } +// +// Helper Functions +// + class LLViewerChatterBoxInvitationAcceptResponder : public LLHTTPClient::Responder { @@ -814,10 +1279,8 @@ public: { if ( gIMMgr) { - LLFloaterIMPanel* floaterp = - gIMMgr->findFloaterBySession(mSessionID); - - if (floaterp) + LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); + if (speaker_mgr) { //we've accepted our invitation //and received a list of agents that were @@ -831,30 +1294,30 @@ public: //but unfortunately, our base that we are receiving here //may not be the most up to date. It was accurate at //some point in time though. - floaterp->setSpeakers(content); + speaker_mgr->setSpeakers(content); //we now have our base of users in the session //that was accurate at some point, but maybe not now //so now we apply all of the udpates we've received //in case of race conditions - floaterp->updateSpeakersList( - gIMMgr->getPendingAgentListUpdates(mSessionID)); + speaker_mgr->updateSpeakers(gIMMgr->getPendingAgentListUpdates(mSessionID)); + } - if ( mInvitiationType == LLIMMgr::INVITATION_TYPE_VOICE ) - { - floaterp->requestAutoConnect(); - LLFloaterIMPanel::onClickStartCall(floaterp); - // always open IM window when connecting to voice - LLFloaterChatterBox::showInstance(TRUE); - } - else if ( mInvitiationType == LLIMMgr::INVITATION_TYPE_IMMEDIATE ) - { - LLFloaterChatterBox::showInstance(TRUE); - } + if (LLIMMgr::INVITATION_TYPE_VOICE == mInvitiationType) + { + gIMMgr->startCall(mSessionID, LLVoiceChannel::INCOMING_CALL); + } + + if ((mInvitiationType == LLIMMgr::INVITATION_TYPE_VOICE + || mInvitiationType == LLIMMgr::INVITATION_TYPE_IMMEDIATE) + && LLIMModel::getInstance()->findIMSession(mSessionID)) + { + // TODO remove in 2010, for voice calls we do not open an IM window + //LLIMFloater::show(mSessionID); } gIMMgr->clearPendingAgentListUpdates(mSessionID); - gIMMgr->clearPendingInviation(mSessionID); + gIMMgr->clearPendingInvitation(mSessionID); } } @@ -864,26 +1327,12 @@ public: if ( gIMMgr ) { gIMMgr->clearPendingAgentListUpdates(mSessionID); - gIMMgr->clearPendingInviation(mSessionID); - - LLFloaterIMPanel* floaterp = - gIMMgr->findFloaterBySession(mSessionID); - - if ( floaterp ) + gIMMgr->clearPendingInvitation(mSessionID); + if ( 404 == statusNum ) { std::string error_string; - - if ( 404 == statusNum ) - { - error_string = "does not exist"; - } - else - { - error_string = "generic"; - } - - floaterp->showSessionStartError( - error_string); + error_string = "session_does_not_exist_error"; + gIMMgr->showSessionStartError(error_string, mSessionID); } } } @@ -893,78 +1342,932 @@ private: LLIMMgr::EInvitationType mInvitiationType; }; + +// the other_participant_id is either an agent_id, a group_id, or an inventory +// folder item_id (collection of calling cards) + +// static +LLUUID LLIMMgr::computeSessionID( + EInstantMessage dialog, + const LLUUID& other_participant_id) +{ + LLUUID session_id; + if (IM_SESSION_GROUP_START == dialog) + { + // slam group session_id to the group_id (other_participant_id) + session_id = other_participant_id; + } + else if (IM_SESSION_CONFERENCE_START == dialog) + { + session_id.generate(); + } + else if (IM_SESSION_INVITE == dialog) + { + // use provided session id for invites + session_id = other_participant_id; + } + else + { + LLUUID agent_id = gAgent.getID(); + if (other_participant_id == agent_id) + { + // if we try to send an IM to ourselves then the XOR would be null + // so we just make the session_id the same as the agent_id + session_id = agent_id; + } + else + { + // peer-to-peer or peer-to-asset session_id is the XOR + session_id = other_participant_id ^ agent_id; + } + } + return session_id; +} + +void +LLIMMgr::showSessionStartError( + const std::string& error_string, + const LLUUID session_id) +{ + if (!hasSession(session_id)) return; + + LLSD args; + args["REASON"] = LLTrans::getString(error_string); + args["RECIPIENT"] = LLIMModel::getInstance()->getName(session_id); + + LLSD payload; + payload["session_id"] = session_id; + + LLNotificationsUtil::add( + "ChatterBoxSessionStartError", + args, + payload, + LLIMMgr::onConfirmForceCloseError); +} + +void +LLIMMgr::showSessionEventError( + const std::string& event_string, + const std::string& error_string, + const LLUUID session_id) +{ + LLSD args; + LLStringUtil::format_map_t event_args; + + event_args["RECIPIENT"] = LLIMModel::getInstance()->getName(session_id); + + args["REASON"] = + LLTrans::getString(error_string); + args["EVENT"] = + LLTrans::getString(event_string, event_args); + + LLNotificationsUtil::add( + "ChatterBoxSessionEventError", + args); +} + +void +LLIMMgr::showSessionForceClose( + const std::string& reason_string, + const LLUUID session_id) +{ + if (!hasSession(session_id)) return; + + LLSD args; + + args["NAME"] = LLIMModel::getInstance()->getName(session_id); + args["REASON"] = LLTrans::getString(reason_string); + + LLSD payload; + payload["session_id"] = session_id; + + LLNotificationsUtil::add( + "ForceCloseChatterBoxSession", + args, + payload, + LLIMMgr::onConfirmForceCloseError); +} + //static -void LLIMMgr::inviteUserResponse(S32 option, void* user_data) +bool +LLIMMgr::onConfirmForceCloseError( + const LLSD& notification, + const LLSD& response) { - LLIMSessionInvite* invitep = (LLIMSessionInvite*)user_data; + //only 1 option really + LLUUID session_id = notification["payload"]["session_id"]; - switch(option) + LLFloater* floater = LLIMFloater::findInstance(session_id); + if ( floater ) + { + floater->closeFloater(FALSE); + } + return false; +} + + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Class LLCallDialogManager +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +LLCallDialogManager::LLCallDialogManager() +{ +} + +LLCallDialogManager::~LLCallDialogManager() +{ +} + +void LLCallDialogManager::initClass() +{ + LLVoiceChannel::setCurrentVoiceChannelChangedCallback(LLCallDialogManager::onVoiceChannelChanged); +} + +void LLCallDialogManager::onVoiceChannelChanged(const LLUUID &session_id) +{ + LLIMModel::LLIMSession* session = LLIMModel::getInstance()->findIMSession(session_id); + if(!session) + { + sPreviousSessionlName = sCurrentSessionlName; + sCurrentSessionlName = ""; // Empty string results in "Nearby Voice Chat" after substitution + return; + } + + if (sSession) { + // store previous session type to process Avaline calls in dialogs + sPreviousSessionType = sSession->mSessionType; + } + + sSession = session; + + static boost::signals2::connection prev_channel_state_changed_connection; + // disconnect previously connected callback to avoid have invalid sSession in onVoiceChannelStateChanged() + prev_channel_state_changed_connection.disconnect(); + prev_channel_state_changed_connection = + sSession->mVoiceChannel->setStateChangedCallback(boost::bind(LLCallDialogManager::onVoiceChannelStateChanged, _1, _2, _3, _4)); + + if(sCurrentSessionlName != session->mName) + { + sPreviousSessionlName = sCurrentSessionlName; + sCurrentSessionlName = session->mName; + } + + if (LLVoiceChannel::getCurrentVoiceChannel()->getState() == LLVoiceChannel::STATE_CALL_STARTED && + LLVoiceChannel::getCurrentVoiceChannel()->getCallDirection() == LLVoiceChannel::OUTGOING_CALL) + { + + //*TODO get rid of duplicated code + LLSD mCallDialogPayload; + mCallDialogPayload["session_id"] = sSession->mSessionID; + mCallDialogPayload["session_name"] = sSession->mName; + mCallDialogPayload["other_user_id"] = sSession->mOtherParticipantID; + mCallDialogPayload["old_channel_name"] = sPreviousSessionlName; + mCallDialogPayload["old_session_type"] = sPreviousSessionType; + mCallDialogPayload["state"] = LLVoiceChannel::STATE_CALL_STARTED; + mCallDialogPayload["disconnected_channel_name"] = sSession->mName; + mCallDialogPayload["session_type"] = sSession->mSessionType; + + LLOutgoingCallDialog* ocd = LLFloaterReg::getTypedInstance<LLOutgoingCallDialog>("outgoing_call", LLOutgoingCallDialog::OCD_KEY); + if(ocd) + { + ocd->show(mCallDialogPayload); + } + } + +} + +void LLCallDialogManager::onVoiceChannelStateChanged(const LLVoiceChannel::EState& old_state, const LLVoiceChannel::EState& new_state, const LLVoiceChannel::EDirection& direction, bool ended_by_agent) +{ + LLSD mCallDialogPayload; + LLOutgoingCallDialog* ocd = NULL; + + if(sOldState == new_state) + { + return; + } + + sOldState = new_state; + + mCallDialogPayload["session_id"] = sSession->mSessionID; + mCallDialogPayload["session_name"] = sSession->mName; + mCallDialogPayload["other_user_id"] = sSession->mOtherParticipantID; + mCallDialogPayload["old_channel_name"] = sPreviousSessionlName; + mCallDialogPayload["old_session_type"] = sPreviousSessionType; + mCallDialogPayload["state"] = new_state; + mCallDialogPayload["disconnected_channel_name"] = sSession->mName; + mCallDialogPayload["session_type"] = sSession->mSessionType; + mCallDialogPayload["ended_by_agent"] = ended_by_agent; + + switch(new_state) + { + case LLVoiceChannel::STATE_CALL_STARTED : + // do not show "Calling to..." if it is incoming call + if(direction == LLVoiceChannel::INCOMING_CALL) + { + return; + } + break; + + case LLVoiceChannel::STATE_HUNG_UP: + // this state is coming before session is changed, so, put it into payload map + mCallDialogPayload["old_session_type"] = sSession->mSessionType; + break; + + case LLVoiceChannel::STATE_CONNECTED : + ocd = LLFloaterReg::findTypedInstance<LLOutgoingCallDialog>("outgoing_call", LLOutgoingCallDialog::OCD_KEY); + if (ocd) + { + ocd->closeFloater(); + } + return; + + default: + break; + } + + ocd = LLFloaterReg::getTypedInstance<LLOutgoingCallDialog>("outgoing_call", LLOutgoingCallDialog::OCD_KEY); + if(ocd) + { + ocd->show(mCallDialogPayload); + } +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Class LLCallDialog +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LLCallDialog::LLCallDialog(const LLSD& payload) + : LLDockableFloater(NULL, false, payload), + + mPayload(payload), + mLifetime(DEFAULT_LIFETIME) +{ + setAutoFocus(FALSE); + // force docked state since this floater doesn't save it between recreations + setDocked(true); +} + +LLCallDialog::~LLCallDialog() +{ + LLUI::removePopup(this); +} + +void LLCallDialog::getAllowedRect(LLRect& rect) +{ + rect = gViewerWindow->getWorldViewRectScaled(); +} + +BOOL LLCallDialog::postBuild() +{ + if (!LLDockableFloater::postBuild()) + return FALSE; + + // dock the dialog to the Speak Button, where other sys messages appear + LLView *anchor_panel = LLBottomTray::getInstance()->getChild<LLView>("speak_panel"); + + setDockControl(new LLDockControl( + anchor_panel, this, + getDockTongue(), LLDockControl::TOP, + boost::bind(&LLCallDialog::getAllowedRect, this, _1))); + + return TRUE; +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Class LLOutgoingCallDialog +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LLOutgoingCallDialog::LLOutgoingCallDialog(const LLSD& payload) : +LLCallDialog(payload) +{ + LLOutgoingCallDialog* instance = LLFloaterReg::findTypedInstance<LLOutgoingCallDialog>("outgoing_call", LLOutgoingCallDialog::OCD_KEY); + if(instance && instance->getVisible()) + { + instance->onCancel(instance); + } +} + +void LLCallDialog::draw() +{ + if (lifetimeHasExpired()) + { + onLifetimeExpired(); + } + + if (getDockControl() != NULL) + { + LLDockableFloater::draw(); + } +} + +// virtual +void LLCallDialog::onOpen(const LLSD& key) +{ + LLDockableFloater::onOpen(key); + + // it should be over the all floaters. EXT-5116 + LLUI::addPopup(this); +} + +void LLCallDialog::setIcon(const LLSD& session_id, const LLSD& participant_id) +{ + // *NOTE: 12/28/2009: check avaline calls: LLVoiceClient::isParticipantAvatar returns false for them + bool participant_is_avatar = LLVoiceClient::getInstance()->isParticipantAvatar(session_id); + + bool is_group = participant_is_avatar && gAgent.isInGroup(session_id); + + LLAvatarIconCtrl* avatar_icon = getChild<LLAvatarIconCtrl>("avatar_icon"); + LLGroupIconCtrl* group_icon = getChild<LLGroupIconCtrl>("group_icon"); + + avatar_icon->setVisible(!is_group); + group_icon->setVisible(is_group); + + if (is_group) + { + group_icon->setValue(session_id); + } + else if (participant_is_avatar) + { + avatar_icon->setValue(participant_id); + } + else + { + avatar_icon->setValue("Avaline_Icon"); + avatar_icon->setToolTip(std::string("")); + } +} + +bool LLCallDialog::lifetimeHasExpired() +{ + if (mLifetimeTimer.getStarted()) + { + F32 elapsed_time = mLifetimeTimer.getElapsedTimeF32(); + if (elapsed_time > mLifetime) + { + return true; + } + } + return false; +} + +void LLCallDialog::onLifetimeExpired() +{ + mLifetimeTimer.stop(); + closeFloater(); +} + +void LLOutgoingCallDialog::show(const LLSD& key) +{ + mPayload = key; + + //will be false only if voice in parcel is disabled and channel we leave is nearby(checked further) + bool show_oldchannel = LLViewerParcelMgr::getInstance()->allowAgentVoice(); + + // hide all text at first + hideAllText(); + + // init notification's lifetime + std::istringstream ss( getString("lifetime") ); + if (!(ss >> mLifetime)) + { + mLifetime = DEFAULT_LIFETIME; + } + + // customize text strings + // tell the user which voice channel they are leaving + if (!mPayload["old_channel_name"].asString().empty()) + { + bool was_avaline_call = LLIMModel::LLIMSession::AVALINE_SESSION == mPayload["old_session_type"].asInteger(); + + std::string old_caller_name = mPayload["old_channel_name"].asString(); + if (was_avaline_call) + { + old_caller_name = LLTextUtil::formatPhoneNumber(old_caller_name); + } + + getChild<LLUICtrl>("leaving")->setTextArg("[CURRENT_CHAT]", old_caller_name); + show_oldchannel = true; + } + else + { + getChild<LLUICtrl>("leaving")->setTextArg("[CURRENT_CHAT]", getString("localchat")); + } + + if (!mPayload["disconnected_channel_name"].asString().empty()) + { + std::string channel_name = mPayload["disconnected_channel_name"].asString(); + if (LLIMModel::LLIMSession::AVALINE_SESSION == mPayload["session_type"].asInteger()) + { + channel_name = LLTextUtil::formatPhoneNumber(channel_name); + } + getChild<LLUICtrl>("nearby")->setTextArg("[VOICE_CHANNEL_NAME]", channel_name); + + // skipping "You will now be reconnected to nearby" in notification when call is ended by disabling voice, + // so no reconnection to nearby chat happens (EXT-4397) + bool voice_works = LLVoiceClient::getInstance()->voiceEnabled() && LLVoiceClient::getInstance()->isVoiceWorking(); + std::string reconnect_nearby = voice_works ? LLTrans::getString("reconnect_nearby") : std::string(); + getChild<LLUICtrl>("nearby")->setTextArg("[RECONNECT_NEARBY]", reconnect_nearby); + + const std::string& nearby_str = mPayload["ended_by_agent"] ? NEARBY_P2P_BY_AGENT : NEARBY_P2P_BY_OTHER; + getChild<LLUICtrl>(nearby_str)->setTextArg("[RECONNECT_NEARBY]", reconnect_nearby); + } + + std::string callee_name = mPayload["session_name"].asString(); + + LLUUID session_id = mPayload["session_id"].asUUID(); + bool is_avatar = LLVoiceClient::getInstance()->isParticipantAvatar(session_id); + + if (callee_name == "anonymous") + { + callee_name = getString("anonymous"); + } + else if (!is_avatar) + { + callee_name = LLTextUtil::formatPhoneNumber(callee_name); + } + + LLSD callee_id = mPayload["other_user_id"]; + // Beautification: Since you know who you called, just show display name + std::string title = callee_name; + std::string final_callee_name = callee_name; + if (mPayload["session_type"].asInteger() == LLIMModel::LLIMSession::P2P_SESSION) + { + LLAvatarName av_name; + if (LLAvatarNameCache::get(callee_id, &av_name)) + { + final_callee_name = av_name.mDisplayName; + title = av_name.getCompleteName(); + } + } + getChild<LLUICtrl>("calling")->setTextArg("[CALLEE_NAME]", final_callee_name); + getChild<LLUICtrl>("connecting")->setTextArg("[CALLEE_NAME]", final_callee_name); + + setTitle(title); + + // for outgoing group calls callee_id == group id == session id + setIcon(callee_id, callee_id); + + // stop timer by default + mLifetimeTimer.stop(); + + // show only necessary strings and controls + switch(mPayload["state"].asInteger()) + { + case LLVoiceChannel::STATE_CALL_STARTED : + getChild<LLTextBox>("calling")->setVisible(true); + getChild<LLButton>("Cancel")->setVisible(true); + if(show_oldchannel) + { + getChild<LLTextBox>("leaving")->setVisible(true); + } + break; + // STATE_READY is here to show appropriate text for ad-hoc and group calls when floater is shown(EXT-6893) + case LLVoiceChannel::STATE_READY : + case LLVoiceChannel::STATE_RINGING : + if(show_oldchannel) + { + getChild<LLTextBox>("leaving")->setVisible(true); + } + getChild<LLTextBox>("connecting")->setVisible(true); + break; + case LLVoiceChannel::STATE_ERROR : + getChild<LLTextBox>("noanswer")->setVisible(true); + getChild<LLButton>("Cancel")->setVisible(false); + setCanClose(true); + mLifetimeTimer.start(); + break; + case LLVoiceChannel::STATE_HUNG_UP : + if (mPayload["session_type"].asInteger() == LLIMModel::LLIMSession::P2P_SESSION) + { + const std::string& nearby_str = mPayload["ended_by_agent"] ? NEARBY_P2P_BY_AGENT : NEARBY_P2P_BY_OTHER; + getChild<LLTextBox>(nearby_str)->setVisible(true); + } + else + { + getChild<LLTextBox>("nearby")->setVisible(true); + } + getChild<LLButton>("Cancel")->setVisible(false); + setCanClose(true); + mLifetimeTimer.start(); + } + + openFloater(LLOutgoingCallDialog::OCD_KEY); +} + +void LLOutgoingCallDialog::hideAllText() +{ + getChild<LLTextBox>("calling")->setVisible(false); + getChild<LLTextBox>("leaving")->setVisible(false); + getChild<LLTextBox>("connecting")->setVisible(false); + getChild<LLTextBox>("nearby_P2P_by_other")->setVisible(false); + getChild<LLTextBox>("nearby_P2P_by_agent")->setVisible(false); + getChild<LLTextBox>("nearby")->setVisible(false); + getChild<LLTextBox>("noanswer")->setVisible(false); +} + +//static +void LLOutgoingCallDialog::onCancel(void* user_data) +{ + LLOutgoingCallDialog* self = (LLOutgoingCallDialog*)user_data; + + if (!gIMMgr) + return; + + LLUUID session_id = self->mPayload["session_id"].asUUID(); + gIMMgr->endCall(session_id); + + self->closeFloater(); +} + + +BOOL LLOutgoingCallDialog::postBuild() +{ + BOOL success = LLCallDialog::postBuild(); + + childSetAction("Cancel", onCancel, this); + + setCanDrag(FALSE); + + return success; +} + + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Class LLIncomingCallDialog +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +LLIncomingCallDialog::LLIncomingCallDialog(const LLSD& payload) : +LLCallDialog(payload) +{ +} + +void LLIncomingCallDialog::onLifetimeExpired() +{ + std::string session_handle = mPayload["session_handle"].asString(); + if (LLVoiceClient::getInstance()->isValidChannel(session_handle)) + { + // restart notification's timer if call is still valid + mLifetimeTimer.start(); + } + else + { + // close invitation if call is already not valid + mLifetimeTimer.stop(); + LLUUID session_id = mPayload["session_id"].asUUID(); + gIMMgr->clearPendingAgentListUpdates(session_id); + gIMMgr->clearPendingInvitation(session_id); + closeFloater(); + } +} + +BOOL LLIncomingCallDialog::postBuild() +{ + LLCallDialog::postBuild(); + + LLUUID session_id = mPayload["session_id"].asUUID(); + LLSD caller_id = mPayload["caller_id"]; + std::string caller_name = mPayload["caller_name"].asString(); + + // init notification's lifetime + std::istringstream ss( getString("lifetime") ); + if (!(ss >> mLifetime)) + { + mLifetime = DEFAULT_LIFETIME; + } + + std::string call_type; + if (gAgent.isInGroup(session_id)) + { + LLStringUtil::format_map_t args; + LLGroupData data; + if (gAgent.getGroupData(session_id, data)) + { + args["[GROUP]"] = data.mName; + call_type = getString(mPayload["notify_box_type"], args); + } + } + else + { + call_type = getString(mPayload["notify_box_type"]); + } + + + // check to see if this is an Avaline call + bool is_avatar = LLVoiceClient::getInstance()->isParticipantAvatar(session_id); + if (caller_name == "anonymous") + { + caller_name = getString("anonymous"); + setCallerName(caller_name, caller_name, call_type); + } + else if (!is_avatar) + { + caller_name = LLTextUtil::formatPhoneNumber(caller_name); + setCallerName(caller_name, caller_name, call_type); + } + else + { + // Get the full name information + LLAvatarNameCache::get(caller_id, + boost::bind(&LLIncomingCallDialog::onAvatarNameCache, + this, _1, _2, call_type)); + } + + setIcon(session_id, caller_id); + + childSetAction("Accept", onAccept, this); + childSetAction("Reject", onReject, this); + childSetAction("Start IM", onStartIM, this); + setDefaultBtn("Accept"); + + std::string notify_box_type = mPayload["notify_box_type"].asString(); + if(notify_box_type != "VoiceInviteGroup" && notify_box_type != "VoiceInviteAdHoc") + { + // starting notification's timer for P2P and AVALINE invitations + mLifetimeTimer.start(); + } + else + { + mLifetimeTimer.stop(); + } + + //it's not possible to connect to existing Ad-Hoc/Group chat through incoming ad-hoc call + //and no IM for avaline + getChildView("Start IM")->setVisible( is_avatar && notify_box_type != "VoiceInviteAdHoc" && notify_box_type != "VoiceInviteGroup"); + + setCanDrag(FALSE); + + return TRUE; +} + +void LLIncomingCallDialog::setCallerName(const std::string& ui_title, + const std::string& ui_label, + const std::string& call_type) +{ + setTitle(ui_title); + + // call_type may be a string like " is calling." + LLUICtrl* caller_name_widget = getChild<LLUICtrl>("caller name"); + caller_name_widget->setValue(ui_label + " " + call_type); +} + +void LLIncomingCallDialog::onAvatarNameCache(const LLUUID& agent_id, + const LLAvatarName& av_name, + const std::string& call_type) +{ + std::string title = av_name.getCompleteName(); + setCallerName(title, av_name.mDisplayName, call_type); +} + +void LLIncomingCallDialog::onOpen(const LLSD& key) +{ + LLCallDialog::onOpen(key); + + LLStringUtil::format_map_t args; + LLGroupData data; + // if it's a group call, retrieve group name to use it in question + if (gAgent.getGroupData(key["session_id"].asUUID(), data)) + { + args["[GROUP]"] = data.mName; + } + // tell the user which voice channel they would be leaving + LLVoiceChannel *voice = LLVoiceChannel::getCurrentVoiceChannel(); + if (voice && !voice->getSessionName().empty()) + { + args["[CURRENT_CHAT]"] = voice->getSessionName(); + getChild<LLUICtrl>("question")->setValue(getString(key["question_type"].asString(), args)); + } + else + { + args["[CURRENT_CHAT]"] = getString("localchat"); + getChild<LLUICtrl>("question")->setValue(getString(key["question_type"].asString(), args)); + } +} + +//static +void LLIncomingCallDialog::onAccept(void* user_data) +{ + LLIncomingCallDialog* self = (LLIncomingCallDialog*)user_data; + self->processCallResponse(0); + self->closeFloater(); +} + +//static +void LLIncomingCallDialog::onReject(void* user_data) +{ + LLIncomingCallDialog* self = (LLIncomingCallDialog*)user_data; + self->processCallResponse(1); + self->closeFloater(); +} + +//static +void LLIncomingCallDialog::onStartIM(void* user_data) +{ + LLIncomingCallDialog* self = (LLIncomingCallDialog*)user_data; + self->processCallResponse(2); + self->closeFloater(); +} + +void LLIncomingCallDialog::processCallResponse(S32 response) +{ + if (!gIMMgr || gDisconnected) + return; + + LLUUID session_id = mPayload["session_id"].asUUID(); + LLUUID caller_id = mPayload["caller_id"].asUUID(); + std::string session_name = mPayload["session_name"].asString(); + EInstantMessage type = (EInstantMessage)mPayload["type"].asInteger(); + LLIMMgr::EInvitationType inv_type = (LLIMMgr::EInvitationType)mPayload["inv_type"].asInteger(); + bool voice = true; + switch(response) + { + case 2: // start IM: just don't start the voice chat + { + voice = false; + /* FALLTHROUGH */ + } case 0: // accept + { + if (type == IM_SESSION_P2P_INVITE) { - if (invitep->mType == IM_SESSION_P2P_INVITE) + // create a normal IM session + session_id = gIMMgr->addP2PSession( + session_name, + caller_id, + mPayload["session_handle"].asString(), + mPayload["session_uri"].asString()); + + if (voice) { - // create a normal IM session - invitep->mSessionID = gIMMgr->addP2PSession( - invitep->mSessionName, - invitep->mCallerID, - invitep->mSessionHandle); - - LLFloaterIMPanel* im_floater = - gIMMgr->findFloaterBySession( - invitep->mSessionID); - if (im_floater) + gIMMgr->startCall(session_id, LLVoiceChannel::INCOMING_CALL); + } + + gIMMgr->clearPendingAgentListUpdates(session_id); + gIMMgr->clearPendingInvitation(session_id); + } + else + { + //session name should not be empty, but it can contain spaces so we don't trim + std::string correct_session_name = session_name; + if (session_name.empty()) + { + llwarns << "Received an empty session name from a server" << llendl; + + switch(type){ + case IM_SESSION_CONFERENCE_START: + case IM_SESSION_GROUP_START: + case IM_SESSION_INVITE: + if (gAgent.isInGroup(session_id)) + { + LLGroupData data; + if (!gAgent.getGroupData(session_id, data)) break; + correct_session_name = data.mName; + } + else + { + // *NOTE: really should be using callbacks here + LLAvatarName av_name; + if (LLAvatarNameCache::get(caller_id, &av_name)) + { + correct_session_name = av_name.mDisplayName + " (" + av_name.mUsername + ")"; + correct_session_name.append(ADHOC_NAME_SUFFIX); + } + } + llinfos << "Corrected session name is " << correct_session_name << llendl; + break; + default: + llwarning("Received an empty session name from a server and failed to generate a new proper session name", 0); + break; + } + } + + LLUUID new_session_id = gIMMgr->addSession(correct_session_name, type, session_id, true); + + std::string url = gAgent.getRegion()->getCapability( + "ChatSessionRequest"); + + if (voice) + { + LLSD data; + data["method"] = "accept invitation"; + data["session-id"] = session_id; + LLHTTPClient::post( + url, + data, + new LLViewerChatterBoxInvitationAcceptResponder( + session_id, + inv_type)); + + // send notification message to the corresponding chat + if (mPayload["notify_box_type"].asString() == "VoiceInviteGroup" || mPayload["notify_box_type"].asString() == "VoiceInviteAdHoc") { - im_floater->requestAutoConnect(); - LLFloaterIMPanel::onClickStartCall(im_floater); - // always open IM window when connecting to voice - LLFloaterChatterBox::showInstance(invitep->mSessionID); + LLStringUtil::format_map_t string_args; + string_args["[NAME]"] = mPayload["caller_name"].asString(); + std::string message = LLTrans::getString("name_started_call", string_args); + LLIMModel::getInstance()->addMessageSilently(session_id, SYSTEM_FROM, LLUUID::null, message); } + } + } + if (voice) + { + break; + } + } + case 1: // decline + { + if (type == IM_SESSION_P2P_INVITE) + { + if(LLVoiceClient::getInstance()) + { + std::string s = mPayload["session_handle"].asString(); + LLVoiceClient::getInstance()->declineInvite(s); + } + } + else + { + std::string url = gAgent.getRegion()->getCapability( + "ChatSessionRequest"); + + LLSD data; + data["method"] = "decline invitation"; + data["session-id"] = session_id; + LLHTTPClient::post( + url, + data, + NULL); + } + } + + gIMMgr->clearPendingAgentListUpdates(session_id); + gIMMgr->clearPendingInvitation(session_id); + } +} - gIMMgr->clearPendingAgentListUpdates(invitep->mSessionID); - gIMMgr->clearPendingInviation(invitep->mSessionID); +bool inviteUserResponse(const LLSD& notification, const LLSD& response) +{ + if (!gIMMgr) + return false; + + const LLSD& payload = notification["payload"]; + LLUUID session_id = payload["session_id"].asUUID(); + EInstantMessage type = (EInstantMessage)payload["type"].asInteger(); + LLIMMgr::EInvitationType inv_type = (LLIMMgr::EInvitationType)payload["inv_type"].asInteger(); + S32 option = LLNotificationsUtil::getSelectedOption(notification, response); + switch(option) + { + case 0: // accept + { + if (type == IM_SESSION_P2P_INVITE) + { + // create a normal IM session + session_id = gIMMgr->addP2PSession( + payload["session_name"].asString(), + payload["caller_id"].asUUID(), + payload["session_handle"].asString(), + payload["session_uri"].asString()); + + gIMMgr->startCall(session_id); + + gIMMgr->clearPendingAgentListUpdates(session_id); + gIMMgr->clearPendingInvitation(session_id); } else { - gIMMgr->addSession( - invitep->mSessionName, - invitep->mType, - invitep->mSessionID); + LLUUID new_session_id = gIMMgr->addSession( + payload["session_name"].asString(), + type, + session_id, true); std::string url = gAgent.getRegion()->getCapability( "ChatSessionRequest"); LLSD data; data["method"] = "accept invitation"; - data["session-id"] = invitep->mSessionID; + data["session-id"] = session_id; LLHTTPClient::post( url, data, new LLViewerChatterBoxInvitationAcceptResponder( - invitep->mSessionID, - invitep->mInvType)); + session_id, + inv_type)); } } break; case 2: // mute (also implies ignore, so this falls through to the "ignore" case below) { // mute the sender of this invite - if (!gMuteListp->isMuted(invitep->mCallerID)) + if (!LLMuteList::getInstance()->isMuted(payload["caller_id"].asUUID())) { - LLMute mute(invitep->mCallerID, invitep->mCallerName, LLMute::AGENT); - gMuteListp->add(mute); + LLMute mute(payload["caller_id"].asUUID(), payload["caller_name"].asString(), LLMute::AGENT); + LLMuteList::getInstance()->add(mute); } } /* FALLTHROUGH */ case 1: // decline { - if (invitep->mType == IM_SESSION_P2P_INVITE) + if (type == IM_SESSION_P2P_INVITE) { - if(gVoiceClient) - { - gVoiceClient->declineInvite(invitep->mSessionHandle); - } + std::string s = payload["session_handle"].asString(); + LLVoiceClient::getInstance()->declineInvite(s); } else { @@ -973,7 +2276,7 @@ void LLIMMgr::inviteUserResponse(S32 option, void* user_data) LLSD data; data["method"] = "decline invitation"; - data["session-id"] = invitep->mSessionID; + data["session-id"] = session_id; LLHTTPClient::post( url, data, @@ -981,86 +2284,403 @@ void LLIMMgr::inviteUserResponse(S32 option, void* user_data) } } - gIMMgr->clearPendingAgentListUpdates(invitep->mSessionID); - gIMMgr->clearPendingInviation(invitep->mSessionID); + gIMMgr->clearPendingAgentListUpdates(session_id); + gIMMgr->clearPendingInvitation(session_id); break; } - delete invitep; + return false; +} + +// +// Member Functions +// + +LLIMMgr::LLIMMgr() +{ + mPendingInvitations = LLSD::emptyMap(); + mPendingAgentListUpdates = LLSD::emptyMap(); + + LLIMModel::getInstance()->addNewMsgCallback(boost::bind(&LLIMFloater::sRemoveTypingIndicator, _1)); +} + +// Add a message to a session. +void LLIMMgr::addMessage( + const LLUUID& session_id, + const LLUUID& target_id, + const std::string& from, + const std::string& msg, + const std::string& session_name, + EInstantMessage dialog, + U32 parent_estate_id, + const LLUUID& region_id, + const LLVector3& position, + bool link_name) // If this is true, then we insert the name and link it to a profile +{ + LLUUID other_participant_id = target_id; + + // don't process muted IMs + if (LLMuteList::getInstance()->isMuted( + other_participant_id, + LLMute::flagTextChat) && !LLMuteList::getInstance()->isLinden(from)) + { + return; + } + + LLUUID new_session_id = session_id; + if (new_session_id.isNull()) + { + //no session ID...compute new one + new_session_id = computeSessionID(dialog, other_participant_id); + } + + //*NOTE session_name is empty in case of incoming P2P sessions + std::string fixed_session_name = from; + if(!session_name.empty() && session_name.size()>1) + { + fixed_session_name = session_name; + } + + bool new_session = !hasSession(new_session_id); + if (new_session) + { + LLIMModel::getInstance()->newSession(new_session_id, fixed_session_name, dialog, other_participant_id); + + // When we get a new IM, and if you are a god, display a bit + // of information about the source. This is to help liaisons + // when answering questions. + if(gAgent.isGodlike()) + { + // *TODO:translate (low priority, god ability) + std::ostringstream bonus_info; + bonus_info << LLTrans::getString("***")+ " "+ LLTrans::getString("IMParentEstate") + ":" + " " + << parent_estate_id + << ((parent_estate_id == 1) ? "," + LLTrans::getString("IMMainland") : "") + << ((parent_estate_id == 5) ? "," + LLTrans::getString ("IMTeen") : ""); + + // once we have web-services (or something) which returns + // information about a region id, we can print this out + // and even have it link to map-teleport or something. + //<< "*** region_id: " << region_id << std::endl + //<< "*** position: " << position << std::endl; + + LLIMModel::instance().addMessage(new_session_id, from, other_participant_id, bonus_info.str()); + } + + make_ui_sound("UISndNewIncomingIMSession"); + } + + LLIMModel::instance().addMessage(new_session_id, from, other_participant_id, msg); +} + +void LLIMMgr::addSystemMessage(const LLUUID& session_id, const std::string& message_name, const LLSD& args) +{ + LLUIString message; + + // null session id means near me (chat history) + if (session_id.isNull()) + { + message = LLTrans::getString(message_name); + message.setArgs(args); + + LLChat chat(message); + chat.mSourceType = CHAT_SOURCE_SYSTEM; + + LLNearbyChat* nearby_chat = LLFloaterReg::getTypedInstance<LLNearbyChat>("nearby_chat", LLSD()); + if(nearby_chat) + { + nearby_chat->addMessage(chat); + } + } + else // going to IM session + { + message = LLTrans::getString(message_name + "-im"); + message.setArgs(args); + if (hasSession(session_id)) + { + gIMMgr->addMessage(session_id, LLUUID::null, SYSTEM_FROM, message.getString()); + } + // log message to file + else + { + std::string session_name; + // since we select user to share item with - his name is already in cache + gCacheName->getFullName(args["user_id"], session_name); + LLIMModel::instance().logToFile(session_name, SYSTEM_FROM, LLUUID::null, message.getString()); + } + } +} + +S32 LLIMMgr::getNumberOfUnreadIM() +{ + std::map<LLUUID, LLIMModel::LLIMSession*>::iterator it; + + S32 num = 0; + for(it = LLIMModel::getInstance()->mId2SessionMap.begin(); it != LLIMModel::getInstance()->mId2SessionMap.end(); ++it) + { + num += (*it).second->mNumUnread; + } + + return num; } -void LLIMMgr::refresh() +S32 LLIMMgr::getNumberOfUnreadParticipantMessages() { + std::map<LLUUID, LLIMModel::LLIMSession*>::iterator it; + + S32 num = 0; + for(it = LLIMModel::getInstance()->mId2SessionMap.begin(); it != LLIMModel::getInstance()->mId2SessionMap.end(); ++it) + { + num += (*it).second->mParticipantUnreadMessageCount; + } + + return num; } -void LLIMMgr::setFloaterOpen(BOOL set_open) +void LLIMMgr::autoStartCallOnStartup(const LLUUID& session_id) { - if (set_open) + LLIMModel::LLIMSession *session = LLIMModel::getInstance()->findIMSession(session_id); + if (!session) return; + + if (session->mSessionInitialized) { - LLFloaterChatterBox::showInstance(); + startCall(session_id); } else { - LLFloaterChatterBox::hideInstance(); - } + session->mStartCallOnInitialize = true; + } } +LLUUID LLIMMgr::addP2PSession(const std::string& name, + const LLUUID& other_participant_id, + const std::string& voice_session_handle, + const std::string& caller_uri) +{ + LLUUID session_id = addSession(name, IM_NOTHING_SPECIAL, other_participant_id, true); + + LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(session_id); + if (speaker_mgr) + { + LLVoiceChannelP2P* voice_channel = dynamic_cast<LLVoiceChannelP2P*>(speaker_mgr->getVoiceChannel()); + if (voice_channel) + { + voice_channel->setSessionHandle(voice_session_handle, caller_uri); + } + } + return session_id; +} -BOOL LLIMMgr::getFloaterOpen() +// This adds a session to the talk view. The name is the local name of +// the session, dialog specifies the type of session. If the session +// exists, it is brought forward. Specifying id = NULL results in an +// im session to everyone. Returns the uuid of the session. +LLUUID LLIMMgr::addSession( + const std::string& name, + EInstantMessage dialog, + const LLUUID& other_participant_id, bool voice) { - return LLFloaterChatterBox::instanceVisible(LLSD()); + LLDynamicArray<LLUUID> ids; + ids.put(other_participant_id); + return addSession(name, dialog, other_participant_id, ids, voice); } - -void LLIMMgr::disconnectAllSessions() + +// Adds a session using the given session_id. If the session already exists +// the dialog type is assumed correct. Returns the uuid of the session. +LLUUID LLIMMgr::addSession( + const std::string& name, + EInstantMessage dialog, + const LLUUID& other_participant_id, + const LLDynamicArray<LLUUID>& ids, bool voice) { - LLFloaterIMPanel* floater = NULL; - std::set<LLHandle<LLFloater> >::iterator handle_it; - for(handle_it = mFloaters.begin(); - handle_it != mFloaters.end(); - ) + if (0 == ids.getLength()) { - floater = (LLFloaterIMPanel*)handle_it->get(); + return LLUUID::null; + } - // MUST do this BEFORE calling floater->onClose() because that may remove the item from the set, causing the subsequent increment to crash. - ++handle_it; + if (name.empty()) + { + llwarning("Session name cannot be null!", 0); + return LLUUID::null; + } + + LLUUID session_id = computeSessionID(dialog,other_participant_id); + + bool new_session = !LLIMModel::getInstance()->findIMSession(session_id); - if (floater) + //works only for outgoing ad-hoc sessions + if (new_session && IM_SESSION_CONFERENCE_START == dialog && ids.size()) + { + LLIMModel::LLIMSession* ad_hoc_found = LLIMModel::getInstance()->findAdHocIMSession(ids); + if (ad_hoc_found) { - floater->setEnabled(FALSE); - floater->close(TRUE); + new_session = false; + session_id = ad_hoc_found->mSessionID; } } + + if (new_session) + { + LLIMModel::getInstance()->newSession(session_id, name, dialog, other_participant_id, ids, voice); + } + + //we don't need to show notes about online/offline, mute/unmute users' statuses for existing sessions + if (!new_session) return session_id; + + //Per Plan's suggestion commented "explicit offline status warning" out to make Dessie happier (see EXT-3609) + //*TODO After February 2010 remove this commented out line if no one will be missing that warning + //noteOfflineUsers(session_id, floater, ids); + + // Only warn for regular IMs - not group IMs + if( dialog == IM_NOTHING_SPECIAL ) + { + noteMutedUsers(session_id, ids); + } + + return session_id; } +bool LLIMMgr::leaveSession(const LLUUID& session_id) +{ + LLIMModel::LLIMSession* im_session = LLIMModel::getInstance()->findIMSession(session_id); + if (!im_session) return false; + + LLIMModel::getInstance()->sendLeaveSession(session_id, im_session->mOtherParticipantID); + gIMMgr->removeSession(session_id); + return true; +} -// This method returns the im panel corresponding to the uuid -// provided. The uuid can either be a session id or an agent -// id. Returns NULL if there is no matching panel. -LLFloaterIMPanel* LLIMMgr::findFloaterBySession(const LLUUID& session_id) +// Removes data associated with a particular session specified by session_id +void LLIMMgr::removeSession(const LLUUID& session_id) +{ + llassert_always(hasSession(session_id)); + + clearPendingInvitation(session_id); + clearPendingAgentListUpdates(session_id); + + LLIMModel::getInstance()->clearSession(session_id); + + notifyObserverSessionRemoved(session_id); +} + +void LLIMMgr::inviteToSession( + const LLUUID& session_id, + const std::string& session_name, + const LLUUID& caller_id, + const std::string& caller_name, + EInstantMessage type, + EInvitationType inv_type, + const std::string& session_handle, + const std::string& session_uri) { - LLFloaterIMPanel* rv = NULL; - std::set<LLHandle<LLFloater> >::iterator handle_it; - for(handle_it = mFloaters.begin(); - handle_it != mFloaters.end(); - ++handle_it) + //ignore invites from muted residents + if (LLMuteList::getInstance()->isMuted(caller_id)) + { + return; + } + + std::string notify_box_type; + // voice invite question is different from default only for group call (EXT-7118) + std::string question_type = "VoiceInviteQuestionDefault"; + + BOOL ad_hoc_invite = FALSE; + if(type == IM_SESSION_P2P_INVITE) + { + //P2P is different...they only have voice invitations + notify_box_type = "VoiceInviteP2P"; + } + else if ( gAgent.isInGroup(session_id) ) + { + //only really old school groups have voice invitations + notify_box_type = "VoiceInviteGroup"; + question_type = "VoiceInviteQuestionGroup"; + } + else if ( inv_type == INVITATION_TYPE_VOICE ) + { + //else it's an ad-hoc + //and a voice ad-hoc + notify_box_type = "VoiceInviteAdHoc"; + ad_hoc_invite = TRUE; + } + else if ( inv_type == INVITATION_TYPE_IMMEDIATE ) + { + notify_box_type = "InviteAdHoc"; + ad_hoc_invite = TRUE; + } + + LLSD payload; + payload["session_id"] = session_id; + payload["session_name"] = session_name; + payload["caller_id"] = caller_id; + payload["caller_name"] = caller_name; + payload["type"] = type; + payload["inv_type"] = inv_type; + payload["session_handle"] = session_handle; + payload["session_uri"] = session_uri; + payload["notify_box_type"] = notify_box_type; + payload["question_type"] = question_type; + + LLVoiceChannel* channelp = LLVoiceChannel::getChannelByID(session_id); + if (channelp && channelp->callStarted()) { - rv = (LLFloaterIMPanel*)handle_it->get(); - if(rv && session_id == rv->getSessionID()) + // you have already started a call to the other user, so just accept the invite + LLNotifications::instance().forceResponse(LLNotification::Params("VoiceInviteP2P").payload(payload), 0); + return; + } + + if (type == IM_SESSION_P2P_INVITE || ad_hoc_invite) + { + // is the inviter a friend? + if (LLAvatarTracker::instance().getBuddyInfo(caller_id) == NULL) { - break; + // if not, and we are ignoring voice invites from non-friends + // then silently decline + if (gSavedSettings.getBOOL("VoiceCallsFriendsOnly")) + { + // invite not from a friend, so decline + LLNotifications::instance().forceResponse(LLNotification::Params("VoiceInviteP2P").payload(payload), 1); + return; + } } - rv = NULL; } - return rv; + + if ( !mPendingInvitations.has(session_id.asString()) ) + { + if (caller_name.empty()) + { + gCacheName->get(caller_id, false, // voice + boost::bind(&LLIMMgr::onInviteNameLookup, payload, _1, _2, _3)); + } + else + { + LLFloaterReg::showInstance("incoming_call", payload, FALSE); + } + mPendingInvitations[session_id.asString()] = LLSD(); + } } +void LLIMMgr::onInviteNameLookup(LLSD payload, const LLUUID& id, const std::string& name, bool is_group) +{ + payload["caller_name"] = name; + payload["session_name"] = payload["caller_name"].asString(); + + std::string notify_box_type = payload["notify_box_type"].asString(); + + LLFloaterReg::showInstance("incoming_call", payload, FALSE); +} + +//*TODO disconnects all sessions +void LLIMMgr::disconnectAllSessions() +{ + //*TODO disconnects all IM sessions +} BOOL LLIMMgr::hasSession(const LLUUID& session_id) { - return (findFloaterBySession(session_id) != NULL); + return LLIMModel::getInstance()->findIMSession(session_id) != NULL; } -void LLIMMgr::clearPendingInviation(const LLUUID& session_id) +void LLIMMgr::clearPendingInvitation(const LLUUID& session_id) { if ( mPendingInvitations.has(session_id.asString()) ) { @@ -1068,6 +2688,34 @@ void LLIMMgr::clearPendingInviation(const LLUUID& session_id) } } +void LLIMMgr::processAgentListUpdates(const LLUUID& session_id, const LLSD& body) +{ + LLIMFloater* im_floater = LLIMFloater::findInstance(session_id); + if ( im_floater ) + { + im_floater->processAgentListUpdates(body); + } + LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(session_id); + if (speaker_mgr) + { + speaker_mgr->updateSpeakers(body); + + // also the same call is added into LLVoiceClient::participantUpdatedEvent because + // sometimes it is called AFTER LLViewerChatterBoxSessionAgentListUpdates::post() + // when moderation state changed too late. See EXT-3544. + speaker_mgr->update(true); + } + else + { + //we don't have a speaker manager yet..something went wrong + //we are probably receiving an update here before + //a start or an acceptance of an invitation. Race condition. + gIMMgr->addPendingAgentListUpdates( + session_id, + body); + } +} + LLSD LLIMMgr::getPendingAgentListUpdates(const LLUUID& session_id) { if ( mPendingAgentListUpdates.has(session_id.asString()) ) @@ -1148,84 +2796,129 @@ void LLIMMgr::clearPendingAgentListUpdates(const LLUUID& session_id) } } -// create a floater and update internal representation for -// consistency. Returns the pointer, caller (the class instance since -// it is a private method) is not responsible for deleting the -// pointer. Add the floater to this but do not select it. -LLFloaterIMPanel* LLIMMgr::createFloater( - const LLUUID& session_id, - const LLUUID& other_participant_id, - const std::string& session_label, - EInstantMessage dialog, - BOOL user_initiated) +void LLIMMgr::notifyObserverSessionAdded(const LLUUID& session_id, const std::string& name, const LLUUID& other_participant_id) { - if (session_id.isNull()) + for (session_observers_list_t::iterator it = mSessionObservers.begin(); it != mSessionObservers.end(); it++) { - llwarns << "Creating LLFloaterIMPanel with null session ID" << llendl; + (*it)->sessionAdded(session_id, name, other_participant_id); } +} - llinfos << "LLIMMgr::createFloater: from " << other_participant_id - << " in session " << session_id << llendl; - LLFloaterIMPanel* floater = new LLFloaterIMPanel(session_label, - session_id, - other_participant_id, - dialog); - LLTabContainer::eInsertionPoint i_pt = user_initiated ? LLTabContainer::RIGHT_OF_CURRENT : LLTabContainer::END; - LLFloaterChatterBox::getInstance(LLSD())->addFloater(floater, FALSE, i_pt); - mFloaters.insert(floater->getHandle()); - return floater; +void LLIMMgr::notifyObserverSessionRemoved(const LLUUID& session_id) +{ + for (session_observers_list_t::iterator it = mSessionObservers.begin(); it != mSessionObservers.end(); it++) + { + (*it)->sessionRemoved(session_id); + } } -LLFloaterIMPanel* LLIMMgr::createFloater( - const LLUUID& session_id, - const LLUUID& other_participant_id, - const std::string& session_label, - const LLDynamicArray<LLUUID>& ids, - EInstantMessage dialog, - BOOL user_initiated) +void LLIMMgr::notifyObserverSessionIDUpdated( const LLUUID& old_session_id, const LLUUID& new_session_id ) { - if (session_id.isNull()) + for (session_observers_list_t::iterator it = mSessionObservers.begin(); it != mSessionObservers.end(); it++) { - llwarns << "Creating LLFloaterIMPanel with null session ID" << llendl; + (*it)->sessionIDUpdated(old_session_id, new_session_id); } - llinfos << "LLIMMgr::createFloater: from " << other_participant_id - << " in session " << session_id << llendl; - LLFloaterIMPanel* floater = new LLFloaterIMPanel(session_label, - session_id, - other_participant_id, - ids, - dialog); - LLTabContainer::eInsertionPoint i_pt = user_initiated ? LLTabContainer::RIGHT_OF_CURRENT : LLTabContainer::END; - LLFloaterChatterBox::getInstance(LLSD())->addFloater(floater, FALSE, i_pt); - mFloaters.insert(floater->getHandle()); - return floater; +} + +void LLIMMgr::addSessionObserver(LLIMSessionObserver *observer) +{ + mSessionObservers.push_back(observer); +} + +void LLIMMgr::removeSessionObserver(LLIMSessionObserver *observer) +{ + mSessionObservers.remove(observer); +} + +bool LLIMMgr::startCall(const LLUUID& session_id, LLVoiceChannel::EDirection direction) +{ + LLVoiceChannel* voice_channel = LLIMModel::getInstance()->getVoiceChannel(session_id); + if (!voice_channel) return false; + + voice_channel->setCallDirection(direction); + voice_channel->activate(); + return true; +} + +bool LLIMMgr::endCall(const LLUUID& session_id) +{ + LLVoiceChannel* voice_channel = LLIMModel::getInstance()->getVoiceChannel(session_id); + if (!voice_channel) return false; + + voice_channel->deactivate(); + LLIMModel::LLIMSession* im_session = LLIMModel::getInstance()->findIMSession(session_id); + if (im_session) + { + // need to update speakers' state + im_session->mSpeakers->update(FALSE); + } + return true; +} + +bool LLIMMgr::isVoiceCall(const LLUUID& session_id) +{ + LLIMModel::LLIMSession* im_session = LLIMModel::getInstance()->findIMSession(session_id); + if (!im_session) return false; + + return im_session->mStartedAsIMCall; } void LLIMMgr::noteOfflineUsers( - LLFloaterIMPanel* floater, + const LLUUID& session_id, const LLDynamicArray<LLUUID>& ids) { S32 count = ids.count(); if(count == 0) { - floater->addHistoryLine(sOnlyUserMessage, gSavedSettings.getColor4("SystemChatColor")); + const std::string& only_user = LLTrans::getString("only_user_message"); + LLIMModel::getInstance()->addMessage(session_id, SYSTEM_FROM, LLUUID::null, only_user); } else { const LLRelationship* info = NULL; LLAvatarTracker& at = LLAvatarTracker::instance(); + LLIMModel& im_model = LLIMModel::instance(); for(S32 i = 0; i < count; ++i) { info = at.getBuddyInfo(ids.get(i)); - std::string first, last; - if(info && !info->isOnline() - && gCacheName->getName(ids.get(i), first, last)) + LLAvatarName av_name; + if (info + && !info->isOnline() + && LLAvatarNameCache::get(ids.get(i), &av_name)) { - LLUIString offline = sOfflineMessage; - offline.setArg("[FIRST]", first); - offline.setArg("[LAST]", last); - floater->addHistoryLine(offline, gSavedSettings.getColor4("SystemChatColor")); + LLUIString offline = LLTrans::getString("offline_message"); + // Use display name only because this user is your friend + offline.setArg("[NAME]", av_name.mDisplayName); + im_model.proccessOnlineOfflineNotification(session_id, offline); + } + } + } +} + +void LLIMMgr::noteMutedUsers(const LLUUID& session_id, + const LLDynamicArray<LLUUID>& ids) +{ + // Don't do this if we don't have a mute list. + LLMuteList *ml = LLMuteList::getInstance(); + if( !ml ) + { + return; + } + + S32 count = ids.count(); + if(count > 0) + { + LLIMModel* im_model = LLIMModel::getInstance(); + + for(S32 i = 0; i < count; ++i) + { + if( ml->isMuted(ids.get(i)) ) + { + LLUIString muted = LLTrans::getString("muted_message"); + + im_model->addMessage(session_id, SYSTEM_FROM, LLUUID::null, muted); + break; } } } @@ -1244,29 +2937,13 @@ void LLIMMgr::processIMTypingStop(const LLIMInfo* im_info) void LLIMMgr::processIMTypingCore(const LLIMInfo* im_info, BOOL typing) { LLUUID session_id = computeSessionID(im_info->mIMType, im_info->mFromID); - LLFloaterIMPanel* floater = findFloaterBySession(session_id); - if (floater) - { - floater->processIMTyping(im_info, typing); - } -} - -void LLIMMgr::updateFloaterSessionID( - const LLUUID& old_session_id, - const LLUUID& new_session_id) -{ - LLFloaterIMPanel* floater = findFloaterBySession(old_session_id); - if (floater) + LLIMFloater* im_floater = LLIMFloater::findInstance(session_id); + if ( im_floater ) { - floater->sessionInitReplyReceived(new_session_id); + im_floater->processIMTyping(im_info, typing); } } -LLFloaterChatterBox* LLIMMgr::getFloater() -{ - return LLFloaterChatterBox::getInstance(LLSD()); -} - class LLViewerChatterBoxSessionStartReply : public LLHTTPNode { public: @@ -1295,40 +2972,31 @@ public: if ( success ) { session_id = body["session_id"].asUUID(); - gIMMgr->updateFloaterSessionID( - temp_session_id, - session_id); - LLFloaterIMPanel* floaterp = gIMMgr->findFloaterBySession(session_id); - if (floaterp) - { - floaterp->setSpeakers(body); - //apply updates we've possibly received previously - floaterp->updateSpeakersList( - gIMMgr->getPendingAgentListUpdates(session_id)); + LLIMModel::getInstance()->processSessionInitializedReply(temp_session_id, session_id); + LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(session_id); + if (speaker_mgr) + { + speaker_mgr->setSpeakers(body); + speaker_mgr->updateSpeakers(gIMMgr->getPendingAgentListUpdates(session_id)); + } + + LLIMFloater* im_floater = LLIMFloater::findInstance(session_id); + if ( im_floater ) + { if ( body.has("session_info") ) { - floaterp->processSessionUpdate(body["session_info"]); + im_floater->processSessionUpdate(body["session_info"]); } - - //aply updates we've possibly received previously - floaterp->updateSpeakersList( - gIMMgr->getPendingAgentListUpdates(session_id)); } + gIMMgr->clearPendingAgentListUpdates(session_id); } else { - //throw an error dialog and close the temp session's - //floater - LLFloaterIMPanel* floater = - gIMMgr->findFloaterBySession(temp_session_id); - - if ( floater ) - { - floater->showSessionStartError(body["error"].asString()); - } + //throw an error dialog and close the temp session's floater + gIMMgr->showSessionStartError(body["error"].asString(), temp_session_id); } gIMMgr->clearPendingAgentListUpdates(session_id); @@ -1361,15 +3029,10 @@ public: if ( !success ) { //throw an error dialog - LLFloaterIMPanel* floater = - gIMMgr->findFloaterBySession(session_id); - - if (floater) - { - floater->showSessionEventError( - body["event"].asString(), - body["error"].asString()); - } + gIMMgr->showSessionEventError( + body["event"].asString(), + body["error"].asString(), + session_id); } } }; @@ -1382,18 +3045,12 @@ public: const LLSD& input) const { LLUUID session_id; - LLString reason; + std::string reason; session_id = input["body"]["session_id"].asUUID(); reason = input["body"]["reason"].asString(); - LLFloaterIMPanel* floater = - gIMMgr ->findFloaterBySession(session_id); - - if ( floater ) - { - floater->showSessionForceClose(reason); - } + gIMMgr->showSessionForceClose(reason, session_id); } }; @@ -1405,21 +3062,8 @@ public: const LLSD& context, const LLSD& input) const { - LLFloaterIMPanel* floaterp = gIMMgr->findFloaterBySession(input["body"]["session_id"].asUUID()); - if (floaterp) - { - floaterp->updateSpeakersList( - input["body"]); - } - else - { - //we don't have a floater yet..something went wrong - //we are probably receiving an update here before - //a start or an acceptance of an invitation. Race condition. - gIMMgr->addPendingAgentListUpdates( - input["body"]["session_id"].asUUID(), - input["body"]); - } + const LLUUID& session_id = input["body"]["session_id"].asUUID(); + gIMMgr->processAgentListUpdates(session_id, input["body"]); } }; @@ -1431,10 +3075,16 @@ public: const LLSD& context, const LLSD& input) const { - LLFloaterIMPanel* floaterp = gIMMgr->findFloaterBySession(input["body"]["session_id"].asUUID()); - if (floaterp) + LLUUID session_id = input["body"]["session_id"].asUUID(); + LLIMFloater* im_floater = LLIMFloater::findInstance(session_id); + if ( im_floater ) + { + im_floater->processSessionUpdate(input["body"]["info"]); + } + LLIMSpeakerMgr* im_mgr = LLIMModel::getInstance()->getSpeakerManager(session_id); + if (im_mgr) { - floaterp->processSessionUpdate(input["body"]["info"]); + im_mgr->processSessionUpdate(input["body"]["info"]); } } }; @@ -1464,7 +3114,6 @@ public: { return; } - char buffer[DB_IM_MSG_BUF_SIZE * 2]; /* Flawfinder: ignore */ LLChat chat; std::string message = message_params["message"].asString(); @@ -1478,23 +3127,13 @@ public: (time_t) message_params["timestamp"].asInteger(); BOOL is_busy = gAgent.getBusy(); - BOOL is_muted = gMuteListp->isMuted( + BOOL is_muted = LLMuteList::getInstance()->isMuted( from_id, - name.c_str(), + name, LLMute::flagTextChat); - BOOL is_linden = gMuteListp->isLinden( - name.c_str()); - char separator_string[3]=": "; /* Flawfinder: ignore */ - int message_offset=0; - - //Handle IRC styled /me messages. - if (!strncmp(message.c_str(), "/me ", 4) || - !strncmp(message.c_str(), "/me'", 4)) - { - strcpy(separator_string,""); /* Flawfinder: ignore */ - message_offset = 3; - } + BOOL is_linden = LLMuteList::getInstance()->isLinden(name); + std::string separator_string(": "); chat.mMuted = is_muted && !is_linden; chat.mFromID = from_id; @@ -1506,24 +3145,14 @@ public: } // standard message, not from system - char saved[MAX_STRING]; /* Flawfinder: ignore */ - saved[0] = '\0'; + std::string saved; if(offline == IM_OFFLINE) { - char time_buf[TIME_STR_LENGTH]; /* Flawfinder: ignore */ - snprintf(saved, /* Flawfinder: ignore */ - MAX_STRING, - "(Saved %s) ", - formatted_time(timestamp, time_buf)); + LLStringUtil::format_map_t args; + args["[LONG_TIMESTAMP]"] = formatted_time(timestamp); + saved = LLTrans::getString("Saved_message", args); } - snprintf( - buffer, - sizeof(buffer), - "%s%s%s%s", - name.c_str(), - separator_string, - saved, - (message.c_str() + message_offset)); /*Flawfinder: ignore*/ + std::string buffer = saved + message; BOOL is_this_agent = FALSE; if(from_id == gAgentID) @@ -1533,24 +3162,14 @@ public: gIMMgr->addMessage( session_id, from_id, - name.c_str(), + name, buffer, - (char*)&bin_bucket[0], + std::string((char*)&bin_bucket[0]), IM_SESSION_INVITE, message_params["parent_estate_id"].asInteger(), message_params["region_id"].asUUID(), - ll_vector3_from_sd(message_params["position"])); - - snprintf( - buffer, - sizeof(buffer), - "IM: %s%s%s%s", - name.c_str(), - separator_string, - saved, - (message.c_str()+message_offset)); /* Flawfinder: ignore */ - chat.mText = buffer; - LLFloaterChat::addChat(chat, TRUE, is_this_agent); + ll_vector3_from_sd(message_params["position"]), + true); //K now we want to accept the invitation std::string url = gAgent.getRegion()->getCapability( @@ -1576,7 +3195,7 @@ public: return; } - if(!LLVoiceClient::voiceEnabled()) + if(!LLVoiceClient::getInstance()->voiceEnabled() || !LLVoiceClient::getInstance()->isVoiceWorking()) { // Don't display voice invites unless the user has voice enabled. return; |