diff --git a/MareAPI b/MareAPI index f75f16f..d105d20 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit f75f16fb13637ba3e2b7cfc4c79de252f4d4eea6 +Subproject commit d105d2050722a0f2e6ab695dc22adbf2437dd9c1 diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index f8218cf..11150ea 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -87,6 +87,8 @@ public class MareConfig : IMareConfiguration public bool ExtraChatTags { get; set; } = false; public bool TypingIndicatorShowOnNameplates { 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 bool MareAPI { get; set; } = true; diff --git a/MareSynchronos/Services/ChatService.cs b/MareSynchronos/Services/ChatService.cs index f893903..71ac0d3 100644 --- a/MareSynchronos/Services/ChatService.cs +++ b/MareSynchronos/Services/ChatService.cs @@ -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; diff --git a/MareSynchronos/Services/ChatTypingDetectionService.cs b/MareSynchronos/Services/ChatTypingDetectionService.cs index b8f6c2d..b21c8ca 100644 --- a/MareSynchronos/Services/ChatTypingDetectionService.cs +++ b/MareSynchronos/Services/ChatTypingDetectionService.cs @@ -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 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) diff --git a/MareSynchronos/Services/PartyListTypingService.cs b/MareSynchronos/Services/PartyListTypingService.cs index 21314a5..505adc9 100644 --- a/MareSynchronos/Services/PartyListTypingService.cs +++ b/MareSynchronos/Services/PartyListTypingService.cs @@ -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(StringComparer.OrdinalIgnoreCase); try { diff --git a/MareSynchronos/Services/TypingIndicatorStateService.cs b/MareSynchronos/Services/TypingIndicatorStateService.cs index 9e4ce2c..84f1cf4 100644 --- a/MareSynchronos/Services/TypingIndicatorStateService.cs +++ b/MareSynchronos/Services/TypingIndicatorStateService.cs @@ -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 _typingUsers = new(StringComparer.Ordinal); private readonly ApiController _apiController; private readonly ILogger _logger; + private readonly MareConfigService _configService; private DateTime _selfTypingLast = DateTime.MinValue; private DateTime _selfTypingStart = DateTime.MinValue; private bool _selfTypingActive; - public TypingIndicatorStateService(ILogger logger, MareMediator mediator, ApiController apiController) + public TypingIndicatorStateService(ILogger logger, MareMediator mediator, ApiController apiController, MareConfigService configService) { _logger = logger; _apiController = apiController; + _configService = configService; Mediator = mediator; mediator.Subscribe(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 GetActiveTypers(TimeSpan maxAge) + public IReadOnlyDictionary 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); } } diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 29d3241..642122c 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -52,6 +52,8 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly AccountRegistrationService _registerService; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly UiSharedService _uiShared; + private readonly TypingIndicatorStateService _typingStateService; + private readonly ChatTypingDetectionService _chatTypingDetectionService; private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123"; private bool _deleteAccountPopupModalShown = false; private string _lastTab = string.Empty; @@ -80,7 +82,9 @@ public class SettingsUi : WindowMediatorSubscriberBase FileCompactor fileCompactor, ApiController apiController, IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor, 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; _pairManager = pairManager; @@ -102,6 +106,8 @@ public class SettingsUi : WindowMediatorSubscriberBase _autoDetectSuppressionService = autoDetectSuppressionService; _fileCompactor = fileCompactor; _uiShared = uiShared; + _typingStateService = typingIndicatorStateService; + _chatTypingDetectionService = chatTypingDetectionService; AllowClickthrough = false; AllowPinning = false; _validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); @@ -1123,8 +1129,10 @@ public class SettingsUi : WindowMediatorSubscriberBase var useNameColors = _configService.Current.UseNameColors; var nameColors = _configService.Current.NameColors; var autoPausedNameColors = _configService.Current.BlockedNameColors; + var typingEnabled = _configService.Current.TypingIndicatorEnabled; var typingIndicatorNameplates = _configService.Current.TypingIndicatorShowOnNameplates; var typingIndicatorPartyList = _configService.Current.TypingIndicatorShowOnPartyList; + var typingShowSelf = _configService.Current.TypingIndicatorShowSelf; if (ImGui.Checkbox("Coloriser les plaques de nom des paires", ref useNameColors)) { _configService.Current.UseNameColors = useNameColors; @@ -1152,42 +1160,67 @@ public class SettingsUi : WindowMediatorSubscriberBase } } - if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates)) + if (ImGui.Checkbox("Activer le système d'indicateur de frappe", ref typingEnabled)) { - _configService.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates; + _configService.Current.TypingIndicatorEnabled = typingEnabled; _configService.Save(); + _chatTypingDetectionService.SoftRestart(); } - _uiShared.DrawHelpText("Ajoute une bulle '...' sur la plaque des paires en train d'écrire."); + _uiShared.DrawHelpText("Active ou désactive complètement l'envoi/la réception et l'affichage des bulles de frappe."); - using (ImRaii.Disabled(!typingIndicatorNameplates)) + using (ImRaii.Disabled(!typingEnabled)) { - using var indentTyping = ImRaii.PushIndent(); - var bubbleSize = _configService.Current.TypingIndicatorBubbleSize; - TypingIndicatorBubbleSize? selectedBubbleSize = _uiShared.DrawCombo("Taille de la bulle de frappe##typingBubbleSize", - Enum.GetValues(), - size => size switch - { - TypingIndicatorBubbleSize.Small => "Petite", - TypingIndicatorBubbleSize.Medium => "Moyenne", - TypingIndicatorBubbleSize.Large => "Grande", - _ => size.ToString() - }, - null, - bubbleSize); - - if (selectedBubbleSize.HasValue && selectedBubbleSize.Value != bubbleSize) + if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates)) { - _configService.Current.TypingIndicatorBubbleSize = selectedBubbleSize.Value; + _configService.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates; _configService.Save(); } - } + _uiShared.DrawHelpText("Ajoute une bulle '...' sur la plaque des paires en train d'écrire."); - if (ImGui.Checkbox("Tracer la frappe dans la liste de groupe", ref typingIndicatorPartyList)) - { - _configService.Current.TypingIndicatorShowOnPartyList = typingIndicatorPartyList; - _configService.Save(); + using (ImRaii.Disabled(!typingIndicatorNameplates)) + { + using var indentTyping = ImRaii.PushIndent(); + var bubbleSize = _configService.Current.TypingIndicatorBubbleSize; + TypingIndicatorBubbleSize? selectedBubbleSize = _uiShared.DrawCombo("Taille de la bulle de frappe##typingBubbleSize", + Enum.GetValues(), + size => size switch + { + TypingIndicatorBubbleSize.Small => "Petite", + TypingIndicatorBubbleSize.Medium => "Moyenne", + TypingIndicatorBubbleSize.Large => "Grande", + _ => size.ToString() + }, + null, + bubbleSize); + + if (selectedBubbleSize.HasValue && selectedBubbleSize.Value != bubbleSize) + { + _configService.Current.TypingIndicatorBubbleSize = selectedBubbleSize.Value; + _configService.Save(); + } + } + + if (ImGui.Checkbox("Tracer la frappe dans la liste de groupe", ref typingIndicatorPartyList)) + { + _configService.Current.TypingIndicatorShowOnPartyList = typingIndicatorPartyList; + _configService.Save(); + } + _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."); } - _uiShared.DrawHelpText("Consigne dans les journaux quand une paire du groupe est en train d'écrire (bulle visuelle ultérieure)."); if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) { diff --git a/MareSynchronos/UI/TypingIndicatorOverlay.cs b/MareSynchronos/UI/TypingIndicatorOverlay.cs index d36418c..6a857ad 100644 --- a/MareSynchronos/UI/TypingIndicatorOverlay.cs +++ b/MareSynchronos/UI/TypingIndicatorOverlay.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Data.Enum; using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Pairs; @@ -75,6 +76,10 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase if (!_clientState.IsLoggedIn) return; + var typingEnabled = _configService.Current.TypingIndicatorEnabled; + if (!typingEnabled) + return; + var showParty = _configService.Current.TypingIndicatorShowOnPartyList; var showNameplates = _configService.Current.TypingIndicatorShowOnNameplates; @@ -97,14 +102,16 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase } } - private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary activeTypers, + private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary activeTypers, bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast) { var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address; if (partyAddon == null || !partyAddon->IsVisible) return; + var showSelf = _configService.Current.TypingIndicatorShowSelf; if (selfActive + && showSelf && (now - selfStart) >= TypingDisplayDelay && (now - selfLast) <= TypingDisplayFade) { @@ -180,14 +187,16 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f))); } - private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary activeTypers, + private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary activeTypers, bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast) { var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty(); if (iconWrap == null || iconWrap.Handle == IntPtr.Zero) return; + var showSelf = _configService.Current.TypingIndicatorShowSelf; if (selfActive + && showSelf && _clientState.LocalPlayer != null && (now - selfStart) >= TypingDisplayDelay && (now - selfLast) <= TypingDisplayFade) @@ -212,11 +221,22 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty; var pairIdent = pair?.Ident ?? string.Empty; 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); 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; } @@ -226,13 +246,20 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase if (!isRelevantMember && !isNearby) 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) { _typedLogger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid); } - _typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})", - uid, objectId, pairName, pairIdent); + _typedLogger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident}, scope={scope})", + uid, objectId, pairName, pairIdent, entry.Scope); if (hasWorldPosition) { diff --git a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs index 788bd59..1782351 100644 --- a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -103,6 +103,21 @@ public partial class ApiController 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 visibleCharacters) { Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));