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;
|
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
|
// Eager-resolve trigger: resolving FailedTellNotifier in this adapter's ctor
|
||||||
// enables its game hook during host startup. StartAsync itself is a no-op.
|
// enables its game hook during host startup. StartAsync itself is a no-op.
|
||||||
internal sealed class FailedTellNotifierInitHostedService(FailedTellNotifier notifier)
|
internal sealed class FailedTellNotifierInitHostedService(FailedTellNotifier notifier)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<IFramework>()
|
sp.GetRequiredService<IFramework>()
|
||||||
));
|
));
|
||||||
services.AddSingleton(sp => new Services.TellRouterService(
|
services.AddSingleton(sp => new Services.TellRouterService(
|
||||||
sp.GetRequiredService<IChatGui>(),
|
sp.GetRequiredService<MessageManager>(),
|
||||||
sp.GetRequiredService<ILogger<Services.TellRouterService>>()
|
sp.GetRequiredService<ILogger<Services.TellRouterService>>()
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -369,6 +369,12 @@ internal static class PluginHostFactory
|
|||||||
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
||||||
sp.GetRequiredService<AutoTellTabsService>()
|
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(
|
services.AddHostedService(
|
||||||
sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
|
sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
|
||||||
sp.GetRequiredService<Integrations.FailedTellNotifier>()
|
sp.GetRequiredService<Integrations.FailedTellNotifier>()
|
||||||
|
|||||||
@@ -1,32 +1,91 @@
|
|||||||
using Dalamud.Game.Chat;
|
using HellionChat.Code;
|
||||||
using Dalamud.Plugin.Services;
|
using HellionChat.Util;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Services;
|
namespace HellionChat.Services;
|
||||||
|
|
||||||
// Skeleton for the upcoming auto-open routing layer. Subscribes to IChatGui
|
// Routes an incoming tell to the configured TellAutoOpenMode (Off/Sidebar/
|
||||||
// up front so the DI graph and Plugin.cs registration stay frozen — when
|
// TopTab/Popout). Decoupled from AutoTellTabsService (Flo decision 2026-06-15):
|
||||||
// the routing logic lands, it drops into OnChatMessage without touching
|
// that service owns tab CREATION + lifecycle; this only REVEALS/pops the tab it
|
||||||
// anything else.
|
// 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
|
internal sealed class TellRouterService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IChatGui _chatGui;
|
private readonly MessageManager _messageManager;
|
||||||
private readonly ILogger<TellRouterService> _logger;
|
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;
|
_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()
|
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