feat(tell-router): auto-open incoming tells per TellAutoOpenMode

This commit is contained in:
2026-06-16 00:50:45 +02:00
parent 7b6871fea4
commit 47a49de8c0
3 changed files with 91 additions and 14 deletions
@@ -101,6 +101,18 @@ internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService s
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class TellRouterServiceInitHostedService(Services.TellRouterService service)
: IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
service.Initialize();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
// Eager-resolve trigger: resolving FailedTellNotifier in this adapter's ctor
// enables its game hook during host startup. StartAsync itself is a no-op.
internal sealed class FailedTellNotifierInitHostedService(FailedTellNotifier notifier)
+7 -1
View File
@@ -122,7 +122,7 @@ internal static class PluginHostFactory
sp.GetRequiredService<IFramework>()
));
services.AddSingleton(sp => new Services.TellRouterService(
sp.GetRequiredService<IChatGui>(),
sp.GetRequiredService<MessageManager>(),
sp.GetRequiredService<ILogger<Services.TellRouterService>>()
));
@@ -369,6 +369,12 @@ internal static class PluginHostFactory
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
sp.GetRequiredService<AutoTellTabsService>()
));
// Must come AFTER AutoTell's registration: both subscribe MessageProcessed,
// and AutoTell subscribing first lets the router's IsOpen-guard see the
// already-opened pop-out (FIFO framework-tick ordering, no double-pop).
services.AddHostedService(sp => new TellRouterServiceInitHostedService(
sp.GetRequiredService<Services.TellRouterService>()
));
services.AddHostedService(
sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
sp.GetRequiredService<Integrations.FailedTellNotifier>()
+72 -13
View File
@@ -1,32 +1,91 @@
using Dalamud.Game.Chat;
using Dalamud.Plugin.Services;
using HellionChat.Code;
using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Services;
// Skeleton for the upcoming auto-open routing layer. Subscribes to IChatGui
// up front so the DI graph and Plugin.cs registration stay frozen — when
// the routing logic lands, it drops into OnChatMessage without touching
// anything else.
// Routes an incoming tell to the configured TellAutoOpenMode (Off/Sidebar/
// TopTab/Popout). Decoupled from AutoTellTabsService (Flo decision 2026-06-15):
// that service owns tab CREATION + lifecycle; this only REVEALS/pops the tab it
// finds. Popout guards on pool.IsOpen so it never double-pops a tab the
// AutoTellTabsOpenAsPopout path already opened. Subscribes to the resolved
// MessageManager.MessageProcessed stream (partner already extracted), not the raw
// IChatGui event, and defers the reveal one tick so the tab exists regardless of
// subscriber order. Wired by TellRouterServiceInitHostedService.
internal sealed class TellRouterService : IDisposable
{
private readonly IChatGui _chatGui;
private readonly MessageManager _messageManager;
private readonly ILogger<TellRouterService> _logger;
private bool _initialized;
public TellRouterService(IChatGui chatGui, ILogger<TellRouterService> logger)
public TellRouterService(MessageManager messageManager, ILogger<TellRouterService> logger)
{
_chatGui = chatGui;
_messageManager = messageManager;
_logger = logger;
_chatGui.ChatMessageUnhandled += OnChatMessage;
}
public void Initialize()
{
if (_initialized)
return;
_messageManager.MessageProcessed += OnMessageProcessed;
_initialized = true;
_logger.LogDebug("TellRouterService online; routing incoming tells by TellAutoOpenMode.");
}
public void Dispose()
{
_chatGui.ChatMessageUnhandled -= OnChatMessage;
if (!_initialized)
return;
_messageManager.MessageProcessed -= OnMessageProcessed;
_initialized = false;
}
private void OnChatMessage(IChatMessage message)
private void OnMessageProcessed(Message message)
{
// Intentional no-op until the routing implementation lands.
var mode = Plugin.Config.TellAutoOpenMode;
if (mode == TellAutoOpenMode.Off)
return;
if (message.Code.Type != ChatType.TellIncoming)
return;
// Partner = sender for an incoming tell. Same payload idiom AutoTellTabs uses
// (AutoTellTabsService.ExtractTellPartner), so the lookup never diverges.
var partner =
ChunkUtil.TryGetPlayerPayload(message.Sender)
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
if (partner == null)
return;
var name = partner.PlayerName;
var world = partner.World.RowId;
// Defer the reveal to the next framework tick. AutoTellTabsService also
// handles this MessageProcessed (synchronously); by the next tick the tab
// exists regardless of subscription order, and the reveal (ActivateTab / pool
// mutation) is serialized with Draw (reference_dalamud_framework_thread).
Plugin.Framework.RunOnFrameworkThread(() =>
{
var tab = AutoTellTabsService.FindTempTab(name, world);
if (tab == null)
return; // nothing to reveal (auto-tell-tabs off -> no tab created)
switch (mode)
{
case TellAutoOpenMode.Sidebar:
case TellAutoOpenMode.TopTab:
Plugin.Instance.MainWindow?.ActivateTab(tab);
break;
case TellAutoOpenMode.Popout:
// IsOpen-guard: don't double-pop a tab the AutoTellTabsOpenAsPopout
// path already opened (the two switches stay decoupled).
if (!Plugin.Instance.ChannelPopoutPool.IsOpen(tab.Identifier))
Plugin.Instance.ChannelPopoutPool.TryOpen(tab);
break;
}
});
}
}