Stop grid jumping: event-driven refresh instead of 5-second timer
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user