First sync paints mail as it arrives. Meaning search DirectML prefers a discrete GPU. Trash retention, logs, and folder menus match typical desktop mail apps.
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index fac645a..f9bddac 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -3,7 +3,7 @@
latest
enable
enable
- 0.3.3
+ 0.3.4
Postclient
Postclient
Maksym Sadovnychyy
diff --git a/src/MaksIT.PostClient.Client/ISemanticSearchService.cs b/src/MaksIT.PostClient.Client/ISemanticSearchService.cs
index d522fa7..5752707 100644
--- a/src/MaksIT.PostClient.Client/ISemanticSearchService.cs
+++ b/src/MaksIT.PostClient.Client/ISemanticSearchService.cs
@@ -19,6 +19,8 @@ public interface ISemanticSearchService : IDisposable {
void NotifySettingsChanged();
+ void Wake();
+
float[]? EmbedQuery(string query);
ArchiveIndexStats IndexStats();
diff --git a/src/MaksIT.PostClient.Client/SemanticSearchService.cs b/src/MaksIT.PostClient.Client/SemanticSearchService.cs
index bebb58b..e28c0c7 100644
--- a/src/MaksIT.PostClient.Client/SemanticSearchService.cs
+++ b/src/MaksIT.PostClient.Client/SemanticSearchService.cs
@@ -17,6 +17,7 @@ public sealed class SemanticSearchService : ISemanticSearchService {
private bool _gpuFailed;
private string _status = "";
private bool _ready;
+ private long _statusTick;
public SemanticSearchService(ConfigurationFileService files, MailArchiveCatalog archive) {
_files = files;
@@ -51,6 +52,10 @@ public sealed class SemanticSearchService : ISemanticSearchService {
public void NotifySettingsChanged() {
_step?.Cancel();
+ Wake();
+ }
+
+ public void Wake() {
try {
_wake.Release();
}
@@ -189,6 +194,9 @@ public sealed class SemanticSearchService : ISemanticSearchService {
var pending = _archive.PendingEmbeddings(EmbeddingModelSpec.Id, 8, ready);
if (pending.Count == 0) {
+ await DelayAsync(TimeSpan.FromMilliseconds(800), token).ConfigureAwait(false);
+ if (_archive.PendingEmbeddings(EmbeddingModelSpec.Id, 1, ready).Count > 0)
+ continue;
SetStatus("", ready: true);
await WaitAsync(token).ConfigureAwait(false);
return;
@@ -307,11 +315,20 @@ public sealed class SemanticSearchService : ISemanticSearchService {
private void SetStatus(string line, bool ready) {
var nextReady = ready && _embedder is not null;
+ var now = Environment.TickCount64;
lock (_gate) {
if (_status == line && _ready == nextReady)
return;
+ var hide = string.IsNullOrWhiteSpace(line);
+ if (!hide && !string.IsNullOrWhiteSpace(_status) && now - _statusTick < 250) {
+ _status = line;
+ _ready = nextReady;
+ return;
+ }
+
_status = line;
_ready = nextReady;
+ _statusTick = now;
}
Changed?.Invoke();
diff --git a/src/MaksIT.PostClient.UI/ViewModels/MainViewModel.cs b/src/MaksIT.PostClient.UI/ViewModels/MainViewModel.cs
index bf35dd8..6ee5231 100644
--- a/src/MaksIT.PostClient.UI/ViewModels/MainViewModel.cs
+++ b/src/MaksIT.PostClient.UI/ViewModels/MainViewModel.cs
@@ -39,6 +39,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
private const int IndexCatalogChunk = 2_000;
private const int IndexUiChunk = 250;
private int _indexBodyTotal;
+ private readonly StatusLineHold _indexHold;
+ private readonly StatusLineHold _semanticHold;
private bool _uiReady;
private MailMessageBody? _reading;
private MessageRowViewModel? _pendingWindow;
@@ -455,6 +457,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
_worker = worker;
_semantic = semantic;
_updates = updates;
+ _indexHold = new StatusLineHold(line => IndexLine = line);
+ _semanticHold = new StatusLineHold(line => SemanticLine = line);
files.Current.EnsureDefaults();
PstStoreMigrator.Migrate(files, archive);
MailArchiveCatalog.MigrateLegacy(files.Current.Mailboxes);
@@ -512,6 +516,8 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
_semantic.Changed -= OnSemanticChanged;
_semantic.Faulted -= OnSemanticFaulted;
_semantic.Dispose();
+ _indexHold.Dispose();
+ _semanticHold.Dispose();
List sessions;
lock (_connections) {
sessions = [.. _connections.Values];
@@ -533,8 +539,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
var line = _semantic.StatusLine;
if (line.Contains("ready", StringComparison.OrdinalIgnoreCase))
line = "";
- if (SemanticLine != line)
- SemanticLine = line;
+ _semanticHold.Set(line);
});
[RelayCommand]
@@ -1862,7 +1867,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
? _archive.RemoveFolder(mailboxId, folder)
: _archive.RemoveUids(mailboxId, folder, uids));
if (IsInitialSyncComplete(mailboxId))
- _semantic.NotifySettingsChanged();
+ _semantic.Wake();
}
private static void DeleteEmlFiles(IEnumerable paths) {
@@ -3172,7 +3177,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
}
if (ready)
- _semantic.NotifySettingsChanged();
+ _semantic.Wake();
}
private async Task LoadMessagesAsync(string folder, CancellationToken token, bool resumeBodies = false) {
@@ -3684,7 +3689,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
MailArchiveMap.BodyText(body, UnwrapEnvelope),
MailArchiveMap.AttachmentIndex(body, UnwrapEnvelope));
if (IsInitialSyncComplete(mailboxId))
- _semantic.NotifySettingsChanged();
+ _semantic.Wake();
}
catch {
}
@@ -3747,7 +3752,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
_backfill?.Cancel();
_backfill?.Dispose();
_backfill = null;
- IndexLine = "";
+ _indexHold.ClearNow();
}
private void EnsureIndexing(string mailboxId) {
@@ -3777,7 +3782,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
_backfill?.Cancel();
_backfill?.Dispose();
_backfill = null;
- IndexLine = "";
+ _indexHold.ClearNow();
}
private void StartIndexing() {
@@ -3858,7 +3863,6 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
if (pending.Count == 0) {
if (!cataloged)
break;
- await SetIndexLineAsync("", token).ConfigureAwait(false);
continue;
}
@@ -3887,8 +3891,10 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
}
}
- if (fetched == 0 && !cataloged)
- break;
+ if (fetched == 0 && !cataloged) {
+ await Task.Delay(500, token).ConfigureAwait(false);
+ continue;
+ }
}
}
catch (OperationCanceledException) {
@@ -3907,10 +3913,14 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
_indexMailboxes.Remove(id);
}
- IndexLine = "";
- _indexBodyTotal = 0;
- if (_indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true }))
- StartIndexing();
+ var again = _indexMailboxes.Any(id => SessionFor(MailboxById(id)) is { IsConnected: true });
+ if (!again) {
+ _indexHold.Set("");
+ _indexBodyTotal = 0;
+ return;
+ }
+
+ StartIndexing();
});
}
}
@@ -3949,7 +3959,7 @@ public partial class MainViewModel : ObservableObject, IAsyncDisposable {
private async Task SetIndexLineAsync(string line, CancellationToken token) =>
await UiAsync(() => {
if (!token.IsCancellationRequested)
- IndexLine = line;
+ _indexHold.Set(line);
}).ConfigureAwait(false);
private MailboxAccount? MailboxById(string mailboxId) =>
diff --git a/src/MaksIT.PostClient.UI/ViewModels/StatusLineHold.cs b/src/MaksIT.PostClient.UI/ViewModels/StatusLineHold.cs
new file mode 100644
index 0000000..c17da19
--- /dev/null
+++ b/src/MaksIT.PostClient.UI/ViewModels/StatusLineHold.cs
@@ -0,0 +1,104 @@
+using Avalonia.Threading;
+
+
+namespace MaksIT.PostClient.UI.ViewModels;
+
+
+internal sealed class StatusLineHold {
+ private readonly Action _apply;
+ private readonly int _minShowMs;
+ private readonly int _hideMs;
+ private string _wanted = "";
+ private string _shown = "";
+ private long _shownAt;
+ private int _gen;
+ private CancellationTokenSource? _delay;
+
+ public StatusLineHold(Action apply, int minShowMs = 250, int hideMs = 800) {
+ _apply = apply;
+ _minShowMs = minShowMs;
+ _hideMs = hideMs;
+ }
+
+ public void Set(string line) {
+ _wanted = line ?? "";
+ CancelDelay();
+ if (!string.IsNullOrWhiteSpace(_wanted)) {
+ _gen++;
+ var now = Environment.TickCount64;
+ if (_shown == _wanted)
+ return;
+ if (_shown.Length > 0 && now - _shownAt < _minShowMs) {
+ Start(FlushLater);
+ return;
+ }
+
+ Show(_wanted);
+ return;
+ }
+
+ var gen = ++_gen;
+ Start(token => HideLater(gen, token));
+ }
+
+ public void ClearNow() {
+ CancelDelay();
+ _gen++;
+ _wanted = "";
+ Show("");
+ }
+
+ public void Dispose() =>
+ CancelDelay();
+
+ private void Start(Func work) {
+ var delay = new CancellationTokenSource();
+ _delay = delay;
+ _ = work(delay.Token);
+ }
+
+ private async Task FlushLater(CancellationToken token) {
+ var wait = _minShowMs - (int)(Environment.TickCount64 - _shownAt);
+ if (wait < 0)
+ wait = 0;
+ try {
+ await Task.Delay(wait, token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) {
+ return;
+ }
+
+ await Dispatcher.UIThread.InvokeAsync(() => {
+ if (!string.IsNullOrWhiteSpace(_wanted) && _wanted != _shown)
+ Show(_wanted);
+ });
+ }
+
+ private async Task HideLater(int gen, CancellationToken token) {
+ try {
+ await Task.Delay(_hideMs, token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) {
+ return;
+ }
+
+ await Dispatcher.UIThread.InvokeAsync(() => {
+ if (gen != _gen)
+ return;
+ if (string.IsNullOrWhiteSpace(_wanted))
+ Show("");
+ });
+ }
+
+ private void Show(string line) {
+ _shown = line;
+ _shownAt = Environment.TickCount64;
+ _apply(line);
+ }
+
+ private void CancelDelay() {
+ _delay?.Cancel();
+ _delay?.Dispose();
+ _delay = null;
+ }
+}