Stop grid jumping: event-driven refresh instead of 5-second timer

This commit is contained in:
2026-04-12 09:19:47 +01:00
parent 4d8342b658
commit 27c6d12183
3 changed files with 100 additions and 14 deletions

View File

@@ -1,5 +1,6 @@
namespace SpamGuard.Services;
using System.Text.Json;
using SpamGuard.Models;
public sealed class ActivityLog
@@ -7,13 +8,21 @@ public sealed class ActivityLog
private readonly List<ActivityEntry> _entries = new();
private readonly object _lock = new();
private readonly int _maxEntries;
private readonly string? _filePath;
public event Action? EntryChanged;
public int TodayChecked => GetTodayCount(_ => true);
public int TodaySpam => GetTodayCount(e => e.Verdict == Verdict.Spam);
public ActivityLog(int maxEntries = 500)
public ActivityLog(int maxEntries = 500, string? dataDirectory = null)
{
_maxEntries = maxEntries;
if (dataDirectory != null)
{
_filePath = Path.Combine(dataDirectory, "activity-log.json");
LoadFromDisk();
}
}
public void Add(ActivityEntry entry)
@@ -24,6 +33,8 @@ public sealed class ActivityLog
if (_entries.Count > _maxEntries)
_entries.RemoveAt(0);
}
SaveToDisk();
EntryChanged?.Invoke();
}
public List<ActivityEntry> GetRecent(int count = 100)
@@ -37,6 +48,29 @@ public sealed class ActivityLog
}
}
public ActivityEntry? GetByIndex(int displayIndex, int displayCount)
{
lock (_lock)
{
var ordered = _entries
.OrderByDescending(e => e.Timestamp)
.Take(displayCount)
.ToList();
return displayIndex >= 0 && displayIndex < ordered.Count ? ordered[displayIndex] : null;
}
}
public void UpdateEntry(ActivityEntry entry, Verdict newVerdict, UserOverride userOverride)
{
lock (_lock)
{
entry.Verdict = newVerdict;
entry.Override = userOverride;
}
SaveToDisk();
EntryChanged?.Invoke();
}
private int GetTodayCount(Func<ActivityEntry, bool> predicate)
{
var today = DateTime.UtcNow.Date;
@@ -45,4 +79,39 @@ public sealed class ActivityLog
return _entries.Count(e => e.Timestamp.Date == today && predicate(e));
}
}
private void SaveToDisk()
{
if (_filePath == null) return;
lock (_lock)
{
var json = JsonSerializer.Serialize(_entries, new JsonSerializerOptions { WriteIndented = true });
var tempPath = _filePath + ".tmp";
File.WriteAllText(tempPath, json);
File.Move(tempPath, _filePath, overwrite: true);
}
}
private void LoadFromDisk()
{
if (_filePath == null || !File.Exists(_filePath)) return;
try
{
var json = File.ReadAllText(_filePath);
var entries = JsonSerializer.Deserialize<List<ActivityEntry>>(json);
if (entries != null)
{
lock (_lock)
{
_entries.AddRange(entries);
while (_entries.Count > _maxEntries)
_entries.RemoveAt(0);
}
}
}
catch
{
// Corrupted file -- start fresh
}
}
}

View File

@@ -125,6 +125,9 @@ public sealed class ActivityLogForm : Form
Controls.Add(_grid);
Controls.Add(_toolbar);
// Subscribe to data changes — refresh only when something actually changes
_activityLog.EntryChanged += RefreshData;
RefreshData();
}
@@ -162,7 +165,6 @@ public sealed class ActivityLogForm : Form
await monitor.MoveToSpamAsync(entry.Uid);
RecordOverride(entry, "spam");
_activityLog.UpdateEntry(entry, Verdict.Spam, UserOverride.MarkedSpam);
RefreshData();
}
catch (Exception ex)
{
@@ -186,7 +188,6 @@ public sealed class ActivityLogForm : Form
_trustedSenders.Save();
RecordOverride(entry, "legitimate");
_activityLog.UpdateEntry(entry, Verdict.Legitimate, UserOverride.MarkedLegitimate);
RefreshData();
}
catch (Exception ex)
{
@@ -290,7 +291,7 @@ public sealed class ActivityLogForm : Form
);
var row = _grid.Rows[rowIndex];
row.Tag = entry; // attach entry for reliable retrieval
row.Tag = entry;
row.DefaultCellStyle.ForeColor = entry.Verdict switch
{
@@ -308,7 +309,6 @@ public sealed class ActivityLogForm : Form
System.Drawing.FontStyle.Bold);
}
// Match by Uid (preferred) or timestamp fallback for Uid==0 entries
if (restoreIndex < 0)
{
if (selectedUid != 0 && entry.Uid == selectedUid)
@@ -345,4 +345,11 @@ public sealed class ActivityLogForm : Form
base.OnFormClosing(e);
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
_activityLog.EntryChanged -= RefreshData;
base.Dispose(disposing);
}
}

