diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.cpp | 68 | ||||
| -rw-r--r-- | src/config.h | 22 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 83 | ||||
| -rw-r--r-- | src/mainwindow.h | 45 |
4 files changed, 170 insertions, 48 deletions
diff --git a/src/config.cpp b/src/config.cpp index 47fadec..a81651f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -47,37 +47,67 @@ QString Account::scopedQuery(const QString &query) const return QStringLiteral("%1 and (%2)").arg(prefix, query); } -QString Account::sentQuery() const +namespace { + +/// Composes `path:"<maildir>/<folder>/**"`, or empty when the folder is unset. +/// +/// The QUOTES are load-bearing, not decoration. A real provider nests both its +/// sent and its drafts folder under a bracketed parent with a localised name, +/// "[Provider]/Posta inviata" and "[Provider]/Bozze", and "[" and "]" are +/// Xapian syntax: unquoted, the term is parsed rather than matched and the +/// query silently returns nothing while looking correct. +/// +/// The path is user config and is interpolated into a query, so this is the +/// only place that composition happens; a caller building it by hand would be +/// a second chance to forget the quotes. +QString folderQuery(const QString &maildir, const QString &folder) { - if (sent.isEmpty()) + if (folder.isEmpty()) return QString(); - - // The QUOTES are load-bearing, not decoration. A real provider nests its - // sent folder under a bracketed parent, "[Provider]/Posta inviata", and - // "[" and "]" are Xapian syntax: unquoted, the term is parsed rather than - // matched and the query silently returns nothing while looking correct. - // - // The path is user config and is interpolated into a query, so this is the - // only place that composition happens; a caller building it by hand would - // be a second chance to forget the quotes. - return QStringLiteral("path:\"%1/%2/**\"").arg(maildir, sent); + return QStringLiteral("path:\"%1/%2/**\"").arg(maildir, folder); } -QString Config::allSentQuery() const +/// Joins the non-empty results of `extract` across `accounts` with " or ". +/// +/// Collect first, join after. Appending "or" per account and trimming the +/// result is the version that produced the defect this guards: an account with +/// no key contributes an empty term, notmuch accepts the bare "or" without +/// complaint, and the query quietly means something else. Measured against a +/// real database, `A or or B` returns 190 where the correct pair returns 211. +QString joinAccountQueries(const QList<Account> &accounts, + QString (Account::*extract)() const) { - // Collect first, join after. Appending "or" per account and trimming the - // result is the version that produced the defect this guards: an account - // with no sent key contributes an empty term, notmuch accepts the bare - // "or" without complaint, and the query quietly means something else. QStringList parts; - for (const Account &account : m_accounts) { - const QString query = account.sentQuery(); + for (const Account &account : accounts) { + const QString query = (account.*extract)(); if (!query.isEmpty()) parts.append(query); } return parts.join(QStringLiteral(" or ")); } +} // namespace + +QString Account::sentQuery() const +{ + return folderQuery(maildir, sent); +} + +QString Account::draftsQuery() const +{ + return folderQuery(maildir, drafts); +} + +QString Config::allSentQuery() const +{ + return joinAccountQueries(m_accounts, &Account::sentQuery); +} + +QString Config::allDraftsQuery() const +{ + return joinAccountQueries(m_accounts, &Account::draftsQuery); +} + QString Config::defaultPath() { const QString base = diff --git a/src/config.h b/src/config.h index e3c5b6e..4092141 100644 --- a/src/config.h +++ b/src/config.h @@ -34,7 +34,13 @@ struct Account QString name; QString address; QString maildir; ///< Relative to notmuch's database.path. - QString drafts; ///< Unused in v1; send is v2. + /// The account's drafts folder, relative to maildir. Optional, exactly as + /// `sent` is, and absent more often: an account that composes elsewhere + /// keeps no local drafts folder at all. + /// + /// Composing drafts is v2. Reading them is not: the placeholder pane + /// counts them (item 67), which is why this is no longer unused. + QString drafts; /// The account's sent folder, relative to maildir. Optional and empty for /// an account that has none, which is a real case rather than a @@ -81,6 +87,13 @@ struct Account /// selector wraps whatever query runs, so a Sent view under one account /// intersects to that account's sent mail and cannot leak another's. QString sentQuery() const; + + /// Matches this account's drafts, or empty when `drafts` is unset. + /// + /// Separate from sentQuery() rather than one parameterised helper: the two + /// keys are independent, and one real account configures `drafts` with no + /// `sent` at all. + QString draftsQuery() const; }; struct SavedQuery @@ -145,6 +158,13 @@ public: /// open-coded at the call site. QString allSentQuery() const; + /// Matches every configured account's drafts, or empty when none has one. + /// + /// Joins only the NON-EMPTY draftsQuery() results, for the same reason + /// allSentQuery() does: notmuch accepts a bare "or" without complaint and + /// silently answers a different question. + QString allDraftsQuery() const; + /// A QDateTime::toString() pattern for the date on a card, or empty for the /// system locale's short format. /// diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index be6cfdb..824bdca 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1385,40 +1385,73 @@ void MainWindow::onAllTagsReady(const QStringList &tags) m_queryCompleter->setTags(tags); } -namespace { +QList<MainWindow::PlaceholderLine> MainWindow::placeholderLines() const +{ + // One list of (query, label-maker) pairs rather than two arrays indexed in + // parallel. The parallel version is what the fixed array was, and its + // hazard is that inserting an entry in one and not the other prints a real + // number against the wrong name, which reads as a plausible pane. + // + // The queries are wire format and deliberately untranslated: `tag:` is + // notmuch syntax, not user-facing prose. Only the labels are translated. + QList<PlaceholderLine> lines = { + { QStringLiteral("tag:unread"), + [this](int n) { return tr("%n unread", "", n); } }, + { QStringLiteral("tag:flagged"), + [this](int n) { return tr("%n flagged", "", n); } }, + { QStringLiteral("tag:inbox"), + [this](int n) { return tr("%n in inbox", "", n); } }, + }; -/// The queries behind the placeholder's helper lines, in render order. -/// -/// Wire format, deliberately untranslated: `tag:` is notmuch syntax, not user -/// -facing prose. Only the labels beside them are translated. -const std::array<const char *, 3> kPlaceholderQueries = { - "tag:unread", - "tag:flagged", - "tag:inbox", -}; + // Sent and drafts are composed from the account folders, not from a tag. + // `tag:draft` counts 0 against a real database and no draft-ish tag exists + // in it, so a tag-based line would be a permanent zero. + // + // Omitted entirely when no account configures the folder, rather than + // shown as 0: item 63 established that a missing sent folder is a real + // configuration, and "0 sent" claims the user has sent nothing. + const QString sent = m_config.allSentQuery(); + if (!sent.isEmpty()) { + lines.append({ sent, [this](int n) { return tr("%n sent", "", n); } }); + } -} // namespace + const QString drafts = m_config.allDraftsQuery(); + if (!drafts.isEmpty()) { + lines.append({ drafts, + [this](int n) { return tr("%n draft(s)", "", n); } }); + } + + return lines; +} + +QStringList MainWindow::placeholderQueries() const +{ + QStringList queries; + for (const PlaceholderLine &line : placeholderLines()) + queries.append(line.query); + return queries; +} QList<HtmlBuilder::PlaceholderHelper> MainWindow::placeholderHelpers() const { QList<HtmlBuilder::PlaceholderHelper> helpers; - // Empty until the first reply lands. Rendering three zeroes meanwhile - // would be worse than rendering nothing: a zero is a claim. - if (m_placeholderCounts.size() == int(kPlaceholderQueries.size())) { - const QStringList labels = { - tr("%n unread", "", m_placeholderCounts.at(0)), - tr("%n flagged", "", m_placeholderCounts.at(1)), - tr("%n in inbox", "", m_placeholderCounts.at(2)), - }; + const QList<PlaceholderLine> lines = placeholderLines(); - for (int i = 0; i < labels.size(); ++i) { + // Empty until the first reply lands. Rendering zeroes meanwhile would be + // worse than rendering nothing: a zero is a claim. + // + // The size check is also what keeps the pairing honest across a config + // that changed shape between the request and the reply: counts that do not + // match the current line list are not this list's answers. + if (m_placeholderCounts.size() == lines.size()) { + for (int i = 0; i < lines.size(); ++i) { // A query notmuch could not count yields -1; skip that line rather // than print a negative number at the user. if (m_placeholderCounts.at(i) < 0) continue; - helpers.append({ labels.at(i), - QString::fromLatin1(kPlaceholderQueries[i]) }); + helpers.append({ lines.at(i).label(m_placeholderCounts.at(i)), + lines.at(i).query }); } } @@ -1438,12 +1471,8 @@ void MainWindow::showPlaceholderPane() { m_messageView->showPlaceholder(placeholderHelpers()); - QStringList queries; - for (const char *query : kPlaceholderQueries) - queries.append(QString::fromLatin1(query)); - QMetaObject::invokeMethod(m_worker, "requestCounts", Qt::QueuedConnection, - Q_ARG(QStringList, queries), + Q_ARG(QStringList, placeholderQueries()), Q_ARG(quint64, ++m_countsGeneration)); } diff --git a/src/mainwindow.h b/src/mainwindow.h index 6e90ba1..0912520 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -165,6 +165,25 @@ public: return !m_recoverThreadId.isEmpty(); } + /// The placeholder's queries, in the order requestCounts() asks for them. + /// + /// A test seam. The worker's reply is paired with these POSITIONALLY, so a + /// test standing in for it has to know the order, and that order now + /// depends on config rather than on a fixed list. + QStringList placeholderQueriesForTesting() const + { + return placeholderQueries(); + } + + /// The helper lines as the pane would render them. + QList<HtmlBuilder::PlaceholderHelper> placeholderHelpersForTesting() const + { + return placeholderHelpers(); + } + + /// The generation the next counts reply must carry to be accepted. + quint64 countsGenerationForTesting() const { return m_countsGeneration; } + protected: void closeEvent(QCloseEvent *event) override; @@ -310,6 +329,30 @@ private: /// The helper lines, built from the last counts received. Rendered with /// whatever the previous answer was until the new one lands, so the pane /// never flashes empty while the worker replies. + /// One placeholder line: the query it counts, and how to label the answer. + /// + /// The label is a callable rather than a string because the count is not + /// known until the worker replies, and `tr("%n ...")` has to be given the + /// number to pick its plural form. + /// + /// Query and label travel together deliberately. The version this replaced + /// held them in two arrays indexed in parallel, where inserting an entry in + /// one and not the other put a real number against the wrong name. + struct PlaceholderLine { + QString query; + std::function<QString(int)> label; + }; + + /// The placeholder's lines, in render order. + /// + /// Built per call rather than cached: the sent and drafts lines come from + /// config, and a cache would be a second source of truth for the pairing + /// the counts reply depends on. + QList<PlaceholderLine> placeholderLines() const; + + /// Just the queries, in the order requestCounts() asks for them. + QStringList placeholderQueries() const; + QList<HtmlBuilder::PlaceholderHelper> placeholderHelpers() const; void showWarnings(); @@ -552,7 +595,7 @@ private: /// Indeterminate, shown only while a sync runs. See setSyncBusy(). QProgressBar *m_syncProgress = nullptr; - /// The last counts the worker answered, one per kPlaceholderQueries entry. + /// The last counts the worker answered, one per placeholderLines() entry. /// Empty until the first reply, which renders the pane without its helper /// lines rather than with three zeroes that would be a lie. QVector<int> m_placeholderCounts; |
