From 72812c0ac053a1a841f3864c9e4cce5cb6dab04a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 11 Aug 2026 19:02:59 +0200 Subject: feat(placeholder): count sent mail and drafts on the blank pane Item 67. The pane counted unread, flagged and inbox from three fixed tag: queries. Sent and drafts cannot join that list as tags: tag:draft counts 0 against a real database and no draft-ish tag exists in it at all, so a tag-based line would be a permanent zero that reads as working code. Both are composed from each account's folder keys instead, the same way the Sent view already composes its query. The drafts key was parsed and documented as unused in v1. Composing drafts is still v2; counting them is not, so Account::draftsQuery() and Config::allDraftsQuery() now mirror the sent pair. The shared body moved into folderQuery() and joinAccountQueries(), so the load-bearing quoting (a provider nests both folders under a bracketed parent, and [ and ] are Xapian syntax) and the bare-"or" guard exist once rather than once per folder type. The fixed array is gone rather than extended. It held queries and labels in two lists indexed in parallel, which is a hazard that grows with the list: an entry inserted in one and not the other prints a real number against the wrong name and looks entirely plausible. placeholderLines() carries each query beside the callable that labels it, so the two cannot drift, and the count reply stays paired by position as the worker requires. A line is omitted when no account configures that folder rather than shown as 0, following item 63: a missing folder is a real configuration, and "0 sent" claims the user has sent nothing. Measured against the real config: 4 sent terms over 601 threads, 5 drafts terms over 3, the extra drafts term coming from the one account that configures drafts and no sent, which is what proves the two are collected independently. Four tests here and four in test_config, mutation-checked at three points: dropping the drafts line, an off-by-one in the label pairing, and removing the -1 guard for an uncountable query. Each mutation fails a test. --- src/mainwindow.cpp | 83 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 27 deletions(-) (limited to 'src/mainwindow.cpp') 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::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 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 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 MainWindow::placeholderHelpers() const { QList 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 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)); } -- cgit v1.2.3