summaryrefslogtreecommitdiffstats
path: root/src/notmuchworker.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-14 19:16:16 +0200
committerDanilo M. <danix@danix.xyz>2026-08-14 19:16:16 +0200
commit4a4f82f7709ab6ee5a84ac0f3b191470b6424c36 (patch)
tree4c839102a1726d36060935ddd4b04b109a27305c /src/notmuchworker.cpp
parentf897153a1196f23fe0d82dc703d98df1363bf3fc (diff)
downloadqtmaildir-4a4f82f7709ab6ee5a84ac0f3b191470b6424c36.tar.gz
qtmaildir-4a4f82f7709ab6ee5a84ac0f3b191470b6424c36.zip
feat(pane): always render one message, never the conversationthread-view-removed
Selecting a thread root used to render the whole conversation, stubs plus the last messages expanded, but only until the thread had been expanded once. After that the identical click rendered a single message. The user reported the inconsistency and asked for the single-message behaviour throughout, and for the conversation view to go. The cause was a timing one, not a race. The root card stands for the thread's first message and onThreadSelected already preferred to load just that, but the model learned the id only when the replies arrived, so a fresh row fell through to a whole-thread render. ThreadSummary now carries firstMessageId from the query itself, so the id is known before any expansion and the fallback is unreachable. It is free: notmuch_thread_get_toplevel_messages reads the index, not the message files, and a walk with it is indistinguishable from one without over a 36,615-thread database. Contrast recipients, which reads every file and stays Sent-only. The Sent view keeps showing what the user sent rather than the thread's opening message, which is often someone else's. There is no matched-messages iterator in libnotmuch, only a count, so that branch walks oldest-first to the first NOTMUCH_MESSAGE_FLAG_MATCH and stops: 0.146s against a 0.143s baseline over 4,515 threads. onThreadLoaded merges into renderMessages, since onMessageLoaded was already delegating to it for the actual painting. It still takes a list because MessageView renders a list; collapsing that is a separate change to a class with its own tests. NotmuchWorker::loadThread is kept and documented as having no UI caller. It is a tested way to read a thread's messages with the match set resolved, used as a helper by the worker's own tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/notmuchworker.cpp')
-rw-r--r--src/notmuchworker.cpp50
1 files changed, 50 insertions, 0 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp
index 6aae397..a3a2fd5 100644
--- a/src/notmuchworker.cpp
+++ b/src/notmuchworker.cpp
@@ -277,6 +277,56 @@ void NotmuchWorker::runQuery(const QString &query, quint64 generation,
if (withRecipients)
summary.recipients = recipientsOf(thread.get());
+ // The message the row's card stands for. Raw pointers on purpose:
+ // messages reached through a thread are owned by the THREAD and freed
+ // with it (notmuch.h:1637), so an NmMessage wrapper here would destroy
+ // memory the thread frees again. Everything must be read while
+ // `thread` is alive, which it is for the rest of this iteration.
+ //
+ // Index-only, so it costs nothing measurable: see
+ // ThreadSummary::firstMessageId.
+ //
+ // 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.
+ //
+ // 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.
+ if (withRecipients) {
+ notmuch_messages_t *all = notmuch_thread_get_messages(thread.get());
+ for (; all && notmuch_messages_valid(all);
+ notmuch_messages_move_to_next(all)) {
+ notmuch_message_t *message = notmuch_messages_get(all);
+ if (!message)
+ continue;
+ notmuch_bool_t matched = FALSE;
+ notmuch_message_get_flag_st(message,
+ NOTMUCH_MESSAGE_FLAG_MATCH,
+ &matched);
+ if (matched) {
+ summary.firstMessageId = QString::fromUtf8(
+ notmuch_message_get_message_id(message));
+ break;
+ }
+ }
+ } else 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(
+ notmuch_message_get_message_id(first));
+ }
+ }
+ }
+
batch.append(summary);
++total;