This commit is contained in:
cake
2025-11-30 15:55:57 +01:00
11 changed files with 458 additions and 54 deletions

View File

@@ -142,12 +142,21 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
return;
}
var transientResources = resources.ToList();
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
List<string> newlyAddedGamePaths = resources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
foreach (var gamePath in transientResources)
List<string> transientResources;
lock (resources)
{
semiTransientResources.Add(gamePath);
transientResources = resources.ToList();
}
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
List<string> newlyAddedGamePaths;
lock (semiTransientResources)
{
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
foreach (var gamePath in transientResources)
{
semiTransientResources.Add(gamePath);
}
}
bool saveConfig = false;
@@ -180,7 +189,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_configurationService.Save();
}
TransientResources[objectKind].Clear();
lock (resources)
{
resources.Clear();
}
}
public void RemoveTransientResource(ObjectKind objectKind, string path)

View File

@@ -1,6 +1,7 @@
using Dalamud.Plugin;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Linq;
namespace LightlessSync.Interop.Ipc.Framework;
@@ -107,7 +108,9 @@ public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcSer
try
{
var plugin = PluginInterface.InstalledPlugins
.FirstOrDefault(p => string.Equals(p.InternalName, Descriptor.InternalName, StringComparison.OrdinalIgnoreCase));
.Where(p => string.Equals(p.InternalName, Descriptor.InternalName, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(p => p.IsLoaded)
.FirstOrDefault();
if (plugin == null)
{
@@ -119,7 +122,7 @@ public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcSer
return IpcConnectionState.VersionMismatch;
}
if (!IsPluginEnabled())
if (!IsPluginEnabled(plugin))
{
return IpcConnectionState.PluginDisabled;
}
@@ -138,8 +141,8 @@ public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcSer
}
}
protected virtual bool IsPluginEnabled()
=> true;
protected virtual bool IsPluginEnabled(IExposedPlugin plugin)
=> plugin.IsLoaded;
protected virtual bool IsPluginReady()
=> true;

View File

@@ -122,7 +122,7 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
}
}
protected override bool IsPluginEnabled()
protected override bool IsPluginEnabled(IExposedPlugin plugin)
{
try
{

View File

@@ -51,9 +51,14 @@ public sealed class PenumbraRedraw : PenumbraBase
return;
}
var redrawSemaphore = _redrawManager.RedrawSemaphore;
var semaphoreAcquired = false;
try
{
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
await redrawSemaphore.WaitAsync(token).ConfigureAwait(false);
semaphoreAcquired = true;
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, chara =>
{
logger.LogDebug("[{ApplicationId}] Calling on IPC: PenumbraRedraw", applicationId);
@@ -62,7 +67,10 @@ public sealed class PenumbraRedraw : PenumbraBase
}
finally
{
_redrawManager.RedrawSemaphore.Release();
if (semaphoreAcquired)
{
redrawSemaphore.Release();
}
}
}

View File

@@ -12,4 +12,5 @@ public sealed class ChatConfig : ILightlessConfiguration
public float ChatWindowOpacity { get; set; } = .97f;
public bool IsWindowPinned { get; set; } = false;
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
public float ChatFontScale { get; set; } = 1.0f;
}

View File

