From f257ee7d3c7464b8aee3bf8c5f5c84cd278836cc Mon Sep 17 00:00:00 2001
From: simon <simon@lindenlab.com>
Date: Mon, 13 Feb 2023 15:19:27 -0800
Subject: [PATCH] SL-18268 - Viewer update to read group chat history. Feature
 described at
 https://community.secondlife.com/blogs/entry/12652-coming-soon-to-a-viewer-near-you-group-chat-history/

---
 indra/newview/llimprocessing.cpp            |  19 +-
 indra/newview/llimview.cpp                  | 712 +++++++++++++++-----
 indra/newview/llimview.h                    |  66 +-
 indra/newview/lllogchat.cpp                 | 121 ++--
 indra/newview/lllogchat.h                   |   3 +-
 indra/newview/llnotificationhandlerutil.cpp |   2 +-
 6 files changed, 690 insertions(+), 233 deletions(-)

diff --git a/indra/newview/llimprocessing.cpp b/indra/newview/llimprocessing.cpp
index af2d168aefe..81f51ca0ea8 100644
--- a/indra/newview/llimprocessing.cpp
+++ b/indra/newview/llimprocessing.cpp
@@ -521,7 +521,9 @@ void LLIMProcessing::processNewMessage(LLUUID from_id,
                     dialog,
                     parent_estate_id,
                     region_id,
-                    position);
+                    position,
+                    false,      // is_region_msg
+                    timestamp);
 
                 if (!gIMMgr->isDNDMessageSend(session_id))
                 {
@@ -592,7 +594,8 @@ void LLIMProcessing::processNewMessage(LLUUID from_id,
                         parent_estate_id,
                         region_id,
                         position,
-                        region_message);
+                        region_message,
+                        timestamp);
                 }
                 else
                 {
@@ -1111,7 +1114,9 @@ void LLIMProcessing::processNewMessage(LLUUID from_id,
                     IM_SESSION_INVITE,
                     parent_estate_id,
                     region_id,
-                    position);
+                    position,
+                    false,      // is_region_msg
+                    timestamp);
             }
             else
             {
@@ -1131,12 +1136,14 @@ void LLIMProcessing::processNewMessage(LLUUID from_id,
                     from_id,
                     name,
                     buffer,
-                    IM_OFFLINE == offline,
-                    ll_safe_string((char*)binary_bucket),
+                    (IM_OFFLINE == offline),
+                    ll_safe_string((char*)binary_bucket),   // session name
                     IM_SESSION_INVITE,
                     parent_estate_id,
                     region_id,
-                    position);
+                    position,
+                    false,      // is_region_msg
+                    timestamp);
             }
             break;
 
diff --git a/indra/newview/llimview.cpp b/indra/newview/llimview.cpp
index 5039e15047e..3125e80296e 100644
--- a/indra/newview/llimview.cpp
+++ b/indra/newview/llimview.cpp
@@ -1,25 +1,25 @@
-/** 
+/**
  * @file LLIMMgr.cpp
  * @brief Container for Instant Messaging
  *
  * $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 Public
  * License along with this library; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- * 
+ *
  * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
  * $/LicenseInfo$
  */
@@ -77,11 +77,17 @@ const static std::string ADHOC_NAME_SUFFIX(" Conference");
 const static std::string NEARBY_P2P_BY_OTHER("nearby_P2P_by_other");
 const static std::string NEARBY_P2P_BY_AGENT("nearby_P2P_by_agent");
 
+// Markers inserted around translated part of chat text
+const static std::string XL8_START_TAG(" (");
+const static std::string XL8_END_TAG(")");
+const S32 XL8_PADDING = 3;  // XL8_START_TAG.size() + XL8_END_TAG.size()
+
 /** Timeout of outgoing session initialization (in seconds) */
 const static U32 SESSION_INITIALIZATION_TIMEOUT = 30;
 
 void startConfrenceCoro(std::string url, LLUUID tempSessionId, LLUUID creatorId, LLUUID otherParticipantId, LLSD agents);
 void chatterBoxInvitationCoro(std::string url, LLUUID sessionId, LLIMMgr::EInvitationType invitationType);
+void chatterBoxHistoryCoro(std::string url, LLUUID sessionId, std::string from, std::string message, U32 timestamp);
 void start_deprecated_conference_chat(const LLUUID& temp_session_id, const LLUUID& creator_id, const LLUUID& other_participant_id, const LLSD& agents_to_invite);
 
 const LLUUID LLOutgoingCallDialog::OCD_KEY = LLUUID("7CF78E11-0CFE-498D-ADB9-1417BF03DDB4");
@@ -112,7 +118,7 @@ void process_dnd_im(const LLSD& notification)
     LLUUID sessionID = data["SESSION_ID"].asUUID();
     LLUUID fromID = data["FROM_ID"].asUUID();
 
-    //re-create the IM session if needed 
+    //re-create the IM session if needed
     //(when coming out of DND mode upon app restart)
     if(!gIMMgr->hasSession(sessionID))
     {
@@ -123,13 +129,13 @@ void process_dnd_im(const LLSD& notification)
         {
             name = av_name.getDisplayName();
         }
-		
-        
-        LLIMModel::getInstance()->newSession(sessionID, 
-            name, 
-            IM_NOTHING_SPECIAL, 
-            fromID, 
-            false, 
+
+
+        LLIMModel::getInstance()->newSession(sessionID,
+            name,
+            IM_NOTHING_SPECIAL,
+            fromID,
+            false,
             false); //will need slight refactor to retrieve whether offline message or not (assume online for now)
     }
 
@@ -312,8 +318,8 @@ void notify_of_message(const LLSD& msg, bool is_dnd_msg)
 			}
 			else
 			{
-				if (is_dnd_msg && (ON_TOP == conversations_floater_status || 
-									NOT_ON_TOP == conversations_floater_status || 
+				if (is_dnd_msg && (ON_TOP == conversations_floater_status ||
+									NOT_ON_TOP == conversations_floater_status ||
 									CLOSED == conversations_floater_status))
 				{
 					im_box->highlightConversationItemWidget(session_id, true);
@@ -372,7 +378,7 @@ void notify_of_message(const LLSD& msg, bool is_dnd_msg)
 	}
 	if (store_dnd_message)
 	{
-		// If in DND mode, allow notification to be stored so upon DND exit 
+		// If in DND mode, allow notification to be stored so upon DND exit
 		// the user will be notified with some limitations (see 'is_dnd_msg' flag checks)
 		if(session_id.notNull()
 			&& participant_id.notNull()
@@ -393,7 +399,7 @@ void startConfrenceCoro(std::string url,
 {
     LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
     LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
-        httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("TwitterConnect", httpPolicy));
+        httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("ConferenceChatStart", httpPolicy));
     LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest);
 
     LLSD postData;
@@ -433,7 +439,7 @@ void chatterBoxInvitationCoro(std::string url, LLUUID sessionId, LLIMMgr::EInvit
 {
     LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
     LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
-        httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("TwitterConnect", httpPolicy));
+        httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("ConferenceInviteStart", httpPolicy));
     LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest);
 
     LLSD postData;
@@ -511,33 +517,124 @@ void chatterBoxInvitationCoro(std::string url, LLUUID sessionId, LLIMMgr::EInvit
 
 }
 
-void translateSuccess(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text, 
-                    bool log2file, std::string originalMsg, std::string expectLang, std::string translation, const std::string detected_language)
+void translateSuccess(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text,
+                        U64 time_n_flags, std::string originalMsg, std::string expectLang, std::string translation, const std::string detected_language)
 {
     std::string message_txt(utf8_text);
-    // filter out non-interesting responses  
+    // filter out non-interesting responses
     if (!translation.empty()
         && ((detected_language.empty()) || (expectLang != detected_language))
         && (LLStringUtil::compareInsensitive(translation, originalMsg) != 0))
-    {
-        message_txt += " (" + LLTranslate::removeNoTranslateTags(translation) + ")";
+    {   // Note - if this format changes, also fix code in addMessagesFromServerHistory()
+        message_txt += XL8_START_TAG + LLTranslate::removeNoTranslateTags(translation) + XL8_END_TAG;
     }
 
-    LLIMModel::getInstance()->processAddingMessage(session_id, from, from_id, message_txt, log2file);
+    // Extract info packed in time_n_flags
+    bool log2file =      (bool)(time_n_flags & (1LL << 32));
+    bool is_region_msg = (bool)(time_n_flags & (1LL << 33));
+    U32 time_stamp = (U32)(time_n_flags & 0x00000000ffffffff);
+
+    LLIMModel::getInstance()->processAddingMessage(session_id, from, from_id, message_txt, log2file, is_region_msg, time_stamp);
 }
 
 void translateFailure(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text,
-                    bool log2file, int status, const std::string err_msg)
+                        U64 time_n_flags, int status, const std::string err_msg)
 {
     std::string message_txt(utf8_text);
     std::string msg = LLTrans::getString("TranslationFailed", LLSD().with("[REASON]", err_msg));
     LLStringUtil::replaceString(msg, "\n", " "); // we want one-line error messages
-    message_txt += " (" + msg + ")";
+    message_txt += XL8_START_TAG + msg + XL8_END_TAG;
+
+    // Extract info packed in time_n_flags
+    bool log2file = (bool)(time_n_flags & (1LL << 32));
+    bool is_region_msg = (bool)(time_n_flags & (1LL << 33));
+    U32 time_stamp = (U32)(time_n_flags & 0x00000000ffffffff);
+
+    LLIMModel::getInstance()->processAddingMessage(session_id, from, from_id, message_txt, log2file, is_region_msg, time_stamp);
+}
+
+void chatterBoxHistoryCoro(std::string url, LLUUID sessionId, std::string from, std::string message, U32 timestamp)
+{   // if parameters from, message and timestamp have values, they are a message that opened chat
+    LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
+    LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
+        httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("ChatHistory", httpPolicy));
+    LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest);
+
+    LLSD postData;
+    postData["method"] = "fetch history";
+    postData["session-id"] = sessionId;
+
+    LL_DEBUGS("ChatHistory") << sessionId << ": Chat history posting " << postData << " to " << url
+        << ", from " << from << ", message " << message << ", timestamp " << (S32)timestamp << LL_ENDL;
+
+    LLSD result = httpAdapter->postAndSuspend(httpRequest, url, postData);
+
+    LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+    LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+
+    if (!status)
+    {
+        LL_WARNS("ChatHistory") << sessionId << ": Bad HTTP response in chatterBoxHistoryCoro"
+            << ", results: " << httpResults << LL_ENDL;
+        return;
+    }
+
+    // Add history to IM session
+    LLSD history = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_CONTENT];
+
+    LL_DEBUGS("ChatHistory") << sessionId << ": Chat server history fetch returned " << history << LL_ENDL;
 
-    LLIMModel::getInstance()->processAddingMessage(session_id, from, from_id, message_txt, log2file);
+    try
+    {
+        LLIMModel::LLIMSession* session = LLIMModel::getInstance()->findIMSession(sessionId);
+        if (session && history.isArray())
+        {   // Result array is sorted oldest to newest
+            if (history.size() > 0)
+            {   // History from the chat server has an integer 'time' value timestamp.   Create 'datetime' string which will match
+                // what we have from the local history cache
+                for (LLSD::array_iterator cur_server_hist = history.beginArray(), endLists = history.endArray();
+                    cur_server_hist != endLists;
+                    cur_server_hist++)
+                {
+                    if ((*cur_server_hist).isMap())
+                    {   // Take the 'time' value from the server and make the date-time string that will be in local cache log files
+                        //   {'from_id':u7aa8c222-8a81-450e-b3d1-9c28491ef717,'message':'Can you hear me now?','from':'Chat Tester','num':i86,'time':r1.66501e+09}
+                        U32 timestamp = (U32)((*cur_server_hist)[LL_IM_TIME].asInteger());
+                        (*cur_server_hist)[LL_IM_DATE_TIME] = LLLogChat::timestamp2LogString(timestamp, true);
+                    }
+                }
+
+                session->addMessagesFromServerHistory(history, from, message, timestamp);
+
+                // Display the newly added messages
+                LLFloaterIMSession* floater = LLFloaterReg::findTypedInstance<LLFloaterIMSession>("impanel", sessionId);
+                if (floater && floater->isInVisibleChain())
+                {
+                    floater->updateMessages();
+                }
+            }
+            else
+            {
+                LL_DEBUGS("ChatHistory") << sessionId << ": Empty history from chat server, nothing to add" << LL_ENDL;
+            }
+        }
+        else if (session && !history.isArray())
+        {
+            LL_WARNS("ChatHistory") << sessionId << ": Bad array data fetching chat history" << LL_ENDL;
+        }
+        else
+        {
+            LL_WARNS("ChatHistory") << sessionId << ": Unable to find session fetching chat history" << LL_ENDL;
+        }
+    }
+    catch (...)
+    {
+        LOG_UNHANDLED_EXCEPTION("chatterBoxHistoryCoro");
+        LL_WARNS("ChatHistory") << "chatterBoxHistoryCoro unhandled exception while processing data for session " << sessionId << LL_ENDL;
+    }
 }
 
