feat(tell-router): auto-open incoming tells per TellAutoOpenMode
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user