diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 09:13:21 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 09:13:21 +0200 |
| commit | 3e196bbaf6a5f3ebf6597e0ada1f559d24627ca6 (patch) | |
| tree | 5d76949bf34ab581826f0f699d60e0fc45b3e175 /src/config.cpp | |
| parent | f8d136432466479c892841bc73bd85e58674da86 (diff) | |
| parent | 0a26961f9a7ae6ab98051e182b92e64165758cf1 (diff) | |
| download | qtmaildir-3e196bbaf6a5f3ebf6597e0ada1f559d24627ca6.tar.gz qtmaildir-3e196bbaf6a5f3ebf6597e0ada1f559d24627ca6.zip | |
Merge branch 'signatures'
Signatures (item 152): one markdown file per signature under
~/.config/qtmaildir/signatures/, spliced into the composer buffer and
chosen from a switch on the editor bar. [compose] signature seeds a new
message, [account.<key>] signature overrides per account, and
[compose] signature_position picks end or above_quote.
Also carries three fixes found by hand-testing it: a saved draft is
indexed so it appears without a sync (item 158), the Drafts filter lists
messages rather than threads so a draft reply can be opened (item 159),
and a resumed draft no longer re-seeds its signature on a From: change.
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 | 73 |
1 files changed, 63 insertions, 10 deletions
diff --git a/src/config.cpp b/src/config.cpp index 23c7364..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 @@ -459,6 +471,17 @@ void Config::load(const QString &path) account.sent = settings.value(QStringLiteral("sent")).toString().trimmed(); + // Optional, and a STARTING value rather than a binding: the composer's + // switch keeps every signature reachable whichever account is + // selected. Left empty when absent, so the composer can tell "this + // account says nothing" from "this account says none" and fall through + // to [compose] signature itself; resolving that here would collapse + // the two. Trimmed for the same reason as sent, above: a trailing + // space would be carried into a filename lookup and match nothing, + // which is invisible in a config file. + account.signature = + settings.value(QStringLiteral("signature")).toString().trimmed(); + // Mandatory, unlike sent: Delete moves a file into this folder, so an // account without one cannot delete at all. Trimmed for the same // reason as sent, above. @@ -547,6 +570,31 @@ void Config::load(const QString &path) m_compose.sendHtml = settings.value(QStringLiteral("send_html"), true).toBool(); + // Trimmed for the same reason the account key is: it reaches a filename + // lookup, where a trailing space matches nothing invisibly. + m_compose.signature = + settings.value(QStringLiteral("signature")).toString().trimmed(); + + // The same shape as quote_position directly above: an absent key is + // silent and the struct default holds, but a PRESENT and malformed value + // is reported rather than silently accepted. value(key, default) alone + // would read "signature_position = abov" as above_quote. + const QString signaturePosition = + settings.value(QStringLiteral("signature_position"), + QStringLiteral("end")) + .toString().trimmed(); + if (signaturePosition.compare(QStringLiteral("above_quote"), + Qt::CaseInsensitive) == 0) { + m_compose.signaturePosition = Signatures::Position::AboveQuote; + } else if (signaturePosition.compare(QStringLiteral("end"), + Qt::CaseInsensitive) == 0) { + m_compose.signaturePosition = Signatures::Position::End; + } else { + addProblem(tr("[compose] signature_position '%1' is not recognised; " + "expected end or above_quote. Using end.") + .arg(signaturePosition)); + } + // Three numerics, all following the shape already established at // message_zoom, toolbar_icon_size, mark_read_delay_ms and // auto_sync_delay_ms elsewhere in this function: a QVariant, a checked @@ -802,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)) { @@ -869,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()); @@ -951,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 @@ -969,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 |
