aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/notmuchworker.cpp120
-rw-r--r--src/threadlistmodel.cpp28
-rw-r--r--src/threadlistmodel.h5
3 files changed, 122 insertions, 31 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp
index 3a0b2c1..c8a2485 100644
--- a/src/notmuchworker.cpp
+++ b/src/notmuchworker.cpp
@@ -27,6 +27,8 @@
#include <notmuch.h>
+#include <algorithm>
+
#include <QDateTime>
#include <QDir>
#include <QDirIterator>
@@ -468,6 +470,11 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation,
batch.reserve(kBatchSize);
int total = 0;
+ // A flat view's rows, held back until every thread has been walked so they
+ // can be ordered against each other rather than thread by thread. Empty
+ // for a threaded query, which emits in notmuch's own order as it goes.
+ QVector<ThreadSummary> flatRows;
+
for (; notmuch_threads_valid(threads.get());
notmuch_threads_move_to_next(threads.get())) {
@@ -500,17 +507,40 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation,
// Two different questions, and the Sent view asks the second one. A
// normal row stands for the thread's OPENING message. A Sent row
// stands for what the USER sent, usually a reply and often not the
- // opening message at all, so it takes the first message the query
- // MATCHED. withRecipients is exactly the Sent query, which is why it
- // selects between them rather than carrying a second flag that could
- // disagree with it.
+ // opening message at all.
+ //
+ // **One row per MATCHED MESSAGE, not per thread** (item 191). The Sent
+ // and Drafts views are flat (`Config::generatorIsFlat`), so they are
+ // lists of messages and a conversation the user replied to twice owes
+ // them two rows. This branch used to stop at the first match, which
+ // made the second reply reachable NOWHERE in the view: measured on the
+ // developer's real mail, a message sent at 12:42 was missing while the
+ // row above it, dated 12:42, opened a message from three weeks
+ // earlier. The date came from the thread and the body from the one
+ // chosen message, and the two cannot agree once a thread matches
+ // twice.
+ //
+ // `date` and `subject` are therefore overridden per message here.
+ // Everything above this point is thread-wide and correct for a
+ // threaded view; for a flat row the MESSAGE is the row.
//
// Note notmuch_thread_get_matched_messages returns a COUNT, not an
// iterator; there is no matched-messages list. The match state is a
- // per-message flag, so the Sent branch walks in oldest-first order and
- // stops at the first match. Measured at 0.146s against a 0.143s
- // baseline over 4,515 threads: the walk stops early and reads the
- // index, so it is as free as the toplevel call.
+ // per-message flag, so this walks the thread in oldest-first order and
+ // takes every message carrying it. The walk is the same one that was
+ // measured at 0.146s against a 0.143s baseline over 4,515 threads; it
+ // no longer stops early, but it still reads only the index.
+ //
+ // **The rows are collected and sorted as ONE list, because the sort
+ // notmuch applied is a THREAD sort.** `notmuch_query_set_sort` orders
+ // the threads this loop visits; it says nothing about the messages
+ // inside one, and it gives every row of a thread that thread's single
+ // position. Emitting them in the thread's own oldest-first walk put a
+ // reply from three weeks ago ABOVE the one sent today, both sitting
+ // where their shared thread sorted. Sorting each thread's rows
+ // in place does not fix it either: a message from ANOTHER thread dated
+ // between them still cannot land between them. A flat view is a list
+ // of messages, so it has to be ordered as one.
if (withRecipients) {
notmuch_messages_t *all = notmuch_thread_get_messages(thread.get());
for (; all && notmuch_messages_valid(all);
@@ -522,24 +552,43 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation,
notmuch_message_get_flag_st(message,
NOTMUCH_MESSAGE_FLAG_MATCH,
&matched);
- if (matched) {
- summary.firstMessageId = QString::fromUtf8(
- notmuch_message_get_message_id(message));
- // The card's own tags, beside the thread's union above.
- // Same walk, same index read, no extra query.
- summary.firstMessageTags = tagsOf(message);
- // The card's sender, for the avatar hash (item 169). Same
- // walk, and From is in the index like the tags.
- summary.firstMessageSender = senderAddressOf(message);
- // Which account this belongs to, for Delete's destination.
- summary.firstMessagePath = QDir(dbRoot).relativeFilePath(
- QString::fromUtf8(
- notmuch_message_get_filename(message)));
- break;
- }
+ if (!matched)
+ continue;
+
+ // A copy per matched message, so each row carries the
+ // thread-wide fields set above and its own identity below.
+ ThreadSummary row = summary;
+ row.firstMessageId = QString::fromUtf8(
+ notmuch_message_get_message_id(message));
+ // The card's own tags, beside the thread's union above.
+ // Same walk, same index read, no extra query.
+ row.firstMessageTags = tagsOf(message);
+ // The card's sender, for the avatar hash (item 169). Same
+ // walk, and From is in the index like the tags.
+ row.firstMessageSender = senderAddressOf(message);
+ // Which account this belongs to, for Delete's destination.
+ row.firstMessagePath = QDir(dbRoot).relativeFilePath(
+ QString::fromUtf8(
+ notmuch_message_get_filename(message)));
+ // The row IS this message, so it is dated and titled by it.
+ // Reading the thread's newest date here is what put a sent
+ // message under a stranger's date.
+ row.date = QDateTime::fromSecsSinceEpoch(
+ notmuch_message_get_date(message));
+ const char *subject =
+ notmuch_message_get_header(message, "subject");
+ if (subject && *subject)
+ row.subject = QString::fromUtf8(subject);
+
+ flatRows.append(row);
}
- } else if (notmuch_messages_t *top =
- notmuch_thread_get_toplevel_messages(thread.get())) {
+
+ // Sorted and emitted once the whole query has been walked, below.
+ continue;
+ }
+
+ if (notmuch_messages_t *top =
+ notmuch_thread_get_toplevel_messages(thread.get())) {
if (notmuch_messages_valid(top)) {
if (notmuch_message_t *first = notmuch_messages_get(top)) {
summary.firstMessageId = QString::fromUtf8(
@@ -568,6 +617,27 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation,
}
}
+ // The flat views (Sent, Drafts) are lists of MESSAGES, so they are ordered
+ // by each row's own date across the whole result. std::stable_sort so rows
+ // sharing a timestamp keep the order the walk found them in rather than
+ // swapping between identical queries.
+ if (!flatRows.isEmpty()) {
+ std::stable_sort(flatRows.begin(), flatRows.end(),
+ [sort](const ThreadSummary &a, const ThreadSummary &b) {
+ return sort == OldestFirst ? a.date < b.date : a.date > b.date;
+ });
+
+ for (const ThreadSummary &row : flatRows) {
+ batch.append(row);
+ ++total;
+ if (batch.size() >= kBatchSize) {
+ emit threadsReady(batch, generation);
+ batch.clear();
+ batch.reserve(kBatchSize);
+ }
+ }
+ }
+
if (!batch.isEmpty())
emit threadsReady(batch, generation);
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index 5c44dd4..ad1359b 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -820,6 +820,22 @@ void ThreadListModel::appendBatch(const QVector<ThreadSummary> &batch)
endInsertRows();
}
+QString ThreadListModel::rowKeyFor(const ThreadSummary &summary) const
+{
+ // What makes a ROW unique, which is not always its thread. A flat view
+ // (Sent, Drafts) emits one row per matched MESSAGE since item 191, so two
+ // rows can share a threadId and keying on that alone silently drops one of
+ // them on every reconcile: the QHash below would map both to one row and
+ // the second would look like a thread that had vanished.
+ //
+ // Falls back to the thread id when a flat row somehow carries no message
+ // id, which keeps the old behaviour rather than collapsing every such row
+ // onto one empty key.
+ if (m_flatMode && !summary.firstMessageId.isEmpty())
+ return summary.firstMessageId;
+ return summary.threadId;
+}
+
void ThreadListModel::reconcile(const QVector<ThreadSummary> &threads)
{
// Removals first, walking BACKWARDS. Each beginRemoveRows renumbers
@@ -832,14 +848,14 @@ void ThreadListModel::reconcile(const QVector<ThreadSummary> &threads)
QSet<QString> wanted;
wanted.reserve(threads.size());
for (const ThreadSummary &summary : threads)
- wanted.insert(summary.threadId);
+ wanted.insert(rowKeyFor(summary));
for (int row = m_threads.size() - 1; row >= 0; --row) {
- if (wanted.contains(m_threads.at(row).summary.threadId))
+ if (wanted.contains(rowKeyFor(m_threads.at(row).summary)))
continue;
int first = row;
while (first > 0
- && !wanted.contains(m_threads.at(first - 1).summary.threadId))
+ && !wanted.contains(rowKeyFor(m_threads.at(first - 1).summary)))
--first;
beginRemoveRows({}, first, row);
m_threads.remove(first, row - first + 1);
@@ -852,7 +868,7 @@ void ThreadListModel::reconcile(const QVector<ThreadSummary> &threads)
QHash<QString, int> present;
present.reserve(m_threads.size());
for (int row = 0; row < m_threads.size(); ++row)
- present.insert(m_threads.at(row).summary.threadId, row);
+ present.insert(rowKeyFor(m_threads.at(row).summary), row);
// Insertions, forwards, at the position the RESULT gives them. Walking the
// result in order means each new thread is placed against rows already
@@ -860,7 +876,7 @@ void ThreadListModel::reconcile(const QVector<ThreadSummary> &threads)
// know what that order means.
for (int target = 0; target < threads.size(); ++target) {
const ThreadSummary &summary = threads.at(target);
- const auto it = present.constFind(summary.threadId);
+ const auto it = present.constFind(rowKeyFor(summary));
if (it == present.constEnd()) {
const int at = qMin(target, m_threads.size());
@@ -903,7 +919,7 @@ void ThreadListModel::reconcile(const QVector<ThreadSummary> &threads)
if (entry.value() >= target && entry.value() < row)
++entry.value();
}
- present[summary.threadId] = target;
+ present[rowKeyFor(summary)] = target;
row = target;
}
diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h
index fee4a1f..bed099d 100644
--- a/src/threadlistmodel.h
+++ b/src/threadlistmodel.h
@@ -430,6 +430,11 @@ private:
bool loaded = false;
};
+ /// What makes a ROW unique: the message id in a flat view, the thread id
+ /// otherwise. Two flat rows can share a thread since item 191, so
+ /// reconcile() cannot key on threadId alone without dropping one of them.
+ QString rowKeyFor(const ThreadSummary &summary) const;
+
/// A newly arrived thread, with its card's own message seeded from the
/// query.
///