aboutsummaryrefslogtreecommitdiffstats
path: root/src/config.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-25 09:12:29 +0200
committerDanilo M. <danix@danix.xyz>2026-08-25 09:12:29 +0200
commit0a26961f9a7ae6ab98051e182b92e64165758cf1 (patch)
tree5d76949bf34ab581826f0f699d60e0fc45b3e175 /src/config.cpp
parent8399a2652584e348ba73f7059d9e178958855897 (diff)
downloadqtmaildir-0a26961f9a7ae6ab98051e182b92e64165758cf1.tar.gz
qtmaildir-0a26961f9a7ae6ab98051e182b92e64165758cf1.zip
fix(drafts): list drafts as messages, not threadssignatures
The Drafts filter shipped threaded in item 138, reasoning that a draft reply belongs with the conversation it answers. That reasoning cost the feature: a thread row stands for its first matched message, which for a draft reply is the message being replied to, so the draft itself had no row of its own and double-clicking the conversation opened nothing. Reversed with the user. Drafts now follows Sent; Trash deliberately does not, since a deleted message still belongs to its conversation and nothing there has to be reachable for editing. The view mode was decided in three places that each compared against "sent" and had to agree: builtinFilter(), the reader that reapplies the mode, and the writer that skips storing what the generator implies. generatorIsFlat() is now the one closed set they share, and builtinFilter() sets flat from it rather than inside a branch so the set cannot drift from the labels. Setting only the branch would have looked correct. Its save/load pair survives by accident, because the writer's skip knew only "sent" and so would have stored the key for drafts. The gap is the reader's fallback, for a file carrying no flat key at all: an older build, a migration or a hand edit comes back threaded against a flat button, and the next save persists the disagreement. theDraftsFilterIsThreadedNotFlat is inverted rather than deleted, keeping its history, and now also pins Trash as threaded. The round trip is covered by extending aGeneratedEntryWritesNoRedundantKeys, which already asserted that property for Sent. Mutation-checked: reverting generatorIsFlat() to "sent" alone fails both. Suite 37 of 38; undoMovesTheMessageBack is item 136, pre-existing and on an unrelated path. Closes item 159. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8
Diffstat (limited to 'src/config.cpp')
-rw-r--r--src/config.cpp37
1 files changed, 27 insertions, 10 deletions
diff --git a/src/config.cpp b/src/config.cpp
index 534ba72..d91259a 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -80,6 +80,18 @@ QString generatorTag(const QString &generator)
return QString();
}
+/// Whether a generator lists MESSAGES rather than threads. "sent" folds a
+/// user's own message back into the conversation it answers, and "drafts" is
+/// worse: a thread row stands for its first matched message, which for a draft
+/// reply is the message being replied TO, so the draft itself is unreachable.
+/// "trash" stays threaded, since a deleted message still belongs to its
+/// conversation. Closed set, and the one place the three views are decided.
+bool generatorIsFlat(const QString &generator)
+{
+ return generator == QStringLiteral("sent")
+ || generator == QStringLiteral("drafts");
+}
+
} // namespace
QString Account::scopedQuery(const QString &query) const
@@ -838,14 +850,14 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings)
query.query = object.value(QStringLiteral("query")).toString();
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
+ // A generator carries its own view mode, so a flat one 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");
+ || generatorIsFlat(query.generated);
if (query.isGenerated()
&& !kQueryGenerators.contains(query.generated)) {
@@ -905,7 +917,7 @@ bool Config::saveSavedQueries() const
object.insert(QStringLiteral("account"), query.account);
// Skipped when the generator already implies it, which loadSavedQueries
// reapplies on the way back in.
- if (query.flat && query.generated != QStringLiteral("sent"))
+ if (query.flat && !generatorIsFlat(query.generated))
object.insert(QStringLiteral("flat"), true);
for (auto it = query.unknown.begin(); it != query.unknown.end(); ++it)
object.insert(it.key(), it.value());
@@ -987,6 +999,9 @@ SavedQuery Config::builtinFilter(const QString &generator)
SavedQuery filter;
filter.generated = generator;
+ // One source for the view mode, shared with the saved-query round trip, so
+ // a branch below cannot disagree with what loadSavedQueries reapplies.
+ filter.flat = generatorIsFlat(generator);
// Translated, because these are the labels on the buttons. The GENERATOR
// name is not: it is stored in queries.json and matched against a closed
@@ -1005,16 +1020,18 @@ SavedQuery Config::builtinFilter(const QString &generator)
filter.name = tr("Important");
} else if (generator == QStringLiteral("sent")) {
filter.name = tr("Sent");
- // Messages rather than threads, and the only filter that sets this. A
- // thread would fold the user's sent message back into the conversation
- // it belongs to, which is item 63's finding.
- filter.flat = true;
+ // Flat, per generatorIsFlat(): a thread would fold the user's sent
+ // message back into the conversation it belongs to, item 63's finding.
} else if (generator == QStringLiteral("drafts")) {
// The LABEL is translated; the generator stays `drafts`, which is what
// queries.json stores and what a closed set is matched against.
filter.name = tr("Drafts");
- // NOT flat, like Trash and unlike Sent: a draft reply belongs with the
- // conversation it answers.
+ // Flat, per generatorIsFlat(). Item 138 chose threaded, reasoning that
+ // a draft reply belongs with the conversation it answers; item 159
+ // reversed it on what that cost. A thread row stands for its first
+ // MATCHED message, which for a draft reply is the message being
+ // replied TO, so the draft itself had no row of its own and
+ // double-clicking the conversation opened nothing.
} else if (generator == QStringLiteral("trash")) {
filter.name = tr("Trash");
// NOT flat, unlike Sent. A deleted message still belongs to its