Stop grid jumping: event-driven refresh instead of 5-second timer
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
namespace SpamGuard.Services;
|
namespace SpamGuard.Services;
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
using SpamGuard.Models;
|
using SpamGuard.Models;
|
||||||
|
|
||||||
public sealed class ActivityLog
|
public sealed class ActivityLog
|
||||||
@@ -7,13 +8,21 @@ public sealed class ActivityLog
|
|||||||
private readonly List<ActivityEntry> _entries = new();
|
private readonly List<ActivityEntry> _entries = new();
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
private readonly int _maxEntries;
|
private readonly int _maxEntries;
|
||||||
|
private readonly string? _filePath;
|
||||||
|
|
||||||
|
public event Action? EntryChanged;
|
||||||
|
|
||||||
public int TodayChecked => GetTodayCount(_ => true);
|
public int TodayChecked => GetTodayCount(_ => true);
|
||||||
public int TodaySpam => GetTodayCount(e => e.Verdict == Verdict.Spam);
|
public int TodaySpam => GetTodayCount(e => e.Verdict == Verdict.Spam);
|
||||||
|
|
||||||
public ActivityLog(int maxEntries = 500)
|
public ActivityLog(int maxEntries = 500, string? dataDirectory = null)
|
||||||
{
|
{
|
||||||
_maxEntries = maxEntries;
|
_maxEntries = maxEntries;
|
||||||
|
if (dataDirectory != null)
|
||||||
|
{
|
||||||
|
_filePath = Path.Combine(dataDirectory, "activity-log.json");
|
||||||
|
LoadFromDisk();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Add(ActivityEntry entry)
|
public void Add(ActivityEntry entry)
|
||||||
@@ -24,6 +33,8 @@ public sealed class ActivityLog
|
|||||||
if (_entries.Count > _maxEntries)
|
if (_entries.Count > _maxEntries)
|
||||||
_entries.RemoveAt(0);
|
_entries.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
SaveToDisk();
|
||||||
|
EntryChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ActivityEntry> GetRecent(int count = 100)
|
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)
|
private int GetTodayCount(Func<ActivityEntry, bool> predicate)
|
||||||
{
|
{
|
||||||
var today = DateTime.UtcNow.Date;
|
var today = DateTime.UtcNow.Date;
|
||||||
@@ -45,4 +79,39 @@ public sealed class ActivityLog
|
|||||||
return _entries.Count(e => e.Timestamp.Date == today && predicate(e));
|
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(_grid);
|
||||||
Controls.Add(_toolbar);
|
Controls.Add(_toolbar);
|
||||||
|
|
||||||
|
// Subscribe to data changes — refresh only when something actually changes
|
||||||
|
_activityLog.EntryChanged += RefreshData;
|
||||||
|
|
||||||
RefreshData();
|
RefreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +165,6 @@ public sealed class ActivityLogForm : Form
|
|||||||
await monitor.MoveToSpamAsync(entry.Uid);
|
await monitor.MoveToSpamAsync(entry.Uid);
|
||||||
RecordOverride(entry, "spam");
|
RecordOverride(entry, "spam");
|
||||||
_activityLog.UpdateEntry(entry, Verdict.Spam, UserOverride.MarkedSpam);
|
_activityLog.UpdateEntry(entry, Verdict.Spam, UserOverride.MarkedSpam);
|
||||||
RefreshData();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -186,7 +188,6 @@ public sealed class ActivityLogForm : Form
|
|||||||
_trustedSenders.Save();
|
_trustedSenders.Save();
|
||||||
RecordOverride(entry, "legitimate");
|
RecordOverride(entry, "legitimate");
|
||||||
_activityLog.UpdateEntry(entry, Verdict.Legitimate, UserOverride.MarkedLegitimate);
|
_activityLog.UpdateEntry(entry, Verdict.Legitimate, UserOverride.MarkedLegitimate);
|
||||||
RefreshData();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -290,7 +291,7 @@ public sealed class ActivityLogForm : Form
|
|||||||
);
|
);
|
||||||
|
|
||||||
var row = _grid.Rows[rowIndex];
|
var row = _grid.Rows[rowIndex];
|
||||||
row.Tag = entry; // attach entry for reliable retrieval
|
row.Tag = entry;
|
||||||
|
|
||||||
row.DefaultCellStyle.ForeColor = entry.Verdict switch
|
row.DefaultCellStyle.ForeColor = entry.Verdict switch
|
||||||
{
|
{
|
||||||
@@ -308,7 +309,6 @@ public sealed class ActivityLogForm : Form
|
|||||||
System.Drawing.FontStyle.Bold);
|
System.Drawing.FontStyle.Bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match by Uid (preferred) or timestamp fallback for Uid==0 entries
|
|
||||||
if (restoreIndex < 0)
|
if (restoreIndex < 0)
|
||||||
{
|
{
|
||||||
if (selectedUid != 0 && entry.Uid == selectedUid)
|
if (selectedUid != 0 && entry.Uid == selectedUid)
|
||||||
@@ -345,4 +345,11 @@ public sealed class ActivityLogForm : Form
|
|||||||
base.OnFormClosing(e);
|
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.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using SpamGuard.Services;
|
using SpamGuard.Services;
|
||||||
|
using SpamGuard.State;
|
||||||
|
|
||||||
public sealed class TrayApplicationContext : ApplicationContext
|
public sealed class TrayApplicationContext : ApplicationContext
|
||||||
{
|
{
|
||||||
private readonly NotifyIcon _notifyIcon;
|
private readonly NotifyIcon _notifyIcon;
|
||||||
private readonly System.Windows.Forms.Timer _refreshTimer;
|
private readonly System.Windows.Forms.Timer _refreshTimer;
|
||||||
private readonly ActivityLog _activityLog;
|
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 readonly IHost _host;
|
||||||
private ActivityLogForm? _logForm;
|
private ActivityLogForm? _logForm;
|
||||||
private readonly ToolStripMenuItem _pauseMenuItem;
|
private readonly ToolStripMenuItem _pauseMenuItem;
|
||||||
|
|
||||||
|
private bool AllPaused => _monitors.All(m => m.IsPaused);
|
||||||
|
|
||||||
public TrayApplicationContext(IHost host)
|
public TrayApplicationContext(IHost host)
|
||||||
{
|
{
|
||||||
_host = host;
|
_host = host;
|
||||||
_activityLog = host.Services.GetRequiredService<ActivityLog>();
|
_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);
|
_pauseMenuItem = new ToolStripMenuItem("Pause", null, OnPauseResume);
|
||||||
|
|
||||||
@@ -39,6 +48,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
|
|
||||||
_notifyIcon.DoubleClick += OnViewLog;
|
_notifyIcon.DoubleClick += OnViewLog;
|
||||||
|
|
||||||
|
// Timer only updates tray tooltip and icon — form refresh is event-driven
|
||||||
_refreshTimer = new System.Windows.Forms.Timer { Interval = 5000 };
|
_refreshTimer = new System.Windows.Forms.Timer { Interval = 5000 };
|
||||||
_refreshTimer.Tick += OnRefreshTick;
|
_refreshTimer.Tick += OnRefreshTick;
|
||||||
_refreshTimer.Start();
|
_refreshTimer.Start();
|
||||||
@@ -51,8 +61,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
var tooltip = $"SpamGuard - {checked_} checked, {spam} spam caught today";
|
var tooltip = $"SpamGuard - {checked_} checked, {spam} spam caught today";
|
||||||
_notifyIcon.Text = tooltip.Length > 127 ? tooltip[..127] : tooltip;
|
_notifyIcon.Text = tooltip.Length > 127 ? tooltip[..127] : tooltip;
|
||||||
|
|
||||||
// Update icon based on state
|
if (!AllPaused)
|
||||||
if (!_monitor.IsPaused)
|
|
||||||
{
|
{
|
||||||
var recent = _activityLog.GetRecent(1);
|
var recent = _activityLog.GetRecent(1);
|
||||||
var hasRecentError = recent.Count > 0
|
var hasRecentError = recent.Count > 0
|
||||||
@@ -61,14 +70,15 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
_notifyIcon.Icon = hasRecentError ? IconGenerator.Red : IconGenerator.Green;
|
_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)
|
private void OnViewLog(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_logForm == null || _logForm.IsDisposed)
|
if (_logForm == null || _logForm.IsDisposed)
|
||||||
{
|
{
|
||||||
_logForm = new ActivityLogForm(_activityLog);
|
_logForm = new ActivityLogForm(
|
||||||
|
_activityLog, _monitors, _trustedSenders, _blockedDomains, _overrideStore);
|
||||||
}
|
}
|
||||||
_logForm.Show();
|
_logForm.Show();
|
||||||
_logForm.BringToFront();
|
_logForm.BringToFront();
|
||||||
@@ -76,15 +86,15 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
|
|
||||||
private void OnPauseResume(object? sender, EventArgs e)
|
private void OnPauseResume(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_monitor.IsPaused)
|
if (AllPaused)
|
||||||
{
|
{
|
||||||
_monitor.Resume();
|
foreach (var m in _monitors) m.Resume();
|
||||||
_pauseMenuItem.Text = "Pause";
|
_pauseMenuItem.Text = "Pause";
|
||||||
_notifyIcon.Icon = IconGenerator.Green;
|
_notifyIcon.Icon = IconGenerator.Green;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_monitor.Pause();
|
foreach (var m in _monitors) m.Pause();
|
||||||
_pauseMenuItem.Text = "Resume";
|
_pauseMenuItem.Text = "Resume";
|
||||||
_notifyIcon.Icon = IconGenerator.Yellow;
|
_notifyIcon.Icon = IconGenerator.Yellow;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user