-LLIMModel::LLIMModel() 
+LLIMModel::LLIMModel()
 {
 	addNewMsgCallback(boost::bind(&LLFloaterIMSession::newIMCallback, _1));
 	addNewMsgCallback(boost::bind(&on_new_message, _1));
@@ -582,25 +679,25 @@ LLIMModel::LLIMSession::LLIMSession(const LLUUID& session_id, const std::string&
 		else
 		{
 			mSessionType = ADHOC_SESSION;
-		} 
+		}
 	}
 
 	if(mVoiceChannel)
 	{
 		mVoiceChannelStateChangeConnection = mVoiceChannel->setStateChangedCallback(boost::bind(&LLIMSession::onVoiceChannelStateChanged, this, _1, _2, _3));
 	}
-		
+
 	mSpeakers = new LLIMSpeakerMgr(mVoiceChannel);
 
 	// All participants will be added to the list of people we've recently interacted with.
 
-	// we need to add only _active_ speakers...so comment this. 
+	// we need to add only _active_ speakers...so comment this.
 	// may delete this later on cleanup
 	//mSpeakers->addListener(&LLRecentPeople::instance(), "add");
 
 	//we need to wait for session initialization for outgoing ad-hoc and group chat session
 	//correct session id for initiated ad-hoc chat will be received from the server
