diff --git a/LightlessAPI b/LightlessAPI index dfb0594..efc0ef0 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit dfb0594a5be49994cda6d95aa0d995bd93cdfbc0 +Subproject commit efc0ef09f9a3bf774f5e946a3b5e473865338be2 diff --git a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs index 829c737..65709d1 100644 --- a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs +++ b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs @@ -5,6 +5,7 @@ using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer; +using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; namespace LightlessSync.PlayerData.Handlers; @@ -376,8 +377,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP { if (Address == IntPtr.Zero) return DrawCondition.ObjectZero; if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero; - var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags) != 0x0; - if (renderFlags) return DrawCondition.RenderFlags; + var visibilityFlags = ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags; + if (visibilityFlags != VisibilityFlags.None) return DrawCondition.RenderFlags; if (ObjectKind == ObjectKind.Player) { diff --git a/LightlessSync/Services/ActorTracking/ActorObjectService.cs b/LightlessSync/Services/ActorTracking/ActorObjectService.cs index f9c4615..c76d6dd 100644 --- a/LightlessSync/Services/ActorTracking/ActorObjectService.cs +++ b/LightlessSync/Services/ActorTracking/ActorObjectService.cs @@ -783,7 +783,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable if (drawObject == null) return false; - if ((ushort)gameObject->RenderFlags == 2048) + if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None) return false; var characterBase = (CharacterBase*)drawObject; diff --git a/LightlessSync/Services/Chat/ChatModels.cs b/LightlessSync/Services/Chat/ChatModels.cs index ba89084..0f35f7c 100644 --- a/LightlessSync/Services/Chat/ChatModels.cs +++ b/LightlessSync/Services/Chat/ChatModels.cs @@ -3,10 +3,21 @@ using LightlessSync.API.Dto.Chat; namespace LightlessSync.Services.Chat; public sealed record ChatMessageEntry( - ChatMessageDto Payload, + ChatMessageDto? Payload, string DisplayName, bool FromSelf, - DateTime ReceivedAtUtc); + DateTime ReceivedAtUtc, + ChatSystemEntry? SystemMessage = null) +{ + public bool IsSystem => SystemMessage is not null; +} + +public enum ChatSystemEntryType +{ + ZoneSeparator +} + +public sealed record ChatSystemEntry(ChatSystemEntryType Type, string? ZoneName); public readonly record struct ChatChannelSnapshot( string Key, diff --git a/LightlessSync/Services/Chat/ZoneChatService.cs b/LightlessSync/Services/Chat/ZoneChatService.cs index 8e86b49..55009ab 100644 --- a/LightlessSync/Services/Chat/ZoneChatService.cs +++ b/LightlessSync/Services/Chat/ZoneChatService.cs @@ -240,8 +240,22 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS } } - public Task ResolveParticipantAsync(ChatChannelDescriptor descriptor, string token) - => _apiController.ResolveChatParticipant(new ChatParticipantResolveRequestDto(descriptor, token)); + public async Task SetParticipantMuteAsync(ChatChannelDescriptor descriptor, string token, bool mute) + { + if (string.IsNullOrWhiteSpace(token)) + return false; + + try + { + await _apiController.SetChatParticipantMute(new ChatParticipantMuteRequestDto(descriptor, token, mute)).ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to update chat participant mute state"); + return false; + } + } public Task ReportMessageAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext) { @@ -534,6 +548,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS } bool shouldForceSend; + ChatMessageEntry? zoneSeparatorEntry = null; using (_sync.EnterScope()) { @@ -544,11 +559,24 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS state.IsAvailable = _chatEnabled; state.StatusText = _chatEnabled ? null : "Chat services disabled"; + var previousDescriptor = _lastZoneDescriptor; + var zoneChanged = previousDescriptor.HasValue && !ChannelDescriptorsMatch(previousDescriptor.Value, descriptor.Value); + _activeChannelKey = ZoneChannelKey; - shouldForceSend = force || !_lastZoneDescriptor.HasValue || !ChannelDescriptorsMatch(_lastZoneDescriptor.Value, descriptor.Value); + shouldForceSend = force || !previousDescriptor.HasValue || zoneChanged; + if (zoneChanged && state.Messages.Any(m => !m.IsSystem)) + { + zoneSeparatorEntry = AddZoneSeparatorLocked(state, definition.Value.DisplayName); + } + _lastZoneDescriptor = descriptor; } + if (zoneSeparatorEntry is not null) + { + Mediator.Publish(new ChatChannelMessageAdded(ZoneChannelKey, zoneSeparatorEntry)); + } + PublishChannelListChanged(); await SendPresenceAsync(descriptor.Value, territoryId, isActive: true, force: shouldForceSend).ConfigureAwait(false); } @@ -561,7 +589,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId) { ChatChannelDescriptor? descriptor = null; - bool clearedHistory = false; using (_sync.EnterScope()) { @@ -570,15 +597,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS if (_channels.TryGetValue(ZoneChannelKey, out var state)) { - if (state.Messages.Count > 0) - { - state.Messages.Clear(); - state.HasUnread = false; - state.UnreadCount = 0; - _lastReadCounts[ZoneChannelKey] = 0; - clearedHistory = true; - } - state.IsConnected = _isConnected; state.IsAvailable = false; state.StatusText = !_chatEnabled @@ -593,11 +611,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS } } - if (clearedHistory) - { - PublishHistoryCleared(ZoneChannelKey); - } - PublishChannelListChanged(); if (descriptor.HasValue) @@ -1007,6 +1020,39 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS return new ChatMessageEntry(dto, displayName, fromSelf, DateTime.UtcNow); } + private ChatMessageEntry AddZoneSeparatorLocked(ChatChannelState state, string zoneDisplayName) + { + var separator = new ChatMessageEntry( + null, + string.Empty, + false, + DateTime.UtcNow, + new ChatSystemEntry(ChatSystemEntryType.ZoneSeparator, zoneDisplayName)); + + state.Messages.Add(separator); + if (state.Messages.Count > MaxMessageHistory) + { + state.Messages.RemoveAt(0); + } + + if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal)) + { + state.HasUnread = false; + state.UnreadCount = 0; + _lastReadCounts[ZoneChannelKey] = state.Messages.Count; + } + else if (_lastReadCounts.TryGetValue(ZoneChannelKey, out var readCount)) + { + _lastReadCounts[ZoneChannelKey] = readCount + 1; + } + else + { + _lastReadCounts[ZoneChannelKey] = state.Messages.Count; + } + + return separator; + } + private string ResolveDisplayName(ChatMessageDto dto, bool fromSelf) { var isZone = dto.Channel.Type == ChatChannelType.Zone; @@ -1070,8 +1116,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated()); - private void PublishHistoryCleared(string channelKey) => Mediator.Publish(new ChatChannelHistoryCleared(channelKey)); - private static IEnumerable EnumerateTerritoryKeys(string? value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 5614505..06d480b 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -26,6 +26,7 @@ using System.Runtime.CompilerServices; using System.Text; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; +using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; namespace LightlessSync.Services; @@ -707,7 +708,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber const int tick = 250; int curWaitTime = 0; _logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X")); - while (obj->RenderFlags != 0x00 && curWaitTime < timeOut) + while (obj->RenderFlags != VisibilityFlags.None && curWaitTime < timeOut) { _logger.LogTrace($"Waiting for gpose actor to finish drawing"); curWaitTime += tick; @@ -752,7 +753,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber bool isDrawingChanged = false; if ((nint)drawObj != IntPtr.Zero) { - isDrawing = (ushort)gameObj->RenderFlags == 0b100000000000; + isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None; if (!isDrawing) { isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0; diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index f8250b9..758b9f5 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -133,7 +133,6 @@ public record PairDownloadStatusMessage(List<(string PlayerName, float Progress, public record VisibilityChange : MessageBase; public record ChatChannelsUpdated : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; -public record ChatChannelHistoryCleared(string ChannelKey) : MessageBase; public record GroupCollectionChangedMessage : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase; #pragma warning restore S2094 diff --git a/LightlessSync/Services/Profiles/LightlessProfileManager.cs b/LightlessSync/Services/Profiles/LightlessProfileManager.cs index 2f854e8..fd8c19c 100644 --- a/LightlessSync/Services/Profiles/LightlessProfileManager.cs +++ b/LightlessSync/Services/Profiles/LightlessProfileManager.cs @@ -327,7 +327,12 @@ public class LightlessProfileManager : MediatorSubscriberBase if (profile == null) return null; - var userData = profile.User; + if (profile.User is null) + { + Logger.LogWarning("Lightfinder profile response missing user info for CID {HashedCid}", hashedCid); + } + + var userData = profile.User ?? new UserData(hashedCid, Alias: "Lightfinder User"); var profileTags = profile.Tags ?? _emptyTagSet; var profileData = BuildProfileData(userData, profile, profileTags); _lightlessProfiles[userData] = profileData; diff --git a/LightlessSync/UI/StandaloneProfileUi.cs b/LightlessSync/UI/StandaloneProfileUi.cs index 684caef..d1ebdbe 100644 --- a/LightlessSync/UI/StandaloneProfileUi.cs +++ b/LightlessSync/UI/StandaloneProfileUi.cs @@ -146,12 +146,19 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase if (string.IsNullOrEmpty(hashedCid)) return LightfinderDisplayName; - var (name, address) = dalamudUtilService.FindPlayerByNameHash(hashedCid); - if (string.IsNullOrEmpty(name)) - return LightfinderDisplayName; + try + { + var (name, address) = dalamudUtilService.FindPlayerByNameHash(hashedCid); + if (string.IsNullOrEmpty(name)) + return LightfinderDisplayName; - var world = dalamudUtilService.GetWorldNameFromPlayerAddress(address); - return string.IsNullOrEmpty(world) ? name : $"{name} ({world})"; + var world = dalamudUtilService.GetWorldNameFromPlayerAddress(address); + return string.IsNullOrEmpty(world) ? name : $"{name} ({world})"; + } + catch + { + return LightfinderDisplayName; + } } protected override void DrawInternal() diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs index 93edc5d..e7574e8 100644 --- a/LightlessSync/UI/ZoneChatUi.cs +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -95,13 +95,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase .Apply(); Mediator.Subscribe(this, OnChatChannelMessageAdded); - Mediator.Subscribe(this, msg => - { - if (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, msg.ChannelKey, StringComparison.Ordinal)) - { - _scrollToBottom = true; - } - }); Mediator.Subscribe(this, _ => _scrollToBottom = true); } @@ -250,6 +243,21 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase for (var i = 0; i < channel.Messages.Count; i++) { var message = channel.Messages[i]; + ImGui.PushID(i); + + if (message.IsSystem) + { + DrawSystemEntry(message); + ImGui.PopID(); + continue; + } + + if (message.Payload is not { } payload) + { + ImGui.PopID(); + continue; + } + var timestampText = string.Empty; if (showTimestamps) { @@ -257,24 +265,21 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite; - ImGui.PushID(i); ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {message.Payload.Message}"); + ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}"); ImGui.PopStyleColor(); if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}")) { - var contextLocalTimestamp = message.Payload.SentAtUtc.ToLocalTime(); + var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime(); var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture); ImGui.TextDisabled(contextTimestampText); ImGui.Separator(); + var actionIndex = 0; foreach (var action in GetContextMenuActions(channel, message)) { - if (ImGui.MenuItem(action.Label, string.Empty, false, action.IsEnabled)) - { - action.Execute(); - } + DrawContextMenuAction(action, actionIndex++); } ImGui.EndPopup(); @@ -538,6 +543,13 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase return; } + if (message.Payload is not { } payload) + { + CloseReportPopup(); + ImGui.EndPopup(); + return; + } + if (_reportSubmissionResult is { } pendingResult) { _reportSubmissionResult = null; @@ -563,11 +575,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.TextUnformatted(channelLabel); ImGui.TextUnformatted($"Sender: {message.DisplayName}"); - ImGui.TextUnformatted($"Sent: {message.Payload.SentAtUtc.ToLocalTime().ToString("g", CultureInfo.CurrentCulture)}"); + ImGui.TextUnformatted($"Sent: {payload.SentAtUtc.ToLocalTime().ToString("g", CultureInfo.CurrentCulture)}"); ImGui.Separator(); ImGui.PushTextWrapPos(ImGui.GetWindowContentRegionMax().X); - ImGui.TextWrapped(message.Payload.Message); + ImGui.TextWrapped(payload.Message); ImGui.PopTextWrapPos(); ImGui.Separator(); @@ -633,9 +645,15 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message) { + if (message.Payload is not { } payload) + { + _logger.LogDebug("Ignoring report popup request for non-message entry in {ChannelKey}", channel.Key); + return; + } + _reportTargetChannel = channel; _reportTargetMessage = message; - _logger.LogDebug("Opening report popup for channel {ChannelKey}, message {MessageId}", channel.Key, message.Payload.MessageId); + _logger.LogDebug("Opening report popup for channel {ChannelKey}, message {MessageId}", channel.Key, payload.MessageId); _reportReason = string.Empty; _reportAdditionalContext = string.Empty; _reportError = null; @@ -650,6 +668,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase if (_reportSubmitting) return; + if (message.Payload is not { } payload) + { + _reportError = "Unable to report this message."; + return; + } + var trimmedReason = _reportReason.Trim(); if (trimmedReason.Length == 0) { @@ -666,7 +690,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _reportSubmissionResult = null; var descriptor = channel.Descriptor; - var messageId = message.Payload.MessageId; + var messageId = payload.MessageId; if (string.IsNullOrWhiteSpace(messageId)) { _reportSubmitting = false; @@ -743,25 +767,33 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private IEnumerable GetContextMenuActions(ChatChannelSnapshot channel, ChatMessageEntry message) { - if (TryCreateCopyMessageAction(message, out var copyAction)) + if (message.IsSystem || message.Payload is not { } payload) + yield break; + + if (TryCreateCopyMessageAction(message, payload, out var copyAction)) { yield return copyAction; } - if (TryCreateViewProfileAction(channel, message, out var viewProfile)) + if (TryCreateViewProfileAction(channel, message, payload, out var viewProfile)) { yield return viewProfile; } - if (TryCreateReportMessageAction(channel, message, out var reportAction)) + if (TryCreateMuteParticipantAction(channel, message, payload, out var muteAction)) + { + yield return muteAction; + } + + if (TryCreateReportMessageAction(channel, message, payload, out var reportAction)) { yield return reportAction; } } - private static bool TryCreateCopyMessageAction(ChatMessageEntry message, out ChatMessageContextAction action) + private static bool TryCreateCopyMessageAction(ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action) { - var text = message.Payload.Message; + var text = payload.Message; if (string.IsNullOrEmpty(text)) { action = default; @@ -769,20 +801,21 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } action = new ChatMessageContextAction( + FontAwesomeIcon.Clipboard, "Copy Message", true, () => ImGui.SetClipboardText(text)); return true; } - private bool TryCreateViewProfileAction(ChatChannelSnapshot channel, ChatMessageEntry message, out ChatMessageContextAction action) + private bool TryCreateViewProfileAction(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action) { action = default; switch (channel.Type) { case ChatChannelType.Group: { - var user = message.Payload.Sender.User; + var user = payload.Sender.User; if (user?.UID is not { Length: > 0 }) return false; @@ -790,6 +823,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase if (snapshot.PairsByUid.TryGetValue(user.UID, out var pair) && pair is not null) { action = new ChatMessageContextAction( + FontAwesomeIcon.User, "View Profile", true, () => Mediator.Publish(new ProfileOpenStandaloneMessage(pair))); @@ -797,41 +831,64 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } action = new ChatMessageContextAction( + FontAwesomeIcon.User, "View Profile", true, () => RunContextAction(() => OpenStandardProfileAsync(user))); return true; } - case ChatChannelType.Zone: - if (!message.Payload.Sender.CanResolveProfile) + if (!payload.Sender.CanResolveProfile) return false; - if (string.IsNullOrEmpty(message.Payload.Sender.Token)) + var hashedCid = payload.Sender.HashedCid; + if (string.IsNullOrEmpty(hashedCid)) return false; action = new ChatMessageContextAction( + FontAwesomeIcon.User, "View Profile", true, - () => RunContextAction(() => OpenZoneParticipantProfileAsync(channel.Descriptor, message.Payload.Sender.Token))); + () => RunContextAction(() => OpenLightfinderProfileInternalAsync(hashedCid))); return true; - default: return false; } } - private bool TryCreateReportMessageAction(ChatChannelSnapshot channel, ChatMessageEntry message, out ChatMessageContextAction action) + private bool TryCreateMuteParticipantAction(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action) { action = default; if (message.FromSelf) return false; - if (string.IsNullOrWhiteSpace(message.Payload.MessageId)) + if (string.IsNullOrEmpty(payload.Sender.Token)) + return false; + + var safeName = string.IsNullOrWhiteSpace(message.DisplayName) + ? "Participant" + : message.DisplayName; + + action = new ChatMessageContextAction( + FontAwesomeIcon.VolumeMute, + $"Mute '{safeName}'", + true, + () => RunContextAction(() => _zoneChatService.SetParticipantMuteAsync(channel.Descriptor, payload.Sender.Token!, true))); + return true; + } + + private bool TryCreateReportMessageAction(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action) + { + action = default; + if (message.FromSelf) + return false; + + if (string.IsNullOrWhiteSpace(payload.MessageId)) return false; action = new ChatMessageContextAction( + FontAwesomeIcon.ExclamationTriangle, "Report Message", true, () => OpenReportPopup(channel, message)); @@ -863,24 +920,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase }); } - private async Task OpenZoneParticipantProfileAsync(ChatChannelDescriptor descriptor, string token) + private void OnChatChannelMessageAdded(ChatChannelMessageAdded message) { - var result = await _zoneChatService.ResolveParticipantAsync(descriptor, token).ConfigureAwait(false); - if (result is null) + if (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, message.ChannelKey, StringComparison.Ordinal)) { - Mediator.Publish(new NotificationMessage("Zone Chat", "Participant is no longer available.", NotificationType.Warning, TimeSpan.FromSeconds(3))); - return; + _scrollToBottom = true; } - - var resolved = result.Value; - var hashedCid = resolved.Sender.HashedCid; - if (string.IsNullOrEmpty(hashedCid)) - { - Mediator.Publish(new NotificationMessage("Zone Chat", "This participant remains anonymous.", NotificationType.Warning, TimeSpan.FromSeconds(3))); - return; - } - - await OpenLightfinderProfileInternalAsync(hashedCid).ConfigureAwait(false); } private async Task OpenLightfinderProfileInternalAsync(string hashedCid) @@ -901,14 +946,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase Mediator.Publish(new OpenLightfinderProfileMessage(sanitizedUser, profile.Value.ProfileData, hashedCid)); } - private void OnChatChannelMessageAdded(ChatChannelMessageAdded message) - { - if (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, message.ChannelKey, StringComparison.Ordinal)) - { - _scrollToBottom = true; - } - } - private void EnsureSelectedChannel(IReadOnlyList channels) { if (_selectedChannelKey is not null && channels.Any(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal))) @@ -1374,5 +1411,86 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f); } - private readonly record struct ChatMessageContextAction(string Label, bool IsEnabled, Action Execute); + private void DrawSystemEntry(ChatMessageEntry entry) + { + var system = entry.SystemMessage; + if (system is null) + return; + + switch (system.Type) + { + case ChatSystemEntryType.ZoneSeparator: + DrawZoneSeparatorEntry(system, entry.ReceivedAtUtc); + break; + } + } + + private void DrawZoneSeparatorEntry(ChatSystemEntry systemEntry, DateTime timestampUtc) + { + ImGui.Spacing(); + + var zoneName = string.IsNullOrWhiteSpace(systemEntry.ZoneName) ? "Zone" : systemEntry.ZoneName; + var localTime = timestampUtc.ToLocalTime(); + var label = $"{localTime.ToString("HH:mm", CultureInfo.CurrentCulture)} - {zoneName}"; + var availableWidth = ImGui.GetContentRegionAvail().X; + var textSize = ImGui.CalcTextSize(label); + var cursor = ImGui.GetCursorPos(); + var textPosX = cursor.X + MathF.Max(0f, (availableWidth - textSize.X) * 0.5f); + + ImGui.SetCursorPos(new Vector2(textPosX, cursor.Y)); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey2); + ImGui.TextUnformatted(label); + ImGui.PopStyleColor(); + + var nextY = ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y * 0.35f; + ImGui.SetCursorPos(new Vector2(cursor.X, nextY)); + ImGui.Separator(); + ImGui.Spacing(); + } + + private void DrawContextMenuAction(ChatMessageContextAction action, int index) + { + ImGui.PushID(index); + using var disabled = ImRaii.Disabled(!action.IsEnabled); + + var availableWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X); + var clicked = ImGui.Selectable("##chat_ctx_action", false, ImGuiSelectableFlags.None, new Vector2(availableWidth, 0f)); + + var drawList = ImGui.GetWindowDrawList(); + var itemMin = ImGui.GetItemRectMin(); + var itemMax = ImGui.GetItemRectMax(); + var itemHeight = itemMax.Y - itemMin.Y; + var style = ImGui.GetStyle(); + var textColor = ImGui.GetColorU32(action.IsEnabled ? ImGuiCol.Text : ImGuiCol.TextDisabled); + + var textSize = ImGui.CalcTextSize(action.Label); + var textPos = new Vector2(itemMin.X + style.FramePadding.X, itemMin.Y + (itemHeight - textSize.Y) * 0.5f); + + if (action.Icon.HasValue) + { + var iconSize = _uiSharedService.GetIconSize(action.Icon.Value); + var iconPos = new Vector2( + itemMin.X + style.FramePadding.X, + itemMin.Y + (itemHeight - iconSize.Y) * 0.5f); + + using (_uiSharedService.IconFont.Push()) + { + drawList.AddText(iconPos, textColor, action.Icon.Value.ToIconString()); + } + + textPos.X = iconPos.X + iconSize.X + style.ItemSpacing.X; + } + + drawList.AddText(textPos, textColor, action.Label); + + if (clicked && action.IsEnabled) + { + ImGui.CloseCurrentPopup(); + action.Execute(); + } + + ImGui.PopID(); + } + + private readonly record struct ChatMessageContextAction(FontAwesomeIcon? Icon, string Label, bool IsEnabled, Action Execute); } diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index 0a39219..24448c7 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -60,10 +60,10 @@ public partial class ApiController await _lightlessHub.InvokeAsync(nameof(ReportChatMessage), request).ConfigureAwait(false); } - public async Task ResolveChatParticipant(ChatParticipantResolveRequestDto request) + public async Task SetChatParticipantMute(ChatParticipantMuteRequestDto request) { - if (!IsConnected || _lightlessHub is null) return null; - return await _lightlessHub.InvokeAsync(nameof(ResolveChatParticipant), request).ConfigureAwait(false); + if (!IsConnected || _lightlessHub is null) return; + await _lightlessHub.InvokeAsync(nameof(SetChatParticipantMute), request).ConfigureAwait(false); } public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) diff --git a/ffxiv_pictomancy b/ffxiv_pictomancy index 788bc33..66c9667 160000 --- a/ffxiv_pictomancy +++ b/ffxiv_pictomancy @@ -1 +1 @@ -Subproject commit 788bc339a67e7a3db01a47a954034a83b9c3b61b +Subproject commit 66c96678a29454f178c681d8920a8ee0a9d50c40