Update 0.1.9 - Correctif UI + Default Synchronisation settings + Detect TypeChat

This commit is contained in:
2025-09-29 00:19:45 +02:00
parent 6572fdcc27
commit fca730557e
22 changed files with 803 additions and 74 deletions

Submodule MareAPI updated: fa9b7bce43...3b175900c1

View File

@@ -1,4 +1,5 @@
using MareSynchronos.MareConfiguration.Models; using System.Collections.Generic;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.UI; using MareSynchronos.UI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -60,6 +61,11 @@ public class MareConfig : IMareConfiguration
public bool ShowUploadingBigText { get; set; } = true; public bool ShowUploadingBigText { get; set; } = true;
public bool ShowVisibleUsersSeparately { get; set; } = true; public bool ShowVisibleUsersSeparately { get; set; } = true;
public string LastChangelogVersionSeen { get; set; } = string.Empty; 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<string, SyncOverrideEntry> PairSyncOverrides { get; set; } = new(StringComparer.Ordinal);
public Dictionary<string, SyncOverrideEntry> GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal);
public bool EnableAutoDetectDiscovery { get; set; } = false; public bool EnableAutoDetectDiscovery { get; set; } = false;
public bool AllowAutoDetectPairRequests { get; set; } = false; public bool AllowAutoDetectPairRequests { get; set; } = false;
public int AutoDetectMaxDistanceMeters { get; set; } = 40; public int AutoDetectMaxDistanceMeters { get; set; } = 40;
@@ -78,6 +84,8 @@ public class MareConfig : IMareConfiguration
public int ChatLogKind { get; set; } = 1; // XivChatType.Debug public int ChatLogKind { get; set; } = 1; // XivChatType.Debug
public bool ExtraChatAPI { get; set; } = false; public bool ExtraChatAPI { get; set; } = false;
public bool ExtraChatTags { 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; public bool MareAPI { get; set; } = true;
} }

View File

@@ -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;
}

View File

@@ -150,6 +150,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<SyncDefaultsService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>();
@@ -167,4 +168,4 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
Logger?.LogCritical(ex, "Error during launch of managers"); Logger?.LogCritical(ex, "Error during launch of managers");
} }
} }
} }

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<AssemblyName>UmbraSync</AssemblyName> <AssemblyName>UmbraSync</AssemblyName>
<RootNamespace>UmbraSync</RootNamespace> <RootNamespace>UmbraSync</RootNamespace>
<Version>0.1.8.2</Version> <Version>0.1.9.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -51,7 +51,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
RecreateLazy(); RecreateLazy();
} }
public void AddGroupPair(GroupPairFullInfoDto dto) public void AddGroupPair(GroupPairFullInfoDto dto, bool isInitialLoad = false)
{ {
if (!_allClientPairs.ContainsKey(dto.User)) if (!_allClientPairs.ContainsKey(dto.User))
_allClientPairs[dto.User] = _pairFactory.Create(dto.User); _allClientPairs[dto.User] = _pairFactory.Create(dto.User);
@@ -59,6 +59,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
var group = _allGroups[dto.Group]; var group = _allGroups[dto.Group];
_allClientPairs[dto.User].GroupPair[group] = dto; _allClientPairs[dto.User].GroupPair[group] = dto;
RecreateLazy(); RecreateLazy();
if (!isInitialLoad)
{
Mediator.Publish(new ApplyDefaultGroupPermissionsMessage(dto));
}
} }
public Pair? GetPairByUID(string uid) public Pair? GetPairByUID(string uid)
@@ -88,6 +93,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
LastAddedUser = _allClientPairs[dto.User]; LastAddedUser = _allClientPairs[dto.User];
_allClientPairs[dto.User].ApplyLastReceivedData(); _allClientPairs[dto.User].ApplyLastReceivedData();
RecreateLazy(); RecreateLazy();
if (addToLastAddedUser)
{
Mediator.Publish(new ApplyDefaultPairPermissionsMessage(dto));
}
} }
public void ClearPairs() public void ClearPairs()

View File

