Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4aa3971c5 | |||
| e9ec587e3b | |||
| 39cd7ab801 | |||
| bb6259e14d | |||
| 757370dd53 | |||
| 3f35b76c54 | |||
| 74bdc4f927 | |||
| eb379d84ef | |||
| 7add74dbbe | |||
| e91c7a3888 | |||
| f8b0804321 | |||
| a9d4e9bd69 | |||
| 7e3e4c8b72 | |||
| 397c84be2c | |||
| 269708150d | |||
| a2977ef75b | |||
| baa4d011e8 | |||
| 4810e8b518 | |||
| 133f5c536f | |||
| 92bb368d2b | |||
| 07f47f32e3 | |||
| 141fcbf074 | |||
| 32c410e8e2 | |||
| 824037e55f | |||
| 173cb76bea | |||
| 2736551505 | |||
| 0679a0e57a |
@@ -372,6 +372,8 @@ MigrationBackup/
|
|||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
#Specs und Plan datein
|
||||||
|
/.superpowers/
|
||||||
TestResults
|
TestResults
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
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
|
called out in the yaml changelog so users can see what it
|
||||||
derives from. -->
|
derives from. -->
|
||||||
<Version>0.3.0</Version>
|
<Version>0.4.0</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<!-- HellionChat fork: assembly is renamed so Dalamud uses
|
<!-- HellionChat fork: assembly is renamed so Dalamud uses
|
||||||
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
|
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using ChatTwo.Util;
|
|||||||
using Dalamud;
|
using Dalamud;
|
||||||
using Dalamud.Configuration;
|
using Dalamud.Configuration;
|
||||||
using Dalamud.Game.ClientState.Keys;
|
using Dalamud.Game.ClientState.Keys;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Interface.FontIdentifier;
|
using Dalamud.Interface.FontIdentifier;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 8;
|
private const int LatestVersion = 9;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
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.
|
// to fall back to the user's chosen system or Dalamud font.
|
||||||
public bool UseHellionFont = true;
|
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;
|
||||||
|
// 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)
|
public int GetRetentionDays(ChatType type)
|
||||||
{
|
{
|
||||||
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
|
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
|
||||||
@@ -112,7 +132,12 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool MoreCompactPretty;
|
public bool MoreCompactPretty;
|
||||||
public bool HideSameTimestamps;
|
public bool HideSameTimestamps;
|
||||||
public bool ShowNoviceNetwork;
|
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 PrintChangelog = true;
|
||||||
public bool OnlyPreviewIf;
|
public bool OnlyPreviewIf;
|
||||||
public int PreviewMinimum = 1;
|
public int PreviewMinimum = 1;
|
||||||
@@ -230,7 +255,17 @@ public class Configuration : IPluginConfiguration
|
|||||||
TooltipOffset = other.TooltipOffset;
|
TooltipOffset = other.TooltipOffset;
|
||||||
WindowAlpha = other.WindowAlpha;
|
WindowAlpha = other.WindowAlpha;
|
||||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
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;
|
OverrideStyle = other.OverrideStyle;
|
||||||
ChosenStyle = other.ChosenStyle;
|
ChosenStyle = other.ChosenStyle;
|
||||||
ChatTabForward = other.ChatTabForward;
|
ChatTabForward = other.ChatTabForward;
|
||||||
@@ -249,6 +284,12 @@ public class Configuration : IPluginConfiguration
|
|||||||
HellionThemeEnabled = other.HellionThemeEnabled;
|
HellionThemeEnabled = other.HellionThemeEnabled;
|
||||||
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
||||||
UseHellionFont = other.UseHellionFont;
|
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();
|
[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)
|
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)
|
public void AddMessage(Message message, bool unread = true)
|
||||||
@@ -375,6 +434,7 @@ public class Tab
|
|||||||
IsTempTab = IsTempTab,
|
IsTempTab = IsTempTab,
|
||||||
AllSenderMessages = AllSenderMessages,
|
AllSenderMessages = AllSenderMessages,
|
||||||
TellTarget = TellTarget.From(TellTarget),
|
TellTarget = TellTarget.From(TellTarget),
|
||||||
|
IsGreeted = IsGreeted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,23 +32,23 @@ public static class EmoteCache
|
|||||||
private struct Top100()
|
private struct Top100()
|
||||||
{
|
{
|
||||||
[JsonPropertyName("emote")]
|
[JsonPropertyName("emote")]
|
||||||
public Emote Emote = default;
|
public Emote Emote { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")]
|
||||||
public string Id = string.Empty;
|
public string Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public struct Emote()
|
public struct Emote()
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")]
|
||||||
public string Id = string.Empty;
|
public string Id { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("code")]
|
[JsonPropertyName("code")]
|
||||||
public string Code = string.Empty;
|
public string Code { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("imageType")]
|
[JsonPropertyName("imageType")]
|
||||||
public string ImageType = string.Empty;
|
public string ImageType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum LoadingState
|
public enum LoadingState
|
||||||
|
|||||||
@@ -40,6 +40,68 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
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 5–15 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 1–50) with LRU drop that prefers
|
||||||
|
greeted tabs first, then sorts by last activity
|
||||||
|
- History preload from the local message store (default 20 tells,
|
||||||
|
range 0–100) 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**
|
**Hellion Chat 0.3.0 — Audit hardening, brand sweep and rebrand of slash commands**
|
||||||
|
|
||||||
This release closes the remaining audit follow-ups from the
|
This release closes the remaining audit follow-ups from the
|
||||||
|
|||||||
@@ -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)
|
internal unsafe MessageManager(Plugin plugin)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
@@ -266,6 +273,8 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
if (tab.Matches(message))
|
if (tab.Matches(message))
|
||||||
tab.AddMessage(message, unread);
|
tab.AddMessage(message, unread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageProcessed?.Invoke(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class NameFormatting
|
internal class NameFormatting
|
||||||
|
|||||||
@@ -602,6 +602,84 @@ internal class MessageStore : IDisposable
|
|||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
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>
|
/// <summary>
|
||||||
/// Marks a message as deleted so it won't get returned in queries.
|
/// Marks a message as deleted so it won't get returned in queries.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
internal Commands Commands { get; }
|
internal Commands Commands { get; }
|
||||||
internal GameFunctions.GameFunctions Functions { get; }
|
internal GameFunctions.GameFunctions Functions { get; }
|
||||||
internal MessageManager MessageManager { get; }
|
internal MessageManager MessageManager { get; }
|
||||||
|
internal AutoTellTabsService AutoTellTabsService { get; }
|
||||||
internal IpcManager Ipc { get; }
|
internal IpcManager Ipc { get; }
|
||||||
internal ExtraChat ExtraChat { get; }
|
internal ExtraChat ExtraChat { get; }
|
||||||
internal TypingIpc TypingIpc { get; }
|
internal TypingIpc TypingIpc { get; }
|
||||||
@@ -100,6 +101,12 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
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
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
// TODO Remove after 01.07.2026
|
// TODO Remove after 01.07.2026
|
||||||
// Migrate old channel values
|
// 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)
|
if (Config.Tabs.Count == 0)
|
||||||
Config.Tabs.Add(TabsUtil.VanillaGeneral);
|
Config.Tabs.Add(TabsUtil.VanillaGeneral);
|
||||||
|
|
||||||
@@ -184,6 +210,14 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
MessageManager = new MessageManager(this); // Does it require UI?
|
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
|
// Hellion Chat — daily retention sweep, off-thread so it never
|
||||||
// blocks plugin load. Skips itself when disabled or already ran
|
// blocks plugin load. Skips itself when disabled or already ran
|
||||||
// within the past 24 hours.
|
// within the past 24 hours.
|
||||||
@@ -274,6 +308,10 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
TypingIpc?.Dispose();
|
TypingIpc?.Dispose();
|
||||||
ExtraChat?.Dispose();
|
ExtraChat?.Dispose();
|
||||||
Ipc?.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();
|
MessageManager?.DisposeAsync().AsTask().Wait();
|
||||||
Functions?.Dispose();
|
Functions?.Dispose();
|
||||||
Commands?.Dispose();
|
Commands?.Dispose();
|
||||||
@@ -491,7 +529,17 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
internal void SaveConfig()
|
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);
|
Interface.SavePluginConfig(Config);
|
||||||
|
|
||||||
|
Config.Tabs.Clear();
|
||||||
|
Config.Tabs.AddRange(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void LanguageChanged(string langCode)
|
internal void LanguageChanged(string langCode)
|
||||||
|
|||||||
+28
@@ -164,4 +164,32 @@ internal class HellionStrings
|
|||||||
internal static string About_Localization_P1 => Get(nameof(About_Localization_P1));
|
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_Localization_P2 => Get(nameof(About_Localization_P2));
|
||||||
internal static string About_Translators_TreeNode => Get(nameof(About_Translators_TreeNode));
|
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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,4 +366,76 @@
|
|||||||
<data name="About_Translators_TreeNode" xml:space="preserve">
|
<data name="About_Translators_TreeNode" xml:space="preserve">
|
||||||
<value>Chat-2-Community-Übersetzer (Upstream)</value>
|
<value>Chat-2-Community-Übersetzer (Upstream)</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|||||||
@@ -366,4 +366,76 @@
|
|||||||
<data name="About_Translators_TreeNode" xml:space="preserve">
|
<data name="About_Translators_TreeNode" xml:space="preserve">
|
||||||
<value>Chat 2 community translators (upstream)</value>
|
<value>Chat 2 community translators (upstream)</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|||||||
@@ -1303,14 +1303,80 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (child)
|
if (child)
|
||||||
{
|
{
|
||||||
var previousTab = Plugin.CurrentTab;
|
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++)
|
for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++)
|
||||||
{
|
{
|
||||||
var tab = Plugin.Config.Tabs[tabI];
|
var tab = Plugin.Config.Tabs[tabI];
|
||||||
if (tab.PopOut)
|
if (tab.PopOut)
|
||||||
continue;
|
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 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);
|
DrawTabContextMenu(tab, tabI);
|
||||||
|
|
||||||
if (!clicked && Plugin.WantedTab != tabI)
|
if (!clicked && Plugin.WantedTab != tabI)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using ChatTwo.Resources;
|
using ChatTwo.Resources;
|
||||||
using ChatTwo.Util;
|
using ChatTwo.Util;
|
||||||
using Dalamud.Interface.Style;
|
using Dalamud.Interface.Style;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
@@ -92,6 +93,12 @@ internal sealed class ChatLog : ISettingsTab
|
|||||||
Plugin.ChatLogWindow.Position = pos;
|
Plugin.ChatLogWindow.Position = pos;
|
||||||
ImGuiUtil.WarningText(Language.Options_AdjustPosition_Warning);
|
ImGuiUtil.WarningText(Language.Options_AdjustPosition_Warning);
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
DrawAutoTellTabsSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Mutable.OverrideStyle)
|
if (!Mutable.OverrideStyle)
|
||||||
@@ -116,4 +123,37 @@ internal sealed class ChatLog : ISettingsTab
|
|||||||
|
|
||||||
ImGui.Spacing();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using ChatTwo.Privacy;
|
|||||||
using ChatTwo.Resources;
|
using ChatTwo.Resources;
|
||||||
using ChatTwo.Util;
|
using ChatTwo.Util;
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
@@ -186,6 +187,33 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
DrawExportSection();
|
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()
|
private void DrawExportSection()
|
||||||
|
|||||||
@@ -398,6 +398,60 @@ internal static class ChunkUtil
|
|||||||
return builder.ToString();
|
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]);
|
internal static readonly RawPayload PeriodicRecruitmentLink = new([0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]);
|
||||||
|
|
||||||
private static uint GetInteger(BinaryReader input)
|
private static uint GetInteger(BinaryReader input)
|
||||||
|
|||||||
@@ -215,6 +215,24 @@ internal static class ImGuiUtil
|
|||||||
ImGui.TextUnformatted(text);
|
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)
|
internal static void WarningText(string text, bool wrap = true)
|
||||||
{
|
{
|
||||||
var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent();
|
var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Hellion Chat
|
# 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.
|
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
|
## Projektstatus
|
||||||
|
|
||||||
**Version 0.3.0** | Stand: Mai 2026
|
**Version 0.3.1** | Stand: Mai 2026
|
||||||
|
|
||||||
Alle Bootstrap-Phasen abgeschlossen:
|
Alle Bootstrap-Phasen abgeschlossen:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user