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 /src | |
| 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.
Diffstat (limited to 'src')
| -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 |
4 files changed, 23 insertions, 165 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 |
