diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-10 20:43:05 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-10 20:43:05 +0200 |
| commit | ed0e085377440a68cef63ade6dc2afd322c22df9 (patch) | |
| tree | a8b4e5631a6eb5ff56341253f3775b45e3a4c026 /src/messageview.h | |
| parent | 39a055fef99a3ce6877829753f384843b6a19177 (diff) | |
| download | qtmaildir-ed0e085377440a68cef63ade6dc2afd322c22df9.tar.gz qtmaildir-ed0e085377440a68cef63ade6dc2afd322c22df9.zip | |
feat(view): follow a background sync without a keystroke
The thread list now updates itself when a sync finishes, whether it is
empty or populated. New threads appear where the sort puts them, threads
that stopped matching leave, and threads whose state changed repaint.
Refreshing used to mean re-running the query, which cleared the model,
the selection, the message pane and the undo stack, so 0.8.0 declined to
do it on a cron timer and asked the user to press Enter instead. The
result was a list that quietly disagreed with the database: mail indexed
by cron never appeared, and an Unread view read to the end sat empty in
front of it.
ThreadListModel::reconcile() diffs a result against the current rows by
thread id instead, so a surviving thread keeps its row, its persistent
index and its loaded replies. Order comes from the result and is never
imposed here, which is what makes the sort dropdown authoritative.
The undo constraint this was sized around did not exist: no undo entry
was ever keyed on a row. ThreadTagCommand stores thread ids and
MessageTagCommand stores message ids, and applyTagChange() looks its
target up by id, so an entry already survived its rows leaving the view.
A thread read out of the current view now leaves the list, which is
correct and would otherwise strand the reader, so MessageView grows a
notice saying the open thread no longer matches, with a button that
re-queries it. Recovery lists the whole conversation, expands it, and
restores the message that was on screen rather than reopening at the
first one.
Ten defects were found building this, nine of them by hand testing:
- SyncMonitor::start() polls synchronously, so an idle lock file emits
stateChanged(Idle) from inside buildUi() and the first handler to
touch a widget segfaults before the window exists.
- QTreeView sets a current index when it takes focus with none set, and
current drives loading, so new mail opened itself and was marked read
without the user having looked at it. Selection is now required.
- The notice outlived what it described, both when the pane was blanked
and when another message replaced it.
- Retiring the "Background sync completed" message left the bar claiming
a sync was still running: silent means saying nothing new, not leaving
a stale claim on screen.
- A thread root sets both the thread id and the message id, so treating
the message id as the message-row case discarded it for the commonest
way to open a thread.
- A freshly queried root does not know its own first message until the
tree loads, so recovery selected nothing and left the pane blank.
- A user query mid-recovery had its result hijacked by the pending
selection.
- MessageView emitted the recovery signal with its own members, so a
direct connection handed MainWindow references that runCurrentQuery()
then cleared by blanking the pane. The ids went empty mid-slot and no
recovery ever ran. Every test passed against this, because reaching a
slot through invokeMethod copies its arguments.
A Qt signal argument is a reference until something copies it. Emitting
a member to a slot that can re-enter the emitter is a use-after-write,
and it presents as a wrong value rather than as a crash.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/messageview.h')
| -rw-r--r-- | src/messageview.h | 39 |
1 files changed, 39 insertions, 0 deletions
diff --git a/src/messageview.h b/src/messageview.h index 135c85d..63fd6d8 100644 --- a/src/messageview.h +++ b/src/messageview.h @@ -104,6 +104,28 @@ public: qreal zoomFactor() const; void setZoomFactor(qreal factor); + /// Shows or hides the notice saying the rendered thread no longer matches + /// the current query. + /// + /// Modelled on the remote-content bar rather than on a dialog: the message + /// stays readable underneath, and the way back is one click. Passing an + /// empty id hides it. + /// + /// The pane does not decide this for itself. It renders whatever it was + /// last given and has no idea what the thread list holds, so the window + /// tells it after a refresh. + /// `messageId` is the message on screen, empty when a whole thread is + /// rendered. It rides along so recovery can restore the reader's place + /// rather than reopening the thread at its first message. + void setStaleThread(const QString &threadId, const QString &messageId); + + /// The thread the stale notice offers to bring back, empty when hidden. + QString staleThreadId() const { return m_staleThreadId; } + + /// The message the stale notice would restore, empty when a whole thread + /// is rendered or the notice is hidden. + QString staleMessageId() const { return m_staleMessageId; } + public slots: void toggleHtml(); void loadRemoteContent(); @@ -125,6 +147,16 @@ signals: /// app" is a boundary worth keeping shut rather than arguing about. void queryRequested(const QString &query); + /// The user asked to see a thread that stopped matching the current query. + /// + /// Carries the thread id and the message that was on screen, because + /// recovering the thread alone would land the user on its first message + /// rather than the one they were reading. The window runs the query, + /// expands the thread and restores the selection; the view knows none of + /// that. + void staleThreadRecoveryRequested(const QString &threadId, + const QString &messageId); + protected: /// Turns Ctrl+wheel over the body into zoom, and Ctrl+middle-click into a /// reset. Both events are delivered to the web view's internal QQuickWidget @@ -185,6 +217,13 @@ private: QLabel *m_headerLabel = nullptr; QLabel *m_blockedLabel = nullptr; QPushButton *m_loadRemoteButton = nullptr; + + /// The stale-thread notice and the thread it offers to restore. + QWidget *m_staleBar = nullptr; + QLabel *m_staleLabel = nullptr; + QPushButton *m_staleButton = nullptr; + QString m_staleThreadId; + QString m_staleMessageId; QPushButton *m_detailsButton = nullptr; QWidget *m_attachmentBar = nullptr; TagStrip *m_tagStrip = nullptr; |
