summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/threadlistmodel.cpp37
-rw-r--r--src/threadlistmodel.h20
-rw-r--r--tests/test_threadlistmodel.cpp69
3 files changed, 126 insertions, 0 deletions
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index 9a74041..0956e6b 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -268,6 +268,37 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
return QStringList();
case PillColoursRole:
return QVariantList();
+ case MessageOwnTagsRole: {
+ // Set difference against the parent THREAD, not against a global
+ // list: "own" means "not already said by the card above this one".
+ // The parent row indexes m_threads directly, which is the same
+ // mapping messageAt() uses to reach this node.
+ const int threadRow = index.parent().row();
+ const QStringList threadTags =
+ (threadRow >= 0 && threadRow < m_threads.size())
+ ? m_threads.at(threadRow).summary.tags
+ : QStringList();
+ QStringList own;
+ for (const QString &tag : node.tags) {
+ if (!threadTags.contains(tag))
+ own.append(tag);
+ }
+ // Sorted, so a reply does not reshuffle its own chips between
+ // repaints, matching what PillTagsRole already guarantees.
+ own.sort();
+ return own;
+ }
+ case MessageOwnColoursRole: {
+ const QStringList own =
+ data(index, MessageOwnTagsRole).toStringList();
+ QVariantList colours;
+ colours.reserve(own.size());
+ for (const QString &tag : own) {
+ colours.append(m_tagColors ? m_tagColors->colourFor(tag)
+ : TagColors().colourFor(tag));
+ }
+ return colours;
+ }
case AccountLabelRole:
return QString();
case Qt::DisplayRole:
@@ -344,6 +375,12 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
if (role == TagsRole)
return thread.tags;
+ if (role == MessageOwnTagsRole)
+ return QStringList();
+
+ if (role == MessageOwnColoursRole)
+ return QVariantList();
+
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,
diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h
index b80488b..b13165c 100644
--- a/src/threadlistmodel.h
+++ b/src/threadlistmodel.h
@@ -103,6 +103,26 @@ public:
/// delegate cannot call hasChildren without the model, and the same
/// answer has to reach the cell that reserves room for the glyph.
HasRepliesRole,
+
+ /// The tags this MESSAGE carries that its thread does not.
+ ///
+ /// A reply card shows these and nothing else. Showing a reply's full
+ /// tag set instead was measured against the user's own database and
+ /// rejected: of 48691 messages, 7 carry `unread` and 75 carry
+ /// `flagged`, and both are already drawn another way (the sender's
+ /// weight, and the mark on line 2). Every other tag is applied to a
+ /// whole thread and is identical on all its messages, so full sets
+ /// would repeat the thread's own chips down the entire expansion,
+ /// which is the striping the old row-wide strip existed to avoid.
+ ///
+ /// Empty on a thread row, which has no thread to differ from.
+ MessageOwnTagsRole,
+
+ /// The colours for MessageOwnTagsRole, in the same order. Supplied by
+ /// the model for the same reason as PillColoursRole: it owns the
+ /// TagColors instance, and a delegate reading config itself would be a
+ /// second source of truth.
+ MessageOwnColoursRole,
};
/// Row fill for a thread tagged `deleted`, and for one tagged `spam`.
diff --git a/tests/test_threadlistmodel.cpp b/tests/test_threadlistmodel.cpp
index 3d131cf..4df0269 100644
--- a/tests/test_threadlistmodel.cpp
+++ b/tests/test_threadlistmodel.cpp
@@ -31,6 +31,8 @@ private slots:
void rootRowsSurviveTheTreeConversion();
void repliesBecomeChildRowsUnderTheirThread();
void messageRowsShowTheirOwnSenderAndSubject();
+ void replyShowsOnlyItsOwnTags();
+ void replySharingEveryThreadTagShowsNone();
void reloadingAThreadReplacesItsRepliesRatherThanRepeatingThem();
void anUnexpandedMultiMessageThreadOffersAnExpander();
void scopeFollowsTheSelectedRowKind();
@@ -1044,5 +1046,72 @@ void TestThreadListModel::attachmentColumnIsFirstAndMarksOnlyTaggedThreads()
Qt::DisplayRole).toString().isEmpty());
}
+void TestThreadListModel::replyShowsOnlyItsOwnTags()
+{
+ ThreadListModel model;
+ ThreadSummary thread;
+ thread.threadId = QStringLiteral("T1");
+ thread.subject = QStringLiteral("Build fails");
+ thread.totalCount = 2;
+ thread.tags = { QStringLiteral("inbox"), QStringLiteral("work") };
+ model.appendBatch({ thread });
+
+ MessageNode reply;
+ reply.messageId = QStringLiteral("M2");
+ reply.threadId = QStringLiteral("T1");
+ reply.from = QStringLiteral("bob@example.org");
+ reply.depth = 1;
+ // Two the thread already has, one it does not.
+ reply.tags = { QStringLiteral("inbox"), QStringLiteral("work"),
+ QStringLiteral("todo") };
+ model.setThreadMessages(QStringLiteral("T1"), { reply });
+
+ const QModelIndex threadIndex = model.index(0, 0);
+ QVERIFY(model.hasChildren(threadIndex));
+ const QModelIndex replyIndex = model.index(0, 0, threadIndex);
+ QVERIFY(replyIndex.isValid());
+
+ const QStringList own =
+ replyIndex.data(ThreadListModel::MessageOwnTagsRole).toStringList();
+ QCOMPARE(own, QStringList{ QStringLiteral("todo") });
+
+ // The colours must line up with the names one for one, or the delegate
+ // walks the two lists together and paints a chip in another tag's colour.
+ const QVariantList colours =
+ replyIndex.data(ThreadListModel::MessageOwnColoursRole).toList();
+ QCOMPARE(colours.size(), own.size());
+ QVERIFY(colours.first().value<QColor>().isValid());
+}
+
+void TestThreadListModel::replySharingEveryThreadTagShowsNone()
+{
+ ThreadListModel model;
+ ThreadSummary thread;
+ thread.threadId = QStringLiteral("T1");
+ thread.totalCount = 2;
+ thread.tags = { QStringLiteral("inbox"), QStringLiteral("work") };
+ model.appendBatch({ thread });
+
+ MessageNode reply;
+ reply.messageId = QStringLiteral("M2");
+ reply.threadId = QStringLiteral("T1");
+ reply.depth = 1;
+ reply.tags = { QStringLiteral("inbox"), QStringLiteral("work") };
+ model.setThreadMessages(QStringLiteral("T1"), { reply });
+
+ const QModelIndex replyIndex = model.index(0, 0, model.index(0, 0));
+ QVERIFY(replyIndex.isValid());
+ QVERIFY(replyIndex.data(ThreadListModel::MessageOwnTagsRole)
+ .toStringList()
+ .isEmpty());
+
+ // A thread row has no thread to differ from, so it never answers these:
+ // its own chips come from PillTagsRole.
+ QVERIFY(model.index(0, 0)
+ .data(ThreadListModel::MessageOwnTagsRole)
+ .toStringList()
+ .isEmpty());
+}
+
QTEST_MAIN(TestThreadListModel)
#include "test_threadlistmodel.moc"