aboutsummaryrefslogtreecommitdiffstats
path: root/src/mainwindow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mainwindow.cpp')
-rw-r--r--src/mainwindow.cpp114
1 files changed, 109 insertions, 5 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 6c66953..37411e6 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1784,9 +1784,8 @@ void MainWindow::registerActions()
purgeSelected();
});
addAction(QStringLiteral("spam"), tr("Mark &spam"),
- tr("Add spam and remove inbox"), [this]() {
- tagSelected({ QStringLiteral("spam") }, { QStringLiteral("inbox") },
- tr("Mark spam"));
+ tr("Move the selected messages to the spam folder"), [this]() {
+ spamSelected();
});
// Item 57. The LABEL is "Important"; the action name and the tag are both
// still `flag`/`flagged`, deliberately. The name is what a user writes in
@@ -3972,7 +3971,8 @@ void MainWindow::refreshScopedActionLabels()
tr("Move every message of the selected threads out of the "
"trash"));
relabel(QStringLiteral("spam"), tr("Mark thread as &spam"),
- tr("Add spam and remove inbox on the selected threads"));
+ tr("Move every message of the selected threads to the spam "
+ "folder"));
relabel(QStringLiteral("flag"), tr("&Important thread"),
tr("Mark every message of the selected threads as important"));
} else {
@@ -3983,7 +3983,7 @@ void MainWindow::refreshScopedActionLabels()
relabel(QStringLiteral("restore"), tr("&Restore from trash"),
tr("Move the selected messages out of the trash"));
relabel(QStringLiteral("spam"), tr("Mark &spam"),
- tr("Add spam and remove inbox"));
+ tr("Move the selected messages to the spam folder"));
relabel(QStringLiteral("flag"), tr("&Important"),
tr("Add or remove the important tag"));
}
@@ -6413,6 +6413,105 @@ void MainWindow::trashThreads(const QStringList &threadIds)
Q_ARG(QString, QStringLiteral("delete_thread")));
}
+void MainWindow::spamSelected()
+{
+ const QModelIndexList rows =
+ m_threadView->selectionModel()->selectedRows();
+ if (rows.isEmpty())
+ return;
+
+ // Delete's sibling, resolved the same per-row way (item 177): a
+ // conversation row spams its conversation, a thread of one spams its
+ // message. Both halves are run, because a selection really can hold one of
+ // each; they travel different routes for the reason trashSelected()
+ // records.
+ const ActionScope scope = m_model->scopeForSelection(rows);
+
+ if (!scope.threadIds.isEmpty())
+ spamThreads(scope.threadIds);
+
+ if (scope.messageIds.isEmpty())
+ return;
+
+ QHash<QString, QString> pathById;
+ for (const QString &messageId : scope.messageIds)
+ pathById.insert(messageId, m_model->messageById(messageId).filePath);
+
+ spamMessages(scope.messageIds, pathById, scope.messageIds.size());
+}
+
+void MainWindow::spamMessages(const QStringList &messageIds,
+ const QHash<QString, QString> &pathById,
+ int messageCount,
+ const QStringList &wholeThreadIds)
+{
+ if (messageIds.isEmpty())
+ return;
+
+ // Grouped by destination, exactly as trashMessages() is: moveMessages()
+ // takes one folder per call, and a selection can span accounts with
+ // different spam folders.
+ QHash<QString, QStringList> bySpam;
+ QStringList unconfigured;
+ for (const QString &messageId : messageIds) {
+ const Account account =
+ accountForMessagePath(pathById.value(messageId));
+ if (account.spam.isEmpty()) {
+ unconfigured.append(messageId);
+ continue;
+ }
+ bySpam[account.maildir + QLatin1Char('/') + account.spam]
+ .append(messageId);
+ }
+
+ // The second line of defence, as for trash: the config loader warns, but a
+ // user who never fixed it still needs the gesture to say it did nothing
+ // rather than move the file somewhere invented.
+ if (!unconfigured.isEmpty()) {
+ m_statusLabel->setText(
+ tr("%n message(s) could not be marked as spam: no spam folder is "
+ "configured for their account.", "", int(unconfigured.size())));
+ }
+
+ if (bySpam.isEmpty())
+ return;
+
+ for (auto it = bySpam.cbegin(); it != bySpam.cend(); ++it) {
+ // `unread` and `inbox` go with the message, exactly as Delete strips
+ // them: marking spam is a decision about the message, and without the
+ // `inbox` removal a message spammed FROM the inbox keeps the tag the
+ // Inbox filter matches on and stays in that view. The origin is the
+ // placeholder, resolved per message once the move is confirmed.
+ sendMove(it.value(), it.key(),
+ { QStringLiteral("spam"), kOriginTagPlaceholder() },
+ { QStringLiteral("unread"), QStringLiteral("inbox") },
+ tr("Mark spam"), false, wholeThreadIds);
+ }
+
+ announceAction(
+ tr("%1: %n message(s)", "", messageCount).arg(tr("Mark spam")));
+}
+
+void MainWindow::spamThreads(const QStringList &threadIds)
+{
+ if (threadIds.isEmpty())
+ return;
+
+ // Asked of the WORKER rather than resolved here, and repainted HERE
+ // synchronously before it: the same shape as trashThreads(), for the same
+ // two reasons. An unexpanded thread's reply ids and paths exist only in
+ // the database; and the card must not wait for that round trip.
+ for (const QString &threadId : threadIds)
+ m_model->applyTagChange(threadId, { QStringLiteral("spam") },
+ { QStringLiteral("inbox") });
+
+ m_pendingThreadScope = threadIds;
+ QMetaObject::invokeMethod(m_worker, "resolveThreadMessages",
+ Qt::QueuedConnection,
+ Q_ARG(QStringList, threadIds),
+ Q_ARG(QString, QStringLiteral("spam_thread")));
+}
+
void MainWindow::onThreadMessagesResolved(const QStringList &messageIds,
const QStringList &paths,
const QStringList &tags,
@@ -6467,6 +6566,11 @@ void MainWindow::onThreadMessagesResolved(const QStringList &messageIds,
return;
}
+ if (requestTag == QStringLiteral("spam_thread")) {
+ spamMessages(messageIds, pathById, messageIds.size(), threadScope);
+ return;
+ }
+
if (requestTag == QStringLiteral("restore_messages")) {
restoreResolvedMessages(messageIds, paths, tags);
return;