Compare commits

...

27 Commits

Author SHA1 Message Date
JonKazama-Hellion d4aa3971c5 merge Auto-Tell-Tabs Merge branch'feature/auto-tell-tabs' 2026-05-02 14:48:37 +02:00
JonKazama-Hellion e9ec587e3b chore: bump to 0.4.0 with auto-tell-tabs changelog 2026-05-02 14:33:52 +02:00
JonKazama-Hellion 39cd7ab801 feat(auto-tell-tabs): make greeted toggle button opt-in (default off, greeter-specific) 2026-05-02 14:26:13 +02:00
JonKazama-Hellion bb6259e14d fix(auto-tell-tabs): make greeted toggle button more compact and transparent 2026-05-02 14:24:13 +02:00
JonKazama-Hellion 757370dd53 feat(auto-tell-tabs): add settings sections in chat and privacy tabs with help-marker pattern 2026-05-02 14:19:35 +02:00
JonKazama-Hellion 3f35b76c54 feat(auto-tell-tabs): render section header and greeted toggle in tab sidebar 2026-05-02 14:08:54 +02:00
JonKazama-Hellion 74bdc4f927 feat(auto-tell-tabs): add hint string for plugins that suppress tells (XIV Messanger) 2026-05-02 14:06:27 +02:00
JonKazama-Hellion eb379d84ef feat(auto-tell-tabs): add i18n strings and history preload markers 2026-05-02 14:00:29 +02:00
JonKazama-Hellion 7add74dbbe feat(auto-tell-tabs): enable sidebar tab view by default for fresh installs 2026-05-02 13:56:10 +02:00
JonKazama-Hellion e91c7a3888 fix(auto-tell-tabs): preserve temp tabs across Configuration.UpdateFrom 2026-05-02 13:49:47 +02:00
JonKazama-Hellion f8b0804321 fix(auto-tell-tabs): fall back to SeString payloads for tell sender extraction 2026-05-02 13:45:00 +02:00
JonKazama-Hellion a9d4e9bd69 feat(auto-tell-tabs): wire AutoTellTabsService into plugin lifecycle 2026-05-02 13:29:09 +02:00
JonKazama-Hellion 7e3e4c8b72 feat(auto-tell-tabs): implement greeted toggle with frame-race guard 2026-05-02 13:28:17 +02:00
JonKazama-Hellion 397c84be2c feat(auto-tell-tabs): spawn temp tab with synchronous history preload 2026-05-02 13:27:53 +02:00
JonKazama-Hellion 269708150d feat(auto-tell-tabs): implement logout cleanup of temp tabs 2026-05-02 13:02:15 +02:00
JonKazama-Hellion a2977ef75b feat(auto-tell-tabs): implement LRU drop with greeted priority 2026-05-02 13:01:51 +02:00
JonKazama-Hellion baa4d011e8 feat(auto-tell-tabs): implement HandleTell with sender extraction and tab lookup 2026-05-02 13:01:27 +02:00
JonKazama-Hellion 4810e8b518 feat(auto-tell-tabs): add AutoTellTabsService skeleton with lifecycle 2026-05-02 12:59:58 +02:00
JonKazama-Hellion 133f5c536f feat(auto-tell-tabs): emit MessageProcessed event after tab routing 2026-05-02 12:57:49 +02:00
JonKazama-Hellion 92bb368d2b feat(auto-tell-tabs): add GetTellHistoryWithSender query and ChunkUtil sender helper 2026-05-02 12:52:58 +02:00
JonKazama-Hellion 07f47f32e3 feat(auto-tell-tabs): bump configuration version to 9 with migration notice 2026-05-02 12:40:22 +02:00
JonKazama-Hellion 141fcbf074 feat(auto-tell-tabs): filter temp tabs from config save and load (defense-in-depth) 2026-05-02 12:37:06 +02:00
JonKazama-Hellion 32c410e8e2 feat(auto-tell-tabs): add IsGreeted flag and Tab.Matches sender filter for temp tabs 2026-05-02 12:30:30 +02:00
JonKazama-Hellion 824037e55f feat(auto-tell-tabs): add configuration fields for auto tell tabs 2026-05-02 12:27:55 +02:00
JonKazama-Hellion 173cb76bea fix: ignore brainstorming and spec workspace directories 2026-05-02 12:20:55 +02:00
JonKazama-Hellion 2736551505 Bump to 0.3.1 with the upstream emote regression fix
Quick patch release. The previous commit on this branch is a
straight cherry-pick of Infi's upstream "Fix a regression from
API 15 updates" (ff899ff), which restores BetterTTV emote
deserialisation by switching the Emote and Top100 DTOs from
public fields to public properties so System.Text.Json picks up
the [JsonPropertyName] attributes correctly.

