summaryrefslogtreecommitdiffstats
path: root/src/threadlistmodel.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-08 10:22:33 +0200
committerDanilo M. <danix@danix.xyz>2026-08-10 08:23:05 +0200
commit879a117cba62df57dfcb0c0dfd4383308fa07f13 (patch)
tree6c46b4617f766e99ad386f115c3acce39e87f9b7 /src/threadlistmodel.cpp
parent2dd35b2327975a16e81018f8cd704c32a1e68cf5 (diff)
downloadqtmaildir-879a117cba62df57dfcb0c0dfd4383308fa07f13.tar.gz
qtmaildir-879a117cba62df57dfcb0c0dfd4383308fa07f13.zip
feat(model): expose a thread's replies as child rows
setThreadMessages drops the depth-0 message: it is the thread's first message and the root row already stands for it. Keeping it would show a thread of seven as one root and seven children, contradicting the reply count the row advertises. Calling again replaces rather than appends, so a thread reloaded after a sync does not list its replies twice. A message row reports its own sender and subject, not the thread's. That is the mistake worth guarding: the thread's author summary usually contains the first sender too, so reading it renders something plausible for the root's own reply and wrong for every other one. Mutation-checked, and the wrong version returns 'Alice' where 'Bob' belongs. Child rows carry no tag pills. The strip is a row-wide band of the thread's tags; one under each reply would stripe the list and repeat identical tags down the expansion.
Diffstat (limited to 'src/threadlistmodel.cpp')
-rw-r--r--src/threadlistmodel.cpp129
1 files changed, 124 insertions, 5 deletions
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index 3194859..6fc2177 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -169,12 +169,60 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
return {};
}
- // Message rows are handled in Task 4; until then only thread rows exist and
- // a child index cannot be produced. The bound is checked against the thread
- // list only after establishing this IS a thread row, since a child row's
+ // A message row. Handled before the bounds check below, since a child row's
// number indexes its siblings, not m_threads.
- if (index.parent().isValid())
- return {};
+ if (isMessageRow(index)) {
+ const MessageNode node = messageAt(index);
+ if (node.messageId.isEmpty())
+ return {};
+
+ switch (role) {
+ case IsMessageRole:
+ return true;
+ case MessageIdRole:
+ return node.messageId;
+ case MessageDepthRole:
+ return node.depth;
+ case ThreadIdRole:
+ // A message row still belongs to a thread, and a caller that only
+ // needs the containing thread must not have to walk up itself.
+ return node.threadId;
+ case TagsRole:
+ case PillTagsRole:
+ // No strip under a child row: the strip is a ROW-wide band carrying
+ // the thread's tags, and one under every reply would stripe the
+ // list and repeat the same tags down the whole expansion.
+ return QStringList();
+ case PillColoursRole:
+ return QVariantList();
+ case AccountLabelRole:
+ return QString();
+ case Qt::DisplayRole:
+ switch (index.column()) {
+ case AuthorsColumn:
+ // The REPLY's sender, not the thread's author summary. Reading
+ // the thread's fields here would look almost right, since the
+ // first sender usually appears in both.
+ return node.from;
+ case SubjectColumn:
+ return node.subject;
+ case DateColumn:
+ return node.date.toString(QStringLiteral("yyyy-MM-dd hh:mm"));
+ case AttachmentColumn:
+ return node.hasAttachment() ? attachmentGlyph() : QString();
+ case FlagColumn:
+ return node.isFlagged() ? flagGlyph() : QString();
+ default:
+ return {};
+ }
+ case Qt::ForegroundRole:
+ // Same rule as a thread row: read recedes, unread stays at the
+ // palette's own colour.
+ return node.isUnread() ? QVariant() : QVariant(readColour());
+ default:
+ return {};
+ }
+ }
if (index.row() >= m_threads.size())
return {};
@@ -184,6 +232,19 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
if (role == ThreadIdRole)
return thread.threadId;
+ // Answered rather than left to fall through as an invalid QVariant. An
+ // invalid one converts to false and an empty string anyway, so the
+ // behaviour is the same, but a role the model never mentions is a latent
+ // bug the next reader has to prove is harmless.
+ if (role == IsMessageRole)
+ return false;
+
+ if (role == MessageIdRole)
+ return QString();
+
+ if (role == MessageDepthRole)
+ return 0;
+
if (role == TagsRole)
return thread.tags;
@@ -379,6 +440,64 @@ void ThreadListModel::clear()
endResetModel();
}
+void ThreadListModel::setThreadMessages(const QString &threadId,
+ const QVector<MessageNode> &nodes)
+{
+ for (int row = 0; row < m_threads.size(); ++row) {
+ if (m_threads.at(row).summary.threadId != threadId)
+ continue;
+
+ const QModelIndex parent = index(row, 0, QModelIndex());
+
+ // Replace, not append. A thread reloaded after a sync would otherwise
+ // list every reply twice.
+ if (!m_threads.at(row).children.isEmpty()) {
+ beginRemoveRows(parent, 0, m_threads.at(row).children.size() - 1);
+ m_threads[row].children.clear();
+ endRemoveRows();
+ }
+
+ QVector<MessageNode> children;
+ children.reserve(nodes.size());
+ for (const MessageNode &node : nodes) {
+ if (node.depth > 0)
+ children.append(node);
+ }
+
+ if (!children.isEmpty()) {
+ beginInsertRows(parent, 0, children.size() - 1);
+ m_threads[row].children = children;
+ endInsertRows();
+ }
+
+ // Set even when there are no replies: that is the difference between a
+ // single-message thread and one whose replies were never fetched.
+ m_threads[row].loaded = true;
+ return;
+ }
+}
+
+bool ThreadListModel::isMessageRow(const QModelIndex &index) const
+{
+ return index.isValid() && index.parent().isValid();
+}
+
+MessageNode ThreadListModel::messageAt(const QModelIndex &index) const
+{
+ if (!isMessageRow(index))
+ return {};
+
+ const int threadRow = index.parent().row();
+ if (threadRow < 0 || threadRow >= m_threads.size())
+ return {};
+
+ const QVector<MessageNode> &children = m_threads.at(threadRow).children;
+ if (index.row() < 0 || index.row() >= children.size())
+ return {};
+
+ return children.at(index.row());
+}
+
ThreadSummary ThreadListModel::threadAt(int row) const
{
if (row < 0 || row >= m_threads.size())