summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mainwindow.cpp15
-rw-r--r--src/threadlistmodel.cpp81
-rw-r--r--src/threadlistmodel.h17
3 files changed, 109 insertions, 4 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 17a5a65..ae5e8fc 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -3482,10 +3482,17 @@ void MainWindow::sendMessageTagChange(const QStringList &messageIds,
if (messageIds.isEmpty())
return;
- // No optimistic model update. applyTagChange is keyed by THREAD and would
- // repaint the whole row as though every message in it had changed, which
- // for a one-message edit is a lie the user would see and then watch
- // silently correct itself on the next query.
+ // Optimistic, but scoped to the message. applyTagChange() is keyed by
+ // THREAD and would repaint the whole row as though every message in it had
+ // changed, which for a one-message edit is a lie; applyMessageTagChange()
+ // updates that message and lets the thread's own tags follow only when the
+ // answer is unambiguous.
+ //
+ // Not optional for auto mark-read: without it the write goes out, the
+ // status bar counts an unsynced edit, and the card stays bold with
+ // `unread` on it until the next query. The user reported exactly that.
+ for (const QString &messageId : messageIds)
+ m_model->applyMessageTagChange(messageId, add, remove);
// The accounts this touches, resolved through the containing threads: the
// account is a property of the thread, and the sync needs the channel
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;
+ }
+}
diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h
index 2b8d2b0..f7abaf8 100644
--- a/src/threadlistmodel.h
+++ b/src/threadlistmodel.h
@@ -273,6 +273,23 @@ public:
void applyTagChange(const QString &threadId, const QStringList &added,
const QStringList &removed);
+ /// The same, scoped to ONE message.
+ ///
+ /// Updates that message's own tags wherever it is held: as a child row if
+ /// the thread is expanded, and as `first` when it is the thread's opening
+ /// message. The thread's summary tags follow only when the change is
+ /// unambiguous for the whole thread, which for `unread` means no other
+ /// message still carries it, since a thread reads as unread while any of
+ /// its messages does.
+ ///
+ /// Exists because auto mark-read touches one message and the card still
+ /// has to stop looking unread. applyTagChange() above cannot serve that:
+ /// it rewrites the thread's tags directly, which for a multi-message
+ /// thread would claim every reply had been read.
+ void applyMessageTagChange(const QString &messageId,
+ const QStringList &added,
+ const QStringList &removed);
+
private:
/// One thread root and the message rows expanded under it.
///