1519 lines
59 KiB
C#
1519 lines
59 KiB
C#
using System.Globalization;
|
|
using System.Numerics;
|
|
using LightlessSync.API.Data;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Colors;
|
|
using Dalamud.Interface.Utility;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using LightlessSync.API.Dto.Chat;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.LightlessConfiguration.Models;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.Chat;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.UI.Services;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using LightlessSync.WebAPI.SignalR.Utils;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LightlessSync.UI;
|
|
|
|
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;
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly LightlessProfileManager _profileManager;
|
|
private readonly ApiController _apiController;
|
|
private readonly ChatConfigService _chatConfigService;
|
|
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
|
|
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
|
|
private float _currentWindowOpacity = DefaultWindowOpacity;
|
|
private bool _isWindowPinned;
|
|
private bool _showRulesOverlay;
|
|
private bool _refocusChatInput;
|
|
private string? _refocusChatInputKey;
|
|
|
|
private string? _selectedChannelKey;
|
|
private bool _scrollToBottom = true;
|
|
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,
|
|
LightlessMediator mediator,
|
|
UiSharedService uiSharedService,
|
|
ZoneChatService zoneChatService,
|
|
PairUiService pairUiService,
|
|
LightlessProfileManager profileManager,
|
|
ChatConfigService chatConfigService,
|
|
ApiController apiController,
|
|
PerformanceCollectorService performanceCollectorService)
|
|
: base(logger, mediator, "Lightless Chat", performanceCollectorService)
|
|
{
|
|
_uiSharedService = uiSharedService;
|
|
_zoneChatService = zoneChatService;
|
|
_pairUiService = pairUiService;
|
|
_profileManager = profileManager;
|
|
_chatConfigService = chatConfigService;
|
|
_apiController = apiController;
|
|
_isWindowPinned = _chatConfigService.Current.IsWindowPinned;
|
|
_showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen;
|
|
if (_chatConfigService.Current.AutoOpenChatOnPluginLoad)
|
|
{
|
|
IsOpen = true;
|
|
}
|
|
_unpinnedWindowFlags = Flags;
|
|
RefreshWindowFlags();
|
|
Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale;
|
|
SizeCondition = ImGuiCond.FirstUseEver;
|
|
WindowBuilder.For(this)
|
|
.SetSizeConstraints(
|
|
new Vector2(320f, 260f) * ImGuiHelpers.GlobalScale,
|
|
new Vector2(900f, 900f) * ImGuiHelpers.GlobalScale)
|
|
.Apply();
|
|
|
|
Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded);
|
|
Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true);
|
|
}
|
|
|
|
public override void PreDraw()
|
|
{
|
|
RefreshWindowFlags();
|
|
base.PreDraw();
|
|
_currentWindowOpacity = Math.Clamp(_chatConfigService.Current.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
|
ImGui.SetNextWindowBgAlpha(_currentWindowOpacity);
|
|
}
|
|
|
|
protected override void DrawInternal()
|
|
{
|
|
var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg];
|
|
childBgColor.W *= _currentWindowOpacity;
|
|
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor);
|
|
DrawConnectionControls();
|
|
|
|
var channels = _zoneChatService.GetChannelsSnapshot();
|
|
DrawReportPopup();
|
|
|
|
if (channels.Count == 0)
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
|
ImGui.TextWrapped("No chat channels available.");
|
|
ImGui.PopStyleColor();
|
|
return;
|
|
}
|
|
|
|
EnsureSelectedChannel(channels);
|
|
CleanupDrafts(channels);
|
|
|
|
DrawChannelButtons(channels);
|
|
|
|
if (_selectedChannelKey is null)
|
|
return;
|
|
|
|
var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
|
|
if (activeChannel.Equals(default(ChatChannelSnapshot)))
|
|
{
|
|
activeChannel = channels[0];
|
|
_selectedChannelKey = activeChannel.Key;
|
|
}
|
|
|
|
_zoneChatService.SetActiveChannel(activeChannel.Key);
|
|
|
|
DrawHeader(activeChannel);
|
|
ImGui.Separator();
|
|
DrawMessageArea(activeChannel, _currentWindowOpacity);
|
|
ImGui.Separator();
|
|
DrawInput(activeChannel);
|
|
|
|
if (_showRulesOverlay)
|
|
{
|
|
DrawRulesOverlay();
|
|
}
|
|
}
|
|
|
|
private static void DrawHeader(ChatChannelSnapshot channel)
|
|
{
|
|
var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell";
|
|
Vector4 color;
|
|
|
|
if (!channel.IsConnected)
|
|
{
|
|
color = UIColors.Get("DimRed");
|
|
}
|
|
else if (!channel.IsAvailable)
|
|
{
|
|
color = ImGuiColors.DalamudGrey3;
|
|
}
|
|
else
|
|
{
|
|
color = channel.Type == ChatChannelType.Zone ? UIColors.Get("LightlessPurple") : UIColors.Get("LightlessBlue");
|
|
}
|
|
|
|
ImGui.TextColored(color, $"{prefix}: {channel.DisplayName}");
|
|
|
|
if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0)
|
|
{
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted($"World #{channel.Descriptor.WorldId}");
|
|
}
|
|
|
|
var showInlineDisabled = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase);
|
|
if (showInlineDisabled)
|
|
{
|
|
ImGui.SameLine();
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
|
ImGui.TextUnformatted($"| {channel.StatusText}");
|
|
ImGui.PopStyleColor();
|
|
}
|
|
else if (!string.IsNullOrEmpty(channel.StatusText))
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
|
ImGui.TextWrapped(channel.StatusText);
|
|
ImGui.PopStyleColor();
|
|
}
|
|
}
|
|
|
|
private void DrawMessageArea(ChatChannelSnapshot channel, float windowOpacity)
|
|
{
|
|
var available = ImGui.GetContentRegionAvail();
|
|
var inputHeight = ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y;
|
|
var height = Math.Max(100f * ImGuiHelpers.GlobalScale, available.Y - inputHeight);
|
|
|
|
using var child = ImRaii.Child($"chat_messages_{channel.Key}", new Vector2(-1, height), false);
|
|
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();
|
|
var gradientBottom = UIColors.Get("LightlessPurple");
|
|
var bottomAlpha = 0.12f * windowOpacity;
|
|
var bottomColorVec = new Vector4(gradientBottom.X, gradientBottom.Y, gradientBottom.Z, bottomAlpha);
|
|
var topColorVec = new Vector4(gradientBottom.X, gradientBottom.Y, gradientBottom.Z, 0.0f);
|
|
var bottomColor = ImGui.ColorConvertFloat4ToU32(bottomColorVec);
|
|
var topColor = ImGui.ColorConvertFloat4ToU32(topColorVec);
|
|
drawList.AddRectFilledMultiColor(
|
|
windowPos,
|
|
windowPos + windowSize,
|
|
topColor,
|
|
topColor,
|
|
bottomColor,
|
|
bottomColor);
|
|
|
|
var showTimestamps = _chatConfigService.Current.ShowMessageTimestamps;
|
|
|
|
if (channel.Messages.Count == 0)
|
|
{
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
|
ImGui.TextWrapped("Chat history will appear here when available.");
|
|
ImGui.PopStyleColor();
|
|
}
|
|
else
|
|
{
|
|
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)
|
|
{
|
|
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
|
}
|
|
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
|
|
|
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
|
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
|
|
ImGui.PopStyleColor();
|
|
|
|
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
|
{
|
|
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))
|
|
{
|
|
DrawContextMenuAction(action, actionIndex++);
|
|
}
|
|
|
|
ImGui.EndPopup();
|
|
}
|
|
|
|
ImGui.PopID();
|
|
}
|
|
}
|
|
|
|
if (_scrollToBottom)
|
|
{
|
|
ImGui.SetScrollHereY(1f);
|
|
_scrollToBottom = false;
|
|
}
|
|
|
|
if (restoreFontScale)
|
|
{
|
|
ImGui.SetWindowFontScale(1f);
|
|
}
|
|
}
|
|
|
|
private void DrawInput(ChatChannelSnapshot channel)
|
|
{
|
|
const int MaxMessageLength = ZoneChatService.MaxOutgoingLength;
|
|
var canSend = channel.IsConnected && channel.IsAvailable;
|
|
_draftMessages.TryGetValue(channel.Key, out var draft);
|
|
draft ??= string.Empty;
|
|
|
|
var style = ImGui.GetStyle();
|
|
var sendButtonWidth = 100f * ImGuiHelpers.GlobalScale;
|
|
var counterWidth = ImGui.CalcTextSize($"{MaxMessageLength}/{MaxMessageLength}").X;
|
|
var reservedWidth = sendButtonWidth + counterWidth + style.ItemSpacing.X * 2f;
|
|
|
|
ImGui.SetNextItemWidth(-reservedWidth);
|
|
var inputId = $"##chat-input-{channel.Key}";
|
|
if (_refocusChatInput && string.Equals(_refocusChatInputKey, channel.Key, StringComparison.Ordinal))
|
|
{
|
|
ImGui.SetKeyboardFocusHere();
|
|
_refocusChatInput = false;
|
|
_refocusChatInputKey = null;
|
|
}
|
|
ImGui.InputText(inputId, ref draft, MaxMessageLength);
|
|
var enterPressed = ImGui.IsItemFocused()
|
|
&& (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter));
|
|
_draftMessages[channel.Key] = draft;
|
|
|
|
ImGui.SameLine();
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
|
ImGui.TextUnformatted($"{draft.Length}/{MaxMessageLength}");
|
|
ImGui.PopStyleColor();
|
|
|
|
ImGui.SameLine();
|
|
var buttonScreenPos = ImGui.GetCursorScreenPos();
|
|
var rightEdgeScreen = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X;
|
|
var desiredButtonX = rightEdgeScreen - sendButtonWidth;
|
|
var minButtonX = buttonScreenPos.X + style.ItemSpacing.X;
|
|
var finalButtonX = MathF.Max(minButtonX, desiredButtonX);
|
|
ImGui.SetCursorScreenPos(new Vector2(finalButtonX, buttonScreenPos.Y));
|
|
var sendColor = UIColors.Get("LightlessPurpleDefault");
|
|
var sendHovered = UIColors.Get("LightlessPurple");
|
|
var sendActive = UIColors.Get("LightlessPurpleActive");
|
|
ImGui.PushStyleColor(ImGuiCol.Button, sendColor);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, sendHovered);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, sendActive);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 6f * ImGuiHelpers.GlobalScale);
|
|
var sendClicked = false;
|
|
using (ImRaii.Disabled(!canSend))
|
|
{
|
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PaperPlane, $"Send##chat-send-{channel.Key}", 100f * ImGuiHelpers.GlobalScale, center: true))
|
|
{
|
|
sendClicked = true;
|
|
}
|
|
}
|
|
ImGui.PopStyleVar();
|
|
ImGui.PopStyleColor(3);
|
|
|
|
if (canSend && (enterPressed || sendClicked))
|
|
{
|
|
_refocusChatInput = true;
|
|
_refocusChatInputKey = channel.Key;
|
|
if (TrySendDraft(channel, draft))
|
|
{
|
|
_draftMessages[channel.Key] = string.Empty;
|
|
_scrollToBottom = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawRulesOverlay()
|
|
{
|
|
var windowPos = ImGui.GetWindowPos();
|
|
var windowSize = ImGui.GetWindowSize();
|
|
var parentContentMin = ImGui.GetWindowContentRegionMin();
|
|
var parentContentMax = ImGui.GetWindowContentRegionMax();
|
|
var overlayPos = windowPos + parentContentMin;
|
|
var overlaySize = parentContentMax - parentContentMin;
|
|
|
|
if (overlaySize.X <= 0f || overlaySize.Y <= 0f)
|
|
{
|
|
overlayPos = windowPos;
|
|
overlaySize = windowSize;
|
|
}
|
|
|
|
ImGui.SetNextWindowFocus();
|
|
ImGui.SetNextWindowPos(overlayPos);
|
|
ImGui.SetNextWindowSize(overlaySize);
|
|
ImGui.SetNextWindowBgAlpha(0.86f);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 6f * ImGuiHelpers.GlobalScale);
|
|
ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero);
|
|
|
|
var overlayFlags = ImGuiWindowFlags.NoDecoration
|
|
| ImGuiWindowFlags.NoMove
|
|
| ImGuiWindowFlags.NoScrollbar
|
|
| ImGuiWindowFlags.NoSavedSettings;
|
|
|
|
var overlayOpen = true;
|
|
if (ImGui.Begin("##zone_chat_rules_overlay", ref overlayOpen, overlayFlags))
|
|
{
|
|
var contentMin = ImGui.GetWindowContentRegionMin();
|
|
var contentMax = ImGui.GetWindowContentRegionMax();
|
|
var contentWidth = contentMax.X - contentMin.X;
|
|
var title = "Chat Rules";
|
|
var titleWidth = ImGui.CalcTextSize(title).X;
|
|
|
|
ImGui.SetCursorPosX(contentMin.X + Math.Max(0f, (contentWidth - titleWidth) * 0.5f));
|
|
ImGui.TextUnformatted(title);
|
|
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
var style = ImGui.GetStyle();
|
|
var buttonWidth = 180f * ImGuiHelpers.GlobalScale;
|
|
var buttonHeight = ImGui.GetFrameHeight();
|
|
var buttonSpacing = Math.Max(0f, style.ItemSpacing.Y);
|
|
var buttonTopY = Math.Max(contentMin.Y, contentMax.Y - buttonHeight);
|
|
var childHeight = Math.Max(0f, buttonTopY - buttonSpacing - ImGui.GetCursorPosY());
|
|
|
|
using (var child = ImRaii.Child("zone_chat_rules_overlay_scroll", new Vector2(-1f, childHeight), false))
|
|
{
|
|
if (child)
|
|
{
|
|
var childContentMin = ImGui.GetWindowContentRegionMin();
|
|
var childContentMax = ImGui.GetWindowContentRegionMax();
|
|
var childContentWidth = childContentMax.X - childContentMin.X;
|
|
|
|
ImGui.PushTextWrapPos(childContentMin.X + childContentWidth);
|
|
|
|
_uiSharedService.MediumText("Basic Chat Rules", UIColors.Get("LightlessBlue"));
|
|
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
|
new SeStringUtils.RichTextEntry("Do "),
|
|
new SeStringUtils.RichTextEntry("NOT share", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry(" confidential, personal, or account information-yours or anyone else's", UIColors.Get("LightlessYellow"), true),
|
|
new SeStringUtils.RichTextEntry("."));
|
|
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
|
new SeStringUtils.RichTextEntry("Respect ALL participants; "),
|
|
new SeStringUtils.RichTextEntry("no harassment, hate speech, or personal attacks", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry("."));
|
|
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
|
new SeStringUtils.RichTextEntry("AVOID ", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry("disruptive behaviors such as "),
|
|
new SeStringUtils.RichTextEntry("spamming or flooding", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry("."));
|
|
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
|
new SeStringUtils.RichTextEntry("Absolutely "),
|
|
new SeStringUtils.RichTextEntry("NO discussion, sharing, or solicitation of illegal content or activities;", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry("."));
|
|
|
|
ImGui.Dummy(new Vector2(5));
|
|
|
|
ImGui.Separator();
|
|
_uiSharedService.MediumText("Zone Chat Rules", UIColors.Get("LightlessGreen"));
|
|
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
|
new SeStringUtils.RichTextEntry("NO ADVERTISEMENTS", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry(" whatsoever for any kind of "),
|
|
new SeStringUtils.RichTextEntry("services, venues, clubs, marketboard deals, or similar offerings", UIColors.Get("LightlessYellow"), true),
|
|
new SeStringUtils.RichTextEntry("."));
|
|
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
|
new SeStringUtils.RichTextEntry("Mature", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry(" or "),
|
|
new SeStringUtils.RichTextEntry("NSFW", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry(" content "),
|
|
new SeStringUtils.RichTextEntry("(including suggestive emotes, explicit innuendo, or roleplay (including requests) )", UIColors.Get("LightlessYellow"), true),
|
|
new SeStringUtils.RichTextEntry(" is"),
|
|
new SeStringUtils.RichTextEntry(" strictly prohibited ", UIColors.Get("DimRed"), true),
|
|
new SeStringUtils.RichTextEntry("in Zone Chat."));
|
|
|
|
ImGui.Dummy(new Vector2(5));
|
|
|
|
ImGui.Separator();
|
|
_uiSharedService.MediumText("Syncshell Chat Rules", UIColors.Get("LightlessYellow"));
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("Syncshell chats are self-moderated (their own set rules) by it's owner and appointed moderators. If they fail to enforce chat rules within their syncshell, the owner (and its moderators) may face punishment."));
|
|
|
|
ImGui.Dummy(new Vector2(5));
|
|
|
|
ImGui.Separator();
|
|
_uiSharedService.MediumText("Reporting & Punishments", UIColors.Get("LightlessBlue"));
|
|
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"),
|
|
new SeStringUtils.RichTextEntry("Report rule-breakers by right clicking on the sent message and clicking report or via the mod-mail channel on the Discord."),
|
|
new SeStringUtils.RichTextEntry(" (False reports may be punished.) ", UIColors.Get("DimRed"), true));
|
|
|
|
_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 possible, but will be accepted only in clear cases of error.) ", UIColors.Get("DimRed"), true));
|
|
|
|
ImGui.PopTextWrapPos();
|
|
}
|
|
}
|
|
|
|
var spacingStartY = Math.Max(ImGui.GetCursorPosY(), buttonTopY - buttonSpacing);
|
|
ImGui.SetCursorPosY(spacingStartY);
|
|
|
|
if (buttonSpacing > 0f)
|
|
{
|
|
var actualSpacing = Math.Max(0f, buttonTopY - spacingStartY);
|
|
if (actualSpacing > 0f)
|
|
{
|
|
ImGui.Dummy(new Vector2(0f, actualSpacing));
|
|
}
|
|
}
|
|
|
|
ImGui.SetCursorPosY(buttonTopY);
|
|
|
|
var buttonX = contentMin.X + Math.Max(0f, (contentWidth - buttonWidth) * 0.5f);
|
|
ImGui.SetCursorPosX(buttonX);
|
|
|
|
if (ImGui.Button("I Understand", new Vector2(buttonWidth, buttonHeight)))
|
|
{
|
|
_showRulesOverlay = false;
|
|
}
|
|
|
|
if (!overlayOpen)
|
|
{
|
|
_showRulesOverlay = false;
|
|
}
|
|
}
|
|
|
|
ImGui.End();
|
|
ImGui.PopStyleColor();
|
|
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 (message.Payload is not { } payload)
|
|
{
|
|
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: {payload.SentAtUtc.ToLocalTime().ToString("g", CultureInfo.CurrentCulture)}");
|
|
|
|
ImGui.Separator();
|
|
ImGui.PushTextWrapPos(ImGui.GetWindowContentRegionMax().X);
|
|
ImGui.TextWrapped(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))
|
|
&& _reportReason.Length > ReportReasonMaxLength)
|
|
{
|
|
_reportReason = _reportReason[..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))
|
|
&& _reportAdditionalContext.Length > ReportContextMaxLength)
|
|
{
|
|
_reportAdditionalContext = _reportAdditionalContext[..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)
|
|
{
|
|
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, 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;
|
|
|
|
if (message.Payload is not { } payload)
|
|
{
|
|
_reportError = "Unable to report this message.";
|
|
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 = 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();
|
|
if (trimmed.Length == 0)
|
|
return false;
|
|
|
|
bool succeeded;
|
|
try
|
|
{
|
|
succeeded = _zoneChatService.SendMessageAsync(channel.Descriptor, trimmed).GetAwaiter().GetResult();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to send chat message");
|
|
succeeded = false;
|
|
}
|
|
|
|
return succeeded;
|
|
}
|
|
|
|
private IEnumerable<ChatMessageContextAction> GetContextMenuActions(ChatChannelSnapshot channel, ChatMessageEntry message)
|
|
{
|
|
if (message.IsSystem || message.Payload is not { } payload)
|
|
yield break;
|
|
|
|
if (TryCreateCopyMessageAction(message, payload, out var copyAction))
|
|
{
|
|
yield return copyAction;
|
|
}
|
|
|
|
if (TryCreateViewProfileAction(channel, message, payload, out var viewProfile))
|
|
{
|
|
yield return viewProfile;
|
|
}
|
|
|
|
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, ChatMessageDto payload, out ChatMessageContextAction action)
|
|
{
|
|
var text = payload.Message;
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
action = default;
|
|
return false;
|
|
}
|
|
|
|
action = new ChatMessageContextAction(
|
|
FontAwesomeIcon.Clipboard,
|
|
"Copy Message",
|
|
true,
|
|
() => ImGui.SetClipboardText(text));
|
|
return true;
|
|
}
|
|
|
|
private bool TryCreateViewProfileAction(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action)
|
|
{
|
|
action = default;
|
|
switch (channel.Type)
|
|
{
|
|
case ChatChannelType.Group:
|
|
{
|
|
var user = payload.Sender.User;
|
|
if (user?.UID is not { Length: > 0 })
|
|
return false;
|
|
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
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)));
|
|
return true;
|
|
}
|
|
|
|
action = new ChatMessageContextAction(
|
|
FontAwesomeIcon.User,
|
|
"View Profile",
|
|
true,
|
|
() => RunContextAction(() => OpenStandardProfileAsync(user)));
|
|
return true;
|
|
|
|
}
|
|
case ChatChannelType.Zone:
|
|
if (!payload.Sender.CanResolveProfile)
|
|
return false;
|
|
|
|
var hashedCid = payload.Sender.HashedCid;
|
|
if (string.IsNullOrEmpty(hashedCid))
|
|
return false;
|
|
|
|
action = new ChatMessageContextAction(
|
|
FontAwesomeIcon.User,
|
|
"View Profile",
|
|
true,
|
|
() => RunContextAction(() => OpenLightfinderProfileInternalAsync(hashedCid)));
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool TryCreateMuteParticipantAction(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action)
|
|
{
|
|
action = default;
|
|
if (message.FromSelf)
|
|
return false;
|
|
|
|
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));
|
|
|
|
return true;
|
|
}
|
|
|
|
private Task OpenStandardProfileAsync(UserData user)
|
|
{
|
|
_profileManager.GetLightlessProfile(user);
|
|
_profileManager.GetLightlessUserProfile(user);
|
|
Mediator.Publish(new OpenUserProfileMessage(user));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private void RunContextAction(Func<Task> action)
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await action().ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Chat context action failed");
|
|
Mediator.Publish(new NotificationMessage("Zone Chat", "Action failed to complete.", NotificationType.Error, TimeSpan.FromSeconds(3)));
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnChatChannelMessageAdded(ChatChannelMessageAdded message)
|
|
{
|
|
if (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, message.ChannelKey, StringComparison.Ordinal))
|
|
{
|
|
_scrollToBottom = true;
|
|
}
|
|
}
|
|
|
|
private async Task OpenLightfinderProfileInternalAsync(string hashedCid)
|
|
{
|
|
var profile = await _profileManager.GetLightfinderProfileAsync(hashedCid).ConfigureAwait(false);
|
|
if (profile is null)
|
|
{
|
|
Mediator.Publish(new NotificationMessage("Zone Chat", "Unable to load Lightfinder profile information.", NotificationType.Warning, TimeSpan.FromSeconds(3)));
|
|
return;
|
|
}
|
|
|
|
var sanitizedUser = profile.Value.User with
|
|
{
|
|
UID = "Lightfinder User",
|
|
Alias = "Lightfinder User"
|
|
};
|
|
|
|
Mediator.Publish(new OpenLightfinderProfileMessage(sanitizedUser, profile.Value.ProfileData, hashedCid));
|
|
}
|
|
|
|
private void EnsureSelectedChannel(IReadOnlyList<ChatChannelSnapshot> channels)
|
|
{
|
|
if (_selectedChannelKey is not null && channels.Any(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal)))
|
|
return;
|
|
|
|
_selectedChannelKey = channels.Count > 0 ? channels[0].Key : null;
|
|
if (_selectedChannelKey is not null)
|
|
{
|
|
_zoneChatService.SetActiveChannel(_selectedChannelKey);
|
|
_scrollToBottom = true;
|
|
}
|
|
}
|
|
|
|
private void CleanupDrafts(IReadOnlyList<ChatChannelSnapshot> channels)
|
|
{
|
|
var existingKeys = new HashSet<string>(channels.Select(c => c.Key), StringComparer.Ordinal);
|
|
foreach (var key in _draftMessages.Keys.ToList())
|
|
{
|
|
if (!existingKeys.Contains(key))
|
|
{
|
|
_draftMessages.Remove(key);
|
|
}
|
|
}
|
|
|
|
if (_refocusChatInputKey is not null && !existingKeys.Contains(_refocusChatInputKey))
|
|
{
|
|
_refocusChatInputKey = null;
|
|
_refocusChatInput = false;
|
|
}
|
|
}
|
|
|
|
private void DrawConnectionControls()
|
|
{
|
|
var hubState = _apiController.ServerState;
|
|
var chatEnabled = _zoneChatService.IsChatEnabled;
|
|
var chatConnected = _zoneChatService.IsChatConnected;
|
|
var buttonLabel = chatEnabled ? "Disable Chat" : "Enable Chat";
|
|
var style = ImGui.GetStyle();
|
|
var cursorStart = ImGui.GetCursorPos();
|
|
var contentRightX = cursorStart.X + ImGui.GetContentRegionAvail().X;
|
|
var rulesButtonWidth = 90f * ImGuiHelpers.GlobalScale;
|
|
|
|
using (ImRaii.Group())
|
|
{
|
|
if (ImGui.Button(buttonLabel, new Vector2(130f * ImGuiHelpers.GlobalScale, 0f)))
|
|
{
|
|
ToggleChatConnection(chatEnabled);
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
var chatStatusText = chatEnabled
|
|
? (chatConnected ? "Chat: Connected" : "Chat: Waiting")
|
|
: "Chat: Disabled";
|
|
var statusColor = chatEnabled
|
|
? (chatConnected ? UIColors.Get("LightlessGreen") : UIColors.Get("LightlessYellow"))
|
|
: ImGuiColors.DalamudGrey3;
|
|
ImGui.PushStyleColor(ImGuiCol.Text, statusColor);
|
|
ImGui.TextUnformatted(chatStatusText);
|
|
ImGui.PopStyleColor();
|
|
|
|
if (!string.IsNullOrWhiteSpace(_apiController.AuthFailureMessage) && hubState == ServerState.Unauthorized)
|
|
{
|
|
ImGui.SameLine();
|
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
|
ImGui.TextUnformatted(_apiController.AuthFailureMessage);
|
|
ImGui.PopStyleColor();
|
|
}
|
|
}
|
|
|
|
var groupSize = ImGui.GetItemRectSize();
|
|
var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X;
|
|
var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X);
|
|
var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X;
|
|
var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock;
|
|
var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X;
|
|
var blockWidth = rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth;
|
|
var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X
|
|
? contentRightX - blockWidth
|
|
: minBlockX;
|
|
desiredBlockX = Math.Max(cursorStart.X, desiredBlockX);
|
|
var rulesPos = new Vector2(desiredBlockX, cursorStart.Y);
|
|
var settingsPos = new Vector2(desiredBlockX + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
|
var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
|
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPos(rulesPos);
|
|
if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f)))
|
|
{
|
|
_showRulesOverlay = true;
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
ImGui.SetTooltip("Show chat rules");
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPos(settingsPos);
|
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Cog))
|
|
{
|
|
ImGui.OpenPopup(SettingsPopupId);
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
ImGui.SetTooltip("Chat settings");
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
ImGui.SetCursorPos(pinPos);
|
|
using (ImRaii.PushId("window_pin_button"))
|
|
{
|
|
var restorePinColors = false;
|
|
if (_isWindowPinned)
|
|
{
|
|
var pinBase = UIColors.Get("LightlessPurpleDefault");
|
|
var pinHover = UIColors.Get("LightlessPurple").WithAlpha(0.9f);
|
|
var pinActive = UIColors.Get("LightlessPurpleActive");
|
|
ImGui.PushStyleColor(ImGuiCol.Button, pinBase);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, pinHover);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, pinActive);
|
|
restorePinColors = true;
|
|
}
|
|
|
|
var pinClicked = _uiSharedService.IconButton(pinIcon);
|
|
|
|
if (restorePinColors)
|
|
{
|
|
ImGui.PopStyleColor(3);
|
|
}
|
|
|
|
if (pinClicked)
|
|
{
|
|
ToggleWindowPinned();
|
|
}
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
ImGui.SetTooltip(_isWindowPinned ? "Unpin window" : "Pin window");
|
|
}
|
|
|
|
DrawChatSettingsPopup();
|
|
|
|
ImGui.Separator();
|
|
}
|
|
|
|
private void ToggleWindowPinned()
|
|
{
|
|
_isWindowPinned = !_isWindowPinned;
|
|
_chatConfigService.Current.IsWindowPinned = _isWindowPinned;
|
|
_chatConfigService.Save();
|
|
RefreshWindowFlags();
|
|
}
|
|
|
|
private void RefreshWindowFlags()
|
|
{
|
|
Flags = _unpinnedWindowFlags & ~ImGuiWindowFlags.NoCollapse;
|
|
if (_isWindowPinned)
|
|
{
|
|
Flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
|
|
}
|
|
}
|
|
|
|
private void DrawChatSettingsPopup()
|
|
{
|
|
const ImGuiWindowFlags popupFlags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings;
|
|
if (!ImGui.BeginPopup(SettingsPopupId, popupFlags))
|
|
return;
|
|
|
|
ImGui.TextUnformatted("Chat Settings");
|
|
ImGui.Separator();
|
|
|
|
var chatConfig = _chatConfigService.Current;
|
|
|
|
var autoEnable = chatConfig.AutoEnableChatOnLogin;
|
|
if (ImGui.Checkbox("Auto-enable chat on login", ref autoEnable))
|
|
{
|
|
chatConfig.AutoEnableChatOnLogin = autoEnable;
|
|
_chatConfigService.Save();
|
|
if (autoEnable && !_zoneChatService.IsChatEnabled)
|
|
{
|
|
ToggleChatConnection(currentlyEnabled: false);
|
|
}
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
ImGui.SetTooltip("Automatically connect to chat whenever Lightless loads.");
|
|
}
|
|
|
|
var autoOpen = chatConfig.AutoOpenChatOnPluginLoad;
|
|
if (ImGui.Checkbox("Auto-open chat window on plugin load", ref autoOpen))
|
|
{
|
|
chatConfig.AutoOpenChatOnPluginLoad = autoOpen;
|
|
_chatConfigService.Save();
|
|
if (autoOpen)
|
|
{
|
|
IsOpen = true;
|
|
}
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
ImGui.SetTooltip("Opens the chat window automatically whenever the plugin loads.");
|
|
}
|
|
|
|
var showRules = chatConfig.ShowRulesOverlayOnOpen;
|
|
if (ImGui.Checkbox("Show rules overlay on open", ref showRules))
|
|
{
|
|
chatConfig.ShowRulesOverlayOnOpen = showRules;
|
|
_chatConfigService.Save();
|
|
_showRulesOverlay = showRules;
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
ImGui.SetTooltip("Toggles if the rules popup appears everytime the chat is opened for the first time.");
|
|
}
|
|
|
|
var showTimestamps = chatConfig.ShowMessageTimestamps;
|
|
if (ImGui.Checkbox("Show message timestamps", ref showTimestamps))
|
|
{
|
|
chatConfig.ShowMessageTimestamps = showTimestamps;
|
|
_chatConfigService.Save();
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
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("Adjust scale of chat message text.\nRight-click to reset to default.");
|
|
}
|
|
|
|
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);
|
|
if (resetOpacity)
|
|
{
|
|
windowOpacity = DefaultWindowOpacity;
|
|
opacityChanged = true;
|
|
}
|
|
|
|
if (opacityChanged)
|
|
{
|
|
chatConfig.ChatWindowOpacity = windowOpacity;
|
|
_chatConfigService.Save();
|
|
}
|
|
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default.");
|
|
}
|
|
|
|
ImGui.EndPopup();
|
|
}
|
|
|
|
private void ToggleChatConnection(bool currentlyEnabled)
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await _zoneChatService.SetChatEnabledAsync(!currentlyEnabled).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to toggle chat connection");
|
|
}
|
|
});
|
|
}
|
|
|
|
private void DrawChannelButtons(IReadOnlyList<ChatChannelSnapshot> channels)
|
|
{
|
|
var style = ImGui.GetStyle();
|
|
var baseFramePadding = style.FramePadding;
|
|
var available = ImGui.GetContentRegionAvail().X;
|
|
var buttonHeight = ImGui.GetFrameHeight();
|
|
var arrowWidth = buttonHeight;
|
|
var hasChannels = channels.Count > 0;
|
|
var scrollWidth = hasChannels ? Math.Max(0f, available - (arrowWidth * 2f + style.ItemSpacing.X * 2f)) : 0f;
|
|
if (hasChannels)
|
|
{
|
|
var minimumWidth = 120f * ImGuiHelpers.GlobalScale;
|
|
scrollWidth = Math.Max(scrollWidth, minimumWidth);
|
|
}
|
|
var scrollStep = scrollWidth > 0f ? scrollWidth * 0.9f : 120f;
|
|
if (!hasChannels)
|
|
{
|
|
_pendingChannelScroll = null;
|
|
_channelScroll = 0f;
|
|
_channelScrollMax = 0f;
|
|
}
|
|
var prevScroll = hasChannels ? _channelScroll : 0f;
|
|
var prevMax = hasChannels ? _channelScrollMax : 0f;
|
|
float currentScroll = prevScroll;
|
|
float maxScroll = prevMax;
|
|
|
|
ImGui.PushID("chat_channel_buttons");
|
|
ImGui.BeginGroup();
|
|
|
|
using (ImRaii.Disabled(!hasChannels || prevScroll <= 0.5f))
|
|
{
|
|
var arrowNormal = UIColors.Get("ButtonDefault");
|
|
var arrowHovered = UIColors.Get("LightlessPurple").WithAlpha(0.85f);
|
|
var arrowActive = UIColors.Get("LightlessPurpleDefault").WithAlpha(0.75f);
|
|
ImGui.PushStyleColor(ImGuiCol.Button, arrowNormal);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, arrowHovered);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, arrowActive);
|
|
var clickedLeft = ImGui.ArrowButton("##chat_left", ImGuiDir.Left);
|
|
ImGui.PopStyleColor(3);
|
|
if (clickedLeft)
|
|
{
|
|
_pendingChannelScroll = Math.Max(0f, currentScroll - scrollStep);
|
|
}
|
|
}
|
|
|
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
var childHeight = buttonHeight + style.FramePadding.Y * 2f + style.ScrollbarSize;
|
|
var alignPushed = false;
|
|
if (hasChannels)
|
|
{
|
|
ImGui.PushStyleVar(ImGuiStyleVar.ButtonTextAlign, new Vector2(0f, 0.5f));
|
|
alignPushed = true;
|
|
}
|
|
|
|
const int MaxBadgeDisplay = 99;
|
|
|
|
using (var child = ImRaii.Child("channel_scroll", new Vector2(scrollWidth, childHeight), false, ImGuiWindowFlags.HorizontalScrollbar))
|
|
{
|
|
if (child)
|
|
{
|
|
var first = true;
|
|
foreach (var channel in channels)
|
|
{
|
|
if (!first)
|
|
ImGui.SameLine();
|
|
|
|
var isSelected = string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal);
|
|
var showBadge = !isSelected && channel.UnreadCount > 0;
|
|
var isZoneChannel = channel.Type == ChatChannelType.Zone;
|
|
(string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null;
|
|
|
|
var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault");
|
|
var hovered = isSelected
|
|
? UIColors.Get("LightlessPurple").WithAlpha(0.9f)
|
|
: UIColors.Get("ButtonDefault").WithAlpha(0.85f);
|
|
var active = isSelected
|
|
? UIColors.Get("LightlessPurpleDefault").WithAlpha(0.8f)
|
|
: UIColors.Get("ButtonDefault").WithAlpha(0.75f);
|
|
|
|
ImGui.PushStyleColor(ImGuiCol.Button, normal);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hovered);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, active);
|
|
|
|
if (showBadge)
|
|
{
|
|
var badgeSpacing = 4f * ImGuiHelpers.GlobalScale;
|
|
var badgePadding = new Vector2(4f, 1.5f) * ImGuiHelpers.GlobalScale;
|
|
var badgeText = channel.UnreadCount > MaxBadgeDisplay
|
|
? $"{MaxBadgeDisplay}+"
|
|
: channel.UnreadCount.ToString(CultureInfo.InvariantCulture);
|
|
var badgeTextSize = ImGui.CalcTextSize(badgeText);
|
|
var badgeWidth = badgeTextSize.X + badgePadding.X * 2f;
|
|
var badgeHeight = badgeTextSize.Y + badgePadding.Y * 2f;
|
|
var customPadding = new Vector2(baseFramePadding.X + badgeWidth + badgeSpacing, baseFramePadding.Y);
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, customPadding);
|
|
badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight);
|
|
}
|
|
|
|
var clicked = ImGui.Button($"{channel.DisplayName}##chat_channel_{channel.Key}");
|
|
|
|
if (showBadge)
|
|
{
|
|
ImGui.PopStyleVar();
|
|
}
|
|
|
|
ImGui.PopStyleColor(3);
|
|
|
|
if (clicked && !isSelected)
|
|
{
|
|
_selectedChannelKey = channel.Key;
|
|
_zoneChatService.SetActiveChannel(channel.Key);
|
|
_scrollToBottom = true;
|
|
}
|
|
|
|
var drawList = ImGui.GetWindowDrawList();
|
|
var itemMin = ImGui.GetItemRectMin();
|
|
var itemMax = ImGui.GetItemRectMax();
|
|
|
|
if (isZoneChannel)
|
|
{
|
|
var borderColor = UIColors.Get("LightlessOrange");
|
|
var borderColorU32 = ImGui.ColorConvertFloat4ToU32(borderColor);
|
|
var borderThickness = Math.Max(1f, ImGuiHelpers.GlobalScale);
|
|
drawList.AddRect(itemMin, itemMax, borderColorU32, style.FrameRounding, ImDrawFlags.None, borderThickness);
|
|
}
|
|
|
|
if (showBadge && badgeMetrics is { } metrics)
|
|
{
|
|
var buttonSizeY = itemMax.Y - itemMin.Y;
|
|
var badgeMin = new Vector2(
|
|
itemMin.X + baseFramePadding.X,
|
|
itemMin.Y + (buttonSizeY - metrics.Height) * 0.5f);
|
|
var badgeMax = badgeMin + new Vector2(metrics.Width, metrics.Height);
|
|
var badgeColor = UIColors.Get("DimRed");
|
|
var badgeColorU32 = ImGui.ColorConvertFloat4ToU32(badgeColor);
|
|
drawList.AddRectFilled(badgeMin, badgeMax, badgeColorU32, metrics.Height * 0.5f);
|
|
var textPos = new Vector2(
|
|
badgeMin.X + (metrics.Width - metrics.TextSize.X) * 0.5f,
|
|
badgeMin.Y + (metrics.Height - metrics.TextSize.Y) * 0.5f);
|
|
drawList.AddText(textPos, ImGui.ColorConvertFloat4ToU32(ImGuiColors.DalamudWhite), metrics.Text);
|
|
}
|
|
|
|
first = false;
|
|
}
|
|
|
|
if (_pendingChannelScroll.HasValue)
|
|
{
|
|
ImGui.SetScrollX(_pendingChannelScroll.Value);
|
|
_pendingChannelScroll = null;
|
|
}
|
|
|
|
currentScroll = ImGui.GetScrollX();
|
|
maxScroll = ImGui.GetScrollMaxX();
|
|
}
|
|
}
|
|
|
|
if (alignPushed)
|
|
{
|
|
ImGui.PopStyleVar();
|
|
}
|
|
|
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
|
|
|
using (ImRaii.Disabled(!hasChannels || prevScroll >= prevMax - 0.5f))
|
|
{
|
|
var arrowNormal = UIColors.Get("ButtonDefault");
|
|
var arrowHovered = UIColors.Get("LightlessPurple").WithAlpha(0.85f);
|
|
var arrowActive = UIColors.Get("LightlessPurpleDefault").WithAlpha(0.75f);
|
|
ImGui.PushStyleColor(ImGuiCol.Button, arrowNormal);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, arrowHovered);
|
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, arrowActive);
|
|
var clickedRight = ImGui.ArrowButton("##chat_right", ImGuiDir.Right);
|
|
ImGui.PopStyleColor(3);
|
|
if (clickedRight)
|
|
{
|
|
_pendingChannelScroll = Math.Min(prevScroll + scrollStep, prevMax);
|
|
}
|
|
}
|
|
|
|
ImGui.EndGroup();
|
|
ImGui.PopID();
|
|
|
|
_channelScroll = currentScroll;
|
|
_channelScrollMax = maxScroll;
|
|
|
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f);
|
|
}
|
|
|
|
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);
|
|
}
|