Without this, a fresh Hellion Chat install would happily fetch
emote JSON from BetterTTV but every entry would deserialise to
empty defaults, leaving the cache silently empty.

Manifest version bumps from 0.3.0 to 0.3.1 in csproj, the
HellionChat.yaml manifest plus its changelog, the custom-repo
repo.json (assembly version, testing assembly version and the
three download links), and the README version banner. The 0.3.0
changelog entry stays underneath in chronological order.

Build (Release) verified clean.
2026-05-02 04:11:13 +02:00
Infi 0679a0e57a Fix a regression from API 15 updates
(cherry picked from commit ff899ffe54ef76bcdf9c13b5690957c5b5e17637)
2026-05-02 04:10:22 +02:00
20 changed files with 1181 additions and 19 deletions
+2
View File
@@ -372,6 +372,8 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
#Specs und Plan datein
/.superpowers/
TestResults
*.db-shm
*.db-wal
+167
View File
@@ -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);
}
}
+358
View File
@@ -0,0 +1,358 @@
using System;
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;
// 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)
{
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. 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;
}
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. 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);
}
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). 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);
}
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()
{
// 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)
{
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, (ChatSource, ChatSource)>
{
[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);
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.
foreach (var message in history)
{
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, 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);
}
internal void UnmarkGreeted(Tab tab)
{
SetGreeted(tab, false);
}
internal bool IsGreeted(Tab tab)
{
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)
{
// 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;
}
}
}
}
+1 -1
View File
@@ -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. -->
<Version>0.3.0</Version>
<Version>0.4.0</Version>
<ImplicitUsings>enable</ImplicitUsings>
<!-- HellionChat fork: assembly is renamed so Dalamud uses
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
+64 -4
View File
@@ -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;
@@ -33,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;
@@ -81,6 +82,25 @@ 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 (150). 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 0100; 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)
{
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
@@ -112,7 +132,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 515 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;
@@ -230,7 +255,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;
@@ -249,6 +284,12 @@ 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;
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
}
}
@@ -324,9 +365,27 @@ 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 ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
}
return true;
}
public void AddMessage(Message message, bool unread = true)
@@ -375,6 +434,7 @@ public class Tab
IsTempTab = IsTempTab,
AllSenderMessages = AllSenderMessages,
TellTarget = TellTarget.From(TellTarget),
IsGreeted = IsGreeted,
};
}
+5 -5
View File
@@ -32,23 +32,23 @@ public static class EmoteCache
private struct Top100()
{
[JsonPropertyName("emote")]
public Emote Emote = default;
public Emote Emote { get; set; }
[JsonPropertyName("id")]
public string Id = string.Empty;
public string Id { get; set; }
}
[Serializable]
public struct Emote()
{
[JsonPropertyName("id")]
public string Id = string.Empty;
public string Id { get; set; }
[JsonPropertyName("code")]
public string Code = string.Empty;
public string Code { get; set; }
[JsonPropertyName("imageType")]
public string ImageType = string.Empty;
public string ImageType { get; set; }
}
public enum LoadingState
+62
View File
@@ -40,6 +40,68 @@ tags:
- Replacement
- Privacy
changelog: |-
**Hellion Chat 0.4.0 — Auto-Tell-Tabs**
Auto-Tell-Tabs lets you turn each /tell into a session-only tab
dedicated to that conversation partner. The original use case is
the FFXIV club greeter who has to track 515 parallel "hi, welcome"
exchanges; everyone else can disable the feature in one click and
go back to a single Tell Exclusive tab.
What lands in this release:
- Auto-spawn temp tab "Name@World" on /tell (incoming and outgoing)
- Tab limit (default 15, range 150) with LRU drop that prefers
greeted tabs first, then sorts by last activity
- History preload from the local message store (default 20 tells,
range 0100) with a "— Earlier conversations —" separator above
the live tell that triggered the spawn
- Optional "mark as greeted" toggle button (off by default,
greeter-specific) that dims the tab name and lets you flip the
status
- Section header "Active Tells (n)" or compact-mode separator in
the sidebar between persistent tabs and the temp tabs
- Settings UI under Chat (toggle / limit / compact / greeted-toggle)
and Privacy (history preload count), with hover-tooltip help
markers replacing the previous wall-of-text descriptions for the
new sections
- Save and load filters strip temp tabs from the on-disk config so
a crash or a sidebar-mode toggle never persists or wipes them
Compatibility note: if XIV Messanger or another plugin is
suppressing direct messages, disable its "Suppress DMs" option so
Hellion Chat can receive tells and open the auto tabs.
Configuration version bumps from 8 to 9. Existing users get a one-
shot notification on the first start, defaults are seeded by
property initializers, persistent tabs are untouched.
The vertical sidebar tab view becomes the default for fresh
installs; existing users keep their saved preference.
Inspired by the per-sender tab pattern in XIV InstantMessenger
(Limiana, AGPL-3.0). No code was ported across the licence
boundary; only the architectural concept influenced this design.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.3.1 — Upstream emote regression fix**
Cherry-picks Infi's upstream commit ff899ff "Fix a regression
from API 15 updates" which changes the BetterTTV emote DTOs
(Emote and Top100) from public fields to public properties.
System.Text.Json under the API 15 toolchain only honours the
[JsonPropertyName] attribute on properties, so the previous
field-based version deserialised every fetched emote into empty
default values. Result: BetterTTV emotes were silently broken
on fresh installs. The fix is six lines and applies cleanly on
top of our defensive null-check from earlier; the EmoteCache
path-traversal hardening from 0.3.0 stays as it is.
Authorship of the fix is preserved with git cherry-pick -x, so
Infi shows up as the author on the commit. Thanks to him for
catching it in the upstream codebase.
**Hellion Chat 0.3.0 — Audit hardening, brand sweep and rebrand of slash commands**
This release closes the remaining audit follow-ups from the
+9
View File
@@ -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<Message>? 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
+78
View File
@@ -602,6 +602,84 @@ internal class MessageStore : IDisposable
return new MessageEnumerator(cmd.ExecuteReader());
}
/// <summary>
/// Hellion Chat — Auto-Tell-Tabs history preload.
///
/// Returns up to <paramref name="limit"/> 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.
///
/// <paramref name="sqlScanLimit"/> 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.
/// </summary>
internal IReadOnlyList<Message> 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<Message>();
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;
}
/// <summary>
/// Marks a message as deleted so it won't get returned in queries.
/// </summary>
+48
View File
@@ -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; }
@@ -100,6 +101,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
@@ -167,6 +174,25 @@ 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();
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
{
Title = HellionStrings.AutoTellTabs_Migration_Title,
Content = HellionStrings.AutoTellTabs_Migration_Content,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(20),
});
}
if (Config.Tabs.Count == 0)
Config.Tabs.Add(TabsUtil.VanillaGeneral);
@@ -184,6 +210,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.
@@ -274,6 +308,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();
@@ -491,7 +529,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)
+28
View File
@@ -164,4 +164,32 @@ 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_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));
// 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));
}
+72
View File
@@ -366,4 +366,76 @@
<data name="About_Translators_TreeNode" xml:space="preserve">
<value>Chat-2-Community-Übersetzer (Upstream)</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Runtime-Strings) -->
<data name="AutoTellTabs_Migration_Title" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="AutoTellTabs_Migration_Content" xml:space="preserve">
<value>Auto-Tell-Tabs sind ab Version 0.4.0 standardmäßig aktiv. Du kannst sie im Chat-Tab deaktivieren oder anpassen.</value>
</data>
<data name="AutoTellTabs_SectionHeader" xml:space="preserve">
<value>Aktive Tells</value>
</data>
<data name="AutoTellTabs_HistorySeparator" xml:space="preserve">
<value>— Frühere Unterhaltungen —</value>
</data>
<data name="AutoTellTabs_HistoryLoadError" xml:space="preserve">
<value>Verlauf konnte nicht geladen werden.</value>
</data>
<data name="AutoTellTabs_GreetedTooltip" xml:space="preserve">
<value>Als begrüßt markiert. Klicken um die Markierung zu entfernen.</value>
</data>
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
<value>Als begrüßt markieren.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="ChatLog_AutoTellTabs_Enable_Name" xml:space="preserve">
<value>Bei jedem /tell automatisch einen Tab pro Gesprächspartner öffnen</value>
</data>
<data name="ChatLog_AutoTellTabs_Enable_Description" xml:space="preserve">
<value>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.</value>
</data>
<data name="ChatLog_AutoTellTabs_Limit_Name" xml:space="preserve">
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
</data>
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell.</value>
</data>
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
<value>Kompakte Anzeige</value>
</data>
<data name="ChatLog_AutoTellTabs_Compact_Description" xml:space="preserve">
<value>Zeigt nur einen dünnen Separator zwischen normalen Tabs und Auto-Tell-Tabs, ohne Sektions-Header.</value>
</data>
<data name="ChatLog_AutoTellTabs_GreetedToggle_Name" xml:space="preserve">
<value>„Als begrüßt markieren"-Button anzeigen</value>
</data>
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
<value>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.</value>
</data>
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
<value>Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen.</value>
</data>
<data name="ChatLog_AutoTellTabs_ConflictHint" xml:space="preserve">
<value>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.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Datenschutz-Einstellungstab) -->
<data name="Privacy_AutoTellTabs_Section_Title" xml:space="preserve">
<value>Tell-Verlauf in Auto-Tabs</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Name" xml:space="preserve">
<value>Anzahl der vorgeladenen Tells</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Description" xml:space="preserve">
<value>Wie viele frühere Tell-Nachrichten beim Öffnen eines Auto-Tell-Tabs aus der Datenbank geladen werden. 0 deaktiviert die Vorladung.</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Hint" xml:space="preserve">
<value>Greift nur, wenn Auto-Tell-Tabs im Chat-Tab aktiviert sind.</value>
</data>
</root>
+72
View File
@@ -366,4 +366,76 @@
<data name="About_Translators_TreeNode" xml:space="preserve">
<value>Chat 2 community translators (upstream)</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (runtime strings) -->
<data name="AutoTellTabs_Migration_Title" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="AutoTellTabs_Migration_Content" xml:space="preserve">
<value>Auto-Tell-Tabs are enabled by default starting with version 0.4.0. You can disable or fine-tune them in the Chat tab.</value>
</data>
<data name="AutoTellTabs_SectionHeader" xml:space="preserve">
<value>Active Tells</value>
</data>
<data name="AutoTellTabs_HistorySeparator" xml:space="preserve">
<value>— Earlier conversations —</value>
</data>
<data name="AutoTellTabs_HistoryLoadError" xml:space="preserve">
<value>History could not be loaded.</value>
</data>
<data name="AutoTellTabs_GreetedTooltip" xml:space="preserve">
<value>Marked as greeted. Click to remove the marker.</value>
</data>
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
<value>Mark as greeted.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="ChatLog_AutoTellTabs_Enable_Name" xml:space="preserve">
<value>Open a tab automatically for each tell partner</value>
</data>
<data name="ChatLog_AutoTellTabs_Enable_Description" xml:space="preserve">
<value>When you receive or send a /tell, a temporary tab dedicated to that player is opened automatically. Tabs vanish on logout.</value>
</data>
<data name="ChatLog_AutoTellTabs_Limit_Name" xml:space="preserve">
<value>Maximum number of auto tell tabs</value>
</data>
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
<value>When the limit is reached, greeted tabs with the oldest activity are dropped first. The change applies on the next /tell.</value>
</data>
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
<value>Compact display</value>
</data>
<data name="ChatLog_AutoTellTabs_Compact_Description" xml:space="preserve">
<value>Show only a thin separator between persistent tabs and auto tell tabs, without the section header.</value>
</data>
<data name="ChatLog_AutoTellTabs_GreetedToggle_Name" xml:space="preserve">
<value>Show "mark as greeted" button</value>
</data>
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
<value>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.</value>
</data>
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
<value>The number of preloaded tells is configured in the Privacy tab.</value>
</data>
<data name="ChatLog_AutoTellTabs_ConflictHint" xml:space="preserve">
<value>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.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Privacy settings tab) -->
<data name="Privacy_AutoTellTabs_Section_Title" xml:space="preserve">
<value>Tell history in auto tabs</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Name" xml:space="preserve">
<value>Number of preloaded tells</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Description" xml:space="preserve">
<value>How many earlier tell messages are loaded from the database when an auto tell tab is opened. 0 disables the preload.</value>
</data>
<data name="Privacy_AutoTellTabs_Preload_Hint" xml:space="preserve">
<value>Only takes effect when auto tell tabs are enabled in the Chat tab.</value>
</data>
</root>
+67 -1
View File
@@ -1303,14 +1303,80 @@ 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;
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
// 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;
using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(2, 1)))
using (ImRaii.PushColor(ImGuiCol.Button, 0))
{
if (ImGuiUtil.IconButton(greetedIcon, $"greeted-{tabI}", greetedTooltip))
{
if (tab.IsGreeted)
{
Plugin.AutoTellTabsService.UnmarkGreeted(tab);
}
else
{
Plugin.AutoTellTabsService.MarkGreeted(tab);
}
}
}
ImGui.SameLine();
}
bool clicked;
if (showGreetedAffordance && 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)
+40
View File
@@ -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,37 @@ 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.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
ImGui.Spacing();
ImGuiUtil.HelpText(HellionStrings.ChatLog_AutoTellTabs_PreloadHint);
ImGui.Spacing();
ImGuiUtil.WarningText(HellionStrings.ChatLog_AutoTellTabs_ConflictHint);
}
}
}
+28
View File
@@ -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()
+54
View File
@@ -398,6 +398,60 @@ 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<Chunk> chunks)
{
foreach (var chunk in chunks)
{
if (chunk.Link is PlayerPayload pp)
{
return pp;
}
}
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.
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)
+18
View File
@@ -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();
+2 -2
View File
@@ -1,6 +1,6 @@
# Hellion Chat
**Version 0.3.0** — DSGVO-bewusste Erweiterung von [Chat 2](https://github.com/Infiziert90/ChatTwo) für FINAL FANTASY XIV / Dalamud.
**Version 0.3.1** — DSGVO-bewusste Erweiterung von [Chat 2](https://github.com/Infiziert90/ChatTwo) für FINAL FANTASY XIV / Dalamud.
Hellion Chat baut auf Chat 2 auf und ergänzt es um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle Chat-2-Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank.
@@ -231,7 +231,7 @@ Konflikte in Upstream-Sprach-Ressourcen (`Language.<lang>.resx`) kommen häufig
## Projektstatus
**Version 0.3.0** | Stand: Mai 2026
**Version 0.3.1** | Stand: Mai 2026
Alle Bootstrap-Phasen abgeschlossen:
+6 -6
View File
File diff suppressed because one or more lines are too long