Files
LightlessClient/LightlessSync/UI/ZoneChatUi.cs
2025-12-01 03:12:00 +09:00

1391 lines
54 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
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.PlayerData.Pairs;
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 = true;
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, "Zone 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;
SizeConstraints = new()
{
MinimumSize = new Vector2(320f, 260f) * ImGuiHelpers.GlobalScale,
MaximumSize = new Vector2(900f, 900f) * ImGuiHelpers.GlobalScale
};
Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded);
Mediator.Subscribe<ChatChannelHistoryCleared>(this, msg =>
{
if (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, msg.ChannelKey, StringComparison.Ordinal))
{
_scrollToBottom = true;
}
});
Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true);
}
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 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];
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.PushID(i);
ImGui.PushStyleColor(ImGuiCol.Text, color);
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {message.Payload.Message}");
ImGui.PopStyleColor();
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))
{
action.Execute();
}
}
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;
using (ImRaii.Disabled(!canSend))
{
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}";
var send = ImGui.InputText(inputId, ref draft, MaxMessageLength, ImGuiInputTextFlags.EnterReturnsTrue);
_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);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PaperPlane, "Send", 100f * ImGuiHelpers.GlobalScale, center: true))
{
send = true;
}
ImGui.PopStyleVar();
ImGui.PopStyleColor(3);
if (send && 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 (_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();
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 (TryCreateCopyMessageAction(message, out var copyAction))
{
yield return copyAction;
}
if (TryCreateViewProfileAction(channel, message, out var viewProfile))
{
yield return viewProfile;
}
if (TryCreateReportMessageAction(channel, message, out var reportAction))
{
yield return reportAction;
}
}
private bool TryCreateCopyMessageAction(ChatMessageEntry message, out ChatMessageContextAction action)
{
var text = message.Payload.Message;
if (string.IsNullOrEmpty(text))
{
action = default;
return false;
}
action = new ChatMessageContextAction(
"Copy Message",
true,
() => ImGui.SetClipboardText(text));
return true;
}
private bool TryCreateViewProfileAction(ChatChannelSnapshot channel, ChatMessageEntry message, out ChatMessageContextAction action)
{
action = default;
switch (channel.Type)
{
case ChatChannelType.Group:
{
var user = message.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(
"View Profile",
true,
() => Mediator.Publish(new ProfileOpenStandaloneMessage(pair)));
return true;
}
action = new ChatMessageContextAction(
"View Profile",
true,
() => RunContextAction(() => OpenStandardProfileAsync(user)));
return true;
}
case ChatChannelType.Zone:
if (!message.Payload.Sender.CanResolveProfile)
return false;
if (string.IsNullOrEmpty(message.Payload.Sender.Token))
return false;
action = new ChatMessageContextAction(
"View Profile",
true,
() => RunContextAction(() => OpenZoneParticipantProfileAsync(channel.Descriptor, message.Payload.Sender.Token)));
return true;
default:
return false;
}
}
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);
_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 async Task OpenZoneParticipantProfileAsync(ChatChannelDescriptor descriptor, string token)
{
var result = await _zoneChatService.ResolveParticipantAsync(descriptor, token).ConfigureAwait(false);
if (result is null)
{
Mediator.Publish(new NotificationMessage("Zone Chat", "Participant is no longer available.", NotificationType.Warning, TimeSpan.FromSeconds(3)));
return;
}
var resolved = result.Value;
var hashedCid = resolved.Sender.HashedCid;
if (string.IsNullOrEmpty(hashedCid))
{
Mediator.Publish(new NotificationMessage("Zone Chat", "This participant remains anonymous.", NotificationType.Warning, TimeSpan.FromSeconds(3)));
return;
}
await OpenLightfinderProfileInternalAsync(hashedCid).ConfigureAwait(false);
}
private async Task OpenLightfinderProfileInternalAsync(string hashedCid)
{
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 OnChatChannelMessageAdded(ChatChannelMessageAdded message)
{
if (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, message.ChannelKey, StringComparison.Ordinal))
{
_scrollToBottom = true;
}
}
private void EnsureSelectedChannel(IReadOnlyList<ChatChannelSnapshot> channels)
{
if (_selectedChannelKey is not null && channels.Any(channel => channel.Key == _selectedChannelKey))
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);
}
}
}
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;
var badgeText = string.Empty;
var badgePadding = Vector2.Zero;
var badgeTextSize = Vector2.Zero;
float badgeWidth = 0f;
float badgeHeight = 0f;
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;
badgePadding = new Vector2(4f, 1.5f) * ImGuiHelpers.GlobalScale;
badgeText = channel.UnreadCount > MaxBadgeDisplay
? $"{MaxBadgeDisplay}+"
: channel.UnreadCount.ToString(CultureInfo.InvariantCulture);
badgeTextSize = ImGui.CalcTextSize(badgeText);
badgeWidth = badgeTextSize.X + badgePadding.X * 2f;
badgeHeight = badgeTextSize.Y + badgePadding.Y * 2f;
var customPadding = new Vector2(baseFramePadding.X + badgeWidth + badgeSpacing, baseFramePadding.Y);
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, customPadding);
}
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)
{
var buttonSizeY = itemMax.Y - itemMin.Y;
var badgeMin = new Vector2(
itemMin.X + baseFramePadding.X,
itemMin.Y + (buttonSizeY - badgeHeight) * 0.5f);
var badgeMax = badgeMin + new Vector2(badgeWidth, badgeHeight);
var badgeColor = UIColors.Get("DimRed");
var badgeColorU32 = ImGui.ColorConvertFloat4ToU32(badgeColor);
drawList.AddRectFilled(badgeMin, badgeMax, badgeColorU32, badgeHeight * 0.5f);
var textPos = new Vector2(
badgeMin.X + (badgeWidth - badgeTextSize.X) * 0.5f,
badgeMin.Y + (badgeHeight - badgeTextSize.Y) * 0.5f);
drawList.AddText(textPos, ImGui.ColorConvertFloat4ToU32(ImGuiColors.DalamudWhite), badgeText);
}
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 readonly record struct ChatMessageContextAction(string Label, bool IsEnabled, Action Execute);
}