aboutsummaryrefslogtreecommitdiffstats
path: root/src/config.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-13 19:52:13 +0200
committerDanilo M. <danix@danix.xyz>2026-08-13 19:52:13 +0200
commit97c81f8cad571ce9ce724ddab8269e911df05a7c (patch)
tree980f2c801f9723e74c949897de9f8a8a3241d8a3 /src/config.cpp
parent8268bb478dbdf9f23b35c5114c5d13bc98254659 (diff)
downloadqtmaildir-97c81f8cad571ce9ce724ddab8269e911df05a7c.tar.gz
qtmaildir-97c81f8cad571ce9ce724ddab8269e911df05a7c.zip
feat(queries): make Sent a saved query rather than a fixed button
The user asked whether the default queries could be unified with Sent. The answer runs the other way: Sent joins the saved queries rather than the saved queries becoming hardcoded. Inbox, Unread and Important are complete strings that depend on nothing and can never go stale, so generating them would buy nothing and would cost the four things the file just gained: reordering, unpinning, renaming and deleting. Hardcoding them would also make them undeletable, which is a regression for anyone who does not want one of them. Sent is different only in that its query CANNOT be stored: it is composed from every account's `sent` key, so a stored copy goes stale the moment a folder is renamed. That is a property of Sent, not of "default queries". Storing the GENERATOR rather than its output keeps both halves: `"generated": "sent"` still resolves from the accounts at click time, and the entry is an ordinary row that can be reordered, renamed, unpinned or removed. The row now follows one rule instead of carrying one member the user did not own. Two properties had to travel with the entry. The composed query, resolved through Config::resolvedQuery() so what lands in the bar is what actually ran; and FLAT mode, since a sent view lists messages and a threaded one folds every reply back into the conversation the user sent one message into. The sent generator implies flat rather than trusting the file to say so, because a hand-edited row would otherwise produce a threaded sent view. An unknown generator is reported but the row is KEPT: a later build may know it, and dropping it here would delete it from the file on the next save, which is the same data loss the unknown-field handling exists to prevent. A generator whose accounts configure nothing is skipped entirely, exactly as the hardcoded button was hidden rather than offering one that finds nothing. Eight new tests. The four pre-existing Sent tests reach this through migration and were left alone, which is what proves the migrated path still behaves; the new ones cover a STORED file, which is the path every launch after the first takes. Mutations: a generator resolving to nothing fails three, ignoring flat fails two, and not skipping an empty generator fails one. A rename test guards the property the change exists for, since anything keyed on the literal name "Sent" would break it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/config.cpp')
-rw-r--r--src/config.cpp66
1 files changed, 64 insertions, 2 deletions
diff --git a/src/config.cpp b/src/config.cpp
index 8cd5e56..9edba50 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -51,6 +51,14 @@ constexpr int kMaxToolbarIconSize = 64;
/// two-repo change and no hook stops tagging if it is half-deployed.
constexpr int kQueriesFormatVersion = 1;
+/// Generators a saved query may name in its `generated` field.
+///
+/// A closed set, checked on load so a typo is reported rather than producing a
+/// button that silently finds nothing. Adding one here needs no format bump:
+/// an older build keeps the row and reports it, which is why an unknown
+/// generator is a problem rather than a reason to drop the entry.
+const QStringList kQueryGenerators = { QStringLiteral("sent") };
+
} // namespace
QString Account::scopedQuery(const QString &query) const
@@ -457,9 +465,26 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings)
}
settings.endGroup();
+ // Sent was a hardcoded button beside the saved queries and becomes an
+ // ordinary row here, so it can be reordered, renamed, unpinned or
+ // removed like any other. It stays GENERATED, so it still follows the
+ // accounts. Appended last, where the button already sat.
+ //
+ // Only when an account actually configures a sent folder: the button
+ // was hidden entirely otherwise, and migrating a row that always finds
+ // nothing would be worse than what it replaces.
+ if (!allSentQuery().isEmpty()) {
+ SavedQuery sent;
+ sent.name = QStringLiteral("Sent");
+ sent.generated = QStringLiteral("sent");
+ sent.pinned = true;
+ sent.flat = true;
+ m_savedQueries.append(sent);
+ }
+
// Order is alphabetical here because childKeys() is genuinely all the
// INI knows. The user reorders once and it sticks from then on.
- if (!names.isEmpty() && !saveSavedQueries()) {
+ if (!m_savedQueries.isEmpty() && !saveSavedQueries()) {
addProblem(QStringLiteral("Could not write saved queries to %1.")
.arg(m_queriesPath));
}
@@ -516,6 +541,26 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings)
query.query = object.value(QStringLiteral("query")).toString();
query.pinned = object.value(QStringLiteral("pinned")).toBool(false);
query.account = object.value(QStringLiteral("account")).toString();
+ query.generated = object.value(QStringLiteral("generated")).toString();
+ // A generator carries its own view mode, so "sent" is flat whether or
+ // not the file says so. Storing it as a plain field would let a
+ // hand-edited or migrated-from-elsewhere row produce a THREADED sent
+ // view, which folds every reply back into the conversation the user
+ // sent one message into. The file may still set it for an ordinary
+ // query.
+ query.flat = object.value(QStringLiteral("flat")).toBool(false)
+ || query.generated == QStringLiteral("sent");
+
+ if (query.isGenerated()
+ && !kQueryGenerators.contains(query.generated)) {
+ // Reported but KEPT. A later build may know this generator, and
+ // dropping the row here would delete it from the file on the next
+ // save, which is the same data loss the unknown-field handling
+ // exists to prevent.
+ addProblem(QStringLiteral("Saved query '%1' uses an unknown "
+ "generator '%2' and will find nothing.")
+ .arg(query.name, query.generated));
+ }
if (query.name.isEmpty()) {
addProblem(QStringLiteral("A saved query in %1 has no name and was "
@@ -526,7 +571,8 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings)
for (auto it = object.begin(); it != object.end(); ++it) {
static const QStringList known = {
QStringLiteral("name"), QStringLiteral("query"),
- QStringLiteral("pinned"), QStringLiteral("account")
+ QStringLiteral("pinned"), QStringLiteral("account"),
+ QStringLiteral("generated"), QStringLiteral("flat")
};
if (!known.contains(it.key()))
query.unknown.insert(it.key(), it.value());
@@ -550,6 +596,10 @@ bool Config::saveSavedQueries() const
object.insert(QStringLiteral("pinned"), true);
if (!query.account.isEmpty())
object.insert(QStringLiteral("account"), query.account);
+ if (query.isGenerated())
+ object.insert(QStringLiteral("generated"), query.generated);
+ if (query.flat)
+ object.insert(QStringLiteral("flat"), true);
for (auto it = query.unknown.begin(); it != query.unknown.end(); ++it)
object.insert(it.key(), it.value());
array.append(object);
@@ -572,6 +622,18 @@ bool Config::saveSavedQueries() const
QString Config::resolvedQuery(const SavedQuery &query) const
{
+ // Composed from the accounts every time it is asked for, which is the
+ // point: the answer follows the config rather than a copy of it taken when
+ // the entry was written.
+ if (query.isGenerated()) {
+ if (query.generated == QStringLiteral("sent"))
+ return allSentQuery();
+ // An unknown generator was reported on load. Empty rather than the
+ // bare stored query, which for a generated entry is empty anyway and
+ // would otherwise run as "match everything".
+ return QString();
+ }
+
if (query.account.isEmpty())
return query.query;