summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/keymap.cpp6
-rw-r--r--src/mainwindow.cpp102
-rw-r--r--src/mainwindow.h16
3 files changed, 122 insertions, 2 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp
index b7cf8f5..098a85d 100644
--- a/src/keymap.cpp
+++ b/src/keymap.cpp
@@ -41,6 +41,12 @@ QStringList KeyMap::knownActions()
// carries no default binding for exactly the same reason: the act is
// identical and so is the hazard.
QStringLiteral("purge"),
+ // Empty trash's other sibling, per ACCOUNT: it MOVES every message in
+ // the spam folder to that account's trash, so it is undoable and
+ // carries no confirmation. No default binding either: a bulk move
+ // deserves a deliberate gesture, and since item 132 an unbound action
+ // is menu-reachable rather than broken.
+ QStringLiteral("empty_spam"),
QStringLiteral("spam"),
QStringLiteral("toggle_unread"),
QStringLiteral("mark_all_read"),
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 4a4a8d1..9198839 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1783,6 +1783,16 @@ void MainWindow::registerActions()
tr("Permanently delete the selected messages"), [this]() {
purgeSelected();
});
+ // Empty Trash's other sibling, but a MOVE rather than a purge: it moves
+ // every message in the spam folder to that account's trash, so it is
+ // undoable and asks nothing. The mnemonic is on "f&older" rather than the
+ // brief's `s&pam` because Alt+P is already Re&ply and Alt+S is Mark &spam,
+ // and no letter of "Empty spam" is free in this menu.
+ addAction(QStringLiteral("empty_spam"), tr("Empty spam f&older..."),
+ tr("Move every message in the spam folder to the trash"),
+ [this]() {
+ emptySpam();
+ });
addAction(QStringLiteral("spam"), tr("Mark &spam"),
tr("Move the selected messages to the spam folder"), [this]() {
spamSelected();
@@ -2072,6 +2082,13 @@ const QHash<QString, QPair<QString, QString>> kThemeIcons = {
// different amounts of mail must not look identical. `user-trash` is
// the theme's own wastebasket, which reads as "this one, gone".
{ QStringLiteral("purge"), { QStringLiteral("user-trash"), QString() } },
+ // Empty Spam SHARES purge's `user-trash`, deliberately: both move mail
+ // into the trash, and the standard wastebasket is the honest glyph. The
+ // no-duplicates rule is about the icon-only TOOLBAR, and this is a
+ // Message-menu-only entry that always carries its text, so it is named in
+ // noTwoActionsShareAnIcon()'s exception list. Putting it on the toolbar
+ // fails that test rather than passing silently.
+ { QStringLiteral("empty_spam"), { QStringLiteral("user-trash"), QString() } },
{ QStringLiteral("undo"), { QStringLiteral("edit-undo"), QString() } },
// `bug` first, per the user's choice, with the standard junk name behind
// it so a theme without the bug still draws a junk icon.
@@ -2197,6 +2214,7 @@ void MainWindow::buildMenus()
// the five filters would read as one of them.
messageMenu->addAction(m_actions.value(QStringLiteral("cleanup_stranded")));
messageMenu->addAction(m_actions.value(QStringLiteral("empty_trash")));
+ messageMenu->addAction(m_actions.value(QStringLiteral("empty_spam")));
messageMenu->addAction(m_actions.value(QStringLiteral("tag_rules")));
auto *viewMenu = menuBar()->addMenu(tr("&View"));
@@ -3718,6 +3736,28 @@ bool MainWindow::isShowingTrash() const
return false;
}
+bool MainWindow::isShowingSpam() const
+{
+ // Exact sibling of isShowingTrash(), and path-based for the same reason:
+ // the Spam filter matches a folder rather than a tag, so a message put
+ // there by another client is in the view and carries no tag of ours. Both
+ // scopes, because the view composes with the account dropdown.
+ const QString query = m_lastQuery.trimmed();
+ if (query.isEmpty())
+ return false;
+
+ const QString all = m_config.allSpamQuery().trimmed();
+ if (!all.isEmpty() && query == all)
+ return true;
+
+ for (const Account &account : m_config.accounts()) {
+ const QString spam = account.spamQuery().trimmed();
+ if (!spam.isEmpty() && query == spam)
+ return true;
+ }
+ return false;
+}
+
void MainWindow::updateViewWideActions()
{
// Only meaningful on mail that is actually in a trash folder. An enabled
@@ -6576,6 +6616,31 @@ void MainWindow::onThreadMessagesResolved(const QStringList &messageIds,
return;
}
+ if (requestTag == QStringLiteral("empty_spam")) {
+ // Grouped per account, because each group travels to that account's
+ // own trash: one destination composed once would put one account's
+ // junk in another's trash. The origin tag is the placeholder, resolved
+ // per message by onMessagesMoved() to the spam folder it is leaving;
+ // Task 3's overwrite rule strips the previous origin, so a message
+ // that travelled inbox -> spam -> trash keeps exactly one.
+ QHash<QString, QStringList> byTrash;
+ for (int i = 0; i < messageIds.size(); ++i) {
+ const Account account = accountForMessagePath(paths.at(i));
+ if (account.maildir.isEmpty() || account.trash.isEmpty())
+ continue;
+ byTrash[account.maildir + QLatin1Char('/') + account.trash]
+ .append(messageIds.at(i));
+ }
+ for (auto it = byTrash.cbegin(); it != byTrash.cend(); ++it) {
+ sendMove(it.value(), it.key(),
+ { QStringLiteral("deleted"), kOriginTagPlaceholder() },
+ { QStringLiteral("spam"), QStringLiteral("unread"),
+ QStringLiteral("inbox") },
+ tr("Empty spam"));
+ }
+ return;
+ }
+
if (requestTag == QStringLiteral("purge_selection")) {
// No scope named: the prompt says "the selection", which is what the
// user pointed at, rather than an account they did not.
@@ -6899,6 +6964,37 @@ void MainWindow::emptyTrash()
Q_ARG(QString, QStringLiteral("empty_trash")));
}
+void MainWindow::emptySpam()
+{
+ // Scoped to the account selector, like emptyTrash(): All accounts empties
+ // every configured spam folder, a selected account only its own. Unlike
+ // emptyTrash() there is no confirmation, because this is a MOVE and every
+ // mutation that can be undone gets undo instead of a dialog.
+ const QString accountKey = m_accountBox->currentData().toString();
+ const QString query = accountKey.isEmpty()
+ ? m_config.allSpamQuery()
+ : m_config.account(accountKey).spamQuery();
+
+ // An account with no spam folder configured produces an EMPTY query, and
+ // an empty notmuch query matches EVERYTHING. Refusing here rather than
+ // relying on the worker's guard is the whole safety of the action: the
+ // message names the cause.
+ if (query.isEmpty()) {
+ showTransientStatus(tr("No spam folder is configured"));
+ return;
+ }
+
+ if (!m_worker) {
+ showTransientStatus(tr("Not connected to the mail index"));
+ return;
+ }
+
+ QMetaObject::invokeMethod(m_worker, "resolveQueryMessages",
+ Qt::QueuedConnection,
+ Q_ARG(QString, query),
+ Q_ARG(QString, QStringLiteral("empty_spam")));
+}
+
void MainWindow::purgeSelected()
{
const QModelIndexList rows =
@@ -7488,8 +7584,10 @@ void MainWindow::onMessagesMoved(const QMap<QString, QString> &originByMessageId
//
// Gated on isShowingTrash() and not on the destination: a Delete is a move
// too and reaches this same slot, and refreshing after every delete would
- // make a row vanish from under the user in every other view.
- if (isShowingTrash())
+ // make a row vanish from under the user in every other view. The Spam view
+ // is the same case: Empty Spam is path-based, so moved rows stop matching
+ // and only a refresh can say so.
+ if (isShowingTrash() || isShowingSpam())
refreshCurrentQuery();
// The undo entries are pushed inside the loop above, one per origin
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 1fc8822..ab65f1d 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -1321,6 +1321,17 @@ private:
/// which holds whatever the current view happens to show.
void emptyTrash();
+ /// Moves every message in the spam folder to that account's trash. Unlike
+ /// emptyTrash() this is NOT a purge: it is a move, so it is undoable and
+ /// asks nothing. Scoped to the account selector like every other
+ /// account-aware surface, and grouped per account because the destination
+ /// differs: one account's spam moves to that account's own trash.
+ ///
+ /// Asynchronous for the same reason emptyTrash() is, but no confirm follows:
+ /// the answer arrives at onThreadMessagesResolved() tagged `empty_spam`,
+ /// which resolves the destinations from each message's own path.
+ void emptySpam();
+
/// Empty trash's sibling, scoped to the SELECTION rather than to the
/// account's whole trash. The act is identical, `purgeMessages()` in both
/// cases, and so are its safeguards: it confirms, and it carries no
@@ -1362,6 +1373,11 @@ private:
/// in it, and such a message carries no tag of ours.
bool isShowingTrash() const;
+ /// Whether the current query IS a spam view, for either scope. The exact
+ /// sibling of isShowingTrash(), for the same reason: Empty Spam drops rows
+ /// from a path-based view that no tag change can express.
+ bool isShowingSpam() const;
+
/// The `moved-from:` tag naming `dbRelativeFolder`, or empty when no
/// account owns it.
///