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
+}