summaryrefslogtreecommitdiff
path: root/indra/newview
diff options
context:
space:
mode:
authorpepper <3782201+rohvani@users.noreply.github.com>2026-08-13 20:42:21 -0400
committerpepper <3782201+rohvani@users.noreply.github.com>2026-08-13 20:42:21 -0400
commitae176d7e2aa309435d2f7cb6363b51132c205a50 (patch)
tree0d37a2d7f3f22f97bd4584e8d7014a7e137c703a /indra/newview
parent908ce3d98677af795b0f0fd61bd7b26f2e79bfaf (diff)
Reconcile stitched history with live messages
Replace exact legacy transcript occurrences with canonical ChatService rows while preserving local ordering and consuming repeated messages one-for-one. Carry service timestamps into normalized IM history and keep system sender identity null. Filter authoritative P2P history refreshes against same-minute live rows so open sessions do not display duplicate messages after asynchronous reloads.
Diffstat (limited to 'indra/newview')
-rw-r--r--indra/newview/llchatservicehistory.cpp189
-rw-r--r--indra/newview/llchatservicehistory.h4
-rw-r--r--indra/newview/llimview.cpp33
3 files changed, 199 insertions, 27 deletions
diff --git a/indra/newview/llchatservicehistory.cpp b/indra/newview/llchatservicehistory.cpp
index c4da82b6d1..8fd37595cf 100644
--- a/indra/newview/llchatservicehistory.cpp
+++ b/indra/newview/llchatservicehistory.cpp
@@ -73,6 +73,7 @@ const char* const ENABLED_SETTING = "ChatServiceEnabled";
const char* const INDEX_NAME = "chat_service_index.csv";
const char* const STATE_NAME = "chat_service_state.xml";
const char* const PENDING_NAME = "(name pending)";
+const char* const LEGACY_WALL_TIME = "chat_service_legacy_wall_time";
const F64 REQUEST_SPACING = 2.1;
const F64 RETRY_DELAY = 5.0;
const F64 RATE_LIMIT_DELAY = 120.0;
@@ -2559,6 +2560,74 @@ bool legacyWallEpoch(const std::string& text, F64& epoch)
}
}
+bool sameSenderAndText(const LLSD& left, const LLSD& right)
+{
+ if (left[LL_IM_TEXT].asString() != right[LL_IM_TEXT].asString())
+ {
+ return false;
+ }
+
+ // Prefer exact resident identity when both sources carry it; otherwise require
+ // the transcript names to agree exactly.
+ const bool left_has_id = left[LL_IM_FROM_ID].isDefined();
+ const bool right_has_id = right[LL_IM_FROM_ID].isDefined();
+ return left_has_id && right_has_id
+ ? left[LL_IM_FROM_ID].asUUID() == right[LL_IM_FROM_ID].asUUID()
+ : left[LL_IM_FROM].asString() == right[LL_IM_FROM].asString();
+}
+
+bool sameSenderAndText(const LLSD& legacy, const Row& service)
+{
+ if (legacy[LL_IM_TEXT].asString() != service.message)
+ {
+ return false;
+ }
+
+ return legacy[LL_IM_FROM_ID].isDefined()
+ ? legacy[LL_IM_FROM_ID].asUUID() == service.from_id
+ : legacy[LL_IM_FROM].asString() == service.from_name;
+}
+
+bool sameLegacyMinute(F64 wall_epoch, F64 utc_epoch)
+{
+ // Legacy transcript minutes are SLT but do not encode whether UTC-7 or UTC-8
+ // applied. Either exact minute may identify the authoritative service row.
+ for (const F64 offset : { 7.0 * 3600.0, 8.0 * 3600.0 })
+ {
+ const F64 minute = wall_epoch + offset;
+ if (utc_epoch >= minute && utc_epoch < minute + 60.0)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+S64 utcMinute(F64 epoch)
+{
+ return static_cast<S64>(epoch) / 60;
+}
+
+bool sameHistoryLiveOccurrence(const LLSD& history, const LLSD& live)
+{
+ if (!sameSenderAndText(history, live) ||
+ !live["timestamp"].isInteger() || live["timestamp"].asInteger() <= 0)
+ {
+ return false;
+ }
+
+ const F64 live_epoch = static_cast<U32>(live["timestamp"].asInteger());
+ if (history["chat_service_msg_id"].isString() &&
+ history["timestamp"].isInteger() && history["timestamp"].asInteger() > 0)
+ {
+ const U32 history_epoch = static_cast<U32>(history["timestamp"].asInteger());
+ return history_epoch / 60 == static_cast<U32>(live_epoch) / 60;
+ }
+
+ return history[LEGACY_WALL_TIME].isReal() &&
+ sameLegacyMinute(history[LEGACY_WALL_TIME].asReal(), live_epoch);
+}
+
LLSD serviceMessage(const Row& row)
{
LLSD message;
@@ -2571,6 +2640,7 @@ LLSD serviceMessage(const Row& row)
message[LL_IM_FROM] = row.from_name;
message[LL_IM_FROM_ID] = row.from_id;
message[LL_IM_TEXT] = row.message;
+ message["timestamp"] = static_cast<S32>(timestamp);
message["is_history"] = true;
message["chat_service_msg_id"] = row.msg_id;
return message;
@@ -2611,6 +2681,36 @@ LLChatServiceHistory::HistoryResult readStitched(
LLChatServiceHistoryAccess::loadLegacy(path, legacy, parameters);
}
+ // Canonical rows win exact identity over preview rows, then all service rows
+ // share one stable TimeUUID order for seam reconciliation and final display.
+ std::set<std::string> canonical_ids;
+ std::vector<Row> service = archive.display_rows;
+ for (const Row& row : service)
+ {
+ canonical_ids.insert(row.msg_id);
+ }
+ for (const Row& row : preview)
+ {
+ if (!canonical_ids.count(row.msg_id))
+ {
+ service.push_back(row);
+ }
+ }
+ std::sort(service.begin(), service.end(), [](const Row& left, const Row& right)
+ {
+ return left.key < right.key;
+ });
+ std::vector<bool> service_placed(service.size(), false);
+ std::multimap<S64, size_t> canonical_minutes;
+ for (size_t pos = 0; pos < service.size(); ++pos)
+ {
+ if (canonical_ids.count(service[pos].msg_id))
+ {
+ canonical_minutes.emplace(
+ utcMinute(LLDate(service[pos].created_at).secondsSinceEpoch()), pos);
+ }
+ }
+
const F64 service_epoch = archive.has_oldest
? static_cast<F64>(archive.oldest.ticks - UUID_EPOCH) / 10000000.0 : 0.0;
std::vector<std::pair<F64, LLSD>> dated;
@@ -2628,7 +2728,36 @@ LLChatServiceHistory::HistoryResult readStitched(
}
else if (!archive.has_oldest || wall + 7.0 * 3600.0 < service_epoch)
{
- dated.emplace_back(wall, message);
+ LLSD stitched = message;
+ stitched[LEGACY_WALL_TIME] = wall;
+
+ // Replace an exact legacy occurrence in place with its canonical row.
+ // This keeps local ordering around same-minute system messages while
+ // consuming only one occurrence from each source.
+ size_t match = service.size();
+ for (const F64 offset : { 7.0 * 3600.0, 8.0 * 3600.0 })
+ {
+ const auto range = canonical_minutes.equal_range(utcMinute(wall + offset));
+ for (auto candidate = range.first; candidate != range.second; ++candidate)
+ {
+ const size_t pos = candidate->second;
+ if (!service_placed[pos] && sameSenderAndText(message, service[pos]))
+ {
+ match = pos;
+ break;
+ }
+ }
+ if (match != service.size())
+ {
+ break;
+ }
+ }
+ if (match != service.size())
+ {
+ stitched = serviceMessage(service[match]);
+ service_placed[match] = true;
+ }
+ dated.emplace_back(wall, stitched);
}
}
std::stable_sort(dated.begin(), dated.end(),
@@ -2647,32 +2776,15 @@ LLChatServiceHistory::HistoryResult readStitched(
result.messages.push_back(message);
}
- // Merge the transient first page by exact service identity. It may improve
- // presentation but never changes the durable seam or archive summary.
- std::set<std::string> canonical_ids;
- std::vector<Row> service = archive.display_rows;
- for (const Row& row : service)
- {
- canonical_ids.insert(row.msg_id);
- }
- for (const Row& row : preview)
+ // Append service rows that did not replace their exact legacy occurrence.
+ for (size_t pos = 0; pos < service.size(); ++pos)
{
- if (!canonical_ids.count(row.msg_id))
+ if (!service_placed[pos])
{
- service.push_back(row);
+ result.messages.push_back(serviceMessage(service[pos]));
}
}
- std::sort(service.begin(), service.end(), [](const Row& left, const Row& right)
- {
- return left.key < right.key;
- });
-
- for (const Row& row : service)
- {
- result.messages.push_back(serviceMessage(row));
- }
-
while (limit && result.messages.size() > limit)
{
result.messages.pop_front();
@@ -2941,6 +3053,39 @@ boost::signals2::connection LLChatServiceHistory::setSnapshotChanged(
return sSnapshotSignal.connect(callback);
}
+std::list<LLSD> LLChatServiceHistory::filterLiveDuplicates(
+ const std::list<LLSD>& history, const std::list<LLSD>& live)
+{
+ std::vector<const LLSD*> live_rows;
+ for (const LLSD& message : live)
+ {
+ live_rows.push_back(&message);
+ }
+ std::vector<bool> consumed(live_rows.size(), false);
+
+ // Matching is occurrence-aware: repeated identical messages consume repeated
+ // rows one-for-one instead of collapsing to one value.
+ std::list<LLSD> filtered;
+ for (const LLSD& message : history)
+ {
+ bool duplicate = false;
+ for (size_t pos = 0; pos < live_rows.size(); ++pos)
+ {
+ if (!consumed[pos] && sameHistoryLiveOccurrence(message, *live_rows[pos]))
+ {
+ consumed[pos] = true;
+ duplicate = true;
+ break;
+ }
+ }
+ if (!duplicate)
+ {
+ filtered.push_back(message);
+ }
+ }
+ return filtered;
+}
+
std::list<LLSD> LLChatServiceHistory::mergeHeadPreview(
const std::list<LLSD>& loaded, const Snapshot& snapshot, U32 limit)
{
diff --git a/indra/newview/llchatservicehistory.h b/indra/newview/llchatservicehistory.h
index afbe220f1a..5e7028ad53 100644
--- a/indra/newview/llchatservicehistory.h
+++ b/indra/newview/llchatservicehistory.h
@@ -87,6 +87,10 @@ namespace LLChatServiceHistory
std::list<LLSD> mergeHeadPreview(const std::list<LLSD>& loaded,
const Snapshot& snapshot, U32 limit);
+ // Remove one historical occurrence for each exact same-minute live occurrence.
+ std::list<LLSD> filterLiveDuplicates(const std::list<LLSD>& history,
+ const std::list<LLSD>& live);
+
// The callback runs on the main queue after legacy and service storage are read.
bool loadStitchedHistory(const LLUUID& resident_id, const std::string& legacy_stem,
U32 limit, const history_callback_t& callback);
diff --git a/indra/newview/llimview.cpp b/indra/newview/llimview.cpp
index c6425aa529..21789b6735 100644
--- a/indra/newview/llimview.cpp
+++ b/indra/newview/llimview.cpp
@@ -1468,10 +1468,15 @@ void LLIMModel::LLIMSession::replaceHistoricalMessages(const chat_message_list_t
}
}
+ // An authoritative refresh may contain rows that this open session already
+ // displays live. Consume those exact occurrences before rebuilding the overlay.
+ const chat_message_list_t filtered = isP2P()
+ ? LLChatServiceHistory::filterLiveDuplicates(history, live) : history;
+
// Normalize stitched LLLogChat fields into the IM model shape and reverse the
// ascending input into the model's newest-first historical order.
chat_message_list_t historical;
- for (const LLSD& source : history)
+ for (const LLSD& source : filtered)
{
const std::string from = source[LL_IM_FROM].asString();
LLUUID from_id;
@@ -1493,7 +1498,7 @@ void LLIMModel::LLIMSession::replaceHistoricalMessages(const chat_message_list_t
message["from_id"] = from_id;
message["message"] = source[LL_IM_TEXT];
message["time"] = source[LL_IM_TIME];
- message["timestamp"] = 0;
+ message["timestamp"] = source["timestamp"].asInteger();
message["is_history"] = true;
message["is_region_msg"] = false;
historical.push_front(message);
@@ -1953,8 +1958,19 @@ bool LLIMModel::addToHistory(const LLUUID& session_id,
return false;
}
- // 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);
+ // P2P live rows keep their actual arrival/send time so a later asynchronous
+ // history refresh can reconcile the same logged occurrence exactly by minute.
+ U32 model_timestamp = timestamp;
+ if (!model_timestamp && session->isP2PSessionType())
+ {
+ model_timestamp = static_cast<U32>(time_corrected());
+ }
+
+ // This is where a normal arriving message is added to the session. The time
+ // string created here omits the full date.
+ session->addMessage(from, from_id, utf8_text,
+ LLLogChat::timestamp2LogString(model_timestamp, false),
+ false, is_region_msg, model_timestamp);
return true;
}
@@ -3440,7 +3456,14 @@ void LLIMMgr::addMessage(
{
LLUUID other_participant_id = target_id;
std::string message_display_name = (display_name.empty()) ? from : std::string(display_name);
- if (display_id.isNull() && (display_name.empty()))
+
+ // System notices use the participant UUID for session routing only; their model
+ // identity stays null so they render and reconcile as system rows.
+ if (message_display_name == SYSTEM_FROM)
+ {
+ display_id = LLUUID::null;
+ }
+ else if (display_id.isNull() && display_name.empty())
{
display_id = other_participant_id;
}