diff options
| author | pepper <3782201+rohvani@users.noreply.github.com> | 2026-08-13 20:42:21 -0400 |
|---|---|---|
| committer | pepper <3782201+rohvani@users.noreply.github.com> | 2026-08-13 20:42:21 -0400 |
| commit | ae176d7e2aa309435d2f7cb6363b51132c205a50 (patch) | |
| tree | 0d37a2d7f3f22f97bd4584e8d7014a7e137c703a /indra/newview | |
| parent | 908ce3d98677af795b0f0fd61bd7b26f2e79bfaf (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.cpp | 189 | ||||
| -rw-r--r-- | indra/newview/llchatservicehistory.h | 4 | ||||
| -rw-r--r-- | indra/newview/llimview.cpp | 33 |
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; } |
