- gate keybind pill-sync on IsChannelOrExistingLinkshell so an empty linkshell slot no longer desyncs the pill from the real send channel - close manually-popped pop-out windows on logout via an IsOpen filter instead of the PopOut flag (which manual pops never set) - read the router's tell-tab lookup through a lock-wrapped accessor so the framework thread cannot enumerate Config.Tabs mid worker-thread mutation - add a "switch on every tell" toggle (default on) and make the auto-open mode pick the matching layout, so Sidebar vs Top-tab are distinct - comment corrections (stale/contradictory text, TEST-MIRROR path depth)
113 lines
4.6 KiB
C#
113 lines
4.6 KiB
C#
using HellionChat.Code;
|
|
using HellionChat.Util;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace HellionChat.Services;
|
|
|
|
// 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 (a resolved Message), 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 MessageManager _messageManager;
|
|
private readonly ILogger<TellRouterService> _logger;
|
|
private bool _initialized;
|
|
|
|
public TellRouterService(MessageManager messageManager, ILogger<TellRouterService> logger)
|
|
{
|
|
_messageManager = messageManager;
|
|
_logger = logger;
|
|
}
|
|
|
|
public void Initialize()
|
|
{
|
|
if (_initialized)
|
|
return;
|
|
|
|
_messageManager.MessageProcessed += OnMessageProcessed;
|
|
_initialized = true;
|
|
_logger.LogDebug("TellRouterService online; routing incoming tells by TellAutoOpenMode.");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_initialized)
|
|
return;
|
|
|
|
_messageManager.MessageProcessed -= OnMessageProcessed;
|
|
_initialized = false;
|
|
}
|
|
|
|
private void OnMessageProcessed(Message message)
|
|
{
|
|
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(() =>
|
|
{
|
|
// Lock-safe lookup: AutoTellTabs mutates Config.Tabs under its lock on the
|
|
// worker thread, so we read through its guarded accessor, not the static.
|
|
var tab = Plugin.Instance.AutoTellTabsService?.FindTempTabSafe(name, world);
|
|
if (tab == null)
|
|
return; // nothing to reveal (auto-tell-tabs off -> no tab created)
|
|
|
|
switch (mode)
|
|
{
|
|
case TellAutoOpenMode.Sidebar:
|
|
case TellAutoOpenMode.TopTab:
|
|
// Switching to the tab on every tell is user-gated
|
|
// (TellAutoOpenSwitchAlways, default on); when off the tab still
|
|
// appears with its unread badge but the active tab is left alone.
|
|
// The mode also picks the layout, so Sidebar vs TopTab are actually
|
|
// distinct outcomes, not the same ActivateTab.
|
|
if (Plugin.Config.TellAutoOpenSwitchAlways)
|
|
{
|
|
var wantLayout =
|
|
mode == TellAutoOpenMode.TopTab
|
|
? MainWindowLayoutMode.TopTabs
|
|
: MainWindowLayoutMode.Sidebar;
|
|
if (Plugin.Config.MainWindowLayoutMode != wantLayout)
|
|
{
|
|
Plugin.Config.MainWindowLayoutMode = wantLayout;
|
|
Plugin.Instance.SaveConfig();
|
|
}
|
|
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
}
|