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/threadlistmodel.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'src/threadlistmodel.cpp') diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp index 20d6915..3e079ed 100644 --- a/src/threadlistmodel.cpp +++ b/src/threadlistmodel.cpp @@ -391,11 +391,22 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const return false; if (role == MessageIdRole) { - // The thread's FIRST message, once known, because the root card is - // that message: selecting it renders one message rather than the whole - // conversation. Empty before the replies are loaded, which is the - // caller's signal to load the thread instead of guessing at a message. - return m_threads.at(index.row()).first.messageId; + // The thread's FIRST message, because the root card IS that message: + // selecting it renders one message, never the whole conversation. + // + // The summary carries this from the query, so it is known before the + // thread has ever been expanded. It used to come only from `first`, + // populated when the replies loaded, which left this empty on a fresh + // row and sent the caller down a whole-thread render instead. The same + // click then behaved differently once the thread had been opened, + // which is what the user reported as item 66. + // + // `first` is still preferred when present: after an expansion it is + // the same message, read from the tree that is now authoritative for + // this thread's shape. + const ThreadNode &node = m_threads.at(index.row()); + return node.first.messageId.isEmpty() ? node.summary.firstMessageId + : node.first.messageId; } if (role == MessageDepthRole) -- cgit v1.2.3