diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-14 20:02:14 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-14 20:02:14 +0200 |
| commit | bde7409ef817089298718376e46a57b2d303cf02 (patch) | |
| tree | 5fea161c839e30474db0a16c79f20b5fca976ffb /src/threadlistmodel.cpp | |
| parent | 66f1159136de5e2f032ddc83b5b4b4bde2659291 (diff) | |
| download | qtmaildir-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.cpp | 81 |
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; + } +} |
