diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 09:12:29 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 09:12:29 +0200 |
| commit | 0a26961f9a7ae6ab98051e182b92e64165758cf1 (patch) | |
| tree | 5d76949bf34ab581826f0f699d60e0fc45b3e175 /src/config.cpp | |
| parent | 8399a2652584e348ba73f7059d9e178958855897 (diff) | |
| download | qtmaildir-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.cpp | 37 |
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 |
