mu-query-threads: small refinements

Ensure we sort correctly (sort_container), since the last message in a
thread is not necessarily the last (when not sorted by thread-date).

own_date_key is redundant, so let's remove it.
This commit is contained in:
Dirk-Jan C. Binnema
2026-08-15 22:39:24 +03:00
committed by Seth Ladygo
parent f31b5eedde
commit 541a4ad00d
2 changed files with 53 additions and 21 deletions

View File

@ -62,7 +62,7 @@ struct Container {
return up && up == other->ur_parent();
}
template <typename Func> void for_each_child(Func&& func) {
template <typename Func> void for_each_child(const Func& func) {
auto it{children.rbegin()};
while (it != children.rend()) {
auto next = std::next(it);
@ -80,13 +80,6 @@ struct Container {
std::string thread_date_key;
// The container's own date-key, set once from its query-match and never
// overwritten by the bubble-up above. Siblings must sort by this, not by
// thread_date_key -- otherwise a subthread gaining a new, later message
// (e.g. a reply to one message in a patch series) reorders it among its
// siblings, even though its own date didn't change.
std::string own_date_key;
Option<QueryMatch&> query_match;
bool is_nuked{};
Container* parent{};
@ -195,12 +188,14 @@ determine_id_table(QueryResultsType& qres)
}();
// We sort by date (ascending), *except* for the root; we don't
// know what query_matchs will be at the root level yet, so remember
// both. Moreover, even when sorting the top-level in descending
// know what query_matchs will be at the root level yet, so
// remember both.
//
// Moreover, even when sorting the top-level in descending
// order, still sort the thread levels below that in ascending
// order, keeping sibling order stable.
container.own_date_key = container.thread_date_key =
container.query_match->date_key = mi.date_str().value_or("");
container.thread_date_key = container.query_match->date_key =
mi.date_str().value_or("");
// initial guess for the thread-date; might be updated
// later.
@ -494,17 +489,24 @@ sort_container(Container& container)
for (auto& child : container.children)
sort_container(*child);
// now sort this level, by each child's own date, falling back to
// thread_date_key if the container has no message of its own; use a
// stable sort so messages with equal dates keep their original (mset)
// order.
std::ranges::stable_sort(container.children, {}, [](const Container* c) {
return c->own_date_key.empty() ? c->thread_date_key : c->own_date_key;
// now sort this level, by each child's _own_ date, not the (bubbled-up)
// thread-date -- otherwise a subthread gaining a new message could
// reorder wrongly.
//
// Fall back to the thread-date for containers without a message of
// their own; use a stable sort so messages with equal dates keep their
// original (mset) order.
std::ranges::stable_sort(container.children, {},
[](const Container* c) -> const std::string& {
return c->query_match ? c->query_match->date_key : c->thread_date_key;
});
// and 'bubble up' the date of the *newest* message with a date. We
// reasonably assume that it's later than its parent.
const auto& newest_date = container.children.back()->thread_date_key;
// reasonably assume that it's later than its parent. Note that since
// children are not sorted by thread-date, the newest is not necessarily
// the last child.
const auto& newest_date = std::ranges::max(
container.children, {}, &Container::thread_date_key)->thread_date_key;
if (!newest_date.empty())
container.thread_date_key = newest_date;
}
@ -735,6 +737,32 @@ test_sibling_order_stable()
});
}
static void
test_thread_date_bubble_up()
{
// thread A's newest message (date 9) hangs under a middle sibling;
// thread B (date 5) is older than that, so ascending order must put A
// *after* B.
auto results = MockQueryResults{
MockQueryResult{"a", "1", {}},
MockQueryResult{"a1", "2", {"a"}},
MockQueryResult{"a2", "3", {"a"}},
MockQueryResult{"reply-a1", "9", {"a", "a1"}},
MockQueryResult{"b", "5", {}},
};
calculate_threads(results, false);
assert_thread_paths(results,
{
{"b", "0"},
{"a", "1"},
{"a1", "1:0"},
{"reply-a1", "1:0:0"},
{"a2", "1:1"},
});
}
static void
test_id_table_inconsistent()
{
@ -966,6 +994,7 @@ try {
g_test_add_func("/threader/sort/ascending", test_sort_ascending);
g_test_add_func("/threader/sort/decending", test_sort_descending);
g_test_add_func("/threader/sort/sibling-order-stable", test_sibling_order_stable);
g_test_add_func("/threader/sort/thread-date-bubble-up", test_thread_date_bubble_up);
g_test_add_func("/threader/id-table-inconsistent", test_id_table_inconsistent);
g_test_add_func("/threader/dups/dup-last", test_dups_dup_last);

View File

@ -1,5 +1,5 @@
/*
** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** Copyright (C) 2021-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
@ -28,6 +28,9 @@ namespace Mu {
* thread-paths for each message, so we can let Xapian order them in the correct
* order.
*
* This is strongly inspired by the JWZ message threading algorithm
* https://www.jwz.org/doc/threading.html
*
* Note - threads are sorted chronologically, and the messages below the top
* level are always sorted in ascending orde
*