From 458aa5f933915a844c2a1c59f2f06a9563769cc9 Mon Sep 17 00:00:00 2001 From: azyges Date: Sat, 4 Oct 2025 01:57:50 +0900 Subject: [PATCH] bunch of changes - incoming pair requests - auto fill notes when paired - vanity colored uid at the top - notifications now resolve player names - hide lightfinder icon when not connected - fixed download snapshot crashing the ui, supposedly --- LightlessAPI | 2 +- LightlessSync/Plugin.cs | 3 +- LightlessSync/Services/Mediator/Messages.cs | 1 + LightlessSync/Services/PairRequestService.cs | 189 ++++++++++++++ LightlessSync/UI/BroadcastUI.cs | 8 +- LightlessSync/UI/CompactUI.cs | 95 ++++++- LightlessSync/UI/TopTabMenu.cs | 236 +++++++++++++++++- .../ApiController.Functions.Callbacks.cs | 22 ++ LightlessSync/WebAPI/SignalR/ApiController.cs | 5 +- 9 files changed, 542 insertions(+), 19 deletions(-) create mode 100644 LightlessSync/Services/PairRequestService.cs diff --git a/LightlessAPI b/LightlessAPI index 69f0e31..6c542c0 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 69f0e310bd78e0c56eab298199e6e2ca15bf56bd +Subproject commit 6c542c0ccca0327896ef895f9de02a76869ea311 diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index bb66c5a..c3b1216 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -1,4 +1,4 @@ -using Dalamud.Game; +using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; @@ -113,6 +113,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 2f7d8d2..b753341 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -100,6 +100,7 @@ public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase; +public record PairRequestsUpdatedMessage : MessageBase; public record VisibilityChange : MessageBase; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/LightlessSync/Services/PairRequestService.cs b/LightlessSync/Services/PairRequestService.cs new file mode 100644 index 0000000..998ea42 --- /dev/null +++ b/LightlessSync/Services/PairRequestService.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Services; + +public sealed class PairRequestService : DisposableMediatorSubscriberBase +{ + private readonly DalamudUtilService _dalamudUtil; + private readonly PairManager _pairManager; + private readonly object _syncRoot = new(); + private readonly List _requests = []; + + private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5); + + public PairRequestService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager) + : base(logger, mediator) + { + _dalamudUtil = dalamudUtil; + _pairManager = pairManager; + + Mediator.Subscribe(this, _ => + { + bool removed; + lock (_syncRoot) + { + removed = CleanupExpiredUnsafe(); + } + + if (removed) + { + Mediator.Publish(new PairRequestsUpdatedMessage()); + } + }); + } + + public PairRequestDisplay RegisterIncomingRequest(string hashedCid, string messageTemplate) + { + if (string.IsNullOrWhiteSpace(hashedCid)) + { + hashedCid = string.Empty; + } + + messageTemplate ??= string.Empty; + + PairRequestEntry entry = new(hashedCid, messageTemplate, DateTime.UtcNow); + lock (_syncRoot) + { + CleanupExpiredUnsafe(); + var index = _requests.FindIndex(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)); + if (index >= 0) + { + _requests[index] = entry; + } + else + { + _requests.Add(entry); + } + } + + var display = _dalamudUtil.IsOnFrameworkThread + ? ToDisplay(entry) + : _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult(); + + Mediator.Publish(new PairRequestsUpdatedMessage()); + return display; + } + + public IReadOnlyList GetActiveRequests() + { + List entries; + lock (_syncRoot) + { + CleanupExpiredUnsafe(); + entries = _requests + .OrderByDescending(r => r.ReceivedAt) + .ToList(); + } + + return _dalamudUtil.IsOnFrameworkThread + ? entries.Select(ToDisplay).ToList() + : _dalamudUtil.RunOnFrameworkThread(() => entries.Select(ToDisplay).ToList()).GetAwaiter().GetResult(); + } + + public bool RemoveRequest(string hashedCid) + { + bool removed; + lock (_syncRoot) + { + removed = _requests.RemoveAll(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)) > 0; + } + + if (removed) + { + Mediator.Publish(new PairRequestsUpdatedMessage()); + } + + return removed; + } + + public bool HasPendingRequests() + { + lock (_syncRoot) + { + CleanupExpiredUnsafe(); + return _requests.Count > 0; + } + } + + private PairRequestDisplay ToDisplay(PairRequestEntry entry) + { + var displayName = ResolveDisplayName(entry.HashedCid); + var message = FormatMessage(entry.MessageTemplate, displayName); + return new PairRequestDisplay(entry.HashedCid, displayName, message, entry.ReceivedAt); + } + + private string ResolveDisplayName(string hashedCid) + { + if (string.IsNullOrWhiteSpace(hashedCid)) + { + return string.Empty; + } + + var (name, address) = _dalamudUtil.FindPlayerByNameHash(hashedCid); + if (!string.IsNullOrWhiteSpace(name)) + { + var worldName = _dalamudUtil.GetWorldNameFromPlayerAddress(address); + return !string.IsNullOrWhiteSpace(worldName) + ? $"{name} @ {worldName}" + : name; + } + + var pair = _pairManager + .GetOnlineUserPairs() + .FirstOrDefault(p => string.Equals(p.Ident, hashedCid, StringComparison.Ordinal)); + + if (pair != null) + { + if (!string.IsNullOrWhiteSpace(pair.PlayerName)) + { + return pair.PlayerName; + } + + if (!string.IsNullOrWhiteSpace(pair.UserData.AliasOrUID)) + { + return pair.UserData.AliasOrUID; + } + } + + return string.Empty; + } + + private static string FormatMessage(string template, string displayName) + { + var safeName = string.IsNullOrWhiteSpace(displayName) ? "Someone" : displayName; + template ??= string.Empty; + const string placeholder = "{DisplayName}"; + + if (!string.IsNullOrEmpty(template) && template.Contains(placeholder, StringComparison.Ordinal)) + { + return template.Replace(placeholder, safeName, StringComparison.Ordinal); + } + + if (!string.IsNullOrWhiteSpace(template)) + { + return $"{safeName}: {template}"; + } + + return $"{safeName} sent you a pair request."; + } + + private bool CleanupExpiredUnsafe() + { + if (_requests.Count == 0) + { + return false; + } + + var now = DateTime.UtcNow; + return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0; + } + + private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt); + + public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt); +} diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index dc0d740..ae3d17a 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -46,8 +46,8 @@ namespace LightlessSync.UI IsOpen = false; this.SizeConstraints = new() { - MinimumSize = new(600, 450), - MaximumSize = new(750, 510) + MinimumSize = new(600, 465), + MaximumSize = new(750, 525) }; mediator.Subscribe(this, async _ => await RefreshSyncshells().ConfigureAwait(false)); @@ -143,11 +143,11 @@ namespace LightlessSync.UI _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "This lets other Lightless users know you use Lightless."); _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "While enabled, you and other people using Lightfinder can see each other identified as Lightless users."); - ImGui.Indent(5f); + ImGui.Indent(15f); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); ImGui.Text("- This is done using a 'Lightless' label above player nameplates."); ImGui.PopStyleColor(); - ImGui.Unindent(5f); + ImGui.Unindent(15f); ImGuiHelpers.ScaledDummy(3f); diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 170af20..7edbefc 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -1,4 +1,4 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -24,6 +24,7 @@ using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Globalization; +using System.Linq; using System.Numerics; using System.Reflection; @@ -85,7 +86,7 @@ public class CompactUi : WindowMediatorSubscriberBase IpcManager ipcManager, BroadcastService broadcastService, CharacterAnalyzer characterAnalyzer, - PlayerPerformanceConfigService playerPerformanceConfig) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) + PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; _configService = configService; @@ -103,7 +104,7 @@ public class CompactUi : WindowMediatorSubscriberBase _renamePairTagUi = renameTagUi; _ipcManager = ipcManager; _broadcastService = broadcastService; - _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService); + _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService); AllowPinning = true; AllowClickthrough = false; @@ -401,7 +402,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.TextUnformatted("No uploads in progress"); } - var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList(); + var currentDownloads = BuildCurrentDownloadSnapshot(); ImGui.AlignTextToFramePadding(); _uiSharedService.IconText(FontAwesomeIcon.Download); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); @@ -428,10 +429,53 @@ public class CompactUi : WindowMediatorSubscriberBase } } + + private List BuildCurrentDownloadSnapshot() + { + List snapshot = new(); + + foreach (var kvp in _currentDownloads.ToArray()) + { + var value = kvp.Value; + if (value == null || value.Count == 0) + continue; + + try + { + snapshot.AddRange(value.Values.ToArray()); + } + catch (System.ArgumentException) + { + // skibidi + } + } + + return snapshot; + } + private void DrawUIDHeader() { var uidText = GetUidText(); + Vector4? vanityTextColor = null; + Vector4? vanityGlowColor = null; + bool useVanityColors = false; + + if (_configService.Current.useColoredUIDs && _apiController.HasVanity) + { + if (!string.IsNullOrWhiteSpace(_apiController.TextColorHex)) + { + vanityTextColor = UIColors.HexToRgba(_apiController.TextColorHex); + } + + if (!string.IsNullOrWhiteSpace(_apiController.TextGlowColorHex)) + { + vanityGlowColor = UIColors.HexToRgba(_apiController.TextGlowColorHex); + } + + useVanityColors = vanityTextColor is not null || vanityGlowColor is not null; + } + //Getting information of character and triangles threshold to show overlimit status in UID bar. _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); @@ -518,12 +562,30 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.SetCursorPosY(cursorY); ImGui.SetCursorPosX(uidStartX); + + bool headerItemClicked; using (_uiSharedService.UidFont.Push()) { - ImGui.TextColored(GetUidColor(), uidText); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(uidText); + if (useVanityColors) + { + var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor); + var cursorPos = ImGui.GetCursorScreenPos(); + var fontPtr = ImGui.GetFont(); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr); + } + else + { + ImGui.TextColored(GetUidColor(), uidText); + } } + + headerItemClicked = ImGui.IsItemClicked(); + + if (headerItemClicked) + { + ImGui.SetClipboardText(uidText); + } + UiSharedService.AttachToolTip("Click to copy"); if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected) @@ -583,7 +645,7 @@ public class CompactUi : WindowMediatorSubscriberBase if (_apiController.ServerState is ServerState.Connected) { - if (ImGui.IsItemClicked()) + if (headerItemClicked) { ImGui.SetClipboardText(_apiController.DisplayName); } @@ -592,9 +654,22 @@ public class CompactUi : WindowMediatorSubscriberBase { var origTextSize = ImGui.CalcTextSize(_apiController.UID); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2)); - ImGui.TextColored(GetUidColor(), _apiController.UID); + + if (useVanityColors) + { + var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor); + var cursorPos = ImGui.GetCursorScreenPos(); + var fontPtr = ImGui.GetFont(); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr); + } + else + { + ImGui.TextColored(GetUidColor(), _apiController.UID); + } + + bool uidFooterClicked = ImGui.IsItemClicked(); UiSharedService.AttachToolTip("Click to copy"); - if (ImGui.IsItemClicked()) + if (uidFooterClicked) { _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); } diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 52fcc13..8a9e6c9 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -5,10 +5,18 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; +using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using LightlessSync.WebAPI; +using Serilog; +using System; +using System.Collections.Generic; using System.Numerics; +using System.Reflection.Emit; +using System.Threading.Tasks; namespace LightlessSync.UI; @@ -19,18 +27,25 @@ public class TopTabMenu private readonly LightlessMediator _lightlessMediator; private readonly PairManager _pairManager; + private readonly PairRequestService _pairRequestService; + private readonly DalamudUtilService _dalamudUtilService; + private readonly HashSet _pendingPairRequestActions = new(StringComparer.Ordinal); + private bool _pairRequestsExpanded; // useless for now + private int _lastRequestCount; private readonly UiSharedService _uiSharedService; private string _filter = string.Empty; private int _globalControlCountdown = 0; - + private float _pairRequestsHeight = 150f; private string _pairToAdd = string.Empty; private SelectedTab _selectedTab = SelectedTab.None; - public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService) + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) { _lightlessMediator = lightlessMediator; _apiController = apiController; _pairManager = pairManager; + _pairRequestService = pairRequestService; + _dalamudUtilService = dalamudUtilService; _uiSharedService = uiSharedService; } @@ -182,6 +197,22 @@ public class TopTabMenu } if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); + + #if DEBUG + if (ImGui.Button("Add Test Pair Request")) + { + var fakeCid = Guid.NewGuid().ToString("N"); + var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request"); + _lightlessMediator.Publish(new NotificationMessage( + "Pair request received (debug)", + display.Message, + NotificationType.Info, + TimeSpan.FromSeconds(5))); + } + #endif + + DrawIncomingPairRequests(availableWidth); + ImGui.Separator(); DrawFilter(availableWidth, spacing.X); @@ -205,6 +236,207 @@ public class TopTabMenu UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); } + private void DrawIncomingPairRequests(float availableWidth) + { + var requests = _pairRequestService.GetActiveRequests(); + var count = requests.Count; + if (count == 0) + { + _pairRequestsExpanded = false; + _lastRequestCount = 0; + return; + } + + if (count > _lastRequestCount) + { + _pairRequestsExpanded = true; + } + _lastRequestCount = count; + + var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader"; + + using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple"))) + using (ImRaii.PushColor(ImGuiCol.HeaderHovered, UIColors.Get("LightlessPurpleActive"))) + using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple"))) + { + bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen); + _pairRequestsExpanded = open; + + if (ImGui.IsItemHovered()) + UiSharedService.AttachToolTip("Expand to view incoming pair requests."); + + if (open) + { + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + //var desiredHeight = Math.Clamp(count * lineHeight * 2f, 130f * ImGuiHelpers.GlobalScale, 185f * ImGuiHelpers.GlobalScale); we use resize bar instead + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 2f); + + using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple"))) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f)) + { + if (ImGui.BeginChild("##IncomingPairRequestsOuter", new Vector2(availableWidth + 5f, _pairRequestsHeight), true)) + { + var defaultChildBg = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg]; + using (ImRaii.PushColor(ImGuiCol.ChildBg, defaultChildBg)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f)) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(6, 6))) + { + if (ImGui.BeginChild("##IncomingPairRequestsInner", new Vector2(0, 0), true)) + { + using (ImRaii.PushColor(ImGuiCol.TableBorderStrong, ImGui.GetStyle().Colors[(int)ImGuiCol.Border])) + using (ImRaii.PushColor(ImGuiCol.TableBorderLight, ImGui.GetStyle().Colors[(int)ImGuiCol.Border])) + { + DrawPairRequestList(requests); + } + } + ImGui.EndChild(); + } + } + ImGui.EndChild(); + + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault")); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple")); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive")); + ImGui.Button("##resizeHandle", new Vector2(availableWidth, 4f)); + ImGui.PopStyleColor(3); + + if (ImGui.IsItemActive()) + { + _pairRequestsHeight += ImGui.GetIO().MouseDelta.Y; + _pairRequestsHeight = Math.Clamp(_pairRequestsHeight, 100f, 300f); + } + } + } + } + } + + private void DrawPairRequestList(IReadOnlyList requests) + { + float playerColWidth = 207f * ImGuiHelpers.GlobalScale; + float receivedColWidth = 73f * ImGuiHelpers.GlobalScale; + float actionsColWidth = 50f * ImGuiHelpers.GlobalScale; + + ImGui.Separator(); + ImGui.TextUnformatted("Player"); + ImGui.SameLine(playerColWidth + 2f); + ImGui.TextUnformatted("Received"); + ImGui.SameLine(playerColWidth + receivedColWidth + 12f); + ImGui.TextUnformatted("Actions"); + ImGui.Separator(); + + foreach (var request in requests) + { + ImGui.BeginGroup(); + + var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; + + ImGui.TextUnformatted(label.Truncate(26)); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted(label); + ImGui.EndTooltip(); + } + + ImGui.SameLine(playerColWidth); + + ImGui.TextUnformatted(GetRelativeTime(request.ReceivedAt)); + ImGui.SameLine(playerColWidth + receivedColWidth); + + DrawPairRequestActions(request); + + ImGui.EndGroup(); + } + } + + private void DrawPairRequestActions(PairRequestService.PairRequestDisplay request) + { + using var id = ImRaii.PushId(request.HashedCid); + var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; + var inFlight = _pendingPairRequestActions.Contains(request.HashedCid); + using (ImRaii.Disabled(inFlight)) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Check)) + { + _ = AcceptPairRequestAsync(request); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Accept request"); + + ImGui.SameLine(); + + if (_uiSharedService.IconButton(FontAwesomeIcon.Times)) + { + RejectPairRequest(request.HashedCid, label); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Decline request"); + } + } + + private static string GetRelativeTime(DateTime receivedAt) + { + var delta = DateTime.UtcNow - receivedAt; + if (delta <= TimeSpan.FromSeconds(10)) + { + return "Just now"; + } + + if (delta.TotalMinutes >= 1) + { + return $"{Math.Floor(delta.TotalMinutes)}m {delta.Seconds:D2}s ago"; + } + + return $"{delta.Seconds}s ago"; + } + + private async Task AcceptPairRequestAsync(PairRequestService.PairRequestDisplay request) + { + if (!_pendingPairRequestActions.Add(request.HashedCid)) + { + return; + } + + try + { + var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); + _pairRequestService.RemoveRequest(request.HashedCid); + + var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; + _lightlessMediator.Publish(new NotificationMessage( + "Pair request accepted", + $"Sent a pair request back to {display}.", + NotificationType.Info, + TimeSpan.FromSeconds(3))); + } + catch (Exception ex) + { + _lightlessMediator.Publish(new NotificationMessage( + "Failed to accept pair request", + ex.Message, + NotificationType.Error, + TimeSpan.FromSeconds(5))); + } + finally + { + _pendingPairRequestActions.Remove(request.HashedCid); + } + } + + private void RejectPairRequest(string hashedCid, string playerName) + { + if (!_pairRequestService.RemoveRequest(hashedCid)) + { + return; + } + + _lightlessMediator.Publish(new NotificationMessage("Pair request declined", "Declined " + playerName + "'s pending pair request.", + NotificationType.Info, + TimeSpan.FromSeconds(3))); + } + private void DrawFilter(float availableWidth, float spacingX) { var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Ban, "Clear"); diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 457361a..263c87a 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -105,6 +105,21 @@ public partial class ApiController return Task.CompletedTask; } + public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto) + { + if (dto == null) + return Task.CompletedTask; + + var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); + + Mediator.Publish(new NotificationMessage( + "Pair request received", + request.Message, + NotificationType.Info, + TimeSpan.FromSeconds(5))); + + return Task.CompletedTask; + } public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) { SystemInfoDto = systemInfo; @@ -277,6 +292,7 @@ public partial class ApiController _lightlessHub!.On(nameof(Client_GroupSendInfo), act); } + public void OnGroupUpdateProfile(Action act) { if (_initialized) return; @@ -289,6 +305,12 @@ public partial class ApiController _lightlessHub!.On(nameof(Client_ReceiveServerMessage), act); } + public void OnReceiveBroadcastPairRequest(Action act) + { + if (_initialized) return; + _lightlessHub!.On(nameof(Client_ReceiveBroadcastPairRequest), act); + } + public void OnUpdateSystemInfo(Action act) { if (_initialized) return; diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index efa6e6f..90be67f 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -28,6 +28,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private readonly DalamudUtilService _dalamudUtil; private readonly HubFactory _hubFactory; private readonly PairManager _pairManager; + private readonly PairRequestService _pairRequestService; private readonly ServerConfigurationManager _serverManager; private readonly TokenProvider _tokenProvider; private readonly LightlessConfigService _lightlessConfigService; @@ -42,12 +43,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private CensusUpdateMessage? _lastCensus; public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, - PairManager pairManager, ServerConfigurationManager serverManager, LightlessMediator mediator, + PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator, TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator) { _hubFactory = hubFactory; _dalamudUtil = dalamudUtil; _pairManager = pairManager; + _pairRequestService = pairRequestService; _serverManager = serverManager; _tokenProvider = tokenProvider; _lightlessConfigService = lightlessConfigService; @@ -428,6 +430,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogDebug("Initializing data"); OnDownloadReady((guid) => _ = Client_DownloadReady(guid)); OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg)); + OnReceiveBroadcastPairRequest(dto => _ = Client_ReceiveBroadcastPairRequest(dto)); OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto)); OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto));