From 824037e55fc2123cac214a46b0652963a46a5a89 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 12:27:55 +0200 Subject: [PATCH 01/23] feat(auto-tell-tabs): add configuration fields for auto tell tabs --- ChatTwo/Configuration.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index 44fcb95..dcf545a 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -81,6 +81,20 @@ public class Configuration : IPluginConfiguration // to fall back to the user's chosen system or Dalamud font. public bool UseHellionFont = true; + // Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing + // /tell spawns a session-only tab dedicated to that conversation + // partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian). + public bool EnableAutoTellTabs = true; + // Hard cap on simultaneously open auto tell tabs. Range enforced by the + // settings slider (1–50). LRU drop favors greeted tabs first. + public int AutoTellTabsLimit = 15; + // When true the sidebar shows only a thin separator before the temp + // tabs; when false a section header "Active Tells (n)" is rendered. + public bool AutoTellTabsCompactDisplay; + // Number of prior tells to preload from the message store when an + // auto tell tab is spawned. Range 0–100; 0 disables preload. + public int AutoTellTabsHistoryPreload = 20; + public int GetRetentionDays(ChatType type) { if (RetentionPerChannelDays.TryGetValue(type, out var userOverride)) @@ -249,6 +263,11 @@ public class Configuration : IPluginConfiguration HellionThemeEnabled = other.HellionThemeEnabled; HellionThemeWindowOpacity = other.HellionThemeWindowOpacity; UseHellionFont = other.UseHellionFont; + + EnableAutoTellTabs = other.EnableAutoTellTabs; + AutoTellTabsLimit = other.AutoTellTabsLimit; + AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; + AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload; } } From 32c410e8e20adbc49f2cde9f0bcf375345ef3f50 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 12:30:30 +0200 Subject: [PATCH 02/23] feat(auto-tell-tabs): add IsGreeted flag and Tab.Matches sender filter for temp tabs --- ChatTwo/Configuration.cs | 51 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index dcf545a..37abad3 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -6,6 +6,7 @@ using ChatTwo.Util; using Dalamud; using Dalamud.Configuration; using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.FontIdentifier; using Dalamud.Bindings.ImGui; @@ -343,9 +344,56 @@ public class Tab [NonSerialized] public Guid Identifier = Guid.NewGuid(); + // Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the + // sidebar to mark a tell partner as already greeted in the current + // session. NonSerialized because the temp tab itself is session-only. + [NonSerialized] public bool IsGreeted; + public bool Matches(Message message) { - return message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels); + if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels)) + { + return false; + } + + // Auto-tell temp tabs are bound to a single conversation partner; + // every other tell that matches the channel filter must NOT land + // here, otherwise all temp tabs would mirror "Tell Exclusive". + if (IsTempTab && TellTarget?.IsSet() == true) + { + return MatchesTempTabSender(message); + } + + return true; + } + + private bool MatchesTempTabSender(Message message) + { + var senderPayload = ExtractPlayerPayload(message.Sender); + if (senderPayload == null) + { + senderPayload = ExtractPlayerPayload(message.Content); + } + if (senderPayload == null) + { + return false; + } + + var nameMatches = string.Equals(senderPayload.PlayerName, TellTarget.Name, StringComparison.OrdinalIgnoreCase); + var worldMatches = senderPayload.World.RowId == TellTarget.World; + return nameMatches && worldMatches; + } + + private static PlayerPayload? ExtractPlayerPayload(IReadOnlyList chunks) + { + foreach (var chunk in chunks) + { + if (chunk.Link is PlayerPayload pp) + { + return pp; + } + } + return null; } public void AddMessage(Message message, bool unread = true) @@ -394,6 +442,7 @@ public class Tab IsTempTab = IsTempTab, AllSenderMessages = AllSenderMessages, TellTarget = TellTarget.From(TellTarget), + IsGreeted = IsGreeted, }; } From 141fcbf074ce133d7402c45bf0b91dd2f231499c Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 12:37:06 +0200 Subject: [PATCH 03/23] feat(auto-tell-tabs): filter temp tabs from config save and load (defense-in-depth) --- ChatTwo/Plugin.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 3c9e441..92a1c74 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -100,6 +100,12 @@ public sealed class Plugin : IDalamudPlugin Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); + // Hellion Chat — Auto-Tell-Tabs Defense-in-Depth. SaveConfig + // already strips temp tabs before persistence, but a previous + // crash or external write could have left them in the JSON. + // Drop them on load to guarantee the session-only invariant. + Config.Tabs.RemoveAll(t => t.IsTempTab); + #pragma warning disable CS0618 // Type or member is obsolete // TODO Remove after 01.07.2026 // Migrate old channel values @@ -491,7 +497,17 @@ public sealed class Plugin : IDalamudPlugin internal void SaveConfig() { + // Hellion Chat — Auto-Tell-Tabs are session-only. Strip them out + // before serialization so a crash mid-session can never persist + // them. We snapshot the full tab list first and restore it after + // the save, preserving the user's order and open conversations. + var snapshot = Config.Tabs.ToList(); + Config.Tabs.RemoveAll(t => t.IsTempTab); + Interface.SavePluginConfig(Config); + + Config.Tabs.Clear(); + Config.Tabs.AddRange(snapshot); } internal void LanguageChanged(string langCode) From 07f47f32e3dd06ca85f2c71a045cf2d2d29e322d Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 12:40:22 +0200 Subject: [PATCH 04/23] feat(auto-tell-tabs): bump configuration version to 9 with migration notice --- ChatTwo/Configuration.cs | 2 +- ChatTwo/Plugin.cs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index 37abad3..f10c3f0 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -34,7 +34,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - private const int LatestVersion = 8; + private const int LatestVersion = 9; public int Version { get; set; } = LatestVersion; diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 92a1c74..04ef3a0 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -173,6 +173,26 @@ public sealed class Plugin : IDalamudPlugin }); } + // Hellion Chat v8→v9: Auto-Tell-Tabs feature seeded with + // property-initializer defaults (enabled, limit 15, history 20, + // section header on). No data migration needed — just bump the + // version and notify the user once so the feature does not + // surprise them. + if (Config.Version <= 8) + { + Config.Version = 9; + SaveConfig(); + + // TODO Task 14: replace with HellionStrings.AutoTellTabs_Migration_Title / _Content + Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification + { + Title = "Auto-Tell-Tabs", + Content = "Auto-Tell-Tabs sind ab Version 0.4.0 standardmäßig aktiv. Du kannst sie im Chat-Tab deaktivieren oder anpassen.", + Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, + InitialDuration = TimeSpan.FromSeconds(20), + }); + } + if (Config.Tabs.Count == 0) Config.Tabs.Add(TabsUtil.VanillaGeneral); From 92bb368d2bdcb948debef0f7712749b5976f5caf Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 12:52:58 +0200 Subject: [PATCH 05/23] feat(auto-tell-tabs): add GetTellHistoryWithSender query and ChunkUtil sender helper --- ChatTwo.Tests/AutoTellTabsHistoryTest.cs | 167 +++++++++++++++++++++++ ChatTwo/Configuration.cs | 31 +---- ChatTwo/MessageStore.cs | 78 +++++++++++ ChatTwo/Util/ChunkUtil.cs | 34 +++++ 4 files changed, 280 insertions(+), 30 deletions(-) create mode 100644 ChatTwo.Tests/AutoTellTabsHistoryTest.cs diff --git a/ChatTwo.Tests/AutoTellTabsHistoryTest.cs b/ChatTwo.Tests/AutoTellTabsHistoryTest.cs new file mode 100644 index 0000000..0c47836 --- /dev/null +++ b/ChatTwo.Tests/AutoTellTabsHistoryTest.cs @@ -0,0 +1,167 @@ +using System; +using System.IO; +using System.Linq; +using ChatTwo.Code; +using ChatTwo.Util; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using JetBrains.Annotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ChatTwo.Tests; + +// Hellion Chat — Auto-Tell-Tabs history-preload coverage. +// +// These tests exercise MessageStore.GetTellHistoryWithSender, the query the +// AutoTellTabsService uses to populate a freshly spawned temp tab with the +// last conversations with that player. +// +// NOTE: like the rest of ChatTwo.Tests today, these will fail at runtime +// until the project's Dalamud.dll runtime dependency is sorted out (see +// Phase-2 backlog item "Test-Projekt fixen"). Compile-time the suite builds +// fine via DALAMUD_HOME, so the tests guard against API drift even before +// they can be executed locally. +[TestClass] +[TestSubject(typeof(MessageStore))] +public class AutoTellTabsHistoryTest +{ + public TestContext TestContext { get; set; } + + [TestMethod] + [Timeout(5000)] + public void GetTellHistoryWithSender_FiltersByNameAndWorld() + { + var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_"); + var dbPath = Path.Join(tempDir.FullName, "test.db"); + TestContext.WriteLine("Using database path: " + dbPath); + using var store = new MessageStore(dbPath); + + const ulong receiver = 99001; + var now = DateTimeOffset.UtcNow; + + // Two tells with the target sender, one with a different sender on + // the same world, one with the same name on a different world. Only + // the first two should make it into the result. + var asukaLichIn = TellMessage("Asuka", 76, receiver, now.AddMinutes(-30), ChatType.TellIncoming); + var asukaLichOut = TellMessage("Asuka", 76, receiver, now.AddMinutes(-20), ChatType.TellOutgoing); + var broboLich = TellMessage("Brobo", 76, receiver, now.AddMinutes(-10), ChatType.TellIncoming); + var asukaOmega = TellMessage("Asuka", 90, receiver, now.AddMinutes(-5), ChatType.TellIncoming); + + store.UpsertMessage(asukaLichIn); + store.UpsertMessage(asukaLichOut); + store.UpsertMessage(broboLich); + store.UpsertMessage(asukaOmega); + + var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 50); + + Assert.AreEqual(2, result.Count); + // Result is oldest-first so a tab can append messages chronologically. + Assert.AreEqual(asukaLichIn.Id, result[0].Id); + Assert.AreEqual(asukaLichOut.Id, result[1].Id); + } + + [TestMethod] + [Timeout(5000)] + public void GetTellHistoryWithSender_RespectsLimit() + { + var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_"); + var dbPath = Path.Join(tempDir.FullName, "test.db"); + TestContext.WriteLine("Using database path: " + dbPath); + using var store = new MessageStore(dbPath); + + const ulong receiver = 99002; + var now = DateTimeOffset.UtcNow; + + for (var i = 0; i < 30; i++) + { + var msg = TellMessage("Asuka", 76, receiver, now.AddMinutes(-i - 1), ChatType.TellIncoming); + store.UpsertMessage(msg); + } + + var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 5); + + Assert.AreEqual(5, result.Count); + } + + [TestMethod] + [Timeout(5000)] + public void GetTellHistoryWithSender_ZeroLimitReturnsEmpty() + { + var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_"); + var dbPath = Path.Join(tempDir.FullName, "test.db"); + TestContext.WriteLine("Using database path: " + dbPath); + using var store = new MessageStore(dbPath); + + const ulong receiver = 99003; + + var msg = TellMessage("Asuka", 76, receiver, DateTimeOffset.UtcNow, ChatType.TellIncoming); + store.UpsertMessage(msg); + + var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 0); + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + [Timeout(5000)] + public void GetTellHistoryWithSender_IgnoresOtherReceivers() + { + var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_"); + var dbPath = Path.Join(tempDir.FullName, "test.db"); + TestContext.WriteLine("Using database path: " + dbPath); + using var store = new MessageStore(dbPath); + + const ulong ourReceiver = 99004; + const ulong otherReceiver = 99005; + var now = DateTimeOffset.UtcNow; + + // Tell on the local player's account. + var ours = TellMessage("Asuka", 76, ourReceiver, now.AddMinutes(-1), ChatType.TellIncoming); + // Same sender, but logged against a different local character — + // common when the user has alts. Must not surface. + var foreign = TellMessage("Asuka", 76, otherReceiver, now, ChatType.TellIncoming); + + store.UpsertMessage(ours); + store.UpsertMessage(foreign); + + var result = store.GetTellHistoryWithSender(ourReceiver, "Asuka", 76, limit: 50); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual(ours.Id, result[0].Id); + } + + private static Message TellMessage( + string senderName, + uint senderWorld, + ulong receiver, + DateTimeOffset dateTime, + ChatType chatType) + { + var senderSeString = new SeStringBuilder() + .Add(new PlayerPayload(senderName, senderWorld)) + .AddText(senderName) + .Add(RawPayload.LinkTerminator) + .Build(); + + var contentSeString = new SeStringBuilder() + .AddText("test message") + .Build(); + + var senderChunks = ChunkUtil.ToChunks(senderSeString, ChunkSource.Sender, chatType).ToList(); + var contentChunks = ChunkUtil.ToChunks(contentSeString, ChunkSource.Content, chatType).ToList(); + + var chatCode = new ChatCode((XivChatType)chatType, XivChatRelationKind.LocalPlayer, XivChatRelationKind.LocalPlayer); + return new Message( + Guid.NewGuid(), + receiver, + 0, + dateTime, + chatCode, + senderChunks, + contentChunks, + senderSeString, + contentSeString, + Guid.Empty); + } +} diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index f10c3f0..e783164 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -361,41 +361,12 @@ public class Tab // here, otherwise all temp tabs would mirror "Tell Exclusive". if (IsTempTab && TellTarget?.IsSet() == true) { - return MatchesTempTabSender(message); + return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World); } return true; } - private bool MatchesTempTabSender(Message message) - { - var senderPayload = ExtractPlayerPayload(message.Sender); - if (senderPayload == null) - { - senderPayload = ExtractPlayerPayload(message.Content); - } - if (senderPayload == null) - { - return false; - } - - var nameMatches = string.Equals(senderPayload.PlayerName, TellTarget.Name, StringComparison.OrdinalIgnoreCase); - var worldMatches = senderPayload.World.RowId == TellTarget.World; - return nameMatches && worldMatches; - } - - private static PlayerPayload? ExtractPlayerPayload(IReadOnlyList chunks) - { - foreach (var chunk in chunks) - { - if (chunk.Link is PlayerPayload pp) - { - return pp; - } - } - return null; - } - public void AddMessage(Message message, bool unread = true) { Messages.AddPrune(message, MessageManager.MessageDisplayLimit); diff --git a/ChatTwo/MessageStore.cs b/ChatTwo/MessageStore.cs index 3dee96c..5f6b565 100644 --- a/ChatTwo/MessageStore.cs +++ b/ChatTwo/MessageStore.cs @@ -602,6 +602,84 @@ internal class MessageStore : IDisposable return new MessageEnumerator(cmd.ExecuteReader()); } + /// + /// Hellion Chat — Auto-Tell-Tabs history preload. + /// + /// Returns up to tells exchanged with the named + /// player, oldest-first, ready to be added to a freshly spawned auto + /// tell tab. The Sender column is a serialized chunk blob, so SQL on its + /// own cannot filter by player identity; we narrow with SQL on Receiver + /// + ChatType (cheap, indexed) and let the client do the final + /// PlayerPayload comparison on the result set. + /// + /// caps how many recent tells we scan + /// before giving up. 500 covers around 10 days for an active greeter + /// and stays well under the 20 ms budget required to keep the spawn on + /// the message-processing worker thread. + /// + internal IReadOnlyList GetTellHistoryWithSender( + ulong receiver, + string senderName, + uint senderWorld, + int limit, + int sqlScanLimit = 500) + { + if (limit <= 0) + { + return []; + } + + using var cmd = Connection.CreateCommand(); + cmd.CommandText = @" + SELECT + Id, + Receiver, + ContentId, + Date, + ChatType, + SourceKind, + TargetKind, + Sender, + Content, + SenderSource, + ContentSource, + ExtraChatChannel + FROM messages + WHERE deleted = false + AND Receiver = $Receiver + AND ChatType IN ($TellIncoming, $TellOutgoing) + ORDER BY Date DESC + LIMIT $ScanLimit; + "; + cmd.CommandTimeout = 60; + cmd.Parameters.AddWithValue("$Receiver", receiver); + cmd.Parameters.AddWithValue("$TellIncoming", (int)ChatType.TellIncoming); + cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing); + cmd.Parameters.AddWithValue("$ScanLimit", sqlScanLimit); + + var collected = new List(); + using var enumerator = new MessageEnumerator(cmd.ExecuteReader()); + foreach (var message in enumerator) + { + if (!ChunkUtil.MatchesSender(message, senderName, senderWorld)) + { + continue; + } + + collected.Add(message); + if (collected.Count >= limit) + { + break; + } + } + + // SQL was DESC (newest-first) so we hit the limit on the most + // recent matching tells. Reverse to oldest-first for chronological + // display in the tab. + collected.Reverse(); + return collected; + } + /// /// Marks a message as deleted so it won't get returned in queries. /// diff --git a/ChatTwo/Util/ChunkUtil.cs b/ChatTwo/Util/ChunkUtil.cs index 6f4728c..48702e7 100755 --- a/ChatTwo/Util/ChunkUtil.cs +++ b/ChatTwo/Util/ChunkUtil.cs @@ -398,6 +398,40 @@ internal static class ChunkUtil return builder.ToString(); } + // Hellion Chat — shared helper for Auto-Tell-Tabs and the MessageStore + // history-preload query. Walks the chunk list once and returns the + // first PlayerPayload it finds, or null when the message has no + // resolved player link (e.g. system messages, GM tells we already + // skipped earlier in the pipeline). + internal static PlayerPayload? TryGetPlayerPayload(IReadOnlyList chunks) + { + foreach (var chunk in chunks) + { + if (chunk.Link is PlayerPayload pp) + { + return pp; + } + } + return null; + } + + // True when the message's sender (or, as a fallback, content) carries a + // PlayerPayload that matches the given identity. Used by both the + // Tab.Matches sender filter and the MessageStore tell-history scan. + internal static bool MatchesSender(Message message, string senderName, uint senderWorld) + { + var payload = TryGetPlayerPayload(message.Sender) ?? TryGetPlayerPayload(message.Content); + if (payload == null) + { + return false; + } + if (!string.Equals(payload.PlayerName, senderName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + return payload.World.RowId == senderWorld; + } + internal static readonly RawPayload PeriodicRecruitmentLink = new([0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]); private static uint GetInteger(BinaryReader input) From 133f5c536f90b73a144c9bbce3f00d1e9304f717 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 12:57:49 +0200 Subject: [PATCH 06/23] feat(auto-tell-tabs): emit MessageProcessed event after tab routing --- ChatTwo/MessageManager.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ChatTwo/MessageManager.cs b/ChatTwo/MessageManager.cs index b74179b..e81075d 100644 --- a/ChatTwo/MessageManager.cs +++ b/ChatTwo/MessageManager.cs @@ -50,6 +50,13 @@ internal class MessageManager : IAsyncDisposable } } + // Hellion Chat — Auto-Tell-Tabs hook. Fires after a fully processed + // message has been routed to all matching persistent tabs and stored + // in the database. The AutoTellTabsService subscribes to spawn or + // refresh temp tabs without having to wedge itself into ProcessMessage + // directly. + public event Action? MessageProcessed; + internal unsafe MessageManager(Plugin plugin) { Plugin = plugin; @@ -266,6 +273,8 @@ internal class MessageManager : IAsyncDisposable if (tab.Matches(message)) tab.AddMessage(message, unread); } + + MessageProcessed?.Invoke(message); } internal class NameFormatting From 4810e8b518ee34e01c357d2f306368764417a75e Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 12:59:58 +0200 Subject: [PATCH 07/23] feat(auto-tell-tabs): add AutoTellTabsService skeleton with lifecycle --- ChatTwo/AutoTellTabsService.cs | 89 ++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 ChatTwo/AutoTellTabsService.cs diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs new file mode 100644 index 0000000..f4c5d22 --- /dev/null +++ b/ChatTwo/AutoTellTabsService.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; + +namespace ChatTwo; + +// Hellion Chat — Auto-Tell-Tabs. +// +// Spawns a session-only tab per /tell partner so a club greeter can track +// multiple parallel conversations without losing context. Subscribes to +// MessageManager.MessageProcessed for live tells and to ClientState.Logout +// for the cleanup pass; everything else hangs off these two entry points. +// +// See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian vault). +internal sealed class AutoTellTabsService : IDisposable +{ + private readonly Plugin _plugin; + private readonly MessageManager _messageManager; + private readonly MessageStore _store; + private readonly object _tempTabsLock = new(); + + private bool _initialized; + + internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store) + { + _plugin = plugin; + _messageManager = messageManager; + _store = store; + } + + internal int ActiveTempTabCount + { + get + { + lock (_tempTabsLock) + { + return Plugin.Config.Tabs.Count(t => t.IsTempTab); + } + } + } + + internal void Initialize() + { + if (_initialized) + { + return; + } + + _messageManager.MessageProcessed += HandleTell; + Plugin.ClientState.Logout += OnLogout; + _initialized = true; + } + + public void Dispose() + { + if (!_initialized) + { + return; + } + + Plugin.ClientState.Logout -= OnLogout; + _messageManager.MessageProcessed -= HandleTell; + _initialized = false; + } + + internal void HandleTell(Message message) + { + // Stub — implemented in Task 8. + } + + internal void MarkGreeted(Tab tab) + { + // Stub — implemented in Task 12. + } + + internal void UnmarkGreeted(Tab tab) + { + // Stub — implemented in Task 12. + } + + internal bool IsGreeted(Tab tab) + { + return tab.IsGreeted; + } + + private void OnLogout(int type, int code) + { + // Stub — implemented in Task 10. + } +} From baa4d011e8ce63399c982dd5f049d51a8b6d1799 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:01:27 +0200 Subject: [PATCH 08/23] feat(auto-tell-tabs): implement HandleTell with sender extraction and tab lookup --- ChatTwo/AutoTellTabsService.cs | 92 +++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs index f4c5d22..716c016 100644 --- a/ChatTwo/AutoTellTabsService.cs +++ b/ChatTwo/AutoTellTabsService.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using ChatTwo.Code; +using ChatTwo.Util; namespace ChatTwo; @@ -64,7 +66,95 @@ internal sealed class AutoTellTabsService : IDisposable internal void HandleTell(Message message) { - // Stub — implemented in Task 8. + if (!Plugin.Config.EnableAutoTellTabs) + { + return; + } + + if (message.Code.Type != ChatType.TellIncoming && message.Code.Type != ChatType.TellOutgoing) + { + return; + } + + var partner = ExtractTellPartner(message); + if (partner == null) + { + // Real message without a player payload — e.g. GM tells, which + // we deliberately skip. Warn once so we notice future regressions. + Plugin.Log.Warning("[AutoTellTabs] Could not extract tell partner from message; skipping spawn."); + return; + } + + lock (_tempTabsLock) + { + var existing = FindTempTab(partner.Value.Name, partner.Value.World); + if (existing != null) + { + // Tab already exists; Tab.Matches has already routed this + // message via the MessageManager pipeline (see Task 2 sender + // filter). + return; + } + + if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit) + { + DropOldestTempTab(); + } + + SpawnTempTab(partner.Value, message); + } + } + + private (string Name, uint World)? ExtractTellPartner(Message message) + { + if (message.Code.Type == ChatType.TellIncoming) + { + // Incoming tell: the sender is the conversation partner. + var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender); + if (fromSender != null) + { + return (fromSender.PlayerName, fromSender.World.RowId); + } + return null; + } + + // Outgoing tell: the local player is the sender, the partner shows + // up either as a payload in the content (for tells typed via the + // Chat 2 input bar) or as the channel's tracked tell target (set by + // the SetContextTellTarget game hook). + var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content); + if (fromContent != null) + { + return (fromContent.PlayerName, fromContent.World.RowId); + } + + var current = _plugin.CurrentTab.CurrentChannel.TellTarget + ?? _plugin.CurrentTab.CurrentChannel.TempTellTarget; + if (current != null && current.IsSet()) + { + return (current.Name, current.World); + } + + return null; + } + + private Tab? FindTempTab(string name, uint world) + { + return Plugin.Config.Tabs.FirstOrDefault(t => + t.IsTempTab + && t.TellTarget != null + && string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase) + && t.TellTarget.World == world); + } + + private void DropOldestTempTab() + { + // Stub — implemented in Task 9. + } + + private void SpawnTempTab((string Name, uint World) partner, Message currentMessage) + { + // Stub — implemented in Task 11. } internal void MarkGreeted(Tab tab) From a2977ef75b0e6d115dfd06eeabb66708ebcf3573 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:01:51 +0200 Subject: [PATCH 09/23] feat(auto-tell-tabs): implement LRU drop with greeted priority --- ChatTwo/AutoTellTabsService.cs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs index 716c016..d05e668 100644 --- a/ChatTwo/AutoTellTabsService.cs +++ b/ChatTwo/AutoTellTabsService.cs @@ -149,7 +149,30 @@ internal sealed class AutoTellTabsService : IDisposable private void DropOldestTempTab() { - // Stub — implemented in Task 9. + // Greeted tabs are dropped before un-greeted ones (the user said + // "I'm done with that conversation"), and within each bucket we + // pick the oldest LastActivity. This protects active conversations + // and unfinished greetings while still freeing up a slot. + var victim = Plugin.Config.Tabs + .Select((tab, idx) => (Tab: tab, Index: idx)) + .Where(t => t.Tab.IsTempTab) + .OrderByDescending(t => t.Tab.IsGreeted) + .ThenBy(t => t.Tab.LastActivity) + .FirstOrDefault(); + + if (victim.Tab == null) + { + return; + } + + Plugin.Config.Tabs.RemoveAt(victim.Index); + + // Re-anchor the active tab so the user does not silently end up on + // a different conversation when their tab gets dropped or shifted. + if (victim.Index <= _plugin.LastTab) + { + _plugin.WantedTab = 0; + } } private void SpawnTempTab((string Name, uint World) partner, Message currentMessage) From 269708150d95c689c69b0c27ceba1444e70da00e Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:02:15 +0200 Subject: [PATCH 10/23] feat(auto-tell-tabs): implement logout cleanup of temp tabs --- ChatTwo/AutoTellTabsService.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs index d05e668..ce662c4 100644 --- a/ChatTwo/AutoTellTabsService.cs +++ b/ChatTwo/AutoTellTabsService.cs @@ -197,6 +197,24 @@ internal sealed class AutoTellTabsService : IDisposable private void OnLogout(int type, int code) { - // Stub — implemented in Task 10. + lock (_tempTabsLock) + { + // Snapshot whether the active tab is about to be removed, BEFORE + // we mutate the list — index lookups would lie to us afterwards. + var lastIndex = _plugin.LastTab; + var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count; + var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab; + + Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab); + + // Force a switch to tab 0 if the active tab was a temp tab OR + // if drops before the active index pushed LastTab out of range. + // Otherwise the user keeps their current persistent tab. + var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count; + if (currentWasTempTab || !stillValid) + { + _plugin.WantedTab = 0; + } + } } } From 397c84be2c8e22da9c26e23001f7bb9e47e12614 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:27:53 +0200 Subject: [PATCH 11/23] feat(auto-tell-tabs): spawn temp tab with synchronous history preload --- ChatTwo/AutoTellTabsService.cs | 77 +++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs index ce662c4..661d8bf 100644 --- a/ChatTwo/AutoTellTabsService.cs +++ b/ChatTwo/AutoTellTabsService.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using ChatTwo.Code; +using ChatTwo.GameFunctions.Types; using ChatTwo.Util; namespace ChatTwo; @@ -177,7 +179,80 @@ internal sealed class AutoTellTabsService : IDisposable private void SpawnTempTab((string Name, uint World) partner, Message currentMessage) { - // Stub — implemented in Task 11. + var tab = BuildTempTab(partner.Name, partner.World); + + // Preload first so the tab opens with chronological history above + // the current message — and so a slow DB query never causes a + // visible "empty tab, then history pops in" effect on screen. + PreloadHistory(tab, partner.Name, partner.World); + + tab.AddMessage(currentMessage, unread: true); + Plugin.Config.Tabs.Add(tab); + } + + private static Tab BuildTempTab(string playerName, uint worldRowId) + { + return new Tab + { + Name = FormatTabName(playerName, worldRowId), + IsTempTab = true, + AllSenderMessages = true, + TellTarget = new TellTarget(playerName, worldRowId, 0, TellReason.Direct), + Channel = InputChannel.Tell, + DisplayTimestamp = true, + UnreadMode = UnreadMode.Unseen, + HideWhenInactive = false, + SelectedChannels = new Dictionary + { + [ChatType.TellIncoming] = (ChatSourceExt.All, ChatSourceExt.All), + [ChatType.TellOutgoing] = (ChatSourceExt.All, ChatSourceExt.All), + }, + }; + } + + private static string FormatTabName(string playerName, uint worldRowId) + { + if (Sheets.WorldSheet.TryGetRow(worldRowId, out var worldRow)) + { + return $"{playerName}@{worldRow.Name}"; + } + // World sheet lookup miss is rare (only for FFXIV worlds Dalamud has + // not yet seen). Fall back to the raw RowId so the user still has a + // unique, readable label. + return $"{playerName}@World{worldRowId}"; + } + + private void PreloadHistory(Tab tab, string senderName, uint senderWorld) + { + var preloadCount = Plugin.Config.AutoTellTabsHistoryPreload; + if (preloadCount <= 0) + { + return; + } + + try + { + var history = _store.GetTellHistoryWithSender( + _messageManager.CurrentContentId, + senderName, + senderWorld, + preloadCount); + + // The history list is already oldest-first, so a plain AddPrune + // loop produces the chronological order the user expects to see + // when the tab opens. + foreach (var message in history) + { + tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit); + } + } + catch (Exception ex) + { + // Non-fatal: the tab still spawns, the user just sees only the + // current message. The error logs once with full stack trace + // for diagnosis. + Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed"); + } } internal void MarkGreeted(Tab tab) From 7e3e4c8b729f28cf0e7982fc141e233388076469 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:28:17 +0200 Subject: [PATCH 12/23] feat(auto-tell-tabs): implement greeted toggle with frame-race guard --- ChatTwo/AutoTellTabsService.cs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs index 661d8bf..06454bf 100644 --- a/ChatTwo/AutoTellTabsService.cs +++ b/ChatTwo/AutoTellTabsService.cs @@ -257,12 +257,12 @@ internal sealed class AutoTellTabsService : IDisposable internal void MarkGreeted(Tab tab) { - // Stub — implemented in Task 12. + SetGreeted(tab, true); } internal void UnmarkGreeted(Tab tab) { - // Stub — implemented in Task 12. + SetGreeted(tab, false); } internal bool IsGreeted(Tab tab) @@ -270,6 +270,27 @@ internal sealed class AutoTellTabsService : IDisposable return tab.IsGreeted; } + private void SetGreeted(Tab tab, bool greeted) + { + if (tab == null) + { + return; + } + + lock (_tempTabsLock) + { + // Frame-race guard (E5): the sidebar might still render a tab + // that has already been removed by LRU drop or logout cleanup. + // Silently skip the toggle so we don't mutate stale state. + if (!Plugin.Config.Tabs.Contains(tab)) + { + return; + } + + tab.IsGreeted = greeted; + } + } + private void OnLogout(int type, int code) { lock (_tempTabsLock) From a9d4e9bd69a1978ce957f9ccc24dda0e060b9b07 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:29:09 +0200 Subject: [PATCH 13/23] feat(auto-tell-tabs): wire AutoTellTabsService into plugin lifecycle --- ChatTwo/Plugin.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 04ef3a0..d60ed3a 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -57,6 +57,7 @@ public sealed class Plugin : IDalamudPlugin internal Commands Commands { get; } internal GameFunctions.GameFunctions Functions { get; } internal MessageManager MessageManager { get; } + internal AutoTellTabsService AutoTellTabsService { get; } internal IpcManager Ipc { get; } internal ExtraChat ExtraChat { get; } internal TypingIpc TypingIpc { get; } @@ -210,6 +211,14 @@ public sealed class Plugin : IDalamudPlugin MessageManager = new MessageManager(this); // Does it require UI? + // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the + // MessageManager's MessageProcessed event for live tells and + // to ClientState.Logout for the cleanup pass. Created after + // MessageManager so the constructor can hand off the live + // store and event source. + AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store); + AutoTellTabsService.Initialize(); + // Hellion Chat — daily retention sweep, off-thread so it never // blocks plugin load. Skips itself when disabled or already ran // within the past 24 hours. @@ -300,6 +309,10 @@ public sealed class Plugin : IDalamudPlugin TypingIpc?.Dispose(); ExtraChat?.Dispose(); Ipc?.Dispose(); + // Dispose the Auto-Tell-Tabs service before MessageManager so it + // can cleanly unsubscribe from the MessageProcessed event before + // its source goes away. + AutoTellTabsService?.Dispose(); MessageManager?.DisposeAsync().AsTask().Wait(); Functions?.Dispose(); Commands?.Dispose(); From f8b0804321b251492260a24b287ef1e66785d3a7 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:45:00 +0200 Subject: [PATCH 14/23] fix(auto-tell-tabs): fall back to SeString payloads for tell sender extraction --- ChatTwo/AutoTellTabsService.cs | 25 +++++++++++++++++++------ ChatTwo/Util/ChunkUtil.cs | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs index 06454bf..497ca57 100644 --- a/ChatTwo/AutoTellTabsService.cs +++ b/ChatTwo/AutoTellTabsService.cs @@ -82,8 +82,14 @@ internal sealed class AutoTellTabsService : IDisposable if (partner == null) { // Real message without a player payload — e.g. GM tells, which - // we deliberately skip. Warn once so we notice future regressions. - Plugin.Log.Warning("[AutoTellTabs] Could not extract tell partner from message; skipping spawn."); + // we deliberately skip. The diagnostics make future regressions + // (FFXIV changing tell payload shape, new edge cases) findable + // without having to crank up debug logging at the source. + Plugin.Log.Warning( + $"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, " + + $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, " + + $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, " + + $"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}"); return; } @@ -111,8 +117,12 @@ internal sealed class AutoTellTabsService : IDisposable { if (message.Code.Type == ChatType.TellIncoming) { - // Incoming tell: the sender is the conversation partner. - var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender); + // Incoming tell: the sender is the conversation partner. The + // PlayerPayload normally rides on a chunk's Link slot, but for + // some tell types FFXIV only puts it in the raw SeString — + // fall back to that before giving up. + var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender) + ?? ChunkUtil.TryGetPlayerPayload(message.SenderSource); if (fromSender != null) { return (fromSender.PlayerName, fromSender.World.RowId); @@ -123,8 +133,11 @@ internal sealed class AutoTellTabsService : IDisposable // Outgoing tell: the local player is the sender, the partner shows // up either as a payload in the content (for tells typed via the // Chat 2 input bar) or as the channel's tracked tell target (set by - // the SetContextTellTarget game hook). - var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content); + // the SetContextTellTarget game hook). Same SeString fallback. + var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content) + ?? ChunkUtil.TryGetPlayerPayload(message.ContentSource) + ?? ChunkUtil.TryGetPlayerPayload(message.Sender) + ?? ChunkUtil.TryGetPlayerPayload(message.SenderSource); if (fromContent != null) { return (fromContent.PlayerName, fromContent.World.RowId); diff --git a/ChatTwo/Util/ChunkUtil.cs b/ChatTwo/Util/ChunkUtil.cs index 48702e7..8077ac7 100755 --- a/ChatTwo/Util/ChunkUtil.cs +++ b/ChatTwo/Util/ChunkUtil.cs @@ -415,6 +415,26 @@ internal static class ChunkUtil return null; } + // Fallback for tells where the PlayerPayload lives in the raw SeString + // payload list rather than on a chunk's Link slot. Same semantics as + // the chunk-walking variant above: returns the first PlayerPayload or + // null if the SeString has none. + internal static PlayerPayload? TryGetPlayerPayload(SeString? seString) + { + if (seString == null) + { + return null; + } + foreach (var payload in seString.Payloads) + { + if (payload is PlayerPayload pp) + { + return pp; + } + } + return null; + } + // True when the message's sender (or, as a fallback, content) carries a // PlayerPayload that matches the given identity. Used by both the // Tab.Matches sender filter and the MessageStore tell-history scan. From e91c7a38884b7d32a3c53a53a10ef99f0744615d Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:49:47 +0200 Subject: [PATCH 15/23] fix(auto-tell-tabs): preserve temp tabs across Configuration.UpdateFrom --- ChatTwo/Configuration.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index e783164..6b05665 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -245,7 +245,17 @@ public class Configuration : IPluginConfiguration TooltipOffset = other.TooltipOffset; WindowAlpha = other.WindowAlpha; ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); - Tabs = other.Tabs.Select(t => t.Clone()).ToList(); + + // Hellion Chat — Auto-Tell-Tabs are session-only and therefore + // never present in a disk-loaded copy. Keep the live temp tabs of + // *this* configuration alive across an UpdateFrom so a settings + // save (or sidebar-mode toggle) does not silently destroy the + // user's open tell conversations. Persistent tabs from `other` + // still get the regular clone-replace treatment. + var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList(); + Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t => t.Clone()).ToList(); + Tabs.AddRange(liveTempTabs); + OverrideStyle = other.OverrideStyle; ChosenStyle = other.ChosenStyle; ChatTabForward = other.ChatTabForward; From 7add74dbbe820487e3e95c595889a2a6ac079676 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 13:56:10 +0200 Subject: [PATCH 16/23] feat(auto-tell-tabs): enable sidebar tab view by default for fresh installs --- ChatTwo/Configuration.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index 6b05665..042abcf 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -127,7 +127,12 @@ public class Configuration : IPluginConfiguration public bool MoreCompactPretty; public bool HideSameTimestamps; public bool ShowNoviceNetwork; - public bool SidebarTabView; + // Hellion Chat — vertical sidebar tab layout reads better than the + // horizontal tab strip in the company of Auto-Tell-Tabs (a club + // greeter typically tracks 5–15 simultaneous conversations). Bestand + // users keep their saved value untouched — only fresh installs pick + // up the new default. + public bool SidebarTabView = true; public bool PrintChangelog = true; public bool OnlyPreviewIf; public int PreviewMinimum = 1; From eb379d84eff3d2691bd77f18f86f2330c887a695 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 14:00:29 +0200 Subject: [PATCH 17/23] feat(auto-tell-tabs): add i18n strings and history preload markers --- ChatTwo/AutoTellTabsService.cs | 35 ++++++++++- ChatTwo/Plugin.cs | 5 +- ChatTwo/Resources/HellionStrings.Designer.cs | 25 ++++++++ ChatTwo/Resources/HellionStrings.de.resx | 63 ++++++++++++++++++++ ChatTwo/Resources/HellionStrings.resx | 63 ++++++++++++++++++++ 5 files changed, 185 insertions(+), 6 deletions(-) diff --git a/ChatTwo/AutoTellTabsService.cs b/ChatTwo/AutoTellTabsService.cs index 497ca57..35ca374 100644 --- a/ChatTwo/AutoTellTabsService.cs +++ b/ChatTwo/AutoTellTabsService.cs @@ -3,7 +3,10 @@ using System.Collections.Generic; using System.Linq; using ChatTwo.Code; using ChatTwo.GameFunctions.Types; +using ChatTwo.Resources; using ChatTwo.Util; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; namespace ChatTwo; @@ -251,6 +254,14 @@ internal sealed class AutoTellTabsService : IDisposable senderWorld, preloadCount); + if (history.Count == 0) + { + // No prior tells with this player — leave the tab to start + // empty so the user does not see a "history loaded" marker + // sitting alone above the very first message. + return; + } + // The history list is already oldest-first, so a plain AddPrune // loop produces the chronological order the user expects to see // when the tab opens. @@ -258,16 +269,34 @@ internal sealed class AutoTellTabsService : IDisposable { tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit); } + + // Visible separator between the loaded history and the live + // tell that triggered this spawn. Goes in last so it sorts + // after the historical messages but before the current one. + tab.Messages.AddPrune( + MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator), + MessageManager.MessageDisplayLimit); } catch (Exception ex) { - // Non-fatal: the tab still spawns, the user just sees only the - // current message. The error logs once with full stack trace - // for diagnosis. + // Non-fatal: the tab still spawns, but the user gets a visible + // notice instead of silently missing history. The error logs + // once with full stack trace for diagnosis. Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed"); + tab.Messages.AddPrune( + MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError), + MessageManager.MessageDisplayLimit); } } + private static Message MakeSystemMarker(string text) + { + var seString = new SeStringBuilder().AddText(text).Build(); + var chunks = ChunkUtil.ToChunks(seString, ChunkSource.Content, ChatType.System).ToList(); + var code = new ChatCode((XivChatType)ChatType.System, 0, 0); + return Message.FakeMessage(chunks, code); + } + internal void MarkGreeted(Tab tab) { SetGreeted(tab, true); diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index d60ed3a..63b101e 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -184,11 +184,10 @@ public sealed class Plugin : IDalamudPlugin Config.Version = 9; SaveConfig(); - // TODO Task 14: replace with HellionStrings.AutoTellTabs_Migration_Title / _Content Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification { - Title = "Auto-Tell-Tabs", - Content = "Auto-Tell-Tabs sind ab Version 0.4.0 standardmäßig aktiv. Du kannst sie im Chat-Tab deaktivieren oder anpassen.", + Title = HellionStrings.AutoTellTabs_Migration_Title, + Content = HellionStrings.AutoTellTabs_Migration_Content, Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, InitialDuration = TimeSpan.FromSeconds(20), }); diff --git a/ChatTwo/Resources/HellionStrings.Designer.cs b/ChatTwo/Resources/HellionStrings.Designer.cs index 02e9289..86bc5c9 100644 --- a/ChatTwo/Resources/HellionStrings.Designer.cs +++ b/ChatTwo/Resources/HellionStrings.Designer.cs @@ -164,4 +164,29 @@ internal class HellionStrings internal static string About_Localization_P1 => Get(nameof(About_Localization_P1)); internal static string About_Localization_P2 => Get(nameof(About_Localization_P2)); internal static string About_Translators_TreeNode => Get(nameof(About_Translators_TreeNode)); + + // Hellion Chat — Auto-Tell-Tabs runtime strings + internal static string AutoTellTabs_Migration_Title => Get(nameof(AutoTellTabs_Migration_Title)); + internal static string AutoTellTabs_Migration_Content => Get(nameof(AutoTellTabs_Migration_Content)); + internal static string AutoTellTabs_SectionHeader => Get(nameof(AutoTellTabs_SectionHeader)); + internal static string AutoTellTabs_HistorySeparator => Get(nameof(AutoTellTabs_HistorySeparator)); + internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError)); + internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip)); + internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip)); + + // Hellion Chat — Auto-Tell-Tabs Chat settings tab + internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title)); + internal static string ChatLog_AutoTellTabs_Enable_Name => Get(nameof(ChatLog_AutoTellTabs_Enable_Name)); + internal static string ChatLog_AutoTellTabs_Enable_Description => Get(nameof(ChatLog_AutoTellTabs_Enable_Description)); + internal static string ChatLog_AutoTellTabs_Limit_Name => Get(nameof(ChatLog_AutoTellTabs_Limit_Name)); + internal static string ChatLog_AutoTellTabs_Limit_Description => Get(nameof(ChatLog_AutoTellTabs_Limit_Description)); + internal static string ChatLog_AutoTellTabs_Compact_Name => Get(nameof(ChatLog_AutoTellTabs_Compact_Name)); + internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description)); + internal static string ChatLog_AutoTellTabs_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint)); + + // Hellion Chat — Auto-Tell-Tabs Privacy settings tab + internal static string Privacy_AutoTellTabs_Section_Title => Get(nameof(Privacy_AutoTellTabs_Section_Title)); + internal static string Privacy_AutoTellTabs_Preload_Name => Get(nameof(Privacy_AutoTellTabs_Preload_Name)); + internal static string Privacy_AutoTellTabs_Preload_Description => Get(nameof(Privacy_AutoTellTabs_Preload_Description)); + internal static string Privacy_AutoTellTabs_Preload_Hint => Get(nameof(Privacy_AutoTellTabs_Preload_Hint)); } diff --git a/ChatTwo/Resources/HellionStrings.de.resx b/ChatTwo/Resources/HellionStrings.de.resx index d13339b..6e82d8a 100644 --- a/ChatTwo/Resources/HellionStrings.de.resx +++ b/ChatTwo/Resources/HellionStrings.de.resx @@ -366,4 +366,67 @@ Chat-2-Community-Übersetzer (Upstream) + + + + Auto-Tell-Tabs + + + Auto-Tell-Tabs sind ab Version 0.4.0 standardmäßig aktiv. Du kannst sie im Chat-Tab deaktivieren oder anpassen. + + + Aktive Tells + + + — Frühere Unterhaltungen — + + + Verlauf konnte nicht geladen werden. + + + Als begrüßt markiert. Klicken um die Markierung zu entfernen. + + + Als begrüßt markieren. + + + + + Auto-Tell-Tabs + + + Bei jedem /tell automatisch einen Tab pro Gesprächspartner öffnen + + + Sobald du einen /tell empfängst oder sendest, wird automatisch ein temporärer Tab für diesen Spieler geöffnet. Die Tabs verschwinden beim Logout. + + + Maximale Anzahl der Auto-Tell-Tabs + + + Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell. + + + Kompakte Anzeige + + + Zeigt nur einen dünnen Separator zwischen normalen Tabs und Auto-Tell-Tabs, ohne Sektions-Header. + + + Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen. + + + + + Tell-Verlauf in Auto-Tabs + + + Anzahl der vorgeladenen Tells + + + Wie viele frühere Tell-Nachrichten beim Öffnen eines Auto-Tell-Tabs aus der Datenbank geladen werden. 0 deaktiviert die Vorladung. + + + Greift nur, wenn Auto-Tell-Tabs im Chat-Tab aktiviert sind. + diff --git a/ChatTwo/Resources/HellionStrings.resx b/ChatTwo/Resources/HellionStrings.resx index 52c1d43..58fc3bc 100644 --- a/ChatTwo/Resources/HellionStrings.resx +++ b/ChatTwo/Resources/HellionStrings.resx @@ -366,4 +366,67 @@ Chat 2 community translators (upstream) + + + + Auto-Tell-Tabs + + + Auto-Tell-Tabs are enabled by default starting with version 0.4.0. You can disable or fine-tune them in the Chat tab. + + + Active Tells + + + — Earlier conversations — + + + History could not be loaded. + + + Marked as greeted. Click to remove the marker. + + + Mark as greeted. + + + + + Auto-Tell-Tabs + + + Open a tab automatically for each tell partner + + + When you receive or send a /tell, a temporary tab dedicated to that player is opened automatically. Tabs vanish on logout. + + + Maximum number of auto tell tabs + + + When the limit is reached, greeted tabs with the oldest activity are dropped first. The change applies on the next /tell. + + + Compact display + + + Show only a thin separator between persistent tabs and auto tell tabs, without the section header. + + + The number of preloaded tells is configured in the Privacy tab. + + + + + Tell history in auto tabs + + + Number of preloaded tells + + + How many earlier tell messages are loaded from the database when an auto tell tab is opened. 0 disables the preload. + + + Only takes effect when auto tell tabs are enabled in the Chat tab. + From 74bdc4f927d9d14841bd9a4d86229b9bee4cfe35 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 14:06:27 +0200 Subject: [PATCH 18/23] feat(auto-tell-tabs): add hint string for plugins that suppress tells (XIV Messanger) --- ChatTwo/Resources/HellionStrings.Designer.cs | 1 + ChatTwo/Resources/HellionStrings.de.resx | 3 +++ ChatTwo/Resources/HellionStrings.resx | 3 +++ 3 files changed, 7 insertions(+) diff --git a/ChatTwo/Resources/HellionStrings.Designer.cs b/ChatTwo/Resources/HellionStrings.Designer.cs index 86bc5c9..e05d332 100644 --- a/ChatTwo/Resources/HellionStrings.Designer.cs +++ b/ChatTwo/Resources/HellionStrings.Designer.cs @@ -183,6 +183,7 @@ internal class HellionStrings internal static string ChatLog_AutoTellTabs_Compact_Name => Get(nameof(ChatLog_AutoTellTabs_Compact_Name)); internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description)); internal static string ChatLog_AutoTellTabs_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint)); + internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint)); // Hellion Chat — Auto-Tell-Tabs Privacy settings tab internal static string Privacy_AutoTellTabs_Section_Title => Get(nameof(Privacy_AutoTellTabs_Section_Title)); diff --git a/ChatTwo/Resources/HellionStrings.de.resx b/ChatTwo/Resources/HellionStrings.de.resx index 6e82d8a..17401fd 100644 --- a/ChatTwo/Resources/HellionStrings.de.resx +++ b/ChatTwo/Resources/HellionStrings.de.resx @@ -415,6 +415,9 @@ Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen. + + Hinweis: Falls XIV Messanger oder ein ähnliches Plugin Tells unterdrückt, dort die Option „Suppress DMs" deaktivieren, damit Hellion Chat Tells empfangen und die Auto-Tabs öffnen kann. + diff --git a/ChatTwo/Resources/HellionStrings.resx b/ChatTwo/Resources/HellionStrings.resx index 58fc3bc..3402f84 100644 --- a/ChatTwo/Resources/HellionStrings.resx +++ b/ChatTwo/Resources/HellionStrings.resx @@ -415,6 +415,9 @@ The number of preloaded tells is configured in the Privacy tab. + + Heads-up: if XIV Messanger or a similar plugin is suppressing direct messages, turn its "Suppress DMs" option off so Hellion Chat can receive tells and open the auto tabs. + From 3f35b76c54e71581b7e4a62da4982fce0582b824 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 14:08:54 +0200 Subject: [PATCH 19/23] feat(auto-tell-tabs): render section header and greeted toggle in tab sidebar --- ChatTwo/Ui/ChatLogWindow.cs | 60 ++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index 018085c..fcf3299 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -1303,14 +1303,72 @@ public sealed class ChatLogWindow : Window if (child) { var previousTab = Plugin.CurrentTab; + // Hellion Chat — auto-tell-tabs section divider rendered + // exactly once before the first temp tab, with a live unit + // counter pulled directly from the tab list. + var tempTabHeaderRendered = false; + var tempTabCount = Plugin.Config.Tabs.Count(t => t.IsTempTab); + for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++) { var tab = Plugin.Config.Tabs[tabI]; if (tab.PopOut) continue; + if (tab.IsTempTab && !tempTabHeaderRendered) + { + ImGui.Separator(); + if (!Plugin.Config.AutoTellTabsCompactDisplay) + { + ImGui.TextDisabled($"{HellionStrings.AutoTellTabs_SectionHeader} ({tempTabCount})"); + } + tempTabHeaderRendered = true; + } + var unread = tabI == Plugin.LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})"; - var clicked = ImGui.Selectable($"{tab.Name}{unread}###log-tab-{tabI}", Plugin.LastTab == tabI || Plugin.WantedTab == tabI); + var selectableLabel = $"{tab.Name}{unread}###log-tab-{tabI}"; + var isCurrentTab = Plugin.LastTab == tabI || Plugin.WantedTab == tabI; + + if (tab.IsTempTab) + { + // Greeted toggle sits left of the selectable so the + // click areas stay separate. The icon also doubles + // as the visual "I'm done with this person" cue. + var greetedIcon = tab.IsGreeted ? FontAwesomeIcon.CheckCircle : FontAwesomeIcon.Check; + var greetedTooltip = tab.IsGreeted + ? HellionStrings.AutoTellTabs_GreetedTooltip + : HellionStrings.AutoTellTabs_UnGreetedTooltip; + + if (ImGuiUtil.IconButton(greetedIcon, $"greeted-{tabI}", greetedTooltip)) + { + if (tab.IsGreeted) + { + Plugin.AutoTellTabsService.UnmarkGreeted(tab); + } + else + { + Plugin.AutoTellTabsService.MarkGreeted(tab); + } + } + ImGui.SameLine(); + } + + bool clicked; + if (tab.IsTempTab && tab.IsGreeted) + { + // Dim the tab name once the user marked the partner + // as greeted, so a glance at the sidebar tells them + // who still needs attention. + using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled))) + { + clicked = ImGui.Selectable(selectableLabel, isCurrentTab); + } + } + else + { + clicked = ImGui.Selectable(selectableLabel, isCurrentTab); + } + DrawTabContextMenu(tab, tabI); if (!clicked && Plugin.WantedTab != tabI) From 757370dd53297e9e35ef2d17ce803f97dd39bf86 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 14:19:35 +0200 Subject: [PATCH 20/23] feat(auto-tell-tabs): add settings sections in chat and privacy tabs with help-marker pattern --- ChatTwo/Ui/SettingsTabs/ChatLog.cs | 37 ++++++++++++++++++++++++++++++ ChatTwo/Ui/SettingsTabs/Privacy.cs | 28 ++++++++++++++++++++++ ChatTwo/Util/ImGuiUtil.cs | 18 +++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/ChatTwo/Ui/SettingsTabs/ChatLog.cs b/ChatTwo/Ui/SettingsTabs/ChatLog.cs index b8bb944..426a34c 100644 --- a/ChatTwo/Ui/SettingsTabs/ChatLog.cs +++ b/ChatTwo/Ui/SettingsTabs/ChatLog.cs @@ -1,6 +1,7 @@ using ChatTwo.Resources; using ChatTwo.Util; using Dalamud.Interface.Style; +using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Bindings.ImGui; @@ -92,6 +93,12 @@ internal sealed class ChatLog : ISettingsTab Plugin.ChatLogWindow.Position = pos; ImGuiUtil.WarningText(Language.Options_AdjustPosition_Warning); ImGui.Spacing(); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + DrawAutoTellTabsSection(); } if (!Mutable.OverrideStyle) @@ -116,4 +123,34 @@ internal sealed class ChatLog : ISettingsTab ImGui.Spacing(); } + + private void DrawAutoTellTabsSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.ChatLog_AutoTellTabs_Section_Title); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Enable_Name, ref Mutable.EnableAutoTellTabs); + ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Enable_Description); + + ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale); + var limit = Mutable.AutoTellTabsLimit; + if (ImGui.SliderInt(HellionStrings.ChatLog_AutoTellTabs_Limit_Name, ref limit, 1, 50)) + { + Mutable.AutoTellTabsLimit = limit; + } + ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Limit_Description); + + ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay); + ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description); + + ImGui.Spacing(); + ImGuiUtil.HelpText(HellionStrings.ChatLog_AutoTellTabs_PreloadHint); + + ImGui.Spacing(); + ImGuiUtil.WarningText(HellionStrings.ChatLog_AutoTellTabs_ConflictHint); + } + } } diff --git a/ChatTwo/Ui/SettingsTabs/Privacy.cs b/ChatTwo/Ui/SettingsTabs/Privacy.cs index 9db5cfe..46d3ca9 100644 --- a/ChatTwo/Ui/SettingsTabs/Privacy.cs +++ b/ChatTwo/Ui/SettingsTabs/Privacy.cs @@ -4,6 +4,7 @@ using ChatTwo.Privacy; using ChatTwo.Resources; using ChatTwo.Util; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Bindings.ImGui; @@ -186,6 +187,33 @@ internal sealed class Privacy : ISettingsTab ImGui.Spacing(); DrawExportSection(); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + DrawAutoTellTabsPreloadSection(); + } + + private void DrawAutoTellTabsPreloadSection() + { + using var tree = ImRaii.TreeNode(HellionStrings.Privacy_AutoTellTabs_Section_Title); + if (!tree.Success) + return; + + using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) + { + var preload = Mutable.AutoTellTabsHistoryPreload; + ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale); + if (ImGui.SliderInt(HellionStrings.Privacy_AutoTellTabs_Preload_Name, ref preload, 0, 100)) + { + Mutable.AutoTellTabsHistoryPreload = preload; + } + ImGuiUtil.HelpMarker(HellionStrings.Privacy_AutoTellTabs_Preload_Description); + + ImGui.Spacing(); + ImGuiUtil.HelpText(HellionStrings.Privacy_AutoTellTabs_Preload_Hint); + } } private void DrawExportSection() diff --git a/ChatTwo/Util/ImGuiUtil.cs b/ChatTwo/Util/ImGuiUtil.cs index 75b17fb..366dcce 100755 --- a/ChatTwo/Util/ImGuiUtil.cs +++ b/ChatTwo/Util/ImGuiUtil.cs @@ -215,6 +215,24 @@ internal static class ImGuiUtil ImGui.TextUnformatted(text); } + // Hellion Chat — compact help affordance: a dimmed "(?)" glyph rendered + // on the same line as the previous item, with the long-form description + // tucked into a hover tooltip. Lets us keep the settings panes scannable + // instead of stacking a wall of HelpText paragraphs under every option. + internal static void HelpMarker(string description) + { + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled])) + ImGui.TextUnformatted("(?)"); + + if (!ImGui.IsItemHovered()) + return; + + using var tooltip = ImRaii.Tooltip(); + using (ImRaii.TextWrapPos(35.0f * ImGui.GetFontSize())) + ImGui.TextUnformatted(description); + } + internal static void WarningText(string text, bool wrap = true) { var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent(); From bb6259e14db69fd857bd5ee9862c9d4f7b4a9c08 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 14:24:13 +0200 Subject: [PATCH 21/23] fix(auto-tell-tabs): make greeted toggle button more compact and transparent --- ChatTwo/Ui/ChatLogWindow.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index fcf3299..d222db1 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -1334,20 +1334,26 @@ public sealed class ChatLogWindow : Window // Greeted toggle sits left of the selectable so the // click areas stay separate. The icon also doubles // as the visual "I'm done with this person" cue. + // Compact frame padding keeps the icon dezent next + // to the tab name instead of a chunky button block. var greetedIcon = tab.IsGreeted ? FontAwesomeIcon.CheckCircle : FontAwesomeIcon.Check; var greetedTooltip = tab.IsGreeted ? HellionStrings.AutoTellTabs_GreetedTooltip : HellionStrings.AutoTellTabs_UnGreetedTooltip; - if (ImGuiUtil.IconButton(greetedIcon, $"greeted-{tabI}", greetedTooltip)) + using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(2, 1))) + using (ImRaii.PushColor(ImGuiCol.Button, 0)) { - if (tab.IsGreeted) + if (ImGuiUtil.IconButton(greetedIcon, $"greeted-{tabI}", greetedTooltip)) { - Plugin.AutoTellTabsService.UnmarkGreeted(tab); - } - else - { - Plugin.AutoTellTabsService.MarkGreeted(tab); + if (tab.IsGreeted) + { + Plugin.AutoTellTabsService.UnmarkGreeted(tab); + } + else + { + Plugin.AutoTellTabsService.MarkGreeted(tab); + } } } ImGui.SameLine(); From 39cd7ab801fdb844ea79643fcb93eebb7e71a060 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 14:26:13 +0200 Subject: [PATCH 22/23] feat(auto-tell-tabs): make greeted toggle button opt-in (default off, greeter-specific) --- ChatTwo/Configuration.cs | 6 ++++++ ChatTwo/Resources/HellionStrings.Designer.cs | 2 ++ ChatTwo/Resources/HellionStrings.de.resx | 6 ++++++ ChatTwo/Resources/HellionStrings.resx | 6 ++++++ ChatTwo/Ui/ChatLogWindow.cs | 6 ++++-- ChatTwo/Ui/SettingsTabs/ChatLog.cs | 3 +++ 6 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index 042abcf..915f5e1 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -95,6 +95,11 @@ public class Configuration : IPluginConfiguration // Number of prior tells to preload from the message store when an // auto tell tab is spawned. Range 0–100; 0 disables preload. public int AutoTellTabsHistoryPreload = 20; + // Show the greeter "marked-as-greeted" toggle button next to each + // temp tab and dim the tab name when set. Off by default because the + // workflow is specific to club-greeter use cases — most users just + // want the auto tabs themselves without the extra UI affordance. + public bool AutoTellTabsShowGreetedToggle; public int GetRetentionDays(ChatType type) { @@ -284,6 +289,7 @@ public class Configuration : IPluginConfiguration AutoTellTabsLimit = other.AutoTellTabsLimit; AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload; + AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle; } } diff --git a/ChatTwo/Resources/HellionStrings.Designer.cs b/ChatTwo/Resources/HellionStrings.Designer.cs index e05d332..b2a7c9e 100644 --- a/ChatTwo/Resources/HellionStrings.Designer.cs +++ b/ChatTwo/Resources/HellionStrings.Designer.cs @@ -182,6 +182,8 @@ internal class HellionStrings internal static string ChatLog_AutoTellTabs_Limit_Description => Get(nameof(ChatLog_AutoTellTabs_Limit_Description)); internal static string ChatLog_AutoTellTabs_Compact_Name => Get(nameof(ChatLog_AutoTellTabs_Compact_Name)); internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description)); + internal static string ChatLog_AutoTellTabs_GreetedToggle_Name => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Name)); + internal static string ChatLog_AutoTellTabs_GreetedToggle_Description => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Description)); internal static string ChatLog_AutoTellTabs_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint)); internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint)); diff --git a/ChatTwo/Resources/HellionStrings.de.resx b/ChatTwo/Resources/HellionStrings.de.resx index 17401fd..c4d2826 100644 --- a/ChatTwo/Resources/HellionStrings.de.resx +++ b/ChatTwo/Resources/HellionStrings.de.resx @@ -412,6 +412,12 @@ Zeigt nur einen dünnen Separator zwischen normalen Tabs und Auto-Tell-Tabs, ohne Sektions-Header. + + „Als begrüßt markieren"-Button anzeigen + + + Fügt neben jedem Auto-Tell-Tab einen Klick-Button hinzu, um einen Gesprächspartner als bereits begrüßt zu markieren — der Tab-Name wird dann gedimmt. Nützlich für Club-Greeter, die parallel viele Konversationen führen. Standardmäßig aus. + Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen. diff --git a/ChatTwo/Resources/HellionStrings.resx b/ChatTwo/Resources/HellionStrings.resx index 3402f84..01cb688 100644 --- a/ChatTwo/Resources/HellionStrings.resx +++ b/ChatTwo/Resources/HellionStrings.resx @@ -412,6 +412,12 @@ Show only a thin separator between persistent tabs and auto tell tabs, without the section header. + + Show "mark as greeted" button + + + Adds a click-to-toggle button next to each auto tell tab to mark a partner as already greeted, dimming the tab name when set. Useful for club greeters tracking many parallel conversations; off by default. + The number of preloaded tells is configured in the Privacy tab. diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index d222db1..0ba15f6 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -1329,7 +1329,9 @@ public sealed class ChatLogWindow : Window var selectableLabel = $"{tab.Name}{unread}###log-tab-{tabI}"; var isCurrentTab = Plugin.LastTab == tabI || Plugin.WantedTab == tabI; - if (tab.IsTempTab) + var showGreetedAffordance = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle; + + if (showGreetedAffordance) { // Greeted toggle sits left of the selectable so the // click areas stay separate. The icon also doubles @@ -1360,7 +1362,7 @@ public sealed class ChatLogWindow : Window } bool clicked; - if (tab.IsTempTab && tab.IsGreeted) + if (showGreetedAffordance && tab.IsGreeted) { // Dim the tab name once the user marked the partner // as greeted, so a glance at the sidebar tells them diff --git a/ChatTwo/Ui/SettingsTabs/ChatLog.cs b/ChatTwo/Ui/SettingsTabs/ChatLog.cs index 426a34c..a88f5be 100644 --- a/ChatTwo/Ui/SettingsTabs/ChatLog.cs +++ b/ChatTwo/Ui/SettingsTabs/ChatLog.cs @@ -146,6 +146,9 @@ internal sealed class ChatLog : ISettingsTab ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay); ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description); + ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle); + ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description); + ImGui.Spacing(); ImGuiUtil.HelpText(HellionStrings.ChatLog_AutoTellTabs_PreloadHint); From e9ec587e3bdc70f495cd3055fea2d5c51b4ded90 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Sat, 2 May 2026 14:33:52 +0200 Subject: [PATCH 23/23] chore: bump to 0.4.0 with auto-tell-tabs changelog --- ChatTwo/ChatTwo.csproj | 2 +- ChatTwo/HellionChat.yaml | 45 ++++++++++++++++++++++++++++++++++++++++ repo.json | 12 +++++------ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/ChatTwo/ChatTwo.csproj b/ChatTwo/ChatTwo.csproj index 6cce6af..d1d9dce 100755 --- a/ChatTwo/ChatTwo.csproj +++ b/ChatTwo/ChatTwo.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 0.3.1 + 0.4.0 enable