/** * @file llimpanel.cpp * @brief LLIMPanel class definition * * $LicenseInfo:firstyear=2001&license=viewergpl$ * * Copyright (c) 2001-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 "llimpanel.h" #include "indra_constants.h" #include "llfloaterreg.h" #include "llfocusmgr.h" #include "llfontgl.h" #include "llrect.h" #include "llerror.h" #include "llmultifloater.h" #include "llstring.h" #include "message.h" #include "lltextbox.h" #include "llagent.h" #include "llbutton.h" #include "llbottomtray.h" #include "llcallingcard.h" #include "llchannelmanager.h" #include "llchat.h" #include "llchiclet.h" #include "llconsole.h" #include "llgroupactions.h" #include "llfloater.h" #include "llfloateractivespeakers.h" #include "llavataractions.h" #include "llinventory.h" #include "llinventorymodel.h" #include "llfloaterinventory.h" #include "lliconctrl.h" #include "llkeyboard.h" #include "lllineeditor.h" #include "llpanelimcontrolpanel.h" #include "llrecentpeople.h" #include "llresmgr.h" #include "lltooldraganddrop.h" #include "lltrans.h" #include "lltabcontainer.h" #include "llviewertexteditor.h" #include "llviewermessage.h" #include "llviewerstats.h" #include "llviewercontrol.h" #include "lluictrlfactory.h" #include "llviewerwindow.h" #include "llvoicechannel.h" #include "lllogchat.h" #include "llweb.h" #include "llhttpclient.h" #include "llmutelist.h" #include "llstylemap.h" #include "llappviewer.h" // // Constants // const S32 LINE_HEIGHT = 16; const S32 MIN_WIDTH = 200; const S32 MIN_HEIGHT = 130; // // Statics // // static std::string sTitleString = "Instant Message with [NAME]"; static std::string sTypingStartString = "[NAME]: ..."; static std::string sSessionStartString = "Starting session with [NAME] please wait."; // // LLFloaterIMPanel // LLFloaterIMPanel::LLFloaterIMPanel(const std::string& session_label, const LLUUID& session_id, const LLUUID& other_participant_id, const std::vector& ids, EInstantMessage dialog) : LLFloater(session_id), mInputEditor(NULL), mHistoryEditor(NULL), mSessionUUID(session_id), mSessionLabel(session_label), mSessionInitialized(FALSE), mSessionStartMsgPos(0), mOtherParticipantUUID(other_participant_id), mDialog(dialog), mSessionInitialTargetIDs(ids), mTyping(FALSE), mOtherTyping(FALSE), mTypingLineStartIndex(0), mSentTypingState(TRUE), mNumUnreadMessages(0), mShowSpeakersOnConnect(TRUE), mTextIMPossible(TRUE), mProfileButtonEnabled(TRUE), mCallBackEnabled(TRUE), mSpeakerPanel(NULL), mFirstKeystrokeTimer(), mLastKeystrokeTimer() { std::string xml_filename; switch(mDialog) { case IM_SESSION_GROUP_START: mFactoryMap["active_speakers_panel"] = LLCallbackMap(createSpeakersPanel, this); xml_filename = "floater_instant_message_group.xml"; break; case IM_SESSION_INVITE: mFactoryMap["active_speakers_panel"] = LLCallbackMap(createSpeakersPanel, this); if (gAgent.isInGroup(mSessionUUID)) { xml_filename = "floater_instant_message_group.xml"; } else // must be invite to ad hoc IM { xml_filename = "floater_instant_message_ad_hoc.xml"; } break; case IM_SESSION_P2P_INVITE: xml_filename = "floater_instant_message.xml"; break; case IM_SESSION_CONFERENCE_START: mFactoryMap["active_speakers_panel"] = LLCallbackMap(createSpeakersPanel, this); xml_filename = "floater_instant_message_ad_hoc.xml"; break; // just received text from another user case IM_NOTHING_SPECIAL: xml_filename = "floater_instant_message.xml"; mTextIMPossible = LLVoiceClient::getInstance()->isSessionTextIMPossible(mSessionUUID); mProfileButtonEnabled = LLVoiceClient::getInstance()->isParticipantAvatar(mSessionUUID); mCallBackEnabled = LLVoiceClient::getInstance()->isSessionCallBackPossible(mSessionUUID); break; default: llwarns << "Unknown session type" << llendl; xml_filename = "floater_instant_message.xml"; break; } LLUICtrlFactory::getInstance()->buildFloater(this, xml_filename, NULL); setTitle(mSessionLabel); mInputEditor->setMaxTextLength(DB_IM_MSG_STR_LEN); // enable line history support for instant message bar mInputEditor->setEnableLineHistory(TRUE); //*TODO we probably need the same "awaiting message" thing in LLIMFloater LLIMModel::LLIMSession* im_session = LLIMModel::getInstance()->findIMSession(mSessionUUID); if (!im_session) { llerror("im session with id " + mSessionUUID.asString() + " does not exist!", 0); return; } mSessionInitialized = im_session->mSessionInitialized; if (!mSessionInitialized) { //locally echo a little "starting session" message LLUIString session_start = sSessionStartString; session_start.setArg("[NAME]", getTitle()); mSessionStartMsgPos = mHistoryEditor->getWText().length(); addHistoryLine( session_start, LLUIColorTable::instance().getColor("SystemChatColor"), false); } } LLFloaterIMPanel::~LLFloaterIMPanel() { //delete focus lost callback mFocusCallbackConnection.disconnect(); } BOOL LLFloaterIMPanel::postBuild() { setVisibleCallback(boost::bind(&LLFloaterIMPanel::onVisibilityChange, this, _2)); mInputEditor = getChild("chat_editor"); mInputEditor->setFocusReceivedCallback( boost::bind(onInputEditorFocusReceived, _1, this) ); mFocusCallbackConnection = mInputEditor->setFocusLostCallback( boost::bind(onInputEditorFocusLost, _1, this)); mInputEditor->setKeystrokeCallback( onInputEditorKeystroke, this ); mInputEditor->setCommitCallback( onCommitChat, this ); mInputEditor->setCommitOnFocusLost( FALSE ); mInputEditor->setRevertOnEsc( FALSE ); mInputEditor->setReplaceNewlinesWithSpaces( FALSE ); childSetAction("profile_callee_btn", onClickProfile, this); childSetAction("group_info_btn", onClickGroupInfo, this); childSetAction("start_call_btn", onClickStartCall, this); childSetAction("end_call_btn", onClickEndCall, this); childSetAction("send_btn", onClickSend, this); childSetAction("toggle_active_speakers_btn", onClickToggleActiveSpeakers, this); childSetAction("moderator_kick_speaker", onKickSpeaker, this); //LLButton* close_btn = getChild("close_btn"); //close_btn->setClickedCallback(&LLFloaterIMPanel::onClickClose, this); mHistoryEditor = getChild("im_history"); if ( IM_SESSION_GROUP_START == mDialog ) { childSetEnabled("profile_btn", FALSE); } if(!mProfileButtonEnabled) { childSetEnabled("profile_callee_btn", FALSE); } sTitleString = getString("title_string"); sTypingStartString = getString("typing_start_string"); sSessionStartString = getString("session_start_string"); if (mSpeakerPanel) { mSpeakerPanel->refreshSpeakers(); } if (mDialog == IM_NOTHING_SPECIAL) { childSetAction("mute_btn", onClickMuteVoice, this); childSetCommitCallback("speaker_volume", onVolumeChange, this); } setDefaultBtn("send_btn"); return TRUE; } void* LLFloaterIMPanel::createSpeakersPanel(void* data) { LLFloaterIMPanel* floaterp = (LLFloaterIMPanel*)data; LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(floaterp->mSessionUUID); floaterp->mSpeakerPanel = new LLPanelActiveSpeakers(speaker_mgr, TRUE); return floaterp->mSpeakerPanel; } //static void LLFloaterIMPanel::onClickMuteVoice(void* user_data) { LLFloaterIMPanel* floaterp = (LLFloaterIMPanel*)user_data; if (floaterp) { BOOL is_muted = LLMuteList::getInstance()->isMuted(floaterp->mOtherParticipantUUID, LLMute::flagVoiceChat); LLMute mute(floaterp->mOtherParticipantUUID, floaterp->getTitle(), LLMute::AGENT); if (!is_muted) { LLMuteList::getInstance()->add(mute, LLMute::flagVoiceChat); } else { LLMuteList::getInstance()->remove(mute, LLMute::flagVoiceChat); } } } //static void LLFloaterIMPanel::onVolumeChange(LLUICtrl* source, void* user_data) { LLFloaterIMPanel* floaterp = (LLFloaterIMPanel*)user_data; if (floaterp) { LLVoiceClient::getInstance()->setUserVolume(floaterp->mOtherParticipantUUID, (F32)source->getValue().asReal()); } } // virtual void LLFloaterIMPanel::draw() { LLViewerRegion* region = gAgent.getRegion(); BOOL enable_connect = (region && region->getCapability("ChatSessionRequest") != "") && mSessionInitialized && LLVoiceClient::getInstance()->voiceEnabled() && mCallBackEnabled; // hide/show start call and end call buttons LLVoiceChannel* voice_channel = LLIMModel::getInstance()->getVoiceChannel(mSessionUUID); if (!voice_channel) return; childSetVisible("end_call_btn", LLVoiceClient::getInstance()->voiceEnabled() && voice_channel->getState() >= LLVoiceChannel::STATE_CALL_STARTED); childSetVisible("start_call_btn", LLVoiceClient::getInstance()->voiceEnabled() && voice_channel->getState() < LLVoiceChannel::STATE_CALL_STARTED); childSetEnabled("start_call_btn", enable_connect); childSetEnabled("send_btn", !childGetValue("chat_editor").asString().empty()); LLPointer self_speaker; LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionUUID); if (speaker_mgr) { self_speaker = speaker_mgr->findSpeaker(gAgent.getID()); } if(!mTextIMPossible) { mInputEditor->setEnabled(FALSE); mInputEditor->setLabel(getString("unavailable_text_label")); } else if (self_speaker.notNull() && self_speaker->mModeratorMutedText) { mInputEditor->setEnabled(FALSE); mInputEditor->setLabel(getString("muted_text_label")); } else { mInputEditor->setEnabled(TRUE); mInputEditor->setLabel(getString("default_text_label")); } // show speakers window when voice first connects if (mShowSpeakersOnConnect && voice_channel->isActive()) { childSetVisible("active_speakers_panel", TRUE); mShowSpeakersOnConnect = FALSE; } childSetValue("toggle_active_speakers_btn", childIsVisible("active_speakers_panel")); if (mTyping) { // Time out if user hasn't typed for a while. if (mLastKeystrokeTimer.getElapsedTimeF32() > LLAgent::TYPING_TIMEOUT_SECS) { setTyping(FALSE); } // If we are typing, and it's been a little while, send the // typing indicator if (!mSentTypingState && mFirstKeystrokeTimer.getElapsedTimeF32() > 1.f) { sendTypingState(TRUE); mSentTypingState = TRUE; } } // use embedded panel if available if (mSpeakerPanel) { if (mSpeakerPanel->getVisible()) { mSpeakerPanel->refreshSpeakers(); } } else { // refresh volume and mute checkbox childSetVisible("speaker_volume", LLVoiceClient::getInstance()->voiceEnabled() && voice_channel->isActive()); childSetValue("speaker_volume", LLVoiceClient::getInstance()->getUserVolume(mOtherParticipantUUID)); childSetValue("mute_btn", LLMuteList::getInstance()->isMuted(mOtherParticipantUUID, LLMute::flagVoiceChat)); childSetVisible("mute_btn", LLVoiceClient::getInstance()->voiceEnabled() && voice_channel->isActive()); } LLFloater::draw(); } 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 LLFloaterIMPanel::inviteToSession(const std::vector& ids) { LLViewerRegion* region = gAgent.getRegion(); if (!region) { return FALSE; } S32 count = ids.size(); if( isInviteAllowed() && (count > 0) ) { llinfos << "LLFloaterIMPanel::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"] = mSessionUUID; LLHTTPClient::post( url, data, new LLSessionInviteResponder( mSessionUUID)); } else { llinfos << "LLFloaterIMPanel::inviteToSession -" << " no need to invite agents for " << mDialog << llendl; // successful add, because everyone that needed to get added // was added. } return TRUE; } void LLFloaterIMPanel::addHistoryLine(const std::string &utf8msg, const LLColor4& color, bool log_to_file, const LLUUID& source, const std::string& name) { // start tab flashing when receiving im for background session from user if (source != LLUUID::null) { LLMultiFloater* hostp = getHost(); if( !isInVisibleChain() && hostp && source != gAgent.getID()) { hostp->setFloaterFlashing(this, TRUE); } } // Now we're adding the actual line of text, so erase the // "Foo is typing..." text segment, and the optional timestamp // if it was present. JC removeTypingIndicator(NULL); // Actually add the line std::string timestring; bool prepend_newline = true; if (gSavedSettings.getBOOL("IMShowTimestamps")) { timestring = mHistoryEditor->appendTime(prepend_newline); prepend_newline = false; } std::string separator_string(": "); // 'name' is a sender name that we want to hotlink so that clicking on it opens a profile. if (!name.empty()) // If name exists, then add it to the front of the message. { // Don't hotlink any messages from the system (e.g. "Second Life:"), so just add those in plain text. if (name == SYSTEM_FROM) { mHistoryEditor->appendText(name + separator_string, prepend_newline, LLStyle::Params().color(color)); } else { // Convert the name to a hotlink and add to message. mHistoryEditor->appendText(name + separator_string, prepend_newline, LLStyleMap::instance().lookupAgent(source)); } prepend_newline = false; } mHistoryEditor->appendText(utf8msg, prepend_newline, LLStyle::Params().color(color)); mHistoryEditor->blockUndo(); if (!isInVisibleChain()) { mNumUnreadMessages++; } } void LLFloaterIMPanel::setInputFocus( BOOL b ) { mInputEditor->setFocus( b ); } void LLFloaterIMPanel::selectAll() { mInputEditor->selectAll(); } void LLFloaterIMPanel::selectNone() { mInputEditor->deselect(); } BOOL LLFloaterIMPanel::handleKeyHere( KEY key, MASK mask ) { BOOL handled = FALSE; if( KEY_RETURN == key && mask == MASK_NONE) { sendMsg(); handled = TRUE; } else if ( KEY_ESCAPE == key ) { handled = TRUE; gFocusMgr.setKeyboardFocus(NULL); } // May need to call base class LLPanel::handleKeyHere if not handled // in order to tab between buttons. JNC 1.2.2002 return handled; } BOOL LLFloaterIMPanel::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, mSessionUUID, 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 LLFloaterIMPanel::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 LLFloaterIMPanel::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 LLFloaterIMPanel::isInviteAllowed() const { return ( (IM_SESSION_CONFERENCE_START == mDialog) || (IM_SESSION_INVITE == mDialog) ); } // static void LLFloaterIMPanel::onTabClick(void* userdata) { LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; self->setInputFocus(TRUE); } // static void LLFloaterIMPanel::onClickProfile( void* userdata ) { // Bring up the Profile window LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; if (self->getOtherParticipantID().notNull()) { LLAvatarActions::showProfile(self->getOtherParticipantID()); } } // static void LLFloaterIMPanel::onClickGroupInfo( void* userdata ) { // Bring up the Profile window LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; LLGroupActions::show(self->mSessionUUID); } // static void LLFloaterIMPanel::onClickClose( void* userdata ) { LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; if(self) { self->closeFloater(); } } // static void LLFloaterIMPanel::onClickStartCall(void* userdata) { LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; gIMMgr->startCall(self->mSessionUUID); } // static void LLFloaterIMPanel::onClickEndCall(void* userdata) { LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; gIMMgr->endCall(self->mSessionUUID); } // static void LLFloaterIMPanel::onClickSend(void* userdata) { LLFloaterIMPanel* self = (LLFloaterIMPanel*)userdata; self->sendMsg(); } // static void LLFloaterIMPanel::onClickToggleActiveSpeakers(void* userdata) { LLFloaterIMPanel* self = (LLFloaterIMPanel*)userdata; self->childSetVisible("active_speakers_panel", !self->childIsVisible("active_speakers_panel")); } // static void LLFloaterIMPanel::onCommitChat(LLUICtrl* caller, void* userdata) { LLFloaterIMPanel* self= (LLFloaterIMPanel*) userdata; self->sendMsg(); } // static void LLFloaterIMPanel::onInputEditorFocusReceived( LLFocusableElement* caller, void* userdata ) { LLFloaterIMPanel* self= (LLFloaterIMPanel*) userdata; self->mHistoryEditor->setCursorAndScrollToEnd(); } // static void LLFloaterIMPanel::onInputEditorFocusLost(LLFocusableElement* caller, void* userdata) { LLFloaterIMPanel* self = (LLFloaterIMPanel*) userdata; self->setTyping(FALSE); } // static void LLFloaterIMPanel::onInputEditorKeystroke(LLLineEditor* caller, void* userdata) { LLFloaterIMPanel* self = (LLFloaterIMPanel*)userdata; std::string text = self->mInputEditor->getText(); if (!text.empty()) { self->setTyping(TRUE); } else { // Deleting all text counts as stopping typing. self->setTyping(FALSE); } } // virtual void LLFloaterIMPanel::onClose(bool app_quitting) { setTyping(FALSE); gIMMgr->leaveSession(mSessionUUID); // *HACK hide the voice floater LLFloaterReg::hideInstance("voice_call", mSessionUUID); } void LLFloaterIMPanel::onVisibilityChange(const LLSD& new_visibility) { if (new_visibility.asBoolean()) { mNumUnreadMessages = 0; } LLVoiceChannel* voice_channel = LLIMModel::getInstance()->getVoiceChannel(mSessionUUID); if (voice_channel && voice_channel->getState() == LLVoiceChannel::STATE_CONNECTED) { if (new_visibility.asBoolean()) LLFloaterReg::showInstance("voice_call", mSessionUUID); else LLFloaterReg::hideInstance("voice_call", mSessionUUID); } } void LLFloaterIMPanel::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()) { // store sent line in history, duplicates will get filtered if (mInputEditor) mInputEditor->updateHistory(); // 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, mSessionUUID, mOtherParticipantUUID, mDialog); } else { //queue up the message to send once the session is //initialized mQueuedMsgsForInit.append(utf8_text); } } LLViewerStats::getInstance()->incStat(LLViewerStats::ST_IM_COUNT); mInputEditor->setText(LLStringUtil::null); } // Don't need to actually send the typing stop message, the other // client will infer it from receiving the message. mTyping = FALSE; mSentTypingState = TRUE; } void LLFloaterIMPanel::processSessionUpdate(const LLSD& session_update) { if ( session_update.has("moderated_mode") && session_update["moderated_mode"].has("voice") ) { BOOL voice_moderated = session_update["moderated_mode"]["voice"]; if (voice_moderated) { setTitle(mSessionLabel + std::string(" ") + getString("moderated_chat_label")); } else { setTitle(mSessionLabel); } //update the speakers dropdown too, if it's available if (mSpeakerPanel) { mSpeakerPanel->setVoiceModerationCtrlMode(voice_moderated); } } } void LLFloaterIMPanel::sessionInitReplyReceived(const LLUUID& session_id) { mSessionUUID = session_id; mSessionInitialized = TRUE; //we assume the history editor hasn't moved at all since //we added the starting session message //so, we count how many characters to remove S32 chars_to_remove = mHistoryEditor->getWText().length() - mSessionStartMsgPos; mHistoryEditor->removeTextFromEnd(chars_to_remove); //and now, send the queued msg LLSD::array_iterator iter; for ( iter = mQueuedMsgsForInit.beginArray(); iter != mQueuedMsgsForInit.endArray(); ++iter) { LLIMModel::sendMessage( iter->asString(), mSessionUUID, mOtherParticipantUUID, mDialog); } } void LLFloaterIMPanel::setTyping(BOOL typing) { LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionUUID); if (typing) { // Every time you type something, reset this timer mLastKeystrokeTimer.reset(); if (!mTyping) { // You just started typing. mFirstKeystrokeTimer.reset(); // Will send typing state after a short delay. mSentTypingState = FALSE; } if (speaker_mgr) speaker_mgr->setSpeakerTyping(gAgent.getID(), TRUE); } else { if (mTyping) { // you just stopped typing, send state immediately sendTypingState(FALSE); mSentTypingState = TRUE; } if (speaker_mgr) speaker_mgr->setSpeakerTyping(gAgent.getID(), FALSE); } mTyping = typing; } void LLFloaterIMPanel::sendTypingState(BOOL typing) { // Don't want to send typing indicators to multiple people, potentially too // much network traffic. Only send in person-to-person IMs. if (mDialog != IM_NOTHING_SPECIAL) return; LLIMModel::instance().sendTypingState(mSessionUUID, mOtherParticipantUUID, typing); } void LLFloaterIMPanel::processIMTyping(const LLIMInfo* im_info, BOOL typing) { if (typing) { // other user started typing addTypingIndicator(im_info->mName); } else { // other user stopped typing removeTypingIndicator(im_info); } } void LLFloaterIMPanel::addTypingIndicator(const std::string &name) { // we may have lost a "stop-typing" packet, don't add it twice if (!mOtherTyping) { mTypingLineStartIndex = mHistoryEditor->getWText().length(); LLUIString typing_start = sTypingStartString; typing_start.setArg("[NAME]", name); addHistoryLine(typing_start, LLUIColorTable::instance().getColor("SystemChatColor"), false); mOtherTypingName = name; mOtherTyping = TRUE; } // MBW -- XXX -- merge from release broke this (argument to this function changed from an LLIMInfo to a name) // Richard will fix. // mSpeakers->setSpeakerTyping(im_info->mFromID, TRUE); } void LLFloaterIMPanel::removeTypingIndicator(const LLIMInfo* im_info) { if (mOtherTyping) { // Must do this first, otherwise addHistoryLine calls us again. mOtherTyping = FALSE; S32 chars_to_remove = mHistoryEditor->getWText().length() - mTypingLineStartIndex; mHistoryEditor->removeTextFromEnd(chars_to_remove); if (im_info) { LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionUUID); if (speaker_mgr) { speaker_mgr->setSpeakerTyping(im_info->mFromID, FALSE); } } } } //static void LLFloaterIMPanel::onKickSpeaker(void* user_data) { }