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

Submodule MareAPI updated: f75f16fb13...d105d20507

View File

@@ -87,6 +87,8 @@ public class MareConfig : IMareConfiguration
public bool ExtraChatTags { get; set; } = false; public bool ExtraChatTags { get; set; } = false;
public bool TypingIndicatorShowOnNameplates { get; set; } = true; public bool TypingIndicatorShowOnNameplates { get; set; } = true;
public bool TypingIndicatorShowOnPartyList { get; set; } = true; public bool TypingIndicatorShowOnPartyList { get; set; } = true;
public bool TypingIndicatorEnabled { get; set; } = true;
public bool TypingIndicatorShowSelf { get; set; } = true;
public TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large; public TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large;
public bool MareAPI { get; set; } = true; public bool MareAPI { get; set; } = true;

View File

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

View File

@@ -10,6 +10,8 @@ using FFXIVClientStructs.FFXIV.Client.UI.Shell;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using MareSynchronos.MareConfiguration;
using MareSynchronos.API.Data.Enum;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
@@ -24,16 +26,18 @@ public sealed class ChatTypingDetectionService : IDisposable
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly IPartyList _partyList; private readonly IPartyList _partyList;
private readonly MareConfigService _configService;
private string _lastChatText = string.Empty; private string _lastChatText = string.Empty;
private bool _isTyping; private bool _isTyping;
private bool _notifyingRemote; private bool _notifyingRemote;
private bool _serverSupportWarnLogged; private bool _serverSupportWarnLogged;
private bool _remoteNotificationsEnabled; private bool _remoteNotificationsEnabled;
private bool _subscribed;
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework, public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList, IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
TypingIndicatorStateService typingStateService, ApiController apiController) TypingIndicatorStateService typingStateService, ApiController apiController, MareConfigService configService)
{ {
_logger = logger; _logger = logger;
_framework = framework; _framework = framework;
@@ -44,17 +48,50 @@ public sealed class ChatTypingDetectionService : IDisposable
_partyList = partyList; _partyList = partyList;
_typingStateService = typingStateService; _typingStateService = typingStateService;
_apiController = apiController; _apiController = apiController;
_configService = configService;
_framework.Update += OnFrameworkUpdate; Subscribe();
_logger.LogInformation("ChatTypingDetectionService initialized"); _logger.LogInformation("ChatTypingDetectionService initialized");
} }
public void Dispose() public void Dispose()
{ {
_framework.Update -= OnFrameworkUpdate; Unsubscribe();
ResetTypingState(); 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) private void OnFrameworkUpdate(IFramework framework)
{ {
try try
@@ -65,6 +102,13 @@ public sealed class ChatTypingDetectionService : IDisposable
return; return;
} }
if (!_configService.Current.TypingIndicatorEnabled)
{
ResetTypingState();
_chatService.ClearTypingState();
return;
}
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText)) if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
{ {
ResetTypingState(); ResetTypingState();
@@ -89,7 +133,8 @@ public sealed class ChatTypingDetectionService : IDisposable
{ {
if (notifyRemote) if (notifyRemote)
{ {
_chatService.NotifyTypingKeystroke(); var scope = GetCurrentTypingScope();
_chatService.NotifyTypingKeystroke(scope);
_notifyingRemote = true; _notifyingRemote = true;
} }
@@ -120,6 +165,35 @@ public sealed class ChatTypingDetectionService : IDisposable
_typingStateService.SetSelfTypingLocal(false); _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) private static bool IsIgnoredCommand(string chatText)
{ {
if (string.IsNullOrWhiteSpace(chatText)) if (string.IsNullOrWhiteSpace(chatText))
@@ -146,6 +220,11 @@ public sealed class ChatTypingDetectionService : IDisposable
{ {
try try
{ {
if (!_configService.Current.TypingIndicatorEnabled)
{
return false;
}
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState; var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
var connected = _apiController.IsConnected; var connected = _apiController.IsConnected;
if (!connected || !supportsTypingState) if (!connected || !supportsTypingState)

View File

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

View File

@@ -5,24 +5,27 @@ using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.MareConfiguration;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposable 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 ConcurrentDictionary<string, TypingEntry> _typingUsers = new(StringComparer.Ordinal);
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly ILogger<TypingIndicatorStateService> _logger; private readonly ILogger<TypingIndicatorStateService> _logger;
private readonly MareConfigService _configService;
private DateTime _selfTypingLast = DateTime.MinValue; private DateTime _selfTypingLast = DateTime.MinValue;
private DateTime _selfTypingStart = DateTime.MinValue; private DateTime _selfTypingStart = DateTime.MinValue;
private bool _selfTypingActive; 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; _logger = logger;
_apiController = apiController; _apiController = apiController;
_configService = configService;
Mediator = mediator; Mediator = mediator;
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState); mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
@@ -51,8 +54,19 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
_selfTypingActive = isTyping; _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) private void OnTypingState(UserTypingStateMessage msg)
{ {
if (!_configService.Current.TypingIndicatorEnabled)
return;
var uid = msg.Typing.User.UID; var uid = msg.Typing.User.UID;
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@@ -74,8 +88,8 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
else if (msg.Typing.IsTyping) else if (msg.Typing.IsTyping)
{ {
_typingUsers.AddOrUpdate(uid, _typingUsers.AddOrUpdate(uid,
_ => new TypingEntry(msg.Typing.User, now, now), _ => new TypingEntry(msg.Typing.User, now, now, msg.Typing.Scope),
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now)); (_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now, msg.Typing.Scope));
} }
else else
{ {
@@ -101,7 +115,7 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
return true; 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; var now = DateTime.UtcNow;
foreach (var kvp in _typingUsers.ToArray()) 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);
} }
} }

View File

@@ -52,6 +52,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly AccountRegistrationService _registerService; private readonly AccountRegistrationService _registerService;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
private readonly TypingIndicatorStateService _typingStateService;
private readonly ChatTypingDetectionService _chatTypingDetectionService;
private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123"; private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123";
private bool _deleteAccountPopupModalShown = false; private bool _deleteAccountPopupModalShown = false;
private string _lastTab = string.Empty; private string _lastTab = string.Empty;
@@ -80,7 +82,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
FileCompactor fileCompactor, ApiController apiController, FileCompactor fileCompactor, ApiController apiController,
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor, IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor,
DalamudUtilService dalamudUtilService, AccountRegistrationService registerService, DalamudUtilService dalamudUtilService, AccountRegistrationService registerService,
AutoDetectSuppressionService autoDetectSuppressionService) : base(logger, mediator, "Umbra Settings", performanceCollector) AutoDetectSuppressionService autoDetectSuppressionService,
TypingIndicatorStateService typingIndicatorStateService,
ChatTypingDetectionService chatTypingDetectionService) : base(logger, mediator, "Umbra Settings", performanceCollector)
{ {
_configService = configService; _configService = configService;
_pairManager = pairManager; _pairManager = pairManager;
@@ -102,6 +106,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
_autoDetectSuppressionService = autoDetectSuppressionService; _autoDetectSuppressionService = autoDetectSuppressionService;
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_uiShared = uiShared; _uiShared = uiShared;
_typingStateService = typingIndicatorStateService;
_chatTypingDetectionService = chatTypingDetectionService;
AllowClickthrough = false; AllowClickthrough = false;
AllowPinning = false; AllowPinning = false;
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); _validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
@@ -1123,8 +1129,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
var useNameColors = _configService.Current.UseNameColors; var useNameColors = _configService.Current.UseNameColors;
var nameColors = _configService.Current.NameColors; var nameColors = _configService.Current.NameColors;
var autoPausedNameColors = _configService.Current.BlockedNameColors; var autoPausedNameColors = _configService.Current.BlockedNameColors;
var typingEnabled = _configService.Current.TypingIndicatorEnabled;
var typingIndicatorNameplates = _configService.Current.TypingIndicatorShowOnNameplates; var typingIndicatorNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
var typingIndicatorPartyList = _configService.Current.TypingIndicatorShowOnPartyList; var typingIndicatorPartyList = _configService.Current.TypingIndicatorShowOnPartyList;
var typingShowSelf = _configService.Current.TypingIndicatorShowSelf;
if (ImGui.Checkbox("Coloriser les plaques de nom des paires", ref useNameColors)) if (ImGui.Checkbox("Coloriser les plaques de nom des paires", ref useNameColors))
{ {
_configService.Current.UseNameColors = useNameColors; _configService.Current.UseNameColors = useNameColors;
@@ -1152,6 +1160,16 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
} }
if (ImGui.Checkbox("Activer le système d'indicateur de frappe", ref typingEnabled))
{
_configService.Current.TypingIndicatorEnabled = typingEnabled;
_configService.Save();
_chatTypingDetectionService.SoftRestart();
}
_uiShared.DrawHelpText("Active ou désactive complètement l'envoi/la réception et l'affichage des bulles de frappe.");
using (ImRaii.Disabled(!typingEnabled))
{
if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates)) if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates))
{ {
_configService.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates; _configService.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates;
@@ -1189,6 +1207,21 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
_uiShared.DrawHelpText("Consigne dans les journaux quand une paire du groupe est en train d'écrire (bulle visuelle ultérieure)."); _uiShared.DrawHelpText("Consigne dans les journaux quand une paire du groupe est en train d'écrire (bulle visuelle ultérieure).");
if (ImGui.Checkbox("Afficher ma propre bulle", ref typingShowSelf))
{
_configService.Current.TypingIndicatorShowSelf = typingShowSelf;
_configService.Save();
}
_uiShared.DrawHelpText("Affiche votre propre bulle lorsque vous tapez (utile pour test/retour visuel).");
if (ImGui.Button("Redémarrer le système de frappe"))
{
_chatTypingDetectionService.SoftRestart();
_guiHookService.RequestRedraw();
}
_uiShared.DrawHelpText("Vide l'état local et réinitialise les timers sans redémarrer le plugin.");
}
if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate))
{ {
_configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate;

View File

@@ -7,6 +7,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models; using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
@@ -75,6 +76,10 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
if (!_clientState.IsLoggedIn) if (!_clientState.IsLoggedIn)
return; return;
var typingEnabled = _configService.Current.TypingIndicatorEnabled;
if (!typingEnabled)
return;
var showParty = _configService.Current.TypingIndicatorShowOnPartyList; var showParty = _configService.Current.TypingIndicatorShowOnPartyList;
var showNameplates = _configService.Current.TypingIndicatorShowOnNameplates; var showNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
@@ -97,14 +102,16 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
} }
} }
private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers, private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope)> activeTypers,
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast) bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
{ {
var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address; var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address;
if (partyAddon == null || !partyAddon->IsVisible) if (partyAddon == null || !partyAddon->IsVisible)
return; return;
var showSelf = _configService.Current.TypingIndicatorShowSelf;
if (selfActive if (selfActive
&& showSelf
&& (now - selfStart) >= TypingDisplayDelay && (now - selfStart) >= TypingDisplayDelay
&& (now - selfLast) <= TypingDisplayFade) && (now - selfLast) <= TypingDisplayFade)
{ {
@@ -180,14 +187,16 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f))); ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f)));
} }
private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers, private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate, MareSynchronos.API.Data.Enum.TypingScope Scope)> activeTypers,
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast) bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
{ {
var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty(); var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty();
if (iconWrap == null || iconWrap.Handle == IntPtr.Zero) if (iconWrap == null || iconWrap.Handle == IntPtr.Zero)
return; return;
var showSelf = _configService.Current.TypingIndicatorShowSelf;
if (selfActive if (selfActive
&& showSelf
&& _clientState.LocalPlayer != null && _clientState.LocalPlayer != null
&& (now - selfStart) >= TypingDisplayDelay && (now - selfStart) >= TypingDisplayDelay
&& (now - selfLast) <= TypingDisplayFade) && (now - selfLast) <= TypingDisplayFade)
@@ -212,11 +221,22 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty; var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
var pairIdent = pair?.Ident ?? string.Empty; var pairIdent = pair?.Ident ?? string.Empty;
var isPartyMember = IsPartyMember(objectId, pairName); var isPartyMember = IsPartyMember(objectId, pairName);
// Enforce party-only visibility when the scope is Party/CrossParty
if (entry.Scope is TypingScope.Party or TypingScope.CrossParty)
{
if (!isPartyMember)
{
_typedLogger.LogTrace("TypingIndicator: suppressed non-party bubble for {uid} due to scope={scope}", uid, entry.Scope);
continue;
}
}
var isRelevantMember = IsPlayerRelevant(pair, isPartyMember); var isRelevantMember = IsPlayerRelevant(pair, isPartyMember);
if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId)) if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId))
{ {
_typedLogger.LogTrace("TypingIndicator: drew nameplate bubble for {uid} (objectId={objectId})", uid, objectId); _typedLogger.LogTrace("TypingIndicator: drew nameplate bubble for {uid} (objectId={objectId}, scope={scope})", uid, objectId, entry.Scope);
continue; continue;
} }
@@ -226,13 +246,20 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
if (!isRelevantMember && !isNearby) if (!isRelevantMember && !isNearby)
continue; continue;
// For Party/CrossParty scope, do not draw fallback world icon for non-party even if nearby
if (entry.Scope is TypingScope.Party or TypingScope.CrossParty)
{
if (!isPartyMember)
continue;
}
if (pair == null) if (pair == null)
{ {
_typedLogger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid); _typedLogger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
} }
_typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})", _typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident}, scope={scope})",
uid, objectId, pairName, pairIdent); uid, objectId, pairName, pairIdent, entry.Scope);
if (hasWorldPosition) if (hasWorldPosition)
{ {

View File

@@ -103,6 +103,21 @@ public partial class ApiController
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false); await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false);
} }
public async Task UserSetTypingState(bool isTyping, MareSynchronos.API.Data.Enum.TypingScope scope)
{
CheckConnection();
try
{
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping, scope).ConfigureAwait(false);
}
catch (Exception ex)
{
// fallback for older servers without scope support
Logger.LogDebug(ex, "UserSetTypingState(scope) not supported on server, falling back to legacy call");
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false);
}
}
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters) private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
{ {
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID))); Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));