boom
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
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.API.Dto.Group;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services;
|
||||
@@ -14,13 +17,17 @@ using LightlessSync.Services.Chat;
|
||||
using LightlessSync.Services.LightFinder;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI.Style;
|
||||
using LightlessSync.Utils;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using OtterGui.Text;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
@@ -31,6 +38,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
private const string SettingsPopupId = "zone_chat_settings_popup";
|
||||
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
|
||||
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
|
||||
private const string EmotePickerPopupId = "zone_chat_emote_picker";
|
||||
private const int EmotePickerColumns = 10;
|
||||
private const float DefaultWindowOpacity = .97f;
|
||||
private const float DefaultUnfocusedWindowOpacity = 0.6f;
|
||||
private const float MinWindowOpacity = 0.05f;
|
||||
@@ -46,6 +55,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly ZoneChatService _zoneChatService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly PairFactory _pairFactory;
|
||||
private readonly ChatEmoteService _chatEmoteService;
|
||||
private readonly LightFinderService _lightFinderService;
|
||||
private readonly LightlessProfileManager _profileManager;
|
||||
private readonly ApiController _apiController;
|
||||
@@ -54,6 +65,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly IUiBuilder _uiBuilder;
|
||||
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, List<string>> _pendingDraftClears = new(StringComparer.Ordinal);
|
||||
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
|
||||
private float _currentWindowOpacity = DefaultWindowOpacity;
|
||||
private float _baseWindowOpacity = DefaultWindowOpacity;
|
||||
@@ -81,6 +93,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
private ChatReportResult? _reportSubmissionResult;
|
||||
private string? _dragChannelKey;
|
||||
private string? _dragHoverKey;
|
||||
private bool _openEmotePicker;
|
||||
private string _emoteFilter = string.Empty;
|
||||
private bool _HideStateActive;
|
||||
private bool _HideStateWasOpen;
|
||||
private bool _pushedStyle;
|
||||
@@ -91,6 +105,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
UiSharedService uiSharedService,
|
||||
ZoneChatService zoneChatService,
|
||||
PairUiService pairUiService,
|
||||
PairFactory pairFactory,
|
||||
ChatEmoteService chatEmoteService,
|
||||
LightFinderService lightFinderService,
|
||||
LightlessProfileManager profileManager,
|
||||
ChatConfigService chatConfigService,
|
||||
@@ -104,6 +120,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
_uiSharedService = uiSharedService;
|
||||
_zoneChatService = zoneChatService;
|
||||
_pairUiService = pairUiService;
|
||||
_pairFactory = pairFactory;
|
||||
_chatEmoteService = chatEmoteService;
|
||||
_lightFinderService = lightFinderService;
|
||||
_profileManager = profileManager;
|
||||
_chatConfigService = chatConfigService;
|
||||
@@ -188,7 +206,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
private void ApplyUiVisibilitySettings()
|
||||
{
|
||||
var config = _chatConfigService.Current;
|
||||
_uiBuilder.DisableAutomaticUiHide = config.ShowWhenUiHidden;
|
||||
_uiBuilder.DisableUserUiHide = true;
|
||||
_uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes;
|
||||
_uiBuilder.DisableGposeUiHide = config.ShowInGpose;
|
||||
}
|
||||
@@ -197,6 +215,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
var config = _chatConfigService.Current;
|
||||
|
||||
if (!config.ShowWhenUiHidden && _dalamudUtilService.IsGameUiHidden)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config.HideInCombat && _dalamudUtilService.IsInCombat)
|
||||
{
|
||||
return true;
|
||||
@@ -386,6 +409,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
bottomColor);
|
||||
|
||||
var showTimestamps = _chatConfigService.Current.ShowMessageTimestamps;
|
||||
_chatEmoteService.EnsureGlobalEmotesLoaded();
|
||||
PairUiSnapshot? pairSnapshot = null;
|
||||
var itemSpacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
|
||||
if (channel.Messages.Count == 0)
|
||||
{
|
||||
@@ -423,16 +449,109 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
||||
}
|
||||
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
||||
var showRoleIcons = false;
|
||||
var isOwner = false;
|
||||
var isModerator = false;
|
||||
var isPinned = false;
|
||||
|
||||
if (channel.Type == ChatChannelType.Group
|
||||
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
||||
&& payload.Sender.User is not null)
|
||||
{
|
||||
pairSnapshot ??= _pairUiService.GetSnapshot();
|
||||
var groupId = channel.Descriptor.CustomKey;
|
||||
if (!string.IsNullOrWhiteSpace(groupId)
|
||||
&& pairSnapshot.GroupsByGid.TryGetValue(groupId, out var groupInfo))
|
||||
{
|
||||
var senderUid = payload.Sender.User.UID;
|
||||
isOwner = string.Equals(senderUid, groupInfo.OwnerUID, StringComparison.Ordinal);
|
||||
if (groupInfo.GroupPairUserInfos.TryGetValue(senderUid, out var info))
|
||||
{
|
||||
isModerator = info.IsModerator();
|
||||
isPinned = info.IsPinned();
|
||||
}
|
||||
}
|
||||
|
||||
showRoleIcons = isOwner || isModerator || isPinned;
|
||||
}
|
||||
|
||||
ImGui.BeginGroup();
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
|
||||
ImGui.PopStyleColor();
|
||||
if (showRoleIcons)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(timestampText))
|
||||
{
|
||||
ImGui.TextUnformatted(timestampText);
|
||||
ImGui.SameLine(0f, 0f);
|
||||
}
|
||||
|
||||
var hasIcon = false;
|
||||
if (isModerator)
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.UserShield, UIColors.Get("LightlessPurple"));
|
||||
UiSharedService.AttachToolTip("Moderator");
|
||||
hasIcon = true;
|
||||
}
|
||||
|
||||
if (isOwner)
|
||||
{
|
||||
if (hasIcon)
|
||||
{
|
||||
ImGui.SameLine(0f, itemSpacing);
|
||||
}
|
||||
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Crown, UIColors.Get("LightlessYellow"));
|
||||
UiSharedService.AttachToolTip("Owner");
|
||||
hasIcon = true;
|
||||
}
|
||||
|
||||
if (isPinned)
|
||||
{
|
||||
if (hasIcon)
|
||||
{
|
||||
ImGui.SameLine(0f, itemSpacing);
|
||||
}
|
||||
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Thumbtack, UIColors.Get("LightlessBlue"));
|
||||
UiSharedService.AttachToolTip("Pinned");
|
||||
hasIcon = true;
|
||||
}
|
||||
|
||||
if (hasIcon)
|
||||
{
|
||||
ImGui.SameLine(0f, itemSpacing);
|
||||
}
|
||||
|
||||
var messageStartX = ImGui.GetCursorPosX();
|
||||
DrawChatMessageWithEmotes($"{message.DisplayName}: ", payload.Message, messageStartX);
|
||||
}
|
||||
else
|
||||
{
|
||||
var messageStartX = ImGui.GetCursorPosX();
|
||||
DrawChatMessageWithEmotes($"{timestampText}{message.DisplayName}: ", payload.Message, messageStartX);
|
||||
}
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.EndGroup();
|
||||
|
||||
ImGui.SetNextWindowSizeConstraints(
|
||||
new Vector2(190f * ImGuiHelpers.GlobalScale, 0f),
|
||||
new Vector2(float.MaxValue, float.MaxValue));
|
||||
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);
|
||||
if (channel.Type == ChatChannelType.Group
|
||||
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
||||
&& payload.Sender.User is not null)
|
||||
{
|
||||
var aliasOrUid = payload.Sender.User.AliasOrUID;
|
||||
if (!string.IsNullOrWhiteSpace(aliasOrUid)
|
||||
&& !string.Equals(message.DisplayName, aliasOrUid, StringComparison.Ordinal))
|
||||
{
|
||||
ImGui.TextDisabled(aliasOrUid);
|
||||
}
|
||||
}
|
||||
ImGui.Separator();
|
||||
|
||||
var actionIndex = 0;
|
||||
@@ -461,6 +580,335 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawChatMessageWithEmotes(string prefix, string message, float lineStartX)
|
||||
{
|
||||
var segments = BuildChatSegments(prefix, message);
|
||||
var firstOnLine = true;
|
||||
var emoteSize = new Vector2(ImGui.GetTextLineHeight());
|
||||
var remainingWidth = ImGui.GetContentRegionAvail().X;
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (segment.IsLineBreak)
|
||||
{
|
||||
if (firstOnLine)
|
||||
{
|
||||
ImGui.NewLine();
|
||||
}
|
||||
ImGui.SetCursorPosX(lineStartX);
|
||||
firstOnLine = true;
|
||||
remainingWidth = ImGui.GetContentRegionAvail().X;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (segment.IsWhitespace && firstOnLine)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var segmentWidth = segment.IsEmote ? emoteSize.X : ImGui.CalcTextSize(segment.Text).X;
|
||||
if (!firstOnLine)
|
||||
{
|
||||
if (segmentWidth > remainingWidth)
|
||||
{
|
||||
ImGui.SetCursorPosX(lineStartX);
|
||||
firstOnLine = true;
|
||||
remainingWidth = ImGui.GetContentRegionAvail().X;
|
||||
if (segment.IsWhitespace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.SameLine(0f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (segment.IsEmote && segment.Texture is not null)
|
||||
{
|
||||
ImGui.Image(segment.Texture.Handle, emoteSize);
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
DrawEmoteTooltip(segment.EmoteName ?? string.Empty, segment.Texture);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted(segment.Text);
|
||||
}
|
||||
|
||||
remainingWidth -= segmentWidth;
|
||||
firstOnLine = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void DrawEmotePickerPopup(ref string draft, string channelKey)
|
||||
{
|
||||
if (_openEmotePicker)
|
||||
{
|
||||
ImGui.OpenPopup(EmotePickerPopupId);
|
||||
_openEmotePicker = false;
|
||||
}
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
var scale = ImGuiHelpers.GlobalScale;
|
||||
var emoteSize = 32f * scale;
|
||||
var itemWidth = emoteSize + (style.FramePadding.X * 2f);
|
||||
var gridWidth = (itemWidth * EmotePickerColumns) + (style.ItemSpacing.X * Math.Max(0, EmotePickerColumns - 1));
|
||||
var scrollbarPadding = style.ScrollbarSize + (style.ItemSpacing.X * 2f) + (8f * scale);
|
||||
var windowWidth = gridWidth + scrollbarPadding + (style.WindowPadding.X * 2f);
|
||||
ImGui.SetNextWindowSize(new Vector2(windowWidth, 340f * scale), ImGuiCond.Always);
|
||||
if (!ImGui.BeginPopup(EmotePickerPopupId))
|
||||
return;
|
||||
|
||||
ImGui.TextUnformatted("Emotes");
|
||||
ImGui.Separator();
|
||||
|
||||
ImGui.SetNextItemWidth(-1f);
|
||||
ImGui.InputTextWithHint("##emote_filter", "Search Emotes", ref _emoteFilter, 50);
|
||||
ImGui.Spacing();
|
||||
|
||||
var emotes = _chatEmoteService.GetEmoteNames();
|
||||
var filter = _emoteFilter.Trim();
|
||||
var hasFilter = filter.Length > 0;
|
||||
|
||||
using (var child = ImRaii.Child("emote_picker_list", new Vector2(-1f, 0f), true))
|
||||
{
|
||||
if (child)
|
||||
{
|
||||
var any = false;
|
||||
var itemHeight = emoteSize + (style.FramePadding.Y * 2f);
|
||||
var cellWidth = itemWidth + style.ItemSpacing.X;
|
||||
var availableWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X);
|
||||
var maxColumns = Math.Max(1, (int)MathF.Floor((availableWidth + style.ItemSpacing.X) / cellWidth));
|
||||
var columns = Math.Max(1, Math.Min(EmotePickerColumns, maxColumns));
|
||||
var columnIndex = 0;
|
||||
foreach (var emote in emotes)
|
||||
{
|
||||
if (hasFilter && !emote.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
IDalamudTextureWrap? texture = null;
|
||||
_chatEmoteService.TryGetEmote(emote, out texture);
|
||||
|
||||
ImGui.PushID(emote);
|
||||
var clicked = false;
|
||||
if (texture is not null)
|
||||
{
|
||||
clicked = ImGui.ImageButton(texture.Handle, new Vector2(emoteSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
clicked = ImGui.Button("?", new Vector2(itemWidth, itemHeight));
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
DrawEmoteTooltip(emote, texture);
|
||||
}
|
||||
|
||||
ImGui.PopID();
|
||||
|
||||
if (clicked)
|
||||
{
|
||||
AppendEmoteToDraft(ref draft, emote);
|
||||
_draftMessages[channelKey] = draft;
|
||||
_refocusChatInput = true;
|
||||
_refocusChatInputKey = channelKey;
|
||||
ImGui.CloseCurrentPopup();
|
||||
break;
|
||||
}
|
||||
|
||||
columnIndex++;
|
||||
if (columnIndex < columns)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
columnIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!any)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||
ImGui.TextUnformatted(emotes.Count == 0 ? "Loading emotes..." : "No emotes found.");
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
private static void AppendEmoteToDraft(ref string draft, string emote)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(draft))
|
||||
{
|
||||
draft = emote;
|
||||
return;
|
||||
}
|
||||
|
||||
if (char.IsWhiteSpace(draft[^1]))
|
||||
{
|
||||
draft += emote;
|
||||
}
|
||||
else
|
||||
{
|
||||
draft += " " + emote;
|
||||
}
|
||||
}
|
||||
|
||||
private List<ChatSegment> BuildChatSegments(string prefix, string message)
|
||||
{
|
||||
var segments = new List<ChatSegment>(Math.Max(16, message.Length / 4));
|
||||
AppendChatSegments(segments, prefix, allowEmotes: false);
|
||||
AppendChatSegments(segments, message, allowEmotes: true);
|
||||
return segments;
|
||||
}
|
||||
|
||||
private void AppendChatSegments(List<ChatSegment> segments, string text, bool allowEmotes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = 0;
|
||||
while (index < text.Length)
|
||||
{
|
||||
if (text[index] == '\n')
|
||||
{
|
||||
segments.Add(ChatSegment.LineBreak());
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (text[index] == '\r')
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsWhiteSpace(text[index]))
|
||||
{
|
||||
var start = index;
|
||||
while (index < text.Length && char.IsWhiteSpace(text[index]) && text[index] != '\n' && text[index] != '\r')
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
segments.Add(ChatSegment.FromText(text[start..index], isWhitespace: true));
|
||||
continue;
|
||||
}
|
||||
|
||||
var tokenStart = index;
|
||||
while (index < text.Length && !char.IsWhiteSpace(text[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
var token = text[tokenStart..index];
|
||||
if (allowEmotes && TrySplitToken(token, out var leading, out var core, out var trailing))
|
||||
{
|
||||
if (_chatEmoteService.TryGetEmote(core, out var texture) && texture is not null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(leading))
|
||||
{
|
||||
segments.Add(ChatSegment.FromText(leading));
|
||||
}
|
||||
|
||||
segments.Add(ChatSegment.Emote(texture, core));
|
||||
|
||||
if (!string.IsNullOrEmpty(trailing))
|
||||
{
|
||||
segments.Add(ChatSegment.FromText(trailing));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
segments.Add(ChatSegment.FromText(token));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySplitToken(string token, out string leading, out string core, out string trailing)
|
||||
{
|
||||
leading = string.Empty;
|
||||
core = string.Empty;
|
||||
trailing = string.Empty;
|
||||
|
||||
var start = 0;
|
||||
while (start < token.Length && !IsEmoteChar(token[start]))
|
||||
{
|
||||
start++;
|
||||
}
|
||||
|
||||
var end = token.Length - 1;
|
||||
while (end >= start && !IsEmoteChar(token[end]))
|
||||
{
|
||||
end--;
|
||||
}
|
||||
|
||||
if (start > end)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
leading = token[..start];
|
||||
core = token[start..(end + 1)];
|
||||
trailing = token[(end + 1)..];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsEmoteChar(char value)
|
||||
{
|
||||
return char.IsLetterOrDigit(value) || value == '_' || value == '-' || value == '!';
|
||||
}
|
||||
|
||||
private void DrawEmoteTooltip(string name, IDalamudTextureWrap? texture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name) && texture is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.SetWindowFontScale(1f);
|
||||
|
||||
if (texture is not null)
|
||||
{
|
||||
var size = 48f * ImGuiHelpers.GlobalScale;
|
||||
ImGui.Image(texture.Handle, new Vector2(size));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
if (texture is not null)
|
||||
{
|
||||
ImGui.Spacing();
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted(name);
|
||||
}
|
||||
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
private readonly record struct ChatSegment(string Text, IDalamudTextureWrap? Texture, string? EmoteName, bool IsEmote, bool IsWhitespace, bool IsLineBreak)
|
||||
{
|
||||
public static ChatSegment FromText(string text, bool isWhitespace = false) => new(text, null, null, false, isWhitespace, false);
|
||||
public static ChatSegment Emote(IDalamudTextureWrap texture, string name) => new(string.Empty, texture, name, true, false, false);
|
||||
public static ChatSegment LineBreak() => new(string.Empty, null, null, false, false, true);
|
||||
}
|
||||
|
||||
private void DrawInput(ChatChannelSnapshot channel)
|
||||
{
|
||||
const int MaxMessageLength = ZoneChatService.MaxOutgoingLength;
|
||||
@@ -469,9 +917,10 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
draft ??= string.Empty;
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
var sendButtonWidth = 100f * ImGuiHelpers.GlobalScale;
|
||||
var sendButtonWidth = 70f * ImGuiHelpers.GlobalScale;
|
||||
var emoteButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Comments).X;
|
||||
var counterWidth = ImGui.CalcTextSize($"{MaxMessageLength}/{MaxMessageLength}").X;
|
||||
var reservedWidth = sendButtonWidth + counterWidth + style.ItemSpacing.X * 2f;
|
||||
var reservedWidth = sendButtonWidth + emoteButtonWidth + counterWidth + style.ItemSpacing.X * 3f;
|
||||
|
||||
ImGui.SetNextItemWidth(-reservedWidth);
|
||||
var inputId = $"##chat-input-{channel.Key}";
|
||||
@@ -482,7 +931,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
_refocusChatInputKey = null;
|
||||
}
|
||||
ImGui.InputText(inputId, ref draft, MaxMessageLength);
|
||||
if (ImGui.IsItemActive() || ImGui.IsItemFocused())
|
||||
if (ImGui.IsItemActive())
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var itemMin = ImGui.GetItemRectMin();
|
||||
@@ -504,10 +953,22 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
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 desiredSendX = rightEdgeScreen - sendButtonWidth;
|
||||
var sendX = MathF.Max(minButtonX + emoteButtonWidth + style.ItemSpacing.X, desiredSendX);
|
||||
var emoteX = sendX - style.ItemSpacing.X - emoteButtonWidth;
|
||||
|
||||
ImGui.SetCursorScreenPos(new Vector2(emoteX, buttonScreenPos.Y));
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Comments))
|
||||
{
|
||||
_openEmotePicker = true;
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.SetTooltip("Open Emotes");
|
||||
}
|
||||
|
||||
ImGui.SetCursorScreenPos(new Vector2(sendX, buttonScreenPos.Y));
|
||||
var sendColor = UIColors.Get("LightlessPurpleDefault");
|
||||
var sendHovered = UIColors.Get("LightlessPurple");
|
||||
var sendActive = UIColors.Get("LightlessPurpleActive");
|
||||
@@ -518,7 +979,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
var sendClicked = false;
|
||||
using (ImRaii.Disabled(!canSend))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PaperPlane, $"Send##chat-send-{channel.Key}", 100f * ImGuiHelpers.GlobalScale, center: true))
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PaperPlane, $"Send##chat-send-{channel.Key}", sendButtonWidth, center: true))
|
||||
{
|
||||
sendClicked = true;
|
||||
}
|
||||
@@ -526,47 +987,56 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
ImGui.PopStyleVar();
|
||||
ImGui.PopStyleColor(3);
|
||||
|
||||
DrawEmotePickerPopup(ref draft, channel.Key);
|
||||
|
||||
if (canSend && (enterPressed || sendClicked))
|
||||
{
|
||||
_refocusChatInput = true;
|
||||
_refocusChatInputKey = channel.Key;
|
||||
if (TrySendDraft(channel, draft))
|
||||
var sanitized = SanitizeOutgoingDraft(draft);
|
||||
if (sanitized is not null)
|
||||
{
|
||||
_draftMessages[channel.Key] = string.Empty;
|
||||
_scrollToBottom = true;
|
||||
TrackPendingDraftClear(channel.Key, sanitized);
|
||||
if (TrySendDraft(channel, sanitized))
|
||||
{
|
||||
_scrollToBottom = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
RemovePendingDraftClear(channel.Key, sanitized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
parentContentMin = Vector2.Zero;
|
||||
overlaySize = ImGui.GetWindowSize();
|
||||
}
|
||||
|
||||
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 previousCursor = ImGui.GetCursorPos();
|
||||
ImGui.SetCursorPos(parentContentMin);
|
||||
|
||||
var overlayFlags = ImGuiWindowFlags.NoDecoration
|
||||
| ImGuiWindowFlags.NoMove
|
||||
| ImGuiWindowFlags.NoScrollbar
|
||||
var bgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg];
|
||||
bgColor.W = 0.86f;
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 6f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, 0f);
|
||||
ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero);
|
||||
ImGui.PushStyleColor(ImGuiCol.ChildBg, bgColor);
|
||||
|
||||
var overlayFlags = ImGuiWindowFlags.NoScrollbar
|
||||
| ImGuiWindowFlags.NoScrollWithMouse
|
||||
| ImGuiWindowFlags.NoSavedSettings;
|
||||
|
||||
var overlayOpen = true;
|
||||
if (ImGui.Begin("##zone_chat_rules_overlay", ref overlayOpen, overlayFlags))
|
||||
if (ImGui.BeginChild("##zone_chat_rules_overlay", overlaySize, false, overlayFlags))
|
||||
{
|
||||
var contentMin = ImGui.GetWindowContentRegionMin();
|
||||
var contentMax = ImGui.GetWindowContentRegionMax();
|
||||
@@ -686,16 +1156,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
_showRulesOverlay = false;
|
||||
}
|
||||
|
||||
if (!overlayOpen)
|
||||
{
|
||||
_showRulesOverlay = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.End();
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.PopStyleVar();
|
||||
ImGui.EndChild();
|
||||
ImGui.PopStyleColor(2);
|
||||
ImGui.PopStyleVar(2);
|
||||
ImGui.SetCursorPos(previousCursor);
|
||||
}
|
||||
|
||||
private void DrawReportPopup()
|
||||
@@ -943,16 +1409,15 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
_reportPopupRequested = false;
|
||||
}
|
||||
|
||||
private bool TrySendDraft(ChatChannelSnapshot channel, string draft)
|
||||
private bool TrySendDraft(ChatChannelSnapshot channel, string sanitizedMessage)
|
||||
{
|
||||
var trimmed = draft.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
if (string.IsNullOrWhiteSpace(sanitizedMessage))
|
||||
return false;
|
||||
|
||||
bool succeeded;
|
||||
try
|
||||
{
|
||||
succeeded = _zoneChatService.SendMessageAsync(channel.Descriptor, trimmed).GetAwaiter().GetResult();
|
||||
succeeded = _zoneChatService.SendMessageAsync(channel.Descriptor, sanitizedMessage).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -987,6 +1452,21 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
yield return reportAction;
|
||||
}
|
||||
|
||||
var moderationActions = new List<ChatMessageContextAction>();
|
||||
foreach (var action in GetSyncshellModerationActions(channel, message, payload))
|
||||
{
|
||||
moderationActions.Add(action);
|
||||
}
|
||||
|
||||
if (moderationActions.Count > 0)
|
||||
{
|
||||
yield return ChatMessageContextAction.Separator();
|
||||
foreach (var action in moderationActions)
|
||||
{
|
||||
yield return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryCreateCopyMessageAction(ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action)
|
||||
@@ -1094,6 +1574,91 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerable<ChatMessageContextAction> GetSyncshellModerationActions(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload)
|
||||
{
|
||||
if (channel.Type != ChatChannelType.Group)
|
||||
yield break;
|
||||
|
||||
if (message.FromSelf)
|
||||
yield break;
|
||||
|
||||
if (payload.Sender.Kind != ChatSenderKind.IdentifiedUser || payload.Sender.User is null)
|
||||
yield break;
|
||||
|
||||
var groupId = channel.Descriptor.CustomKey;
|
||||
if (string.IsNullOrWhiteSpace(groupId))
|
||||
yield break;
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
if (!snapshot.GroupsByGid.TryGetValue(groupId, out var groupInfo))
|
||||
yield break;
|
||||
|
||||
var sender = payload.Sender.User;
|
||||
var senderUid = sender.UID;
|
||||
if (string.IsNullOrWhiteSpace(senderUid))
|
||||
yield break;
|
||||
|
||||
var selfIsOwner = string.Equals(groupInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
||||
var selfIsModerator = groupInfo.GroupUserInfo.IsModerator();
|
||||
if (!selfIsOwner && !selfIsModerator)
|
||||
yield break;
|
||||
|
||||
var senderInfo = groupInfo.GroupPairUserInfos.TryGetValue(senderUid, out var info) ? info : GroupPairUserInfo.None;
|
||||
var userIsModerator = senderInfo.IsModerator();
|
||||
var userIsPinned = senderInfo.IsPinned();
|
||||
|
||||
var showModeratorActions = selfIsOwner || (selfIsModerator && !userIsModerator);
|
||||
if (!showModeratorActions)
|
||||
yield break;
|
||||
|
||||
if (showModeratorActions)
|
||||
{
|
||||
var pinLabel = userIsPinned ? "Unpin user" : "Pin user";
|
||||
yield return new ChatMessageContextAction(
|
||||
FontAwesomeIcon.Thumbtack,
|
||||
pinLabel,
|
||||
true,
|
||||
() =>
|
||||
{
|
||||
var updatedInfo = senderInfo;
|
||||
updatedInfo.SetPinned(!userIsPinned);
|
||||
_ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(groupInfo.Group, sender, updatedInfo));
|
||||
});
|
||||
|
||||
var removeEnabled = UiSharedService.CtrlPressed();
|
||||
var removeLabel = removeEnabled ? "Remove user" : "Remove user (Hold CTRL)";
|
||||
yield return new ChatMessageContextAction(
|
||||
FontAwesomeIcon.Trash,
|
||||
removeLabel,
|
||||
removeEnabled,
|
||||
() => _ = _apiController.GroupRemoveUser(new GroupPairDto(groupInfo.Group, sender)),
|
||||
"Syncshell action: removes the user from the syncshell, not just chat.");
|
||||
|
||||
var banPair = ResolveBanPair(snapshot, senderUid, sender, groupInfo);
|
||||
var banEnabled = UiSharedService.CtrlPressed();
|
||||
var banLabel = banEnabled ? "Ban user" : "Ban user (Hold CTRL)";
|
||||
yield return new ChatMessageContextAction(
|
||||
FontAwesomeIcon.UserSlash,
|
||||
banLabel,
|
||||
banEnabled,
|
||||
() => Mediator.Publish(new OpenBanUserPopupMessage(banPair!, groupInfo)),
|
||||
"Hold CTRL to ban the user from the syncshell, not just chat.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Pair? ResolveBanPair(PairUiSnapshot snapshot, string senderUid, UserData sender, GroupFullInfoDto groupInfo)
|
||||
{
|
||||
if (snapshot.PairsByUid.TryGetValue(senderUid, out var pair))
|
||||
{
|
||||
return pair;
|
||||
}
|
||||
|
||||
var connection = new PairConnection(sender);
|
||||
var entry = new PairDisplayEntry(new PairUniqueIdentifier(senderUid), connection, new[] { groupInfo }, null);
|
||||
return _pairFactory.Create(entry);
|
||||
}
|
||||
|
||||
private Task OpenStandardProfileAsync(UserData user)
|
||||
{
|
||||
_profileManager.GetLightlessProfile(user);
|
||||
@@ -1124,6 +1689,92 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
_scrollToBottom = true;
|
||||
}
|
||||
|
||||
if (!message.Message.FromSelf || message.Message.Payload?.Message is not { Length: > 0 } payloadText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var matchedPending = false;
|
||||
if (_pendingDraftClears.TryGetValue(message.ChannelKey, out var pending))
|
||||
{
|
||||
var pendingIndex = pending.FindIndex(text => string.Equals(text, payloadText, StringComparison.Ordinal));
|
||||
if (pendingIndex >= 0)
|
||||
{
|
||||
pending.RemoveAt(pendingIndex);
|
||||
matchedPending = true;
|
||||
if (pending.Count == 0)
|
||||
{
|
||||
_pendingDraftClears.Remove(message.ChannelKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedPending && _draftMessages.TryGetValue(message.ChannelKey, out var currentDraft))
|
||||
{
|
||||
var sanitizedCurrent = SanitizeOutgoingDraft(currentDraft);
|
||||
if (sanitizedCurrent is not null && string.Equals(sanitizedCurrent, payloadText, StringComparison.Ordinal))
|
||||
{
|
||||
_draftMessages[message.ChannelKey] = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SanitizeOutgoingDraft(string draft)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(draft))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sanitized = draft.Trim().ReplaceLineEndings(" ");
|
||||
if (sanitized.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sanitized.Length > ZoneChatService.MaxOutgoingLength)
|
||||
{
|
||||
sanitized = sanitized[..ZoneChatService.MaxOutgoingLength];
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private void TrackPendingDraftClear(string channelKey, string message)
|
||||
{
|
||||
if (!_pendingDraftClears.TryGetValue(channelKey, out var pending))
|
||||
{
|
||||
pending = new List<string>();
|
||||
_pendingDraftClears[channelKey] = pending;
|
||||
}
|
||||
|
||||
pending.Add(message);
|
||||
const int MaxPendingDrafts = 12;
|
||||
if (pending.Count > MaxPendingDrafts)
|
||||
{
|
||||
pending.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemovePendingDraftClear(string channelKey, string message)
|
||||
{
|
||||
if (!_pendingDraftClears.TryGetValue(channelKey, out var pending))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = pending.FindIndex(text => string.Equals(text, message, StringComparison.Ordinal));
|
||||
if (index < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pending.RemoveAt(index);
|
||||
if (pending.Count == 0)
|
||||
{
|
||||
_pendingDraftClears.Remove(channelKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenLightfinderProfileInternalAsync(string hashedCid)
|
||||
@@ -1407,6 +2058,17 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
ImGui.SetTooltip("Toggles the timestamp prefix on messages.");
|
||||
}
|
||||
|
||||
var showNotesInSyncshellChat = chatConfig.ShowNotesInSyncshellChat;
|
||||
if (ImGui.Checkbox("Show notes in syncshell chat", ref showNotesInSyncshellChat))
|
||||
{
|
||||
chatConfig.ShowNotesInSyncshellChat = showNotesInSyncshellChat;
|
||||
_chatConfigService.Save();
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.SetTooltip("When enabled, your notes replace user names in syncshell chat.");
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted("Chat Visibility");
|
||||
|
||||
@@ -1993,6 +2655,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
private void DrawContextMenuAction(ChatMessageContextAction action, int index)
|
||||
{
|
||||
ImGui.PushID(index);
|
||||
if (action.IsSeparator)
|
||||
{
|
||||
ImGui.Separator();
|
||||
ImGui.PopID();
|
||||
return;
|
||||
}
|
||||
using var disabled = ImRaii.Disabled(!action.IsEnabled);
|
||||
|
||||
var availableWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X);
|
||||
@@ -2025,6 +2693,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
|
||||
drawList.AddText(textPos, textColor, action.Label);
|
||||
|
||||
if (action.Tooltip is { Length: > 0 } && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
{
|
||||
ImGui.SetTooltip(action.Tooltip);
|
||||
}
|
||||
|
||||
if (clicked && action.IsEnabled)
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
@@ -2034,5 +2707,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
ImGui.PopID();
|
||||
}
|
||||
|
||||
private readonly record struct ChatMessageContextAction(FontAwesomeIcon? Icon, string Label, bool IsEnabled, Action Execute);
|
||||
private static void NoopContextAction()
|
||||
{
|
||||
}
|
||||
|
||||
private readonly record struct ChatMessageContextAction(FontAwesomeIcon? Icon, string Label, bool IsEnabled, Action Execute, string? Tooltip = null, bool IsSeparator = false)
|
||||
{
|
||||
public static ChatMessageContextAction Separator() => new(null, string.Empty, false, ZoneChatUi.NoopContextAction, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user