fix(settings): conditional refilter on save — preserve chat history for cosmetic changes

Settings.Save() unconditionally ran ClearAllTabs + FilterAllTabsAsync
after every save. The cycle reloads messages from the DB, which silently
wipes any in-session message that wasn't persisted — Privacy-First
configurations block most channels from the DB, so all unlogged
channels (Allgemein/Say/Yell/Shout under default filters) showed up
empty after every settings save.

New HasFilterRelevantChanges helper compares Mutable to Plugin.Config
across:
- PrivacyFilterEnabled
- PrivacyPersistChannels (HashSet<ChatType>)
- PrivacyPersistUnknownChannels
- FilterIncludePreviousSessions
- per-persistent-tab: Identifier (reorder/swap), SelectedChannels,
  ExtraChatAll, ExtraChatChannels

Refilter only runs if any of those changed. Cosmetic settings (theme,
tab icons, layout, fonts, language) leave the chat log untouched.
Combined with the prior UpdateFrom Identifier-mapping fix and the
TempTab skip in ClearAllTabs/FilterAllTabs, both persistent and
Auto-Tell tabs now fully survive a settings save.

Reported by Flo from in-game testing 2026-05-05/06: 'der allgemein
chat tab z.b immernoch gecleart wird' / 'alle vom plugin nicht
geloggten channel sind dann leer'.

Also updated yaml changelog, docs/CHANGELOG.md and .github/forge-posts/
v1.2.0.md to describe the actual fix shape rather than the partial
UpdateFrom-only fix that preceded it.
This commit is contained in:
2026-05-06 00:05:49 +02:00
parent 56621669b2
commit 4b43fdb0ad
4 changed files with 79 additions and 12 deletions
+9 -7
View File
@@ -114,13 +114,15 @@ changelog: |-
Bug fixes from in-game testing:
- Settings save no longer wipes the in-session chat history of
persistent tabs (Tab.Clone preserved Identifier but not
Messages — Identifier-based mapping now restores the live
MessageList onto cloned tabs).
- Settings save no longer clears Auto-Tell tabs either
(ClearAllTabs/FilterAllTabs now skip TempTabs since their
messages have no DB persistence to refilter from).
- Settings save no longer wipes chat history by default. The
heavy ClearAllTabs + FilterAllTabsAsync refilter cycle now
only runs when a filter-relevant setting actually changed
(Privacy filter, persisted channels, per-tab channel
selection). Cosmetic changes — theme, tab icons, layout
flags — keep the in-session chat intact. Combined with an
Identifier-based MessageList restore in Configuration.
UpdateFrom and a TempTab skip in ClearAllTabs/FilterAllTabs,
persistent tabs and Auto-Tell tabs both survive the save.
- Sidebar buttons now align vertically with the first message
row (top padding mirrors the chat header toolbar height).
- Sidebar child window no longer paints the top padding with
+67 -2
View File
@@ -210,14 +210,24 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
var fontSizeChanged = Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001
|| Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001;
var italicStateChanged = Mutable.ItalicEnabled != Plugin.Config.ItalicEnabled;
// v1.2.0 — Refilter only if a filter-relevant setting actually
// changed. The Clear+Refilter cycle reloads messages from the DB,
// which silently wipes any in-session message that wasn't
// persisted (Privacy-First config blocks most channels from DB).
// Cosmetic changes (theme, tab icons, layout flags) trigger no
// refilter — chat history stays intact.
var filtersChanged = HasFilterRelevantChanges();
Plugin.Config.UpdateFrom(Mutable, true);
// save after 60 frames have passed, which should hopefully not
// commit any changes that cause a crash
Plugin.DeferredSaveFrames = 60;
Plugin.MessageManager.ClearAllTabs();
Plugin.MessageManager.FilterAllTabsAsync();
if (filtersChanged)
{
Plugin.MessageManager.ClearAllTabs();
Plugin.MessageManager.FilterAllTabsAsync();
}
if (fontChanged || fontSizeChanged || italicStateChanged)
Plugin.FontManager.BuildFonts();
@@ -233,4 +243,59 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
Initialise();
}
/// <summary>
/// v1.2.0 — Detects whether any setting that influences message
/// filtering changed between Plugin.Config and the Mutable working
/// copy. Used to gate the heavy ClearAllTabs+FilterAllTabsAsync cycle
/// in Save: cosmetic changes (theme, tab icons, layout flags) do not
/// touch the chat log, only filter-relevant changes do. Without this
/// gate, every settings save wipes the chat history of any channel
/// the Privacy filter blocks from being persisted to the DB —
/// reported by Flo from in-game testing 2026-05-05/06.
/// </summary>
private bool HasFilterRelevantChanges()
{
// Top-level privacy controls.
if (Mutable.PrivacyFilterEnabled != Plugin.Config.PrivacyFilterEnabled) return true;
if (Mutable.PrivacyPersistUnknownChannels != Plugin.Config.PrivacyPersistUnknownChannels) return true;
if (!Mutable.PrivacyPersistChannels.SetEquals(Plugin.Config.PrivacyPersistChannels)) return true;
// FilterIncludePreviousSessions changes the GetMostRecentMessages
// window in MessageManager.FilterAllTabs and is therefore filter-
// relevant even though it lives outside the Privacy block.
if (Mutable.FilterIncludePreviousSessions != Plugin.Config.FilterIncludePreviousSessions) return true;
// Per-tab channel selection. Compare persistent tabs only —
// TempTabs are session-only and never refiltered anyway.
var origPersistent = Plugin.Config.Tabs.Where(t => !t.IsTempTab).ToList();
var newPersistent = Mutable.Tabs.Where(t => !t.IsTempTab).ToList();
if (origPersistent.Count != newPersistent.Count) return true; // add or delete
for (var i = 0; i < origPersistent.Count; i++)
{
var orig = origPersistent[i];
var neu = newPersistent[i];
// Identifier mismatch at the same index means reorder or
// a slot got swapped — treat as filter-relevant so the new
// channel-selection layout actually applies.
if (orig.Identifier != neu.Identifier) return true;
if (orig.ExtraChatAll != neu.ExtraChatAll) return true;
if (!orig.ExtraChatChannels.SetEquals(neu.ExtraChatChannels)) return true;
// SelectedChannels is a Dictionary<ChatType, (ChatSource, ChatSource)>
// — value-tuple equality already does the right thing per-pair.
if (orig.SelectedChannels.Count != neu.SelectedChannels.Count) return true;
foreach (var pair in orig.SelectedChannels)
{
if (!neu.SelectedChannels.TryGetValue(pair.Key, out var nv)) return true;
if (!pair.Value.Equals(nv)) return true;
}
}
return false;
}
}