@@ -13,6 +13,8 @@ public class TransientConfig : ILightlessConfiguration
public Dictionary<uint, List<string>> JobSpecificCache { get; set; } = [];
public Dictionary<uint, List<string>> JobSpecificPetCache { get; set; } = [];
private readonly object _cacheLock = new();
public TransientPlayerConfig()
{
@@ -39,45 +41,51 @@ public class TransientConfig : ILightlessConfiguration
public int RemovePath(string gamePath, ObjectKind objectKind)
{
int removedEntries = 0;
if (objectKind == ObjectKind.Player)
lock (_cacheLock)
{
if (GlobalPersistentCache.Remove(gamePath)) removedEntries++;
foreach (var kvp in JobSpecificCache)
int removedEntries = 0;
if (objectKind == ObjectKind.Player)
{
if (kvp.Value.Remove(gamePath)) removedEntries++;
if (GlobalPersistentCache.Remove(gamePath)) removedEntries++;
foreach (var kvp in JobSpecificCache)
{
if (kvp.Value.Remove(gamePath)) removedEntries++;
}
}
}
if (objectKind == ObjectKind.Pet)
{
foreach (var kvp in JobSpecificPetCache)
if (objectKind == ObjectKind.Pet)
{
if (kvp.Value.Remove(gamePath)) removedEntries++;
foreach (var kvp in JobSpecificPetCache)
{
if (kvp.Value.Remove(gamePath)) removedEntries++;
}
}
return removedEntries;
}
return removedEntries;
}
public void AddOrElevate(uint jobId, string gamePath)
{
// check if it's in the global cache, if yes, do nothing
if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal))
lock (_cacheLock)
{
return;
}
// check if it's in the global cache, if yes, do nothing
if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal))
{
return;
}
if (ElevateIfNeeded(jobId, gamePath)) return;
if (ElevateIfNeeded(jobId, gamePath)) return;
// check if the jobid is already in the cache to start
if (!JobSpecificCache.TryGetValue(jobId, out var jobCache))
{
JobSpecificCache[jobId] = jobCache = new();
}
// check if the jobid is already in the cache to start
if (!JobSpecificCache.TryGetValue(jobId, out var jobCache))
{
JobSpecificCache[jobId] = jobCache = new();
}
// check if the path is already in the job specific cache
if (!jobCache.Contains(gamePath, StringComparer.Ordinal))
{
jobCache.Add(gamePath);
// check if the path is already in the job specific cache
if (!jobCache.Contains(gamePath, StringComparer.Ordinal))
{
jobCache.Add(gamePath);
}
}
}
}

View File

@@ -19,3 +19,5 @@ public readonly record struct ChatChannelSnapshot(
bool HasUnread,
int UnreadCount,
IReadOnlyList<ChatMessageEntry> Messages);
public readonly record struct ChatReportResult(bool Success, string? ErrorMessage);

View File