View File

@@ -4,22 +4,31 @@ namespace SpamGuard.Tray;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SpamGuard.Services;
using SpamGuard.State;
public sealed class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _notifyIcon;
private readonly System.Windows.Forms.Timer _refreshTimer;
private readonly ActivityLog _activityLog;
private readonly InboxMonitorService _monitor;
private readonly IReadOnlyList<InboxMonitorService> _monitors;
private readonly TrustedSenderStore _trustedSenders;
private readonly BlockedDomainStore _blockedDomains;
private readonly OverrideStore _overrideStore;
private readonly IHost _host;
private ActivityLogForm? _logForm;
private readonly ToolStripMenuItem _pauseMenuItem;
private bool AllPaused => _monitors.All(m => m.IsPaused);
public TrayApplicationContext(IHost host)
{
_host = host;
_activityLog = host.Services.GetRequiredService<ActivityLog>();
_monitor = host.Services.GetRequiredService<InboxMonitorService>();
_monitors = host.Services.GetServices<InboxMonitorService>().ToList();
_trustedSenders = host.Services.GetRequiredService<TrustedSenderStore>();
_blockedDomains = host.Services.GetRequiredService<BlockedDomainStore>();
_overrideStore = host.Services.GetRequiredService<OverrideStore>();
_pauseMenuItem = new ToolStripMenuItem("Pause", null, OnPauseResume);
@@ -39,6 +48,7 @@ public sealed class TrayApplicationContext : ApplicationContext
_notifyIcon.DoubleClick += OnViewLog;
// Timer only updates tray tooltip and icon — form refresh is event-driven
_refreshTimer = new System.Windows.Forms.Timer { Interval = 5000 };
_refreshTimer.Tick += OnRefreshTick;
_refreshTimer.Start();
@@ -51,8 +61,7 @@ public sealed class TrayApplicationContext : ApplicationContext
var tooltip = $"SpamGuard - {checked_} checked, {spam} spam caught today";
_notifyIcon.Text = tooltip.Length > 127 ? tooltip[..127] : tooltip;
// Update icon based on state
if (!_monitor.IsPaused)
if (!AllPaused)
{
var recent = _activityLog.GetRecent(1);
var hasRecentError = recent.Count > 0
@@ -61,14 +70,15 @@ public sealed class TrayApplicationContext : ApplicationContext
_notifyIcon.Icon = hasRecentError ? IconGenerator.Red : IconGenerator.Green;
}
_logForm?.RefreshData();
// Form refresh removed — ActivityLogForm subscribes to ActivityLog.EntryChanged directly
}
private void OnViewLog(object? sender, EventArgs e)
{
if (_logForm == null || _logForm.IsDisposed)
{
_logForm = new ActivityLogForm(_activityLog);
_logForm = new ActivityLogForm(
_activityLog, _monitors, _trustedSenders, _blockedDomains, _overrideStore);
}
_logForm.Show();
_logForm.BringToFront();
@@ -76,15 +86,15 @@ public sealed class TrayApplicationContext : ApplicationContext
private void OnPauseResume(object? sender, EventArgs e)
{
if (_monitor.IsPaused)
if (AllPaused)
{
_monitor.Resume();
foreach (var m in _monitors) m.Resume();
_pauseMenuItem.Text = "Pause";
_notifyIcon.Icon = IconGenerator.Green;
}
else
{
_monitor.Pause();
foreach (var m in _monitors) m.Pause();
_pauseMenuItem.Text = "Resume";
_notifyIcon.Icon = IconGenerator.Yellow;
}