@@ -115,6 +115,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PluginWarningNotificationService>(); collection.AddSingleton<PluginWarningNotificationService>();
collection.AddSingleton<FileCompactor>(); collection.AddSingleton<FileCompactor>();
collection.AddSingleton<TagHandler>(); collection.AddSingleton<TagHandler>();
collection.AddSingleton<SyncDefaultsService>();
collection.AddSingleton<UidDisplayHandler>(); collection.AddSingleton<UidDisplayHandler>();
collection.AddSingleton<PluginWatcherService>(); collection.AddSingleton<PluginWatcherService>();
collection.AddSingleton<PlayerPerformanceService>(); collection.AddSingleton<PlayerPerformanceService>();
@@ -146,6 +147,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IpcManager>(); collection.AddSingleton<IpcManager>();
collection.AddSingleton<NotificationService>(); collection.AddSingleton<NotificationService>();
collection.AddSingleton<TemporarySyncshellNotificationService>(); collection.AddSingleton<TemporarySyncshellNotificationService>();
collection.AddSingleton<PartyListTypingService>();
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -218,6 +220,16 @@ public sealed class Plugin : IDalamudPlugin
}) })
.Build(); .Build();
try
{
var partyListTypingService = _host.Services.GetRequiredService<PartyListTypingService>();
pluginInterface.UiBuilder.Draw += partyListTypingService.Draw;
}
catch (Exception e)
{
pluginLog.Warning(e, "Failed to initialize PartyListTypingService draw hook");
}
_ = Task.Run(async () => { _ = Task.Run(async () => {
try try
{ {

View File

@@ -1,3 +1,4 @@
using System.Threading;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
@@ -30,6 +31,11 @@ public class ChatService : DisposableMediatorSubscriberBase
private readonly Lazy<GameChatHooks> _gameChatHooks; private readonly Lazy<GameChatHooks> _gameChatHooks;
private readonly object _typingLock = new();
private CancellationTokenSource? _typingCts;
private bool _isTypingAnnounced;
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController, public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController,
PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui, PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui,
MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator) MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
@@ -46,13 +52,12 @@ public class ChatService : DisposableMediatorSubscriberBase
Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat); Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat);
_gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell)); _gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell));
// Initialize chat hooks in advance
_ = Task.Run(() => _ = Task.Run(() =>
{ {
try try
{ {
_ = _gameChatHooks.Value; _ = _gameChatHooks.Value;
_isTypingAnnounced = false;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -64,9 +69,74 @@ public class ChatService : DisposableMediatorSubscriberBase
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
_typingCts?.Cancel();
_typingCts?.Dispose();
if (_gameChatHooks.IsValueCreated) if (_gameChatHooks.IsValueCreated)
_gameChatHooks.Value!.Dispose(); _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) private void HandleUserChat(UserChatMsgMessage message)
{ {
@@ -124,7 +194,6 @@ public class ChatService : DisposableMediatorSubscriberBase
msg.AddText($"[SS{shellNumber}]<"); msg.AddText($"[SS{shellNumber}]<");
if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal)) if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal))
{ {
// Don't link to your own character
msg.AddText(chatMsg.SenderName); msg.AddText(chatMsg.SenderName);
} }
else else
@@ -142,8 +211,6 @@ public class ChatService : DisposableMediatorSubscriberBase
Type = logKind Type = logKind
}); });
} }
// Print an example message to the configured global chat channel
public void PrintChannelExample(string message, string gid = "") public void PrintChannelExample(string message, string gid = "")
{ {
int chatType = _mareConfig.Current.ChatLogKind; int chatType = _mareConfig.Current.ChatLogKind;
@@ -164,8 +231,6 @@ public class ChatService : DisposableMediatorSubscriberBase
Type = (XivChatType)chatType Type = (XivChatType)chatType
}); });
} }
// Called to update the active chat shell name if its renamed
public void MaybeUpdateShellName(int shellNumber) public void MaybeUpdateShellName(int shellNumber)
{ {
if (_mareConfig.Current.DisableSyncshellChat) if (_mareConfig.Current.DisableSyncshellChat)
@@ -178,7 +243,6 @@ public class ChatService : DisposableMediatorSubscriberBase
{ {
if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null) 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)) if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal))
SwitchChatShell(shellNumber); SwitchChatShell(shellNumber);
} }
@@ -197,7 +261,6 @@ public class ChatService : DisposableMediatorSubscriberBase
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
{ {
var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID; 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() _gameChatHooks.Value.ChatChannelOverride = new()
{ {
ChannelName = $"SS [{shellNumber}]: {name}", ChannelName = $"SS [{shellNumber}]: {name}",
@@ -221,7 +284,6 @@ public class ChatService : DisposableMediatorSubscriberBase
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
{ {
_ = Task.Run(async () => { _ = Task.Run(async () => {
// Should cache the name and home world instead of fetching it every time
var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => { var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => {
return new ChatMessage() return new ChatMessage()
{ {
@@ -230,6 +292,7 @@ public class ChatService : DisposableMediatorSubscriberBase
PayloadContent = chatBytes PayloadContent = chatBytes
}; };
}).ConfigureAwait(false); }).ConfigureAwait(false);
ClearTypingState();
await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false); await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false);
}).ConfigureAwait(false); }).ConfigureAwait(false);
return; return;

View File

@@ -7,6 +7,9 @@ using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.UI; using MareSynchronos.UI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MareSynchronos.API.Dto.User;
using System.Collections.Concurrent;
using Dalamud.Game.Text;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
@@ -20,6 +23,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase
private readonly IPartyList _partyList; private readonly IPartyList _partyList;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly ConcurrentDictionary<string, DateTime> _typingUsers = new();
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
private bool _isModified = false; private bool _isModified = false;
private bool _namePlateRoleColorsEnabled = false; private bool _namePlateRoleColorsEnabled = false;
@@ -41,6 +47,18 @@ public class GuiHookService : DisposableMediatorSubscriberBase
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck()); Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck());
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw()); Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw());
Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw()); Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw());
Mediator.Subscribe<UserTypingStateMessage>(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) public void RequestRedraw(bool force = false)
@@ -72,6 +90,7 @@ public class GuiHookService : DisposableMediatorSubscriberBase
if (!_configService.Current.UseNameColors) if (!_configService.Current.UseNameColors)
return; return;
var showTypingIndicator = _configService.Current.TypingIndicatorShowOnNameplates;
var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue); var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue);
var visibleUsersIds = visibleUsers.Select(u => (ulong)u.PlayerCharacterId).ToHashSet(); var visibleUsersIds = visibleUsers.Select(u => (ulong)u.PlayerCharacterId).ToHashSet();
@@ -95,6 +114,17 @@ public class GuiHookService : DisposableMediatorSubscriberBase
BuildColorEndSeString(colors) BuildColorEndSeString(colors)
); );
_isModified = true; _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();
}
} }
} }
} }

