diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-28 11:37:21 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-28 11:37:21 +0200 |
| commit | 20eb3877c4502a0b8ebe770a563066d8c85387ba (patch) | |
| tree | 08c5ad3490094289b00137f66e35055b7d6b0d64 | |
| parent | 9bec59c98a9f00e6e7f2c0181659a7639bc04e72 (diff) | |
| download | qtmaildir-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.cpp | 16 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 17 | ||||
| -rw-r--r-- | src/threadlistmodel.cpp | 123 | ||||
| -rw-r--r-- | src/threadlistmodel.h | 32 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 68 | ||||
| -rw-r--r-- | tests/test_threadlistmodel.cpp | 177 |
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() |
