aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-01 10:31:52 +0200
committerDanilo M. <danix@danix.xyz>2026-10-01 10:31:52 +0200
commit63c7a1fcaadb1e26c1597d5b86ce85066890ece3 (patch)
tree80fd9e156af879818521015f0d9ad8384886c3da
parentb12aada52d682c45aa1e66e13dc448dc894aee79 (diff)
downloadqtmaildir-63c7a1fcaadb1e26c1597d5b86ce85066890ece3.tar.gz
qtmaildir-63c7a1fcaadb1e26c1597d5b86ce85066890ece3.zip
fix: open a conversation's first message from the overview
Clicking the first message in a conversation overview's unread list did nothing visible. applyPendingDashboardSelection() matched the conversation row on the root's message id, so it reselected the row that was already current and the overview stayed on screen. Since item 177 setThreadMessages keeps a conversation's first message as child 0, so it has a row of its own. Only a thread of one now answers for its message on its own row, the same guard 236ac86 added to the recovery. AGENTS.md still said a thread's first message is never among its children, which has been true only for a thread of one since item 177. It is corrected to say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-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