View File

@@ -3,6 +3,7 @@ using MareSynchronos.API.Data;
using MareSynchronos.API.Dto; using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.CharaData; using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.MareConfiguration.Models; using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
@@ -52,6 +53,7 @@ public record HaltScanMessage(string Source) : MessageBase;
public record ResumeScanMessage(string Source) : MessageBase; public record ResumeScanMessage(string Source) : MessageBase;
public record NotificationMessage public record NotificationMessage
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase; (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 CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
@@ -90,6 +92,7 @@ public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBas
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage; public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase; public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
public record GroupChatMsgMessage(GroupDto GroupInfo, 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 RecalculatePerformanceMessage(string? UID) : MessageBase;
public record NameplateRedrawMessage : MessageBase; public record NameplateRedrawMessage : MessageBase;
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID); 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<NearbyEntry> Entries) : MessageBase; public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
public record NearbyDetectionToggled(bool Enabled) : MessageBase; public record NearbyDetectionToggled(bool Enabled) : MessageBase;
public record AllowPairRequestsToggled(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); public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
#pragma warning restore S2094 #pragma warning restore S2094

View File

@@ -32,6 +32,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Mediator.Subscribe<NotificationMessage>(this, ShowNotification); Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
Mediator.Subscribe<DualNotificationMessage>(this, ShowDualNotification);
return Task.CompletedTask; 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) private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction)
{ {
appendInstruction = false; appendInstruction = false;

View File

@@ -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<PartyListTypingService> _logger;
private readonly IPartyList _partyList;
private readonly MareConfigService _configService;
private readonly PairManager _pairManager;
private readonly ConcurrentDictionary<string, DateTime> _typingUsers = new();
private readonly ConcurrentDictionary<string, DateTime> _typingNames = new(StringComparer.OrdinalIgnoreCase);
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
public PartyListTypingService(ILogger<PartyListTypingService> logger,
MareMediator mediator,
IPartyList partyList,
PairManager pairManager,
MareConfigService configService)
: base(logger, mediator)
{
_logger = logger;
_partyList = partyList;
_pairManager = pairManager;
_configService = configService;
Mediator.Subscribe<UserTypingStateMessage>(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<string, string>(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);
}
}
}
}
}

View File

