added chat report functionality and some other random stuff
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -122,7 +122,7 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool IsPluginEnabled()
|
||||
protected override bool IsPluginEnabled(IExposedPlugin plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -29,9 +29,12 @@ 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 int ReportReasonMaxLength = 500;
|
||||
private const int ReportContextMaxLength = 1000;
|
||||
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly ZoneChatService _zoneChatService;
|
||||
@@ -50,6 +53,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 +124,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
DrawConnectionControls();
|
||||
|
||||
var channels = _zoneChatService.GetChannelsSnapshot();
|
||||
DrawReportPopup();
|
||||
|
||||
if (channels.Count == 0)
|
||||
{
|
||||
@@ -482,6 +495,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 +741,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 +811,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);
|
||||
|
||||
Reference in New Issue
Block a user