diff --git a/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs index 9065b81..dcdfc78 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace LightlessSync.LightlessConfiguration.Configurations; @@ -13,4 +14,11 @@ public sealed class ChatConfig : ILightlessConfiguration public bool IsWindowPinned { get; set; } = false; public bool AutoOpenChatOnPluginLoad { get; set; } = false; public float ChatFontScale { get; set; } = 1.0f; + public bool HideInCombat { get; set; } = false; + public bool HideInDuty { get; set; } = false; + public bool ShowWhenUiHidden { get; set; } = true; + public bool ShowInCutscenes { get; set; } = true; + public bool ShowInGpose { get; set; } = true; + public List ChannelOrder { get; set; } = new(); + public Dictionary PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal); } diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index d8e5ee7..58374e3 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -1,5 +1,6 @@ using Dalamud.Game; using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; using Dalamud.Plugin; @@ -105,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true)); services.AddSingleton(gameGui); services.AddSingleton(addonLifecycle); + services.AddSingleton(pluginInterface.UiBuilder); // Core singletons services.AddSingleton(); diff --git a/LightlessSync/Services/Chat/ZoneChatService.cs b/LightlessSync/Services/Chat/ZoneChatService.cs index 55009ab..6eebf4f 100644 --- a/LightlessSync/Services/Chat/ZoneChatService.cs +++ b/LightlessSync/Services/Chat/ZoneChatService.cs @@ -23,6 +23,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS private readonly DalamudUtilService _dalamudUtilService; private readonly ActorObjectService _actorObjectService; private readonly PairUiService _pairUiService; + private readonly ChatConfigService _chatConfigService; private readonly Lock _sync = new(); @@ -57,6 +58,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS _dalamudUtilService = dalamudUtilService; _actorObjectService = actorObjectService; _pairUiService = pairUiService; + _chatConfigService = chatConfigService; _isLoggedIn = _dalamudUtilService.IsLoggedIn; _isConnected = _apiController.IsConnected; @@ -136,6 +138,42 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS } } + public void MoveChannel(string draggedKey, string targetKey) + { + if (string.IsNullOrWhiteSpace(draggedKey) || string.IsNullOrWhiteSpace(targetKey)) + { + return; + } + + bool updated = false; + using (_sync.EnterScope()) + { + if (!_channels.ContainsKey(draggedKey) || !_channels.ContainsKey(targetKey)) + { + return; + } + + var fromIndex = _channelOrder.IndexOf(draggedKey); + var toIndex = _channelOrder.IndexOf(targetKey); + if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex) + { + return; + } + + _channelOrder.RemoveAt(fromIndex); + var insertIndex = Math.Clamp(toIndex, 0, _channelOrder.Count); + _channelOrder.Insert(insertIndex, draggedKey); + _chatConfigService.Current.ChannelOrder = new List(_channelOrder); + _chatConfigService.Save(); + updated = true; + } + + if (updated) + { + PublishChannelListChanged(); + } + } + public Task SetChatEnabledAsync(bool enabled) => enabled ? EnableChatAsync() : DisableChatAsync(); @@ -512,7 +550,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS if (!_isLoggedIn || !_apiController.IsConnected) { - await LeaveCurrentZoneAsync(force, 0).ConfigureAwait(false); + await LeaveCurrentZoneAsync(force, 0, 0).ConfigureAwait(false); return; } @@ -520,6 +558,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false); var territoryId = (ushort)location.TerritoryId; + var worldId = (ushort)location.ServerId; string? zoneKey; ZoneChannelDefinition? definition = null; @@ -536,14 +575,14 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS if (definition is null) { - await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false); + await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false); return; } var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false); if (descriptor is null) { - await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false); + await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false); return; } @@ -586,7 +625,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS } } - private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId) + private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId, ushort worldId) { ChatChannelDescriptor? descriptor = null; @@ -602,7 +641,27 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS state.StatusText = !_chatEnabled ? "Chat services disabled" : (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server"); - state.DisplayName = "Zone Chat"; + if (territoryId != 0 + && _dalamudUtilService.TerritoryData.Value.TryGetValue(territoryId, out var territoryName) + && !string.IsNullOrWhiteSpace(territoryName)) + { + state.DisplayName = territoryName; + } + else + { + state.DisplayName = "Zone Chat"; + } + + if (worldId != 0) + { + state.Descriptor = new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = worldId, + ZoneId = territoryId, + CustomKey = string.Empty + }; + } } if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal)) @@ -1092,17 +1151,50 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS { _channelOrder.Clear(); - if (_channels.ContainsKey(ZoneChannelKey)) + var configuredOrder = _chatConfigService.Current.ChannelOrder; + if (configuredOrder.Count > 0) { - _channelOrder.Add(ZoneChannelKey); + var seen = new HashSet(StringComparer.Ordinal); + foreach (var key in configuredOrder) + { + if (_channels.ContainsKey(key) && seen.Add(key)) + { + _channelOrder.Add(key); + } + } + + var remaining = _channels.Values + .Where(state => !seen.Contains(state.Key)) + .ToList(); + + if (remaining.Count > 0) + { + var zoneKeys = remaining + .Where(state => state.Type == ChatChannelType.Zone) + .Select(state => state.Key); + var groupKeys = remaining + .Where(state => state.Type == ChatChannelType.Group) + .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase) + .Select(state => state.Key); + + _channelOrder.AddRange(zoneKeys); + _channelOrder.AddRange(groupKeys); + } } + else + { + if (_channels.ContainsKey(ZoneChannelKey)) + { + _channelOrder.Add(ZoneChannelKey); + } - var groups = _channels.Values - .Where(state => state.Type == ChatChannelType.Group) - .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase) - .Select(state => state.Key); + var groups = _channels.Values + .Where(state => state.Type == ChatChannelType.Group) + .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase) + .Select(state => state.Key); - _channelOrder.AddRange(groups); + _channelOrder.AddRange(groups); + } if (_activeChannelKey is null && _channelOrder.Count > 0) { diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 06d480b..c8668eb 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -239,6 +239,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsInCombat { get; private set; } = false; public bool IsPerforming { get; private set; } = false; public bool IsInInstance { get; private set; } = false; + public bool IsInDuty => _condition[ConditionFlag.BoundByDuty]; public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles; public uint ClassJobId => _classJobId!.Value; public Lazy> JobData { get; private set; } @@ -248,6 +249,32 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsLodEnabled { get; private set; } public LightlessMediator Mediator { get; } + public bool IsInFieldOperation + { + get + { + if (!IsInDuty) + { + return false; + } + + var territoryId = _clientState.TerritoryType; + if (territoryId == 0) + { + return false; + } + + if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name)) + { + return false; + } + + return name.Contains("Eureka", StringComparison.OrdinalIgnoreCase) + || name.Contains("Bozja", StringComparison.OrdinalIgnoreCase) + || name.Contains("Zadnor", StringComparison.OrdinalIgnoreCase); + } + } + public IGameObject? CreateGameObject(IntPtr reference) { EnsureIsOnFramework(); diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 94c8add..2958ebc 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -27,7 +27,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { private const float MinTextureFilterPaneWidth = 305f; private const float MaxTextureFilterPaneWidth = 405f; - private const float MinTextureDetailPaneWidth = 580f; + private const float MinTextureDetailPaneWidth = 480f; private const float MaxTextureDetailPaneWidth = 720f; private const float SelectedFilePanelLogicalHeight = 90f; private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f); @@ -111,7 +111,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _hasUpdate = true; }); WindowBuilder.For(this) - .SetSizeConstraints(new Vector2(1650, 1000), new Vector2(3840, 2160)) + .SetSizeConstraints(new Vector2(1240, 680), new Vector2(3840, 2160)) .Apply(); _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged; diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs index 6944759..668bcb8 100644 --- a/LightlessSync/UI/ZoneChatUi.cs +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -12,6 +12,7 @@ using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services; using LightlessSync.Services.Chat; using LightlessSync.Services.Mediator; +using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; @@ -23,8 +24,10 @@ namespace LightlessSync.UI; public sealed class ZoneChatUi : WindowMediatorSubscriberBase { private const string ChatDisabledStatus = "Chat services disabled"; + private const string ZoneUnavailableStatus = "Zone chat is only available in major cities."; 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 float DefaultWindowOpacity = .97f; private const float MinWindowOpacity = 0.05f; private const float MaxWindowOpacity = 1f; @@ -32,6 +35,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private const float MaxChatFontScale = 1.5f; private const int ReportReasonMaxLength = 500; private const int ReportContextMaxLength = 1000; + private const int MaxChannelNoteTabLength = 25; private readonly UiSharedService _uiSharedService; private readonly ZoneChatService _zoneChatService; @@ -39,6 +43,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private readonly LightlessProfileManager _profileManager; private readonly ApiController _apiController; private readonly ChatConfigService _chatConfigService; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly DalamudUtilService _dalamudUtilService; + private readonly IUiBuilder _uiBuilder; private readonly Dictionary _draftMessages = new(StringComparer.Ordinal); private readonly ImGuiWindowFlags _unpinnedWindowFlags; private float _currentWindowOpacity = DefaultWindowOpacity; @@ -61,6 +68,10 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase private bool _reportSubmitting; private string? _reportError; private ChatReportResult? _reportSubmissionResult; + private string? _dragChannelKey; + private string? _dragHoverKey; + private bool _HideStateActive; + private bool _HideStateWasOpen; public ZoneChatUi( ILogger logger, @@ -70,6 +81,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase PairUiService pairUiService, LightlessProfileManager profileManager, ChatConfigService chatConfigService, + ServerConfigurationManager serverConfigurationManager, + DalamudUtilService dalamudUtilService, + IUiBuilder uiBuilder, ApiController apiController, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Chat", performanceCollectorService) @@ -79,6 +93,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _pairUiService = pairUiService; _profileManager = profileManager; _chatConfigService = chatConfigService; + _serverConfigurationManager = serverConfigurationManager; + _dalamudUtilService = dalamudUtilService; + _uiBuilder = uiBuilder; _apiController = apiController; _isWindowPinned = _chatConfigService.Current.IsWindowPinned; _showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen; @@ -88,6 +105,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } _unpinnedWindowFlags = Flags; RefreshWindowFlags(); + ApplyUiVisibilitySettings(); Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale; SizeCondition = ImGuiCond.FirstUseEver; WindowBuilder.For(this) @@ -98,6 +116,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase Mediator.Subscribe(this, OnChatChannelMessageAdded); Mediator.Subscribe(this, _ => _scrollToBottom = true); + Mediator.Subscribe(this, _ => UpdateHideState()); + Mediator.Subscribe(this, _ => UpdateHideState()); } public override void PreDraw() @@ -108,6 +128,55 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.SetNextWindowBgAlpha(_currentWindowOpacity); } + private void UpdateHideState() + { + ApplyUiVisibilitySettings(); + var shouldHide = ShouldHide(); + if (shouldHide) + { + _HideStateWasOpen |= IsOpen; + if (IsOpen) + { + IsOpen = false; + } + _HideStateActive = true; + } + else if (_HideStateActive) + { + if (_HideStateWasOpen) + { + IsOpen = true; + } + _HideStateActive = false; + _HideStateWasOpen = false; + } + } + + private void ApplyUiVisibilitySettings() + { + var config = _chatConfigService.Current; + _uiBuilder.DisableAutomaticUiHide = config.ShowWhenUiHidden; + _uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes; + _uiBuilder.DisableGposeUiHide = config.ShowInGpose; + } + + private bool ShouldHide() + { + var config = _chatConfigService.Current; + + if (config.HideInCombat && _dalamudUtilService.IsInCombat) + { + return true; + } + + if (config.HideInDuty && _dalamudUtilService.IsInDuty && !_dalamudUtilService.IsInFieldOperation) + { + return true; + } + + return false; + } + protected override void DrawInternal() { var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg]; @@ -155,7 +224,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase } } - private static void DrawHeader(ChatChannelSnapshot channel) + private void DrawHeader(ChatChannelSnapshot channel) { var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell"; Vector4 color; @@ -178,11 +247,18 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0) { ImGui.SameLine(); - ImGui.TextUnformatted($"World #{channel.Descriptor.WorldId}"); + var worldId = channel.Descriptor.WorldId; + var worldName = _dalamudUtilService.WorldData.Value.TryGetValue(worldId, out var name) ? name : $"World #{worldId}"; + ImGui.TextUnformatted(worldName); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"World ID: {worldId}"); + } } - var showInlineDisabled = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase); - if (showInlineDisabled) + var showInlineStatus = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase) + || string.Equals(channel.StatusText, ZoneUnavailableStatus, StringComparison.OrdinalIgnoreCase); + if (showInlineStatus) { ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); @@ -324,6 +400,15 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _refocusChatInputKey = null; } ImGui.InputText(inputId, ref draft, MaxMessageLength); + if (ImGui.IsItemActive() || ImGui.IsItemFocused()) + { + var drawList = ImGui.GetWindowDrawList(); + var itemMin = ImGui.GetItemRectMin(); + var itemMax = ImGui.GetItemRectMax(); + var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f); + var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight); + drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale)); + } var enterPressed = ImGui.IsItemFocused() && (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter)); _draftMessages[channel.Key] = draft; @@ -480,7 +565,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase 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.")); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("Syncshell chats are self-moderated (their own set rules) by it's owner and appointed moderators.")); ImGui.Dummy(new Vector2(5)); @@ -1187,6 +1272,71 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ImGui.SetTooltip("Toggles the timestamp prefix on messages."); } + ImGui.Separator(); + ImGui.TextUnformatted("Chat Visibility"); + + var autoHideCombat = chatConfig.HideInCombat; + if (ImGui.Checkbox("Hide in combat", ref autoHideCombat)) + { + chatConfig.HideInCombat = autoHideCombat; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Temporarily hides the chat window while in combat."); + } + + var autoHideDuty = chatConfig.HideInDuty; + if (ImGui.Checkbox("Hide in duty (Not in field operations)", ref autoHideDuty)) + { + chatConfig.HideInDuty = autoHideDuty; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Hides the chat window inside duties."); + } + + var showWhenUiHidden = chatConfig.ShowWhenUiHidden; + if (ImGui.Checkbox("Show when game UI is hidden", ref showWhenUiHidden)) + { + chatConfig.ShowWhenUiHidden = showWhenUiHidden; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible when the game UI is hidden."); + } + + var showInCutscenes = chatConfig.ShowInCutscenes; + if (ImGui.Checkbox("Show in cutscenes", ref showInCutscenes)) + { + chatConfig.ShowInCutscenes = showInCutscenes; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible during cutscenes."); + } + + var showInGpose = chatConfig.ShowInGpose; + if (ImGui.Checkbox("Show in group pose", ref showInGpose)) + { + chatConfig.ShowInGpose = showInGpose; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible in /gpose."); + } + + ImGui.Separator(); + 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); @@ -1244,7 +1394,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase }); } - private void DrawChannelButtons(IReadOnlyList channels) + private unsafe void DrawChannelButtons(IReadOnlyList channels) { var style = ImGui.GetStyle(); var baseFramePadding = style.FramePadding; @@ -1305,6 +1455,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase { if (child) { + var dragActive = _dragChannelKey is not null && ImGui.IsMouseDragging(ImGuiMouseButton.Left); + var hoveredTargetThisFrame = false; var first = true; foreach (var channel in channels) { @@ -1315,6 +1467,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase var showBadge = !isSelected && channel.UnreadCount > 0; var isZoneChannel = channel.Type == ChatChannelType.Zone; (string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null; + var channelLabel = GetChannelTabLabel(channel); var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault"); var hovered = isSelected @@ -1343,7 +1496,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight); } - var clicked = ImGui.Button($"{channel.DisplayName}##chat_channel_{channel.Key}"); + var clicked = ImGui.Button($"{channelLabel}##chat_channel_{channel.Key}"); if (showBadge) { @@ -1359,10 +1512,77 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _scrollToBottom = true; } + if (ShouldShowChannelTabContextMenu(channel) + && ImGui.BeginPopupContextItem($"chat_channel_ctx##{channel.Key}")) + { + DrawChannelTabContextMenu(channel); + ImGui.EndPopup(); + } + + if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None)) + { + if (!string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = null; + } + + _dragChannelKey = channel.Key; + ImGui.SetDragDropPayload(ChannelDragPayloadId, null, 0); + ImGui.TextUnformatted(channelLabel); + ImGui.EndDragDropSource(); + } + + var isDragTarget = false; + + if (ImGui.BeginDragDropTarget()) + { + var acceptFlags = ImGuiDragDropFlags.AcceptBeforeDelivery | ImGuiDragDropFlags.AcceptNoDrawDefaultRect; + var payload = ImGui.AcceptDragDropPayload(ChannelDragPayloadId, acceptFlags); + if (!payload.IsNull && _dragChannelKey is { } draggedKey + && !string.Equals(draggedKey, channel.Key, StringComparison.Ordinal)) + { + isDragTarget = true; + if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = channel.Key; + _zoneChatService.MoveChannel(draggedKey, channel.Key); + } + } + + ImGui.EndDragDropTarget(); + } + + var isHoveredDuringDrag = dragActive + && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem | ImGuiHoveredFlags.AllowWhenOverlapped); + + if (!isDragTarget && isHoveredDuringDrag + && !string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal)) + { + isDragTarget = true; + if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = channel.Key; + _zoneChatService.MoveChannel(_dragChannelKey!, channel.Key); + } + } + var drawList = ImGui.GetWindowDrawList(); var itemMin = ImGui.GetItemRectMin(); var itemMax = ImGui.GetItemRectMax(); + if (isHoveredDuringDrag) + { + var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f); + var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight); + drawList.AddRectFilled(itemMin, itemMax, highlightU32, style.FrameRounding); + drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale)); + } + + if (isDragTarget) + { + hoveredTargetThisFrame = true; + } + if (isZoneChannel) { var borderColor = UIColors.Get("LightlessOrange"); @@ -1390,6 +1610,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase first = false; } + if (dragActive && !hoveredTargetThisFrame) + { + _dragHoverKey = null; + } + if (_pendingChannelScroll.HasValue) { ImGui.SetScrollX(_pendingChannelScroll.Value); @@ -1430,9 +1655,123 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase _channelScroll = currentScroll; _channelScrollMax = maxScroll; + if (_dragChannelKey is not null && !ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + _dragChannelKey = null; + _dragHoverKey = null; + } + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f); } + private string GetChannelTabLabel(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return channel.DisplayName; + } + + if (!_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) || !preferNote) + { + return channel.DisplayName; + } + + var note = GetChannelNote(channel); + if (string.IsNullOrWhiteSpace(note)) + { + return channel.DisplayName; + } + + return TruncateChannelNoteForTab(note); + } + + private static string TruncateChannelNoteForTab(string note) + { + if (note.Length <= MaxChannelNoteTabLength) + { + return note; + } + + var ellipsis = "..."; + var maxPrefix = Math.Max(0, MaxChannelNoteTabLength - ellipsis.Length); + return note[..maxPrefix] + ellipsis; + } + + private bool ShouldShowChannelTabContextMenu(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return false; + } + + if (_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) && preferNote) + { + return true; + } + + var note = GetChannelNote(channel); + return !string.IsNullOrWhiteSpace(note); + } + + private void DrawChannelTabContextMenu(ChatChannelSnapshot channel) + { + var preferNote = _chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var value) && value; + var note = GetChannelNote(channel); + var hasNote = !string.IsNullOrWhiteSpace(note); + if (preferNote || hasNote) + { + var label = preferNote ? "Prefer name instead" : "Prefer note instead"; + if (ImGui.MenuItem(label)) + { + SetPreferNoteForChannel(channel.Key, !preferNote); + } + } + + if (preferNote) + { + ImGui.Separator(); + ImGui.TextDisabled("Name:"); + ImGui.TextWrapped(channel.DisplayName); + } + + if (hasNote) + { + ImGui.Separator(); + ImGui.TextDisabled("Note:"); + ImGui.TextWrapped(note); + } + } + + private string? GetChannelNote(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return null; + } + + var gid = channel.Descriptor.CustomKey; + if (string.IsNullOrWhiteSpace(gid)) + { + return null; + } + + return _serverConfigurationManager.GetNoteForGid(gid); + } + + private void SetPreferNoteForChannel(string channelKey, bool preferNote) + { + if (preferNote) + { + _chatConfigService.Current.PreferNotesForChannels[channelKey] = true; + } + else + { + _chatConfigService.Current.PreferNotesForChannels.Remove(channelKey); + } + + _chatConfigService.Save(); + } + private void DrawSystemEntry(ChatMessageEntry entry) { var system = entry.SystemMessage; diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index af98de9..011a6d8 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -584,7 +584,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto)); OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto)); OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto)); - _lightlessHub.On(nameof(Client_ChatReceive), (Func)Client_ChatReceive); + if (!_initialized) + { + _lightlessHub.On(nameof(Client_ChatReceive), (Func)Client_ChatReceive); + } OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto)); OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));