@@ -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<SyncDefaultsService> logger, MareMediator mediator,
MareConfigService configService, ApiController apiController, PairManager pairManager) : base(logger, mediator)
{
_configService = configService;
_apiController = apiController;
_pairManager = pairManager;
Mediator.Subscribe<ApplyDefaultPairPermissionsMessage>(this, OnApplyPairDefaults);
Mediator.Subscribe<ApplyDefaultGroupPermissionsMessage>(this, OnApplyGroupDefaults);
Mediator.Subscribe<ApplyDefaultsToAllSyncsMessage>(this, _ => ApplyDefaultsToAll());
Mediator.Subscribe<PairSyncOverrideChanged>(this, OnPairOverrideChanged);
Mediator.Subscribe<GroupSyncOverrideChanged>(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<Task>();
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;
}
}

View File

@@ -169,6 +169,13 @@ public sealed class ChangelogUi : WindowMediatorSubscriberBase
{ {
return new List<ChangelogEntry> return new List<ChangelogEntry>
{ {
new(new Version(0, 1, 9, 0), "0.1.9.0", new List<ChangelogLine>
{
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<ChangelogLine> new(new Version(0, 1, 8, 2), "0.1.8.2", new List<ChangelogLine>
{ {
new("Détection Nearby : la liste rapide ne montre plus que les joueurs réellement invitables."), new("Détection Nearby : la liste rapide ne montre plus que les joueurs réellement invitables."),

View File

@@ -7,6 +7,7 @@ using Dalamud.Utility;
using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.User; using MareSynchronos.API.Dto.User;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
@@ -190,7 +191,7 @@ public class CompactUi : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip("Syncshells"); UiSharedService.AttachToolTip("Syncshells");
ImGui.Separator(); DrawDefaultSyncSettings();
if (!hasShownSyncShells) if (!hasShownSyncShells)
{ {
using (ImRaii.PushId("pairlist")) DrawPairList(); using (ImRaii.PushId("pairlist")) DrawPairList();
@@ -203,7 +204,7 @@ public class CompactUi : WindowMediatorSubscriberBase
using (ImRaii.PushId("transfers")) DrawTransfers(); using (ImRaii.PushId("transfers")) DrawTransfers();
TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight; TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight;
using (ImRaii.PushId("group-user-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs); 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) if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
@@ -253,6 +254,114 @@ public class CompactUi : WindowMediatorSubscriberBase
base.OnClose(); 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<bool> onToggle, Func<string> 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() private void DrawAddCharacter()
{ {
ImGui.Dummy(new(10)); ImGui.Dummy(new(10));
@@ -388,49 +497,26 @@ public class CompactUi : WindowMediatorSubscriberBase
try try
{ {
var inbox = _nearbyPending; // intentionally left blank; pending requests handled in collapsible section below
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();
}
} }
catch { } catch { }
// Add the "En attente" category var pendingCount = _nearbyPending?.Pending.Count ?? 0;
using (ImRaii.PushId("group-Pending")) if (pendingCount > 0)
{ {
var icon = _pendingOpen ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; using (ImRaii.PushId("group-Pending"))
_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)
{ {
ImGui.Indent(); var icon = _pendingOpen ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
if (_nearbyPending != null && _nearbyPending.Pending.Count > 0) _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.AlignTextToFramePadding();
ImGui.TextUnformatted($"{kv.Value} [{kv.Key}]"); ImGui.TextUnformatted($"{kv.Value} [{kv.Key}]");
@@ -447,13 +533,9 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
UiSharedService.AttachToolTip("Dismiss request"); 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 (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
if (_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(); ImGui.Indent();
var nearby = _nearbyEntries == null var nearby = _nearbyEntries == null
? new List<Services.Mediator.NearbyEntry>() ? new List<Services.Mediator.NearbyEntry>()

View File

@@ -272,6 +272,7 @@ public class DrawUserPair : DrawPairBase
{ {
var permissions = entry.UserPair.OwnPermissions; var permissions = entry.UserPair.OwnPermissions;
permissions.SetDisableSounds(!isDisableSounds); permissions.SetDisableSounds(!isDisableSounds);
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, permissions.IsDisableSounds(), null, null));
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
} }
@@ -282,6 +283,7 @@ public class DrawUserPair : DrawPairBase
{ {
var permissions = entry.UserPair.OwnPermissions; var permissions = entry.UserPair.OwnPermissions;
permissions.SetDisableAnimations(!isDisableAnims); permissions.SetDisableAnimations(!isDisableAnims);
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, permissions.IsDisableAnimations(), null));
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
} }
@@ -292,6 +294,7 @@ public class DrawUserPair : DrawPairBase
{ {
var permissions = entry.UserPair.OwnPermissions; var permissions = entry.UserPair.OwnPermissions;
permissions.SetDisableVFX(!isDisableVFX); permissions.SetDisableVFX(!isDisableVFX);
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, null, permissions.IsDisableVFX()));
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
} }

View File

@@ -769,6 +769,7 @@ internal sealed class GroupPanel
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
var perm = groupDto.GroupUserPermissions; var perm = groupDto.GroupUserPermissions;
perm.SetDisableSounds(!perm.IsDisableSounds()); 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)); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
} }
UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell." UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell."
@@ -782,6 +783,7 @@ internal sealed class GroupPanel
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
var perm = groupDto.GroupUserPermissions; var perm = groupDto.GroupUserPermissions;
perm.SetDisableAnimations(!perm.IsDisableAnimations()); 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)); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
} }
UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell." UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell."
@@ -796,6 +798,7 @@ internal sealed class GroupPanel
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
var perm = groupDto.GroupUserPermissions; var perm = groupDto.GroupUserPermissions;
perm.SetDisableVFX(!perm.IsDisableVFX()); 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)); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
} }
UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell." UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell."

View File

@@ -128,6 +128,10 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
using (ImRaii.Disabled(!hasChanges)) using (ImRaii.Disabled(!hasChanges))
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save")) 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)); _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
} }
UiSharedService.AttachToolTip("Save and apply all changes"); UiSharedService.AttachToolTip("Save and apply all changes");
@@ -148,10 +152,15 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
ImGui.SameLine(); ImGui.SameLine();
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default")) if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default"))
{ {
var defaults = _uiSharedService.ConfigService.Current;
_ownPermissions.SetPaused(false); _ownPermissions.SetPaused(false);
_ownPermissions.SetDisableVFX(false); _ownPermissions.SetDisableSounds(defaults.DefaultDisableSounds);
_ownPermissions.SetDisableSounds(false); _ownPermissions.SetDisableAnimations(defaults.DefaultDisableAnimations);
_ownPermissions.SetDisableAnimations(false); _ownPermissions.SetDisableVFX(defaults.DefaultDisableVfx);
Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID,
_ownPermissions.IsDisableSounds(),
_ownPermissions.IsDisableAnimations(),
_ownPermissions.IsDisableVFX()));
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
} }
UiSharedService.AttachToolTip("This will set all permissions to their default setting"); UiSharedService.AttachToolTip("This will set all permissions to their default setting");

