This commit is contained in:
2025-12-28 05:24:12 +09:00
parent 1632258c4f
commit 8f32b375dd
27 changed files with 3040 additions and 482 deletions

View File

@@ -757,14 +757,19 @@ public class DrawUserPair
}
UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell");
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User", _menuWidth, true))
var banEnabled = UiSharedService.CtrlPressed();
var banLabel = banEnabled ? "Ban user" : "Ban user (Hold CTRL)";
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, banLabel, _menuWidth, true) && banEnabled)
{
_mediator.Publish(new OpenBanUserPopupMessage(_pair, group));
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Ban user from this Syncshell");
UiSharedService.AttachToolTip("Hold CTRL to ban user " + (_pair.UserData.AliasOrUID) + " from this Syncshell");
ImGui.Separator();
if (showOwnerActions)
{
ImGui.Separator();
}
}
if (showOwnerActions)

View File

@@ -14,6 +14,7 @@ using LightlessSync.Services.TextureCompression;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using OtterTex;
using System.Buffers.Binary;
using System.Globalization;
using System.Numerics;
using SixLabors.ImageSharp;
@@ -49,6 +50,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _selectedTextureKeys = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, TexturePreviewState> _texturePreviews = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, TextureResolutionInfo?> _textureResolutionCache = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<ObjectKind, TextureWorkspaceTab> _textureWorkspaceTabs = new();
private readonly List<string> _storedPathsToRemove = [];
private readonly Dictionary<string, string> _filePathResolve = [];
@@ -88,6 +90,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private bool _showAlreadyAddedTransients = false;
private bool _acknowledgeReview = false;
private Task<TextureRowBuildResult>? _textureRowsBuildTask;
private CancellationTokenSource? _textureRowsBuildCts;
private ObjectKind _selectedObjectTab;
private TextureUsageCategory? _textureCategoryFilter = null;
@@ -204,9 +209,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
return;
}
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
_cachedAnalysis = CloneAnalysis(_characterAnalyzer.LastAnalysis);
_hasUpdate = false;
_textureRowsDirty = true;
InvalidateTextureRows();
}
private void DrawContentTabs()
@@ -750,7 +755,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_selectedTextureKeys.Clear();
_textureSelections.Clear();
ResetTextureFilters();
_textureRowsDirty = true;
InvalidateTextureRows();
_conversionFailed = false;
}
@@ -762,6 +767,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
preview.Texture?.Dispose();
}
_texturePreviews.Clear();
_textureRowsBuildCts?.Cancel();
_textureRowsBuildCts?.Dispose();
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
}
@@ -775,18 +782,108 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private void EnsureTextureRows()
{
if (!_textureRowsDirty || _cachedAnalysis == null)
if (_cachedAnalysis == null)
{
return;
}
if (_textureRowsDirty && _textureRowsBuildTask == null)
{
_textureRowsBuildCts?.Dispose();
_textureRowsBuildCts = new();
var snapshot = _cachedAnalysis;
_textureRowsBuildTask = Task.Run(() => BuildTextureRows(snapshot, _textureRowsBuildCts.Token), _textureRowsBuildCts.Token);
}
if (_textureRowsBuildTask == null || !_textureRowsBuildTask.IsCompleted)
{
return;
}
var completedTask = _textureRowsBuildTask;
_textureRowsBuildTask = null;
_textureRowsBuildCts?.Dispose();
_textureRowsBuildCts = null;
if (completedTask.IsCanceled)
{
return;
}
if (completedTask.IsFaulted)
{
_logger.LogWarning(completedTask.Exception, "Failed to build texture rows.");
_textureRowsDirty = false;
return;
}
ApplyTextureRowBuild(completedTask.Result);
_textureRowsDirty = false;
}
private void ApplyTextureRowBuild(TextureRowBuildResult result)
{
_textureRows.Clear();
_textureRows.AddRange(result.Rows);
foreach (var row in _textureRows)
{
if (row.IsAlreadyCompressed)
{
_selectedTextureKeys.Remove(row.Key);
_textureSelections.Remove(row.Key);
}
}
_selectedTextureKeys.RemoveWhere(key => !result.ValidKeys.Contains(key));
foreach (var key in _texturePreviews.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
{
preview.Texture?.Dispose();
_texturePreviews.Remove(key);
}
}
foreach (var key in _textureResolutionCache.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key))
{
_textureResolutionCache.Remove(key);
}
}
foreach (var key in _textureSelections.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key))
{
_textureSelections.Remove(key);
continue;
}
_textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
}
if (!string.IsNullOrEmpty(_selectedTextureKey) && !result.ValidKeys.Contains(_selectedTextureKey))
{
_selectedTextureKey = string.Empty;
}
}
private TextureRowBuildResult BuildTextureRows(
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> analysis,
CancellationToken token)
{
var rows = new List<TextureRow>();
HashSet<string> validKeys = new(StringComparer.OrdinalIgnoreCase);
foreach (var (objectKind, entries) in _cachedAnalysis)
foreach (var (objectKind, entries) in analysis)
{
foreach (var entry in entries.Values)
{
token.ThrowIfCancellationRequested();
if (!string.Equals(entry.FileType, "tex", StringComparison.Ordinal))
{
continue;
@@ -828,17 +925,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
suggestion?.Reason);
validKeys.Add(row.Key);
_textureRows.Add(row);
if (row.IsAlreadyCompressed)
{
_selectedTextureKeys.Remove(row.Key);
_textureSelections.Remove(row.Key);
}
rows.Add(row);
}
}
_textureRows.Sort((a, b) =>
rows.Sort((a, b) =>
{
var comp = a.ObjectKind.CompareTo(b.ObjectKind);
if (comp != 0)
@@ -851,34 +942,14 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
return string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);
});
_selectedTextureKeys.RemoveWhere(key => !validKeys.Contains(key));
return new TextureRowBuildResult(rows, validKeys);
}
foreach (var key in _texturePreviews.Keys.ToArray())
{
if (!validKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
{
preview.Texture?.Dispose();
_texturePreviews.Remove(key);
}
}
foreach (var key in _textureSelections.Keys.ToArray())
{
if (!validKeys.Contains(key))
{
_textureSelections.Remove(key);
continue;
}
_textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
}
if (!string.IsNullOrEmpty(_selectedTextureKey) && !validKeys.Contains(_selectedTextureKey))
{
_selectedTextureKey = string.Empty;
}
_textureRowsDirty = false;
private void InvalidateTextureRows()
{
_textureRowsDirty = true;
_textureRowsBuildCts?.Cancel();
_textureResolutionCache.Clear();
}
private static string MakeTextureKey(ObjectKind objectKind, string primaryFilePath) =>
@@ -893,6 +964,30 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_textureSearch = string.Empty;
}
private static Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> CloneAnalysis(
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source)
{
var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count);
foreach (var (objectKind, entries) in source)
{
var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer);
foreach (var (hash, entry) in entries)
{
entryClone[hash] = new CharacterAnalyzer.FileDataEntry(
hash,
entry.FileType,
entry.GamePaths.ToList(),
entry.FilePaths.ToList(),
entry.OriginalSize,
entry.CompressedSize,
entry.Triangles);
}
clone[objectKind] = entryClone;
}
return clone;
}
private void DrawAnalysisOverview(int totalFiles, long totalActualSize, long totalCompressedSize, long totalTriangles, string breakdownTooltip)
{
var scale = ImGuiHelpers.GlobalScale;
@@ -1091,6 +1186,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
public bool IsAlreadyCompressed => CurrentTarget.HasValue;
}
private sealed record TextureRowBuildResult(
List<TextureRow> Rows,
HashSet<string> ValidKeys);
private sealed class TexturePreviewState
{
public Task? LoadTask { get; set; }
@@ -1099,6 +1198,22 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow;
}
private readonly struct TextureResolutionInfo
{
public TextureResolutionInfo(ushort width, ushort height, ushort depth, ushort mipLevels)
{
Width = width;
Height = height;
Depth = depth;
MipLevels = mipLevels;
}
public ushort Width { get; }
public ushort Height { get; }
public ushort Depth { get; }
public ushort MipLevels { get; }
}
private void DrawTextureWorkspace(ObjectKind objectKind, IReadOnlyList<IGrouping<string, CharacterAnalyzer.FileDataEntry>> otherFileGroups)
{
if (!_textureWorkspaceTabs.ContainsKey(objectKind))
@@ -1143,6 +1258,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private void DrawTextureTabContent(ObjectKind objectKind)
{
var scale = ImGuiHelpers.GlobalScale;
if (_textureRowsBuildTask != null && !_textureRowsBuildTask.IsCompleted && _textureRows.Count == 0)
{
UiSharedService.ColorText("Building texture list.", ImGuiColors.DalamudGrey);
return;
}
var objectRows = _textureRows.Where(row => row.ObjectKind == objectKind).ToList();
var hasAnyTextureRows = objectRows.Count > 0;
var availableCategories = objectRows.Select(row => row.Category)
@@ -1404,6 +1524,24 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
ResetTextureFilters();
}
ImGuiHelpers.ScaledDummy(6);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(4);
UiSharedService.ColorText("Texture row colors", UIColors.Get("LightlessPurple"));
DrawTextureRowLegendItem("Selected", UIColors.Get("LightlessYellow"), "This row is selected in the texture table.");
DrawTextureRowLegendItem("Already compressed", UIColors.Get("LightlessGreenDefault"), "Texture is already stored in a compressed format.");
DrawTextureRowLegendItem("Missing analysis data", UIColors.Get("DimRed"), "File size data has not been computed yet.");
}
private static void DrawTextureRowLegendItem(string label, Vector4 color, string description)
{
var scale = ImGuiHelpers.GlobalScale;
var swatchSize = new Vector2(12f * scale, 12f * scale);
ImGui.ColorButton($"##textureRowLegend{label}", color, ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.NoDragDrop, swatchSize);
ImGui.SameLine(0f, 6f * scale);
var wrapPos = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X;
UiSharedService.TextWrapped($"{label}: {description}", wrapPos);
}
private static void DrawEnumFilterCombo<T>(
@@ -1810,7 +1948,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Refresh", 130f * scale))
{
_textureRowsDirty = true;
InvalidateTextureRows();
}
TextureRow? lastSelected = null;
@@ -1976,7 +2114,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
_selectedTextureKeys.Clear();
_textureSelections.Clear();
_textureRowsDirty = true;
InvalidateTextureRows();
}
}
@@ -2197,6 +2335,68 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
}
private TextureResolutionInfo? GetTextureResolution(TextureRow row)
{
if (_textureResolutionCache.TryGetValue(row.Key, out var cached))
{
return cached;
}
var info = TryReadTextureResolution(row.PrimaryFilePath, out var resolved)
? resolved
: (TextureResolutionInfo?)null;
_textureResolutionCache[row.Key] = info;
return info;
}
private static bool TryReadTextureResolution(string path, out TextureResolutionInfo info)
{
info = default;
try
{
Span<byte> buffer = stackalloc byte[16];
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
var read = stream.Read(buffer);
if (read < buffer.Length)
{
return false;
}
var width = BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]);
var height = BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]);
var depth = BinaryPrimitives.ReadUInt16LittleEndian(buffer[12..14]);
var mipLevels = BinaryPrimitives.ReadUInt16LittleEndian(buffer[14..16]);
if (width == 0 || height == 0)
{
return false;
}
if (depth == 0)
{
depth = 1;
}
if (mipLevels == 0)
{
mipLevels = 1;
}
info = new TextureResolutionInfo(width, height, depth, mipLevels);
return true;
}
catch
{
return false;
}
}
private static string FormatTextureResolution(TextureResolutionInfo info)
=> info.Depth > 1
? $"{info.Width} x {info.Height} x {info.Depth}"
: $"{info.Width} x {info.Height}";
private void DrawTextureRow(TextureRow row, IReadOnlyList<TextureCompressionTarget> targets, int index)
{
var key = row.Key;
@@ -2465,6 +2665,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
MetaRow(FontAwesomeIcon.LayerGroup, "Map Type", row.MapKind.ToString());
MetaRow(FontAwesomeIcon.Fingerprint, "Hash", row.Hash, UIColors.Get("LightlessBlue"));
MetaRow(FontAwesomeIcon.InfoCircle, "Current Format", row.Format);
var resolution = GetTextureResolution(row);
var resolutionLabel = resolution.HasValue ? FormatTextureResolution(resolution.Value) : "Unknown";
MetaRow(FontAwesomeIcon.Images, "Resolution", resolutionLabel);
var selectedLabel = hasSelectedInfo ? selectedInfo!.Title : selectedTarget.ToString();
var selectionColor = hasSelectedInfo ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessGreen");

View File

@@ -46,10 +46,12 @@ public sealed class DtrEntry : IDisposable, IHostedService
private string? _lightfinderText;
private string? _lightfinderTooltip;
private Colors _lightfinderColors;
private readonly object _localHashedCidLock = new();
private string? _localHashedCid;
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
private int _localHashedCidRefreshActive;
public DtrEntry(
ILogger<DtrEntry> logger,
@@ -339,29 +341,61 @@ public sealed class DtrEntry : IDisposable, IHostedService
private string? GetLocalHashedCid()
{
var now = DateTime.UtcNow;
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
return _localHashedCid;
try
lock (_localHashedCidLock)
{
var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
var hashedCid = cid.ToString().GetHash256();
_localHashedCid = hashedCid;
_localHashedCidFetchedAt = now;
return hashedCid;
}
catch (Exception ex)
{
if (now >= _localHashedCidNextErrorLog)
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
{
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
return _localHashedCid;
}
_localHashedCid = null;
_localHashedCidFetchedAt = now;
return null;
}
QueueLocalHashedCidRefresh();
lock (_localHashedCidLock)
{
return _localHashedCid;
}
}
private void QueueLocalHashedCidRefresh()
{
if (Interlocked.Exchange(ref _localHashedCidRefreshActive, 1) != 0)
{
return;
}
_ = Task.Run(async () =>
{
try
{
var cid = await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false);
var hashedCid = cid.ToString().GetHash256();
lock (_localHashedCidLock)
{
_localHashedCid = hashedCid;
_localHashedCidFetchedAt = DateTime.UtcNow;
}
}
catch (Exception ex)
{
var now = DateTime.UtcNow;
lock (_localHashedCidLock)
{
if (now >= _localHashedCidNextErrorLog)
{
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
}
_localHashedCid = null;
_localHashedCidFetchedAt = now;
}
}
finally
{
Interlocked.Exchange(ref _localHashedCidRefreshActive, 0);
}
});
}
private List<string> GetNearbyBroadcasts()

View File

@@ -2368,6 +2368,43 @@ public class SettingsUi : WindowMediatorSubscriberBase
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Nameplate Label Rendering");
var labelRenderer = _configService.Current.LightfinderLabelRenderer;
var labelRendererLabel = labelRenderer switch
{
LightfinderLabelRenderer.SignatureHook => "Native nameplate (sig hook)",
_ => "ImGui Overlay",
};
if (ImGui.BeginCombo("Render mode", labelRendererLabel))
{
foreach (var option in Enum.GetValues<LightfinderLabelRenderer>())
{
var optionLabel = option switch
{
LightfinderLabelRenderer.SignatureHook => "Native Nameplate (sig hook)",
_ => "ImGui Overlay",
};
var selected = option == labelRenderer;
if (ImGui.Selectable(optionLabel, selected))
{
_configService.Current.LightfinderLabelRenderer = option;
_configService.Save();
_nameplateService.RequestRedraw();
}
if (selected)
ImGui.SetItemDefaultFocus();
}
ImGui.EndCombo();
}
_uiShared.DrawHelpText("Choose how Lightfinder labels render: the default ImGui overlay or native nameplate nodes via signature hook.");
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TextUnformatted("Alignment");
ImGui.BeginDisabled(autoAlign);
if (ImGui.SliderInt("Label Offset X", ref offsetX, -200, 200))

View File

@@ -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);
}
}