summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.cpp17
-rw-r--r--src/config.h15
-rw-r--r--src/mainwindow.cpp82
-rw-r--r--src/mainwindow.h19
4 files changed, 132 insertions, 1 deletions
diff --git a/src/config.cpp b/src/config.cpp
index a81651f..b1f730c 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -271,6 +271,23 @@ void Config::load(const QString &path)
}
}
+ // Item 71. Same shape as mark_read_delay_ms above, including that zero and
+ // negative are not errors: 0 syncs on the next trip through the event loop
+ // and negative disables the automatic sync entirely, which is how a user who
+ // wants only their cron job turns this off.
+ const QVariant autoSync = settings.value(QStringLiteral("auto_sync_delay_ms"));
+ if (autoSync.isValid()) {
+ bool ok = false;
+ const int value = autoSync.toString().toInt(&ok);
+ if (ok) {
+ m_autoSyncDelayMs = value;
+ } else {
+ addProblem(QStringLiteral("Auto-sync delay '%1' is not a number; "
+ "using the default.")
+ .arg(autoSync.toString()));
+ }
+ }
+
// [completion] is an ordinary section, so this one DOES take its prefix.
// ',' separates entries and '|' separates a value from its description:
// two different characters because QSettings splits comma lists itself,
diff --git a/src/config.h b/src/config.h
index 4092141..ea0b055 100644
--- a/src/config.h
+++ b/src/config.h
@@ -213,6 +213,20 @@ public:
/// the behaviour so a thread stays unread until toggled by hand.
int markReadDelayMs() const { return m_markReadDelayMs; }
+ /// How long to wait after a tag edit before syncing it out on the user's
+ /// behalf. Item 71.
+ ///
+ /// Same three meanings as markReadDelayMs() above, and deliberately so: a
+ /// positive value is the debounce in milliseconds, 0 syncs on the next trip
+ /// through the event loop, and any negative value disables the behaviour so
+ /// edits wait for a manual sync or the user's cron job, which is what every
+ /// release before this one did.
+ ///
+ /// Defaults to 2000. The delay is a debounce, not a schedule: each edit
+ /// restarts it, so a burst of tagging produces one sync after the burst
+ /// rather than one per tag.
+ int autoSyncDelayMs() const { return m_autoSyncDelayMs; }
+
/// User-supplied mimetype completions, APPENDED to the built-in list.
/// Appending rather than replacing means a typo cannot leave completion
/// worse off than the defaults. Mimetypes are the only completion list a
@@ -250,6 +264,7 @@ private:
qreal m_messageZoom = 1.0;
bool m_completionOnFocus = false;
int m_markReadDelayMs = 2000;
+ int m_autoSyncDelayMs = 2000;
SyncOnExit m_syncOnExit = SyncOnExit::Ask;
QList<CompletionEntry> m_extraMimetypes;
QString m_startupQuery = QStringLiteral("Unread");
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 824bdca..04e951d 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -485,6 +485,13 @@ void MainWindow::buildUi()
connect(m_markReadTimer, &QTimer::timeout,
this, &MainWindow::markCurrentThreadRead);
+ m_autoSyncTimer = new QTimer(this);
+ // Named for the same reason: a test can assert that an edit armed the
+ // debounce without waiting out the delay or starting a real mbsync.
+ m_autoSyncTimer->setObjectName(QStringLiteral("autoSyncTimer"));
+ m_autoSyncTimer->setSingleShot(true);
+ connect(m_autoSyncTimer, &QTimer::timeout, this, &MainWindow::runAutoSync);
+
// The pane and its close button travel together: a QPlainTextEdit has
// nowhere to put one, and a pane that appears on a failed sync and can
// never be dismissed is worse than one that does not appear at all.
@@ -2194,7 +2201,22 @@ void MainWindow::onSyncFinished(bool success, int exitCode)
return;
}
- runCurrentQuery();
+ // refreshCurrentQuery(), NOT runCurrentQuery(). A sync this window
+ // started is not a query the user asked to re-run: runCurrentQuery()
+ // clears the model, the undo stack and the message pane, so a sync
+ // landing while a message was open read the user out of it. The cron
+ // path has reconciled instead since item 35, and there was never a
+ // reason for the two to differ.
+ //
+ // Item 71 is what made it matter. A local sync used to happen only
+ // when the user clicked Sync, where blanking was at least explicable;
+ // the automatic one fires two seconds after a tag edit, which is
+ // precisely when the user is still reading the message they tagged.
+ // Reconciling keeps the pane, and updateStaleThreadNotice() then offers
+ // "Show it anyway" for a thread that has stopped matching the query,
+ // which is the reported case: reading in Unread, the thread is marked
+ // read, and it no longer belongs to the view it was opened from.
+ refreshCurrentQuery();
// A sync is the usual way new tags enter the database.
requestAllTags();
} else if (exitCode == kSyncSkippedExitCode) {
@@ -2280,6 +2302,12 @@ void MainWindow::onTagsApplied(const TagChange &change)
updatePendingIndicator();
+ // Item 71. Armed here, where a write is CONFIRMED and the pending count is
+ // already up to date, for the same reason recordPendingEdit() is called
+ // here: a sync scheduled for a write the worker went on to reject would run
+ // for nothing.
+ scheduleAutoSync();
+
// A tag the user has just created is the one they are most likely to type
// again, so do not wait for the next sync to offer it. A set membership
// test, not a query.
@@ -2678,6 +2706,58 @@ void MainWindow::startSync()
setSyncBusy(true);
}
+void MainWindow::scheduleAutoSync()
+{
+ // Negative disables the behaviour entirely, per the config key, and that is
+ // the pre-0.16.0 behaviour: edits wait for a manual sync or the user's cron
+ // job. Checked before anything else so a disabled delay arms nothing.
+ const int delay = m_config.autoSyncDelayMs();
+ if (delay < 0)
+ return;
+
+ // No sync command means the Sync action is already disabled and startSync()
+ // would only put "No sync command configured" in the status bar. Arming a
+ // timer to say that on a delay, for something the user did not ask for, is
+ // worse than staying quiet.
+ if (!m_sync || !m_sync->isAvailable())
+ return;
+
+ // Nothing outstanding, nothing to carry. An edit netted against its own
+ // inverse leaves the count at zero (item 28), and syncing for it would run
+ // mbsync over a mail store that is already where the server left it.
+ if (pendingEditCount() == 0)
+ return;
+
+ // Restart, not stack. Tagging a multi-row selection confirms one write per
+ // thread and "mark all read" confirms one per thread in the view, so an
+ // armed-per-edit timer would be exactly the storm of syncs a debounce is
+ // for. The last edit of a burst decides when the single sync happens.
+ m_autoSyncTimer->start(delay);
+}
+
+void MainWindow::runAutoSync()
+{
+ // The user can have synced by hand, or undone the edit, in the delay. Both
+ // leave nothing to carry, and re-checking here rather than trusting the arm
+ // is what makes the debounce safe to restart freely.
+ if (pendingEditCount() == 0)
+ return;
+
+ // Skip rather than queue when a sync is already in flight, which item 71
+ // requires: the cron job holds the same lock, and mbsync's own answer to a
+ // second run is to fail on it. The edits are not lost by skipping. They stay
+ // pending, and the sync already running is very likely to carry them, since
+ // they reached the mail store at edit time.
+ //
+ // m_externalSyncBusy covers the cron job SyncMonitor can see. A lock taken
+ // between that poll and now is not visible here, and does not need to be:
+ // MailSync::start() fails on a second run and startSync() reports it.
+ if (m_externalSyncBusy || (m_sync && m_sync->isRunning()))
+ return;
+
+ startSync();
+}
+
void MainWindow::recordPendingEdit(const QString &messageId, const QString &tag,
bool added)
{
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 0912520..19635dd 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -390,6 +390,17 @@ private:
/// Redraws the unsynced-edits indicator from pendingEditCount().
void updatePendingIndicator();
+ /// Arms the debounce that syncs a confirmed tag edit out on its own
+ /// (item 71). Does nothing when the delay is negative, when no sync command
+ /// is configured, or when nothing is actually pending.
+ void scheduleAutoSync();
+
+ /// Starts the debounced automatic sync, unless a sync is already running
+ /// (local or external) or the edits it would carry are already gone.
+ ///
+ /// Q_INVOKABLE so a test can fire the debounce without waiting it out.
+ Q_INVOKABLE void runAutoSync();
+
/// Records one confirmed (message, tag) change, cancelling it against an
/// opposite change already outstanding for the same pair.
void recordPendingEdit(const QString &messageId, const QString &tag,
@@ -756,6 +767,14 @@ private:
/// nothing rather than marking the wrong one.
QString m_markReadThreadId;
+ /// Debounces the automatic sync that follows a tag edit (item 71).
+ ///
+ /// Single-shot and RESTARTED by every confirmed edit, for the same reason
+ /// m_markReadTimer is: tagging a multi-row selection confirms one write per
+ /// thread, and one sync per thread is exactly what a debounce exists to
+ /// prevent.
+ QTimer *m_autoSyncTimer = nullptr;
+
/// The optimistic update awaiting confirmation, kept so a worker error can
/// put the model back. Only the most recent one: mutations are sent from
/// the UI thread one user action at a time.