adjust visibility flags, improve chat functionality, bump submodules

This commit is contained in:
2025-12-18 20:49:38 +09:00
parent 7b4e42c487
commit 4ffc2247b2
12 changed files with 281 additions and 95 deletions

View File

@@ -5,6 +5,7 @@ using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer; using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Handlers; namespace LightlessSync.PlayerData.Handlers;
@@ -376,8 +377,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
{ {
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero; if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero; if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags) != 0x0; var visibilityFlags = ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags;
if (renderFlags) return DrawCondition.RenderFlags; if (visibilityFlags != VisibilityFlags.None) return DrawCondition.RenderFlags;
if (ObjectKind == ObjectKind.Player) if (ObjectKind == ObjectKind.Player)
{ {

View File

@@ -783,7 +783,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (drawObject == null) if (drawObject == null)
return false; return false;
if ((ushort)gameObject->RenderFlags == 2048) if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
return false; return false;
var characterBase = (CharacterBase*)drawObject; var characterBase = (CharacterBase*)drawObject;

View File

@@ -3,10 +3,21 @@ using LightlessSync.API.Dto.Chat;
namespace LightlessSync.Services.Chat; namespace LightlessSync.Services.Chat;
public sealed record ChatMessageEntry( public sealed record ChatMessageEntry(
ChatMessageDto Payload, ChatMessageDto? Payload,
string DisplayName, string DisplayName,
bool FromSelf, 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( public readonly record struct ChatChannelSnapshot(
string Key, string Key,

View File

@@ -240,8 +240,22 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
} }
} }
public Task<ChatParticipantResolveResultDto?> ResolveParticipantAsync(ChatChannelDescriptor descriptor, string token) public async Task<bool> SetParticipantMuteAsync(ChatChannelDescriptor descriptor, string token, bool mute)
=> _apiController.ResolveChatParticipant(new ChatParticipantResolveRequestDto(descriptor, token)); {
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<ChatReportResult> ReportMessageAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext) public Task<ChatReportResult> ReportMessageAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext)
{ {
@@ -534,6 +548,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
} }
bool shouldForceSend; bool shouldForceSend;
ChatMessageEntry? zoneSeparatorEntry = null;
using (_sync.EnterScope()) using (_sync.EnterScope())
{ {
@@ -544,11 +559,24 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.IsAvailable = _chatEnabled; state.IsAvailable = _chatEnabled;
state.StatusText = _chatEnabled ? null : "Chat services disabled"; state.StatusText = _chatEnabled ? null : "Chat services disabled";
var previousDescriptor = _lastZoneDescriptor;
var zoneChanged = previousDescriptor.HasValue && !ChannelDescriptorsMatch(previousDescriptor.Value, descriptor.Value);
_activeChannelKey = ZoneChannelKey; _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; _lastZoneDescriptor = descriptor;
} }
if (zoneSeparatorEntry is not null)
{
Mediator.Publish(new ChatChannelMessageAdded(ZoneChannelKey, zoneSeparatorEntry));
}
PublishChannelListChanged(); PublishChannelListChanged();
await SendPresenceAsync(descriptor.Value, territoryId, isActive: true, force: shouldForceSend).ConfigureAwait(false); 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) private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId)
{ {
ChatChannelDescriptor? descriptor = null; ChatChannelDescriptor? descriptor = null;
bool clearedHistory = false;
using (_sync.EnterScope()) using (_sync.EnterScope())
{ {
@@ -570,15 +597,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (_channels.TryGetValue(ZoneChannelKey, out var state)) 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.IsConnected = _isConnected;
state.IsAvailable = false; state.IsAvailable = false;
state.StatusText = !_chatEnabled state.StatusText = !_chatEnabled
@@ -593,11 +611,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
} }
} }
if (clearedHistory)
{
PublishHistoryCleared(ZoneChannelKey);
}
PublishChannelListChanged(); PublishChannelListChanged();
if (descriptor.HasValue) if (descriptor.HasValue)
@@ -1007,6 +1020,39 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
return new ChatMessageEntry(dto, displayName, fromSelf, DateTime.UtcNow); 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) private string ResolveDisplayName(ChatMessageDto dto, bool fromSelf)
{ {
var isZone = dto.Channel.Type == ChatChannelType.Zone; 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 PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
private void PublishHistoryCleared(string channelKey) => Mediator.Publish(new ChatChannelHistoryCleared(channelKey));
private static IEnumerable<string> EnumerateTerritoryKeys(string? value) private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))

