diff --git a/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs b/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs index 24d0446..82e9a06 100644 --- a/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs +++ b/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs @@ -258,7 +258,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber if (entries.Count != _lastLocalCount) { _lastLocalCount = entries.Count; - _logger.LogInformation("Nearby: {count} players detected locally", _lastLocalCount); + _logger.LogTrace("Nearby: {count} players detected locally", _lastLocalCount); } // Try query server if config and endpoints are present @@ -291,7 +291,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber return $"{e.Name}({e.WorldId})->{shortH}"; }); var saltShort = saltHex.Length > 8 ? saltHex[..8] : saltHex; - _logger.LogInformation("Nearby snapshot: {count} entries; salt={saltShort}…; samples=[{samples}]", + _logger.LogTrace("Nearby snapshot: {count} entries; salt={saltShort}…; samples=[{samples}]", entries.Count, saltShort, string.Join(", ", sample)); } } @@ -310,7 +310,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber ushort meWorld = 0; if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc) meWorld = (ushort)mePc.HomeWorld.RowId; - _logger.LogInformation("Nearby self ident: {name} ({world})", displayName, meWorld); + _logger.LogTrace("Nearby self ident: {name} ({world})", displayName, meWorld); selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256(); } } @@ -380,14 +380,31 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber } } } - _logger.LogInformation("Nearby: server returned {count} matches", allMatches.Count); + if (allMatches.Count > 0) + { + _logger.LogInformation("Nearby: server returned {count} matches", allMatches.Count); + } + else + { + _logger.LogTrace("Nearby: server returned {count} matches", allMatches.Count); + } // Log change in number of Umbra matches int matchCount = entries.Count(e => e.IsMatch); if (matchCount != _lastMatchCount) { _lastMatchCount = matchCount; - _logger.LogDebug("Nearby: {count} Umbra users nearby", matchCount); + if (matchCount > 0) + { + var matchSamples = entries.Where(e => e.IsMatch).Take(5) + .Select(e => string.IsNullOrEmpty(e.DisplayName) ? e.Name : e.DisplayName!); + _logger.LogInformation("Nearby: {count} Umbra users nearby [{samples}]", + matchCount, string.Join(", ", matchSamples)); + } + else + { + _logger.LogTrace("Nearby: {count} Umbra users nearby", matchCount); + } } } } diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 92dbbde..a16fa77 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -12,6 +12,7 @@ using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Services.AutoDetect; using MareSynchronos.UI.Components; using MareSynchronos.UI.Handlers; using MareSynchronos.WebAPI; @@ -44,7 +45,8 @@ public class CompactUi : WindowMediatorSubscriberBase private readonly ServerConfigurationManager _serverManager; private readonly Stopwatch _timeout = new(); private readonly CharaDataManager _charaDataManager; - private readonly Services.AutoDetect.NearbyPendingService _nearbyPending; + private readonly NearbyPendingService _nearbyPending; + private readonly AutoDetectRequestService _autoDetectRequestService; private readonly UidDisplayHandler _uidDisplayHandler; private readonly UiSharedService _uiSharedService; private bool _buttonState; @@ -63,7 +65,8 @@ public class CompactUi : WindowMediatorSubscriberBase public CompactUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService, ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager, - Services.AutoDetect.NearbyPendingService nearbyPendingService, + NearbyPendingService nearbyPendingService, + AutoDetectRequestService autoDetectRequestService, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService) { @@ -76,6 +79,7 @@ public class CompactUi : WindowMediatorSubscriberBase _uidDisplayHandler = uidDisplayHandler; _charaDataManager = charaDataManager; _nearbyPending = nearbyPendingService; + _autoDetectRequestService = autoDetectRequestService; var tagHandler = new TagHandler(_serverManager); _groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager); @@ -428,20 +432,31 @@ public class CompactUi : WindowMediatorSubscriberBase isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase)); } - var statusText = isPaired ? "✔ Paired" : (e.AcceptPairRequests ? "➕ Invite" : "⛔ Requests disabled"); - var statusSize = ImGui.CalcTextSize(statusText); - ImGui.SetCursorPosX(right - statusSize.X); + var statusButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus); + ImGui.SetCursorPosX(right - statusButtonSize.X); - if (isPaired || !e.AcceptPairRequests) + if (isPaired) { - ImGui.TextUnformatted(statusText); + _uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.ParsedGreen); + UiSharedService.AttachToolTip("Déjà apparié sur Umbra"); + } + else 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)) + { + _ = _autoDetectRequestService.SendRequestAsync(e.Token!); + } + UiSharedService.AttachToolTip("Envoyer une invitation Umbra"); } else { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Invite", statusSize.X)) - { - Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi))); - } + _uiSharedService.IconText(FontAwesomeIcon.QuestionCircle, ImGuiColors.DalamudGrey3); + UiSharedService.AttachToolTip("Impossible d'inviter ce joueur"); } } } diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs index 91c3735..5ddb379 100644 --- a/MareSynchronos/UI/Components/DrawUserPair.cs +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -16,6 +16,7 @@ namespace MareSynchronos.UI.Components; public class DrawUserPair : DrawPairBase { + private static readonly Vector4 Violet = new(0.63f, 0.25f, 1f, 1f); protected readonly MareMediator _mediator; private readonly SelectGroupForPairUi _selectGroupForPairUi; private readonly CharaDataManager _charaDataManager; @@ -39,12 +40,11 @@ public class DrawUserPair : DrawPairBase protected override void DrawLeftSide(float textPosY, float originalY) { var online = _pair.IsOnline; - var violet = new Vector4(0.69f, 0.27f, 0.93f, 1f); var offlineGrey = ImGuiColors.DalamudGrey3; ImGui.SetCursorPosY(textPosY); ImGui.PushFont(UiBuilder.IconFont); - UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), online ? violet : offlineGrey); + UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), online ? Violet : offlineGrey); ImGui.PopFont(); UiSharedService.AttachToolTip(online ? "User is online" @@ -72,7 +72,7 @@ public class DrawUserPair : DrawPairBase ImGui.SameLine(); ImGui.SetCursorPosY(textPosY); ImGui.PushFont(UiBuilder.IconFont); - UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); + UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), Violet); if (ImGui.IsItemClicked()) { _mediator.Publish(new TargetPairMessage(_pair)); @@ -301,4 +301,4 @@ public class DrawUserPair : DrawPairBase } UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); } -} \ No newline at end of file +} diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs index 0f19e8a..f9fd975 100644 --- a/MareSynchronos/UI/Components/GroupPanel.cs +++ b/MareSynchronos/UI/Components/GroupPanel.cs @@ -52,6 +52,7 @@ internal sealed class GroupPanel private bool _showModalChangePassword; private bool _showModalCreateGroup; private bool _showModalEnterPassword; + private string _newSyncShellAlias = string.Empty; private string _syncShellPassword = string.Empty; private string _syncShellToJoin = string.Empty; @@ -82,7 +83,7 @@ internal sealed class GroupPanel { var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus); ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); - ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 20); + ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 50); ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser; @@ -108,6 +109,7 @@ internal sealed class GroupPanel { _lastCreatedGroup = null; _errorGroupCreate = false; + _newSyncShellAlias = string.Empty; _showModalCreateGroup = true; ImGui.OpenPopup("Create Syncshell"); } @@ -150,13 +152,21 @@ internal sealed class GroupPanel if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags)) { - UiSharedService.TextWrapped("Press the button below to create a new Syncshell."); + UiSharedService.TextWrapped("Donnez un nom à votre Syncshell (optionnel) puis créez-la. Le préfixe 'UMB-' reste inchangé."); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##syncshellalias", "Nom du Syncshell", ref _newSyncShellAlias, 50); + UiSharedService.TextWrapped("Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell."); ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); if (ImGui.Button("Create Syncshell")) { try { - _lastCreatedGroup = ApiController.GroupCreate().Result; + var aliasInput = string.IsNullOrWhiteSpace(_newSyncShellAlias) ? null : _newSyncShellAlias.Trim(); + _lastCreatedGroup = ApiController.GroupCreate(aliasInput).Result; + if (_lastCreatedGroup != null) + { + _newSyncShellAlias = string.Empty; + } } catch { @@ -169,6 +179,10 @@ internal sealed class GroupPanel { ImGui.Separator(); _errorGroupCreate = false; + if (!string.IsNullOrWhiteSpace(_lastCreatedGroup.Group.Alias)) + { + ImGui.TextUnformatted("Syncshell Name: " + _lastCreatedGroup.Group.Alias); + } ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password); @@ -702,4 +716,4 @@ internal sealed class GroupPanel } ImGui.EndChild(); } -} \ No newline at end of file +} diff --git a/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs b/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs index 85011cd..740696f 100644 --- a/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs +++ b/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs @@ -29,24 +29,25 @@ public class DiscoveryApiClient { var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); if (string.IsNullOrEmpty(token)) return []; + var distinctHashes = hashes.Distinct(StringComparer.Ordinal).ToArray(); using var req = new HttpRequestMessage(HttpMethod.Post, endpoint); req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); var body = JsonSerializer.Serialize(new { - hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(), + hashes = distinctHashes, salt = _configProvider.SaltB64 }); req.Content = new StringContent(body, Encoding.UTF8, "application/json"); var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - var token2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + var token2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false); if (string.IsNullOrEmpty(token2)) return []; using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token2); var body2 = JsonSerializer.Serialize(new { - hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(), + hashes = distinctHashes, salt = _configProvider.SaltB64 }); req2.Content = new StringContent(body2, Encoding.UTF8, "application/json"); @@ -77,7 +78,7 @@ public class DiscoveryApiClient var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false); if (string.IsNullOrEmpty(jwt2)) return false; using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); @@ -121,7 +122,7 @@ public class DiscoveryApiClient var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false); if (string.IsNullOrEmpty(jwt2)) return false; using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); @@ -152,7 +153,7 @@ public class DiscoveryApiClient var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false); if (string.IsNullOrEmpty(jwt2)) return false; using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); @@ -179,7 +180,7 @@ public class DiscoveryApiClient var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false); if (string.IsNullOrEmpty(jwt2)) return; using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs index 7a0f54c..8ed94a6 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs @@ -49,10 +49,10 @@ public partial class ApiController await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false); } - public async Task GroupCreate() + public async Task GroupCreate(string? alias = null) { CheckConnection(); - return await _mareHub!.InvokeAsync(nameof(GroupCreate)).ConfigureAwait(false); + return await _mareHub!.InvokeAsync(nameof(GroupCreate), string.IsNullOrWhiteSpace(alias) ? null : alias.Trim()).ConfigureAwait(false); } public async Task> GroupCreateTempInvite(GroupDto group, int amount) @@ -125,4 +125,4 @@ public partial class ApiController { if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected"); } -} \ No newline at end of file +} diff --git a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs index 4867239..e244ce6 100644 --- a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs +++ b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs @@ -172,6 +172,16 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false); } + public async Task ForceRefreshToken(CancellationToken ct) + { + JwtIdentifier? jwtIdentifier = await GetIdentifier().ConfigureAwait(false); + if (jwtIdentifier == null) return null; + + _tokenCache.TryRemove(jwtIdentifier, out _); + _logger.LogTrace("ForceRefresh: Getting new token"); + return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false); + } + public string? GetStapledWellKnown(string apiUrl) { _wellKnownCache.TryGetValue(apiUrl, out var wellKnown); @@ -180,4 +190,4 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber return null; return wellKnown; } -} \ No newline at end of file +}