-	if (!LLIMModel::getInstance()->sendStartSession(mSessionID, mOtherParticipantID, 
+	if (!LLIMModel::getInstance()->sendStartSession(mSessionID, mOtherParticipantID,
 		mInitialTargetIDs, mType))
 	{
 		//we don't need to wait for any responses
@@ -682,7 +779,7 @@ void LLIMModel::LLIMSession::onVoiceChannelStateChanged(const LLVoiceChannel::ES
 					LLStringUtil::format_map_t string_args;
 					string_args["[NAME]"] = other_avatar_name;
 					message = LLTrans::getString("name_started_call", string_args);
-					LLIMModel::getInstance()->addMessage(mSessionID, SYSTEM_FROM, LLUUID::null, message);				
+					LLIMModel::getInstance()->addMessage(mSessionID, SYSTEM_FROM, LLUUID::null, message);
 					break;
 				}
 			case LLVoiceChannel::STATE_CONNECTED :
@@ -786,14 +883,21 @@ void LLIMModel::LLIMSession::sessionInitReplyReceived(const LLUUID& new_session_
 	}
 }
 
-void LLIMModel::LLIMSession::addMessage(const std::string& from, const LLUUID& from_id, const std::string& utf8_text, const std::string& time, const bool is_history, bool is_region_msg)
+void LLIMModel::LLIMSession::addMessage(const std::string& from,
+                                        const LLUUID& from_id,
+                                        const std::string& utf8_text,
+                                        const std::string& time,
+                                        const bool is_history,  // comes from a history file or chat server
+                                        const bool is_region_msg,
+                                        const U32 timestamp)   // may be zero
 {
 	LLSD message;
 	message["from"] = from;
 	message["from_id"] = from_id;
 	message["message"] = utf8_text;
-	message["time"] = time; 
-	message["index"] = (LLSD::Integer)mMsgs.size(); 
+	message["time"] = time;         // string used in display, may be full data YYYY/MM/DD HH:MM or just HH:MM
+    message["timestamp"] = (S32)timestamp;          // use string? LLLogChat::timestamp2LogString(timestamp, true);
+	message["index"] = (LLSD::Integer)mMsgs.size();
 	message["is_history"] = is_history;
 	message["is_region_msg"] = is_region_msg;
 
@@ -814,7 +918,7 @@ void LLIMModel::LLIMSession::addMessage(const std::string& from, const LLUUID& f
 		}
 	}
 
-	mMsgs.push_front(message); 
+	mMsgs.push_front(message);          // Add most recent messages to the front of mMsgs
 
 	if (mSpeakers && from_id.notNull())
 	{
@@ -823,35 +927,281 @@ void LLIMModel::LLIMSession::addMessage(const std::string& from, const LLUUID& f
 	}
 }
 
-void LLIMModel::LLIMSession::addMessagesFromHistory(const std::list<LLSD>& history)
+void LLIMModel::LLIMSession::addMessagesFromHistoryCache(const chat_message_list_t& history)
 {
-	std::list<LLSD>::const_iterator it = history.begin();
-	while (it != history.end())
-	{
-		const LLSD& msg = *it;
+    // Add the messages from the local cached chat history to the session window
+    for (const auto& msg : history)
+    {
+        std::string from = msg[LL_IM_FROM];
+        LLUUID from_id;
+        if (msg[LL_IM_FROM_ID].isDefined())
+        {
+            from_id = msg[LL_IM_FROM_ID].asUUID();
+        }
+        else
+        {   // convert it to a legacy name if we have a complete name
+            std::string legacy_name = gCacheName->buildLegacyName(from);
+            from_id = LLAvatarNameCache::getInstance()->findIdByName(legacy_name);
+        }
 
-		std::string from = msg[LL_IM_FROM];
-		LLUUID from_id;
-		if (msg[LL_IM_FROM_ID].isDefined())
-		{
-			from_id = msg[LL_IM_FROM_ID].asUUID();
-		}
-		else
-		{
-			// convert it to a legacy name if we have a complete name
-			std::string legacy_name = gCacheName->buildLegacyName(from);
-			from_id = LLAvatarNameCache::getInstance()->findIdByName(legacy_name);
-		}
+        // Save the last minute of messages so we can merge with the chat server history.
+        // Really would be nice to have a numeric timestamp in the local cached chat file
+        const std::string & msg_time_str = msg[LL_IM_DATE_TIME].asString();
+        if (mLastHistoryCacheDateTime != msg_time_str)
+        {
+            mLastHistoryCacheDateTime = msg_time_str;   // Reset to the new time
+            mLastHistoryCacheMsgs.clear();
+        }
+        mLastHistoryCacheMsgs.push_front(msg);
+        LL_DEBUGS("ChatHistory") << mSessionID << ": Adding history cache message: " << msg << LL_ENDL;
 
-		std::string timestamp = msg[LL_IM_TIME];
-		std::string text = msg[LL_IM_TEXT];
+        // Add message from history cache to the display
+        addMessage(from, from_id, msg[LL_IM_TEXT], msg[LL_IM_TIME], true, false, 0);   // from history data, not region message, no timestamp
+    }
+}
 
-		addMessage(from, from_id, text, timestamp, true);
+void LLIMModel::LLIMSession::addMessagesFromServerHistory(const LLSD& history,             // Array of chat messages from chat server
+                                                        const std::string& target_from,    // Sender of message that opened chat
+                                                        const std::string& target_message, // Message text that opened chat
+                                                        U32 timestamp)                     // timestamp of message that opened chat
+{   // Add messages from history returned by the chat server.
+
+    // The session mMsgs may contain chat messages from the local history cache file, and possibly one or more newly
+    // arrived chat messages.   If the chat window was manually opened, these will be empty and history can
+    // more easily merged.    The history from the server, however, may overlap what is in the file and those must also be merged.
+
+    // At this point, the session mMsgs can have
+    //   no messages
+    //   nothing from history file cache, but one or more very recently arrived messages,
+    //   messages from history file cache, no recent chat
+    //   messages from history file cache, one or more very recent messages
+    //
+    // The chat history from server can possibly contain:
+    //   no messages
+    //   messages that start back before anything in the local file (obscure case, but possible)
+    //   messages that match messages from the history file cache
+    //   messages from the last hour, new to the viewer
+    //   one or more messages that match most recently received chat (the one that opened the window)
+    // In other words:
+    //   messages from chat server may or may not match what we already have in mMsgs
+    //   We can drop anything that is during the time span covered by the local cache file
+    //   To keep things simple, drop any chat data older than the local cache file
+
+    if (!history.isArray())
+    {
+        LL_WARNS("ChatHistory") << mSessionID << ": Unexpected history data not array, type " << (S32)history.type() << LL_ENDL;
+        return;
+    }
 
-		it++;
-	}
+    if (history.size() == 0)
+    {   // If history is empty
+        LL_DEBUGS("ChatHistory") << mSessionID << ": addMessagesFromServerHistory() has empty history, nothing to merge" << LL_ENDL;
+        return;
+    }
+
+    if (history.size() == 1 &&          // Server chat history has one entry,
+        target_from.length() > 0 &&     // and we have a chat message that just arrived
+        mMsgs.size() > 0)               // and we have some data in the window - assume the history message is there.
+    {   // This is the common case where a group chat is silent for a while, and then one message is sent.
+        LL_DEBUGS("ChatHistory") << mSessionID << ": addMessagesFromServerHistory() only has chat message just received." << LL_ENDL;
+        return;
+    }
+
+    LL_DEBUGS("ChatHistory") << mSessionID << ": addMessagesFromServerHistory() starting with mMsg.size() " << mMsgs.size()
+        << " adding history with " << history.size() << " messages"
+        << ", target_from: " << target_from
+        << ", target_message: " << target_message
+        << ", timestamp: " << (S32)timestamp << LL_ENDL;
+
+    // At start of merging, mMsgs is either empty, has some chat messages read from a local cache file, and may have
+    // one or more messages that just arrived from the server.
+    U32 match_timestamp = 0;
+    chat_message_list_t shift_msgs;
+    if (mMsgs.size() > 0 &&
+        target_from.length() > 0
+        && target_message.length() > 0)
+    {   // Find where to insert the history messages by popping off a few in the session.
+        // The most common case is one duplciate message, the one that opens a chat session
+        while (mMsgs.size() > 0)
+        {
+            // The "time" value from mMsgs is a string, either just time HH:MM or a full date and time
+            LLSD cur_msg = mMsgs.front();       // Get most recent message from the chat display (front of mMsgs list)
+
+            if (cur_msg.isMap())
+            {
+                LL_DEBUGS("ChatHistoryCompare") << mSessionID << ": Finding insertion point, looking at cur_msg: " << cur_msg << LL_ENDL;
+
+                match_timestamp = cur_msg["timestamp"].asInteger();  // get timestamp of message in the session, may be zero
+                if ((S32)timestamp > match_timestamp)
+                {
+                    LL_DEBUGS("ChatHistory") << mSessionID << ": found older chat message: " << cur_msg
+                        << ", timestamp " << (S32)timestamp
+                        << " vs. match_timestamp " << match_timestamp
+                        << ", shift_msgs size is " << shift_msgs.size() << LL_ENDL;
+                    break;
+                }
+                // Have the matching message or one more recent: these need to be at the end
+                shift_msgs.push_front(cur_msg);     // Move chat message to temp list.
+                mMsgs.pop_front();                  // Normally this is just one message
+                LL_DEBUGS("ChatHistory") << mSessionID << ": shifting chat message " << cur_msg
+                    << " to be inserted at end, shift_msgs size is " << shift_msgs.size()
+                    << ", match_timestamp " << match_timestamp
+                    << ", timestamp " << (S32)timestamp << LL_ENDL;
+            }
+            else
+            {
+                LL_DEBUGS("ChatHistory") << mSessionID << ": Unexpected non-map entry in session messages: " << cur_msg << LL_ENDL;
+                return;
+            }
+        }
+    }
+
+    // Now merge messages from server history data into the session display.   The history data
+    // from the local file may overlap with the chat messages from the server.
+    // Drop any messages from the chat server history that are before the latest one from the local history file.
+    // Unfortunately, messages from the local file don't have timestamps - just datetime strings
+    LLSD::array_const_iterator cur_history_iter = history.beginArray();
+    while (cur_history_iter != history.endArray())
+    {
+        const LLSD &cur_server_hist = *cur_history_iter;
+        cur_history_iter++;
+
+        if (cur_server_hist.isMap())
+        {   // Each server history entry looks like
+            //   { 'from':'Laggy Avatar', 'from_id' : u72345678 - 744f - 43b9 - 98af - b06f1c76ddda, 'index' : i24, 'is_history' : 1, 'message' : 'That was slow', 'time' : '02/13/2023 10:03', 'timestamp' : i1676311419 }
+
+            // If we reach the message that opened our window, stop adding messages
+            U32 history_msg_timestamp = (U32)cur_server_hist[LL_IM_TIME].asInteger();
+            if ((match_timestamp > 0 && match_timestamp <= history_msg_timestamp) ||
+                (timestamp > 0 && timestamp <= history_msg_timestamp))
+            {   // we found the message we matched, so stop inserting from chat server history
+                LL_DEBUGS("ChatHistoryCompare") << "Found end of chat history insertion with match_timestamp " << (S32)match_timestamp
+                    << " vs. history_msg_timestamp " << (S32)history_msg_timestamp
+                    << " vs. timestamp " << (S32)timestamp
+                    << LL_ENDL;
+                break;
+            }
+            LL_DEBUGS("ChatHistoryCompare") << "Compared match_timestamp " << (S32)match_timestamp
+                << " vs. history_msg_timestamp " << (S32)history_msg_timestamp << LL_ENDL;
+
+            bool add_chat_to_conversation = true;
+            if (!mLastHistoryCacheDateTime.empty())
+            {   // Skip past the any from server that are older than what we already read from the history file.
+                std::string history_datetime = cur_server_hist[LL_IM_DATE_TIME].asString();
+                if (history_datetime.empty())
+                {
+                    history_datetime = cur_server_hist[LL_IM_TIME].asString();
+                }
+
+                if (history_datetime < mLastHistoryCacheDateTime)
+                {
+                    LL_DEBUGS("ChatHistoryCompare") << "Skipping message from chat server history since it's older than messages the session already has."
+                        << history_datetime << " vs  " << mLastHistoryCacheDateTime << LL_ENDL;
+                    add_chat_to_conversation = false;
+                }
+                else if (history_datetime > mLastHistoryCacheDateTime)
+                {   // The message from the chat server is more recent than the last one from the local cache file.   Add it
+                    LL_DEBUGS("ChatHistoryCompare") << "Found message dated "
+                        << history_datetime << " vs " << mLastHistoryCacheDateTime
+                        << ", adding new message from chat server history " << cur_server_hist << LL_ENDL;
+                }
+                else   // (history_datetime == mLastHistoryCacheDateTime)
+                {      // Messages are in the same minute as the last from the cache log file.
+                    const std::string & history_msg_text = cur_server_hist[LL_IM_TEXT];
+
+                    // Look in the saved messages from the history file that have the same time
+                    for (const auto& scan_msg : mLastHistoryCacheMsgs)
+                    {
+                        LL_DEBUGS("ChatHistoryCompare") << "comparing messages " << scan_msg[LL_IM_TEXT]
+                            << " with " << cur_server_hist << LL_ENDL;
+                        if (scan_msg.size() > 0)
+                        {   // Extra work ... the history_msg_text value may have been translated, i.e. "I am confused (je suis confus)"
+                            //  while the server history will only have the first part "I am confused"
+                            std::string target_compare(scan_msg[LL_IM_TEXT]);
+                            if (target_compare.size() > history_msg_text.size() + XL8_PADDING &&
+                                target_compare.substr(history_msg_text.size(), XL8_START_TAG.size()) == XL8_START_TAG &&
+                                target_compare.substr(target_compare.size() - XL8_END_TAG.size()) == XL8_END_TAG)
+                            {   // This really looks like a "translated string (cadena traducida)" so just compare the source part
+                                LL_DEBUGS("ChatHistory") << mSessionID << ": Found translated chat " << target_compare
+                                    << " when comparing to history " << history_msg_text
+                                    << ", will truncate" << LL_ENDL;
+                                target_compare = target_compare.substr(0, history_msg_text.size());
+                            }
+                            if (history_msg_text == target_compare)
+                            {   // Found a match, so don't add a duplicate chat message to the window
+                                LL_DEBUGS("ChatHistory") << mSessionID << ": Found duplicate message text " << history_msg_text
+                                    << " : " << (S32)history_msg_timestamp << ", matching datetime " << history_datetime << LL_ENDL;
+                                add_chat_to_conversation = false;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            LLUUID sender_id = cur_server_hist[LL_IM_FROM_ID].asUUID();
+            if (add_chat_to_conversation)
+            {   // Check if they're muted
+                if (LLMuteList::getInstance()->isMuted(sender_id, LLMute::flagTextChat))
+                {
+                    add_chat_to_conversation = false;
+                    LL_DEBUGS("ChatHistory") << mSessionID << ": Skipped adding chat from " << sender_id
+                        << " as muted, message: " << cur_server_hist
+                        << LL_ENDL;
+                }
+            }
+
+            if (add_chat_to_conversation)
+            {   // Finally add message to the chat session
+                std::string chat_time_str = LLConversation::createTimestamp((U64Seconds)history_msg_timestamp);
+                std::string sender_name = cur_server_hist[LL_IM_FROM].asString();
+
+                std::string history_msg_text = cur_server_hist[LL_IM_TEXT].asString();
+                LLSD message;
+                message["from"] = sender_name;
+                message["from_id"] = sender_id;
+                message["message"] = history_msg_text;
+                message["time"] = chat_time_str;
+                message["timestamp"] = (S32)history_msg_timestamp;
+                message["index"] = (LLSD::Integer)mMsgs.size();
+                message["is_history"] = true;
+                mMsgs.push_front(message);
+
+                LL_DEBUGS("ChatHistory") << mSessionID << ": push_front() adding group chat history message " << message << LL_ENDL;
+
+                // Add chat history messages to the local cache file, only in the case where we opened the chat window
+                // Need to solve the logic around messages that arrive and open chat - at this point, they've already been added to the
+                //   local history cache file.   If we append messages here, it will be out of order.
+                if (target_from.empty() && target_message.empty())
+                {
+                    LLIMModel::getInstance()->logToFile(LLIMModel::getInstance()->getHistoryFileName(mSessionID),
+                        sender_name, sender_id, history_msg_text);
+                }
+            }
+        }
+    }
+
+    S32 shifted_size = shift_msgs.size();
+    while (shift_msgs.size() > 0)
+    {   // Finally add back any new messages, and tweak the index value to be correct.
+        LLSD newer_message = shift_msgs.front();
+        shift_msgs.pop_front();
+        S32 old_index = newer_message["index"];
+        newer_message["index"] = (LLSD::Integer)mMsgs.size();   // Update the index to match the new position in the conversation
+        LL_DEBUGS("ChatHistory") << mSessionID << ": Re-adding newest group chat history messages from " << newer_message["from"]
+            << ", text: " << newer_message["message"]
+            << " old index " << old_index << ", new index " << newer_message["index"] << LL_ENDL;
+        mMsgs.push_front(newer_message);
+    }
+
+    LL_DEBUGS("ChatHistory") << mSessionID << ": addMessagesFromServerHistory() exiting with mMsg.size() " << mMsgs.size()
+        << ", shifted " << shifted_size << " messages" << LL_ENDL;
+
+    mLastHistoryCacheDateTime.clear();  // Don't need this data
+    mLastHistoryCacheMsgs.clear();
 }
 
+
 void LLIMModel::LLIMSession::chatFromLogFile(LLLogChat::ELogLineType type, const LLSD& msg, void* userdata)
 {
 	if (!userdata) return;
@@ -860,26 +1210,29 @@ void LLIMModel::LLIMSession::chatFromLogFile(LLLogChat::ELogLineType type, const
 
 	if (type == LLLogChat::LOG_LINE)
 	{
-		self->addMessage("", LLSD(), msg["message"].asString(), "", true);
+        LL_DEBUGS("ChatHistory") << "chatFromLogFile() adding LOG_LINE message from " << msg << LL_ENDL;
+        self->addMessage("", LLSD(), msg["message"].asString(), "", true, false, 0);        // from history data, not region message, no timestamp
 	}
 	else if (type == LLLogChat::LOG_LLSD)
 	{
-		self->addMessage(msg["from"].asString(), msg["from_id"].asUUID(), msg["message"].asString(), msg["time"].asString(), true);
+        LL_DEBUGS("ChatHistory") << "chatFromLogFile() adding LOG_LLSD message from " << msg << LL_ENDL;
+        self->addMessage(msg["from"].asString(), msg["from_id"].asUUID(), msg["message"].asString(), msg["time"].asString(), true, false, 0);  // from history data, not region message, no timestamp
 	}
 }
 
 void LLIMModel::LLIMSession::loadHistory()
 {
 	mMsgs.clear();
+    mLastHistoryCacheMsgs.clear();
+    mLastHistoryCacheDateTime.clear();
 
 	if ( gSavedPerAccountSettings.getBOOL("LogShowHistory") )
 	{
-		std::list<LLSD> chat_history;
-
-		//involves parsing of a chat history
+        // read and parse chat history from local file
+        chat_message_list_t chat_history;
 		LLLogChat::loadChatHistory(mHistoryFileName, chat_history, LLSD(), isGroupChat());
-		addMessagesFromHistory(chat_history);
-	}
+        addMessagesFromHistoryCache(chat_history);
+    }
 }
 
 LLIMModel::LLIMSession* LLIMModel::findIMSession(const LLUUID& session_id) const
@@ -899,7 +1252,7 @@ LLIMModel::LLIMSession* LLIMModel::findAdHocIMSession(const uuid_vec_t& ids)
 	for (; it != mId2SessionMap.end(); ++it)
 	{
 		LLIMSession* session = (*it).second;
-	
+
 		if (!session->isAdHoc()) continue;
 		if (session->mInitialTargetIDs.size() != num) continue;
 
@@ -910,8 +1263,8 @@ LLIMModel::LLIMSession* LLIMModel::findAdHocIMSession(const uuid_vec_t& ids)
 		{
 			tmp_list.remove(*iter);
 			++iter;
-			
-			if (tmp_list.empty()) 
+
+			if (tmp_list.empty())
 			{
 				break;
 			}
@@ -967,7 +1320,7 @@ void LLIMModel::LLIMSession::buildHistoryFileName()
 	if (isAdHoc())
 	{
 		/* in case of outgoing ad-hoc sessions we need to make specilized names
-		* if this naming system is ever changed then the filtering definitions in 
+		* if this naming system is ever changed then the filtering definitions in
 		* lllogchat.cpp need to be change acordingly so that the filtering for the
 		* date stamp code introduced in STORM-102 will work properly and not add
 		* a date stamp to the Ad-hoc conferences.
@@ -980,7 +1333,7 @@ void LLIMModel::LLIMSession::buildHistoryFileName()
 		else
 		{
 			//in case of incoming ad-hoc sessions
-			mHistoryFileName = mName + " " + LLLogChat::timestamp(true) + " " + mSessionID.asString().substr(0, 4);
+			mHistoryFileName = mName + " " + LLLogChat::timestamp2LogString(0, true) + " " + mSessionID.asString().substr(0, 4);
 		}
 	}
 	else if (isP2P()) // look up username to use as the log name
@@ -1011,7 +1364,7 @@ void LLIMModel::LLIMSession::buildHistoryFileName()
 LLUUID LLIMModel::LLIMSession::generateHash(const std::set<LLUUID>& sorted_uuids)
 {
 	LLMD5 md5_uuid;
-	
+
 	std::set<LLUUID>::const_iterator it = sorted_uuids.begin();
 	while (it != sorted_uuids.end())
 	{
@@ -1073,7 +1426,7 @@ void LLIMModel::testMessages()
 
 	S32 rand1 = ll_rand(sizeof firstname)/(sizeof firstname[0]);
 	S32 rand2 = ll_rand(sizeof lastname)/(sizeof lastname[0]);
-	
+
 	from = firstname[rand1] + " " + lastname[rand2];
 	bot2_id.generate(from);
 	LLUUID bot2_session_id = LLIMMgr::computeSessionID(IM_NOTHING_SPECIAL, bot2_id);
@@ -1083,7 +1436,7 @@ void LLIMModel::testMessages()
 }
 
 //session name should not be empty
-bool LLIMModel::newSession(const LLUUID& session_id, const std::string& name, const EInstantMessage& type, 
+bool LLIMModel::newSession(const LLUUID& session_id, const std::string& name, const EInstantMessage& type,
 						   const LLUUID& other_participant_id, const uuid_vec_t& ids, bool voice, bool has_offline_msg)
 {
 	if (name.empty())
@@ -1125,7 +1478,7 @@ bool LLIMModel::clearSession(const LLUUID& session_id)
 	return true;
 }
 
-void LLIMModel::getMessages(const LLUUID& session_id, std::list<LLSD>& messages, int start_index, const bool sendNoUnreadMsgs)
+void LLIMModel::getMessages(const LLUUID& session_id, chat_message_list_t& messages, int start_index, const bool sendNoUnreadMsgs)
 {
 	getMessagesSilently(session_id, messages, start_index);
 
@@ -1135,7 +1488,7 @@ void LLIMModel::getMessages(const LLUUID& session_id, std::list<LLSD>& messages,
 	}
 }
 
-void LLIMModel::getMessagesSilently(const LLUUID& session_id, std::list<LLSD>& messages, int start_index)
+void LLIMModel::getMessagesSilently(const LLUUID& session_id, chat_message_list_t& messages, int start_index)
 {
 	LLIMSession* session = findIMSession(session_id);
 	if (!session)
@@ -1146,7 +1499,7 @@ void LLIMModel::getMessagesSilently(const LLUUID& session_id, std::list<LLSD>& m
 
 	int i = session->mMsgs.size() - start_index;
 
-	for (std::list<LLSD>::iterator iter = session->mMsgs.begin();
+	for (chat_message_list_t::iterator iter = session->mMsgs.begin();
 		iter != session->mMsgs.end() && i > 0;
 		iter++)
 	{
@@ -1168,7 +1521,7 @@ void LLIMModel::sendNoUnreadMessages(const LLUUID& session_id)
 
 	session->mNumUnread = 0;
 	session->mParticipantUnreadMessageCount = 0;
-	
+
 	LLSD arg;
 	arg["session_id"] = session_id;
 	arg["num_unread"] = 0;
@@ -1176,17 +1529,23 @@ void LLIMModel::sendNoUnreadMessages(const LLUUID& session_id)
 	mNoUnreadMsgsSignal(arg);
 }
 
-bool LLIMModel::addToHistory(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text, bool is_region_msg) {
-	
+bool LLIMModel::addToHistory(const LLUUID& session_id,
+                             const std::string& from,
+                             const LLUUID& from_id,
+                             const std::string& utf8_text,
+                             bool is_region_msg,
+                             U32 timestamp)
+{
 	LLIMSession* session = findIMSession(session_id);
 
-	if (!session) 
+	if (!session)
 	{
 		LL_WARNS() << "session " << session_id << "does not exist " << LL_ENDL;
 		return false;
 	}
 
-	session->addMessage(from, from_id, utf8_text, LLLogChat::timestamp(false), false, is_region_msg); //might want to add date separately
+    // This is where a normal arriving message is added to the session.   Note that the time string created here is without the full date
+	session->addMessage(from, from_id, utf8_text, LLLogChat::timestamp2LogString(timestamp, false), false, is_region_msg, timestamp);
 
 	return true;
 }
@@ -1194,14 +1553,14 @@ bool LLIMModel::addToHistory(const LLUUID& session_id, const std::string& from,
 bool LLIMModel::logToFile(const std::string& file_name, const std::string& from, const LLUUID& from_id, const std::string& utf8_text)
 {
 	if (gSavedPerAccountSettings.getS32("KeepConversationLogTranscripts") > 1)
-	{	
+	{
 		std::string from_name = from;
 
 		LLAvatarName av_name;
-		if (!from_id.isNull() && 
+		if (!from_id.isNull() &&
 			LLAvatarNameCache::get(from_id, &av_name) &&
 			!av_name.isDisplayNameDefault())
-		{	
+		{
 			from_name = av_name.getCompleteName();
 		}
 
@@ -1216,36 +1575,37 @@ bool LLIMModel::logToFile(const std::string& file_name, const std::string& from,
 }
 
 void LLIMModel::proccessOnlineOfflineNotification(
-	const LLUUID& session_id, 
-	const std::string& utf8_text)
+	const LLUUID& session_id,
+    const std::string& utf8_text)
 {
 	// Add system message to history
 	addMessage(session_id, SYSTEM_FROM, LLUUID::null, utf8_text);
 }
 
-void LLIMModel::addMessage(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, 
-						   const std::string& utf8_text, bool log2file /* = true */, bool is_region_msg /* = false */) {
-
+void LLIMModel::addMessage(const LLUUID& session_id, const std::string& from, const LLUUID& from_id,
+						   const std::string& utf8_text, bool log2file /* = true */, bool is_region_msg, /* = false */ U32 time_stamp /* = 0 */)
+{
     if (gSavedSettings.getBOOL("TranslateChat") && (from != SYSTEM_FROM))
     {
         const std::string from_lang = ""; // leave empty to trigger autodetect
         const std::string to_lang = LLTranslate::getTranslateLanguage();
-
+        U64 time_n_flags = ((U64) time_stamp) | (log2file ? (1LL << 32) : 0) | (is_region_msg ? (1LL << 33) : 0);   // boost::bind has limited parameters
         LLTranslate::translateMessage(from_lang, to_lang, utf8_text,
-            boost::bind(&translateSuccess, session_id, from, from_id, utf8_text, log2file, utf8_text, from_lang, _1, _2),
-            boost::bind(&translateFailure, session_id, from, from_id, utf8_text, log2file, _1, _2));
+            boost::bind(&translateSuccess, session_id, from, from_id, utf8_text, time_n_flags, utf8_text, from_lang, _1, _2),
+            boost::bind(&translateFailure, session_id, from, from_id, utf8_text, time_n_flags, _1, _2));
     }
     else
     {
-        processAddingMessage(session_id, from, from_id, utf8_text, log2file, is_region_msg);
+        processAddingMessage(session_id, from, from_id, utf8_text, log2file, is_region_msg, time_stamp);
     }
 }
 
 void LLIMModel::processAddingMessage(const LLUUID& session_id, const std::string& from, const LLUUID& from_id,
-    const std::string& utf8_text, bool log2file /* = true */, bool is_region_msg /* = false */)
+    const std::string& utf8_text, bool log2file, bool is_region_msg, U32 time_stamp)
 {
-    LLIMSession* session = addMessageSilently(session_id, from, from_id, utf8_text, log2file, is_region_msg);
-    if (!session) return;
+    LLIMSession* session = addMessageSilently(session_id, from, from_id, utf8_text, log2file, is_region_msg, time_stamp);
+    if (!session)
+        return;
 
     //good place to add some1 to recent list
     //other places may be called from message history.
@@ -1261,15 +1621,16 @@ void LLIMModel::processAddingMessage(const LLUUID& session_id, const std::string
     arg["message"] = utf8_text;
     arg["from"] = from;
     arg["from_id"] = from_id;
-    arg["time"] = LLLogChat::timestamp(false);
+    arg["time"] = LLLogChat::timestamp2LogString(time_stamp, true);
     arg["session_type"] = session->mSessionType;
     arg["is_region_msg"] = is_region_msg;
 
     mNewMsgSignal(arg);
 }
 
-LLIMModel::LLIMSession* LLIMModel::addMessageSilently(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, 
-													 const std::string& utf8_text, bool log2file, bool is_region_msg)
+LLIMModel::LLIMSession* LLIMModel::addMessageSilently(const LLUUID& session_id, const std::string& from, const LLUUID& from_id,
+													  const std::string& utf8_text, bool log2file /* = true */, bool is_region_msg, /* false */
+                                                      U32 timestamp /* = 0 */)
 {
 	LLIMSession* session = findIMSession(session_id);
 
@@ -1285,12 +1646,12 @@ LLIMModel::LLIMSession* LLIMModel::addMessageSilently(const LLUUID& session_id,
 		from_name = SYSTEM_FROM;
 	}
 
-	addToHistory(session_id, from_name, from_id, utf8_text, is_region_msg);
+	addToHistory(session_id, from_name, from_id, utf8_text, is_region_msg, timestamp);
 	if (log2file)
 	{
 		logToFile(getHistoryFileName(session_id), from_name, from_id, utf8_text);
 	}
-	
+
 	session->mNumUnread++;
 
 	//update count of unread messages from real participant
@@ -1392,7 +1753,7 @@ const std::string& LLIMModel::getHistoryFileName(const LLUUID& session_id) const
 
 
 // TODO get rid of other participant ID
-void LLIMModel::sendTypingState(LLUUID session_id, LLUUID other_participant_id, BOOL typing) 
+void LLIMModel::sendTypingState(LLUUID session_id, LLUUID other_participant_id, BOOL typing)
 {
 	std::string name;
 	LLAgentUI::buildFullname(name);
@@ -1423,7 +1784,7 @@ void LLIMModel::sendLeaveSession(const LLUUID& session_id, const LLUUID& other_p
 			FALSE,
 			gAgent.getSessionID(),
 			other_participant_id,
-			name, 
+			name,
 			LLStringUtil::null,
 			IM_ONLINE,
 			IM_SESSION_LEAVE,
@@ -1444,7 +1805,7 @@ void LLIMModel::sendMessage(const std::string& utf8_text,
 
 	const LLRelationship* info = NULL;
 	info = LLAvatarTracker::instance().getBuddyInfo(other_participant_id);
-	
+
 	U8 offline = (!info || info->isOnline()) ? IM_ONLINE : IM_OFFLINE;
 	// Old call to send messages to SLim client,  no longer supported.
 	//if((offline == IM_OFFLINE) && (LLVoiceClient::getInstance()->isOnlineSIP(other_participant_id)))
@@ -1452,7 +1813,7 @@ void LLIMModel::sendMessage(const std::string& utf8_text,
 	//	// User is online through the OOW connector, but not with a regular viewer.  Try to send the message via SLVoice.
 	//	sent = LLVoiceClient::getInstance()->sendTextMessage(other_participant_id, utf8_text);
 	//}
-	
+
 	if(!sent)
 	{
 		// Send message normally.
@@ -1509,7 +1870,7 @@ void LLIMModel::sendMessage(const std::string& utf8_text,
 		}
 	}
 
-	if((dialog == IM_NOTHING_SPECIAL) && 
+	if((dialog == IM_NOTHING_SPECIAL) &&
 	   (other_participant_id.notNull()))
 	{
 		// Do we have to replace the /me's here?
@@ -1547,7 +1908,7 @@ void LLIMModel::sendMessage(const std::string& utf8_text,
 		// to Recent People to prevent showing of an item with (?? ?)(?? ?), sans the spaces. See EXT-8246.
 		// Concrete participants will be added into this list once they sent message in chat.
 		if (IM_SESSION_INVITE == dialog) return;
-			
+
 		if (IM_SESSION_CONFERENCE_START == dialog) // outgoing ad-hoc session
 		{
 			// Add only online members of conference to recent list (EXT-8658)
@@ -1629,7 +1990,7 @@ void start_deprecated_conference_chat(
 	for(S32 i = 0; i < count; ++i)
 	{
 		LLUUID agent_id = agents_to_invite[i].asUUID();
-		
+
 		memcpy(pos, &agent_id, UUID_BYTES);
 		pos += UUID_BYTES;
 	}
@@ -1645,7 +2006,7 @@ void start_deprecated_conference_chat(
 		bucket_size);
 
 	gAgent.sendReliableMessage();
- 
+
 	delete[] bucket;
 }
 
@@ -1866,7 +2227,7 @@ void LLCallDialogManager::onVoiceChannelChangedInt(const LLUUID &session_id)
 {
 	LLIMModel::LLIMSession* session = LLIMModel::getInstance()->findIMSession(session_id);
 	if(!session)
-	{		
+	{
 		mPreviousSessionlName = mCurrentSessionlName;
 		mCurrentSessionlName = ""; // Empty string results in "Nearby Voice Chat" after substitution
 		return;
@@ -1889,7 +2250,7 @@ void LLCallDialogManager::onVoiceChannelChangedInt(const LLUUID &session_id)
 	if (LLVoiceChannel::getCurrentVoiceChannel()->getState() == LLVoiceChannel::STATE_CALL_STARTED &&
 		LLVoiceChannel::getCurrentVoiceChannel()->getCallDirection() == LLVoiceChannel::OUTGOING_CALL)
 	{
-		
+
 		//*TODO get rid of duplicated code
 		LLSD mCallDialogPayload;
 		mCallDialogPayload["session_id"] = mSession->mSessionID;
@@ -1904,7 +2265,7 @@ void LLCallDialogManager::onVoiceChannelChangedInt(const LLUUID &session_id)
 		if(ocd)
 		{
 			ocd->show(mCallDialogPayload);
-		}	
+		}
 	}
 
 }
@@ -1937,7 +2298,7 @@ void LLCallDialogManager::onVoiceChannelStateChangedInt(const LLVoiceChannel::ES
 	mCallDialogPayload["ended_by_agent"] = ended_by_agent;
 
 	switch(new_state)
-	{			
+	{
 	case LLVoiceChannel::STATE_CALL_STARTED :
 		// do not show "Calling to..." if it is incoming call
 		if(direction == LLVoiceChannel::INCOMING_CALL)
@@ -1966,7 +2327,7 @@ void LLCallDialogManager::onVoiceChannelStateChangedInt(const LLVoiceChannel::ES
 	if(ocd)
 	{
 		ocd->show(mCallDialogPayload);
-	}	
+	}
 }
 
 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1992,9 +2353,9 @@ BOOL LLCallDialog::postBuild()
 {
 	if (!LLDockableFloater::postBuild() || !gToolBarView)
 		return FALSE;
-	
+
 	dockToToolbarButton("speak");
-	
+
 	return TRUE;
 }
 
@@ -2012,20 +2373,20 @@ LLDockControl::DocAt LLCallDialog::getDockControlPos(const std::string& toolbarB
 {
 	LLCommandId command_id(toolbarButtonName);
 	S32 toolbar_loc = gToolBarView->hasCommand(command_id);
-	
+
 	LLDockControl::DocAt doc_at = LLDockControl::TOP;
-	
+
 	switch (toolbar_loc)
 	{
 		case LLToolBarEnums::TOOLBAR_LEFT:
 			doc_at = LLDockControl::RIGHT;
 			break;
-			
+
 		case LLToolBarEnums::TOOLBAR_RIGHT:
 			doc_at = LLDockControl::LEFT;
 			break;
 	}
-	
+
 	return doc_at;
 }
 
@@ -2040,7 +2401,7 @@ LLCallDialog(payload)
 	if(instance && instance->getVisible())
 	{
 		instance->onCancel(instance);
-	}	
+	}
 }
 
 void LLCallDialog::draw()
@@ -2097,7 +2458,7 @@ bool LLCallDialog::lifetimeHasExpired()
 	if (mLifetimeTimer.getStarted())
 	{
 		F32 elapsed_time = mLifetimeTimer.getElapsedTimeF32();
-		if (elapsed_time > mLifetime) 
+		if (elapsed_time > mLifetime)
 		{
 			return true;
 		}
@@ -2139,7 +2500,7 @@ void LLOutgoingCallDialog::show(const LLSD& key)
 	}
 	else
 	{
-		getChild<LLUICtrl>("leaving")->setTextArg("[CURRENT_CHAT]", getString("localchat"));		
+		getChild<LLUICtrl>("leaving")->setTextArg("[CURRENT_CHAT]", getString("localchat"));
 	}
 
 	if (!mPayload["disconnected_channel_name"].asString().empty())
@@ -2163,7 +2524,7 @@ void LLOutgoingCallDialog::show(const LLSD& key)
 	{
 		callee_name = getString("anonymous");
 	}
-	
+
 	LLSD callee_id = mPayload["other_user_id"];
 	// Beautification:  Since you know who you called, just show display name
 	std::string title = callee_name;
@@ -2219,7 +2580,7 @@ void LLOutgoingCallDialog::show(const LLSD& key)
 		{
 			const std::string& nearby_str = mPayload["ended_by_agent"] ? NEARBY_P2P_BY_AGENT : NEARBY_P2P_BY_OTHER;
 			getChild<LLTextBox>(nearby_str)->setVisible(true);
-		} 
+		}
 		else
 		{
 			getChild<LLTextBox>("nearby")->setVisible(true);
@@ -2253,7 +2614,7 @@ void LLOutgoingCallDialog::onCancel(void* user_data)
 
 	LLUUID session_id = self->mPayload["session_id"].asUUID();
 	gIMMgr->endCall(session_id);
-	
+
 	self->closeFloater();
 }
 
@@ -2338,7 +2699,7 @@ BOOL LLIncomingCallDialog::postBuild()
         LL_INFOS("IMVIEW") << "IncomingCall: notify_box_type was not provided" << LL_ENDL;
         return TRUE;
     }
-	
+
 	// init notification's lifetime
 	std::istringstream ss( getString("lifetime") );
 	if (!(ss >> mLifetime))
@@ -2513,7 +2874,7 @@ void LLIncomingCallDialog::processCallResponse(S32 response, const LLSD &payload
 			if (session_name.empty())
 			{
 				LL_WARNS() << "Received an empty session name from a server" << LL_ENDL;
-				
+
 				switch(type){
 				case IM_SESSION_CONFERENCE_START:
 				case IM_SESSION_GROUP_START:
@@ -2531,17 +2892,17 @@ void LLIncomingCallDialog::processCallResponse(S32 response, const LLSD &payload
 						if (LLAvatarNameCache::get(caller_id, &av_name))
 						{
 							correct_session_name = av_name.getCompleteName();
-							correct_session_name.append(ADHOC_NAME_SUFFIX); 
+							correct_session_name.append(ADHOC_NAME_SUFFIX);
 						}
 					}
 					LL_INFOS("IMVIEW") << "Corrected session name is " << correct_session_name << LL_ENDL;
 					break;
-				default: 
+				default:
 					LL_WARNS("IMVIEW") << "Received an empty session name from a server and failed to generate a new proper session name" << LL_ENDL;
 					break;
 				}
 			}
-			
+
 			gIMMgr->addSession(correct_session_name, type, session_id, true);
 
 			std::string url = gAgent.getRegion()->getCapability(
@@ -2553,7 +2914,7 @@ void LLIncomingCallDialog::processCallResponse(S32 response, const LLSD &payload
                     boost::bind(&chatterBoxInvitationCoro, url,
                     session_id, inv_type));
 
-				// send notification message to the corresponding chat 
+				// send notification message to the corresponding chat
 				if (payload["notify_box_type"].asString() == "VoiceInviteGroup" || payload["notify_box_type"].asString() == "VoiceInviteAdHoc")
 				{
 					LLStringUtil::format_map_t string_args;
@@ -2588,7 +2949,7 @@ void LLIncomingCallDialog::processCallResponse(S32 response, const LLSD &payload
 			data["session-id"] = session_id;
 
             LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost(url, data,
-                "Invitation declined", 
+                "Invitation declined",
                 "Invitation decline failed.");
 		}
 	}
@@ -2608,7 +2969,7 @@ bool inviteUserResponse(const LLSD& notification, const LLSD& response)
 	EInstantMessage type = (EInstantMessage)payload["type"].asInteger();
 	LLIMMgr::EInvitationType inv_type = (LLIMMgr::EInvitationType)payload["inv_type"].asInteger();
 	S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
-	switch(option) 
+	switch(option)
 	{
 	case 0: // accept
 		{
@@ -2652,7 +3013,7 @@ bool inviteUserResponse(const LLSD& notification, const LLSD& response)
 		}
 	}
 	/* FALLTHROUGH */
-	
+
 	case 1: // decline
 	{
 		if (type == IM_SESSION_P2P_INVITE)
@@ -2668,8 +3029,8 @@ bool inviteUserResponse(const LLSD& notification, const LLSD& response)
 			LLSD data;
 			data["method"] = "decline invitation";
 			data["session-id"] = session_id;
-            LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost(url, data, 
-                "Invitation declined.", 
+            LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost(url, data,
+                "Invitation declined.",
                 "Invitation decline failed.");
 		}
 	}
@@ -2678,7 +3039,7 @@ bool inviteUserResponse(const LLSD& notification, const LLSD& response)
 	gIMMgr->clearPendingInvitation(session_id);
 	break;
 	}
-	
+
 	return false;
 }
 
@@ -2694,7 +3055,7 @@ LLIMMgr::LLIMMgr()
 	LLIMModel::getInstance()->addNewMsgCallback(boost::bind(&LLFloaterIMSession::sRemoveTypingIndicator, _1));
 }
 
-// Add a message to a session. 
+// Add a message to a session.
 void LLIMMgr::addMessage(
 	const LLUUID& session_id,
 	const LLUUID& target_id,
@@ -2706,7 +3067,8 @@ void LLIMMgr::addMessage(
 	U32 parent_estate_id,
 	const LLUUID& region_id,
 	const LLVector3& position,
-	bool is_region_msg)
+    bool is_region_msg,
+    U32 timestamp)      // May be zero
 {
 	LLUUID other_participant_id = target_id;
 
@@ -2793,6 +3155,14 @@ void LLIMMgr::addMessage(
 				return;
 			}
 
+            // Fetch group chat history, enabled by default.
+            if (gSavedPerAccountSettings.getBOOL("FetchGroupChatHistory"))
+            {
+                std::string chat_url = gAgent.getRegion()->getCapability("ChatSessionRequest");
+                LLCoros::instance().launch("chatterBoxHistoryCoro",
+                    boost::bind(&chatterBoxHistoryCoro, chat_url, session_id, from, msg, timestamp));
+            }
+
 			//Play sound for new conversations
 			if (!skip_message & !gAgent.isDoNotDisturb() && (gSavedSettings.getBOOL("PlaySoundNewConversation") == TRUE))
 			{
@@ -2808,7 +3178,7 @@ void LLIMMgr::addMessage(
 
 	if (!LLMuteList::getInstance()->isMuted(other_participant_id, LLMute::flagTextChat) && !skip_message)
 	{
-		LLIMModel::instance().addMessage(new_session_id, from, other_participant_id, msg, true, is_region_msg);
+		LLIMModel::instance().addMessage(new_session_id, from, other_participant_id, msg, true, is_region_msg, timestamp);
 	}
 
 	// Open conversation floater if offline messages are present
@@ -2823,7 +3193,7 @@ void LLIMMgr::addMessage(
 void LLIMMgr::addSystemMessage(const LLUUID& session_id, const std::string& message_name, const LLSD& args)
 {
 	LLUIString message;
-	
+
 	// null session id means near me (chat history)
 	if (session_id.isNull())
 	{
@@ -2863,7 +3233,7 @@ void LLIMMgr::addSystemMessage(const LLUUID& session_id, const std::string& mess
 S32 LLIMMgr::getNumberOfUnreadIM()
 {
 	std::map<LLUUID, LLIMModel::LLIMSession*>::iterator it;
-	
+
 	S32 num = 0;
 	for(it = LLIMModel::getInstance()->mId2SessionMap.begin(); it != LLIMModel::getInstance()->mId2SessionMap.end(); ++it)
 	{
@@ -2890,7 +3260,7 @@ void LLIMMgr::autoStartCallOnStartup(const LLUUID& session_id)
 {
 	LLIMModel::LLIMSession *session = LLIMModel::getInstance()->findIMSession(session_id);
 	if (!session) return;
-	
+
 	if (session->mSessionInitialized)
 	{
 		startCall(session_id);
@@ -2898,7 +3268,7 @@ void LLIMMgr::autoStartCallOnStartup(const LLUUID& session_id)
 	else
 	{
 		session->mStartCallOnInitialize = true;
-	}	
+	}
 }
 
 LLUUID LLIMMgr::addP2PSession(const std::string& name,
@@ -2935,7 +3305,7 @@ LLUUID LLIMMgr::addSession(
 	return session_id;
 }
 
-// Adds a session using the given session_id.  If the session already exists 
+// Adds a session using the given session_id.  If the session already exists
 // the dialog type is assumed correct. Returns the uuid of the session.
 LLUUID LLIMMgr::addSession(
 	const std::string& name,
@@ -2998,9 +3368,9 @@ LLUUID LLIMMgr::addSession(
 
 	//we don't need to show notes about online/offline, mute/unmute users' statuses for existing sessions
 	if (!new_session) return session_id;
-	
+
     LL_INFOS("IMVIEW") << "LLIMMgr::addSession, new session added, name = " << name << ", session id = " << session_id << LL_ENDL;
-    
+
 	//Per Plan's suggestion commented "explicit offline status warning" out to make Dessie happier (see EXT-3609)
 	//*TODO After February 2010 remove this commented out line if no one will be missing that warning
 	//noteOfflineUsers(session_id, floater, ids);
@@ -3030,7 +3400,7 @@ bool LLIMMgr::leaveSession(const LLUUID& session_id)
 void LLIMMgr::removeSession(const LLUUID& session_id)
 {
 	llassert_always(hasSession(session_id));
-	
+
 	clearPendingInvitation(session_id);
 	clearPendingAgentListUpdates(session_id);
 
@@ -3042,9 +3412,9 @@ void LLIMMgr::removeSession(const LLUUID& session_id)
 }
 
 void LLIMMgr::inviteToSession(
-	const LLUUID& session_id, 
-	const std::string& session_name, 
-	const LLUUID& caller_id, 
+	const LLUUID& session_id,
+	const std::string& session_name,
+	const LLUUID& caller_id,
 	const std::string& caller_name,
 	EInstantMessage type,
 	EInvitationType inv_type,
@@ -3161,22 +3531,22 @@ void LLIMMgr::inviteToSession(
 	{
 		if (caller_name.empty())
 		{
-			LLAvatarNameCache::get(caller_id, 
+			LLAvatarNameCache::get(caller_id,
 				boost::bind(&LLIMMgr::onInviteNameLookup, payload, _1, _2));
 		}
 		else
 		{
 			LLFloaterReg::showInstance("incoming_call", payload, FALSE);
 		}
-		
-		// Add the caller to the Recent List here (at this point 
+
+		// Add the caller to the Recent List here (at this point
 		// "incoming_call" floater is shown and the recipient can
 		// reject the call), because even if a recipient will reject
 		// the call, the caller should be added to the recent list
 		// anyway. STORM-507.
 		if(type == IM_SESSION_P2P_INVITE)
 			LLRecentPeople::instance().add(caller_id);
-		
+
 		mPendingInvitations[session_id.asString()] = LLSD();
 	}
 }
@@ -3373,7 +3743,7 @@ bool LLIMMgr::startCall(const LLUUID& session_id, LLVoiceChannel::EDirection dir
 {
 	LLVoiceChannel* voice_channel = LLIMModel::getInstance()->getVoiceChannel(session_id);
 	if (!voice_channel) return false;
-	
+
 	voice_channel->setCallDirection(direction);
 	voice_channel->activate();
 	return true;
@@ -3491,7 +3861,7 @@ void LLIMMgr::noteMutedUsers(const LLUUID& session_id,
 	if(count > 0)
 	{
 		LLIMModel* im_model = LLIMModel::getInstance();
-		
+
 		for(S32 i = 0; i < count; ++i)
 		{
 			if( ml->isMuted(ids.at(i)) )
@@ -3569,7 +3939,15 @@ class LLViewerChatterBoxSessionStartReply : public LLHTTPNode
 				if ( body.has("session_info") )
 				{
 					im_floater->processSessionUpdate(body["session_info"]);
-				}
+
+                    // Send request for chat history, if enabled.
+                    if (gSavedPerAccountSettings.getBOOL("FetchGroupChatHistory"))
+                    {
+                        std::string url = gAgent.getRegion()->getCapability("ChatSessionRequest");
+                        LLCoros::instance().launch("chatterBoxHistoryCoro",
+                            boost::bind(&chatterBoxHistoryCoro, url, session_id, "", "", 0));
+                    }
+                }
 			}
 
 			gIMMgr->clearPendingAgentListUpdates(session_id);
@@ -3698,7 +4076,7 @@ class LLViewerChatterBoxInvitation : public LLHTTPNode
 			LLUUID session_id = message_params["id"].asUUID();
 			std::vector<U8> bin_bucket = message_params["data"]["binary_bucket"].asBinary();
 			U8 offline = (U8)message_params["offline"].asInteger();
-			
+
 			time_t timestamp =
 				(time_t) message_params["timestamp"].asInteger();
 
@@ -3735,7 +4113,9 @@ class LLViewerChatterBoxInvitation : public LLHTTPNode
 				IM_SESSION_INVITE,
 				message_params["parent_estate_id"].asInteger(),
 				message_params["region_id"].asUUID(),
-				ll_vector3_from_sd(message_params["position"]));
+				ll_vector3_from_sd(message_params["position"]),
+                false,      // is_region_message
+                timestamp);
 
 			if (LLMuteList::getInstance()->isMuted(from_id, name, LLMute::flagTextChat))
 			{
@@ -3761,8 +4141,8 @@ class LLViewerChatterBoxInvitation : public LLHTTPNode
 			}
 
 			gIMMgr->inviteToSession(
-				input["body"]["session_id"].asUUID(), 
-				input["body"]["session_name"].asString(), 
+				input["body"]["session_id"].asUUID(),
+				input["body"]["session_name"].asString(),
 				input["body"]["from_id"].asUUID(),
 				input["body"]["from_name"].asString(),
 				IM_SESSION_INVITE,
@@ -3771,8 +4151,8 @@ class LLViewerChatterBoxInvitation : public LLHTTPNode
 		else if ( input["body"].has("immediate") )
 		{
 			gIMMgr->inviteToSession(
-				input["body"]["session_id"].asUUID(), 
-				input["body"]["session_name"].asString(), 
+				input["body"]["session_id"].asUUID(),
+				input["body"]["session_name"].asString(),
 				input["body"]["from_id"].asUUID(),
 				input["body"]["from_name"].asString(),
 				IM_SESSION_INVITE,
diff --git a/indra/newview/llimview.h b/indra/newview/llimview.h
index 5e99cc7fca7..946eb02f26b 100644
--- a/indra/newview/llimview.h
+++ b/indra/newview/llimview.h
@@ -42,6 +42,7 @@ class LLAvatarName;
 class LLFriendObserver;
 class LLCallDialogManager;	
 class LLIMSpeakerMgr;
+
 /**
  * Timeout Timer for outgoing Ad-Hoc/Group IM sessions which being initialized by the server
  */
@@ -63,11 +64,14 @@ class LLSessionTimeoutTimer : public LLEventTimer
 class LLIMModel :  public LLSingleton<LLIMModel>
 {
 	LLSINGLETON(LLIMModel);
+
 public:
 
-	struct LLIMSession : public boost::signals2::trackable
+    typedef std::list<LLSD> chat_message_list_t;
+
+    struct LLIMSession : public boost::signals2::trackable
 	{
-		typedef enum e_session_type
+        typedef enum e_session_type
 		{   // for now we have 4 predefined types for a session
 			P2P_SESSION,
 			GROUP_SESSION,
@@ -75,15 +79,23 @@ class LLIMModel :  public LLSingleton<LLIMModel>
 			NONE_SESSION,
 		} SType;
 
-		LLIMSession(const LLUUID& session_id, const std::string& name, 
+		LLIMSession(const LLUUID& session_id, const std::string& name,
 			const EInstantMessage& type, const LLUUID& other_participant_id, const uuid_vec_t& ids, bool voice, bool has_offline_msg);
 		virtual ~LLIMSession();
 
 		void sessionInitReplyReceived(const LLUUID& new_session_id);
-		void addMessagesFromHistory(const std::list<LLSD>& history);
-		void addMessage(const std::string& from, const LLUUID& from_id, const std::string& utf8_text, const std::string& time, const bool is_history = false, bool is_region_msg = false);
-		void onVoiceChannelStateChanged(const LLVoiceChannel::EState& old_state, const LLVoiceChannel::EState& new_state, const LLVoiceChannel::EDirection& direction);
-		
+		void addMessagesFromHistoryCache(const std::list<LLSD>& history);        // From local file
+        void addMessagesFromServerHistory(const LLSD& history, const std::string& target_from, const std::string& target_message, U32 timestamp);  // From chat server
+		void addMessage(const std::string& from,
+                        const LLUUID& from_id,
+                        const std::string& utf8_text,
+                        const std::string& time,
+                        const bool is_history,
+                        const bool is_region_msg,
+                        U32 timestamp);
+
+        void onVoiceChannelStateChanged(const LLVoiceChannel::EState& old_state, const LLVoiceChannel::EState& new_state, const LLVoiceChannel::EDirection& direction);
+
 		/** @deprecated */
 		static void chatFromLogFile(LLLogChat::ELogLineType type, const LLSD& msg, void* userdata);
 
@@ -112,6 +124,10 @@ class LLIMModel :  public LLSingleton<LLIMModel>
 		uuid_vec_t mInitialTargetIDs;
 		std::string mHistoryFileName;
 
+        // Saved messages from the last minute of history read from the local group chat cache file
+        std::string mLastHistoryCacheDateTime;
+        chat_message_list_t mLastHistoryCacheMsgs;
+
 		// connection to voice channel state change signal
 		boost::signals2::connection mVoiceChannelStateChangeConnection;
 
@@ -121,7 +137,7 @@ class LLIMModel :  public LLSingleton<LLIMModel>
 		// does include all incoming messages
 		S32 mNumUnread;
 
-		std::list<LLSD> mMsgs;
+        chat_message_list_t mMsgs;
 
 		LLVoiceChannel* mVoiceChannel;
 		LLIMSpeakerMgr* mSpeakers;
@@ -208,14 +224,27 @@ class LLIMModel :  public LLSingleton<LLIMModel>
 	 * and also saved into a file if log2file is specified.
 	 * It sends new message signal for each added message.
 	 */
-	void addMessage(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text, bool log2file = true, bool is_region_msg = false);
-    void processAddingMessage(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text, bool log2file = true, bool is_region_msg = false);
+	void addMessage(const LLUUID& session_id,
+                    const std::string& from,
+                    const LLUUID& other_participant_id,
+                    const std::string& utf8_text,
+                    bool log2file = true,
+                    bool is_region_msg = false,
+                    U32 time_stamp = 0);
+
+    void processAddingMessage(const LLUUID& session_id,
+                    const std::string& from,
+                    const LLUUID& from_id,
+                    const std::string& utf8_text,
+                    bool log2file,
+                    bool is_region_msg,
+                    U32 time_stamp);
 
 	/**
 	 * Similar to addMessage(...) above but won't send a signal about a new message added
 	 */
-	LLIMModel::LLIMSession* addMessageSilently(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, 
-		const std::string& utf8_text, bool log2file = true, bool is_region_msg = false);
+	LLIMModel::LLIMSession* addMessageSilently(const LLUUID& session_id, const std::string& from, const LLUUID& from_id,
+		const std::string& utf8_text, bool log2file = true, bool is_region_msg = false, U32 timestamp = 0);
 
 	/**
 	 * Add a system message to an IM Model
@@ -223,15 +252,15 @@ class LLIMModel :  public LLSingleton<LLIMModel>
 	void proccessOnlineOfflineNotification(const LLUUID& session_id, const std::string& utf8_text);
 
 	/**
-	 * Get a session's name. 
-	 * For a P2P chat - it's an avatar's name, 
+	 * Get a session's name.
+	 * For a P2P chat - it's an avatar's name,
 	 * For a group chat - it's a group's name
 	 * For an incoming ad-hoc chat - is received from the server and is in a from of "<Avatar's name> Conference"
 	 *	It is updated in LLIMModel::LLIMSession's constructor to localize the "Conference".
 	 */
 	const std::string getName(const LLUUID& session_id) const;
 
-	/** 
+	/**
 	 * Get number of unread messages in a session with session_id
 	 * Returns -1 if the session with session_id doesn't exist
 	 */
@@ -283,7 +312,7 @@ class LLIMModel :  public LLSingleton<LLIMModel>
 	bool logToFile(const std::string& file_name, const std::string& from, const LLUUID& from_id, const std::string& utf8_text);
 
 private:
-	
+
 	/**
 	 * Populate supplied std::list with messages starting from index specified by start_index without
 	 * emitting no unread messages signal.
@@ -293,7 +322,7 @@ class LLIMModel :  public LLSingleton<LLIMModel>
 	/**
 	 * Add message to a list of message associated with session specified by session_id
 	 */
-	bool addToHistory(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text, bool is_region_msg = false);
+	bool addToHistory(const LLUUID& session_id, const std::string& from, const LLUUID& from_id, const std::string& utf8_text, bool is_region_msg, U32 timestamp);
 
 };
 
@@ -335,7 +364,8 @@ class LLIMMgr : public LLSingleton<LLIMMgr>
 					U32 parent_estate_id = 0,
 					const LLUUID& region_id = LLUUID::null,
 					const LLVector3& position = LLVector3::zero,
-					bool is_region_msg = false);
+                    bool is_region_msg = false,
+                    U32 timestamp = 0);
 
 	void addSystemMessage(const LLUUID& session_id, const std::string& message_name, const LLSD& args);
 
diff --git a/indra/newview/lllogchat.cpp b/indra/newview/lllogchat.cpp
index fb9885b454d..ba82ff0b0f6 100644
--- a/indra/newview/lllogchat.cpp
+++ b/indra/newview/lllogchat.cpp
@@ -62,6 +62,7 @@
 const S32 LOG_RECALL_SIZE = 2048;
 
 const std::string LL_IM_TIME("time");
+const std::string LL_IM_DATE_TIME("datetime");
 const std::string LL_IM_TEXT("message");
 const std::string LL_IM_FROM("from");
 const std::string LL_IM_FROM_ID("from_id");
@@ -133,14 +134,14 @@ void append_to_last_message(std::list<LLSD>& messages, const std::string& line)
 	messages.back()[LL_IM_TEXT] = im_text;
 }
 
-std::string remove_utf8_bom(const char* buf)
+const char* remove_utf8_bom(const char* buf)
 {
-	std::string res(buf);
-	if (res[0] == (char)0xEF && res[1] == (char)0xBB && res[2] == (char)0xBF)
-	{
-		res.erase(0, 3);
+    const char* start = buf;
+	if (start[0] == (char)0xEF && start[1] == (char)0xBB && start[2] == (char)0xBF)
+	{   // If string starts with the magic bytes, return pointer after it.
+        start += 3;
 	}
-	return res;
+	return start;
 }
 
 class LLLogChatTimeScanner: public LLSingleton<LLLogChatTimeScanner>
@@ -315,7 +316,7 @@ std::string LLLogChat::cleanFileName(std::string filename)
 	return filename;
 }
 
-std::string LLLogChat::timestamp(bool withdate)
+std::string LLLogChat::timestamp2LogString(U32 timestamp, bool withdate)
 {
 	std::string timeStr;
 	if (withdate)
@@ -333,7 +334,14 @@ std::string LLLogChat::timestamp(bool withdate)
 	}
 
 	LLSD substitution;
-	substitution["datetime"] = (S32)time_corrected();
+    if (timestamp == 0)
+    {
+        substitution["datetime"] = (S32)time_corrected();
+    }
+    else
+    {   // timestamp is correct utc already
+        substitution["datetime"] = (S32)timestamp;
+    }
 
 	LLStringUtil::format (timeStr, substitution);
 	return timeStr;
@@ -355,7 +363,7 @@ void LLLogChat::saveHistory(const std::string& filename,
 		llassert(tmp_filename.size());
 		return;
 	}
-	
+
 	llofstream file(LLLogChat::makeLogFileName(filename).c_str(), std::ios_base::app);
 	if (!file.is_open())
 	{
@@ -366,7 +374,7 @@ void LLLogChat::saveHistory(const std::string& filename,
 	LLSD item;
 
 	if (gSavedPerAccountSettings.getBOOL("LogTimestamp"))
-		 item["time"] = LLLogChat::timestamp(gSavedPerAccountSettings.getBOOL("LogTimestampDate"));
+		 item["time"] = LLLogChat::timestamp2LogString(0, gSavedPerAccountSettings.getBOOL("LogTimestampDate"));
 
 	item["from_id"]	= from_id;
 	item["message"]	= line;
@@ -374,7 +382,7 @@ void LLLogChat::saveHistory(const std::string& filename,
 	//adding "Second Life:" for all system messages to make chat log history parsing more reliable
 	if (from.empty() && from_id.isNull())
 	{
-		item["from"] = SYSTEM_FROM; 
+		item["from"] = SYSTEM_FROM;
 	}
 	else
 	{
@@ -393,37 +401,60 @@ void LLLogChat::loadChatHistory(const std::string& file_name, std::list<LLSD>& m
 {
 	if (file_name.empty())
 	{
-		LL_WARNS("LLLogChat::loadChatHistory") << "Session name is Empty!" << LL_ENDL;
+		LL_WARNS("LLLogChat::loadChatHistory") << "Local history file name is empty!" << LL_ENDL;
 		return ;
 	}
 
 	bool load_all_history = load_params.has("load_all_history") ? load_params["load_all_history"].asBoolean() : false;
 
-	LLFILE* fptr = LLFile::fopen(LLLogChat::makeLogFileName(file_name), "r");/*Flawfinder: ignore*/
+    // Stat the file to find it and get the last history entry time
+    llstat stat_data;
+
+    std::string log_file_name = LLLogChat::makeLogFileName(file_name);
+    LL_DEBUGS("ChatHistory") << "First attempt to stat chat history file " << log_file_name << LL_ENDL;
+
+    S32 no_stat = LLFile::stat(log_file_name, &stat_data);
+
+    if (no_stat)
+    {
+        if (is_group)
+        {
+            std::string old_name(file_name);
+            old_name.erase(old_name.size() - GROUP_CHAT_SUFFIX.size());     // trim off " (group)"
+            log_file_name = LLLogChat::makeLogFileName(old_name);
+            LL_DEBUGS("ChatHistory") << "Attempting to stat adjusted chat history file " << log_file_name << LL_ENDL;
+            no_stat = LLFile::stat(log_file_name, &stat_data);
+            if (!no_stat)
+            {   // Found it without "(group)", copy to new naming style.  We already have the mod time in stat_data
+                log_file_name = LLLogChat::makeLogFileName(file_name);
+                LL_DEBUGS("ChatHistory") << "Attempt to stat copied history file " << log_file_name << LL_ENDL;
+                LLFile::copy(LLLogChat::makeLogFileName(old_name), log_file_name);
+            }
+        }
+        if (no_stat)
+        {
+            log_file_name = LLLogChat::oldLogFileName(file_name);
+            LL_DEBUGS("ChatHistory") << "Attempt to stat old history file name " << log_file_name << LL_ENDL;
+            no_stat = LLFile::stat(log_file_name, &stat_data);
+            if (no_stat)
+            {
+                LL_DEBUGS("ChatHistory") << "No previous conversation log file found for " << file_name << LL_ENDL;
+                return;						//No previous conversation with this name.
+            }
+        }
+    }
+
+    // If we got here, we managed to stat the file.
+    // Open the file to read
+    LLFILE* fptr = LLFile::fopen(log_file_name, "r");       /*Flawfinder: ignore*/
 	if (!fptr)
-	{
-		if (is_group)
-		{
-			std::string old_name(file_name);
-			old_name.erase(old_name.size() - GROUP_CHAT_SUFFIX.size());
-			fptr = LLFile::fopen(LLLogChat::makeLogFileName(old_name), "r");
-			if (fptr)
-			{
-				fclose(fptr);
-				LLFile::copy(LLLogChat::makeLogFileName(old_name), LLLogChat::makeLogFileName(file_name));
-			}
-			fptr = LLFile::fopen(LLLogChat::makeLogFileName(file_name), "r");
-		}
-		if (!fptr)
-		{
-			fptr = LLFile::fopen(LLLogChat::oldLogFileName(file_name), "r");/*Flawfinder: ignore*/
-			if (!fptr)
-			{
-				return;						//No previous conversation with this name.
-			}
-		}
+	{   // Ok, this is strange but not really tragic in the big picture of things
+        LL_WARNS("ChatHistory") << "Unable to read file " << log_file_name << " after stat was successful" << LL_ENDL;
+        return;
 	}
 
+    S32 save_num_messages = messages.size();
+
 	char buffer[LOG_RECALL_SIZE];		/*Flawfinder: ignore*/
 	char *bptr;
 	S32 len;
@@ -441,6 +472,7 @@ void LLLogChat::loadChatHistory(const std::string& file_name, std::list<LLSD>& m
 	while (fgets(buffer, LOG_RECALL_SIZE, fptr)  && !feof(fptr))
 	{
 		len = strlen(buffer) - 1;		/*Flawfinder: ignore*/
+        // backfill any end of line characters with nulls
 		for (bptr = (buffer + len); (*bptr == '\n' || *bptr == '\r') && bptr>buffer; bptr--)	*bptr='\0';
 
 		if (firstline)
@@ -473,6 +505,10 @@ void LLLogChat::loadChatHistory(const std::string& file_name, std::list<LLSD>& m
 		}
 	}
 	fclose(fptr);
+
+    LL_DEBUGS("ChatHistory") << "Read " << (messages.size() - save_num_messages)
+        << " messages of chat history from " << log_file_name
+        << " file mod time " << (F64)stat_data.st_mtime << LL_ENDL;
 }
 
 bool LLLogChat::historyThreadsFinished(LLUUID session_id)
@@ -837,7 +873,8 @@ bool LLLogChat::isTranscriptFileFound(std::string fullname)
 		{
 			//matching a timestamp
 			boost::match_results<std::string::const_iterator> matches;
-			if (ll_regex_match(remove_utf8_bom(buffer), matches, TIMESTAMP))
+            std::string line(remove_utf8_bom(buffer));
+			if (ll_regex_match(line, matches, TIMESTAMP))
 			{
 				result = true;
 			}
@@ -847,7 +884,7 @@ bool LLLogChat::isTranscriptFileFound(std::string fullname)
 	return result;
 }
 
-//*TODO mark object's names in a special way so that they will be distinguishable form avatar name 
+//*TODO mark object's names in a special way so that they will be distinguishable form avatar name
 //which are more strict by its nature (only firstname and secondname)
 //Example, an object's name can be written like "Object <actual_object's_name>"
 void LLChatLogFormatter::format(const LLSD& im, std::ostream& ostr) const
@@ -865,7 +902,7 @@ void LLChatLogFormatter::format(const LLSD& im, std::ostream& ostr) const
 		ostr << '[' << timestamp << ']' << TWO_SPACES;
 	}
 	
-	//*TODO mark object's names in a special way so that they will be distinguishable form avatar name 
+	//*TODO mark object's names in a special way so that they will be distinguishable from avatar name 
 	//which are more strict by its nature (only firstname and secondname)
 	//Example, an object's name can be written like "Object <actual_object's_name>"
 	if (im[LL_IM_FROM].isDefined())
@@ -928,7 +965,9 @@ bool LLChatLogParser::parse(std::string& raw, LLSD& im, const LLSD& parse_params
 		timestamp.erase(0, 1);
 		timestamp.erase(timestamp.length()-1, 1);
 
-		if (cut_off_todays_date)
+        im[LL_IM_DATE_TIME] = timestamp;    // Retain full date-time for merging chat histories
+
+        if (cut_off_todays_date)
 		{
 			LLLogChatTimeScanner::instance().checkAndCutOffDate(timestamp);
 		}
@@ -936,9 +975,9 @@ bool LLChatLogParser::parse(std::string& raw, LLSD& im, const LLSD& parse_params
 		im[LL_IM_TIME] = timestamp;
 	}
 	else
-	{
-		//timestamp is optional
-		im[LL_IM_TIME] = "";
+	{   //timestamp is optional
+        im[LL_IM_DATE_TIME] = "";
+        im[LL_IM_TIME] = "";
 	}
 
 	bool has_stuff = matches[IDX_STUFF].matched;
diff --git a/indra/newview/lllogchat.h b/indra/newview/lllogchat.h
index c4b61ee716b..5dce8ab1d25 100644
--- a/indra/newview/lllogchat.h
+++ b/indra/newview/lllogchat.h
@@ -92,7 +92,7 @@ class LLLogChat : public LLSingleton<LLLogChat>
 		LOG_END
 	};
 
-	static std::string timestamp(bool withdate = false);
+	static std::string timestamp2LogString(U32 timestamp, bool withdate);
 	static std::string makeLogFileName(std::string(filename));
 	static void renameLogFile(const std::string& old_filename, const std::string& new_filename);
 	/**
@@ -201,6 +201,7 @@ extern const std::string GROUP_CHAT_SUFFIX;
 
 // LLSD map lookup constants
 extern const std::string LL_IM_TIME; //("time");
+extern const std::string LL_IM_DATE_TIME; //("datetime");
 extern const std::string LL_IM_TEXT; //("message");
 extern const std::string LL_IM_FROM; //("from");
 extern const std::string LL_IM_FROM_ID; //("from_id");
diff --git a/indra/newview/llnotificationhandlerutil.cpp b/indra/newview/llnotificationhandlerutil.cpp
index 39a0b9b50ed..85adfaab559 100644
--- a/indra/newview/llnotificationhandlerutil.cpp
+++ b/indra/newview/llnotificationhandlerutil.cpp
@@ -275,7 +275,7 @@ void LLHandlerUtil::addNotifPanelToIM(const LLNotificationPtr& notification)
 	LLSD offer;
 	offer["notification_id"] = notification->getID();
 	offer["from"] = SYSTEM_FROM;
-	offer["time"] = LLLogChat::timestamp(false);
+	offer["time"] = LLLogChat::timestamp2LogString(0, false);   // Use current time
 	offer["index"] = (LLSD::Integer)session->mMsgs.size();
 	session->mMsgs.push_front(offer);
 
-- 
GitLab