/** * @file llimfloater.cpp * @brief LLIMFloater class definition * * $LicenseInfo:firstyear=2009&license=viewergpl$ * * Copyright (c) 2009, Linden Research, Inc. * * 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://secondlifegrid.net/programs/open_source/licensing/gplv2 * * 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://secondlifegrid.net/programs/open_source/licensing/flossexception * * 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. * * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, * COMPLETENESS OR PERFORMANCE. * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "llimfloater.h" #include "llagent.h" #include "llappviewer.h" #include "llbutton.h" #include "llbottomtray.h" #include "llchannelmanager.h" #include "llchiclet.h" #include "llfloaterchat.h" #include "llfloaterreg.h" #include "lllineeditor.h" #include "lllogchat.h" #include "llpanelimcontrolpanel.h" #include "llscreenchannel.h" #include "lltrans.h" #include "llchathistory.h" #include "llviewerwindow.h" #include "llvoicechannel.h" #include "lltransientfloatermgr.h" #include "llinventorymodel.h" LLIMFloater::LLIMFloater(const LLUUID& session_id) : LLTransientDockableFloater(NULL, true, session_id), mControlPanel(NULL), mSessionID(session_id), mLastMessageIndex(-1), mDialog(IM_NOTHING_SPECIAL), mChatHistory(NULL), mInputEditor(NULL), mSavedTitle(), mTypingStart(), mShouldSendTypingState(false), mMeTyping(false), mOtherTyping(false), mTypingTimer(), mTypingTimeoutTimer(), mPositioned(false), mSessionInitialized(false) { LLIMModel::LLIMSession* im_session = LLIMModel::getInstance()->findIMSession(mSessionID); if (im_session) { mSessionInitialized = im_session->mSessionInitialized; mDialog = im_session->mType; switch(mDialog){ case IM_NOTHING_SPECIAL: case IM_SESSION_P2P_INVITE: mFactoryMap["panel_im_control_panel"] = LLCallbackMap(createPanelIMControl, this); break; case IM_SESSION_CONFERENCE_START: mFactoryMap["panel_im_control_panel"] = LLCallbackMap(createPanelAdHocControl, this); break; default: mFactoryMap["panel_im_control_panel"] = LLCallbackMap(createPanelGroupControl, this); } } } void LLIMFloater::onFocusLost() { LLIMModel::getInstance()->resetActiveSessionID(); } void LLIMFloater::onFocusReceived() { LLIMModel::getInstance()->setActiveSessionID(mSessionID); } // virtual void LLIMFloater::onClose(bool app_quitting) { setTyping(false); // SJB: We want the close button to hide the session window, not end it // *NOTE: Yhis is functional, but not ideal - it's still closing the floater; we really want to change the behavior of the X button instead. //gIMMgr->leaveSession(mSessionID); } /* static */ void LLIMFloater::newIMCallback(const LLSD& data){ if (data["num_unread"].asInteger() > 0 || data["from_id"].asUUID().isNull()) { LLUUID session_id = data["session_id"].asUUID(); LLIMFloater* floater = LLFloaterReg::findTypedInstance("impanel", session_id); if (floater == NULL) return; // update if visible, otherwise will be updated when opened if (floater->getVisible()) { floater->updateMessages(); } } } void LLIMFloater::onVisibilityChange(const LLSD& new_visibility) { bool visible = new_visibility.asBoolean(); LLVoiceChannel* voice_channel = LLIMModel::getInstance()->getVoiceChannel(mSessionID); if (visible && voice_channel && voice_channel->getState() == LLVoiceChannel::STATE_CONNECTED) { LLFloaterReg::showInstance("voice_call", mSessionID); } else { LLFloaterReg::hideInstance("voice_call", mSessionID); } } void LLIMFloater::onSendMsg( LLUICtrl* ctrl, void* userdata ) { LLIMFloater* self = (LLIMFloater*) userdata; self->sendMsg(); self->setTyping(false); } void LLIMFloater::sendMsg() { if (!gAgent.isGodlike() && (mDialog == IM_NOTHING_SPECIAL) && mOtherParticipantUUID.isNull()) { llinfos << "Cannot send IM to everyone unless you're a god." << llendl; return; } if (mInputEditor) { LLWString text = mInputEditor->getConvertedText(); if(!text.empty()) { // Truncate and convert to UTF8 for transport std::string utf8_text = wstring_to_utf8str(text); utf8_text = utf8str_truncate(utf8_text, MAX_MSG_BUF_SIZE - 1); if (mSessionInitialized) { LLIMModel::sendMessage(utf8_text, mSessionID, mOtherParticipantUUID,mDialog); } else { //queue up the message to send once the session is initialized mQueuedMsgsForInit.append(utf8_text); } mInputEditor->setText(LLStringUtil::null); updateMessages(); } } } LLIMFloater::~LLIMFloater() { } //virtual BOOL LLIMFloater::postBuild() { const LLUUID& other_party_id = LLIMModel::getInstance()->getOtherParticipantID(mSessionID); if (other_party_id.notNull()) { mOtherParticipantUUID = other_party_id; } mControlPanel->setSessionId(mSessionID); mControlPanel->setVisible(gSavedSettings.getBOOL("IMShowControlPanel")); LLButton* slide_left = getChild("slide_left_btn"); slide_left->setVisible(mControlPanel->getVisible()); slide_left->setClickedCallback(boost::bind(&LLIMFloater::onSlide, this)); LLButton* slide_right = getChild("slide_right_btn"); slide_right->setVisible(!mControlPanel->getVisible()); slide_right->setClickedCallback(boost::bind(&LLIMFloater::onSlide, this)); mInputEditor = getChild("chat_editor"); mInputEditor->setMaxTextLength(1023); // enable line history support for instant message bar mInputEditor->setEnableLineHistory(TRUE); mInputEditor->setFocusReceivedCallback( boost::bind(onInputEditorFocusReceived, _1, this) ); mInputEditor->setFocusLostCallback( boost::bind(onInputEditorFocusLost, _1, this) ); mInputEditor->setKeystrokeCallback( onInputEditorKeystroke, this ); mInputEditor->setCommitOnFocusLost( FALSE ); mInputEditor->setRevertOnEsc( FALSE ); mInputEditor->setReplaceNewlinesWithSpaces( FALSE ); std::string session_name(LLIMModel::instance().getName(mSessionID)); mInputEditor->setLabel(LLTrans::getString("IM_to_label") + " " + session_name); LLStringUtil::toUpper(session_name); setTitle(session_name); childSetCommitCallback("chat_editor", onSendMsg, this); mChatHistory = getChild("chat_history"); setDocked(true); mTypingStart = LLTrans::getString("IM_typing_start_string"); // Disable input editor if session cannot accept text LLIMModel::LLIMSession* im_session = LLIMModel::instance().findIMSession(mSessionID); if( im_session && !im_session->mTextIMPossible ) { mInputEditor->setEnabled(FALSE); mInputEditor->setLabel(LLTrans::getString("IM_unavailable_text_label")); } //*TODO if session is not initialized yet, add some sort of a warning message like "starting session...blablabla" //see LLFloaterIMPanel for how it is done (IB) return LLDockableFloater::postBuild(); } // virtual void LLIMFloater::draw() { if ( mMeTyping ) { // Time out if user hasn't typed for a while. if ( mTypingTimeoutTimer.getElapsedTimeF32() > LLAgent::TYPING_TIMEOUT_SECS ) { setTyping(false); } } LLTransientDockableFloater::draw(); } // static void* LLIMFloater::createPanelIMControl(void* userdata) { LLIMFloater *self = (LLIMFloater*)userdata; self->mControlPanel = new LLPanelIMControlPanel(); self->mControlPanel->setXMLFilename("panel_im_control_panel.xml"); return self->mControlPanel; } // static void* LLIMFloater::createPanelGroupControl(void* userdata) { LLIMFloater *self = (LLIMFloater*)userdata; self->mControlPanel = new LLPanelGroupControlPanel(self->mSessionID); self->mControlPanel->setXMLFilename("panel_group_control_panel.xml"); return self->mControlPanel; } // static void* LLIMFloater::createPanelAdHocControl(void* userdata) { LLIMFloater *self = (LLIMFloater*)userdata; self->mControlPanel = new LLPanelAdHocControlPanel(self->mSessionID); self->mControlPanel->setXMLFilename("panel_adhoc_control_panel.xml"); return self->mControlPanel; } void LLIMFloater::onSlide() { LLPanel* im_control_panel = getChild("panel_im_control_panel"); im_control_panel->setVisible(!im_control_panel->getVisible()); gSavedSettings.setBOOL("IMShowControlPanel", im_control_panel->getVisible()); getChild("slide_left_btn")->setVisible(im_control_panel->getVisible()); getChild("slide_right_btn")->setVisible(!im_control_panel->getVisible()); } //static LLIMFloater* LLIMFloater::show(const LLUUID& session_id) { //hide all LLFloaterReg::const_instance_list_t& inst_list = LLFloaterReg::getFloaterList("impanel"); for (LLFloaterReg::const_instance_list_t::const_iterator iter = inst_list.begin(); iter != inst_list.end(); ++iter) { LLIMFloater* floater = dynamic_cast(*iter); if (floater && floater->isDocked()) { floater->setVisible(false); } } LLIMFloater* floater = LLFloaterReg::showTypedInstance("impanel", session_id); floater->updateMessages(); floater->mInputEditor->setFocus(TRUE); if (floater->getDockControl() == NULL) { LLChiclet* chiclet = LLBottomTray::getInstance()->getChicletPanel()->findChiclet( session_id); if (chiclet == NULL) { llerror("Dock chiclet for LLIMFloater doesn't exists", 0); } else { LLBottomTray::getInstance()->getChicletPanel()->scrollToChiclet(chiclet); } floater->setDockControl(new LLDockControl(chiclet, floater, floater->getDockTongue(), LLDockControl::TOP, boost::bind(&LLIMFloater::getAllowedRect, floater, _1))); } return floater; } void LLIMFloater::getAllowedRect(LLRect& rect) { rect = gViewerWindow->getWorldViewRectScaled(); } void LLIMFloater::setDocked(bool docked, bool pop_on_undock) { // update notification channel state LLNotificationsUI::LLScreenChannel* channel = dynamic_cast (LLNotificationsUI::LLChannelManager::getInstance()-> findChannelByID(LLUUID(gSavedSettings.getString("NotificationChannelUUID")))); LLTransientDockableFloater::setDocked(docked, pop_on_undock); // update notification channel state if(channel) { channel->updateShowToastsState(); } } void LLIMFloater::setVisible(BOOL visible) { LLNotificationsUI::LLScreenChannel* channel = dynamic_cast (LLNotificationsUI::LLChannelManager::getInstance()-> findChannelByID(LLUUID(gSavedSettings.getString("NotificationChannelUUID")))); LLTransientDockableFloater::setVisible(visible); // update notification channel state if(channel) { channel->updateShowToastsState(); } } //static bool LLIMFloater::toggle(const LLUUID& session_id) { LLIMFloater* floater = LLFloaterReg::findTypedInstance("impanel", session_id); if (floater && floater->getVisible() && floater->isDocked()) { // clicking on chiclet to close floater just hides it to maintain existing // scroll/text entry state floater->setVisible(false); return false; } else if(floater && !floater->isDocked()) { floater->setVisible(TRUE); floater->setFocus(TRUE); return true; } else { // ensure the list of messages is updated when floater is made visible show(session_id); return true; } } //static LLIMFloater* LLIMFloater::findInstance(const LLUUID& session_id) { return LLFloaterReg::findTypedInstance("impanel", session_id); } void LLIMFloater::sessionInitReplyReceived(const LLUUID& im_session_id) { mSessionInitialized = true; //will be different only for an ad-hoc im session if (mSessionID != im_session_id) { mSessionID = im_session_id; setKey(im_session_id); mControlPanel->setSessionId(im_session_id); } //*TODO here we should remove "starting session..." warning message if we added it in postBuild() (IB) //need to send delayed messaged collected while waiting for session initialization if (!mQueuedMsgsForInit.size()) return; LLSD::array_iterator iter; for ( iter = mQueuedMsgsForInit.beginArray(); iter != mQueuedMsgsForInit.endArray(); ++iter) { LLIMModel::sendMessage(iter->asString(), mSessionID, mOtherParticipantUUID, mDialog); } } void LLIMFloater::updateMessages() { std::list messages; LLIMModel::instance().getMessages(mSessionID, messages, mLastMessageIndex+1); if (messages.size()) { // LLUIColor chat_color = LLUIColorTable::instance().getColor("IMChatColor"); std::ostringstream message; std::list::const_reverse_iterator iter = messages.rbegin(); std::list::const_reverse_iterator iter_end = messages.rend(); for (; iter != iter_end; ++iter) { LLSD msg = *iter; std::string time = msg["time"].asString(); LLUUID from_id = msg["from_id"].asUUID(); std::string from = from_id != gAgentID ? msg["from"].asString() : LLTrans::getString("You"); std::string message = msg["message"].asString(); LLChat chat; chat.mFromID = from_id; chat.mFromName = from; chat.mText = message; chat.mTimeStr = time; //Handle IRC styled /me messages. std::string prefix = message.substr(0, 4); if (prefix == "/me " || prefix == "/me'") { LLColor4 txt_color = LLUIColorTable::instance().getColor("White"); LLViewerChat::getChatColor(chat,txt_color); LLFontGL* fontp = LLViewerChat::getChatFont(); std::string font_name = LLFontGL::nameFromFont(fontp); std::string font_size = LLFontGL::sizeFromFont(fontp); LLStyle::Params append_style_params; append_style_params.color(txt_color); append_style_params.readonly_color(txt_color); append_style_params.font.name(font_name); append_style_params.font.size(font_size); if (from.size() > 0) { append_style_params.font.style = "ITALIC"; chat.mText = from; mChatHistory->appendWidgetMessage(chat, append_style_params); } message = message.substr(3); append_style_params.font.style = "ITALIC"; mChatHistory->appendText(message, FALSE, append_style_params); } else { chat.mText = message; mChatHistory->appendWidgetMessage(chat); } mLastMessageIndex = msg["index"].asInteger(); } } } // static void LLIMFloater::onInputEditorFocusReceived( LLFocusableElement* caller, void* userdata ) { LLIMFloater* self= (LLIMFloater*) userdata; // Allow enabling the LLIMFloater input editor only if session can accept text LLIMModel::LLIMSession* im_session = LLIMModel::instance().findIMSession(self->mSessionID); //TODO: While disabled lllineeditor can receive focus we need to check if it is enabled (EK) if( im_session && im_session->mTextIMPossible && self->mInputEditor->getEnabled()) { //in disconnected state IM input editor should be disabled self->mInputEditor->setEnabled(!gDisconnected); } self->mChatHistory->setCursorAndScrollToEnd(); } // static void LLIMFloater::onInputEditorFocusLost(LLFocusableElement* caller, void* userdata) { LLIMFloater* self = (LLIMFloater*) userdata; self->setTyping(false); } // static void LLIMFloater::onInputEditorKeystroke(LLLineEditor* caller, void* userdata) { LLIMFloater* self = (LLIMFloater*)userdata; std::string text = self->mInputEditor->getText(); if (!text.empty()) { self->setTyping(true); } else { // Deleting all text counts as stopping typing. self->setTyping(false); } } void LLIMFloater::setTyping(bool typing) { if ( typing ) { // Started or proceeded typing, reset the typing timeout timer mTypingTimeoutTimer.reset(); } if ( mMeTyping != typing ) { // Typing state is changed mMeTyping = typing; // So, should send current state mShouldSendTypingState = true; // In case typing is started, send state after some delay mTypingTimer.reset(); } // Don't want to send typing indicators to multiple people, potentially too // much network traffic. Only send in person-to-person IMs. if ( mShouldSendTypingState && mDialog == IM_NOTHING_SPECIAL ) { if ( mMeTyping ) { if ( mTypingTimer.getElapsedTimeF32() > 1.f ) { // Still typing, send 'start typing' notification LLIMModel::instance().sendTypingState(mSessionID, mOtherParticipantUUID, TRUE); mShouldSendTypingState = false; } } else { // Send 'stop typing' notification immediately LLIMModel::instance().sendTypingState(mSessionID, mOtherParticipantUUID, FALSE); mShouldSendTypingState = false; } } LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if (speaker_mgr) speaker_mgr->setSpeakerTyping(gAgent.getID(), FALSE); } void LLIMFloater::processIMTyping(const LLIMInfo* im_info, BOOL typing) { if ( typing ) { // other user started typing addTypingIndicator(im_info); } else { // other user stopped typing removeTypingIndicator(im_info); } } void LLIMFloater::processAgentListUpdates(const LLSD& body) { if ( !body.isMap() ) return; if ( body.has("agent_updates") && body["agent_updates"].isMap() ) { LLSD agent_data = body["agent_updates"].get(gAgentID.asString()); if (agent_data.isMap() && agent_data.has("info")) { LLSD agent_info = agent_data["info"]; if (agent_info.has("mutes")) { BOOL moderator_muted_text = agent_info["mutes"]["text"].asBoolean(); mInputEditor->setEnabled(!moderator_muted_text); std::string label; if (moderator_muted_text) label = LLTrans::getString("IM_muted_text_label"); else label = LLTrans::getString("IM_to_label") + " " + LLIMModel::instance().getName(mSessionID); mInputEditor->setLabel(label); } } } } void LLIMFloater::processSessionUpdate(const LLSD& session_update) { // *TODO : verify following code when moderated mode will be implemented if ( false && session_update.has("moderated_mode") && session_update["moderated_mode"].has("voice") ) { BOOL voice_moderated = session_update["moderated_mode"]["voice"]; const std::string session_label = LLIMModel::instance().getName(mSessionID); if (voice_moderated) { setTitle(session_label + std::string(" ") + LLTrans::getString("IM_moderated_chat_label")); } else { setTitle(session_label); } // *TODO : uncomment this when/if LLPanelActiveSpeakers panel will be added //update the speakers dropdown too //mSpeakerPanel->setVoiceModerationCtrlMode(voice_moderated); } } BOOL LLIMFloater::handleDragAndDrop(S32 x, S32 y, MASK mask, BOOL drop, EDragAndDropType cargo_type, void *cargo_data, EAcceptance *accept, std::string& tooltip_msg) { if (mDialog == IM_NOTHING_SPECIAL) { LLToolDragAndDrop::handleGiveDragAndDrop(mOtherParticipantUUID, mSessionID, drop, cargo_type, cargo_data, accept); } // handle case for dropping calling cards (and folders of calling cards) onto invitation panel for invites else if (isInviteAllowed()) { *accept = ACCEPT_NO; if (cargo_type == DAD_CALLINGCARD) { if (dropCallingCard((LLInventoryItem*)cargo_data, drop)) { *accept = ACCEPT_YES_MULTI; } } else if (cargo_type == DAD_CATEGORY) { if (dropCategory((LLInventoryCategory*)cargo_data, drop)) { *accept = ACCEPT_YES_MULTI; } } } return TRUE; } BOOL LLIMFloater::dropCallingCard(LLInventoryItem* item, BOOL drop) { BOOL rv = isInviteAllowed(); if(rv && item && item->getCreatorUUID().notNull()) { if(drop) { std::vector ids; ids.push_back(item->getCreatorUUID()); inviteToSession(ids); } } else { // set to false if creator uuid is null. rv = FALSE; } return rv; } BOOL LLIMFloater::dropCategory(LLInventoryCategory* category, BOOL drop) { BOOL rv = isInviteAllowed(); if(rv && category) { LLInventoryModel::cat_array_t cats; LLInventoryModel::item_array_t items; LLUniqueBuddyCollector buddies; gInventory.collectDescendentsIf(category->getUUID(), cats, items, LLInventoryModel::EXCLUDE_TRASH, buddies); S32 count = items.count(); if(count == 0) { rv = FALSE; } else if(drop) { std::vector ids; ids.reserve(count); for(S32 i = 0; i < count; ++i) { ids.push_back(items.get(i)->getCreatorUUID()); } inviteToSession(ids); } } return rv; } BOOL LLIMFloater::isInviteAllowed() const { return ( (IM_SESSION_CONFERENCE_START == mDialog) || (IM_SESSION_INVITE == mDialog) ); } class LLSessionInviteResponder : public LLHTTPClient::Responder { public: LLSessionInviteResponder(const LLUUID& session_id) { mSessionID = session_id; } void error(U32 statusNum, const std::string& reason) { llinfos << "Error inviting all agents to session" << llendl; //throw something back to the viewer here? } private: LLUUID mSessionID; }; BOOL LLIMFloater::inviteToSession(const std::vector& ids) { LLViewerRegion* region = gAgent.getRegion(); if (!region) { return FALSE; } S32 count = ids.size(); if( isInviteAllowed() && (count > 0) ) { llinfos << "LLIMFloater::inviteToSession() - inviting participants" << llendl; std::string url = region->getCapability("ChatSessionRequest"); LLSD data; data["params"] = LLSD::emptyArray(); for (int i = 0; i < count; i++) { data["params"].append(ids[i]); } data["method"] = "invite"; data["session-id"] = mSessionID; LLHTTPClient::post( url, data, new LLSessionInviteResponder( mSessionID)); } else { llinfos << "LLIMFloater::inviteToSession -" << " no need to invite agents for " << mDialog << llendl; // successful add, because everyone that needed to get added // was added. } return TRUE; } void LLIMFloater::addTypingIndicator(const LLIMInfo* im_info) { // We may have lost a "stop-typing" packet, don't add it twice if ( im_info && !mOtherTyping ) { mOtherTyping = true; // Create typing is started title string LLUIString typing_start(mTypingStart); typing_start.setArg("[NAME]", im_info->mName); // Save and set new title mSavedTitle = getTitle(); setTitle (typing_start); // Update speaker LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if ( speaker_mgr ) { speaker_mgr->setSpeakerTyping(im_info->mFromID, TRUE); } } } void LLIMFloater::removeTypingIndicator(const LLIMInfo* im_info) { if ( mOtherTyping ) { mOtherTyping = false; // Revert the title to saved one setTitle(mSavedTitle); if ( im_info ) { // Update speaker LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if ( speaker_mgr ) { speaker_mgr->setSpeakerTyping(im_info->mFromID, FALSE); } } } }