aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-28 11:37:21 +0200
committerDanilo M. <danix@danix.xyz>2026-08-28 11:37:21 +0200
commit20eb3877c4502a0b8ebe770a563066d8c85387ba (patch)
tree08c5ad3490094289b00137f66e35055b7d6b0d64
parent9bec59c98a9f00e6e7f2c0181659a7639bc04e72 (diff)
downloadqtmaildir-20eb3877c4502a0b8ebe770a563066d8c85387ba.tar.gz
qtmaildir-20eb3877c4502a0b8ebe770a563066d8c85387ba.zip
feat: draw a conversation row's own tags, in one tier
Items 110 and 111 reconciled a card that showed one message with a row that was a thread. The row is the conversation now, so the union is simply what it means: the first-message substitution, PillOwnCountRole and the seeded first node all go.
-rw-r--r--src/carddelegate.cpp16
-rw-r--r--src/mainwindow.cpp17
-rw-r--r--src/threadlistmodel.cpp123
-rw-r--r--src/threadlistmodel.h32
-rw-r--r--tests/test_mainwindow.cpp68
-rw-r--r--tests/test_threadlistmodel.cpp177
6 files changed, 40 insertions, 393 deletions
diff --git a/src/carddelegate.cpp b/src/carddelegate.cpp
index 2dd29a2..d0ee957 100644
--- a/src/carddelegate.cpp
+++ b/src/carddelegate.cpp
@@ -425,17 +425,11 @@ void CardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
: ThreadListModel::PillColoursRole)
.toList();
- // A thread card draws its own tags at full size and the rest of the
- // conversation's smaller and muted (item 111). The count is where the two
- // tiers meet; a message row has no such split and reports its whole list.
- //
- // Shown rather than dropped, at the user's request: a card sits above a
- // conversation, so what its siblings carry is worth seeing, just not at
- // the same weight. Before the row has been opened everything is in the own
- // tier, so a chip SHRINKS when the split becomes known and none vanishes.
- const int ownCount =
- isMessage ? tags.size()
- : index.data(ThreadListModel::PillOwnCountRole).toInt();
+ // One tier since item 177: a conversation row draws the thread's own tags
+ // and a message row draws its message's, so nothing on a card belongs to
+ // anything but the row. The sibling tier this switched fonts at is Task
+ // 3's to remove.
+ const int ownCount = tags.size();
const QFont ownFont = CardLayout::smallFont(chrome.font);
const QFont siblingFont = CardLayout::siblingFont(chrome.font);
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 231a9a5..b64e20b 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -4058,21 +4058,8 @@ void MainWindow::onMessageLoaded(const QVector<MessageRef> &messages,
return;
// The worker's answer is the authority on what THIS message carries, and
- // it is the only place that truth arrives. Until it does, a thread row can
- // only offer ThreadSummary::tags, which is notmuch's union over the
- // conversation: a four-message thread whose third message is signed makes
- // the root card and the pane both claim `signed` for a message that is not
- // (item 110). Recording it here corrects the card and gives a
- // message-scoped write something to update, which is why marking a root
- // message read left the row bold before.
- //
- // A reply already has its own node from the thread tree, and
- // setRootMessageTags ignores anything that is not a root.
- for (const MessageRef &ref : messages)
- m_model->setRootMessageTags(ref.messageId, ref.tags);
-
- // The pane follows the same correction. setTags() at selection time can
- // only have used the union.
+ // the pane shows one message. setTags() at selection time can only have
+ // used the thread's union.
if (messages.size() == 1)
m_messageView->setTags(messages.first().tags);
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index 1bda728..4a75d47 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -503,24 +503,13 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
const ThreadNode &rowNode = m_threads.at(index.row());
- // A card stands for ONE message since item 108, so it must draw that
- // message's tags and not the thread's. `ThreadSummary::tags` is notmuch's
- // UNION over the conversation: a four-message thread whose third message
- // is signed reads as signed, and the card said so about a message that was
- // not (item 110).
- //
- // Only the tags are substituted. Everything else on the card, the subject,
- // the authors, the date and the reply count, describes the THREAD and is
- // correct as it stands; only the tags were ever the union that lied.
- //
- // `first.tags` is populated when the message is loaded, which is when the
- // user selects the row. Before that the union is the only answer available
- // and is what the card shows, which is why an unopened row can still
- // display a sibling's mark. Narrowing that further needs per-message state
- // in the query itself.
- ThreadSummary thread = rowNode.summary;
- if (!rowNode.first.messageId.isEmpty())
- thread.tags = rowNode.first.tags;
+ // The row IS the conversation since item 177, so `ThreadSummary::tags`,
+ // notmuch's UNION over it, is simply what the row means. Items 110 and 111
+ // substituted the displayed message's tags here and drew the rest in a
+ // muted second tier, to reconcile "this card shows one message" with "this
+ // row is a thread"; the first half of that is gone, so the whole apparatus
+ // is.
+ const ThreadSummary &thread = rowNode.summary;
if (role == ThreadIdRole)
return thread.threadId;
@@ -566,8 +555,7 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
if (role == MessageOwnColoursRole)
return QVariantList();
- if (role == PillTagsRole || role == PillColoursRole
- || role == PillOwnCountRole) {
+ if (role == PillTagsRole || role == PillColoursRole) {
// Everything the row already says another way is dropped: the account
// is the chip in the subject cell, flagged is the star column,
// attachment is the paperclip, unread is the row not being dimmed, and
@@ -601,28 +589,8 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
return pills;
};
- // `thread.tags` is the displayed message's own tags once the row has
- // been opened, and the thread's union before that (see the
- // substitution above). The union is always the full set, so the
- // difference is what belongs only to siblings.
- QStringList pills = pillsFrom(thread.tags);
- const int ownCount = pills.size();
-
- // The sibling tier, appended after the message's own. Shown rather
- // than dropped at the user's request: a card sits above a
- // conversation, so what the rest of it carries is worth seeing, just
- // not at the same weight. The delegate draws these smaller and muted.
- //
- // Empty until the row has been opened, because before that
- // `thread.tags` IS the union and the difference is nothing. That is
- // what makes a chip shrink rather than appear.
- for (const QString &tag : pillsFrom(rowNode.summary.tags)) {
- if (!pills.contains(tag))
- pills.append(tag);
- }
-
- if (role == PillOwnCountRole)
- return ownCount;
+ // One tier: the thread's own tags, which is what the row stands for.
+ const QStringList pills = pillsFrom(thread.tags);
if (role == PillTagsRole)
return pills;
@@ -799,22 +767,11 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
ThreadListModel::ThreadNode
ThreadListModel::nodeFor(const ThreadSummary &summary)
{
- ThreadNode node{ summary, {}, {}, false };
-
- // Only when the query actually supplied them. An empty list here would be
- // indistinguishable from "this message carries nothing", which would put
- // every chip in the sibling tier and mute the whole card.
- if (!summary.firstMessageId.isEmpty()
- && !summary.firstMessageTags.isEmpty()) {
- node.first.messageId = summary.firstMessageId;
- node.first.threadId = summary.threadId;
- node.first.tags = summary.firstMessageTags;
- // Carried alongside the tags, for the same reason messageById()
- // carries it onto a synthesised root: an unexpanded row has to know
- // which account it belongs to before Delete can name a folder.
- node.first.filePath = summary.firstMessagePath;
- }
- return node;
+ // No `first` node. A conversation row stands for the thread and draws the
+ // union; a one-message row's message arrives with its children like any
+ // other. Seeding it here is what made a row an ambiguous half-message
+ // (item 177).
+ return ThreadNode{ summary, {}, {}, false };
}
void ThreadListModel::appendBatch(const QVector<ThreadSummary> &batch)
@@ -926,26 +883,8 @@ void ThreadListModel::reconcile(const QVector<ThreadSummary> &threads)
|| m_threads.at(row).summary.authors != summary.authors
|| m_threads.at(row).summary.date != summary.date
|| m_threads.at(row).summary.totalCount != summary.totalCount
- || m_threads.at(row).summary.matchedCount != summary.matchedCount
- // The card's OWN message, which can move while the thread's union
- // does not: a root read elsewhere leaves the thread unread as long
- // as any reply is. Without this the card kept the tags it was
- // first given, and the sibling tier with them.
- || m_threads.at(row).summary.firstMessageTags
- != summary.firstMessageTags) {
+ || m_threads.at(row).summary.matchedCount != summary.matchedCount) {
m_threads[row].summary = summary;
-
- // The node too, since the card draws its tags from there. Only the
- // tags: the node's children and loaded flag are the expansion
- // state this whole method exists to preserve, and `first` carries
- // no children.
- if (!summary.firstMessageId.isEmpty()
- && !summary.firstMessageTags.isEmpty()) {
- m_threads[row].first.messageId = summary.firstMessageId;
- m_threads[row].first.threadId = summary.threadId;
- m_threads[row].first.tags = summary.firstMessageTags;
- }
-
emit dataChanged(index(row, 0), index(row, 0));
}
}
@@ -1064,36 +1003,6 @@ QString ThreadListModel::threadIdForMessage(const QString &messageId) const
return {};
}
-void ThreadListModel::setRootMessageTags(const QString &messageId,
- const QStringList &tags)
-{
- if (messageId.isEmpty())
- return;
-
- for (int row = 0; row < m_threads.size(); ++row) {
- ThreadNode &node = m_threads[row];
- if (node.summary.firstMessageId != messageId
- && node.first.messageId != messageId) {
- continue;
- }
-
- if (node.first.tags == tags && !node.first.messageId.isEmpty())
- return; // Nothing changed; do not churn the view.
-
- // Enough of a node for the card to draw from. The rest of the display
- // still comes from the summary, which is correct for it: the subject,
- // the authors and the date describe the thread, and only the TAGS were
- // ever the union that lied about this message.
- node.first.messageId = messageId;
- node.first.threadId = node.summary.threadId;
- node.first.tags = tags;
-
- const QModelIndex threadIndex = index(row, 0, QModelIndex());
- emit dataChanged(threadIndex, threadIndex);
- return;
- }
-}
-
MessageNode ThreadListModel::messageById(const QString &messageId) const
{
if (messageId.isEmpty())
diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h
index d1baae1..5cd1392 100644
--- a/src/threadlistmodel.h
+++ b/src/threadlistmodel.h
@@ -74,21 +74,6 @@ public:
/// config itself would be a second source of truth.
PillColoursRole,
- /// How many of PillTagsRole's entries belong to the message the card
- /// DISPLAYS, the rest belonging only to its siblings.
- ///
- /// The card stands for one message but sits above a conversation, so
- /// it shows both: the message's own tags first at full size, then the
- /// thread's other tags smaller and muted. Without the split a card
- /// either claimed a sibling's tag as its own (item 110) or dropped it
- /// and looked like it had lost information.
- ///
- /// Equals the whole list until the row has been opened, since the
- /// per-message tags arrive with the message load and before that the
- /// union is the only answer there is. Chips therefore SHRINK when the
- /// split becomes known; none ever disappears.
- PillOwnCountRole,
-
/// True when the row is a MESSAGE row rather than a thread root.
/// Drives both the action scope and whether the view paints a tag
/// strip under the row.
@@ -341,23 +326,6 @@ public:
/// message the user could select is always findable here.
QString threadIdForMessage(const QString &messageId) const;
- /// Records the tags a MESSAGE really carries, as the worker reported them.
- ///
- /// Exists because `ThreadSummary::tags` is notmuch's UNION over the
- /// thread, which is right for a card standing for a conversation and wrong
- /// for one standing for a message. A four-message thread whose third
- /// message is signed makes the whole thread read as signed, so the root
- /// card and the message pane both claimed a tag the displayed message did
- /// not have.
- ///
- /// Only the ROOT needs this: reply rows already carry their own nodes from
- /// setThreadMessages. Calling it for anything else is a no-op.
- ///
- /// The thread's summary is deliberately NOT rewritten. It describes the
- /// conversation, and three unread siblings do not stop being unread
- /// because this message was read.
- void setRootMessageTags(const QString &messageId, const QStringList &tags);
-
/// A loaded message row's node, found by id rather than by position.
///
/// For callers that know WHICH message they mean and must not depend on it
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index 08589b2..4ed62f9 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -411,7 +411,6 @@ private slots:
void autoMarkReadArmsForAReplyToo();
void taggingTheOpenRootMessageKeepsTheStripPopulated();
void aLoadedMessageCorrectsTheStripFromTheThreadsUnion();
- void aLoadedRootMessageGivesTheCardItsOwnTags();
void aTransientStatusMessageExpires();
void theSelectionCountIsStateAndDoesNotExpire();
void anEditUndoneNettsBackToZero();
@@ -6262,73 +6261,6 @@ void TestMainWindow::aLoadedMessageCorrectsTheStripFromTheThreadsUnion()
"the pane lost a tag the message really carries");
}
-void TestMainWindow::aLoadedRootMessageGivesTheCardItsOwnTags()
-{
- // The same correction, reaching the MODEL, which is what fixes the two
- // repaint reports: "if I mark the root message read the left pane entry
- // doesn't repaint (stays bold)" and the same for delete.
- //
- // The card could not repaint because the model had no per-message tags for
- // a root at all, so a message-scoped write updated the thread summary only
- // when the thread was a single message. A load gives the root the same
- // per-message node a reply has had all along, and from then on the card
- // draws the message it displays.
- const Config config;
- MainWindow window(config);
-
- auto *model = window.findChild<ThreadListModel *>();
- QVERIFY(model);
- auto *view = window.findChild<QTreeView *>();
- QVERIFY(view);
-
- ThreadSummary t = makeThread(QStringLiteral("t1"),
- { QStringLiteral("inbox"),
- QStringLiteral("signed"),
- QStringLiteral("unread") });
- t.totalCount = 4;
- model->appendBatch({ t });
-
- selectThreadRow(view, 0);
- QApplication::processEvents();
-
- MessageRef ref;
- ref.messageId = QStringLiteral("t1-first@example.org");
- ref.tags = QStringList{ QStringLiteral("inbox"), QStringLiteral("unread") };
- QMetaObject::invokeMethod(
- &window, "onMessageLoaded", Qt::DirectConnection,
- Q_ARG(QVector<MessageRef>, QVector<MessageRef>{ ref }),
- Q_ARG(quint64, window.currentGenerationForTesting()));
- QApplication::processEvents();
-
- // The model now knows what the root message itself carries.
- const MessageNode root =
- model->messageById(QStringLiteral("t1-first@example.org"));
- QCOMPARE(root.messageId, QStringLiteral("t1-first@example.org"));
- QVERIFY2(!root.tags.contains(QStringLiteral("signed")),
- "the root's node still carries a sibling's tag");
-
- const QModelIndex threadIndex = model->index(0, 0, QModelIndex());
- QSignalSpy spy(model, &QAbstractItemModel::dataChanged);
-
- auto *toggle = window.findChild<QAction *>(QStringLiteral("toggle_unread"));
- QVERIFY(toggle);
- toggle->trigger();
-
- QVERIFY2(spy.count() >= 1, "marking the root message read repainted nothing");
- QVERIFY2(!model->messageById(QStringLiteral("t1-first@example.org"))
- .isUnread(),
- "the root message is still unread after being marked read");
-
- // The card now draws that message, so it stops looking unread. Asserted on
- // the FONT, which is what the user means by "stays bold".
- QVERIFY2(!model->data(threadIndex, Qt::FontRole).value<QFont>().bold(),
- "the card still reads as unread, so the row stays bold and the "
- "user sees nothing");
- QVERIFY2(model->threadAt(0).isUnread(),
- "the thread summary was rewritten, claiming a four-message thread "
- "is read when three of its messages still are not");
-}
-
void TestMainWindow::aTransientStatusMessageExpires()
{
// "Sync complete" describes an event, not a state, and reads as though it
diff --git a/tests/test_threadlistmodel.cpp b/tests/test_threadlistmodel.cpp
index df579e6..bd35c6b 100644
--- a/tests/test_threadlistmodel.cpp
+++ b/tests/test_threadlistmodel.cpp
@@ -82,9 +82,7 @@ private slots:
void messageScopeSkipsAThreadRowItCannotNameAMessageFor();
void aMessageTagChangeReachesTheRootCardsOwnMessage();
void aMessageTagChangeOnOneOfManyLeavesTheThreadSummaryAlone();
- void aCardListsItsOwnTagsBeforeItsSiblings();
- void theSplitIsKnownBeforeTheRowIsEverOpened();
- void reconcileRefreshesASurvivorsOwnMessageTags();
+ void aConversationRowDrawsTheThreadsTags();
void updatesTagsForMessage();
void tagChangeIsIdempotent();
void tagChangeSignalsExactlyTheChangedRow();
@@ -1412,170 +1410,29 @@ void TestThreadListModel::aMessageTagChangeOnOneOfManyLeavesTheThreadSummaryAlon
"whole conversation was read, though six messages still are not");
}
-void TestThreadListModel::aCardListsItsOwnTagsBeforeItsSiblings()
+void TestThreadListModel::aConversationRowDrawsTheThreadsTags()
{
- // The user, 2026-08-16, looking at a real four-message thread: the card
- // showed `mailing-list/SBo` and `signed`, and `signed` vanished the moment
- // the row was selected, because it belongs to a SIBLING and item 110 made
- // the card stop claiming it.
- //
- // Their answer, which is better than either extreme: show both, and let
- // size say whose is whose. Own tags first at full size, the thread's other
- // tags after, smaller and muted. Nothing disappears; a chip only shrinks
- // once the split becomes known.
+ // Item 110 made a card draw its first message's tags so a four-message
+ // thread would stop claiming a `signed` its displayed message lacked.
+ // Under item 177 the row IS the conversation, so the union is what it
+ // means and the substitution is wrong.
ThreadListModel model;
- ThreadSummary t = makeThread(QStringLiteral("t1"), QStringLiteral("one"));
+ ThreadSummary t = makeThread(QStringLiteral("t1"), QStringLiteral("Talk"));
t.totalCount = 4;
- t.firstMessageId = QStringLiteral("m0@example.org");
- // The UNION, as notmuch reports it: `signed` is a sibling's.
- t.tags = QStringList{ QStringLiteral("inbox"),
- QStringLiteral("mailing-list/SBo"),
- QStringLiteral("signed"),
- QStringLiteral("unread") };
+ t.tags = QStringList{ QStringLiteral("inbox"), QStringLiteral("signed") };
+ t.firstMessageId = QStringLiteral("m1");
+ t.firstMessageTags = QStringList{ QStringLiteral("inbox") };
model.appendBatch({ t });
- const QModelIndex threadIndex = model.index(0, 0, QModelIndex());
-
- // Before the row is opened there is no per-message answer, so every chip
- // is in the own tier. This is what stops anything from appearing to vanish
- // later: the split narrows the tier, it does not remove a chip.
- const QStringList before =
- model.data(threadIndex, ThreadListModel::PillTagsRole).toStringList();
- QVERIFY(before.contains(QStringLiteral("mailing-list/SBo")));
- QVERIFY(before.contains(QStringLiteral("signed")));
- QCOMPARE(model.data(threadIndex, ThreadListModel::PillOwnCountRole).toInt(),
- before.size());
-
- // The message loads, carrying what it really has.
- model.setRootMessageTags(QStringLiteral("m0@example.org"),
- { QStringLiteral("inbox"),
- QStringLiteral("mailing-list/SBo"),
- QStringLiteral("unread") });
-
- const QStringList after =
- model.data(threadIndex, ThreadListModel::PillTagsRole).toStringList();
-
- // Same chips, still all present. The user explicitly did not want the
- // sibling's tag dropped.
- QVERIFY2(after.contains(QStringLiteral("signed")),
- "the sibling's tag was dropped from the card rather than being "
- "shown smaller, which is what looked like a bug");
- QVERIFY2(after.contains(QStringLiteral("mailing-list/SBo")),
- "the card lost a tag the message really carries");
-
- // Own first, siblings after, and the count is where the delegate switches
- // fonts.
- const int own =
- model.data(threadIndex, ThreadListModel::PillOwnCountRole).toInt();
- QVERIFY2(own > 0 && own < after.size(),
- "the split did not happen: every chip is in one tier");
- QCOMPARE(after.mid(0, own),
- QStringList{ QStringLiteral("mailing-list/SBo") });
- QCOMPARE(after.mid(own), QStringList{ QStringLiteral("signed") });
-
- // Colours stay aligned with the tags, since the delegate walks them in
- // step and a shift would colour a chip with its neighbour's colour.
- QCOMPARE(model.data(threadIndex, ThreadListModel::PillColoursRole)
- .toList()
- .size(),
- after.size());
-}
-
-void TestThreadListModel::theSplitIsKnownBeforeTheRowIsEverOpened()
-{
- // The user, 2026-08-16: "not selecting the thread shows the chips at 'main'
- // size, not smaller, not dimmed. After selecting the thread the unioned
- // chips repaint to the correct size/color."
- //
- // The first version derived the split from the message LOAD, so an unopened
- // row had no per-message answer and put every chip in the own tier. That is
- // honest and useless: the list is mostly unopened rows, so the feature was
- // invisible exactly where it was meant to be read, and selecting a row
- // still changed the card.
- //
- // The query knows. The worker already walks to the card's message to get
- // its id, so it reads that message's tags in the same pass and the split
- // arrives with the row.
- ThreadListModel model;
- ThreadSummary t = makeThread(QStringLiteral("t1"), QStringLiteral("one"));
- t.totalCount = 4;
- t.firstMessageId = QStringLiteral("m0@example.org");
- t.tags = QStringList{ QStringLiteral("inbox"),
- QStringLiteral("mailing-list/SBo"),
- QStringLiteral("signed"),
- QStringLiteral("unread") };
- // What the worker now supplies: the CARD's message, not the thread.
- t.firstMessageTags = QStringList{ QStringLiteral("inbox"),
- QStringLiteral("mailing-list/SBo"),
- QStringLiteral("unread") };
- model.appendBatch({ t });
-
- const QModelIndex threadIndex = model.index(0, 0, QModelIndex());
-
- // Never opened, never expanded.
- QCOMPARE(model.rowCount(threadIndex), 0);
-
+ const QModelIndex row = model.index(0, 0, QModelIndex());
const QStringList pills =
- model.data(threadIndex, ThreadListModel::PillTagsRole).toStringList();
- const int own =
- model.data(threadIndex, ThreadListModel::PillOwnCountRole).toInt();
-
- QVERIFY2(own < pills.size(),
- "an unopened row still puts every chip in the own tier, so the "
- "card renders them all at full size and only corrects itself "
- "when the row is selected");
- QCOMPARE(pills.mid(0, own), QStringList{ QStringLiteral("mailing-list/SBo") });
- QCOMPARE(pills.mid(own), QStringList{ QStringLiteral("signed") });
-
- // And a message-scoped write still lands, without a load having happened.
- model.applyMessageTagChange(QStringLiteral("m0@example.org"),
- { QStringLiteral("deleted") }, {});
- QVERIFY(model.messageById(QStringLiteral("m0@example.org")).isDeleted());
- QVERIFY2(!model.threadAt(0).isDeleted(),
- "the thread summary was rewritten for a one-message edit on a "
- "four-message thread");
-}
+ model.data(row, ThreadListModel::PillTagsRole).toStringList();
-void TestThreadListModel::reconcileRefreshesASurvivorsOwnMessageTags()
-{
- // reconcile() keeps a surviving row's NODE, deliberately: its children and
- // its loaded flag are the expansion state the method exists to preserve.
- // That means the per-message tags have to be refreshed explicitly, and the
- // change detector has to notice when only they moved.
- //
- // The case: a sync where the root message alone changed, which is exactly
- // what an external `notmuch tag` or another client does. The thread's union
- // can be identical while the card's own message is not.
- ThreadListModel model;
- ThreadSummary before = makeThread(QStringLiteral("t1"),
- QStringLiteral("one"));
- before.totalCount = 2;
- before.firstMessageId = QStringLiteral("m0@example.org");
- before.tags = QStringList{ QStringLiteral("inbox"),
- QStringLiteral("unread") };
- before.firstMessageTags = QStringList{ QStringLiteral("inbox"),
- QStringLiteral("unread") };
- model.appendBatch({ before });
-
- QVERIFY(model.messageById(QStringLiteral("m0@example.org")).isUnread());
-
- // The root was read elsewhere. The THREAD is still unread, because its
- // reply is, so the union does not move at all.
- ThreadSummary after = before;
- after.firstMessageTags = QStringList{ QStringLiteral("inbox") };
-
- QSignalSpy spy(&model, &QAbstractItemModel::dataChanged);
- model.reconcile({ after });
-
- QVERIFY2(!model.messageById(QStringLiteral("m0@example.org")).isUnread(),
- "a sync that changed only the card's own message left the row "
- "showing the old per-message tags");
- QVERIFY2(spy.count() >= 1,
- "the change was applied without telling the view, so the card "
- "keeps its old pixels until something else repaints it");
-
- // The expansion state is still what reconcile() exists to preserve.
- QCOMPARE(model.threadAt(0).threadId, QStringLiteral("t1"));
+ QVERIFY2(pills.contains(QStringLiteral("signed")),
+ "the card dropped a tag the conversation carries, so a signed "
+ "thread does not read as one until it is expanded");
+ QCOMPARE(model.data(row, ThreadListModel::TagsRole).toStringList(),
+ t.tags);
}
void TestThreadListModel::updatesTagsForMessage()