View File

@@ -26,6 +26,7 @@ using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
namespace LightlessSync.Services; namespace LightlessSync.Services;
@@ -707,7 +708,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
const int tick = 250; const int tick = 250;
int curWaitTime = 0; int curWaitTime = 0;
_logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X")); _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"); _logger.LogTrace($"Waiting for gpose actor to finish drawing");
curWaitTime += tick; curWaitTime += tick;
@@ -752,7 +753,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
bool isDrawingChanged = false; bool isDrawingChanged = false;
if ((nint)drawObj != IntPtr.Zero) if ((nint)drawObj != IntPtr.Zero)
{ {
isDrawing = (ushort)gameObj->RenderFlags == 0b100000000000; isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
if (!isDrawing) if (!isDrawing)
{ {
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0; isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;

View File

@@ -133,7 +133,6 @@ public record PairDownloadStatusMessage(List<(string PlayerName, float Progress,
public record VisibilityChange : MessageBase; public record VisibilityChange : MessageBase;
public record ChatChannelsUpdated : MessageBase; public record ChatChannelsUpdated : MessageBase;
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
public record ChatChannelHistoryCleared(string ChannelKey) : MessageBase;
public record GroupCollectionChangedMessage : MessageBase; public record GroupCollectionChangedMessage : MessageBase;
public record OpenUserProfileMessage(UserData User) : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase;
#pragma warning restore S2094 #pragma warning restore S2094

View File

@@ -327,7 +327,12 @@ public class LightlessProfileManager : MediatorSubscriberBase
if (profile == null) if (profile == null)
return 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 profileTags = profile.Tags ?? _emptyTagSet;
var profileData = BuildProfileData(userData, profile, profileTags); var profileData = BuildProfileData(userData, profile, profileTags);
_lightlessProfiles[userData] = profileData; _lightlessProfiles[userData] = profileData;

View File

@@ -146,12 +146,19 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
if (string.IsNullOrEmpty(hashedCid)) if (string.IsNullOrEmpty(hashedCid))
return LightfinderDisplayName; return LightfinderDisplayName;
var (name, address) = dalamudUtilService.FindPlayerByNameHash(hashedCid); try
if (string.IsNullOrEmpty(name)) {
return LightfinderDisplayName; var (name, address) = dalamudUtilService.FindPlayerByNameHash(hashedCid);
if (string.IsNullOrEmpty(name))
return LightfinderDisplayName;
var world = dalamudUtilService.GetWorldNameFromPlayerAddress(address); var world = dalamudUtilService.GetWorldNameFromPlayerAddress(address);
return string.IsNullOrEmpty(world) ? name : $"{name} ({world})"; return string.IsNullOrEmpty(world) ? name : $"{name} ({world})";
}
catch
{
return LightfinderDisplayName;
}
} }
protected override void DrawInternal() protected override void DrawInternal()

View File

@@ -95,13 +95,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
.Apply(); .Apply();
Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded); Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded);
Mediator.Subscribe<ChatChannelHistoryCleared>(this, msg =>
{
if (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, msg.ChannelKey, StringComparison.Ordinal))
{
_scrollToBottom = true;
}
});
Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true); Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true);
} }
@@ -250,6 +243,21 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
for (var i = 0; i < channel.Messages.Count; i++) for (var i = 0; i < channel.Messages.Count; i++)
{ {
var message = channel.Messages[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; var timestampText = string.Empty;
if (showTimestamps) if (showTimestamps)
{ {
@@ -257,24 +265,21 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
} }
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite; var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
ImGui.PushID(i);
ImGui.PushStyleColor(ImGuiCol.Text, color); ImGui.PushStyleColor(ImGuiCol.Text, color);
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {message.Payload.Message}"); ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
ImGui.PopStyleColor(); ImGui.PopStyleColor();
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}")) 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); var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
ImGui.TextDisabled(contextTimestampText); ImGui.TextDisabled(contextTimestampText);
ImGui.Separator(); ImGui.Separator();
var actionIndex = 0;
foreach (var action in GetContextMenuActions(channel, message)) foreach (var action in GetContextMenuActions(channel, message))
{ {
if (ImGui.MenuItem(action.Label, string.Empty, false, action.IsEnabled)) DrawContextMenuAction(action, actionIndex++);
{
action.Execute();
}
} }
ImGui.EndPopup(); ImGui.EndPopup();
@@ -538,6 +543,13 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
return; return;
} }
if (message.Payload is not { } payload)
{
CloseReportPopup();
ImGui.EndPopup();
return;
}
if (_reportSubmissionResult is { } pendingResult) if (_reportSubmissionResult is { } pendingResult)
{ {
_reportSubmissionResult = null; _reportSubmissionResult = null;
@@ -563,11 +575,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted(channelLabel); ImGui.TextUnformatted(channelLabel);
ImGui.TextUnformatted($"Sender: {message.DisplayName}"); 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.Separator();
ImGui.PushTextWrapPos(ImGui.GetWindowContentRegionMax().X); ImGui.PushTextWrapPos(ImGui.GetWindowContentRegionMax().X);
ImGui.TextWrapped(message.Payload.Message); ImGui.TextWrapped(payload.Message);
ImGui.PopTextWrapPos(); ImGui.PopTextWrapPos();
ImGui.Separator(); ImGui.Separator();
@@ -633,9 +645,15 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message) 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; _reportTargetChannel = channel;
_reportTargetMessage = message; _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; _reportReason = string.Empty;
_reportAdditionalContext = string.Empty; _reportAdditionalContext = string.Empty;
_reportError = null; _reportError = null;
@@ -650,6 +668,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
if (_reportSubmitting) if (_reportSubmitting)
return; return;
if (message.Payload is not { } payload)
{
_reportError = "Unable to report this message.";
return;
}
var trimmedReason = _reportReason.Trim(); var trimmedReason = _reportReason.Trim();
if (trimmedReason.Length == 0) if (trimmedReason.Length == 0)
{ {
@@ -666,7 +690,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
_reportSubmissionResult = null; _reportSubmissionResult = null;
var descriptor = channel.Descriptor; var descriptor = channel.Descriptor;
var messageId = message.Payload.MessageId; var messageId = payload.MessageId;
if (string.IsNullOrWhiteSpace(messageId)) if (string.IsNullOrWhiteSpace(messageId))
{ {
_reportSubmitting = false; _reportSubmitting = false;
@@ -743,25 +767,33 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private IEnumerable<ChatMessageContextAction> GetContextMenuActions(ChatChannelSnapshot channel, ChatMessageEntry message) private IEnumerable<ChatMessageContextAction> 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; yield return copyAction;
} }
if (TryCreateViewProfileAction(channel, message, out var viewProfile)) if (TryCreateViewProfileAction(channel, message, payload, out var viewProfile))
{ {
yield return 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; 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)) if (string.IsNullOrEmpty(text))
{ {
action = default; action = default;
@@ -769,20 +801,21 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
} }
action = new ChatMessageContextAction( action = new ChatMessageContextAction(
FontAwesomeIcon.Clipboard,
"Copy Message", "Copy Message",
true, true,
() => ImGui.SetClipboardText(text)); () => ImGui.SetClipboardText(text));
return true; 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; action = default;
switch (channel.Type) switch (channel.Type)
{ {
case ChatChannelType.Group: case ChatChannelType.Group:
{ {
var user = message.Payload.Sender.User; var user = payload.Sender.User;
if (user?.UID is not { Length: > 0 }) if (user?.UID is not { Length: > 0 })
return false; return false;
@@ -790,6 +823,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
if (snapshot.PairsByUid.TryGetValue(user.UID, out var pair) && pair is not null) if (snapshot.PairsByUid.TryGetValue(user.UID, out var pair) && pair is not null)
{ {
action = new ChatMessageContextAction( action = new ChatMessageContextAction(
FontAwesomeIcon.User,
"View Profile", "View Profile",
true, true,
() => Mediator.Publish(new ProfileOpenStandaloneMessage(pair))); () => Mediator.Publish(new ProfileOpenStandaloneMessage(pair)));
@@ -797,41 +831,64 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
} }
action = new ChatMessageContextAction( action = new ChatMessageContextAction(
FontAwesomeIcon.User,
"View Profile", "View Profile",
true, true,
() => RunContextAction(() => OpenStandardProfileAsync(user))); () => RunContextAction(() => OpenStandardProfileAsync(user)));
return true; return true;
} }
case ChatChannelType.Zone: case ChatChannelType.Zone:
if (!message.Payload.Sender.CanResolveProfile) if (!payload.Sender.CanResolveProfile)
return false; return false;
if (string.IsNullOrEmpty(message.Payload.Sender.Token)) var hashedCid = payload.Sender.HashedCid;
if (string.IsNullOrEmpty(hashedCid))
return false; return false;
action = new ChatMessageContextAction( action = new ChatMessageContextAction(
FontAwesomeIcon.User,
"View Profile", "View Profile",
true, true,
() => RunContextAction(() => OpenZoneParticipantProfileAsync(channel.Descriptor, message.Payload.Sender.Token))); () => RunContextAction(() => OpenLightfinderProfileInternalAsync(hashedCid)));
return true; return true;
default: default:
return false; 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; action = default;
if (message.FromSelf) if (message.FromSelf)
return false; 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; return false;
action = new ChatMessageContextAction( action = new ChatMessageContextAction(
FontAwesomeIcon.ExclamationTriangle,
"Report Message", "Report Message",
true, true,
() => OpenReportPopup(channel, message)); () => 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 (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, message.ChannelKey, StringComparison.Ordinal))
if (result is null)
{ {
Mediator.Publish(new NotificationMessage("Zone Chat", "Participant is no longer available.", NotificationType.Warning, TimeSpan.FromSeconds(3))); _scrollToBottom = true;
return;
} }
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) private async Task OpenLightfinderProfileInternalAsync(string hashedCid)
@@ -901,14 +946,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
Mediator.Publish(new OpenLightfinderProfileMessage(sanitizedUser, profile.Value.ProfileData, hashedCid)); 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<ChatChannelSnapshot> channels) private void EnsureSelectedChannel(IReadOnlyList<ChatChannelSnapshot> channels)
{ {
if (_selectedChannelKey is not null && channels.Any(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal))) 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); 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);
} }

View File

@@ -60,10 +60,10 @@ public partial class ApiController
await _lightlessHub.InvokeAsync(nameof(ReportChatMessage), request).ConfigureAwait(false); await _lightlessHub.InvokeAsync(nameof(ReportChatMessage), request).ConfigureAwait(false);
} }
public async Task<ChatParticipantResolveResultDto?> ResolveChatParticipant(ChatParticipantResolveRequestDto request) public async Task SetChatParticipantMute(ChatParticipantMuteRequestDto request)
{ {
if (!IsConnected || _lightlessHub is null) return null; if (!IsConnected || _lightlessHub is null) return;
return await _lightlessHub.InvokeAsync<ChatParticipantResolveResultDto?>(nameof(ResolveChatParticipant), request).ConfigureAwait(false); await _lightlessHub.InvokeAsync(nameof(SetChatParticipantMute), request).ConfigureAwait(false);
} }
public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null)