From 4a4f82f7709ab6ee5a84ac0f3b191470b6424c36 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 14 Aug 2026 19:16:16 +0200 Subject: feat(pane): always render one message, never the conversation 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 --- src/notmuchworker.cpp | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) (limited to 'src/notmuchworker.cpp') 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; -- cgit v1.2.3