View File

@@ -136,6 +136,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public string PlayerName => _dalamudUtil.GetPlayerName(); public string PlayerName => _dalamudUtil.GetPlayerName();
public IFontHandle UidFont { get; init; } public IFontHandle UidFont { get; init; }
public MareConfigService ConfigService => _configService;
public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value; public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value;
public uint WorldId => _dalamudUtil.GetHomeWorldId(); public uint WorldId => _dalamudUtil.GetHomeWorldId();

View File

@@ -97,6 +97,12 @@ public partial class ApiController
await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false); 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<UserData> visibleCharacters) private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
{ {
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID))); Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));

View File

@@ -138,6 +138,13 @@ public partial class ApiController
return Task.CompletedTask; 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) public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
{ {
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User); Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
@@ -313,6 +320,12 @@ public partial class ApiController
_mareHub!.On(nameof(Client_UserChatMsg), act); _mareHub!.On(nameof(Client_UserChatMsg), act);
} }
public void OnUserTypingState(Action<TypingStateDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserTypingState), act);
}
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act) public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
{ {
if (_initialized) return; if (_initialized) return;

View File

@@ -348,6 +348,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto)); OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto));
OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto)); OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto));
OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto)); OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto));
OnUserTypingState(dto => _ = Client_UserTypingState(dto));
OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto)); OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto));
OnGroupDelete((dto) => _ = Client_GroupDelete(dto)); OnGroupDelete((dto) => _ = Client_GroupDelete(dto));
@@ -393,7 +394,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
foreach (var user in users) foreach (var user in users)
{ {
Logger.LogDebug("Group Pair: {user}", user); 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; ServerState = state;
} }
} }
#pragma warning restore MA0040 #pragma warning restore MA0040