diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index 3bf4c1e..8e2c0df 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -59,6 +59,7 @@ public class MareConfig : IMareConfiguration public bool ShowUploading { get; set; } = true; public bool ShowUploadingBigText { get; set; } = true; public bool ShowVisibleUsersSeparately { get; set; } = true; + public string LastChangelogVersionSeen { get; set; } = string.Empty; public bool EnableAutoDetectDiscovery { get; set; } = false; public bool AllowAutoDetectPairRequests { get; set; } = false; public int AutoDetectMaxDistanceMeters { get; set; } = 40; @@ -79,4 +80,4 @@ public class MareConfig : IMareConfiguration public bool ExtraChatTags { get; set; } = false; public bool MareAPI { get; set; } = true; -} \ No newline at end of file +} diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index c62986e..2eca83f 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ UmbraSync UmbraSync - 0.1.8.1 + 0.1.8.2 diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 2335d9b..63f287d 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -182,6 +182,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); + collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); diff --git a/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs b/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs index 527c08c..ffe2e0f 100644 --- a/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs +++ b/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using MareSynchronos.WebAPI.AutoDetect; using MareSynchronos.MareConfiguration; @@ -15,6 +17,11 @@ public class AutoDetectRequestService private readonly MareConfigService _configService; private readonly DalamudUtilService _dalamud; private readonly MareMediator _mediator; + private readonly object _syncRoot = new(); + private readonly Dictionary _activeCooldowns = new(StringComparer.Ordinal); + private readonly Dictionary _refusalTrackers = new(StringComparer.Ordinal); + private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5); + private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15); public AutoDetectRequestService(ILogger logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService) { @@ -26,7 +33,7 @@ public class AutoDetectRequestService _dalamud = dalamudUtilService; } - public async Task SendRequestAsync(string token, CancellationToken ct = default) + public async Task SendRequestAsync(string token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default) { if (!_configService.Current.AllowAutoDetectPairRequests) { @@ -34,6 +41,47 @@ public class AutoDetectRequestService _mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info)); return false; } + var targetKey = BuildTargetKey(uid, token, targetDisplayName); + if (!string.IsNullOrEmpty(targetKey)) + { + var now = DateTime.UtcNow; + lock (_syncRoot) + { + if (_refusalTrackers.TryGetValue(targetKey, out var tracker)) + { + if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now) + { + PublishLockNotification(tracker.LockUntil.Value - now); + return false; + } + + if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now) + { + tracker.LockUntil = null; + tracker.Count = 0; + if (tracker.Count == 0 && tracker.LockUntil == null) + { + _refusalTrackers.Remove(targetKey); + } + } + } + + if (_activeCooldowns.TryGetValue(targetKey, out var lastSent)) + { + var elapsed = now - lastSent; + if (elapsed < RequestCooldown) + { + PublishCooldownNotification(RequestCooldown - elapsed); + return false; + } + + if (elapsed >= RequestCooldown) + { + _activeCooldowns.Remove(targetKey); + } + } + } + } var endpoint = _configProvider.RequestEndpoint; if (string.IsNullOrEmpty(endpoint)) { @@ -53,10 +101,51 @@ public class AutoDetectRequestService var ok = await _client.SendRequestAsync(endpoint!, token, displayName, ct).ConfigureAwait(false); if (ok) { + if (!string.IsNullOrEmpty(targetKey)) + { + lock (_syncRoot) + { + _activeCooldowns[targetKey] = DateTime.UtcNow; + if (_refusalTrackers.TryGetValue(targetKey, out var tracker)) + { + tracker.Count = 0; + tracker.LockUntil = null; + if (tracker.Count == 0 && tracker.LockUntil == null) + { + _refusalTrackers.Remove(targetKey); + } + } + } + } _mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info)); } else { + if (!string.IsNullOrEmpty(targetKey)) + { + var now = DateTime.UtcNow; + lock (_syncRoot) + { + _activeCooldowns.Remove(targetKey); + if (!_refusalTrackers.TryGetValue(targetKey, out var tracker)) + { + tracker = new RefusalTracker(); + _refusalTrackers[targetKey] = tracker; + } + + if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now) + { + tracker.LockUntil = null; + tracker.Count = 0; + } + + tracker.Count++; + if (tracker.Count >= 3) + { + tracker.LockUntil = now.Add(RefusalLockDuration); + } + } + } _mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning)); } return ok; @@ -80,4 +169,42 @@ public class AutoDetectRequestService _logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint); return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false); } + + private static string? BuildTargetKey(string? uid, string? token, string? displayName) + { + if (!string.IsNullOrEmpty(uid)) return "uid:" + uid; + if (!string.IsNullOrEmpty(token)) return "token:" + token; + if (!string.IsNullOrEmpty(displayName)) return "name:" + displayName; + return null; + } + + private void PublishCooldownNotification(TimeSpan remaining) + { + var durationText = FormatDuration(remaining); + _mediator.Publish(new NotificationMessage("Nearby request en attente", $"Nearby request déjà envoyée. Merci d'attendre environ {durationText} avant de réessayer.", NotificationType.Info, TimeSpan.FromSeconds(5))); + } + + private void PublishLockNotification(TimeSpan remaining) + { + var durationText = FormatDuration(remaining); + _mediator.Publish(new NotificationMessage("Nearby request bloquée", $"Nearby request bloquée après plusieurs refus. Réessayez dans {durationText}.", NotificationType.Warning, TimeSpan.FromSeconds(5))); + } + + private static string FormatDuration(TimeSpan remaining) + { + if (remaining.TotalMinutes >= 1) + { + var minutes = Math.Max(1, (int)Math.Ceiling(remaining.TotalMinutes)); + return minutes == 1 ? "1 minute" : minutes + " minutes"; + } + + var seconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds)); + return seconds == 1 ? "1 seconde" : seconds + " secondes"; + } + + private sealed class RefusalTracker + { + public int Count; + public DateTime? LockUntil; + } } diff --git a/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs b/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs index 8259446..21eb91c 100644 --- a/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs +++ b/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs @@ -201,7 +201,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter) || ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase) || d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase) - || (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)))) + || (_serverConfigurationManager.GetNoteForUid(d.Key.UID) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)))) .ToDictionary(k => k.Key, k => k.Value)) { // filter all poses based on territory, that always must be correct diff --git a/MareSynchronos/Services/NotificationService.cs b/MareSynchronos/Services/NotificationService.cs index 9a48c57..51a9371 100644 --- a/MareSynchronos/Services/NotificationService.cs +++ b/MareSynchronos/Services/NotificationService.cs @@ -1,4 +1,5 @@ -using Dalamud.Game.Text.SeStringHandling; +using System; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using MareSynchronos.MareConfiguration; @@ -81,41 +82,94 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ if (!_dalamudUtilService.IsLoggedIn) return; - switch (msg.Type) + bool appendInstruction; + bool forceChat = ShouldForceChat(msg, out appendInstruction); + var effectiveMessage = forceChat && appendInstruction ? AppendUsyncInstruction(msg.Message) : msg.Message; + var adjustedMsg = forceChat && appendInstruction ? msg with { Message = effectiveMessage } : msg; + + switch (adjustedMsg.Type) { case NotificationType.Info: - ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification); + ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.InfoNotification, forceChat); break; case NotificationType.Warning: - ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification); + ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.WarningNotification, forceChat); break; case NotificationType.Error: - ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification); + ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.ErrorNotification, forceChat); break; } } - private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location) + private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction) { - switch (location) + appendInstruction = false; + + bool IsNearbyRequestText(string? text) { - case NotificationLocation.Toast: - ShowToast(msg); - break; + if (string.IsNullOrEmpty(text)) return false; + return text.Contains("Nearby request", StringComparison.OrdinalIgnoreCase) + || text.Contains("Nearby Request", StringComparison.Ordinal); + } - case NotificationLocation.Chat: - ShowChat(msg); - break; + bool IsNearbyAcceptText(string? text) + { + if (string.IsNullOrEmpty(text)) return false; + return text.Contains("Nearby Accept", StringComparison.OrdinalIgnoreCase); + } - case NotificationLocation.Both: - ShowToast(msg); - ShowChat(msg); - break; + bool isAccept = IsNearbyAcceptText(msg.Title) || IsNearbyAcceptText(msg.Message); + if (isAccept) + return false; - case NotificationLocation.Nowhere: - break; + bool isRequest = IsNearbyRequestText(msg.Title) || IsNearbyRequestText(msg.Message); + if (isRequest) + { + appendInstruction = !IsRequestSentConfirmation(msg); + return true; + } + + return false; + } + + private static bool IsRequestSentConfirmation(NotificationMessage msg) + { + if (string.Equals(msg.Title, "Nearby request sent", StringComparison.OrdinalIgnoreCase)) + return true; + + if (!string.IsNullOrEmpty(msg.Message) && msg.Message.Contains("The other user will receive a request notification.", StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + private static string AppendUsyncInstruction(string? message) + { + const string suffix = " | Ouvrez /usync pour voir l'invitation."; + if (string.IsNullOrWhiteSpace(message)) + return suffix.TrimStart(' ', '|'); + + if (message.Contains("/usync", StringComparison.OrdinalIgnoreCase)) + return message; + + return message.TrimEnd() + suffix; + } + + private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location, bool forceChat) + { + bool showToast = location is NotificationLocation.Toast or NotificationLocation.Both; + bool showChat = forceChat || location is NotificationLocation.Chat or NotificationLocation.Both; + + if (showToast) + { + ShowToast(msg); + } + + if (showChat) + { + ShowChat(msg); } } @@ -138,4 +192,4 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3) }); } -} \ No newline at end of file +} diff --git a/MareSynchronos/UI/AutoDetectUi.cs b/MareSynchronos/UI/AutoDetectUi.cs index f06a9d0..81ff943 100644 --- a/MareSynchronos/UI/AutoDetectUi.cs +++ b/MareSynchronos/UI/AutoDetectUi.cs @@ -108,7 +108,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase } else if (ImGui.Button($"Send request##{e.Name}")) { - _ = _requestService.SendRequestAsync(e.Token!); + _ = _requestService.SendRequestAsync(e.Token!, e.Uid, e.DisplayName); } } } diff --git a/MareSynchronos/UI/ChangelogUi.cs b/MareSynchronos/UI/ChangelogUi.cs new file mode 100644 index 0000000..7da4ec9 --- /dev/null +++ b/MareSynchronos/UI/ChangelogUi.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.UI; + +public sealed class ChangelogUi : WindowMediatorSubscriberBase +{ + private const int AlwaysExpandedEntryCount = 2; + + private readonly MareConfigService _configService; + private readonly UiSharedService _uiShared; + private readonly Version _currentVersion; + private readonly string _currentVersionLabel; + private readonly IReadOnlyList _entries; + + private bool _showAllEntries; + private bool _hasAcknowledgedVersion; + + public ChangelogUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, + MareMediator mediator, PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Umbra Sync - Notes de version", performanceCollectorService) + { + _uiShared = uiShared; + _configService = configService; + _currentVersion = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0); + _currentVersionLabel = _currentVersion.ToString(); + _entries = BuildEntries(); + _hasAcknowledgedVersion = string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal); + + RespectCloseHotkey = true; + SizeConstraints = new() + { + MinimumSize = new(520, 360), + MaximumSize = new(900, 1200) + }; + Flags |= ImGuiWindowFlags.NoResize; + ShowCloseButton = true; + + if (!string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal)) + { + IsOpen = true; + } + } + + public override void OnClose() + { + MarkCurrentVersionAsReadIfNeeded(); + base.OnClose(); + } + + protected override void DrawInternal() + { + _ = _uiShared.DrawOtherPluginState(); + + DrawHeader(); + DrawEntries(); + DrawFooter(); + } + + private void DrawHeader() + { + using (_uiShared.UidFont.Push()) + { + ImGui.TextUnformatted("Notes de version"); + } + + ImGui.TextColored(ImGuiColors.DalamudGrey, $"Version chargée : {_currentVersionLabel}"); + ImGui.Separator(); + } + + private void DrawEntries() + { + bool expandedOldVersions = false; + for (int index = 0; index < _entries.Count; index++) + { + var entry = _entries[index]; + if (!_showAllEntries && index >= AlwaysExpandedEntryCount) + { + if (!expandedOldVersions) + { + expandedOldVersions = ImGui.CollapsingHeader("Historique complet"); + } + + if (!expandedOldVersions) + { + continue; + } + } + + DrawEntry(entry); + } + } + + private void DrawEntry(ChangelogEntry entry) + { + using (ImRaii.PushId(entry.VersionLabel)) + { + ImGui.Spacing(); + UiSharedService.ColorText(entry.VersionLabel, entry.Version == _currentVersion + ? ImGuiColors.HealerGreen + : ImGuiColors.DalamudWhite); + + ImGui.Spacing(); + + foreach (var line in entry.Lines) + { + DrawLine(line); + } + + ImGui.Spacing(); + ImGui.Separator(); + } + } + + private static void DrawLine(ChangelogLine line) + { + using var indent = line.IndentLevel > 0 ? ImRaii.PushIndent(line.IndentLevel) : null; + if (line.Color != null) + { + ImGui.TextColored(line.Color.Value, $"- {line.Text}"); + } + else + { + ImGui.TextUnformatted($"- {line.Text}"); + } + } + + private void DrawFooter() + { + ImGui.Spacing(); + if (!_showAllEntries && _entries.Count > AlwaysExpandedEntryCount) + { + if (ImGui.Button("Tout afficher")) + { + _showAllEntries = true; + } + + ImGui.SameLine(); + } + + if (ImGui.Button("Marquer comme lu")) + { + MarkCurrentVersionAsReadIfNeeded(); + IsOpen = false; + } + } + + private void MarkCurrentVersionAsReadIfNeeded() + { + if (_hasAcknowledgedVersion) + return; + + _configService.Current.LastChangelogVersionSeen = _currentVersionLabel; + _configService.Save(); + _hasAcknowledgedVersion = true; + } + + private static IReadOnlyList BuildEntries() + { + return new List + { + new(new Version(0, 1, 8, 2), "0.1.8.2", new List + { + new("Détection Nearby : la liste rapide ne montre plus que les joueurs réellement invitables."), + new("Sont filtrés automatiquement les personnes refusées ou déjà appairées."), + new("Invitations Nearby : anti-spam de 5 minutes par personne, blocage 15 minutes après trois refus."), + new("Affichage : Correction de l'affichage des notes par défaut plutôt que de l'ID si disponible."), + new("Les notifications de blocage sont envoyées directement dans le tchat."), + new("Overlay DTR : affiche le nombre d'invitations Nearby disponibles dans le titre et l'infobulle."), + new("Poses Nearby : le filtre re-fonctionne avec vos notes locales pour retrouver les entrées correspondantes."), + }), + new(new Version(0, 1, 8, 1), "0.1.8.1", new List + { + new("Correctif 'Vu sous' : l'infobulle affiche désormais le dernier personnage observé."), + new("Invitations AutoDetect : triées en tête de liste pour mieux les repérer."), + new("Invitations AutoDetect : conservées entre les redémarrages du plugin ou du jeu."), + new("Barre de statut serveur : couleur violette adoptée par défaut."), + }), + new(new Version(0, 1, 8, 0), "0.1.8.0", new List + { + new("AutoDetect : détection automatique des joueurs Umbra autour de vous et propositions d'appairage."), + new("AutoDetect : désactivé par défaut pour préserver la confidentialité.", 1, ImGuiColors.DalamudGrey), + new("AutoDetect : activez-le dans 'Transfers' avec les options Nearby detection et Allow pair requests.", 1, ImGuiColors.DalamudGrey), + new("Syncshell temporaire : durée configurable de 1 h à 7 jours, expiration automatique."), + new("Syncshell permanente : possibilité de nommer et d'organiser vos groupes sur la durée."), + new("Interface : palette UmbraSync harmonisée et menus allégés pour l'usage RP."), + }), + }; + } + + private readonly record struct ChangelogEntry(Version Version, string VersionLabel, IReadOnlyList Lines); + + private readonly record struct ChangelogLine(string Text, int IndentLevel = 0, System.Numerics.Vector4? Color = null); +} diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index af9966b..f8d67a9 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -471,69 +471,58 @@ public class CompactUi : WindowMediatorSubscriberBase _uiSharedService.IconText(icon); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen; ImGui.SameLine(); - var onUmbra = _nearbyEntries?.Count(e => e.IsMatch) ?? 0; + var onUmbra = _nearbyEntries?.Count(e => e.IsMatch && e.AcceptPairRequests && !string.IsNullOrEmpty(e.Token) && !IsAlreadyPairedQuickMenu(e)) ?? 0; ImGui.TextUnformatted($"Nearby ({onUmbra})"); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen; - var btnWidth = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, "Nearby"); - var headerRight = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); - ImGui.SameLine(); - ImGui.SetCursorPosX(headerRight - btnWidth); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Nearby", btnWidth)) - { - Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi))); - } - if (_nearbyOpen) { + var btnWidth = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, "Nearby"); + var headerRight = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); + ImGui.SameLine(); + ImGui.SetCursorPosX(headerRight - btnWidth); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Nearby", btnWidth)) + { + Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi))); + } + ImGui.Indent(); var nearby = _nearbyEntries == null ? new List() - : _nearbyEntries.Where(e => e.IsMatch) + : _nearbyEntries.Where(e => e.IsMatch && e.AcceptPairRequests && !string.IsNullOrEmpty(e.Token) && !IsAlreadyPairedQuickMenu(e)) .OrderBy(e => e.Distance) .ToList(); if (nearby.Count == 0) { - UiSharedService.ColorTextWrapped("Aucun joueur detecté.", ImGuiColors.DalamudGrey3); + UiSharedService.ColorTextWrapped("Aucun nouveau joueur detecté.", ImGuiColors.DalamudGrey3); } else { foreach (var e in nearby) { + if (!e.AcceptPairRequests || string.IsNullOrEmpty(e.Token)) + continue; + var name = e.DisplayName ?? e.Name; ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(name); var right = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); ImGui.SameLine(); - bool isPaired = false; - try - { - isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, e.Uid, StringComparison.Ordinal)); - } - catch - { - var key = (e.DisplayName ?? e.Name) ?? string.Empty; - isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase)); - } - var statusButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus); ImGui.SetCursorPosX(right - statusButtonSize.X); - - if (isPaired) - { - _uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.ParsedGreen); - UiSharedService.AttachToolTip("Déjà appairé"); - } - else if (!e.AcceptPairRequests) + if (!e.AcceptPairRequests) { _uiSharedService.IconText(FontAwesomeIcon.Ban, ImGuiColors.DalamudGrey3); UiSharedService.AttachToolTip("Les demandes sont désactivées pour ce joueur"); } else if (!string.IsNullOrEmpty(e.Token)) { - if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus)) + using (ImRaii.PushId(e.Token ?? e.Uid ?? e.Name ?? string.Empty)) { - _ = _autoDetectRequestService.SendRequestAsync(e.Token!); + if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus)) + { + _ = _autoDetectRequestService.SendRequestAsync(e.Token!, e.Uid, e.DisplayName); + } } UiSharedService.AttachToolTip("Envoyer une invitation d'apparaige"); } @@ -553,6 +542,27 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.EndChild(); } + private bool IsAlreadyPairedQuickMenu(Services.Mediator.NearbyEntry entry) + { + try + { + if (!string.IsNullOrEmpty(entry.Uid)) + { + if (_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, entry.Uid, StringComparison.Ordinal))) + return true; + } + + var key = (entry.DisplayName ?? entry.Name) ?? string.Empty; + if (string.IsNullOrEmpty(key)) return false; + + return _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase)); + } + catch + { + return false; + } + } + private void DrawServerStatus() { var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); diff --git a/MareSynchronos/UI/DtrEntry.cs b/MareSynchronos/UI/DtrEntry.cs index 0c2f943..8934d09 100644 --- a/MareSynchronos/UI/DtrEntry.cs +++ b/MareSynchronos/UI/DtrEntry.cs @@ -6,6 +6,7 @@ using Dalamud.Interface; using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Configurations; using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.AutoDetect; using MareSynchronos.Services.Mediator; using MareSynchronos.WebAPI; using Microsoft.Extensions.Hosting; @@ -41,12 +42,13 @@ public sealed class DtrEntry : IDisposable, IHostedService private readonly ILogger _logger; private readonly MareMediator _mareMediator; private readonly PairManager _pairManager; + private readonly NearbyPendingService _nearbyPendingService; private Task? _runTask; private string? _text; private string? _tooltip; private Colors _colors; - public DtrEntry(ILogger logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController) + public DtrEntry(ILogger logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController, NearbyPendingService nearbyPendingService) { _logger = logger; _dtrBar = dtrBar; @@ -55,6 +57,7 @@ public sealed class DtrEntry : IDisposable, IHostedService _mareMediator = mareMediator; _pairManager = pairManager; _apiController = apiController; + _nearbyPendingService = nearbyPendingService; } public void Dispose() @@ -147,8 +150,9 @@ public sealed class DtrEntry : IDisposable, IHostedService if (_apiController.IsConnected) { var pairCount = _pairManager.GetVisibleUserCount(); - - text = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString()); + var baseText = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString()); + var pendingNearby = _nearbyPendingService.Pending.Count; + text = pendingNearby > 0 ? $"{baseText} ({pendingNearby})" : baseText; if (pairCount > 0) { IEnumerable visiblePairs; @@ -165,12 +169,21 @@ public sealed class DtrEntry : IDisposable, IHostedService .Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNoteOrName() : x.PlayerName)); } - tooltip = $"Umbra: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; + tooltip = $"Umbra: Connected"; + if (pendingNearby > 0) + { + tooltip += $"{Environment.NewLine}Invitation en attente : {pendingNearby}"; + } + tooltip += $"{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; colors = _configService.Current.DtrColorsPairsInRange; } else { tooltip = "Umbra: Connected"; + if (pendingNearby > 0) + { + tooltip += $"{Environment.NewLine}Invitation en attente : {pendingNearby}"; + } colors = _configService.Current.DtrColorsDefault; } } diff --git a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs b/MareSynchronos/UI/Handlers/UidDisplayHandler.cs index 2a8246f..4698252 100644 --- a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs +++ b/MareSynchronos/UI/Handlers/UidDisplayHandler.cs @@ -154,16 +154,15 @@ public class UidDisplayHandler public (bool isUid, string text) GetPlayerText(Pair pair) { - // When the user is offline or not visible, always show the raw UID (no alias/note/character name) - if (!pair.IsOnline || !pair.IsVisible) + bool showUidInsteadOfName = ShowUidInsteadOfName(pair); + if (showUidInsteadOfName) { return (true, pair.UserData.UID); } var textIsUid = true; - bool showUidInsteadOfName = ShowUidInsteadOfName(pair); string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID); - if (!showUidInsteadOfName && playerText != null) + if (playerText != null) { if (string.IsNullOrEmpty(playerText)) { @@ -179,7 +178,7 @@ public class UidDisplayHandler playerText = pair.UserData.AliasOrUID; } - if (_mareConfigService.Current.ShowCharacterNames && textIsUid && !showUidInsteadOfName) + if (_mareConfigService.Current.ShowCharacterNames && textIsUid && pair.IsOnline && pair.IsVisible) { var name = pair.PlayerName; if (name != null) @@ -215,8 +214,11 @@ public class UidDisplayHandler private bool ShowUidInsteadOfName(Pair pair) { - _showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName); + if (_showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName)) + { + return showUidInsteadOfName; + } - return showUidInsteadOfName; + return false; } -} \ No newline at end of file +}