aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md10
-rw-r--r--CHANGELOG.md2
-rw-r--r--src/mainwindow.cpp12
-rw-r--r--tests/test_mainwindow.cpp55
4 files changed, 71 insertions, 8 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 8c4ebd7..6df8e18 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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