From fca730557e8e534c137674927940dbd9dd867c78 Mon Sep 17 00:00:00 2001 From: Keda Date: Mon, 29 Sep 2025 00:19:45 +0200 Subject: [PATCH] Update 0.1.9 - Correctif UI + Default Synchronisation settings + Detect TypeChat --- MareAPI | 2 +- .../Configurations/MareConfig.cs | 10 +- .../Models/SyncOverrideEntry.cs | 13 + MareSynchronos/MarePlugin.cs | 3 +- MareSynchronos/MareSynchronos.csproj | 2 +- .../PlayerData/Pairs/PairManager.cs | 12 +- MareSynchronos/Plugin.cs | 12 + MareSynchronos/Services/ChatService.cs | 83 ++++- MareSynchronos/Services/GuiHookService.cs | 30 ++ MareSynchronos/Services/Mediator/Messages.cs | 8 + .../Services/NotificationService.cs | 10 + .../Services/PartyListTypingService.cs | 112 ++++++ .../Services/SyncDefaultsService.cs | 346 ++++++++++++++++++ MareSynchronos/UI/ChangelogUi.cs | 7 + MareSynchronos/UI/CompactUI.cs | 181 ++++++--- MareSynchronos/UI/Components/DrawUserPair.cs | 3 + MareSynchronos/UI/Components/GroupPanel.cs | 3 + MareSynchronos/UI/PermissionWindowUI.cs | 15 +- MareSynchronos/UI/UISharedService.cs | 1 + .../SignalR/ApIController.Functions.Users.cs | 6 + .../ApiController.Functions.Callbacks.cs | 13 + .../WebAPI/SignalR/ApiController.cs | 5 +- 22 files changed, 803 insertions(+), 74 deletions(-) create mode 100644 MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs create mode 100644 MareSynchronos/Services/PartyListTypingService.cs create mode 100644 MareSynchronos/Services/SyncDefaultsService.cs diff --git a/MareAPI b/MareAPI index fa9b7bc..3b17590 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit fa9b7bce43b8baf9ba17d9e1df221fafa20fd6d7 +Subproject commit 3b175900c10a9a152a168b1bd6fee390aa58e8e0 diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index 8e2c0df..0003b83 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -1,4 +1,5 @@ -using MareSynchronos.MareConfiguration.Models; +using System.Collections.Generic; +using MareSynchronos.MareConfiguration.Models; using MareSynchronos.UI; using Microsoft.Extensions.Logging; @@ -60,6 +61,11 @@ public class MareConfig : IMareConfiguration public bool ShowUploadingBigText { get; set; } = true; public bool ShowVisibleUsersSeparately { get; set; } = true; public string LastChangelogVersionSeen { get; set; } = string.Empty; + public bool DefaultDisableSounds { get; set; } = false; + public bool DefaultDisableAnimations { get; set; } = false; + public bool DefaultDisableVfx { get; set; } = false; + public Dictionary PairSyncOverrides { get; set; } = new(StringComparer.Ordinal); + public Dictionary GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal); public bool EnableAutoDetectDiscovery { get; set; } = false; public bool AllowAutoDetectPairRequests { get; set; } = false; public int AutoDetectMaxDistanceMeters { get; set; } = 40; @@ -78,6 +84,8 @@ public class MareConfig : IMareConfiguration public int ChatLogKind { get; set; } = 1; // XivChatType.Debug public bool ExtraChatAPI { get; set; } = false; public bool ExtraChatTags { get; set; } = false; + public bool TypingIndicatorShowOnNameplates { get; set; } = true; + public bool TypingIndicatorShowOnPartyList { get; set; } = true; public bool MareAPI { get; set; } = true; } diff --git a/MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs b/MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs new file mode 100644 index 0000000..308b079 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs @@ -0,0 +1,13 @@ +using System; + +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class SyncOverrideEntry +{ + public bool? DisableSounds { get; set; } + public bool? DisableAnimations { get; set; } + public bool? DisableVfx { get; set; } + + public bool IsEmpty => DisableSounds is null && DisableAnimations is null && DisableVfx is null; +} diff --git a/MareSynchronos/MarePlugin.cs b/MareSynchronos/MarePlugin.cs index 458ac42..d7bf86d 100644 --- a/MareSynchronos/MarePlugin.cs +++ b/MareSynchronos/MarePlugin.cs @@ -150,6 +150,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); @@ -167,4 +168,4 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService Logger?.LogCritical(ex, "Error during launch of managers"); } } -} \ No newline at end of file +} diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 2eca83f..52c6119 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ UmbraSync UmbraSync - 0.1.8.2 + 0.1.9.0 diff --git a/MareSynchronos/PlayerData/Pairs/PairManager.cs b/MareSynchronos/PlayerData/Pairs/PairManager.cs index 5268c35..67a293c 100644 --- a/MareSynchronos/PlayerData/Pairs/PairManager.cs +++ b/MareSynchronos/PlayerData/Pairs/PairManager.cs @@ -51,7 +51,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase RecreateLazy(); } - public void AddGroupPair(GroupPairFullInfoDto dto) + public void AddGroupPair(GroupPairFullInfoDto dto, bool isInitialLoad = false) { if (!_allClientPairs.ContainsKey(dto.User)) _allClientPairs[dto.User] = _pairFactory.Create(dto.User); @@ -59,6 +59,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase var group = _allGroups[dto.Group]; _allClientPairs[dto.User].GroupPair[group] = dto; RecreateLazy(); + + if (!isInitialLoad) + { + Mediator.Publish(new ApplyDefaultGroupPermissionsMessage(dto)); + } } public Pair? GetPairByUID(string uid) @@ -88,6 +93,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase LastAddedUser = _allClientPairs[dto.User]; _allClientPairs[dto.User].ApplyLastReceivedData(); RecreateLazy(); + + if (addToLastAddedUser) + { + Mediator.Publish(new ApplyDefaultPairPermissionsMessage(dto)); + } } public void ClearPairs() diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 63f287d..dfb7e95 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -115,6 +115,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); @@ -146,6 +147,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); @@ -218,6 +220,16 @@ public sealed class Plugin : IDalamudPlugin }) .Build(); + try + { + var partyListTypingService = _host.Services.GetRequiredService(); + pluginInterface.UiBuilder.Draw += partyListTypingService.Draw; + } + catch (Exception e) + { + pluginLog.Warning(e, "Failed to initialize PartyListTypingService draw hook"); + } + _ = Task.Run(async () => { try { diff --git a/MareSynchronos/Services/ChatService.cs b/MareSynchronos/Services/ChatService.cs index 1cf25f1..06a98cb 100644 --- a/MareSynchronos/Services/ChatService.cs +++ b/MareSynchronos/Services/ChatService.cs @@ -1,3 +1,4 @@ +using System.Threading; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; @@ -30,6 +31,11 @@ public class ChatService : DisposableMediatorSubscriberBase private readonly Lazy _gameChatHooks; + private readonly object _typingLock = new(); + private CancellationTokenSource? _typingCts; + private bool _isTypingAnnounced; + private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2); + public ChatService(ILogger logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController, PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator) @@ -46,13 +52,12 @@ public class ChatService : DisposableMediatorSubscriberBase Mediator.Subscribe(this, HandleGroupChat); _gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger(), gameInteropProvider, SendChatShell)); - - // Initialize chat hooks in advance _ = Task.Run(() => { try { _ = _gameChatHooks.Value; + _isTypingAnnounced = false; } catch (Exception ex) { @@ -64,9 +69,74 @@ public class ChatService : DisposableMediatorSubscriberBase protected override void Dispose(bool disposing) { base.Dispose(disposing); + _typingCts?.Cancel(); + _typingCts?.Dispose(); if (_gameChatHooks.IsValueCreated) _gameChatHooks.Value!.Dispose(); } + public void NotifyTypingKeystroke() + { + lock (_typingLock) + { + if (!_isTypingAnnounced) + { + _ = Task.Run(async () => + { + try { await _apiController.UserSetTypingState(true).ConfigureAwait(false); } + catch (Exception ex) { _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=true"); } + }); + _isTypingAnnounced = true; + } + + _typingCts?.Cancel(); + _typingCts?.Dispose(); + _typingCts = new CancellationTokenSource(); + var token = _typingCts.Token; + + _ = Task.Run(async () => + { + try + { + await Task.Delay(TypingIdle, token).ConfigureAwait(false); + await _apiController.UserSetTypingState(false).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + // reset timer + } + catch (Exception ex) + { + _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=false"); + } + finally + { + lock (_typingLock) + { + if (!token.IsCancellationRequested) + _isTypingAnnounced = false; + } + } + }); + } + } + public void ClearTypingState() + { + lock (_typingLock) + { + _typingCts?.Cancel(); + _typingCts?.Dispose(); + _typingCts = null; + if (_isTypingAnnounced) + { + _ = Task.Run(async () => + { + try { await _apiController.UserSetTypingState(false).ConfigureAwait(false); } + catch (Exception ex) { _logger.LogDebug(ex, "ClearTypingState: failed to send typing=false"); } + }); + _isTypingAnnounced = false; + } + } + } private void HandleUserChat(UserChatMsgMessage message) { @@ -124,7 +194,6 @@ public class ChatService : DisposableMediatorSubscriberBase msg.AddText($"[SS{shellNumber}]<"); if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal)) { - // Don't link to your own character msg.AddText(chatMsg.SenderName); } else @@ -142,8 +211,6 @@ public class ChatService : DisposableMediatorSubscriberBase Type = logKind }); } - - // Print an example message to the configured global chat channel public void PrintChannelExample(string message, string gid = "") { int chatType = _mareConfig.Current.ChatLogKind; @@ -164,8 +231,6 @@ public class ChatService : DisposableMediatorSubscriberBase Type = (XivChatType)chatType }); } - - // Called to update the active chat shell name if its renamed public void MaybeUpdateShellName(int shellNumber) { if (_mareConfig.Current.DisableSyncshellChat) @@ -178,7 +243,6 @@ public class ChatService : DisposableMediatorSubscriberBase { if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null) { - // Very dumb and won't handle re-numbering -- need to identify the active chat channel more reliably later if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal)) SwitchChatShell(shellNumber); } @@ -197,7 +261,6 @@ public class ChatService : DisposableMediatorSubscriberBase if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) { var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID; - // BUG: This doesn't always update the chat window e.g. when renaming a group _gameChatHooks.Value.ChatChannelOverride = new() { ChannelName = $"SS [{shellNumber}]: {name}", @@ -221,7 +284,6 @@ public class ChatService : DisposableMediatorSubscriberBase if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) { _ = Task.Run(async () => { - // Should cache the name and home world instead of fetching it every time var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => { return new ChatMessage() { @@ -230,6 +292,7 @@ public class ChatService : DisposableMediatorSubscriberBase PayloadContent = chatBytes }; }).ConfigureAwait(false); + ClearTypingState(); await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false); }).ConfigureAwait(false); return; diff --git a/MareSynchronos/Services/GuiHookService.cs b/MareSynchronos/Services/GuiHookService.cs index 32ea6ad..d4d9467 100644 --- a/MareSynchronos/Services/GuiHookService.cs +++ b/MareSynchronos/Services/GuiHookService.cs @@ -7,6 +7,9 @@ using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services.Mediator; using MareSynchronos.UI; using Microsoft.Extensions.Logging; +using MareSynchronos.API.Dto.User; +using System.Collections.Concurrent; +using Dalamud.Game.Text; namespace MareSynchronos.Services; @@ -20,6 +23,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase private readonly IPartyList _partyList; private readonly PairManager _pairManager; + private readonly ConcurrentDictionary _typingUsers = new(); + private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2); + private bool _isModified = false; private bool _namePlateRoleColorsEnabled = false; @@ -41,6 +47,18 @@ public class GuiHookService : DisposableMediatorSubscriberBase Mediator.Subscribe(this, (_) => GameSettingsCheck()); Mediator.Subscribe(this, (_) => RequestRedraw()); Mediator.Subscribe(this, (_) => RequestRedraw()); + Mediator.Subscribe(this, (msg) => + { + if (msg.Typing.IsTyping) + { + _typingUsers[msg.Typing.User.UID] = DateTime.UtcNow; + } + else + { + _typingUsers.TryRemove(msg.Typing.User.UID, out _); + } + RequestRedraw(); + }); } public void RequestRedraw(bool force = false) @@ -72,6 +90,7 @@ public class GuiHookService : DisposableMediatorSubscriberBase if (!_configService.Current.UseNameColors) return; + var showTypingIndicator = _configService.Current.TypingIndicatorShowOnNameplates; var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue); var visibleUsersIds = visibleUsers.Select(u => (ulong)u.PlayerCharacterId).ToHashSet(); @@ -95,6 +114,17 @@ public class GuiHookService : DisposableMediatorSubscriberBase BuildColorEndSeString(colors) ); _isModified = true; + if (showTypingIndicator + && _typingUsers.TryGetValue(pair.UserData.UID, out var lastTyping) + && (DateTime.UtcNow - lastTyping) < TypingDisplayTime) + { + var ssb = new SeStringBuilder(); + ssb.Append(handler.Name); + ssb.Add(new IconPayload(BitmapFontIcon.AutoTranslateBegin)); + ssb.AddText("..."); + ssb.Add(new IconPayload(BitmapFontIcon.AutoTranslateEnd)); + handler.Name = ssb.Build(); + } } } } diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index 1bbfd71..28c6b50 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -3,6 +3,7 @@ using MareSynchronos.API.Data; using MareSynchronos.API.Dto; using MareSynchronos.API.Dto.CharaData; using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Pairs; @@ -52,6 +53,7 @@ public record HaltScanMessage(string Source) : MessageBase; public record ResumeScanMessage(string Source) : MessageBase; public record NotificationMessage (string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase; +public record DualNotificationMessage(string Title, string Message, NotificationType Type, TimeSpan? ToastDuration = null) : MessageBase; public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; @@ -90,6 +92,7 @@ public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBas public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage; public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase; public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase; +public record UserTypingStateMessage(TypingStateDto Typing) : MessageBase; public record RecalculatePerformanceMessage(string? UID) : MessageBase; public record NameplateRedrawMessage : MessageBase; public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID); @@ -112,6 +115,11 @@ public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMa public record DiscoveryListUpdated(List Entries) : MessageBase; public record NearbyDetectionToggled(bool Enabled) : MessageBase; public record AllowPairRequestsToggled(bool Enabled) : MessageBase; +public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase; +public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase; +public record ApplyDefaultsToAllSyncsMessage : MessageBase; +public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase; +public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase; public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName); #pragma warning restore S2094 diff --git a/MareSynchronos/Services/NotificationService.cs b/MareSynchronos/Services/NotificationService.cs index 51a9371..0f0e545 100644 --- a/MareSynchronos/Services/NotificationService.cs +++ b/MareSynchronos/Services/NotificationService.cs @@ -32,6 +32,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ public Task StartAsync(CancellationToken cancellationToken) { Mediator.Subscribe(this, ShowNotification); + Mediator.Subscribe(this, ShowDualNotification); return Task.CompletedTask; } @@ -103,6 +104,15 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ } } + private void ShowDualNotification(DualNotificationMessage message) + { + if (!_dalamudUtilService.IsLoggedIn) return; + + var baseMsg = new NotificationMessage(message.Title, message.Message, message.Type, message.ToastDuration); + ShowToast(baseMsg); + ShowChat(baseMsg); + } + private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction) { appendInstruction = false; diff --git a/MareSynchronos/Services/PartyListTypingService.cs b/MareSynchronos/Services/PartyListTypingService.cs new file mode 100644 index 0000000..1eaaaf4 --- /dev/null +++ b/MareSynchronos/Services/PartyListTypingService.cs @@ -0,0 +1,112 @@ +using MareSynchronos.MareConfiguration; +using System.Collections.Generic; +using MareSynchronos.PlayerData.Pairs; +using System; +using System.Collections.Concurrent; +using System.Linq; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Plugin.Services; +using MareSynchronos.API.Dto.User; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public class PartyListTypingService : DisposableMediatorSubscriberBase +{ + private readonly ILogger _logger; + private readonly IPartyList _partyList; + private readonly MareConfigService _configService; + private readonly PairManager _pairManager; + private readonly ConcurrentDictionary _typingUsers = new(); + private readonly ConcurrentDictionary _typingNames = new(StringComparer.OrdinalIgnoreCase); + private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2); + + public PartyListTypingService(ILogger logger, + MareMediator mediator, + IPartyList partyList, + PairManager pairManager, + MareConfigService configService) + : base(logger, mediator) + { + _logger = logger; + _partyList = partyList; + _pairManager = pairManager; + _configService = configService; + + Mediator.Subscribe(this, OnUserTyping); + } + + private void OnUserTyping(UserTypingStateMessage msg) + { + var now = DateTime.UtcNow; + var uid = msg.Typing.User.UID; + var aliasOrUid = msg.Typing.User.AliasOrUID ?? uid; + + if (msg.Typing.IsTyping) + { + _typingUsers[uid] = now; + _typingNames[aliasOrUid] = now; + } + else + { + _typingUsers.TryRemove(uid, out _); + _typingNames.TryRemove(aliasOrUid, out _); + } + } + + private static bool HasTypingBubble(SeString name) + { + return name.Payloads.Any(p => p is IconPayload ip && ip.Icon == BitmapFontIcon.AutoTranslateBegin); + } + + private static SeString WithTypingBubble(SeString baseName) + { + var ssb = new SeStringBuilder(); + ssb.Append(baseName); + ssb.Add(new IconPayload(BitmapFontIcon.AutoTranslateBegin)); + ssb.AddText("..."); + ssb.Add(new IconPayload(BitmapFontIcon.AutoTranslateEnd)); + return ssb.Build(); + } + + public void Draw() + { + if (!_configService.Current.TypingIndicatorShowOnPartyList) return; + // Build map of visible users by AliasOrUID -> UID (case-insensitive) + var visibleByAlias = new Dictionary(StringComparer.OrdinalIgnoreCase); + try + { + var visibleUsers = _pairManager.GetVisibleUsers(); + foreach (var u in visibleUsers) + { + var alias = string.IsNullOrEmpty(u.AliasOrUID) ? u.UID : u.AliasOrUID; + if (!visibleByAlias.ContainsKey(alias)) visibleByAlias[alias] = u.UID; + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "PartyListTypingService: failed to get visible users"); + } + + foreach (var member in _partyList) + { + if (string.IsNullOrEmpty(member.Name?.TextValue)) continue; + + var now = DateTime.UtcNow; + var displayName = member.Name.TextValue; + if (visibleByAlias.TryGetValue(displayName, out var uid) + && _typingUsers.TryGetValue(uid, out var last) + && (now - last) < TypingDisplayTime) + { + if (!HasTypingBubble(member.Name)) + { + // IPartyMember.Name is read-only; rendering bubble here requires Addon-level modification. Keeping compile-safe for now. + _logger.LogDebug("PartyListTypingService: bubble would be shown for {name}", displayName); + } + } + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/SyncDefaultsService.cs b/MareSynchronos/Services/SyncDefaultsService.cs new file mode 100644 index 0000000..c1a8e39 --- /dev/null +++ b/MareSynchronos/Services/SyncDefaultsService.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Configurations; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType; + +namespace MareSynchronos.Services; + +public sealed class SyncDefaultsService : DisposableMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly MareConfigService _configService; + private readonly PairManager _pairManager; + + public SyncDefaultsService(ILogger logger, MareMediator mediator, + MareConfigService configService, ApiController apiController, PairManager pairManager) : base(logger, mediator) + { + _configService = configService; + _apiController = apiController; + _pairManager = pairManager; + + Mediator.Subscribe(this, OnApplyPairDefaults); + Mediator.Subscribe(this, OnApplyGroupDefaults); + Mediator.Subscribe(this, _ => ApplyDefaultsToAll()); + Mediator.Subscribe(this, OnPairOverrideChanged); + Mediator.Subscribe(this, OnGroupOverrideChanged); + } + + private void OnApplyPairDefaults(ApplyDefaultPairPermissionsMessage message) + { + var config = _configService.Current; + var permissions = message.Pair.OwnPermissions; + var overrides = TryGetPairOverride(message.Pair.User.UID); + if (!ApplyDefaults(ref permissions, config, overrides)) + return; + + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(message.Pair.User, permissions)); + } + + private void OnApplyGroupDefaults(ApplyDefaultGroupPermissionsMessage message) + { + if (!string.Equals(message.GroupPair.User.UID, _apiController.UID, StringComparison.Ordinal)) + return; + + var config = _configService.Current; + var permissions = message.GroupPair.GroupUserPermissions; + var overrides = TryGetGroupOverride(message.GroupPair.Group.GID); + if (!ApplyDefaults(ref permissions, config, overrides)) + return; + + _ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(message.GroupPair.Group, message.GroupPair.User, permissions)); + } + + private async Task ApplyDefaultsToAllAsync() + { + try + { + var config = _configService.Current; + var tasks = new List(); + int updatedPairs = 0; + int updatedGroups = 0; + + foreach (var pair in _pairManager.DirectPairs.Where(p => p.UserPair != null).ToList()) + { + var permissions = pair.UserPair!.OwnPermissions; + var overrides = TryGetPairOverride(pair.UserData.UID); + if (!ApplyDefaults(ref permissions, config, overrides)) + continue; + + updatedPairs++; + tasks.Add(_apiController.UserSetPairPermissions(new UserPermissionsDto(pair.UserData, permissions))); + } + + var selfUser = new UserData(_apiController.UID); + foreach (var groupInfo in _pairManager.Groups.Values.ToList()) + { + var permissions = groupInfo.GroupUserPermissions; + var overrides = TryGetGroupOverride(groupInfo.Group.GID); + if (!ApplyDefaults(ref permissions, config, overrides)) + continue; + + updatedGroups++; + tasks.Add(_apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupInfo.Group, selfUser, permissions))); + } + + if (tasks.Count > 0) + { + try + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed applying default sync settings to all pairs/groups"); + } + } + + Mediator.Publish(new DualNotificationMessage("Préférences appliquées", BuildSummaryMessage(updatedPairs, updatedGroups), NotificationType.Info)); + } + catch (Exception ex) + { + Logger.LogError(ex, "Unexpected error while applying default sync settings to all pairs/groups"); + Mediator.Publish(new DualNotificationMessage("Préférences appliquées", "Une erreur est survenue lors de l'application des paramètres par défaut.", NotificationType.Error)); + } + } + + private void ApplyDefaultsToAll() => _ = ApplyDefaultsToAllAsync(); + + private static string BuildSummaryMessage(int pairs, int groups) + { + if (pairs == 0 && groups == 0) + return "Aucun pair ou syncshell n'avait besoin d'être modifié."; + + if (pairs > 0 && groups > 0) + return $"Mise à jour de {pairs} pair(s) et {groups} syncshell(s)."; + + if (pairs > 0) + return $"Mise à jour de {pairs} pair(s)."; + + return $"Mise à jour de {groups} syncshell(s)."; + } + + private void OnPairOverrideChanged(PairSyncOverrideChanged message) + { + var overrides = _configService.Current.PairSyncOverrides ??= new(StringComparer.Ordinal); + var entry = overrides.TryGetValue(message.Uid, out var existing) ? existing : new SyncOverrideEntry(); + bool changed = false; + + if (message.DisableSounds.HasValue) + { + var val = message.DisableSounds.Value; + var defaultVal = _configService.Current.DefaultDisableSounds; + var newValue = val == defaultVal ? (bool?)null : val; + if (entry.DisableSounds != newValue) + { + entry.DisableSounds = newValue; + changed = true; + } + } + + if (message.DisableAnimations.HasValue) + { + var val = message.DisableAnimations.Value; + var defaultVal = _configService.Current.DefaultDisableAnimations; + var newValue = val == defaultVal ? (bool?)null : val; + if (entry.DisableAnimations != newValue) + { + entry.DisableAnimations = newValue; + changed = true; + } + } + + if (message.DisableVfx.HasValue) + { + var val = message.DisableVfx.Value; + var defaultVal = _configService.Current.DefaultDisableVfx; + var newValue = val == defaultVal ? (bool?)null : val; + if (entry.DisableVfx != newValue) + { + entry.DisableVfx = newValue; + changed = true; + } + } + + if (!changed) return; + + if (entry.IsEmpty) + overrides.Remove(message.Uid); + else + overrides[message.Uid] = entry; + + _configService.Save(); + } + + private void OnGroupOverrideChanged(GroupSyncOverrideChanged message) + { + var overrides = _configService.Current.GroupSyncOverrides ??= new(StringComparer.Ordinal); + var entry = overrides.TryGetValue(message.Gid, out var existing) ? existing : new SyncOverrideEntry(); + bool changed = false; + + if (message.DisableSounds.HasValue) + { + var val = message.DisableSounds.Value; + var defaultVal = _configService.Current.DefaultDisableSounds; + var newValue = val == defaultVal ? (bool?)null : val; + if (entry.DisableSounds != newValue) + { + entry.DisableSounds = newValue; + changed = true; + } + } + + if (message.DisableAnimations.HasValue) + { + var val = message.DisableAnimations.Value; + var defaultVal = _configService.Current.DefaultDisableAnimations; + var newValue = val == defaultVal ? (bool?)null : val; + if (entry.DisableAnimations != newValue) + { + entry.DisableAnimations = newValue; + changed = true; + } + } + + if (message.DisableVfx.HasValue) + { + var val = message.DisableVfx.Value; + var defaultVal = _configService.Current.DefaultDisableVfx; + var newValue = val == defaultVal ? (bool?)null : val; + if (entry.DisableVfx != newValue) + { + entry.DisableVfx = newValue; + changed = true; + } + } + + if (!changed) return; + + if (entry.IsEmpty) + overrides.Remove(message.Gid); + else + overrides[message.Gid] = entry; + + _configService.Save(); + } + + private SyncOverrideEntry? TryGetPairOverride(string uid) + { + var overrides = _configService.Current.PairSyncOverrides; + return overrides != null && overrides.TryGetValue(uid, out var entry) ? entry : null; + } + + private SyncOverrideEntry? TryGetGroupOverride(string gid) + { + var overrides = _configService.Current.GroupSyncOverrides; + return overrides != null && overrides.TryGetValue(gid, out var entry) ? entry : null; + } + + private static bool ApplyDefaults(ref UserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides) + { + bool changed = false; + if (overrides?.DisableSounds is bool overrideSounds) + { + if (permissions.IsDisableSounds() != overrideSounds) + { + permissions.SetDisableSounds(overrideSounds); + changed = true; + } + } + else if (permissions.IsDisableSounds() != config.DefaultDisableSounds) + { + permissions.SetDisableSounds(config.DefaultDisableSounds); + changed = true; + } + + if (overrides?.DisableAnimations is bool overrideAnims) + { + if (permissions.IsDisableAnimations() != overrideAnims) + { + permissions.SetDisableAnimations(overrideAnims); + changed = true; + } + } + else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations) + { + permissions.SetDisableAnimations(config.DefaultDisableAnimations); + changed = true; + } + + if (overrides?.DisableVfx is bool overrideVfx) + { + if (permissions.IsDisableVFX() != overrideVfx) + { + permissions.SetDisableVFX(overrideVfx); + changed = true; + } + } + else if (permissions.IsDisableVFX() != config.DefaultDisableVfx) + { + permissions.SetDisableVFX(config.DefaultDisableVfx); + changed = true; + } + + return changed; + } + + private static bool ApplyDefaults(ref GroupUserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides) + { + bool changed = false; + if (overrides?.DisableSounds is bool overrideSounds) + { + if (permissions.IsDisableSounds() != overrideSounds) + { + permissions.SetDisableSounds(overrideSounds); + changed = true; + } + } + else if (permissions.IsDisableSounds() != config.DefaultDisableSounds) + { + permissions.SetDisableSounds(config.DefaultDisableSounds); + changed = true; + } + + if (overrides?.DisableAnimations is bool overrideAnims) + { + if (permissions.IsDisableAnimations() != overrideAnims) + { + permissions.SetDisableAnimations(overrideAnims); + changed = true; + } + } + else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations) + { + permissions.SetDisableAnimations(config.DefaultDisableAnimations); + changed = true; + } + + if (overrides?.DisableVfx is bool overrideVfx) + { + if (permissions.IsDisableVFX() != overrideVfx) + { + permissions.SetDisableVFX(overrideVfx); + changed = true; + } + } + else if (permissions.IsDisableVFX() != config.DefaultDisableVfx) + { + permissions.SetDisableVFX(config.DefaultDisableVfx); + changed = true; + } + + return changed; + } +} diff --git a/MareSynchronos/UI/ChangelogUi.cs b/MareSynchronos/UI/ChangelogUi.cs index 7da4ec9..1706970 100644 --- a/MareSynchronos/UI/ChangelogUi.cs +++ b/MareSynchronos/UI/ChangelogUi.cs @@ -169,6 +169,13 @@ public sealed class ChangelogUi : WindowMediatorSubscriberBase { return new List { + new(new Version(0, 1, 9, 0), "0.1.9.0", new List + { + new("Il est désormais possible de configurer par défaut nos choix de synchronisation (VFX, Music, Animation)."), + new("La catégorie 'En attente' ne s'affice uniquement que si une invitation est en attente"), + new("(EN PRÉ VERSION) Il est désormais possible de voir quand une personne appairé est en train d'écrire avec une bulle qui s'affiche."), + new("Correctif : Désormais, les invitation entrantes ne s'affichent qu'une seule fois au lieu de deux."), + }), 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."), diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index f8d67a9..d343a1c 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -7,6 +7,7 @@ using Dalamud.Utility; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto.User; using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; @@ -190,7 +191,7 @@ public class CompactUi : WindowMediatorSubscriberBase UiSharedService.AttachToolTip("Syncshells"); - ImGui.Separator(); + DrawDefaultSyncSettings(); if (!hasShownSyncShells) { using (ImRaii.PushId("pairlist")) DrawPairList(); @@ -203,7 +204,7 @@ public class CompactUi : WindowMediatorSubscriberBase using (ImRaii.PushId("transfers")) DrawTransfers(); TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight; using (ImRaii.PushId("group-user-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs); - using (ImRaii.PushId("grouping-popup")) _selectGroupForPairUi.Draw(); + using (ImRaii.PushId("grouping-popup")) _selectGroupForPairUi.Draw(); } if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null) @@ -253,6 +254,114 @@ public class CompactUi : WindowMediatorSubscriberBase base.OnClose(); } + private void DrawDefaultSyncSettings() + { + ImGuiHelpers.ScaledDummy(4f); + using (ImRaii.PushId("sync-defaults")) + { + const string soundLabel = "Audio"; + const string animLabel = "Anim"; + const string vfxLabel = "VFX"; + const string soundSubject = "de l'audio"; + const string animSubject = "des animations"; + const string vfxSubject = "des effets visuels"; + + bool soundsDisabled = _configService.Current.DefaultDisableSounds; + bool animsDisabled = _configService.Current.DefaultDisableAnimations; + bool vfxDisabled = _configService.Current.DefaultDisableVfx; + bool showNearby = _configService.Current.EnableAutoDetectDiscovery; + + var soundIcon = soundsDisabled ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute; + var animIcon = animsDisabled ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop; + var vfxIcon = vfxDisabled ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle; + + float spacing = ImGui.GetStyle().ItemSpacing.X; + float audioWidth = _uiSharedService.GetIconTextButtonSize(soundIcon, soundLabel); + float animWidth = _uiSharedService.GetIconTextButtonSize(animIcon, animLabel); + float vfxWidth = _uiSharedService.GetIconTextButtonSize(vfxIcon, vfxLabel); + float nearbyWidth = showNearby ? _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, "Nearby") : 0f; + int buttonCount = 3 + (showNearby ? 1 : 0); + float totalWidth = audioWidth + animWidth + vfxWidth + nearbyWidth + spacing * (buttonCount - 1); + float available = ImGui.GetContentRegionAvail().X; + float startCursorX = ImGui.GetCursorPosX(); + if (totalWidth < available) + { + ImGui.SetCursorPosX(startCursorX + (available - totalWidth) / 2f); + } + + DrawDefaultSyncButton(soundIcon, soundLabel, audioWidth, soundsDisabled, + state => + { + _configService.Current.DefaultDisableSounds = state; + _configService.Save(); + PublishSyncDefaultNotification(soundSubject, state); + Mediator.Publish(new ApplyDefaultsToAllSyncsMessage()); + }, + () => DisableStateTooltip(soundSubject, _configService.Current.DefaultDisableSounds)); + + DrawDefaultSyncButton(animIcon, animLabel, animWidth, animsDisabled, + state => + { + _configService.Current.DefaultDisableAnimations = state; + _configService.Save(); + PublishSyncDefaultNotification(animSubject, state); + Mediator.Publish(new ApplyDefaultsToAllSyncsMessage()); + }, + () => DisableStateTooltip(animSubject, _configService.Current.DefaultDisableAnimations), spacing); + + DrawDefaultSyncButton(vfxIcon, vfxLabel, vfxWidth, vfxDisabled, + state => + { + _configService.Current.DefaultDisableVfx = state; + _configService.Save(); + PublishSyncDefaultNotification(vfxSubject, state); + Mediator.Publish(new ApplyDefaultsToAllSyncsMessage()); + }, + () => DisableStateTooltip(vfxSubject, _configService.Current.DefaultDisableVfx), spacing); + + if (showNearby) + { + ImGui.SameLine(0, spacing); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Nearby", nearbyWidth)) + { + Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi))); + } + UiSharedService.AttachToolTip("Ouvrir la détection de proximité"); + } + } + ImGui.Separator(); + } + + private void DrawDefaultSyncButton(FontAwesomeIcon icon, string label, float width, bool currentState, + Action onToggle, Func tooltipProvider, float spacingOverride = -1f) + { + if (spacingOverride >= 0f) + { + ImGui.SameLine(0, spacingOverride); + } + + if (_uiSharedService.IconTextButton(icon, label, width)) + { + var newState = !currentState; + onToggle(newState); + } + + UiSharedService.AttachToolTip(tooltipProvider()); + } + + private static string DisableStateTooltip(string context, bool disabled) + { + var state = disabled ? "désactivée" : "activée"; + return $"Synchronisation {context} par défaut : {state}.\nCliquez pour modifier."; + } + + private void PublishSyncDefaultNotification(string context, bool disabled) + { + var state = disabled ? "désactivée" : "activée"; + var message = $"Synchronisation {context} par défaut {state}."; + Mediator.Publish(new DualNotificationMessage("Préférence de synchronisation", message, NotificationType.Info)); + } + private void DrawAddCharacter() { ImGui.Dummy(new(10)); @@ -388,49 +497,26 @@ public class CompactUi : WindowMediatorSubscriberBase try { - var inbox = _nearbyPending; - if (inbox != null && inbox.Pending.Count > 0) - { - ImGuiHelpers.ScaledDummy(6); - _uiSharedService.BigText("Incoming requests"); - foreach (var kv in inbox.Pending) - { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"{kv.Value} [{kv.Key}]"); - ImGui.SameLine(); - if (_uiSharedService.IconButton(FontAwesomeIcon.Check)) - { - _ = inbox.AcceptAsync(kv.Key); - } - UiSharedService.AttachToolTip("Accept and add as pair"); - ImGui.SameLine(); - if (_uiSharedService.IconButton(FontAwesomeIcon.Times)) - { - inbox.Remove(kv.Key); - } - UiSharedService.AttachToolTip("Dismiss request"); - } - ImGui.Separator(); - } + // intentionally left blank; pending requests handled in collapsible section below } catch { } - // Add the "En attente" category - using (ImRaii.PushId("group-Pending")) + var pendingCount = _nearbyPending?.Pending.Count ?? 0; + if (pendingCount > 0) { - var icon = _pendingOpen ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; - _uiSharedService.IconText(icon); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _pendingOpen = !_pendingOpen; - ImGui.SameLine(); - ImGui.TextUnformatted($"En attente ({_nearbyPending?.Pending.Count ?? 0})"); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _pendingOpen = !_pendingOpen; - - if (_pendingOpen) + using (ImRaii.PushId("group-Pending")) { - ImGui.Indent(); - if (_nearbyPending != null && _nearbyPending.Pending.Count > 0) + var icon = _pendingOpen ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; + _uiSharedService.IconText(icon); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _pendingOpen = !_pendingOpen; + ImGui.SameLine(); + ImGui.TextUnformatted($"En attente ({pendingCount})"); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _pendingOpen = !_pendingOpen; + + if (_pendingOpen) { - foreach (var kv in _nearbyPending.Pending) + ImGui.Indent(); + foreach (var kv in _nearbyPending!.Pending) { ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted($"{kv.Value} [{kv.Key}]"); @@ -447,13 +533,9 @@ public class CompactUi : WindowMediatorSubscriberBase } UiSharedService.AttachToolTip("Dismiss request"); } + ImGui.Unindent(); + ImGui.Separator(); } - else - { - UiSharedService.ColorTextWrapped("Aucune invitation en attente.", ImGuiColors.DalamudGrey3); - } - ImGui.Unindent(); - ImGui.Separator(); } } @@ -476,15 +558,6 @@ public class CompactUi : WindowMediatorSubscriberBase if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen; 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() diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs index 5ddb379..06e102c 100644 --- a/MareSynchronos/UI/Components/DrawUserPair.cs +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -272,6 +272,7 @@ public class DrawUserPair : DrawPairBase { var permissions = entry.UserPair.OwnPermissions; permissions.SetDisableSounds(!isDisableSounds); + _mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, permissions.IsDisableSounds(), null, null)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); } @@ -282,6 +283,7 @@ public class DrawUserPair : DrawPairBase { var permissions = entry.UserPair.OwnPermissions; permissions.SetDisableAnimations(!isDisableAnims); + _mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, permissions.IsDisableAnimations(), null)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); } @@ -292,6 +294,7 @@ public class DrawUserPair : DrawPairBase { var permissions = entry.UserPair.OwnPermissions; permissions.SetDisableVFX(!isDisableVFX); + _mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, null, permissions.IsDisableVFX())); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); } diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs index 134d05f..f63412f 100644 --- a/MareSynchronos/UI/Components/GroupPanel.cs +++ b/MareSynchronos/UI/Components/GroupPanel.cs @@ -769,6 +769,7 @@ internal sealed class GroupPanel ImGui.CloseCurrentPopup(); var perm = groupDto.GroupUserPermissions; perm.SetDisableSounds(!perm.IsDisableSounds()); + _mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, perm.IsDisableSounds(), null, null)); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); } UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell." @@ -782,6 +783,7 @@ internal sealed class GroupPanel ImGui.CloseCurrentPopup(); var perm = groupDto.GroupUserPermissions; perm.SetDisableAnimations(!perm.IsDisableAnimations()); + _mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, null, perm.IsDisableAnimations(), null)); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); } UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell." @@ -796,6 +798,7 @@ internal sealed class GroupPanel ImGui.CloseCurrentPopup(); var perm = groupDto.GroupUserPermissions; perm.SetDisableVFX(!perm.IsDisableVFX()); + _mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, null, null, perm.IsDisableVFX())); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); } UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell." diff --git a/MareSynchronos/UI/PermissionWindowUI.cs b/MareSynchronos/UI/PermissionWindowUI.cs index fe68c4d..de64754 100644 --- a/MareSynchronos/UI/PermissionWindowUI.cs +++ b/MareSynchronos/UI/PermissionWindowUI.cs @@ -128,6 +128,10 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase using (ImRaii.Disabled(!hasChanges)) if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save")) { + Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID, + _ownPermissions.IsDisableSounds(), + _ownPermissions.IsDisableAnimations(), + _ownPermissions.IsDisableVFX())); _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); } UiSharedService.AttachToolTip("Save and apply all changes"); @@ -148,10 +152,15 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase ImGui.SameLine(); if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default")) { + var defaults = _uiSharedService.ConfigService.Current; _ownPermissions.SetPaused(false); - _ownPermissions.SetDisableVFX(false); - _ownPermissions.SetDisableSounds(false); - _ownPermissions.SetDisableAnimations(false); + _ownPermissions.SetDisableSounds(defaults.DefaultDisableSounds); + _ownPermissions.SetDisableAnimations(defaults.DefaultDisableAnimations); + _ownPermissions.SetDisableVFX(defaults.DefaultDisableVfx); + Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID, + _ownPermissions.IsDisableSounds(), + _ownPermissions.IsDisableAnimations(), + _ownPermissions.IsDisableVFX())); _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); } UiSharedService.AttachToolTip("This will set all permissions to their default setting"); diff --git a/MareSynchronos/UI/UISharedService.cs b/MareSynchronos/UI/UISharedService.cs index f5a207a..7547fd7 100644 --- a/MareSynchronos/UI/UISharedService.cs +++ b/MareSynchronos/UI/UISharedService.cs @@ -136,6 +136,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public string PlayerName => _dalamudUtil.GetPlayerName(); public IFontHandle UidFont { get; init; } + public MareConfigService ConfigService => _configService; public Dictionary WorldData => _dalamudUtil.WorldData.Value; public uint WorldId => _dalamudUtil.GetHomeWorldId(); diff --git a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs index 08862d3..788bd59 100644 --- a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -97,6 +97,12 @@ public partial class ApiController await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false); } + public async Task UserSetTypingState(bool isTyping) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false); + } + private async Task PushCharacterDataInternal(CharacterData character, List visibleCharacters) { Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID))); diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 662c0e6..c59e55d 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -138,6 +138,13 @@ public partial class ApiController return Task.CompletedTask; } + public Task Client_UserTypingState(TypingStateDto dto) + { + Logger.LogTrace("Client_UserTypingState: {uid} typing={typing}", dto.User.UID, dto.IsTyping); + Mediator.Publish(new UserTypingStateMessage(dto)); + return Task.CompletedTask; + } + public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) { Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User); @@ -313,6 +320,12 @@ public partial class ApiController _mareHub!.On(nameof(Client_UserChatMsg), act); } + public void OnUserTypingState(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserTypingState), act); + } + public void OnUserReceiveCharacterData(Action act) { if (_initialized) return; diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.cs b/MareSynchronos/WebAPI/SignalR/ApiController.cs index 15f478d..57188e3 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.cs @@ -348,6 +348,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto)); OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto)); OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto)); + OnUserTypingState(dto => _ = Client_UserTypingState(dto)); OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto)); OnGroupDelete((dto) => _ = Client_GroupDelete(dto)); @@ -393,7 +394,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM foreach (var user in users) { Logger.LogDebug("Group Pair: {user}", user); - _pairManager.AddGroupPair(user); + _pairManager.AddGroupPair(user, isInitialLoad: true); } } } @@ -479,4 +480,4 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM ServerState = state; } } -#pragma warning restore MA0040 \ No newline at end of file +#pragma warning restore MA0040