feat(privacy): mark cleanup preview as stale when whitelist changes

The preview block caches the deletion estimate from the last refresh.
When the user toggles whitelist channels afterwards the cached number
no longer reflects the current selection. Snapshot the whitelist on
refresh and detect drift on every frame; on drift, grey out the counts
and surface a stale hint plus an emphasised refresh button. Sits
alongside the existing Cleanup_Help_SavedNote, which warns about a
different mismatch (mutable vs saved) and stays as-is.
This commit is contained in:
2026-05-02 21:22:12 +02:00
parent 4d977d5118
commit 7c52e890e6
4 changed files with 47 additions and 3 deletions
+1
View File
@@ -64,6 +64,7 @@ internal class HellionStrings
internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading)); internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading));
internal static string Cleanup_Help_Intro => Get(nameof(Cleanup_Help_Intro)); internal static string Cleanup_Help_Intro => Get(nameof(Cleanup_Help_Intro));
internal static string Cleanup_Help_SavedNote => Get(nameof(Cleanup_Help_SavedNote)); internal static string Cleanup_Help_SavedNote => Get(nameof(Cleanup_Help_SavedNote));
internal static string Cleanup_Preview_Stale => Get(nameof(Cleanup_Preview_Stale));
internal static string Retention_Help_SavedNote => Get(nameof(Retention_Help_SavedNote)); internal static string Retention_Help_SavedNote => Get(nameof(Retention_Help_SavedNote));
internal static string Cleanup_RefreshPreview => Get(nameof(Cleanup_RefreshPreview)); internal static string Cleanup_RefreshPreview => Get(nameof(Cleanup_RefreshPreview));
internal static string Cleanup_NoPreview => Get(nameof(Cleanup_NoPreview)); internal static string Cleanup_NoPreview => Get(nameof(Cleanup_NoPreview));
+3
View File
@@ -81,6 +81,9 @@
<data name="Retention_Help_SavedNote" xml:space="preserve"> <data name="Retention_Help_SavedNote" xml:space="preserve">
<value>Der manuelle Lauf nutzt deine GESPEICHERTE Retention-Policy, nicht die Slider-Werte oben. Klicke zuerst Speichern, wenn der Lauf deine aktuellen Änderungen anwenden soll.</value> <value>Der manuelle Lauf nutzt deine GESPEICHERTE Retention-Policy, nicht die Slider-Werte oben. Klicke zuerst Speichern, wenn der Lauf deine aktuellen Änderungen anwenden soll.</value>
</data> </data>
<data name="Cleanup_Preview_Stale" xml:space="preserve">
<value>Vorschau veraltet, deine Whitelist hat sich seit dem letzten Aktualisieren geändert. Klicke Aktualisieren, um neu zu berechnen.</value>
</data>
<data name="Cleanup_RefreshPreview" xml:space="preserve"> <data name="Cleanup_RefreshPreview" xml:space="preserve">
<value>Vorschau aktualisieren</value> <value>Vorschau aktualisieren</value>
</data> </data>
+3
View File
@@ -81,6 +81,9 @@
<data name="Retention_Help_SavedNote" xml:space="preserve"> <data name="Retention_Help_SavedNote" xml:space="preserve">
<value>The manual sweep uses your SAVED retention policy, not the slider values above. Click Save first if you want the run to apply your current edits.</value> <value>The manual sweep uses your SAVED retention policy, not the slider values above. Click Save first if you want the run to apply your current edits.</value>
</data> </data>
<data name="Cleanup_Preview_Stale" xml:space="preserve">
<value>Preview is out of date — your whitelist has changed since the last refresh. Click Refresh to recalculate.</value>
</data>
<data name="Cleanup_RefreshPreview" xml:space="preserve"> <data name="Cleanup_RefreshPreview" xml:space="preserve">
<value>Refresh preview</value> <value>Refresh preview</value>
</data> </data>
+37
View File
@@ -3,6 +3,7 @@ using ChatTwo.Export;
using ChatTwo.Privacy; using ChatTwo.Privacy;
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Util; using ChatTwo.Util;
using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
@@ -55,6 +56,8 @@ internal sealed class Privacy : ISettingsTab
private long CleanupKeepCount; private long CleanupKeepCount;
private long CleanupDeleteCount; private long CleanupDeleteCount;
private bool CleanupRunning; private bool CleanupRunning;
private bool CleanupPreviewStale;
private HashSet<ChatType>? CleanupPreviewSnapshot;
// The retention-running state lives on Plugin so the auto-sweep and // The retention-running state lives on Plugin so the auto-sweep and
// this manual button see the same flag. UI reads stay lock-free // this manual button see the same flag. UI reads stay lock-free
@@ -484,6 +487,21 @@ internal sealed class Privacy : ISettingsTab
ImGui.Spacing(); ImGui.Spacing();
// Drift-detection between the snapshot taken at last refresh
// and the current Mutable whitelist. Cleanup itself runs on
// the SAVED policy (Cleanup_Help_SavedNote covers that), but
// the user usually expects "the preview reflects what I just
// ticked" — so we surface the divergence instead of silently
// showing stale numbers.
if (CleanupPreviewSnapshot is not null
&& !CleanupPreviewSnapshot.SetEquals(Mutable.PrivacyPersistChannels))
{
CleanupPreviewStale = true;
}
using (var emphasis = CleanupPreviewStale
? ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.6f })
: null)
using (ImRaii.Disabled(CleanupRunning)) using (ImRaii.Disabled(CleanupRunning))
{ {
if (ImGui.Button(HellionStrings.Cleanup_RefreshPreview)) if (ImGui.Button(HellionStrings.Cleanup_RefreshPreview))
@@ -496,10 +514,22 @@ internal sealed class Privacy : ISettingsTab
return; return;
} }
if (CleanupPreviewStale)
{
ImGui.Spacing(); ImGui.Spacing();
ImGuiUtil.HelpText(HellionStrings.Cleanup_Preview_Stale);
}
ImGui.Spacing();
using (var staleColor = CleanupPreviewStale
? ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)
: null)
{
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount)); ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount));
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount)); ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount));
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount)); ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount));
}
using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown)) using (var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown))
{ {
@@ -555,6 +585,13 @@ internal sealed class Privacy : ISettingsTab
else else
CleanupDeleteCount += count; CleanupDeleteCount += count;
} }
// Snapshot the whitelist as it stood at preview-time so the
// render pass can flag the user about subsequent edits. Only
// updated on success — if the preview throws, the previous
// snapshot stays in place so stale-detection keeps working.
CleanupPreviewSnapshot = new HashSet<ChatType>(Mutable.PrivacyPersistChannels);
CleanupPreviewStale = false;
} }
catch (Exception e) catch (Exception e)
{ {