aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-06 16:55:16 +0200
committerDanilo M. <danix@danix.xyz>2026-09-06 16:55:16 +0200
commit6326030b7179580b934ba852b0fd98577bfb464a (patch)
treeba44b32f456d888ab2adc8a86d3a94db916a380f /src
parent51b8fd5708d23108d532be1cf38e4bb619eeb777 (diff)
downloadqtmaildir-6326030b7179580b934ba852b0fd98577bfb464a.tar.gz
qtmaildir-6326030b7179580b934ba852b0fd98577bfb464a.zip
fix: index the sent copy so it appears in the Sent view at once
ComposeWindow filed the sent copy into the account's sent folder and discarded the path DraftStore::write() returned, using the result only to test for failure. Nothing announced the file, so notmuch never learned it existed, and the Sent view is a path query over the index rather than a listing of the folder: the message was on disk and invisible until the next notmuch new, which here is a cron tick up to ten minutes away. Measured immediately after a send: 65 files in the account's Sent folder, 64 messages indexed for the same path. This is item 158's defect one path over. That item established the rule for drafts, and MainWindow wires both halves of it, so the send path was already telling the worker to DROP the draft's entry while never telling it to add the sent copy's. The missing half is the one the user sees. A sentCopyFiled signal, emitted only where the write succeeded, connected to the worker's existing indexDraftFile. That slot is generic despite its name: it calls notmuch_database_index_file, applies nothing draft-specific, and its previousPath already defaults to empty, which is right for a copy that replaces nothing. The connection is a lambda whose context object is m_worker, and that is load-bearing. indexDraftFile takes two arguments where the signal carries one, so a direct slot connection does not compile; the context object is what queues the call onto the worker's thread and keeps notmuch off the GUI thread. Simplifying it to a plain call reads as tidier and would cross that boundary, so the comment says so. The test asserts on the signal, on the file existing, and on it being inside the Sent folder. Indexing itself is already covered against a real database in test_notmuchworker; what was unproven was that anything ever called it for a sent copy. Announcing a path that was never written is the ghost entry removeIndexedFile exists to undo, which is why the existence check is there. Indexing is not repainting: a Sent view already on screen does not gain the row from this, and whether it should refresh after a send is left as a separate question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jq9gXquUo9W4KXDagJXMmn
Diffstat (limited to 'src')
-rw-r--r--src/composewindow.cpp8
-rw-r--r--src/composewindow.h14
-rw-r--r--src/mainwindow.cpp18
3 files changed, 40 insertions, 0 deletions
diff --git a/src/composewindow.cpp b/src/composewindow.cpp
index 218edef..59228ba 100644
--- a/src/composewindow.cpp
+++ b/src/composewindow.cpp
@@ -1616,6 +1616,14 @@ void ComposeWindow::send()
if (!filed.ok()) {
sentCopyFailed = true;
sentCopyError = filed.error;
+ } else {
+ // Item 192. The Sent view queries the INDEX, so a file
+ // notmuch has not seen is invisible there until the next
+ // sync. The path was previously discarded, which is why a
+ // message just sent did not appear: measured as 65 files
+ // against 64 indexed. Only on the success branch, since
+ // indexing a path that was never written leaves a ghost.
+ emit sentCopyFiled(filed.path);
}
}
diff --git a/src/composewindow.h b/src/composewindow.h
index 87b105a..09b47de 100644
--- a/src/composewindow.h
+++ b/src/composewindow.h
@@ -190,6 +190,20 @@ signals:
/// \p path is the file that was removed, absolute.
void draftRemoved(const QString &path);
+ /// The sent copy was filed, so it must be indexed at once.
+ ///
+ /// Item 192. The Sent view is a `path:` query over the INDEX rather than a
+ /// directory listing, so a correctly written file notmuch has never seen
+ /// is invisible there until the next `notmuch new`, which is a cron tick
+ /// away. This is item 158's rule for drafts applied to the other half of
+ /// the send: that half already emits draftRemoved() so the draft's entry
+ /// goes, while nothing added the sent copy's.
+ ///
+ /// \p path is the file just written, absolute. Emitted ONLY when the write
+ /// succeeded: announcing a path that was never written would put a ghost
+ /// in the index.
+ void sentCopyFiled(const QString &path);
+
/// A send succeeded, and the message it answers should record that.
///
/// Item 68. \p sourceMessageId is the Message-ID of the message replied to
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 6c6bc23..abc7875 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1277,6 +1277,24 @@ void MainWindow::openComposer(const ComposeContext &context)
connect(composer, &ComposeWindow::draftRemoved, m_worker,
&NotmuchWorker::removeIndexedFile);
+ // And the sent copy must be indexed at once, for the same reason a draft
+ // is (item 192): the Sent view is a path query over the index, so a file
+ // notmuch has not seen is invisible there until the next sync. This half
+ // was missing while the draft REMOVAL above was already wired.
+ //
+ // indexDraftFile despite the name: it calls notmuch_database_index_file
+ // and applies nothing draft-specific, and its previousPath defaults to
+ // empty, which is exactly right for a sent copy that replaces nothing.
+ // A lambda rather than a direct slot connection, because indexDraftFile
+ // takes two arguments and the signal carries one; a default argument does
+ // not fill the gap across a connect(). The lambda's context object is
+ // m_worker, so it RUNS ON THE WORKER'S THREAD: notmuch never touches the
+ // GUI thread, which is the boundary the whole design rests on.
+ connect(composer, &ComposeWindow::sentCopyFiled, m_worker,
+ [this](const QString &path) {
+ m_worker->indexDraftFile(path);
+ });
+
// Item 68. The R and P Maildir flags, on the message the send answered.
//
// sendMessageTagChange, NOT tagSelected: this deliberately does not go on