/** * @file fschathistory.cpp * @brief LLTextEditor base class * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General PublicatarActions::pay(getAvat * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ // Original file: LLChatHistory.cpp #include "llviewerprecompiledheaders.h" #include "fschathistory.h" #include #include "llavatarnamecache.h" #include "llinstantmessage.h" #include "llimview.h" #include "llcommandhandler.h" #include "llpanel.h" #include "lluictrlfactory.h" #include "llscrollcontainer.h" #include "llagent.h" #include "llagentdata.h" #include "llavataractions.h" #include "llavatariconctrl.h" #include "llcallingcard.h" //for LLAvatarTracker #include "llgroupactions.h" #include "llgroupmgr.h" #include "llspeakers.h" //for LLIMSpeakerMgr #include "lltrans.h" #include "llfloaterreg.h" #include "llfloaterreporter.h" #include "llfloatersidepanelcontainer.h" #include "llstylemap.h" #include "llslurl.h" #include "lllayoutstack.h" #include "llnotifications.h" #include "llnotificationsutil.h" #include "lltoastnotifypanel.h" #include "lltooltip.h" #include "llviewerregion.h" #include "llviewertexteditor.h" #include "llworld.h" #include "lluiconstants.h" #include "llstring.h" #include "llurlaction.h" #include "llviewercontrol.h" #include "llviewermenu.h" #include "llviewernetwork.h" #if LL_SDL2 #include "llwindow.h" #endif #include "fscommon.h" #include "llchatentry.h" #include "llfocusmgr.h" #include "llkeyboard.h" #include "llpanelblockedlist.h" #include "rlvactions.h" #include "rlvcommon.h" #include "rlvhandler.h" static LLDefaultChildRegistry::Register r("fs_chat_history"); const static std::string NEW_LINE(rawstr_to_utf8("\n")); const static std::string SLURL_APP_AGENT = "secondlife:///app/agent/"; const static std::string SLURL_ABOUT = "/about"; // support for secondlife:///app/objectim/{UUID}/ SLapps class LLObjectIMHandler : public LLCommandHandler { public: // requests will be throttled from a non-trusted browser LLObjectIMHandler() : LLCommandHandler("objectim", UNTRUSTED_THROTTLE) {} bool handle(const LLSD& params, const LLSD& query_map, const std::string& grid, LLMediaCtrl* web) { if (params.size() < 1) { return false; } LLUUID object_id; if (!object_id.set(params[0], false)) { return false; } LLSD payload; payload["object_id"] = object_id; payload["owner_id"] = query_map["owner"]; // [RLVa:KB] - Checked: 2010-11-02 (RLVa-1.2.2a) | Modified: RLVa-1.2.2a if (query_map.has("rlv_shownames")) payload["rlv_shownames"] = query_map["rlv_shownames"]; // [/RLVa:KB] payload["name"] = query_map["name"]; payload["slurl"] = LLWeb::escapeURL(query_map["slurl"]); payload["group_owned"] = query_map["groupowned"]; LLFloaterReg::showInstance("inspect_remote_object", payload); return true; } }; LLObjectIMHandler gObjectIMHandler; class FSChatHistoryHeader: public LLPanel { public: FSChatHistoryHeader() : LLPanel(), mInfoCtrl(NULL), // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.2a) | Added: RLVa-1.2.0f mShowContextMenu(true), mShowInfoCtrl(true), // [/RLVa:KB] mPopupMenuHandleAvatar(), mPopupMenuHandleObject(), mAvatarID(), mSourceType(CHAT_SOURCE_UNKNOWN), mType(CHAT_TYPE_NORMAL), // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports mFrom(), mSessionID(), mCreationTime(time_corrected()), mMinUserNameWidth(0), mUserNameFont(NULL), mUserNameTextBox(NULL), mNeedsTimeBox(true), mTimeBoxTextBox(NULL), mHeaderLayoutStack(NULL), mAvatarNameCacheConnection(), mTime(0) {} static FSChatHistoryHeader* createInstance(const std::string& file_name) { FSChatHistoryHeader* pInstance = new FSChatHistoryHeader; pInstance->buildFromFile(file_name); return pInstance; } ~FSChatHistoryHeader() { if (mAvatarNameCacheConnection.connected()) { mAvatarNameCacheConnection.disconnect(); } auto menu = mPopupMenuHandleAvatar.get(); if (menu) { menu->die(); mPopupMenuHandleAvatar.markDead(); } menu = mPopupMenuHandleObject.get(); if (menu) { menu->die(); mPopupMenuHandleObject.markDead(); } } bool handleMouseUp(S32 x, S32 y, MASK mask) { return LLPanel::handleMouseUp(x,y,mask); } void onObjectIconContextMenuItemClicked(const LLSD& userdata) { std::string level = userdata.asString(); if (level == "profile") { LLFloaterReg::showInstance("inspect_remote_object", mObjectData); } else if (level == "block") { LLMuteList::getInstance()->add(LLMute(getAvatarId(), mFrom, LLMute::OBJECT)); LLPanelBlockedList::showPanelAndSelect(getAvatarId()); } else if (level == "unblock") { LLMuteList::getInstance()->remove(LLMute(getAvatarId(), mFrom, LLMute::OBJECT)); } else if (level == "map") { std::string url = "secondlife://" + mObjectData["slurl"].asString(); LLUrlAction::showLocationOnMap(url); } else if (level == "teleport") { std::string url = "secondlife://" + mObjectData["slurl"].asString(); LLUrlAction::teleportToLocation(url); } else if (level == "obj_zoom_in") { LLUUID obj_id = mObjectData["object_id"]; if (obj_id.notNull()) { handle_zoom_to_object(obj_id); } } } bool onObjectIconContextMenuItemVisible(const LLSD& userdata) { std::string level = userdata.asString(); if (level == "is_blocked") { return LLMuteList::getInstance()->isMuted(getAvatarId(), mFrom, LLMute::flagTextChat); } else if (level == "not_blocked") { return !LLMuteList::getInstance()->isMuted(getAvatarId(), mFrom, LLMute::flagTextChat); } else if (level == "obj_zoom_in") { LLUUID obj_id = mObjectData["object_id"]; if (obj_id.notNull()) { return nullptr != gObjectList.findObject(mAvatarID); } return false; } return false; } void banGroupMember(const LLUUID& participant_uuid) { LLUUID group_uuid = mSessionID; LLGroupMgrGroupData* gdatap = LLGroupMgr::getInstance()->getGroupData(group_uuid); if (!gdatap) { // Not a group return; } gdatap->banMemberById(participant_uuid); } bool canBanInGroup() { LLUUID group_uuid = mSessionID; LLGroupMgrGroupData* gdatap = LLGroupMgr::getInstance()->getGroupData(group_uuid); if (!gdatap) { // Not a group return false; } if (gAgent.hasPowerInGroup(group_uuid, GP_ROLE_REMOVE_MEMBER) && gAgent.hasPowerInGroup(group_uuid, GP_GROUP_BAN_ACCESS)) { return true; } return false; } bool canBanGroupMember(const LLUUID& participant_uuid) { LLUUID group_uuid = mSessionID; LLGroupMgrGroupData* gdatap = LLGroupMgr::getInstance()->getGroupData(group_uuid); if (!gdatap) { // Not a group return false; } if (gdatap->mPendingBanRequest) { return false; } if (gAgentID == getAvatarId()) { //Don't ban self return false; } if (gdatap->isRoleMemberDataComplete()) { if (gdatap->mMembers.size()) { LLGroupMgrGroupData::member_list_t::iterator mi = gdatap->mMembers.find(participant_uuid); if (mi != gdatap->mMembers.end()) { LLGroupMemberData* member_data = (*mi).second; // Is the member an owner? if (member_data && member_data->isInRole(gdatap->mOwnerRole)) { return false; } if (gAgent.hasPowerInGroup(group_uuid, GP_ROLE_REMOVE_MEMBER) && gAgent.hasPowerInGroup(group_uuid, GP_GROUP_BAN_ACCESS)) { return true; } } } } LLSpeakerMgr * speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if (speaker_mgr) { LLSpeaker * speakerp = speaker_mgr->findSpeaker(participant_uuid).get(); if (speakerp && gAgent.hasPowerInGroup(group_uuid, GP_ROLE_REMOVE_MEMBER) && gAgent.hasPowerInGroup(group_uuid, GP_GROUP_BAN_ACCESS)) { return true; } } return false; } bool isGroupModerator() { LLSpeakerMgr * speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if (!speaker_mgr) { LL_WARNS() << "Speaker manager is missing" << LL_ENDL; return false; } // Is session a group call/chat? if(gAgent.isInGroup(mSessionID)) { LLSpeaker * speakerp = speaker_mgr->findSpeaker(gAgentID).get(); // Is agent a moderator? return speakerp && speakerp->mIsModerator; } return false; } bool canModerate(const std::string& userdata) { // only group moderators can perform actions related to this "enable callback" if (!isGroupModerator() || gAgentID == getAvatarId()) { return false; } LLSpeakerMgr * speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if (!speaker_mgr) { return false; } LLSpeaker * speakerp = speaker_mgr->findSpeaker(getAvatarId()).get(); if (!speakerp) { return false; } bool voice_channel = speakerp->isInVoiceChannel(); if ("can_moderate_voice" == userdata) { return voice_channel; } else if ("can_mute" == userdata) { return voice_channel && (speakerp->mStatus != LLSpeaker::STATUS_MUTED); } else if ("can_unmute" == userdata) { return speakerp->mStatus == LLSpeaker::STATUS_MUTED; } else if ("can_allow_text_chat" == userdata) { return true; } return false; } void onAvatarIconContextMenuItemClicked(const LLSD& userdata) { std::string param = userdata.asString(); if (param == "profile") { LLAvatarActions::showProfile(getAvatarId()); } else if (param == "im") { LLAvatarActions::startIM(getAvatarId()); } else if (param == "teleport_to") { LLAvatarActions::teleportTo(getAvatarId()); } else if (param == "teleport") { LLAvatarActions::offerTeleport(getAvatarId()); } else if (param == "request_teleport") { LLAvatarActions::teleportRequest(getAvatarId()); } else if (param == "voice_call") { LLAvatarActions::startCall(getAvatarId()); } else if (param == "chat_history") { LLAvatarActions::viewChatHistory(getAvatarId()); } else if (param == "add") { LLAvatarActions::requestFriendshipDialog(getAvatarId(), mFrom); } else if (param == "add_set") { LLAvatarActions::addToContactSet(getAvatarId()); } else if (param == "remove") { LLAvatarActions::removeFriendDialog(getAvatarId()); } else if (param == "invite_to_group") { LLAvatarActions::inviteToGroup(getAvatarId()); } else if (param == "zoom_in") { handle_zoom_to_object(getAvatarId()); } else if (param == "map") { LLAvatarActions::showOnMap(getAvatarId()); } else if (param == "track") { LLAvatarActions::track(getAvatarId()); } else if (param == "share") { LLAvatarActions::share(getAvatarId()); } else if (param == "pay") { LLAvatarActions::pay(getAvatarId()); } else if (param == "copy_name") { LLUrlAction::copyLabelToClipboard(LLSLURL("agent", getAvatarId(), "inspect").getSLURLString()); } else if (param == "copy_url") { LLUrlAction::copyURLToClipboard(LLSLURL("agent", getAvatarId(), "about").getSLURLString()); } else if (param == "report_abuse") { std::string time_string; if (mTime > 0) // have frame time { time_t current_time = time_corrected(); time_t message_time = (time_t)(current_time - LLFrameTimer::getElapsedSeconds() + mTime); time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" + LLTrans::getString("TimeDay") + "]/[" + LLTrans::getString("TimeYear") + "] [" + LLTrans::getString("TimeHour") + "]:[" + LLTrans::getString("TimeMin") + "]"; LLSD substitution; substitution["datetime"] = (S32)message_time; LLStringUtil::format(time_string, substitution); } else { // From history. This might be empty or not full. // See LLChatLogParser::parse time_string = getChild("time_box")->getValue().asString(); // Just add current date if not full. // Should be fine since both times are supposed to be stl if (!time_string.empty() && time_string.size() < 7) { time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" + LLTrans::getString("TimeDay") + "]/[" + LLTrans::getString("TimeYear") + "] " + time_string; LLSD substitution; // To avoid adding today's date to yesterday's timestamp, // use creation time instead of current time substitution["datetime"] = (S32)mCreationTime; LLStringUtil::format(time_string, substitution); } } LLFloaterReporter::showFromChat(mAvatarID, mFrom, time_string, mText); } else if (param == "block_unblock") { LLAvatarActions::toggleMute(getAvatarId(), LLMute::flagVoiceChat); } else if (param == "mute_unmute") { LLAvatarActions::toggleMute(getAvatarId(), LLMute::flagTextChat); } else if (param == "allow_text_chat") { LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); speaker_mgr->allowTextChat(getAvatarId(), true); } else if (param == "forbid_text_chat") { LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); speaker_mgr->allowTextChat(getAvatarId(), false); } else if (param == "group_mute") { LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if (speaker_mgr) { speaker_mgr->moderateVoiceParticipant(getAvatarId(), false); } } else if (param == "group_unmute") { LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if (speaker_mgr) { speaker_mgr->moderateVoiceParticipant(getAvatarId(), true); } } else if (param == "ban_member") { // don't do safety checks as participant list is broken, just ban // banGroupMember(getAvatarId()); std::vector ids; ids.push_back(getAvatarId()); LLGroupMgr::getInstance()->sendGroupBanRequest(LLGroupMgr::REQUEST_POST, mSessionID, LLGroupMgr::BAN_CREATE, ids); LLGroupMgr::getInstance()->sendGroupMemberEjects(mSessionID, ids); LLGroupMgr::getInstance()->sendGroupMembersRequest(mSessionID); // } } bool onAvatarIconContextMenuItemChecked(const LLSD& userdata) { std::string param = userdata.asString(); if (param == "is_blocked") { return LLMuteList::getInstance()->isMuted(getAvatarId(), LLMute::flagVoiceChat); } if (param == "is_muted") { return LLMuteList::getInstance()->isMuted(getAvatarId(), LLMute::flagTextChat); } else if (param == "is_allowed_text_chat") { if (gAgent.isInGroup(mSessionID)) { LLIMSpeakerMgr* speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); const LLSpeaker * speakerp = speaker_mgr->findSpeaker(getAvatarId()); if (NULL != speakerp) { return !speakerp->mModeratorMutedText; } } return false; } return false; } bool onAvatarIconContextMenuItemEnabled(const LLSD& userdata) { std::string param = userdata.asString(); if (param == "can_allow_text_chat" || param == "can_mute" || param == "can_unmute") { return canModerate(userdata); } else if (param == "report_abuse") { return gAgentID != mAvatarID; } // [RLVa:KB] - @pay else if (param == "can_pay") { return RlvActions::canPayAvatar(getAvatarId()); } // [/RLVa:KB] else if (param == "can_ban_member") { return canBanGroupMember(getAvatarId()); } return false; } bool onAvatarIconContextMenuItemVisible(const LLSD& userdata) { std::string param = userdata.asString(); if (param == "show_mute") { LLSpeakerMgr * speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if (speaker_mgr) { LLSpeaker * speakerp = speaker_mgr->findSpeaker(getAvatarId()).get(); if (speakerp) { return speakerp->isInVoiceChannel() && speakerp->mStatus != LLSpeaker::STATUS_MUTED; } } return false; } else if (param == "show_unmute") { LLSpeakerMgr * speaker_mgr = LLIMModel::getInstance()->getSpeakerManager(mSessionID); if (speaker_mgr) { LLSpeaker * speakerp = speaker_mgr->findSpeaker(getAvatarId()).get(); if (speakerp) { return speakerp->mStatus == LLSpeaker::STATUS_MUTED; } } return false; } return false; } bool postBuild() { setDoubleClickCallback(boost::bind(&FSChatHistoryHeader::showInspector, this)); setMouseEnterCallback(boost::bind(&FSChatHistoryHeader::showInfoCtrl, this)); setMouseLeaveCallback(boost::bind(&FSChatHistoryHeader::hideInfoCtrl, this)); mUserNameTextBox = getChild("user_name"); mTimeBoxTextBox = getChild("time_box"); mHeaderLayoutStack = getChild("header_ls"); mInfoCtrl = LLUICtrlFactory::getInstance()->createFromFile("inspector_info_ctrl.xml", this, LLPanel::child_registry_t::instance()); if (mInfoCtrl) { mInfoCtrl->setCommitCallback(boost::bind(&FSChatHistoryHeader::onClickInfoCtrl, mInfoCtrl)); mInfoCtrl->setVisible(false); } else { LL_ERRS() << "Failed to create an interface element due to missing or corrupted file inspector_info_ctrl.xml" << LL_ENDL; } return LLPanel::postBuild(); } bool pointInChild(const std::string& name,S32 x,S32 y) { LLUICtrl* child = findChild(name); if(!child) return false; LLView* parent = child->getParent(); if(parent!=this) { x-=parent->getRect().mLeft; y-=parent->getRect().mBottom; } S32 local_x = x - child->getRect().mLeft ; S32 local_y = y - child->getRect().mBottom ; return child->pointInView(local_x, local_y); } bool handleRightMouseDown(S32 x, S32 y, MASK mask) { if(pointInChild("avatar_icon",x,y) || pointInChild("user_name",x,y)) { showContextMenu(x,y); return true; } return LLPanel::handleRightMouseDown(x,y,mask); } void showInspector() { // if (mAvatarID.isNull() && CHAT_SOURCE_SYSTEM != mSourceType && CHAT_SOURCE_REGION != mSourceType) return; // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.2a) | Added: RLVa-1.2.0f // Don't double-click show the inspector if we're not showing the info control if ( (!mShowInfoCtrl) || (mAvatarID.isNull() && CHAT_SOURCE_SYSTEM != mSourceType && CHAT_SOURCE_REGION != mSourceType) ) return; // [/RLVa:KB] if (mSourceType == CHAT_SOURCE_OBJECT) { LLFloaterReg::showInstance("inspect_remote_object", mObjectData); } else if (mSourceType == CHAT_SOURCE_AGENT || (mSourceType == CHAT_SOURCE_SYSTEM && mType == CHAT_TYPE_RADAR)) // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports { LLUrlAction::executeSLURL(LLSLURL("agent", mAvatarID, "inspect").getSLURLString()); } //if chat source is system, you may add "else" here to define behaviour. } static void onClickInfoCtrl(LLUICtrl* info_ctrl) { if (!info_ctrl) return; FSChatHistoryHeader* header = dynamic_cast(info_ctrl->getParent()); if (!header) return; header->showInspector(); } const LLUUID& getAvatarId () const { return mAvatarID;} void setup(const LLChat& chat, const LLStyle::Params& style_params, const LLSD& args) { mAvatarID = chat.mFromID; mSessionID = chat.mSessionID; mSourceType = chat.mSourceType; mType = chat.mChatType; // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports mNameStyleParams = style_params; // To be able to report a message, we need a copy of it's text // and it's easier to store text directly than trying to get // it from a lltextsegment or chat's mEditor mText = chat.mText; mTime = chat.mTime; //*TODO overly defensive thing, source type should be maintained out there if((chat.mFromID.isNull() && chat.mFromName.empty()) || (chat.mFromName == SYSTEM_FROM && chat.mFromID.isNull())) { mSourceType = CHAT_SOURCE_SYSTEM; } // Use the original font defined in panel_chat_header.xml mNameStyleParams.font.name = "SansSerifSmall"; // To be able to use the group chat moderator options, we use the // original font style "BOLD" for everything except group chats. // Group chats have the option to show moderators in bold, so // we display both display and username in "NORMAL" for now. static LLCachedControl fsHighlightGroupMods(gSavedSettings, "FSHighlightGroupMods"); LLIMModel::LLIMSession* session = LLIMModel::instance().findIMSession(mSessionID); if (!fsHighlightGroupMods || !session || !session->isGroupSessionType()) { mNameStyleParams.font.style = "BOLD"; } mUserNameFont = mNameStyleParams.font(); if (!mUserNameTextBox) { mUserNameTextBox = getChild("user_name"); mTimeBoxTextBox = getChild("time_box"); } mUserNameTextBox->setReadOnlyColor(mNameStyleParams.readonly_color()); mUserNameTextBox->setColor(mNameStyleParams.color()); mUserNameTextBox->setFont(mUserNameFont); // Make sure we use the correct font style for everything after the display name mNameStyleParams.font.style = style_params.font.style; if (mSourceType == CHAT_SOURCE_TELEPORT && chat.mChatStyle == CHAT_STYLE_TELEPORT_SEP) { mFrom = chat.mFromName; mNeedsTimeBox = false; mUserNameTextBox->setValue(mFrom); updateMinUserNameWidth(); LLUIColor sep_color = LLUIColorTable::instance().getColor("ChatTeleportSeparatorColor"); setTransparentColor(sep_color); mTimeBoxTextBox->setVisible(false); } else if (chat.mFromName.empty() //|| mSourceType == CHAT_SOURCE_SYSTEM // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports || (mSourceType == CHAT_SOURCE_SYSTEM && mType != CHAT_TYPE_RADAR) || mAvatarID.isNull()) { if (mSourceType == CHAT_SOURCE_UNKNOWN) { // Avatar names may come up as CHAT_SOURCE_UNKNOWN - don't append the grid name in that case mFrom = chat.mFromName; } else { mFrom = LLTrans::getString("SECOND_LIFE"); // Will automatically be substituted! if (!chat.mFromName.empty() && (mFrom != chat.mFromName)) { mFrom += " (" + chat.mFromName + ")"; } } mUserNameTextBox->setValue(mFrom); updateMinUserNameWidth(); } else if ((mSourceType == CHAT_SOURCE_AGENT || (mSourceType == CHAT_SOURCE_SYSTEM && mType == CHAT_TYPE_RADAR)) && !mAvatarID.isNull() && chat.mChatStyle != CHAT_STYLE_SERVER_HISTORY && chat.mChatStyle != CHAT_STYLE_HISTORY) { // ...from a normal user, lookup the name and fill in later. // *NOTE: Do not do this for chat history logs, otherwise the viewer // will flood the People API with lookup requests on startup // Start with blank so sample data from XUI XML doesn't // flash on the screen // user_name->setValue( LLSD() ); // fetchAvatarName(); // [RLVa:KB] - Checked: 2010-11-01 (RLVa-1.2.2a) | Added: RLVa-1.2.2a if (!chat.mRlvNamesFiltered) { mUserNameTextBox->setValue( LLSD() ); fetchAvatarName(); } else { // If the agent's chat was subject to @shownames=n we should display their anonimized name mFrom = chat.mFromName; mUserNameTextBox->setValue(mFrom); mUserNameTextBox->setToolTip(mFrom); setToolTip(mFrom); updateMinUserNameWidth(); } // [/RLVa:KB] } else if (chat.mChatStyle == CHAT_STYLE_HISTORY || chat.mChatStyle == CHAT_STYLE_SERVER_HISTORY || (mSourceType == CHAT_SOURCE_AGENT || (mSourceType == CHAT_SOURCE_SYSTEM && mType == CHAT_TYPE_RADAR))) { //if it's an avatar name with a username add formatting auto username_start = chat.mFromName.rfind(" ("); auto username_end = chat.mFromName.rfind(')'); if (username_start != std::string::npos && username_end == (chat.mFromName.length() - 1)) { mFrom = chat.mFromName.substr(0, username_start); mUserNameTextBox->setValue(mFrom); } else { // If the agent's chat was subject to @shownames=n we should display their anonimized name mFrom = chat.mFromName; mUserNameTextBox->setValue(mFrom); updateMinUserNameWidth(); } } else { // ...from an object, just use name as given mFrom = chat.mFromName; mUserNameTextBox->setValue(mFrom); updateMinUserNameWidth(); } setTimeField(chat); // Set up the icon. LLAvatarIconCtrl* icon = getChild("avatar_icon"); if(mSourceType != CHAT_SOURCE_AGENT || mAvatarID.isNull()) icon->setDrawTooltip(false); // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.2a) | Added: RLVa-1.2.0f // Don't show the context menu, info control or avatar icon tooltip if this chat was subject to @shownames=n if ( (chat.mRlvNamesFiltered) && ((mSourceType == CHAT_SOURCE_AGENT || (mSourceType == CHAT_SOURCE_SYSTEM && mType == CHAT_TYPE_RADAR)) || (CHAT_SOURCE_OBJECT == mSourceType)) ) { mShowInfoCtrl = mShowContextMenu = false; icon->setDrawTooltip(false); } // [/RLVa:KB] switch (mSourceType) { case CHAT_SOURCE_AGENT: if (!chat.mRlvNamesFiltered) { icon->setValue(chat.mFromID); } else { icon->setValue(LLSD("Unknown_Icon")); } break; case CHAT_SOURCE_OBJECT: icon->setValue(LLSD("OBJECT_Icon")); break; case CHAT_SOURCE_SYSTEM: case CHAT_SOURCE_REGION: //icon->setValue(LLSD("SL_Logo")); // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports if(chat.mChatType == CHAT_TYPE_RADAR) { if (!chat.mRlvNamesFiltered) { icon->setValue(chat.mFromID); } else { icon->setValue(LLSD("Unknown_Icon")); } } else { icon->setValue(LLSD("SL_Logo")); } // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports break; case CHAT_SOURCE_TELEPORT: icon->setValue(LLSD("Command_Destinations_Icon")); break; case CHAT_SOURCE_UNKNOWN: icon->setValue(LLSD("Unknown_Icon")); break; } // In case the message came from an object, save the object info // to be able properly show its profile. if ( chat.mSourceType == CHAT_SOURCE_OBJECT) { std::string slurl = args["slurl"].asString(); if (slurl.empty()) { LLViewerRegion *region = LLWorld::getInstance()->getRegionFromPosAgent(chat.mPosAgent); if(region) { LLSLURL region_slurl(region->getName(), chat.mPosAgent); slurl = region_slurl.getLocationString(); } } LLSD payload; payload["object_id"] = chat.mFromID; payload["name"] = chat.mFromName; payload["owner_id"] = chat.mOwnerID; payload["slurl"] = LLWeb::escapeURL(slurl); mObjectData = payload; } } /*virtual*/ void draw() { LLRect user_name_rect = mUserNameTextBox->getRect(); S32 user_name_width = user_name_rect.getWidth(); S32 time_box_width = mTimeBoxTextBox->getRect().getWidth(); if (mNeedsTimeBox && !mTimeBoxTextBox->getVisible() && user_name_width > mMinUserNameWidth) { user_name_rect.mRight -= time_box_width; mUserNameTextBox->reshape(user_name_rect.getWidth(), user_name_rect.getHeight()); mUserNameTextBox->setRect(user_name_rect); mTimeBoxTextBox->setVisible(true); } LLPanel::draw(); } void updateMinUserNameWidth() { if (mUserNameFont) { const LLWString& text = mUserNameTextBox->getWText(); mMinUserNameWidth = mUserNameFont->getWidth(text.c_str()) + PADDING; } } protected: static const S32 PADDING = 20; void showContextMenu(S32 x,S32 y) { // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.2a) | Added: RLVa-1.2.0f if (!mShowContextMenu) return; // [/RLVa:KB] if(mSourceType == CHAT_SOURCE_SYSTEM) //showSystemContextMenu(x,y); // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports { // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports if(mType == CHAT_TYPE_RADAR) { showAvatarContextMenu(x,y); } else { showSystemContextMenu(x,y); } } // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports if(mAvatarID.notNull() && mSourceType == CHAT_SOURCE_AGENT) showAvatarContextMenu(x,y); if(mAvatarID.notNull() && mSourceType == CHAT_SOURCE_OBJECT) showObjectContextMenu(x,y); } void showSystemContextMenu(S32 x,S32 y) { } void showObjectContextMenu(S32 x,S32 y) { LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandleObject.get(); if (!menu) { LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar registrar_enable; registrar.add("ObjectIcon.Action", boost::bind(&FSChatHistoryHeader::onObjectIconContextMenuItemClicked, this, _2)); registrar_enable.add("ObjectIcon.Visible", boost::bind(&FSChatHistoryHeader::onObjectIconContextMenuItemVisible, this, _2)); menu = LLUICtrlFactory::getInstance()->createFromFile("menu_object_icon.xml", gMenuHolder, LLViewerMenuHolderGL::child_registry_t::instance()); if (menu) { mPopupMenuHandleObject = menu->getHandle(); menu->updateParent(LLMenuGL::sMenuContainer); LLMenuGL::showPopup(this, menu, x, y); } else { LL_WARNS() << " Failed to create menu_object_icon.xml" << LL_ENDL; } } else { LLMenuGL::showPopup(this, menu, x, y); } } void showAvatarContextMenu(S32 x,S32 y) { LLMenuGL* menu = (LLMenuGL*)mPopupMenuHandleAvatar.get(); if (!menu) { LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; LLUICtrl::EnableCallbackRegistry::ScopedRegistrar registrar_enable; registrar.add("AvatarIcon.Action", boost::bind(&FSChatHistoryHeader::onAvatarIconContextMenuItemClicked, this, _2)); registrar_enable.add("AvatarIcon.Check", boost::bind(&FSChatHistoryHeader::onAvatarIconContextMenuItemChecked, this, _2)); registrar_enable.add("AvatarIcon.Enable", boost::bind(&FSChatHistoryHeader::onAvatarIconContextMenuItemEnabled, this, _2)); registrar_enable.add("AvatarIcon.Visible", boost::bind(&FSChatHistoryHeader::onAvatarIconContextMenuItemVisible, this, _2)); menu = LLUICtrlFactory::getInstance()->createFromFile("menu_avatar_icon.xml", gMenuHolder, LLViewerMenuHolderGL::child_registry_t::instance()); if (menu) { mPopupMenuHandleAvatar = menu->getHandle(); } else { LL_WARNS() << " Failed to create menu_avatar_icon.xml" << LL_ENDL; } } if(menu) { bool is_friend = LLAvatarActions::isFriend(mAvatarID); bool is_group_session = gAgent.isInGroup(mSessionID); menu->setItemEnabled("Add Friend", !is_friend); menu->setItemEnabled("Remove Friend", is_friend); menu->setItemVisible("Moderator Options Separator", is_group_session && isGroupModerator()); menu->setItemVisible("Moderator Options", is_group_session && isGroupModerator()); menu->setItemVisible("Group Ban Separator", is_group_session && canBanInGroup()); menu->setItemVisible("BanMember", is_group_session && canBanInGroup()); if(gAgentID == mAvatarID) { menu->setItemEnabled("Add Friend", false); menu->setItemEnabled("Send IM", false); menu->setItemEnabled("Remove Friend", false); menu->setItemEnabled("Offer Teleport",false); menu->setItemEnabled("Request Teleport",false); menu->setItemEnabled("Teleport to",false); menu->setItemEnabled("Voice Call", false); menu->setItemEnabled("Chat History", false); menu->setItemEnabled("Add Contact Set", false); menu->setItemEnabled("Invite Group", false); menu->setItemEnabled("Zoom In", true); menu->setItemEnabled("track", false); menu->setItemEnabled("Share", false); menu->setItemEnabled("Pay", false); menu->setItemEnabled("Block Unblock", false); menu->setItemEnabled("Mute Text", false); } else { if (mSessionID == LLIMMgr::computeSessionID(IM_NOTHING_SPECIAL, mAvatarID)) { menu->setItemVisible("Send IM", false); } menu->setItemEnabled("Teleport to", FSCommon::checkIsActionEnabled(mAvatarID, EFSRegistrarFunctionActionType::FS_RGSTR_ACT_TELEPORT_TO)); menu->setItemEnabled("Offer Teleport", LLAvatarActions::canOfferTeleport(mAvatarID)); menu->setItemEnabled("Request Teleport", LLAvatarActions::canRequestTeleport(mAvatarID)); menu->setItemEnabled("Voice Call", LLAvatarActions::canCall()); menu->setItemEnabled("Zoom In", FSCommon::checkIsActionEnabled(mAvatarID, EFSRegistrarFunctionActionType::FS_RGSTR_ACT_ZOOM_IN)); menu->setItemEnabled("track", FSCommon::checkIsActionEnabled(mAvatarID, EFSRegistrarFunctionActionType::FS_RGSTR_ACT_TRACK_AVATAR)); menu->setItemEnabled("Block Unblock", LLAvatarActions::canBlock(mAvatarID)); menu->setItemEnabled("Mute Text", LLAvatarActions::canBlock(mAvatarID)); menu->setItemEnabled("Chat History", LLLogChat::isTranscriptExist(mAvatarID)); } menu->setItemEnabled("Map", (LLAvatarTracker::instance().isBuddyOnline(mAvatarID) && is_agent_mappable(mAvatarID)) || gAgent.isGodlike() ); menu->buildDrawLabels(); menu->updateParent(LLMenuGL::sMenuContainer); LLMenuGL::showPopup(this, menu, x, y); } } void showInfoCtrl() { // const bool isVisible = !mAvatarID.isNull() && !mFrom.empty() && (CHAT_SOURCE_SYSTEM != mSourceType || mType == CHAT_TYPE_RADAR) && CHAT_SOURCE_REGION != mSourceType; // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.2a) | Added: RLVa-1.2.0f const bool isVisible = mShowInfoCtrl && !mAvatarID.isNull() && !mFrom.empty() && (CHAT_SOURCE_SYSTEM != mSourceType || mType == CHAT_TYPE_RADAR) && CHAT_SOURCE_REGION != mSourceType; // [/RLVa:KB] if (isVisible) { const LLRect sticky_rect = mUserNameTextBox->getRect(); S32 icon_x = llmin(sticky_rect.mLeft + mUserNameTextBox->getTextBoundingRect().getWidth() + 7, sticky_rect.mRight - 3); if (gSavedSettings.getBOOL("ShowChatMiniIcons")) { LLAvatarIconCtrl* icon = getChild("avatar_icon"); icon_x += icon->getRect().getWidth() + icon->getRect().mLeft; } mInfoCtrl->setOrigin(icon_x, sticky_rect.getCenterY() - mInfoCtrl->getRect().getHeight() / 2 ) ; } mInfoCtrl->setVisible(isVisible); } void hideInfoCtrl() { mInfoCtrl->setVisible(false); } private: void setTimeField(const LLChat& chat) { LLRect rect_before = mTimeBoxTextBox->getRect(); mTimeBoxTextBox->setValue(chat.mTimeStr); // set necessary textbox width to fit all text mTimeBoxTextBox->reshapeToFitText(); LLRect rect_after = mTimeBoxTextBox->getRect(); // move rect to the left to correct position... S32 delta_pos_x = rect_before.getWidth() - rect_after.getWidth(); S32 delta_pos_y = rect_before.getHeight() - rect_after.getHeight(); mTimeBoxTextBox->translate(delta_pos_x, delta_pos_y); //... & change width of the name control //const LLRect& user_rect = mUserNameTextBox->getRect(); //mUserNameTextBox->reshape(user_rect.getWidth() + delta_pos_x, user_rect.getHeight()); const LLRect& layoutstack_rect = mHeaderLayoutStack->getRect(); mHeaderLayoutStack->reshape(layoutstack_rect.getWidth() + delta_pos_x, layoutstack_rect.getHeight()); } void fetchAvatarName() { if (mAvatarID.notNull()) { if (mAvatarNameCacheConnection.connected()) { mAvatarNameCacheConnection.disconnect(); } mAvatarNameCacheConnection = LLAvatarNameCache::get(mAvatarID, boost::bind(&FSChatHistoryHeader::onAvatarNameCache, this, _1, _2)); } } void onAvatarNameCache(const LLUUID& agent_id, const LLAvatarName& av_name) { mAvatarNameCacheConnection.disconnect(); mFrom = av_name.getDisplayName(); mUserNameTextBox->setValue( LLSD(mFrom) ); mUserNameTextBox->setToolTip( av_name.getUserName() ); if (gSavedSettings.getBOOL("NameTagShowUsernames") && av_name.useDisplayNames() && !av_name.isDisplayNameDefault()) { LLStyle::Params style_params_name; LLUIColor userNameColor = LLUIColorTable::instance().getColor("EmphasisColor"); style_params_name.color(userNameColor); style_params_name.font.name("SansSerifSmall"); style_params_name.font.style(mNameStyleParams.font.style); style_params_name.readonly_color(userNameColor); mUserNameTextBox->appendText(" - " + av_name.getUserNameForDisplay(), false, style_params_name); } setToolTip( av_name.getUserName() ); // name might have changed, update width updateMinUserNameWidth(); } protected: LLHandle mPopupMenuHandleAvatar; LLHandle mPopupMenuHandleObject; LLUICtrl* mInfoCtrl; LLUUID mAvatarID; LLSD mObjectData; EChatSourceType mSourceType; EChatType mType; // FS:LO FIRE-1439 - Clickable avatar names on local chat radar crossing reports std::string mFrom; LLUUID mSessionID; std::string mText; F64 mTime; // IM's frame time time_t mCreationTime; // Views's time // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.2a) | Added: RLVa-1.2.0f bool mShowContextMenu; bool mShowInfoCtrl; // [/RLVa:KB] S32 mMinUserNameWidth; const LLFontGL* mUserNameFont; LLTextBox* mUserNameTextBox; LLTextBox* mTimeBoxTextBox; LLLayoutStack* mHeaderLayoutStack; LLStyle::Params mNameStyleParams; bool mNeedsTimeBox; private: boost::signals2::connection mAvatarNameCacheConnection; }; FSChatHistory::FSChatHistory(const FSChatHistory::Params& p) : LLTextEditor(p), // FIRE-8600: TAB out of chat history mMessageHeaderFilename(p.message_header), mMessageSeparatorFilename(p.message_separator), mLeftTextPad(p.left_text_pad), mRightTextPad(p.right_text_pad), mLeftWidgetPad(p.left_widget_pad), mRightWidgetPad(p.right_widget_pad), mTopSeparatorPad(p.top_separator_pad), mBottomSeparatorPad(p.bottom_separator_pad), mTopHeaderPad(p.top_header_pad), mBottomHeaderPad(p.bottom_header_pad), mChatInputLine(NULL), // FIRE-8602: Typing in chat history focuses chat input line mIsLastMessageFromLog(false), mScrollToBottom(false), mUnreadChatSources(0) { mLineSpacingPixels = llclamp(gSavedSettings.getS32("FSFontChatLineSpacingPixels"), 0, 36); mTextVAlign = LLFontGL::VAlign::VCENTER; mUseColor = true; setIsObjectBlockedCallback(boost::bind(&LLMuteList::isMuted, LLMuteList::getInstance(), _1, _2, 0)); } LLSD FSChatHistory::getValue() const { return LLSD(getText()); } FSChatHistory::~FSChatHistory() { this->clear(); } void FSChatHistory::updateChatInputLine() { if(!mChatInputLine) { // get our focus root LLUICtrl* focusRoot=findRootMostFocusRoot(); if(focusRoot) { // focus on the next item that is a text input control focusRoot->focusNextItem(true); // remember the control's pointer if it really is a LLLineEditor mChatInputLine = dynamic_cast(gFocusMgr.getKeyboardFocus()); } } } #if LL_SDL2 void FSChatHistory::setFocus(bool b) { LLTextEditor::setFocus(b); // IME - Don't do anything here if IME is disabled if (!gSavedSettings.getBOOL("SDL2IMEEnabled")) { return; } // IME - International input compositing, i.e. for Japanese / Chinese text input updateChatInputLine(); if (b && mChatInputLine) { // Make sure the IME is in the right place, on top of the input line LLRect screen_pos = mChatInputLine->calcScreenRect(); LLCoordGL ime_pos(screen_pos.mLeft, screen_pos.mBottom + gSavedSettings.getS32("SDL2IMEChatHistoryVerticalOffset")); // shift by a few pixels so the IME doesn't pop to the left side when the input // field is very close to the left edge ime_pos.mX = (S32) (ime_pos.mX * LLUI::getScaleFactor().mV[VX]) + 5; ime_pos.mY = (S32) (ime_pos.mY * LLUI::getScaleFactor().mV[VY]); getWindow()->setLanguageTextInput(ime_pos); } // this floater is not an LLPreeditor but we are only interested in the pointer anyway // so hopefully we will get away with this getWindow()->allowLanguageTextInput((LLPreeditor*) this, true); } #endif void FSChatHistory::initFromParams(const FSChatHistory::Params& p) { // initialize the LLTextEditor base class first ... -Zi LLTextEditor::initFromParams(p); /// then set up these properties, because otherwise they would get lost when we /// do this in the constructor only -Zi setEnabled(false); setReadOnly(true); setShowContextMenu(true); } LLView* FSChatHistory::getSeparator() { LLPanel* separator = LLUICtrlFactory::getInstance()->createFromFile(mMessageSeparatorFilename, NULL, LLPanel::child_registry_t::instance()); return separator; } LLView* FSChatHistory::getHeader(const LLChat& chat,const LLStyle::Params& style_params, const LLSD& args) { FSChatHistoryHeader* header = FSChatHistoryHeader::createInstance(mMessageHeaderFilename); if (header) { header->setup(chat, style_params, args); } return header; } void FSChatHistory::clear() { mLastFromName.clear(); // workaround: Setting the text to an empty line before clear() gets rid of // the scrollbar, if present, which otherwise would get stuck until the next // line was appended. -Zi setText(std::string(" \n")); // FIRE-8600: TAB out of chat history mLastFromID = LLUUID::null; } enum e_moderation_options { NORMAL = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4, BOLD_UNDERLINE = 5, ITALIC_UNDERLINE = 6, BOLD_ITALIC_UNDERLINE = 7 }; std::string applyModeratorStyle(U32 moderator_style) { std::string style; switch (moderator_style) { case BOLD: style = "BOLD"; break; case ITALIC: style = "ITALIC"; break; case BOLD_ITALIC: style = "BOLDITALIC"; break; case UNDERLINE: style = "UNDERLINE"; break; case BOLD_UNDERLINE: style = "BOLDUNDERLINE"; break; case ITALIC_UNDERLINE: style = "ITALICUNDERLINE"; break; case BOLD_ITALIC_UNDERLINE: style = "BOLDITALICUNDERLINE"; break; case NORMAL: default: style = "NORMAL"; break; } return style; } static LLTrace::BlockTimerStatHandle FTM_APPEND_MESSAGE("Append Chat Message"); void FSChatHistory::appendMessage(const LLChat& chat, const LLSD &args, const LLStyle::Params& input_append_params) { LL_RECORD_BLOCK_TIME(FTM_APPEND_MESSAGE); // Ansa: FIRE-12754: Hack around a weird issue where the doc size magically increases by 1px // during draw if the doc exceeds the visible space and the scrollbar is getting visible. mScrollToBottom = (mScroller->isAtBottom() || mScroller->getScrollbar(LLScrollContainer::VERTICAL)->getDocPosMax() <= 1); bool use_plain_text_chat_history = args["use_plain_text_chat_history"].asBoolean(); bool square_brackets = false; // square brackets necessary for a system messages bool is_p2p = args.has("is_p2p") && args["is_p2p"].asBoolean(); bool is_conversation_log = args.has("conversation_log") && args["conversation_log"].asBoolean(); // Don't dim chat in conversation log bool is_local = args.has("is_local") && args["is_local"].asBoolean(); bool from_me = chat.mFromID == gAgent.getID(); setPlainText(use_plain_text_chat_history); if (!scrolledToEnd() && !from_me && !chat.mFromName.empty()) { mUnreadChatSources++; if (!mUnreadMessagesUpdateSignal.empty()) { mUnreadMessagesUpdateSignal(mUnreadChatSources); } } F32 alpha = 1.f; LLUIColor txt_color = LLUIColorTable::instance().getColor("White"); LLUIColor name_color = LLUIColorTable::instance().getColor("ChatNameColor"); LLViewerChat::getChatColor(chat, txt_color, alpha, LLSD().with("is_local", is_local)); LLFontGL* fontp = LLViewerChat::getChatFont(); std::string font_name = LLFontGL::nameFromFont(fontp); std::string font_size = LLFontGL::sizeFromFont(fontp); LLStyle::Params body_message_params; body_message_params.color(txt_color); body_message_params.readonly_color(txt_color); body_message_params.alpha(alpha); body_message_params.font.name(font_name); body_message_params.font.size(font_size); body_message_params.font.style(input_append_params.font.style); LLStyle::Params name_params(body_message_params); name_params.color(name_color); name_params.readonly_color(name_color); std::string name_font_style_postfix = use_plain_text_chat_history ? "UNDERLINE" : ""; name_params.font.style = name_font_style_postfix; // This will be used when hovering the name (LLTextBase::appendAndHighlightTextImpl() will filter it out) std::string delimiter_style = "NORMAL"; // FS:LO FIRE-2899 - Faded text for IMs in nearby chat F32 FSIMChatHistoryFade = gSavedSettings.getF32("FSIMChatHistoryFade"); if (FSIMChatHistoryFade > 1.0f) { FSIMChatHistoryFade = 1.0f; gSavedSettings.setF32("FSIMChatHistoryFade",FSIMChatHistoryFade); } // FS:LO FIRE-2899 - Faded text for IMs in nearby chat //IRC styled /me messages. bool irc_me = FSCommon::is_irc_me_prefix(chat.mText); // Delimiter after a name in header copy/past and in plain text mode std::string delimiter = ": "; std::string shout = LLTrans::getString("shout"); std::string whisper = LLTrans::getString("whisper"); if (chat.mChatType == CHAT_TYPE_SHOUT || chat.mChatType == CHAT_TYPE_WHISPER || // FS:TS FIRE-6049: No : in radar chat header chat.mChatType == CHAT_TYPE_RADAR || // FS:TS FIRE-6049: No : in radar chat header chat.mText.compare(0, shout.length(), shout) == 0 || chat.mText.compare(0, whisper.length(), whisper) == 0) { delimiter = " "; } // Don't add any delimiter after name in irc styled messages if (irc_me || chat.mChatStyle == CHAT_STYLE_IRC) { delimiter = LLStringUtil::null; // italics for emotes -Zi if (gSavedSettings.getBOOL("EmotesUseItalic")) { body_message_params.font.style = "ITALIC"; name_params.font.style = "ITALIC" + name_font_style_postfix; } } if (chat.mChatType == CHAT_TYPE_WHISPER && gSavedSettings.getBOOL("FSEmphasizeShoutWhisper")) { body_message_params.font.style = "ITALIC"; name_params.font.style = "ITALIC" + name_font_style_postfix; delimiter_style = "ITALIC"; } else if(chat.mChatType == CHAT_TYPE_SHOUT && gSavedSettings.getBOOL("FSEmphasizeShoutWhisper")) { body_message_params.font.style = "BOLD"; name_params.font.style = "BOLD" + name_font_style_postfix; delimiter_style = "BOLD"; } bool message_from_log = (chat.mChatStyle == CHAT_STYLE_HISTORY || chat.mChatStyle == CHAT_STYLE_SERVER_HISTORY); bool teleport_separator = chat.mSourceType == CHAT_SOURCE_TELEPORT; // We graying out chat history by graying out messages that contains full date in a time string if (message_from_log && !is_conversation_log) { if (chat.mChatStyle == CHAT_STYLE_HISTORY) { txt_color = LLUIColorTable::instance().getColor("ChatHistoryMessageFromLog"); } else if (chat.mChatStyle == CHAT_STYLE_SERVER_HISTORY) { txt_color = LLUIColorTable::instance().getColor("ChatHistoryMessageFromServerLog"); } body_message_params.color(txt_color); body_message_params.readonly_color(txt_color); name_params.color(txt_color); name_params.readonly_color(txt_color); } // FS-1734 seperate name and text styles for moderator bool moderator_style_active = false; U32 moderator_name_style_value = gSavedSettings.getU32("FSModNameStyle"); U32 moderator_body_style_value = gSavedSettings.getU32("FSModTextStyle"); std::string moderator_name_style = applyModeratorStyle(moderator_name_style_value); std::string moderator_body_style = applyModeratorStyle(moderator_body_style_value); std::string moderator_timestamp_style = moderator_name_style; if (chat.mChatStyle == CHAT_STYLE_MODERATOR) { moderator_style_active = true; delimiter_style = moderator_name_style; if (moderator_name_style_value < UNDERLINE) { // Need to add "UNDERLINE" here because we want to underline the name on hover moderator_name_style += name_font_style_postfix; } else { // Always contains underline - don't show it on hover only name_params.can_underline_on_hover = false; } name_params.font.style(moderator_name_style); body_message_params.font.style(moderator_body_style); if ( irc_me && gSavedSettings.getBOOL("EmotesUseItalic") ) { if ( (ITALIC & moderator_name_style_value) != ITALIC )//HG: if ITALIC isn't one of the styles... add it { moderator_name_style += "ITALIC"; moderator_timestamp_style += "ITALIC"; name_params.font.style(moderator_name_style); } if ( (ITALIC & moderator_body_style_value) != ITALIC ) { moderator_body_style += "ITALIC"; body_message_params.font.style(moderator_body_style); } body_message_params.font.style(moderator_body_style); } } // FS-1734 seperate name and text styles for moderator bool prependNewLineState = getText().size() != 0; // compact mode: show a timestamp and name if (use_plain_text_chat_history) { square_brackets = (chat.mSourceType == CHAT_SOURCE_SYSTEM && chat.mChatType != CHAT_TYPE_RADAR && gSavedSettings.getBOOL("FSIMSystemMessageBrackets")); LLStyle::Params timestamp_style(body_message_params); // out of the timestamp if (args["show_time"].asBoolean() && !teleport_separator) { LLUIColor timestamp_color = LLUIColorTable::instance().getColor("ChatTimestampColor"); timestamp_style.color(timestamp_color); timestamp_style.readonly_color(timestamp_color); if (message_from_log && !is_conversation_log) { timestamp_style.color.alpha = FSIMChatHistoryFade; timestamp_style.readonly_color.alpha = FSIMChatHistoryFade; } // FS-1734 seperate name and text styles for moderator if ( moderator_style_active ) { timestamp_style.font.style(moderator_timestamp_style); } // FS-1734 seperate name and text styles for moderator appendText("[" + chat.mTimeStr + "] ", prependNewLineState, timestamp_style); prependNewLineState = false; } // out the opening square bracket (if need) if (square_brackets) { appendText("[", prependNewLineState, body_message_params); prependNewLineState = false; } // names showing if ( (!is_p2p || args["show_names_for_p2p_conv"].asBoolean()) && utf8str_trim(chat.mFromName).size() != 0) { // Don't hotlink any messages from the system (e.g. "Second Life:"), so just add those in plain text. if ( chat.mSourceType == CHAT_SOURCE_OBJECT && chat.mFromID.notNull()) { // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.0f) | Added: RLVa-1.2.0f // NOTE-RLVa: we don't need to do any @shownames or @showloc filtering here because we'll already have an existing URL std::string url = chat.mURL; RLV_ASSERT( (url.empty()) || (std::string::npos != url.find("objectim")) ); if ( (url.empty()) || (std::string::npos == url.find("objectim")) ) { // [/RLVa:KB] // for object IMs, create a secondlife:///app/objectim SLapp /*std::string*/ url = LLViewerChat::getSenderSLURL(chat, args); // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.0f) | Added: RLVa-1.2.0f } // [/RLVa:KB] // set the link for the object name to be the objectim SLapp // (don't let object names with hyperlinks override our objectim Url) LLStyle::Params link_params(body_message_params); link_params.color.control = "ChatNameObjectColor"; LLUIColor link_color = LLUIColorTable::instance().getColor("ChatNameObjectColor"); link_params.color = link_color; link_params.readonly_color = link_color; if (message_from_log && !is_conversation_log) { link_params.color.alpha = FSIMChatHistoryFade; link_params.readonly_color.alpha = FSIMChatHistoryFade; } link_params.is_link = true; link_params.link_href = url; appendText(chat.mFromName + delimiter, prependNewLineState, link_params); // FIRE-8600: TAB out of chat history prependNewLineState = false; } // else if (chat.mFromName != SYSTEM_FROM && chat.mFromID.notNull() && !message_from_log && chat.mSourceType != CHAT_SOURCE_REGION) // [RLVa:KB] - Checked: 2010-04-22 (RLVa-1.2.0f) | Added: RLVa-1.2.0f else if (chat.mFromName != SYSTEM_FROM && chat.mFromID.notNull() && !message_from_log && chat.mSourceType != CHAT_SOURCE_REGION && !chat.mRlvNamesFiltered) // [/RLVa:KB] { name_params.color = name_color; name_params.readonly_color = name_color; if (chat.mChatType == CHAT_TYPE_IM || chat.mChatType == CHAT_TYPE_IM_GROUP) { name_params.color.alpha = FSIMChatHistoryFade; name_params.readonly_color.alpha = FSIMChatHistoryFade; appendText(LLTrans::getString("IMPrefix") + " ", prependNewLineState, name_params); prependNewLineState = false; if (!chat.mFromNameGroup.empty()) { appendText(chat.mFromNameGroup, prependNewLineState, name_params); prependNewLineState = false; } } name_params.use_default_link_style = false; name_params.link_href = LLSLURL(from_me ? "agentself" : "agent", chat.mFromID, "inspect").getSLURLString(); // Add link to avatar's inspector and delimiter to message. appendText(std::string(name_params.link_href), prependNewLineState, name_params); prependNewLineState = false; if (delimiter.length() > 0 && delimiter[0] == ':') { LLStyle::Params delimiter_params(body_message_params); delimiter_params.font.style = delimiter_style; appendText(":", prependNewLineState, delimiter_params); prependNewLineState = false; appendText(delimiter.substr(1, std::string::npos), prependNewLineState, body_message_params); } else { appendText(delimiter, prependNewLineState, body_message_params); } prependNewLineState = false; } else if (teleport_separator) { std::string tp_text = LLTrans::getString("teleport_preamble_compact_chat"); appendText(tp_text + " " + chat.mFromName + "", prependNewLineState, body_message_params); prependNewLineState = false; } else { appendText("" + chat.mFromName + "" + delimiter, prependNewLineState, body_message_params); prependNewLineState = false; } } } else // showing timestamp and name in the expanded mode { prependNewLineState = false; LLView* view = NULL; LLInlineViewSegment::Params p; p.force_newline = true; p.left_pad = mLeftWidgetPad; p.right_pad = mRightWidgetPad; LLDate new_message_time = LLDate::now(); bool needs_header_text = false; if (!teleport_separator && mLastFromName == chat.mFromName && mLastFromID == chat.mFromID && mLastMessageTime.notNull() && (new_message_time.secondsSinceEpoch() - mLastMessageTime.secondsSinceEpoch()) < 60.0 && mIsLastMessageFromLog == message_from_log) //distinguish between current and previous chat session's histories { view = getSeparator(); p.top_pad = mTopSeparatorPad; p.bottom_pad = mBottomSeparatorPad; if (!view) { // Might be wiser to make this LL_ERRS, getSeparator() should work in case of correct instalation. LL_WARNS() << "Failed to create separator from " << mMessageSeparatorFilename << ": can't append to history" << LL_ENDL; return; } } else { needs_header_text = true; view = getHeader(chat, name_params, args); if (getLength() == 0) p.top_pad = 0; else p.top_pad = mTopHeaderPad; if (teleport_separator) { p.bottom_pad = mBottomSeparatorPad; } else { p.bottom_pad = mBottomHeaderPad; } if (!view) { LL_WARNS() << "Failed to create header from " << mMessageHeaderFilename << ": can't append to history" << LL_ENDL; return; } } p.view = view; //Prepare the rect for the view LLRect target_rect = getDocumentView()->getRect(); // FIRE-8600: TAB out of chat history // squeeze down the widget by subtracting padding off left and right target_rect.mLeft += mLeftWidgetPad + getHPad(); // FIRE-8600: TAB out of chat history target_rect.mRight -= mRightWidgetPad; view->reshape(target_rect.getWidth(), view->getRect().getHeight()); view->setOrigin(target_rect.mLeft, view->getRect().mBottom); std::string widget_associated_text = "\n"; if (needs_header_text) { widget_associated_text += "[" + chat.mTimeStr + "] "; if (utf8str_trim(chat.mFromName).size() != 0 && chat.mFromName != SYSTEM_FROM) { widget_associated_text += chat.mFromName + delimiter; } } appendWidget(p, widget_associated_text, false); // FIRE-8600: TAB out of chat history mLastFromName = chat.mFromName; mLastFromID = chat.mFromID; mLastMessageTime = new_message_time; mIsLastMessageFromLog = message_from_log; } // body of the message processing // notify processing if (chat.mNotifId.notNull()) { LLNotificationPtr notification = LLNotificationsUtil::find(chat.mNotifId); if (notification != NULL) { bool create_toast = true; if (notification->getName() == "OfferFriendship") { // We don't want multiple friendship offers to appear, this code checks if there are previous offers // by iterating though all panels. // Note: it might be better to simply add a "pending offer" flag somewhere for (auto& ti : LLToastNotifyPanel::instance_snapshot()) { LLIMToastNotifyPanel * imtoastp = dynamic_cast(&ti); const std::string& notification_name = ti.getNotificationName(); if (notification_name == "OfferFriendship" && ti.isControlPanelEnabled() && imtoastp) { create_toast = false; break; } } } if (create_toast) { LLIMToastNotifyPanel* notify_box = new LLIMToastNotifyPanel( notification, chat.mSessionID, LLRect::null, true, this); //Prepare the rect for the view LLRect target_rect = getDocumentView()->getRect(); // squeeze down the widget by subtracting padding off left and right target_rect.mLeft += mLeftWidgetPad + getHPad(); target_rect.mRight -= mRightWidgetPad; notify_box->reshape(target_rect.getWidth(), notify_box->getRect().getHeight()); notify_box->setOrigin(target_rect.mLeft, notify_box->getRect().mBottom); LLInlineViewSegment::Params params; params.view = notify_box; params.left_pad = mLeftWidgetPad; params.right_pad = mRightWidgetPad; appendWidget(params, "\n", false); } } } // usual messages showing else if (!teleport_separator) { std::string message = irc_me ? chat.mText.substr(3) : chat.mText; //MESSAGE TEXT PROCESSING //*HACK getting rid of redundant sender names in system notifications sent using sender name (see EXT-5010) if (use_plain_text_chat_history && !from_me && chat.mFromID.notNull()) { std::string slurl_about = SLURL_APP_AGENT + chat.mFromID.asString() + SLURL_ABOUT; if (message.length() > slurl_about.length() && message.compare(0, slurl_about.length(), slurl_about) == 0) { message = message.substr(slurl_about.length(), message.length()-1); } } if (irc_me && !use_plain_text_chat_history) { if (chat.mSourceType == CHAT_SOURCE_AGENT) { static LLCachedControl useDisplayNames(gSavedSettings, "UseDisplayNames"); static LLCachedControl nameTagShowUsernames(gSavedSettings, "NameTagShowUsernames"); std::string name_format = (useDisplayNames && nameTagShowUsernames) ? "completename" : "displayname"; if (is_local && gRlvHandler.hasBehaviour(RLV_BHVR_SHOWNAMES)) { name_format = "rlvanonym"; } message = LLSLURL("agent", chat.mFromID, name_format).getSLURLString() + message; } else { message = chat.mFromName + message; } } if(chat.mSourceType != CHAT_SOURCE_OBJECT && (chat.mChatType == CHAT_TYPE_IM || chat.mChatType == CHAT_TYPE_IM_GROUP)) // FS::LO Fix for FIRE-6334; Fade IM Text into background of chat history default setting should not be 0.5; made object IM text not fade into the background as per phoenix behavior. { body_message_params.color.alpha = FSIMChatHistoryFade; body_message_params.readonly_color.alpha = FSIMChatHistoryFade; body_message_params.selected_color.alpha = FSIMChatHistoryFade; } // FS:LO FIRE-2899 - Faded text for IMs in nearby chat if (square_brackets) { message += "]"; } bool is_trusted = isContentTrusted(); setContentTrusted(chat.mFromID.isNull() && is_p2p); // Set trusted content temporarily for system messages setPlainText((use_plain_text_chat_history && is_p2p) ? chat.mFromID.notNull() : use_plain_text_chat_history); appendText(message, prependNewLineState, body_message_params); // FIRE-8600: TAB out of chat history setContentTrusted(is_trusted); setPlainText(use_plain_text_chat_history); // Uncomment this if we never need to append to the end of a message. [FS:CR] //prependNewLineState = false; } blockUndo(); // FIRE-8600: TAB out of chat history // automatically scroll to end when receiving chat from myself if (from_me) { setCursorAndScrollToEnd(); // FIRE-8600: TAB out of chat history } } // FIRE-8602: Typing in chat history focuses chat input line bool FSChatHistory::handleUnicodeCharHere(llwchar uni_char) { // do not change focus when the CTRL key is used to make copy/select all etc. possible if(gKeyboard->currentMask(false) & MASK_CONTROL) { // instead, let the base class handle things return LLTextEditor::handleUnicodeCharHere(uni_char); } // we might not know which is our chat input line yet updateChatInputLine(); // do we know our chat input line now? if(mChatInputLine) { // we do, so focus on it mChatInputLine->setFocus(true); // and let it handle the keystroke return mChatInputLine->handleUnicodeCharHere(uni_char); } // we don't know what to do, so let our base class handle the keystroke return LLTextEditor::handleUnicodeCharHere(uni_char); } void FSChatHistory::draw() { LLTextEditor::draw(); // Ansa: FIRE-12754: Hack around a weird issue where the doc size magically increases by 1px // during draw if the doc exceeds the visible space and the scrollbar is getting visible. if (mScrollToBottom) { mScroller->goToBottom(); mScrollToBottom = false; } if (scrolledToEnd() && mUnreadChatSources != 0) { mUnreadChatSources = 0; if (!mUnreadMessagesUpdateSignal.empty()) { mUnreadMessagesUpdateSignal(mUnreadChatSources); } } } //