summaryrefslogtreecommitdiffstats
path: root/src/threadlistmodel.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-14 20:02:14 +0200
committerDanilo M. <danix@danix.xyz>2026-08-14 20:02:14 +0200
commitbde7409ef817089298718376e46a57b2d303cf02 (patch)
tree5fea161c839e30474db0a16c79f20b5fca976ffb /src/threadlistmodel.cpp
parent66f1159136de5e2f032ddc83b5b4b4bde2659291 (diff)
downloadqtmaildir-bde7409ef817089298718376e46a57b2d303cf02.tar.gz
qtmaildir-bde7409ef817089298718376e46a57b2d303cf02.zip
fix(read): repaint the card when one message is marked read
Follows the message-scoped mark-read. The user reported the write going out with nothing visible changing: the status bar counted an unsynced edit while the card stayed bold and the message pane still showed the `unread` tag, until the next query corrected it. sendMessageTagChange made no optimistic model update on purpose, because applyTagChange is keyed by THREAD and repainting a whole row for a one-message edit would claim every reply had changed too. That trade is right for an explicit tag edit and wrong for auto mark-read, where the visible change IS the feature and the delay exists to deliver it. ThreadListModel::applyMessageTagChange updates the message wherever it is held, as a child row and as `first`, and lets the thread's summary follow only when the answer is unambiguous: a thread reads as unread while ANY message does, so the tag is cleared from the thread only when no other message still carries it. For an unexpanded multi-message thread the per-message tags are not loaded, so the summary is left for the next query rather than guessed at. Mutation checked: without the call the card holds `unread` for the full timeout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/threadlistmodel.cpp')
-rw-r--r--src/threadlistmodel.cpp81
1 files changed, 81 insertions, 0 deletions
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index 3e079ed..04a5493 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -888,3 +888,84 @@ void ThreadListModel::applyTagChange(const QString &threadId,
return;
}
}
+
+void ThreadListModel::applyMessageTagChange(const QString &messageId,
+ const QStringList &added,
+ const QStringList &removed)
+{
+ const auto retag = [&added, &removed](QStringList *tags) {
+ for (const QString &tag : removed)
+ tags->removeAll(tag);
+ for (const QString &tag : added) {
+ if (!tags->contains(tag))
+ tags->append(tag);
+ }
+ };
+
+ for (int row = 0; row < m_threads.size(); ++row) {
+ ThreadNode &node = m_threads[row];
+ bool touched = false;
+
+ // `first` is a copy of the opening message rather than an alias into
+ // children, so both have to be updated when they name the same one.
+ if (node.first.messageId == messageId) {
+ retag(&node.first.tags);
+ touched = true;
+ }
+
+ for (int child = 0; child < node.children.size(); ++child) {
+ if (node.children.at(child).messageId != messageId)
+ continue;
+ retag(&node.children[child].tags);
+ touched = true;
+ const QModelIndex childIndex = index(child, 0, index(row, 0));
+ emit dataChanged(childIndex, childIndex);
+ }
+
+ // The thread has not been expanded and does not open with this
+ // message, so nothing here holds it. The summary may still need to
+ // follow, which the totalCount check below decides.
+ if (!touched && node.summary.firstMessageId != messageId
+ && node.summary.totalCount > 1) {
+ continue;
+ }
+
+ // The thread's own tags follow only when the answer is unambiguous.
+ //
+ // A thread carries `unread` while ANY of its messages does, so a
+ // one-message change can only clear it from the thread when there is
+ // nothing else left to carry it. With one message in the thread that
+ // is certain. With more, the honest answer needs every message's tags,
+ // which are only loaded once the thread has been expanded; until then
+ // the summary is left alone rather than guessed at, and the next query
+ // corrects it.
+ const bool wholeThread =
+ node.summary.totalCount <= 1
+ || (!node.children.isEmpty()
+ && node.children.size() >= node.summary.totalCount);
+ if (!wholeThread) {
+ if (touched)
+ emit dataChanged(index(row, 0), index(row, 0));
+ continue;
+ }
+
+ for (const QString &tag : removed) {
+ bool stillHeld = false;
+ for (const MessageNode &child : node.children) {
+ if (child.messageId != messageId && child.tags.contains(tag)) {
+ stillHeld = true;
+ break;
+ }
+ }
+ if (!stillHeld)
+ node.summary.tags.removeAll(tag);
+ }
+ for (const QString &tag : added) {
+ if (!node.summary.tags.contains(tag))
+ node.summary.tags.append(tag);
+ }
+
+ emit dataChanged(index(row, 0), index(row, 0));
+ return;
+ }
+}