fix(tells): load pinned tell history after login, not before it

A pinned tell tab came up empty for the rest of the session, reported by a
tester and reproduced from his config.

RehydratePinnedTabs runs from a hosted service at plugin start. It queries tell
history by character, and CurrentContentId is 0 until a character is logged in
-- LastContentId only gets set from the framework tick. The game loads plugins
at boot, so the normal path queries for character zero, finds nothing, and there
was no second attempt: the service subscribed to Logout but not Login.

It defers now when no character is available and completes on the login that
follows. The pending flag keeps a later character switch from appending a second
copy of the history to tabs that already have it.

Two things hid this. Reloading the plugin in a running session, which is what
development looks like, always has a character available. And it only shows up
if you pin a tell tab at all -- the same path for non-temp tabs already handles
the boot case explicitly, one file over:

    if (pluginInterface.Reason is not PluginLoadReason.Boot)
        manager.FilterAllTabsAsync();

Also surfaces FilterIncludePreviousSessions in the Chat tab, which decides
whether the log shows anything from before the current session and had no
control at all -- written only by the first-run wizard, and only if the user
reached step 3. Skip the wizard and it stays false forever. It applies
immediately rather than at next launch, since the user is looking at the window
when they ask for it.
This commit is contained in:
2026-08-18 18:44:54 +02:00
parent 57780351d4
commit ae9b503776
3 changed files with 67 additions and 4 deletions
+34
View File
@@ -39,6 +39,10 @@ internal sealed class AutoTellTabsService : IDisposable
private bool _initialized;
// Set when Initialize ran before a character was available; cleared once the
// history has actually been loaded.
private bool _rehydratePending;
internal AutoTellTabsService(
Plugin plugin,
MessageManager messageManager,
@@ -76,12 +80,30 @@ internal sealed class AutoTellTabsService : IDisposable
RehydratePinnedTabs();
_messageManager.MessageProcessed += HandleTell;
Plugin.ClientState.Login += OnLogin;
Plugin.ClientState.Logout += OnLogout;
_initialized = true;
}
// Deferred when the plugin starts before a character is logged in, which is
// the normal case: the game loads plugins at boot. CurrentContentId is 0
// until then, so the history query would look up tells for character zero,
// find none, and leave every pinned tab blank for the whole session.
//
// Only visible to someone who actually pins a tell tab AND starts the game
// with the plugin already installed. Reloading the plugin in a running
// session -- what a developer does all day -- hides it completely.
private void RehydratePinnedTabs()
{
if (_messageManager.CurrentContentId == 0)
{
_logger.LogDebug("[Pin] Rehydrate deferred: no character yet, waiting for login");
_rehydratePending = true;
return;
}
_rehydratePending = false;
var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
_logger.LogDebug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found");
@@ -122,6 +144,7 @@ internal sealed class AutoTellTabsService : IDisposable
return;
}
Plugin.ClientState.Login -= OnLogin;
Plugin.ClientState.Logout -= OnLogout;
_messageManager.MessageProcessed -= HandleTell;
_initialized = false;
@@ -490,6 +513,17 @@ internal sealed class AutoTellTabsService : IDisposable
}
}
// Fires on the login that follows a boot-time start, and on every character
// switch after one. Guarded by the pending flag so a switch does not append
// a second copy of the history to tabs that already have it.
private void OnLogin()
{
if (!_rehydratePending)
return;
RehydratePinnedTabs();
}
private void OnLogout(int type, int code)
{
lock (TabsListLock)
@@ -6,10 +6,12 @@ namespace HellionChat.Ui.Components.Settings.Tabs;
internal sealed class ChatTab
{
private readonly Plugin _plugin;
private readonly SettingsWidgets _w;
public ChatTab(Plugin plugin, TokenResolver resolver)
{
_plugin = plugin;
_w = new SettingsWidgets(plugin, new SettingsPalette(resolver));
}
@@ -53,6 +55,33 @@ internal sealed class ChatTab
);
}
if (_w.Section(ImGui.GetID("chat.history"u8), "History"))
{
// The one setting that decides whether the chat log shows anything
// from before this game session. It had no control at all: only the
// first-run wizard ever wrote it, and only if the user actually
// reached step 3 -- skip the wizard and it stays on its default of
// false, leaving the log empty on every launch with no way to fix it.
//
// Named for what it does. The stored name describes filtering, and
// the wizard's own "Load previous session on startup" checkbox
// writes a field nothing reads.
_w.ToggleRow(
ImGui.GetID("chat.history.previoussessions"u8),
"Show history from previous sessions",
"Off means the log starts empty each time the game launches and "
+ "only fills with messages received since.",
() => Plugin.Config.FilterIncludePreviousSessions,
v =>
{
Plugin.Config.FilterIncludePreviousSessions = v;
// Takes effect at once rather than on the next launch: the
// window is open and the user just asked for the history.
_plugin.MessageManager.FilterAllTabs();
}
);
}
if (_w.Section(ImGui.GetID("chat.commandhelp"u8), "Command help", open: false))
{
_w.EnumComboRow(
@@ -57,8 +57,8 @@ internal sealed class SurfaceBackdrop
// the full height is exactly the case that bands, because its handful of
// alpha steps each cover twenty-odd pixels.
var fade = MathF.Min(190f * Metrics.Scale, (max.Y - min.Y) * 0.45f);
dl.DrawEdgeTint(min, max, 0x00FFFFFFu | ((uint)(0x22 * strength * opacity) << 24), fade);
dl.DrawEdgeTint(min, max, (uint)(0x30 * strength * opacity) << 24, fade, fromBottom: true);
dl.DrawEdgeTint(min, max, 0x00FFFFFFu | ((uint)(0x10 * strength * opacity) << 24), fade);
dl.DrawEdgeTint(min, max, (uint)(0x38 * strength * opacity) << 24, fade, fromBottom: true);
if (darken > 0f)
dl.AddRectFilled(min, max, (uint)(0xFF * darken * opacity) << 24);
@@ -69,8 +69,8 @@ internal sealed class SurfaceBackdrop
dl.AddRectFilledMultiColor(
min,
new Vector2(max.X, min.Y + (max.Y - min.Y) * accentWashHeight),
ColourUtil.ApplyAlpha(accent, 0.07f * opacity),
ColourUtil.ApplyAlpha(accent, 0.035f * opacity),
ColourUtil.ApplyAlpha(accent, 0.045f * opacity),
ColourUtil.ApplyAlpha(accent, 0.02f * opacity),
accent & 0x00FFFFFFu,
accent & 0x00FFFFFFu
);