Ajout de la gestion avancée de l'indicateur de frappe et des paramètres associés

This commit is contained in:
2025-11-02 17:21:25 +01:00
parent 699678f641
commit e3d9300ca3
9 changed files with 221 additions and 48 deletions

View File

@@ -8,6 +8,7 @@ using Dalamud.Plugin.Services;
using MareSynchronos.API.Data;
using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
@@ -38,6 +39,7 @@ public class ChatService : DisposableMediatorSubscriberBase
private CancellationTokenSource? _typingCts;
private bool _isTypingAnnounced;
private DateTime _lastTypingSent = DateTime.MinValue;
private TypingScope _lastScope = TypingScope.Unknown;
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
private static readonly TimeSpan TypingResendInterval = TimeSpan.FromMilliseconds(750);
@@ -79,7 +81,7 @@ public class ChatService : DisposableMediatorSubscriberBase
if (_gameChatHooks.IsValueCreated)
_gameChatHooks.Value!.Dispose();
}
public void NotifyTypingKeystroke()
public void NotifyTypingKeystroke(TypingScope scope)
{
lock (_typingLock)
{
@@ -88,11 +90,12 @@ public class ChatService : DisposableMediatorSubscriberBase
{
_ = Task.Run(async () =>
{
try { await _apiController.UserSetTypingState(true).ConfigureAwait(false); }
try { await _apiController.UserSetTypingState(true, scope).ConfigureAwait(false); }
catch (Exception ex) { _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=true"); }
});
_isTypingAnnounced = true;
_lastTypingSent = now;
_lastScope = scope;
}
_typingCts?.Cancel();
@@ -105,7 +108,7 @@ public class ChatService : DisposableMediatorSubscriberBase
try
{
await Task.Delay(TypingIdle, token).ConfigureAwait(false);
await _apiController.UserSetTypingState(false).ConfigureAwait(false);
await _apiController.UserSetTypingState(false, _lastScope).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
@@ -140,7 +143,7 @@ public class ChatService : DisposableMediatorSubscriberBase
{
_ = Task.Run(async () =>
{
try { await _apiController.UserSetTypingState(false).ConfigureAwait(false); }
try { await _apiController.UserSetTypingState(false, _lastScope).ConfigureAwait(false); }
catch (Exception ex) { _logger.LogDebug(ex, "ClearTypingState: failed to send typing=false"); }
});
_isTypingAnnounced = false;

View File

@@ -10,6 +10,8 @@ using FFXIVClientStructs.FFXIV.Client.UI.Shell;
using Microsoft.Extensions.Logging;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.WebAPI;
using MareSynchronos.MareConfiguration;
using MareSynchronos.API.Data.Enum;
namespace MareSynchronos.Services;
@@ -24,16 +26,18 @@ public sealed class ChatTypingDetectionService : IDisposable
private readonly ApiController _apiController;
private readonly PairManager _pairManager;
private readonly IPartyList _partyList;
private readonly MareConfigService _configService;
private string _lastChatText = string.Empty;
private bool _isTyping;
private bool _notifyingRemote;
private bool _serverSupportWarnLogged;
private bool _remoteNotificationsEnabled;
private bool _subscribed;
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
TypingIndicatorStateService typingStateService, ApiController apiController)
TypingIndicatorStateService typingStateService, ApiController apiController, MareConfigService configService)
{
_logger = logger;
_framework = framework;
@@ -44,17 +48,50 @@ public sealed class ChatTypingDetectionService : IDisposable
_partyList = partyList;
_typingStateService = typingStateService;
_apiController = apiController;
_configService = configService;
_framework.Update += OnFrameworkUpdate;
Subscribe();
_logger.LogInformation("ChatTypingDetectionService initialized");
}
public void Dispose()
{
_framework.Update -= OnFrameworkUpdate;
Unsubscribe();
ResetTypingState();
}
public void SoftRestart()
{
try
{
_logger.LogInformation("TypingDetection: soft restarting");
Unsubscribe();
ResetTypingState();
_chatService.ClearTypingState();
_typingStateService.ClearAll();
Subscribe();
_logger.LogInformation("TypingDetection: soft restart completed");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "TypingDetection: soft restart failed");
}
}
private void Subscribe()
{
if (_subscribed) return;
_framework.Update += OnFrameworkUpdate;
_subscribed = true;
}
private void Unsubscribe()
{
if (!_subscribed) return;
_framework.Update -= OnFrameworkUpdate;
_subscribed = false;
}
private void OnFrameworkUpdate(IFramework framework)
{
try
@@ -65,6 +102,13 @@ public sealed class ChatTypingDetectionService : IDisposable
return;
}
if (!_configService.Current.TypingIndicatorEnabled)
{
ResetTypingState();
_chatService.ClearTypingState();
return;
}
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
{
ResetTypingState();
@@ -89,7 +133,8 @@ public sealed class ChatTypingDetectionService : IDisposable
{
if (notifyRemote)
{
_chatService.NotifyTypingKeystroke();
var scope = GetCurrentTypingScope();
_chatService.NotifyTypingKeystroke(scope);
_notifyingRemote = true;
}
@@ -120,6 +165,35 @@ public sealed class ChatTypingDetectionService : IDisposable
_typingStateService.SetSelfTypingLocal(false);
}
private unsafe TypingScope GetCurrentTypingScope()
{
try
{
var shellModule = RaptureShellModule.Instance();
if (shellModule == null)
return TypingScope.Unknown;
var chatType = (XivChatType)shellModule->ChatType;
switch (chatType)
{
case XivChatType.Say:
case XivChatType.Shout:
case XivChatType.Yell:
return TypingScope.Proximity;
case XivChatType.Party:
return TypingScope.Party;
case XivChatType.CrossParty:
return TypingScope.CrossParty;
default:
return TypingScope.Unknown;
}
}
catch
{
return TypingScope.Unknown;
}
}
private static bool IsIgnoredCommand(string chatText)
{
if (string.IsNullOrWhiteSpace(chatText))
@@ -146,6 +220,11 @@ public sealed class ChatTypingDetectionService : IDisposable
{
try
{
if (!_configService.Current.TypingIndicatorEnabled)
{
return false;
}
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
var connected = _apiController.IsConnected;
if (!connected || !supportsTypingState)

View File

@@ -41,8 +41,8 @@ public class PartyListTypingService : DisposableMediatorSubscriberBase
public void Draw()
{
if (!_configService.Current.TypingIndicatorEnabled) return;
if (!_configService.Current.TypingIndicatorShowOnPartyList) return;
// Build map of visible users by AliasOrUID -> UID (case-insensitive)
var visibleByAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{

View File

@@ -5,24 +5,27 @@ using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using MareSynchronos.API.Data;
using MareSynchronos.MareConfiguration;
namespace MareSynchronos.Services;
public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposable
{
private sealed record TypingEntry(UserData User, DateTime FirstSeen, DateTime LastUpdate);
private sealed record TypingEntry(UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope);
private readonly ConcurrentDictionary<string, TypingEntry> _typingUsers = new(StringComparer.Ordinal);
private readonly ApiController _apiController;
private readonly ILogger<TypingIndicatorStateService> _logger;
private readonly MareConfigService _configService;
private DateTime _selfTypingLast = DateTime.MinValue;
private DateTime _selfTypingStart = DateTime.MinValue;
private bool _selfTypingActive;
public TypingIndicatorStateService(ILogger<TypingIndicatorStateService> logger, MareMediator mediator, ApiController apiController)
public TypingIndicatorStateService(ILogger<TypingIndicatorStateService> logger, MareMediator mediator, ApiController apiController, MareConfigService configService)
{
_logger = logger;
_apiController = apiController;
_configService = configService;
Mediator = mediator;
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
@@ -51,8 +54,19 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
_selfTypingActive = isTyping;
}
public void ClearAll()
{
_typingUsers.Clear();
_selfTypingActive = false;
_selfTypingStart = DateTime.MinValue;
_selfTypingLast = DateTime.MinValue;
_logger.LogDebug("TypingIndicatorStateService: cleared all typing state");
}
private void OnTypingState(UserTypingStateMessage msg)
{
if (!_configService.Current.TypingIndicatorEnabled)
return;
var uid = msg.Typing.User.UID;
var now = DateTime.UtcNow;
@@ -74,8 +88,8 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
else if (msg.Typing.IsTyping)
{
_typingUsers.AddOrUpdate(uid,
_ => new TypingEntry(msg.Typing.User, now, now),
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now));
_ => new TypingEntry(msg.Typing.User, now, now, msg.Typing.Scope),
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now, msg.Typing.Scope));
}
else
{
@@ -101,7 +115,7 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
return true;
}
public IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> GetActiveTypers(TimeSpan maxAge)
public IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope)> GetActiveTypers(TimeSpan maxAge)
{
var now = DateTime.UtcNow;
foreach (var kvp in _typingUsers.ToArray())
@@ -112,6 +126,6 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
}
}
return _typingUsers.ToDictionary(k => k.Key, v => (v.Value.User, v.Value.FirstSeen, v.Value.LastUpdate), StringComparer.Ordinal);
return _typingUsers.ToDictionary(k => k.Key, v => (v.Value.User, v.Value.FirstSeen, v.Value.LastUpdate, v.Value.Scope), StringComparer.Ordinal);
}
}