diff options
| -rw-r--r-- | AGENTS.md | 10 | ||||
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 12 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 55 |
4 files changed, 71 insertions, 8 deletions
@@ -625,9 +625,13 @@ symptom named only `markCurrentThreadRead` (which was in fact protected by an unrelated guard and could not fire). One bad accessor produced four defects with one symptom between them. -**A thread's first message is NOT among its children, and two lookups forgot -it.** `setThreadMessages` drops depth 0 because the root row stands for that -message, so `children` never holds it. `applyMessageTagChange` and +**A thread of one's message is NOT among its children, and two lookups forgot +it.** Since item 177 `setThreadMessages` keeps a CONVERSATION's first message +as child 0, so it has a row of its own, and a lookup or selection that names it +must land there rather than on the conversation row (236ac86 and the dashboard +selection both matched the root's id and opened the dashboard instead). A +single-message thread still has no child, since its row IS the message. +Before item 177 every thread dropped depth 0. `applyMessageTagChange` and `messageById` both search the root first now (item 109); before that, a message-scoped write to a root card repainted nothing, and the strip refresh set the pane's chips to the empty node the lookup returned, destroying a strip that diff --git a/CHANGELOG.md b/CHANGELOG.md index a92ff4e..31ad3d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ point at which they are stable. - Searching on selected text containing a double quote no longer lets the rest of the selection escape the query. Quotes are now doubled, which is notmuch's own escape; the backslash form ended the quoted term early. +- Clicking a conversation's first message in the overview's unread list now + opens that message, instead of doing nothing visible. ## [0.30.0] - 2026-09-29 diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 434f5d0..19373e3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5555,11 +5555,13 @@ void MainWindow::applyPendingDashboardSelection() if (m_model->threadAt(row).threadId != m_dashboardSelectThreadId) continue; - // The thread's first message is the ROOT row, not a child: - // setThreadMessages drops depth 0 because the root stands for it, so - // looking for it among the children finds nothing. - if (m_model->data(thread, ThreadListModel::MessageIdRole).toString() - == m_dashboardSelectMessageId) { + // Only a thread of one answers for its message on its own row. A + // CONVERSATION keeps its first message as child 0 since item 177, so + // matching the conversation row on the root's id reselected the row + // already showing the dashboard and the click did nothing visible. + if (!m_model->isConversationRow(thread) + && m_model->data(thread, ThreadListModel::MessageIdRole).toString() + == m_dashboardSelectMessageId) { selectRowAt(thread); m_dashboardSelectMessageId.clear(); m_dashboardSelectThreadId.clear(); diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 0aaf8ff..7ef89e7 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -390,6 +390,7 @@ private slots: void recoveringAStaleThreadQueriesTheWholeThread(); void recoveryReselectsTheMessageThatWasBeingRead(); void recoveryOnTheFirstMessageSelectsItsOwnRow(); + void aDashboardEntryForTheFirstMessageSelectsItsOwnRow(); void doubleClickingAThreadOpensThatThreadAlone(); void doubleClickingAReplyOpensItsThreadNotTheReplyAlone(); void doubleClickingDoesNotLeaveTheMarkReadTimerArmed(); @@ -3538,6 +3539,60 @@ void TestMainWindow::recoveryOnTheFirstMessageSelectsItsOwnRow() QCOMPARE(model->threadFor(current).threadId, QStringLiteral("T1")); } +void TestMainWindow::aDashboardEntryForTheFirstMessageSelectsItsOwnRow() +{ + // The dashboard's unread list can name the conversation's FIRST message. + // Since item 177 that message is child 0 with a row of its own, so the + // entry must land there. Matching the conversation row on the root's id + // reselected the row already current, and the click did nothing visible. + const Config config; + MainWindow window(config); + + auto *queryEdit = window.findChild<QLineEdit *>(); + QVERIFY(queryEdit); + queryEdit->setText(QStringLiteral("tag:unread")); + queryEdit->returnPressed(); + const quint64 generation = window.currentGenerationForTesting(); + + auto *model = window.findChild<ThreadListModel *>(); + QVERIFY(model); + auto *view = window.findChild<ThreadListView *>(); + QVERIFY(view); + + ThreadSummary thread = makeThread(QStringLiteral("T1"), {}); + thread.totalCount = 2; + const QVector<ThreadSummary> result{ thread }; + QMetaObject::invokeMethod(&window, "onThreadsReady", + Q_ARG(QVector<ThreadSummary>, result), + Q_ARG(quint64, generation)); + view->setCurrentIndex(model->index(0, 0, QModelIndex())); + QVERIFY(model->isConversationRow(view->currentIndex())); + + QMetaObject::invokeMethod(&window, "selectMessageInCurrentThread", + Q_ARG(QString, QStringLiteral("m0@example.org"))); + + MessageNode root; + root.messageId = QStringLiteral("m0@example.org"); + root.threadId = QStringLiteral("T1"); + root.depth = 0; + MessageNode reply; + reply.messageId = QStringLiteral("m1@example.org"); + reply.threadId = QStringLiteral("T1"); + reply.depth = 1; + const QVector<MessageNode> nodes{ root, reply }; + QMetaObject::invokeMethod(&window, "onThreadTreeLoaded", + Q_ARG(QVector<MessageNode>, nodes), + Q_ARG(quint64, generation)); + + const QModelIndex current = view->currentIndex(); + QVERIFY2(current.isValid(), "the entry selected nothing"); + QVERIFY2(model->isMessageRow(current), + "the first message landed on the conversation row, which keeps " + "the dashboard on screen"); + QCOMPARE(model->messageAt(current).messageId, + QStringLiteral("m0@example.org")); +} + void TestMainWindow::doubleClickingAThreadOpensThatThreadAlone() { // Item 91, the thread case: "double click on a thread loads the whole |
