aboutsummaryrefslogtreecommitdiffstats
path: root/src/threadlistmodel.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-28 17:56:25 +0200
committerDanilo M. <danix@danix.xyz>2026-08-28 17:56:25 +0200
commita346bef6c08e9725f3ab41f8237f01e9eb69eea7 (patch)
treeca3ea16c23da8971d6b9f8f7d4867845e5c25e0c /src/threadlistmodel.cpp
parentf934029d3333396bde6ec306ab300641929c7cdc (diff)
downloadqtmaildir-a346bef6c08e9725f3ab41f8237f01e9eb69eea7.tar.gz
qtmaildir-a346bef6c08e9725f3ab41f8237f01e9eb69eea7.zip
feat: judge a row's membership on the thread's union
Closes item 170 under item 177. A conversation belongs to a view while any of its messages match, so reading one message of a thread no longer takes the conversation out of the Unread view. The current row is never evicted, and an automatic write defers its eviction until the selection moves.
Diffstat (limited to 'src/threadlistmodel.cpp')
-rw-r--r--src/threadlistmodel.cpp54
1 files changed, 54 insertions, 0 deletions
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index fd4899b..0559694 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -206,6 +206,34 @@ void ThreadListModel::setTrashView(bool trash)
}
}
+void ThreadListModel::removeThreadsWithoutTag(const QStringList &threadIds,
+ const QString &tag)
+{
+ if (tag.isEmpty() || threadIds.isEmpty())
+ return;
+
+ // Backwards for the same reason the sweeping form below is: each removal
+ // renumbers everything after it.
+ for (int row = m_threads.size() - 1; row >= 0; --row) {
+ const ThreadNode &node = m_threads.at(row);
+ if (!threadIds.contains(node.summary.threadId))
+ continue;
+
+ // The SUMMARY, which is notmuch's union over the conversation, and
+ // never `first.tags`. A thread belongs to a view while ANY of its
+ // messages match it (item 177), so reading the message a 44-message
+ // card happens to draw must not evict the conversation while two of
+ // its replies are still unread. `first.tags` is right for what the
+ // card PAINTS and wrong for whether the row belongs here at all.
+ if (node.summary.tags.contains(tag))
+ continue;
+
+ beginRemoveRows({}, row, row);
+ m_threads.remove(row);
+ endRemoveRows();
+ }
+}
+
void ThreadListModel::removeThreadsWithoutTag(const QString &tag)
{
if (tag.isEmpty() || m_threads.isEmpty())
@@ -995,6 +1023,15 @@ MessageNode ThreadListModel::messageAt(const QModelIndex &index) const
QString ThreadListModel::threadIdForMessage(const QString &messageId) const
{
for (const ThreadNode &node : m_threads) {
+ // The ROOT first. A thread's first message is not among its children
+ // (item 109: setThreadMessages drops depth 0 because the root row
+ // stands for it), so a search over children alone answers "no thread"
+ // for every thread row's own message, which is the id an ordinary tag
+ // action resolves to since item 108.
+ if (node.first.messageId == messageId
+ || node.summary.firstMessageId == messageId) {
+ return node.summary.threadId;
+ }
for (const MessageNode &child : node.children) {
if (child.messageId == messageId)
return node.summary.threadId;
@@ -1003,6 +1040,23 @@ QString ThreadListModel::threadIdForMessage(const QString &messageId) const
return {};
}
+bool ThreadListModel::hasThread(const QString &threadId) const
+{
+ return std::any_of(m_threads.cbegin(), m_threads.cend(),
+ [&threadId](const ThreadNode &node) {
+ return node.summary.threadId == threadId;
+ });
+}
+
+int ThreadListModel::threadCountFor(const QString &threadId) const
+{
+ for (const ThreadNode &node : m_threads) {
+ if (node.summary.threadId == threadId)
+ return node.summary.totalCount;
+ }
+ return 0;
+}
+
MessageNode ThreadListModel::messageById(const QString &messageId) const
{
if (messageId.isEmpty())