@@ -17,6 +17,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private const int MaxUnreadCount = 999;
private const string ZoneUnavailableMessage = "Zone chat is only available in major cities.";
private const string ZoneChannelKey = "zone";
private const int MaxReportReasonLength = 500;
private const int MaxReportContextLength = 1000;
private readonly ApiController _apiController;
private readonly ChatConfigService _chatConfigService;
@@ -244,6 +246,65 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
public Task<ChatParticipantResolveResultDto?> ResolveParticipantAsync(ChatChannelDescriptor descriptor, string token)
=> _apiController.ResolveChatParticipant(new ChatParticipantResolveRequestDto(descriptor, token));
public Task<ChatReportResult> ReportMessageAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext)
{
if (string.IsNullOrWhiteSpace(messageId))
{
return Task.FromResult(new ChatReportResult(false, "Unable to locate the selected message."));
}
var trimmedReason = reason?.Trim() ?? string.Empty;
if (trimmedReason.Length == 0)
{
return Task.FromResult(new ChatReportResult(false, "Please describe why you are reporting this message."));
}
lock (_sync)
{
if (!_chatEnabled)
{
return Task.FromResult(new ChatReportResult(false, "Enable chat before reporting messages."));
}
if (!_isConnected)
{
return Task.FromResult(new ChatReportResult(false, "Connect to the chat server before reporting messages."));
}
}
if (trimmedReason.Length > MaxReportReasonLength)
{
trimmedReason = trimmedReason[..MaxReportReasonLength];
}
string? context = null;
if (!string.IsNullOrWhiteSpace(additionalContext))
{
context = additionalContext.Trim();
if (context.Length > MaxReportContextLength)
{
context = context[..MaxReportContextLength];
}
}
var normalizedDescriptor = descriptor.WithNormalizedCustomKey();
return ReportMessageInternalAsync(normalizedDescriptor, messageId.Trim(), trimmedReason, context);
}
private async Task<ChatReportResult> ReportMessageInternalAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext)
{
try
{
await _apiController.ReportChatMessage(new ChatReportSubmitDto(descriptor, messageId, reason, additionalContext)).ConfigureAwait(false);
return new ChatReportResult(true, null);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to submit chat report");
return new ChatReportResult(false, "Failed to submit report. Please try again.");
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
Mediator.Subscribe<DalamudLoginMessage>(this, _ => HandleLogin());

View File

@@ -73,7 +73,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
else
{
_notifications.Add(notification);
_logger.LogDebug("Added new notification: {Title}", notification.Title);
_logger.LogTrace("Added new notification: {Title}", notification.Title);
}
if (!IsOpen) IsOpen = true;
@@ -93,7 +93,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
existing.CreatedAt = DateTime.UtcNow;
}
_logger.LogDebug("Updated existing notification: {Title}", updated.Title);
_logger.LogTrace("Updated existing notification: {Title}", updated.Title);
}
public void RemoveNotification(string id)
@@ -576,7 +576,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
{
var buttonWidth = CalculateActionButtonWidth(notification.Actions.Count, availableWidth);
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
_logger.LogTrace("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
notification.Actions.Count, buttonWidth, availableWidth);
var startX = ImGui.GetCursorPosX();
@@ -606,7 +606,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth)
{
_logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
_logger.LogTrace("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
var buttonColor = action.Color;
buttonColor.W *= alpha;
@@ -633,15 +633,15 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0));
}
_logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
_logger.LogTrace("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
if (buttonPressed)
{
try
{
_logger.LogDebug("Executing action: {ActionId}", action.Id);
_logger.LogTrace("Executing action: {ActionId}", action.Id);
action.OnClick(notification);
_logger.LogDebug("Action executed successfully: {ActionId}", action.Id);
_logger.LogTrace("Action executed successfully: {ActionId}", action.Id);
}
catch (Exception ex)
{

View File

@@ -15,6 +15,7 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Localization;
@@ -975,36 +976,36 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ImGui.SameLine(150);
ColorText("Penumbra", GetBoolColor(_penumbraExists));
AttachToolTip($"Penumbra is " + (_penumbraExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Penumbra", _penumbraExists, _ipcManager.Penumbra.State));
ImGui.SameLine();
ColorText("Glamourer", GetBoolColor(_glamourerExists));
AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Glamourer", _glamourerExists, _ipcManager.Glamourer.State));
ImGui.TextUnformatted("Optional Plugins:");
ImGui.SameLine(150);
ColorText("SimpleHeels", GetBoolColor(_heelsExists));
AttachToolTip($"SimpleHeels is " + (_heelsExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("SimpleHeels", _heelsExists, _ipcManager.Heels.State));
ImGui.SameLine();
ColorText("Customize+", GetBoolColor(_customizePlusExists));
AttachToolTip($"Customize+ is " + (_customizePlusExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Customize+", _customizePlusExists, _ipcManager.CustomizePlus.State));
ImGui.SameLine();
ColorText("Honorific", GetBoolColor(_honorificExists));
AttachToolTip($"Honorific is " + (_honorificExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Honorific", _honorificExists, _ipcManager.Honorific.State));
ImGui.SameLine();
ColorText("Moodles", GetBoolColor(_moodlesExists));
AttachToolTip($"Moodles is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Moodles", _moodlesExists, _ipcManager.Moodles.State));
ImGui.SameLine();
ColorText("PetNicknames", GetBoolColor(_petNamesExists));
AttachToolTip($"PetNicknames is " + (_petNamesExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("PetNicknames", _petNamesExists, _ipcManager.PetNames.State));
ImGui.SameLine();
ColorText("Brio", GetBoolColor(_brioExists));
AttachToolTip($"Brio is " + (_brioExists ? "available and up to date." : "unavailable or not up to date."));
AttachToolTip(BuildPluginTooltip("Brio", _brioExists, _ipcManager.Brio.State));
if (!_penumbraExists || !_glamourerExists)
{
@@ -1015,6 +1016,25 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return true;
}
private static string BuildPluginTooltip(string pluginName, bool isAvailable, IpcConnectionState state)
{
var availability = isAvailable ? "available and up to date." : "unavailable or not up to date.";
return $"{pluginName} is {availability}{Environment.NewLine}IPC State: {DescribeIpcState(state)}";
}
private static string DescribeIpcState(IpcConnectionState state)
=> state switch
{
IpcConnectionState.Unknown => "Not evaluated yet",
IpcConnectionState.MissingPlugin => "Plugin not installed",
IpcConnectionState.VersionMismatch => "Installed version below required minimum",
IpcConnectionState.PluginDisabled => "Plugin installed but disabled",
IpcConnectionState.NotReady => "Plugin is not ready yet",
IpcConnectionState.Available => "Available",
IpcConnectionState.Error => "Error occurred while checking IPC",
_ => state.ToString()
};
public int DrawServiceSelection(bool selectOnChange = false, bool showConnect = true)
{
string[] comboEntries = _serverConfigurationManager.GetServerNames();

View File

@@ -29,9 +29,14 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
{
private const string ChatDisabledStatus = "Chat services disabled";
private const string SettingsPopupId = "zone_chat_settings_popup";
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
private const float DefaultWindowOpacity = .97f;
private const float MinWindowOpacity = 0.05f;
private const float MaxWindowOpacity = 1f;
private const float MinChatFontScale = 0.75f;
private const float MaxChatFontScale = 1.5f;
private const int ReportReasonMaxLength = 500;
private const int ReportContextMaxLength = 1000;
private readonly UiSharedService _uiSharedService;
private readonly ZoneChatService _zoneChatService;
@@ -50,6 +55,15 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private float? _pendingChannelScroll;
private float _channelScroll;
private float _channelScrollMax;
private ChatChannelSnapshot? _reportTargetChannel;
private ChatMessageEntry? _reportTargetMessage;
private string _reportReason = string.Empty;
private string _reportAdditionalContext = string.Empty;
private bool _reportPopupOpen;
private bool _reportPopupRequested;
private bool _reportSubmitting;
private string? _reportError;
private ChatReportResult? _reportSubmissionResult;
public ZoneChatUi(
ILogger<ZoneChatUi> logger,
@@ -112,6 +126,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
DrawConnectionControls();
var channels = _zoneChatService.GetChannelsSnapshot();
DrawReportPopup();
if (channels.Count == 0)
{
@@ -202,6 +217,14 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
if (!child)
return;
var configuredFontScale = Math.Clamp(_chatConfigService.Current.ChatFontScale, MinChatFontScale, MaxChatFontScale);
var restoreFontScale = false;
if (Math.Abs(configuredFontScale - 1f) > 0.001f)
{
ImGui.SetWindowFontScale(configuredFontScale);
restoreFontScale = true;
}
var drawList = ImGui.GetWindowDrawList();
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
@@ -246,6 +269,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
{
var contextLocalTimestamp = message.Payload.SentAtUtc.ToLocalTime();
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
ImGui.TextDisabled(contextTimestampText);
ImGui.Separator();
foreach (var action in GetContextMenuActions(channel, message))
{
if (ImGui.MenuItem(action.Label, string.Empty, false, action.IsEnabled))
@@ -266,6 +294,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.SetScrollHereY(1f);
_scrollToBottom = false;
}
if (restoreFontScale)
{
ImGui.SetWindowFontScale(1f);
}
}
private void DrawInput(ChatChannelSnapshot channel)
@@ -443,7 +476,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"),
new SeStringUtils.RichTextEntry("Punishments scale from a permanent chat ban up to a full Lightless account ban."),
new SeStringUtils.RichTextEntry(" (Appeals are NOT possible.) ", UIColors.Get("DimRed"), true));
new SeStringUtils.RichTextEntry(" (Appeals are possible, but will be accepted only in clear cases of error.) ", UIColors.Get("DimRed"), true));
ImGui.PopTextWrapPos();
}
@@ -482,6 +515,221 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.PopStyleVar();
}
private void DrawReportPopup()
{
if (!_reportPopupOpen)
return;
var desiredPopupSize = new Vector2(520f * ImGuiHelpers.GlobalScale, 0f);
ImGui.SetNextWindowSize(desiredPopupSize, ImGuiCond.Always);
if (_reportPopupRequested)
{
ImGui.OpenPopup(ReportPopupId);
_reportPopupRequested = false;
}
else if (!ImGui.IsPopupOpen(ReportPopupId, ImGuiPopupFlags.AnyPopupLevel))
{
ImGui.OpenPopup(ReportPopupId);
}
var popupFlags = UiSharedService.PopupWindowFlags | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings;
if (!ImGui.BeginPopupModal(ReportPopupId, popupFlags))
return;
if (_reportTargetChannel is not { } channel || _reportTargetMessage is not { } message)
{
CloseReportPopup();
ImGui.EndPopup();
return;
}
if (_reportSubmissionResult is { } pendingResult)
{
_reportSubmissionResult = null;
_reportSubmitting = false;
if (pendingResult.Success)
{
Mediator.Publish(new NotificationMessage("Zone Chat", "Report submitted for moderator review.", NotificationType.Info, TimeSpan.FromSeconds(3)));
CloseReportPopup();
ImGui.EndPopup();
return;
}
_reportError = pendingResult.ErrorMessage ?? "Failed to submit report. Please try again.";
}
var channelPrefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell";
var channelLabel = $"{channelPrefix}: {channel.DisplayName}";
if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0)
{
channelLabel += $" (World #{channel.Descriptor.WorldId})";
}
ImGui.TextUnformatted(channelLabel);
ImGui.TextUnformatted($"Sender: {message.DisplayName}");
ImGui.TextUnformatted($"Sent: {message.Payload.SentAtUtc.ToLocalTime().ToString("g", CultureInfo.CurrentCulture)}");
ImGui.Separator();
ImGui.PushTextWrapPos(ImGui.GetWindowContentRegionMax().X);
ImGui.TextWrapped(message.Payload.Message);
ImGui.PopTextWrapPos();
ImGui.Separator();
ImGui.TextUnformatted("Reason (required)");
if (ImGui.InputTextMultiline("##chat_report_reason", ref _reportReason, ReportReasonMaxLength, new Vector2(-1, 80f * ImGuiHelpers.GlobalScale)))
{
if (_reportReason.Length > ReportReasonMaxLength)
{
_reportReason = _reportReason[..(int)ReportReasonMaxLength];
}
}
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
ImGui.TextUnformatted($"{_reportReason.Length}/{ReportReasonMaxLength}");
ImGui.PopStyleColor();
ImGui.Spacing();
ImGui.TextUnformatted("Additional context (optional)");
if (ImGui.InputTextMultiline("##chat_report_context", ref _reportAdditionalContext, ReportContextMaxLength, new Vector2(-1, 120f * ImGuiHelpers.GlobalScale)))
{
if (_reportAdditionalContext.Length > ReportContextMaxLength)
{
_reportAdditionalContext = _reportAdditionalContext[..(int)ReportContextMaxLength];
}
}
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
ImGui.TextUnformatted($"{_reportAdditionalContext.Length}/{ReportContextMaxLength}");
ImGui.PopStyleColor();
if (!string.IsNullOrEmpty(_reportError))
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.TextWrapped(_reportError);
ImGui.PopStyleColor();
}
if (_reportSubmitting)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
ImGui.TextUnformatted("Submitting report...");
ImGui.PopStyleColor();
}
ImGui.Separator();
var style = ImGui.GetStyle();
var availableWidth = Math.Max(0f, ImGui.GetContentRegionAvail().X);
var buttonWidth = Math.Max(100f * ImGuiHelpers.GlobalScale, (availableWidth - style.ItemSpacing.X) / 2f);
var canSubmit = !_reportSubmitting && _reportReason.Trim().Length > 0;
using (ImRaii.Disabled(!canSubmit))
{
if (ImGui.Button("Submit Report", new Vector2(buttonWidth, 0f)) && canSubmit)
{
BeginReportSubmission(channel, message);
}
}
ImGui.SameLine();
if (ImGui.Button("Cancel", new Vector2(buttonWidth, 0f)))
{
CloseReportPopup();
}
ImGui.EndPopup();
}
private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message)
{
_reportTargetChannel = channel;
_reportTargetMessage = message;
_logger.LogDebug("Opening report popup for channel {ChannelKey}, message {MessageId}", channel.Key, message.Payload.MessageId);
_reportReason = string.Empty;
_reportAdditionalContext = string.Empty;
_reportError = null;
_reportSubmissionResult = null;
_reportSubmitting = false;
_reportPopupOpen = true;
_reportPopupRequested = true;
}
private void BeginReportSubmission(ChatChannelSnapshot channel, ChatMessageEntry message)
{
if (_reportSubmitting)
return;
var trimmedReason = _reportReason.Trim();
if (trimmedReason.Length == 0)
{
_reportError = "Please describe the issue before submitting.";
return;
}
var trimmedContext = string.IsNullOrWhiteSpace(_reportAdditionalContext)
? null
: _reportAdditionalContext.Trim();
_reportSubmitting = true;
_reportError = null;
_reportSubmissionResult = null;
var descriptor = channel.Descriptor;
var messageId = message.Payload.MessageId;
if (string.IsNullOrWhiteSpace(messageId))
{
_reportSubmitting = false;
_reportError = "Unable to report this message.";
_logger.LogWarning("Report submission aborted: missing message id for channel {ChannelKey}", channel.Key);
return;
}
_logger.LogDebug("Submitting chat report for channel {ChannelKey}, message {MessageId}", channel.Key, messageId);
_ = Task.Run(async () =>
{
ChatReportResult result;
try
{
result = await _zoneChatService.ReportMessageAsync(descriptor, messageId, trimmedReason, trimmedContext).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to submit chat report");
result = new ChatReportResult(false, "Failed to submit report. Please try again.");
}
_reportSubmissionResult = result;
if (result.Success)
{
_logger.LogInformation("Chat report submitted successfully for channel {ChannelKey}, message {MessageId}", channel.Key, messageId);
}
else
{
_logger.LogWarning("Chat report submission failed for channel {ChannelKey}, message {MessageId}: {Error}", channel.Key, messageId, result.ErrorMessage);
}
});
}
private void CloseReportPopup()
{
_reportPopupOpen = false;
_reportPopupRequested = false;
ResetReportPopupState();
ImGui.CloseCurrentPopup();
}
private void ResetReportPopupState()
{
_reportTargetChannel = null;
_reportTargetMessage = null;
_reportReason = string.Empty;
_reportAdditionalContext = string.Empty;
_reportError = null;
_reportSubmissionResult = null;
_reportSubmitting = false;
_reportPopupRequested = false;
}
private bool TrySendDraft(ChatChannelSnapshot channel, string draft)
{
var trimmed = draft.Trim();
@@ -513,6 +761,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
{
yield return viewProfile;
}
if (TryCreateReportMessageAction(channel, message, out var reportAction))
{
yield return reportAction;
}
}
private bool TryCreateCopyMessageAction(ChatMessageEntry message, out ChatMessageContextAction action)
@@ -578,6 +831,23 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
}
}
private bool TryCreateReportMessageAction(ChatChannelSnapshot channel, ChatMessageEntry message, out ChatMessageContextAction action)
{
action = default;
if (message.FromSelf)
return false;
if (string.IsNullOrWhiteSpace(message.Payload.MessageId))
return false;
action = new ChatMessageContextAction(
"Report Message",
true,
() => OpenReportPopup(channel, message));
return true;
}
private Task OpenStandardProfileAsync(UserData user)
{
_profileManager.GetLightlessProfile(user);
@@ -867,6 +1137,25 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.SetTooltip("Toggles the timestamp prefix on messages.");
}
var fontScale = Math.Clamp(chatConfig.ChatFontScale, MinChatFontScale, MaxChatFontScale);
var fontScaleChanged = ImGui.SliderFloat("Message font scale", ref fontScale, MinChatFontScale, MaxChatFontScale, "%.2fx");
var resetFontScale = ImGui.IsItemClicked(ImGuiMouseButton.Right);
if (resetFontScale)
{
fontScale = 1.0f;
fontScaleChanged = true;
}
if (fontScaleChanged)
{
chatConfig.ChatFontScale = fontScale;
_chatConfigService.Save();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Adjusts the scale of chat message text.\nRight-click to reset.");
}
var windowOpacity = Math.Clamp(chatConfig.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
var opacityChanged = ImGui.SliderFloat("Window transparency", ref windowOpacity, MinWindowOpacity, MaxWindowOpacity, "%.2f");
var resetOpacity = ImGui.IsItemClicked(ImGuiMouseButton.Right);