Compare commits
38 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 | |||
| 02cbfff748 | |||
| 9c86619c9f | |||
| 6b44310e04 | |||
| 59332ce9ea | |||
| 462530dec5 | |||
| 8e964ca498 | |||
| 1f2cb000a2 | |||
| 4f25c2756b | |||
| de0d2c80cd | |||
| 2ce30383d9 | |||
| a857714064 |
@@ -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.2.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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-7
@@ -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
|
||||||
@@ -168,10 +168,19 @@ public static class EmoteCache
|
|||||||
|
|
||||||
internal async Task<byte[]> LoadAsync(Emote emote)
|
internal async Task<byte[]> LoadAsync(Emote emote)
|
||||||
{
|
{
|
||||||
var dir = Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1");
|
// BetterTTV-supplied Id and ImageType are interpolated straight
|
||||||
|
// into the filename. HTTPS protects the wire, but a compromised
|
||||||
|
// upstream could still hand us "../foo" and write into the
|
||||||
|
// pluginConfigs root (or worse). Resolve the candidate path and
|
||||||
|
// refuse anything that escapes the cache directory.
|
||||||
|
var dir = Path.GetFullPath(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1"));
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
var filePath = Path.Join(dir, $"{emote.Id}.{emote.ImageType}");
|
var dirPrefix = dir.EndsWith(Path.DirectorySeparatorChar) ? dir : dir + Path.DirectorySeparatorChar;
|
||||||
|
var filePath = Path.GetFullPath(Path.Join(dir, $"{emote.Id}.{emote.ImageType}"));
|
||||||
|
if (!filePath.StartsWith(dirPrefix, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException($"Emote path escapes cache directory: id={emote.Id}, type={emote.ImageType}");
|
||||||
|
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
{
|
{
|
||||||
RawData = await File.ReadAllBytesAsync(filePath);
|
RawData = await File.ReadAllBytesAsync(filePath);
|
||||||
|
|||||||
+135
-15
@@ -3,12 +3,12 @@ author: JonKazama-Hellion
|
|||||||
punchline: Chat 2 with privacy controls aligned to EU, US and JP rules
|
punchline: Chat 2 with privacy controls aligned to EU, US and JP rules
|
||||||
description: |-
|
description: |-
|
||||||
Hellion Chat is built on top of Chat 2 with one removal and a stack
|
Hellion Chat is built on top of Chat 2 with one removal and a stack
|
||||||
of privacy controls on top. The /chat2 command, tabs, channel
|
of privacy controls on top. Tabs, channel filters, RGB colours,
|
||||||
filters, RGB colours, emotes, screenshot mode, IPC integration and
|
emotes, screenshot mode, IPC integration and the chat replacement
|
||||||
the chat replacement window itself work the same. The optional
|
window itself work the same. The optional webinterface that Chat 2
|
||||||
webinterface that Chat 2 ships is intentionally not part of this
|
ships is intentionally not part of this fork because it serves a
|
||||||
fork because it could not be hardened to the privacy guarantees
|
different use case from the smaller default footprint Hellion Chat
|
||||||
Hellion Chat makes by default.
|
is built around.
|
||||||
|
|
||||||
On top of that, Hellion Chat adds privacy and data-handling controls
|
On top of that, Hellion Chat adds privacy and data-handling controls
|
||||||
designed to align with the modern data protection rules that apply
|
designed to align with the modern data protection rules that apply
|
||||||
@@ -40,17 +40,137 @@ 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**
|
||||||
|
|
||||||
|
This release closes the remaining audit follow-ups from the
|
||||||
|
0.2.0 cleanup and finishes turning Hellion Chat into a properly
|
||||||
|
branded fork rather than a Chat 2 with a different name.
|
||||||
|
|
||||||
|
Slash commands have been renamed across the board so they no
|
||||||
|
longer collide with the upstream plugin and tell you which
|
||||||
|
plugin owns them at a glance:
|
||||||
|
|
||||||
|
- /chat2 becomes /hellion
|
||||||
|
- /chat2Viewer becomes /hellionView
|
||||||
|
- /clearlog2 becomes /clearhellion
|
||||||
|
- /chat2Debugger becomes /hellionDebugger (internal)
|
||||||
|
- /chat2SeString becomes /hellionSeString (internal)
|
||||||
|
|
||||||
|
This is a breaking change for anyone with macros bound to the
|
||||||
|
old command names. The upstream Chat 2 commands keep working
|
||||||
|
if you also have that plugin installed.
|
||||||
|
|
||||||
|
Privacy and storage hardening based on the post-0.2.0 audit:
|
||||||
|
|
||||||
|
- Privacy filter master switch now states explicitly that the
|
||||||
|
filter only governs storage, not the live chat log
|
||||||
|
- Emote cache refuses to write outside its own directory if a
|
||||||
|
third-party API ever returns a path that escapes
|
||||||
|
- Retention sweep is serialised so the 24h auto-sweep and the
|
||||||
|
manual button cannot launch in parallel and race for the
|
||||||
|
SQLite connection
|
||||||
|
- DbViewer paging uses an int constant and the matching SQL
|
||||||
|
parameter name (the upstream code passed a float and a name
|
||||||
|
without the parameter prefix; both worked in practice but
|
||||||
|
were inconsistent)
|
||||||
|
|
||||||
|
Visual identity now matches the Hellion Online Media website:
|
||||||
|
|
||||||
|
- Theme palette switched to Arctic Cyan plus Ember Orange,
|
||||||
|
matching the website's BRANDING.md tokens
|
||||||
|
- Active tabs and window title bars use a brand-color-dark teal
|
||||||
|
variation as identity colour, replacing the previous slate
|
||||||
|
violet that did not appear in the brand
|
||||||
|
- Resize grips and scrollbar grabs picked up Ember Orange
|
||||||
|
instead of industrial amber on hover and active states
|
||||||
|
|
||||||
|
About tab rewritten and properly localised:
|
||||||
|
|
||||||
|
- New "Why this fork exists" block sets out the mission in
|
||||||
|
neutral terms, framing Chat 2's full-history default as the
|
||||||
|
right one for most users while explaining the narrower
|
||||||
|
default footprint this fork chose
|
||||||
|
- All Hellion-specific About copy now lives in HellionStrings
|
||||||
|
in EN and DE, so German users see the Hellion sections in
|
||||||
|
German rather than the upstream English fallback
|
||||||
|
- Webinterface absence is described as a focus mismatch
|
||||||
|
(different use case, substantial rebuild) rather than as
|
||||||
|
a security issue with the upstream code
|
||||||
|
- Translator list at the bottom of the About tab is reachable
|
||||||
|
again on smaller settings windows
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 0.2.0 — Webinterface removed**
|
**Hellion Chat 0.2.0 — Webinterface removed**
|
||||||
|
|
||||||
Following an internal security and consistency audit the upstream
|
The upstream webinterface has been removed in its entirety. It
|
||||||
webinterface has been removed in its entirety. Hardening it to the
|
serves a different use case from the smaller default footprint
|
||||||
privacy guarantees Hellion Chat makes by default would have meant
|
this fork is built around, namely remote access to chat from a
|
||||||
rewriting the auth flow (the upstream code uses a five-digit
|
second device. Aligning it with the data minimisation defaults
|
||||||
numeric code from System.Random), changing the default bind address
|
Hellion Chat ships with would have meant a substantial rebuild.
|
||||||
(currently every interface), reworking cookie handling and adding
|
Removing it was the cleaner path for this particular fork.
|
||||||
the privacy filter to the live message stream that the webinterface
|
|
||||||
was broadcasting around it. The cumulative cost did not match the
|
|
||||||
niche use case for a fork that wants less network surface, not more.
|
|
||||||
|
|
||||||
What changed in this release:
|
What changed in this release:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+79
-1
@@ -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>
|
||||||
@@ -724,7 +802,7 @@ internal class MessageStore : IDisposable
|
|||||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset) after).ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset) after).ToUnixTimeMilliseconds());
|
||||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset) before).ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset) before).ToUnixTimeMilliseconds());
|
||||||
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
||||||
cmd.Parameters.AddWithValue("OffsetCount", DbViewer.RowPerPage);
|
cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
return new MessageEnumerator(cmd.ExecuteReader());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
@@ -64,6 +65,15 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
internal int DeferredSaveFrames = -1;
|
internal int DeferredSaveFrames = -1;
|
||||||
|
|
||||||
|
// Serialises retention sweeps. The 24h auto-sweep on plugin load and
|
||||||
|
// the manual button in the Privacy tab both run on background threads;
|
||||||
|
// without this gate, hitting the manual button moments after a fresh
|
||||||
|
// plugin start would launch two sweeps in parallel and the second one
|
||||||
|
// would just re-do work the first one already finished. The lock guards
|
||||||
|
// the flag — the flag check itself bails before we touch the database.
|
||||||
|
internal readonly object RetentionSweepLock = new();
|
||||||
|
internal bool RetentionSweepRunning;
|
||||||
|
|
||||||
internal DateTime GameStarted { get; }
|
internal DateTime GameStarted { get; }
|
||||||
|
|
||||||
// Tab management needs to happen outside the chatlog window class for access reasons
|
// Tab management needs to happen outside the chatlog window class for access reasons
|
||||||
@@ -91,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
|
||||||
@@ -158,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);
|
||||||
|
|
||||||
@@ -175,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.
|
||||||
@@ -265,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();
|
||||||
@@ -405,6 +452,16 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
new Thread(() =>
|
new Thread(() =>
|
||||||
{
|
{
|
||||||
|
// Bail out cheaply if a manual sweep is already in flight; the
|
||||||
|
// lock around the actual work would queue us up otherwise and
|
||||||
|
// we would just re-do whatever the manual run already did.
|
||||||
|
lock (RetentionSweepLock)
|
||||||
|
{
|
||||||
|
if (RetentionSweepRunning)
|
||||||
|
return;
|
||||||
|
RetentionSweepRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays);
|
var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays);
|
||||||
@@ -429,6 +486,11 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
{
|
{
|
||||||
Log.Error(e, "Retention sweep failed");
|
Log.Error(e, "Retention sweep failed");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (RetentionSweepLock)
|
||||||
|
RetentionSweepRunning = false;
|
||||||
|
}
|
||||||
}) { IsBackground = true }.Start();
|
}) { IsBackground = true }.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,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)
|
||||||
|
|||||||
+52
@@ -44,6 +44,7 @@ internal class HellionStrings
|
|||||||
internal static string Privacy_Tab_Title => Get(nameof(Privacy_Tab_Title));
|
internal static string Privacy_Tab_Title => Get(nameof(Privacy_Tab_Title));
|
||||||
internal static string Privacy_FilterEnabled_Name => Get(nameof(Privacy_FilterEnabled_Name));
|
internal static string Privacy_FilterEnabled_Name => Get(nameof(Privacy_FilterEnabled_Name));
|
||||||
internal static string Privacy_FilterEnabled_Description => Get(nameof(Privacy_FilterEnabled_Description));
|
internal static string Privacy_FilterEnabled_Description => Get(nameof(Privacy_FilterEnabled_Description));
|
||||||
|
internal static string Privacy_FilterEnabled_StorageOnly_Help => Get(nameof(Privacy_FilterEnabled_StorageOnly_Help));
|
||||||
internal static string Privacy_Whitelist_Help => Get(nameof(Privacy_Whitelist_Help));
|
internal static string Privacy_Whitelist_Help => Get(nameof(Privacy_Whitelist_Help));
|
||||||
internal static string Privacy_Preset_PrivacyFirst => Get(nameof(Privacy_Preset_PrivacyFirst));
|
internal static string Privacy_Preset_PrivacyFirst => Get(nameof(Privacy_Preset_PrivacyFirst));
|
||||||
internal static string Privacy_Preset_ClearAll => Get(nameof(Privacy_Preset_ClearAll));
|
internal static string Privacy_Preset_ClearAll => Get(nameof(Privacy_Preset_ClearAll));
|
||||||
@@ -140,4 +141,55 @@ internal class HellionStrings
|
|||||||
internal static string Theme_WindowOpacity_Help => Get(nameof(Theme_WindowOpacity_Help));
|
internal static string Theme_WindowOpacity_Help => Get(nameof(Theme_WindowOpacity_Help));
|
||||||
internal static string Theme_UseHellionFont_Name => Get(nameof(Theme_UseHellionFont_Name));
|
internal static string Theme_UseHellionFont_Name => Get(nameof(Theme_UseHellionFont_Name));
|
||||||
internal static string Theme_UseHellionFont_Description => Get(nameof(Theme_UseHellionFont_Description));
|
internal static string Theme_UseHellionFont_Description => Get(nameof(Theme_UseHellionFont_Description));
|
||||||
|
|
||||||
|
internal static string About_Maintainer_Heading => Get(nameof(About_Maintainer_Heading));
|
||||||
|
internal static string About_Maintainer_Body => Get(nameof(About_Maintainer_Body));
|
||||||
|
internal static string About_Maintainer_Website_Label => Get(nameof(About_Maintainer_Website_Label));
|
||||||
|
internal static string About_Mission_Heading => Get(nameof(About_Mission_Heading));
|
||||||
|
internal static string About_Mission_P1 => Get(nameof(About_Mission_P1));
|
||||||
|
internal static string About_Mission_P2 => Get(nameof(About_Mission_P2));
|
||||||
|
internal static string About_Mission_P3 => Get(nameof(About_Mission_P3));
|
||||||
|
internal static string About_BuiltOn_Heading => Get(nameof(About_BuiltOn_Heading));
|
||||||
|
internal static string About_BuiltOn_P1 => Get(nameof(About_BuiltOn_P1));
|
||||||
|
internal static string About_BuiltOn_P2 => Get(nameof(About_BuiltOn_P2));
|
||||||
|
internal static string About_BuiltOn_Upstream_Label => Get(nameof(About_BuiltOn_Upstream_Label));
|
||||||
|
internal static string About_License_Heading => Get(nameof(About_License_Heading));
|
||||||
|
internal static string About_License_P1 => Get(nameof(About_License_P1));
|
||||||
|
internal static string About_License_P2 => Get(nameof(About_License_P2));
|
||||||
|
internal static string About_License_P3 => Get(nameof(About_License_P3));
|
||||||
|
internal static string About_SE_Heading => Get(nameof(About_SE_Heading));
|
||||||
|
internal static string About_SE_P1 => Get(nameof(About_SE_P1));
|
||||||
|
internal static string About_SE_P2 => Get(nameof(About_SE_P2));
|
||||||
|
internal static string About_Localization_Heading => Get(nameof(About_Localization_Heading));
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<data name="Privacy_FilterEnabled_Description" xml:space="preserve">
|
<data name="Privacy_FilterEnabled_Description" xml:space="preserve">
|
||||||
<value>Wenn aktiviert, werden nur Nachrichten aus den erlaubten Kanälen in die Datenbank gespeichert. Beim Deaktivieren gilt wieder das Standard-Verhalten von ChatTwo, also alles außer Battle-Logs wird gespeichert.</value>
|
<value>Wenn aktiviert, werden nur Nachrichten aus den erlaubten Kanälen in die Datenbank gespeichert. Beim Deaktivieren gilt wieder das Standard-Verhalten von ChatTwo, also alles außer Battle-Logs wird gespeichert.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
|
||||||
|
<value>Der Filter steuert nur, was in die lokale Datenbank geschrieben wird. Im Chat-Log siehst du weiterhin jede Nachricht live, ausgeschlossene Kanäle werden nur nicht mehr gespeichert. Wenn du Kanäle auch aus der sichtbaren Anzeige entfernen willst, nutze die normalen Chat-Tab-Filter im Spiel.</value>
|
||||||
|
</data>
|
||||||
<data name="Privacy_Whitelist_Help" xml:space="preserve">
|
<data name="Privacy_Whitelist_Help" xml:space="preserve">
|
||||||
<value>Wähle aus, welche Kanäle in die lokale Datenbank gespeichert werden. Standard nach Datensparsamkeit: nur deine eigenen Konversationen. Über die Buttons unten kannst du eine Voreinstellung anwenden.</value>
|
<value>Wähle aus, welche Kanäle in die lokale Datenbank gespeichert werden. Standard nach Datensparsamkeit: nur deine eigenen Konversationen. Über die Buttons unten kannst du eine Voreinstellung anwenden.</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -277,7 +280,7 @@
|
|||||||
<value>Hellion-Theme für alle Plugin-Fenster verwenden</value>
|
<value>Hellion-Theme für alle Plugin-Fenster verwenden</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Theme_Enabled_Description" xml:space="preserve">
|
<data name="Theme_Enabled_Description" xml:space="preserve">
|
||||||
<value>Industrielle HUD-Palette mit cyan-blauen Aktionsfarben, schiefer-violetten Tabs und Bernstein-Akzenten für aktive Zustände, global angewendet auf Chat-Fenster, Einstellungen, Viewer und Wizard. Deaktivieren, um das Standard-Dalamud-Erscheinungsbild zu nutzen.</value>
|
<value>Hellion-Online-Media-Palette aus Arctic Cyan und Ember Orange, angewendet auf Chat-Fenster, Einstellungen, Viewer und Wizard. Deaktivieren, um das Standard-Dalamud-Erscheinungsbild zu nutzen.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Theme_WindowOpacity_Label" xml:space="preserve">
|
<data name="Theme_WindowOpacity_Label" xml:space="preserve">
|
||||||
<value>Fenster-Deckkraft</value>
|
<value>Fenster-Deckkraft</value>
|
||||||
@@ -291,4 +294,148 @@
|
|||||||
<data name="Theme_UseHellionFont_Description" xml:space="preserve">
|
<data name="Theme_UseHellionFont_Description" xml:space="preserve">
|
||||||
<value>Rendert Chat und UI in Exo 2 (SIL Open Font License 1.1), die mit dem Plugin ausgeliefert wird. Deaktivieren, um auf die unter Einstellungen → Schrift gewählte Schriftart zurückzufallen.</value>
|
<value>Rendert Chat und UI in Exo 2 (SIL Open Font License 1.1), die mit dem Plugin ausgeliefert wird. Deaktivieren, um auf die unter Einstellungen → Schrift gewählte Schriftart zurückzufallen.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
|
<data name="About_Maintainer_Heading" xml:space="preserve">
|
||||||
|
<value>Maintainer</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Maintainer_Body" xml:space="preserve">
|
||||||
|
<value>Ich pflege Hellion Chat über Hellion Online Media. Auf der Website findest du die Kontaktdaten für lizenzrechtliche, rechtliche oder geschäftliche Fragen.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Maintainer_Website_Label" xml:space="preserve">
|
||||||
|
<value>Website:</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_Mission_Heading" xml:space="preserve">
|
||||||
|
<value>Warum es diesen Fork gibt</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Mission_P1" xml:space="preserve">
|
||||||
|
<value>Hellion Chat soll Chat 2 nicht ersetzen. Chat 2 liefert ein vollständiges Chat-Erlebnis mit kompletter Historie, die für Filter, Suche und Replay zur Verfügung steht. Dieser Default ist für die meisten Nutzer der richtige. Dieser Fork wählt einen anderen Ansatz: einen kleineren Default-Footprint, mit zusätzlichen Stellschrauben für Nutzer, die weniger fremden Chat auf der Festplatte behalten möchten.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Mission_P2" xml:space="preserve">
|
||||||
|
<value>Der Wunsch nach diesem engeren Default war persönlich. Nach zwei Jahren mit Chat 2 lag meine Datenbank bei über zwei Millionen Nachrichten, der Großteil davon /say, /shout und /yell von Fremden in Limsa. Genau diese Daten machen Chat 2's Voll-Historie nützlich, und die meisten Nutzer behalten sie gerne. Mein eigener Geschmack wollte einen kleineren Default. Also habe ich diesen Fork gebaut.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Mission_P3" xml:space="preserve">
|
||||||
|
<value>Ich strebe keine große Zielgruppe an, und der Fork steht nicht in Konkurrenz zu Chat 2. Der Code liegt offen unter derselben EUPL-1.2-Lizenz wie das Original. Infi, Anna oder sonst jemand dürfen reinschauen, Ideen mitnehmen, Fragen stellen oder das Projekt einfach ignorieren. Alles drei ist für mich in Ordnung.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_BuiltOn_Heading" xml:space="preserve">
|
||||||
|
<value>Aufbauend auf Chat 2</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_BuiltOn_P1" xml:space="preserve">
|
||||||
|
<value>Hellion Chat ist ein Fork von Chat 2 von Infi und Anna (ascclemens). Das Chat-Replacement-Fenster, die IPC-Integration, die Render-Engine und der komplette Storage-Kern stammen aus dem Original.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_BuiltOn_P2" xml:space="preserve">
|
||||||
|
<value>Das Webinterface ist das einzige größere Teil, das ich entfernt habe. Es ist für den Remote-Zugriff auf den Chat von einem zweiten Gerät gebaut, also für einen anderen Fokus als der kleinere Default-Footprint, den dieser Fork verfolgt. Es an diese Defaults anzupassen hätte einen erheblichen Umbau bedeutet, also war die Entfernung der saubere Weg für genau diesen Fork.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_BuiltOn_Upstream_Label" xml:space="preserve">
|
||||||
|
<value>Upstream-Repository:</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_License_Heading" xml:space="preserve">
|
||||||
|
<value>Lizenz</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_License_P1" xml:space="preserve">
|
||||||
|
<value>Hellion Chat und Chat 2 stehen beide unter der European Union Public Licence v1.2 (EUPL-1.2).</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_License_P2" xml:space="preserve">
|
||||||
|
<value>© 2023 bis 2026, die Chat-2-Autoren (Infi, Anna und die Upstream-Mitwirkenden).</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_License_P3" xml:space="preserve">
|
||||||
|
<value>© 2026 Hellion Online Media für die Erweiterungen in diesem Fork.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_SE_Heading" xml:space="preserve">
|
||||||
|
<value>FINAL FANTASY XIV-Hinweis</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_SE_P1" xml:space="preserve">
|
||||||
|
<value>FINAL FANTASY XIV © SQUARE ENIX CO., LTD. Alle Rechte vorbehalten.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_SE_P2" xml:space="preserve">
|
||||||
|
<value>Hellion Chat ist ein inoffizielles Fan-Plugin. Es steht in keiner Verbindung zu Square Enix und wird von ihnen weder unterstützt, gesponsert noch genehmigt.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_Localization_Heading" xml:space="preserve">
|
||||||
|
<value>Lokalisierung</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Localization_P1" xml:space="preserve">
|
||||||
|
<value>Die deutschen Übersetzungen der Hellion-spezifischen Strings stammen von mir. Weitere Sprachen sind aktuell nicht verfügbar.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Localization_P2" xml:space="preserve">
|
||||||
|
<value>Die Übersetzerliste weiter unten gehört zu den Chat-2-Strings auf Crowdin. Diese Freiwilligen haben Chat 2 übersetzt, nicht die Hellion-Erweiterungen.</value>
|
||||||
|
</data>
|
||||||
|
<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>
|
</root>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
<data name="Privacy_FilterEnabled_Description" xml:space="preserve">
|
<data name="Privacy_FilterEnabled_Description" xml:space="preserve">
|
||||||
<value>When enabled, only messages from whitelisted channels are persisted to the database. Disabling restores upstream ChatTwo behavior (everything except battle messages is stored).</value>
|
<value>When enabled, only messages from whitelisted channels are persisted to the database. Disabling restores upstream ChatTwo behavior (everything except battle messages is stored).</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
|
||||||
|
<value>The filter only controls what is written to the local database. The chat log itself keeps showing every message live, disallowed channels just stop being saved. Use the channel hide options in your in-game chat tabs if you want to remove channels from the visible chat.</value>
|
||||||
|
</data>
|
||||||
<data name="Privacy_Whitelist_Help" xml:space="preserve">
|
<data name="Privacy_Whitelist_Help" xml:space="preserve">
|
||||||
<value>Pick which channels are stored in the local database. Privacy-First default: only your own conversations. Use the buttons below to apply a preset.</value>
|
<value>Pick which channels are stored in the local database. Privacy-First default: only your own conversations. Use the buttons below to apply a preset.</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -121,7 +124,7 @@
|
|||||||
<value>Auto-delete messages after a per-channel retention window</value>
|
<value>Auto-delete messages after a per-channel retention window</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Retention_Enabled_Description" xml:space="preserve">
|
<data name="Retention_Enabled_Description" xml:space="preserve">
|
||||||
<value>When enabled, messages older than the configured window are deleted on every plugin start (at most once per 24 hours). Off by default — the plugin never deletes history without your explicit consent.</value>
|
<value>When enabled, messages older than the configured window are deleted on every plugin start (at most once per 24 hours). Off by default. The plugin never deletes history without your explicit consent.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Retention_Default_Label" xml:space="preserve">
|
<data name="Retention_Default_Label" xml:space="preserve">
|
||||||
<value>Default retention (days, 0 = never)</value>
|
<value>Default retention (days, 0 = never)</value>
|
||||||
@@ -277,7 +280,7 @@
|
|||||||
<value>Use the Hellion theme across all plugin windows</value>
|
<value>Use the Hellion theme across all plugin windows</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Theme_Enabled_Description" xml:space="preserve">
|
<data name="Theme_Enabled_Description" xml:space="preserve">
|
||||||
<value>Industrial HUD palette with cyan-teal action accents, slate-violet tabs and amber active highlights, applied globally to chat log, settings, viewers and the wizard. Disable to fall back to the default Dalamud look.</value>
|
<value>Hellion Online Media palette of Arctic Cyan plus Ember Orange, applied across the chat log, settings, viewers and the wizard. Disable to fall back to the default Dalamud look.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Theme_WindowOpacity_Label" xml:space="preserve">
|
<data name="Theme_WindowOpacity_Label" xml:space="preserve">
|
||||||
<value>Window opacity</value>
|
<value>Window opacity</value>
|
||||||
@@ -291,4 +294,148 @@
|
|||||||
<data name="Theme_UseHellionFont_Description" xml:space="preserve">
|
<data name="Theme_UseHellionFont_Description" xml:space="preserve">
|
||||||
<value>Renders chat and UI in Exo 2 (SIL Open Font License 1.1) which ships with the plugin. Disable to fall back to whatever font you picked under Settings → Fonts.</value>
|
<value>Renders chat and UI in Exo 2 (SIL Open Font License 1.1) which ships with the plugin. Disable to fall back to whatever font you picked under Settings → Fonts.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
|
<data name="About_Maintainer_Heading" xml:space="preserve">
|
||||||
|
<value>Maintainer</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Maintainer_Body" xml:space="preserve">
|
||||||
|
<value>I maintain Hellion Chat through Hellion Online Media. The website has the contact details for licensing, legal or business questions.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Maintainer_Website_Label" xml:space="preserve">
|
||||||
|
<value>Website:</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_Mission_Heading" xml:space="preserve">
|
||||||
|
<value>Why this fork exists</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Mission_P1" xml:space="preserve">
|
||||||
|
<value>Hellion Chat is not trying to replace Chat 2. Chat 2 ships a complete chat experience with full history available for filtering, search and replay. That default is the right one for most users. This fork takes a different stance: a smaller default footprint, with extra knobs for users who want to keep less third-party chat on disk.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Mission_P2" xml:space="preserve">
|
||||||
|
<value>The reason I wanted that narrower default was personal. After two years on Chat 2 my database had grown past two million messages, most of them /say, /shout and /yell from strangers in Limsa. That data is exactly what makes Chat 2's full-history view powerful and most users are happy to keep it. For my own taste I wanted a smaller default. So I built this fork.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Mission_P3" xml:space="preserve">
|
||||||
|
<value>I am not chasing a big audience and the fork is not in competition with Chat 2. The code is open under the same EUPL-1.2 licence as the upstream plugin. Infi, Anna or anyone else are welcome to read it, borrow ideas, ask questions, or ignore the project. All three are fine by me.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_BuiltOn_Heading" xml:space="preserve">
|
||||||
|
<value>Built on Chat 2</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_BuiltOn_P1" xml:space="preserve">
|
||||||
|
<value>Hellion Chat is a fork of Chat 2 by Infi and Anna (ascclemens). The chat replacement window, the IPC integration, the rendering engine and the entire storage core come from upstream Chat 2.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_BuiltOn_P2" xml:space="preserve">
|
||||||
|
<value>The webinterface is the only major piece I removed. It is built for remote access to chat from a second device, which is a different focus than the smaller default footprint this fork is built around. Aligning it with these defaults would have meant a substantial rebuild, so removing it was the cleaner path for this particular fork.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_BuiltOn_Upstream_Label" xml:space="preserve">
|
||||||
|
<value>Upstream repository:</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_License_Heading" xml:space="preserve">
|
||||||
|
<value>License</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_License_P1" xml:space="preserve">
|
||||||
|
<value>Hellion Chat and Chat 2 both ship under the European Union Public Licence v1.2 (EUPL-1.2).</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_License_P2" xml:space="preserve">
|
||||||
|
<value>© 2023 to 2026, the Chat 2 authors (Infi, Anna and the upstream contributors).</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_License_P3" xml:space="preserve">
|
||||||
|
<value>© 2026 Hellion Online Media for the additions made in this fork.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_SE_Heading" xml:space="preserve">
|
||||||
|
<value>FINAL FANTASY XIV disclaimer</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_SE_P1" xml:space="preserve">
|
||||||
|
<value>FINAL FANTASY XIV © SQUARE ENIX CO., LTD. All rights reserved.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_SE_P2" xml:space="preserve">
|
||||||
|
<value>Hellion Chat is an unofficial, fan-made plugin. It has no affiliation with Square Enix and is not endorsed, sponsored or approved by them.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data name="About_Localization_Heading" xml:space="preserve">
|
||||||
|
<value>Localization</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Localization_P1" xml:space="preserve">
|
||||||
|
<value>The German translations of the Hellion-specific strings come from me. Other languages are not provided yet.</value>
|
||||||
|
</data>
|
||||||
|
<data name="About_Localization_P2" xml:space="preserve">
|
||||||
|
<value>The translator list below covers the upstream Chat 2 strings on Crowdin. Those volunteers translated Chat 2, not the Hellion additions.</value>
|
||||||
|
</data>
|
||||||
|
<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>
|
</root>
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
SetUpTextCommandChannels();
|
SetUpTextCommandChannels();
|
||||||
SetUpAllCommands();
|
SetUpAllCommands();
|
||||||
|
|
||||||
Plugin.Commands.Register("/clearlog2", "Clear the Chat 2 chat log").Execute += ClearLog;
|
Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log").Execute += ClearLog;
|
||||||
Plugin.Commands.Register("/chat2").Execute += ToggleChat;
|
Plugin.Commands.Register("/hellion").Execute += ToggleChat;
|
||||||
|
|
||||||
Plugin.ClientState.Login += Login;
|
Plugin.ClientState.Login += Login;
|
||||||
Plugin.ClientState.Logout += Logout;
|
Plugin.ClientState.Logout += Logout;
|
||||||
@@ -115,8 +115,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
|
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
|
||||||
Plugin.ClientState.Logout -= Logout;
|
Plugin.ClientState.Logout -= Logout;
|
||||||
Plugin.ClientState.Login -= Login;
|
Plugin.ClientState.Login -= Login;
|
||||||
Plugin.Commands.Register("/chat2").Execute -= ToggleChat;
|
Plugin.Commands.Register("/hellion").Execute -= ToggleChat;
|
||||||
Plugin.Commands.Register("/clearlog2").Execute -= ClearLog;
|
Plugin.Commands.Register("/clearhellion").Execute -= ClearLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Logout(int _, int __)
|
private void Logout(int _, int __)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace ChatTwo.Ui;
|
|||||||
|
|
||||||
public class DbViewer : Window
|
public class DbViewer : Window
|
||||||
{
|
{
|
||||||
public const float RowPerPage = 1000.0f;
|
public const int RowPerPage = 1000;
|
||||||
|
|
||||||
private readonly Plugin Plugin;
|
private readonly Plugin Plugin;
|
||||||
|
|
||||||
@@ -76,19 +76,19 @@ public class DbViewer : Window
|
|||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
Plugin.Commands.Register("/chat2Viewer", "Get access to your message history, with simple filter options.", true).Execute += Toggle;
|
Plugin.Commands.Register("/hellionView", "Get access to your message history, with simple filter options.", true).Execute += Toggle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin.Commands.Register("/chat2Viewer", "Get access to your message history, with simple filter options.", true).Execute -= Toggle;
|
Plugin.Commands.Register("/hellionView", "Get access to your message history, with simple filter options.", true).Execute -= Toggle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Toggle(string _, string __) => Toggle();
|
private void Toggle(string _, string __) => Toggle();
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
var totalPages = (int)Math.Ceiling(Count / RowPerPage);
|
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
|
||||||
if (totalPages < 1)
|
if (totalPages < 1)
|
||||||
totalPages = 1;
|
totalPages = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ public class DebuggerWindow : Window
|
|||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
Plugin.Commands.Register("/chat2Debugger", showInHelp: false).Execute += Toggle;
|
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute += Toggle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin.Commands.Register("/chat2Debugger", showInHelp: false).Execute -= Toggle;
|
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute -= Toggle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Toggle(string _, string __) => Toggle();
|
private void Toggle(string _, string __) => Toggle();
|
||||||
|
|||||||
+63
-47
@@ -21,63 +21,79 @@ internal static class HellionStyle
|
|||||||
{
|
{
|
||||||
// Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs
|
// Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs
|
||||||
// Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui
|
// Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui
|
||||||
// expects.
|
// expects. Hex values are sourced from the Hellion Online Media brand
|
||||||
|
// guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo).
|
||||||
|
|
||||||
// Primary — cyan-teal for actionable controls (buttons, checks, sliders).
|
// Primary — Arctic Cyan, used for every interactive control (buttons,
|
||||||
private const uint PrimaryRgba = 0x00B8D4FF;
|
// checks, sliders, separators when hovered). Three brand stages plus a
|
||||||
private const uint PrimaryHoverRgba = 0x26C6DAFF;
|
// hover that lifts to brand-color-light and a press that drops to
|
||||||
private const uint PrimaryActiveRgba = 0x00838FFF;
|
// brand-color-dark.
|
||||||
|
private const uint PrimaryRgba = 0x00BED2FF; // brand-color
|
||||||
|
private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light
|
||||||
|
private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark
|
||||||
|
|
||||||
// Secondary — industrial amber, used as a warm highlight for active
|
// Identity — brand-color-dark teal for window title bars and the
|
||||||
// states (tab borders, resize grips, scrollbar grabs).
|
// active tab. Sits visibly below the primary cyan on buttons so the
|
||||||
private const uint SecondaryRgba = 0xFFB300FF;
|
// user sees "where am I" (deep teal) versus "what can I click"
|
||||||
private const uint SecondaryHoverRgba = 0xFFC940FF;
|
// (brand cyan) without leaving the cyan family.
|
||||||
private const uint SecondaryActiveRgba = 0xC68400FF;
|
private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark
|
||||||
|
private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light
|
||||||
|
private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab
|
||||||
|
|
||||||
// Tertiary — slate violet, reserved for title bars and the active tab
|
// Accent — Ember Orange for warm highlights on grips and scrollbar
|
||||||
// background so identity beats out the cyan accent without competing
|
// pulls. Replaces the previous industrial amber so the plugin matches
|
||||||
// with it on action controls.
|
// the website's CTA palette. AccentActive is reserved for any future
|
||||||
private const uint TertiaryRgba = 0x7B61FFFF;
|
// pressed-state on accent surfaces; the current slots only need
|
||||||
private const uint TertiaryHoverRgba = 0x9580FFFF;
|
// AccentRgba and AccentHoverRgba.
|
||||||
private const uint TertiaryActiveRgba = 0x5E45D9FF;
|
private const uint AccentRgba = 0xF97316FF; // accent-color
|
||||||
|
private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light
|
||||||
|
|
||||||
// Surfaces — deep slate window/frame backgrounds, steel borders.
|
// Surfaces — Hellion brand background ladder. Window darkest, frame
|
||||||
private const uint WindowBgRgba = 0x0E1A20FF;
|
// hover ladder climbs into surface tones. Matches the website's
|
||||||
private const uint ChildBgRgba = 0x102027FF;
|
// background / background-medium / background-light / surface vars.
|
||||||
private const uint PopupBgRgba = 0x102027FF;
|
private const uint WindowBgRgba = 0x070B12FF; // background
|
||||||
private const uint FrameBgRgba = 0x162831FF;
|
private const uint ChildBgRgba = 0x0C1220FF; // background-medium
|
||||||
private const uint FrameBgHoverRgba = 0x1F3540FF;
|
private const uint PopupBgRgba = 0x0C1220FF; // background-medium
|
||||||
private const uint FrameBgActiveRgba = 0x274250FF;
|
private const uint FrameBgRgba = 0x141E30FF; // background-light
|
||||||
private const uint BorderRgba = 0x37474FFF;
|
private const uint FrameBgHoverRgba = 0x1A2538FF; // surface
|
||||||
|
private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover
|
||||||
|
// Cyan-tinted border — matches website --border-brand (cyan @ 40% α).
|
||||||
|
private const uint BorderRgba = 0x00BED266;
|
||||||
private const uint BorderShadowRgba = 0x00000000;
|
private const uint BorderShadowRgba = 0x00000000;
|
||||||
|
|
||||||
// Headers / collapsing-headers / tree nodes / selectables.
|
// Headers / collapsing-headers / tree nodes / selectables — same
|
||||||
private const uint HeaderRgba = 0x1B2C36FF;
|
// surface ladder as frames so panels feel consistent.
|
||||||
private const uint HeaderHoverRgba = 0x263A45FF;
|
private const uint HeaderRgba = 0x141E30FF;
|
||||||
private const uint HeaderActiveRgba = 0x324A57FF;
|
private const uint HeaderHoverRgba = 0x1A2538FF;
|
||||||
|
private const uint HeaderActiveRgba = 0x22303FFF;
|
||||||
|
|
||||||
// Title bars — tertiary identity for the active state.
|
// Title bars — Identity teal on active so the focused window reads
|
||||||
private const uint TitleBgRgba = 0x0E1A20FF;
|
// as "yours" without using accent or primary slots.
|
||||||
private const uint TitleBgActiveRgba = 0x5E45D9FF;
|
private const uint TitleBgRgba = 0x070B12FF;
|
||||||
private const uint TitleBgCollapsedRgba = 0x0A1318FF;
|
private const uint TitleBgActiveRgba = IdentityRgba;
|
||||||
|
private const uint TitleBgCollapsedRgba = 0x05080EFF;
|
||||||
|
|
||||||
// Tabs — tertiary tint, secondary highlight while hovered/unfocused.
|
// Tabs — neutral inactive, Identity-light on hover, Identity teal on
|
||||||
private const uint TabRgba = 0x162831FF;
|
// active. Unfocused-active uses the deeper Identity stage so an
|
||||||
private const uint TabHoveredRgba = 0x9580FFFF;
|
// unfocused window's active tab still reads but does not pull focus.
|
||||||
private const uint TabActiveRgba = 0x7B61FFFF;
|
private const uint TabRgba = 0x141E30FF;
|
||||||
private const uint TabUnfocusedRgba = 0x12222AFF;
|
private const uint TabHoveredRgba = IdentityHoverRgba;
|
||||||
private const uint TabUnfocusedActiveRgba = 0x5E45D9FF;
|
private const uint TabActiveRgba = IdentityRgba;
|
||||||
|
private const uint TabUnfocusedRgba = 0x0C1220FF;
|
||||||
|
private const uint TabUnfocusedActiveRgba = IdentityDeepRgba;
|
||||||
|
|
||||||
// Scrollbar — slate base, secondary amber on grab.
|
// Scrollbar — Ember on grab so the pull stands out without competing
|
||||||
private const uint ScrollbarBgRgba = 0x0E1A20FF;
|
// with the cyan action buttons. Idle grab is a subtle surface tone,
|
||||||
private const uint ScrollbarGrabRgba = 0x37474FFF;
|
// hover/active climb into accent.
|
||||||
private const uint ScrollbarGrabHoveredRgba = 0xFFC940FF;
|
private const uint ScrollbarBgRgba = 0x070B12FF;
|
||||||
private const uint ScrollbarGrabActiveRgba = 0xFFB300FF;
|
private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover
|
||||||
|
private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba;
|
||||||
|
private const uint ScrollbarGrabActiveRgba = AccentRgba;
|
||||||
|
|
||||||
// Resize grip — secondary amber for the active corner pull.
|
// Resize grip — same Ember treatment as the scrollbar.
|
||||||
private const uint ResizeGripRgba = 0x37474FFF;
|
private const uint ResizeGripRgba = 0x141E30FF;
|
||||||
private const uint ResizeGripHoveredRgba = 0xFFC940FF;
|
private const uint ResizeGripHoveredRgba = AccentHoverRgba;
|
||||||
private const uint ResizeGripActiveRgba = 0xFFB300FF;
|
private const uint ResizeGripActiveRgba = AccentRgba;
|
||||||
|
|
||||||
// Separator and check mark / slider follow the primary cyan.
|
// Separator and check mark / slider follow the primary cyan.
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ public class SeStringDebugger : Window
|
|||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Plugin.Commands.Register("/chat2SeString", showInHelp: false).Execute += Toggle;
|
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute += Toggle;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Plugin.Commands.Register("/chat2SeString", showInHelp: false).Execute -= Toggle;
|
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute -= Toggle;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,14 +52,14 @@ public sealed class SettingsWindow : Window
|
|||||||
|
|
||||||
Initialise();
|
Initialise();
|
||||||
|
|
||||||
Plugin.Commands.Register("/chat2", "Perform various actions with Chat 2.").Execute += Command;
|
Plugin.Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute += Command;
|
||||||
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
|
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
|
Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
|
||||||
Plugin.Commands.Register("/chat2").Execute -= Command;
|
Plugin.Commands.Register("/hellion").Execute -= Command;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Command(string command, string args)
|
private void Command(string command, string args)
|
||||||
|
|||||||
@@ -60,68 +60,69 @@ internal sealed class About : ISettingsTab
|
|||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10.0f);
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
// Hellion-specific maintainer / attribution / license / SE-
|
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Maintainer_Heading);
|
||||||
// disclaimer block. Hand-rolled in English here rather than via
|
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Body);
|
||||||
// HellionStrings — the legal-ish copy stays close to the EUPL-1.2
|
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
|
||||||
// wording and the SE disclaimer is the same in every locale.
|
|
||||||
ImGui.TextColored(ImGuiColors.ParsedGold, "Maintainer");
|
|
||||||
ImGui.TextUnformatted("Hellion Chat is maintained by Hellion Online Media (Florian Wathling).");
|
|
||||||
ImGui.TextUnformatted("Website:");
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
|
||||||
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
|
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
|
||||||
ImGui.TextUnformatted("For licensing, legal or contact inquiries please reach out via the website above.");
|
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10.0f);
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
ImGui.TextColored(ImGuiColors.ParsedGold, "Built on Chat 2");
|
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Mission_Heading);
|
||||||
ImGui.TextUnformatted("Hellion Chat is a fork of Chat 2 by Infi and Anna (ascclemens).");
|
ImGui.TextUnformatted(HellionStrings.About_Mission_P1);
|
||||||
ImGui.TextUnformatted("Every chat replacement feature, the IPC integration, the rendering engine and the storage core come from upstream Chat 2.");
|
ImGui.Spacing();
|
||||||
ImGui.TextUnformatted("The upstream webinterface is intentionally not part of Hellion Chat — it could not be hardened to the privacy guarantees this fork makes by default.");
|
ImGui.TextUnformatted(HellionStrings.About_Mission_P2);
|
||||||
ImGui.TextUnformatted("Upstream repository:");
|
ImGui.Spacing();
|
||||||
|
ImGui.TextUnformatted(HellionStrings.About_Mission_P3);
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
|
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_BuiltOn_Heading);
|
||||||
|
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_P1);
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_P2);
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
|
||||||
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10.0f);
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
ImGui.TextColored(ImGuiColors.ParsedGold, "License");
|
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_License_Heading);
|
||||||
ImGui.TextUnformatted("Hellion Chat and Chat 2 are licensed under the European Union Public License v1.2 (EUPL-1.2).");
|
ImGui.TextUnformatted(HellionStrings.About_License_P1);
|
||||||
ImGui.TextUnformatted("© 2023–2026 the Chat 2 authors (Infi, Anna and the upstream contributors).");
|
ImGui.TextUnformatted(HellionStrings.About_License_P2);
|
||||||
ImGui.TextUnformatted("© 2026 Hellion Online Media — for the Hellion Chat additions.");
|
ImGui.TextUnformatted(HellionStrings.About_License_P3);
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10.0f);
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
ImGui.TextColored(ImGuiColors.DalamudOrange, "FINAL FANTASY XIV disclaimer");
|
ImGui.TextColored(ImGuiColors.DalamudOrange, HellionStrings.About_SE_Heading);
|
||||||
ImGui.TextUnformatted("FINAL FANTASY XIV © SQUARE ENIX CO., LTD. All rights reserved.");
|
ImGui.TextUnformatted(HellionStrings.About_SE_P1);
|
||||||
ImGui.TextUnformatted("Hellion Chat is an unofficial, fan-made plugin and is not affiliated with, endorsed, sponsored or approved by Square Enix.");
|
ImGui.TextUnformatted(HellionStrings.About_SE_P2);
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
ImGui.TextColored(ImGuiColors.ParsedGold, "Localization");
|
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Localization_Heading);
|
||||||
ImGui.TextUnformatted("German translations of Hellion-specific UI strings (HellionStrings.de.resx) are written by the Hellion Online Media maintainer.");
|
ImGui.TextUnformatted(HellionStrings.About_Localization_P1);
|
||||||
ImGui.TextUnformatted("All other locales for Hellion-specific strings are not currently provided.");
|
ImGui.TextUnformatted(HellionStrings.About_Localization_P2);
|
||||||
ImGui.TextUnformatted("The translator list below covers the upstream Chat 2 community translators on Crowdin — their work covers the inherited Chat 2 strings, not the Hellion additions.");
|
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
var height = ImGui.GetContentRegionAvail().Y - ImGui.CalcTextSize("A").Y - ImGui.GetStyle().ItemSpacing.Y * 2;
|
// The translator list lives at the bottom of the About tab. Render
|
||||||
using (var aboutChild = ImRaii.Child("about", new Vector2(-1, height)))
|
// it directly inside the parent scroll container instead of a
|
||||||
|
// fixed-height child — the previous "remaining space" calculation
|
||||||
|
// shrank to zero (or below) once the About copy grew, which made
|
||||||
|
// the section unreachable on smaller settings windows.
|
||||||
|
using (var treeNode = ImRaii.TreeNode(HellionStrings.About_Translators_TreeNode))
|
||||||
{
|
{
|
||||||
if (aboutChild)
|
|
||||||
{
|
|
||||||
using var treeNode = ImRaii.TreeNode("Chat 2 community translators (upstream)");
|
|
||||||
if (treeNode)
|
if (treeNode)
|
||||||
{
|
{
|
||||||
using var translatorChild = ImRaii.Child("translators");
|
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
|
||||||
if (translatorChild)
|
|
||||||
{
|
|
||||||
foreach (var translator in Translators)
|
foreach (var translator in Translators)
|
||||||
ImGui.TextUnformatted(translator);
|
ImGui.TextUnformatted(translator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -55,7 +56,10 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
private long CleanupDeleteCount;
|
private long CleanupDeleteCount;
|
||||||
private bool CleanupRunning;
|
private bool CleanupRunning;
|
||||||
|
|
||||||
private bool RetentionRunning;
|
// The retention-running state lives on Plugin so the auto-sweep and
|
||||||
|
// this manual button see the same flag. UI reads stay lock-free
|
||||||
|
// because ImGui is single-threaded and bool reads are atomic in .NET.
|
||||||
|
private bool RetentionRunning => Plugin.RetentionSweepRunning;
|
||||||
|
|
||||||
// Export form state
|
// Export form state
|
||||||
private int ExportRangeDays = 30;
|
private int ExportRangeDays = 30;
|
||||||
@@ -104,6 +108,8 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
HellionStrings.Privacy_FilterEnabled_Name,
|
HellionStrings.Privacy_FilterEnabled_Name,
|
||||||
HellionStrings.Privacy_FilterEnabled_Description);
|
HellionStrings.Privacy_FilterEnabled_Description);
|
||||||
|
|
||||||
|
ImGuiUtil.HelpText(HellionStrings.Privacy_FilterEnabled_StorageOnly_Help);
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
@@ -181,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()
|
||||||
@@ -408,10 +441,17 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
|
|
||||||
private void StartRetentionRun()
|
private void StartRetentionRun()
|
||||||
{
|
{
|
||||||
if (RetentionRunning)
|
// Take the shared retention lock so we cannot fight the auto-sweep
|
||||||
|
// for the database connection. If the auto-sweep is already in
|
||||||
|
// flight we just bail — the user can press the button again once
|
||||||
|
// it finishes.
|
||||||
|
lock (Plugin.RetentionSweepLock)
|
||||||
|
{
|
||||||
|
if (Plugin.RetentionSweepRunning)
|
||||||
return;
|
return;
|
||||||
|
Plugin.RetentionSweepRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
RetentionRunning = true;
|
|
||||||
var policy = Plugin.Config.RetentionPerChannelDays.ToDictionary(p => (int)(ushort)p.Key, p => p.Value);
|
var policy = Plugin.Config.RetentionPerChannelDays.ToDictionary(p => (int)(ushort)p.Key, p => p.Value);
|
||||||
var defaultDays = Plugin.Config.RetentionDefaultDays;
|
var defaultDays = Plugin.Config.RetentionDefaultDays;
|
||||||
|
|
||||||
@@ -443,7 +483,8 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
RetentionRunning = false;
|
lock (Plugin.RetentionSweepLock)
|
||||||
|
Plugin.RetentionSweepRunning = false;
|
||||||
}
|
}
|
||||||
}) { IsBackground = true }.Start();
|
}) { IsBackground = true }.Start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.2.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.
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ Privates Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo währen
|
|||||||
|
|
||||||
### Was gegenüber Chat 2 fehlt
|
### Was gegenüber Chat 2 fehlt
|
||||||
|
|
||||||
- **Webinterface** wurde in Hellion Chat 0.2.0 entfernt. Der eingebaute HTTP-Server hat unter dem Privacy-Versprechen nicht abgesichert werden können (5-stelliger numerischer Auth-Code aus `System.Random`, Bind auf alle Interfaces per Default, Cookies ohne Security-Flags und ein Server-Sent-Events-Stream der den Privacy-Filter umgangen hat). Wer den Funktionsumfang von Chat 2 vollständig braucht, sollte beim Upstream-Plugin bleiben; Hellion Chat fokussiert auf DSGVO-konforme Persistenz und verzichtet bewusst auf Remote-Zugriffs-Features.
|
- **Webinterface** wurde in Hellion Chat 0.2.0 entfernt. Es bedient einen anderen Anwendungsfall als der Fokus dieses Forks, nämlich Remote-Zugriff auf den Chat von einem zweiten Gerät. An die kleineren Defaults dieses Forks anzupassen hätte einen erheblichen Umbau bedeutet, also ist es ersatzlos entfernt worden. Wer den vollen Funktionsumfang von Chat 2 möchte, ist mit dem Upstream-Plugin besser bedient. Hellion Chat fokussiert sich auf einen schmaleren Datenbestand und verzichtet bewusst auf Remote-Zugriffs-Features.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ Konflikte in Upstream-Sprach-Ressourcen (`Language.<lang>.resx`) kommen häufig
|
|||||||
|
|
||||||
## Projektstatus
|
## Projektstatus
|
||||||
|
|
||||||
**Version 0.2.0** | Stand: Mai 2026
|
**Version 0.3.1** | Stand: Mai 2026
|
||||||
|
|
||||||
Alle Bootstrap-Phasen abgeschlossen:
|
Alle Bootstrap-Phasen abgeschlossen:
|
||||||
|
|
||||||
@@ -244,11 +244,13 @@ Alle Bootstrap-Phasen abgeschlossen:
|
|||||||
- [x] About-Tab im Hellion-Branding mit License + Disclaimer
|
- [x] About-Tab im Hellion-Branding mit License + Disclaimer
|
||||||
- [x] AI-Disclosure dokumentiert (Pair-Klassifikation)
|
- [x] AI-Disclosure dokumentiert (Pair-Klassifikation)
|
||||||
- [x] Webinterface entfernt (Phase 1.5, Audit-Konsequenz aus 2026-05-02)
|
- [x] Webinterface entfernt (Phase 1.5, Audit-Konsequenz aus 2026-05-02)
|
||||||
|
- [x] Audit-Hardening Phase 2 (Path-Traversal, Retention-Race, DbViewer-Konsistenz, Privacy-Filter-Help-Text)
|
||||||
|
- [x] Slash-Commands auf `/hellion`-Familie umbenannt
|
||||||
|
- [x] Theme auf Hellion-Online-Media-Brand-Palette aligned (Arctic Cyan + Ember Orange)
|
||||||
|
- [x] About-Tab vollständig lokalisiert (EN + DE) mit Mission-Statement und neutraler Tonart
|
||||||
|
|
||||||
Phase 2 (offen, kein festes Datum):
|
Phase 3 (offen, kein festes Datum):
|
||||||
|
|
||||||
- [ ] EmoteCache Path-Traversal-Hardening (`Path.GetFullPath` + StartsWith-Check)
|
|
||||||
- [ ] Race-Hardening für `RetentionLastRunAt` (CompareExchange / Lock)
|
|
||||||
- [ ] MySQL/MariaDB-Backend mit Drei-Stufen-Bestätigung
|
- [ ] MySQL/MariaDB-Backend mit Drei-Stufen-Bestätigung
|
||||||
- [ ] PostgreSQL-Backend
|
- [ ] PostgreSQL-Backend
|
||||||
- [ ] Encryption für sensible Channels (AES-256, lokaler Key)
|
- [ ] Encryption für sensible Channels (AES-256, lokaler Key)
|
||||||
|
|||||||
Reference in New Issue
Block a user