From ba4cd918daecb4e1609ad18036deb0cc50641dc2 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Fri, 22 May 2026 16:32:01 +0200 Subject: [PATCH] feat(ui): warn before sending plugin-only symbols --- HellionChat/Configuration.cs | 4 ++ HellionChat/Ui/ChatInputBar.cs | 31 ++++++++++++- HellionChat/Ui/ChatLogWindow.cs | 45 ++++++++++++++++--- HellionChat/Ui/SettingsTabs/Chat.cs | 6 +++ .../_Helpers/PluginDisclosureScanner.cs | 30 +++++++++++++ 5 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 HellionChat/_Helpers/PluginDisclosureScanner.cs diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 06683a4..136b23b 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -199,6 +199,9 @@ public class Configuration : IPluginConfiguration // Toast when a tell the user sent could not be delivered. public bool NotifyFailedTell = true; + + // UI-11: warn before sending a message that carries plugin-only glyphs. + public bool NotifyPluginDisclosure = true; public bool KeepInputFocus = true; public int MaxLinesToRender = 2_500; // 1-10000 public bool Use24HourClock = true; @@ -296,6 +299,7 @@ public class Configuration : IPluginConfiguration PlaySounds = other.PlaySounds; CustomSoundVolume = other.CustomSoundVolume; NotifyFailedTell = other.NotifyFailedTell; + NotifyPluginDisclosure = other.NotifyPluginDisclosure; KeepInputFocus = other.KeepInputFocus; MaxLinesToRender = other.MaxLinesToRender; Use24HourClock = other.Use24HourClock; diff --git a/HellionChat/Ui/ChatInputBar.cs b/HellionChat/Ui/ChatInputBar.cs index 511facb..079c0ec 100644 --- a/HellionChat/Ui/ChatInputBar.cs +++ b/HellionChat/Ui/ChatInputBar.cs @@ -1,9 +1,11 @@ using System; using System.Numerics; using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using HellionChat._Helpers; using HellionChat.Code; +using HellionChat.Resources; using HellionChat.Util; namespace HellionChat.Ui; @@ -19,6 +21,11 @@ public sealed class ChatInputBar private readonly Func _activeTabAccessor; private readonly InputState _state = new(); + // UI-11: the buffer for which a plugin-disclosure warning was already + // shown. A second Enter on the same buffer sends it anyway; editing the + // buffer clears the arming so the next send is re-checked. + private string? _disclosureArmedBuffer; + public ChatInputBar(Plugin plugin, ChatLogWindow host, Func activeTabAccessor) { _plugin = plugin; @@ -80,11 +87,33 @@ public sealed class ChatInputBar { SubmitCompact(tab); } + + // UI-11: disclosure warning, visible only while an armed buffer is held + // unchanged. Editing the buffer clears the condition automatically. + if (Plugin.Config.NotifyPluginDisclosure + && _disclosureArmedBuffer is not null + && _state.Buffer == _disclosureArmedBuffer) + { + ImGui.TextColored(ImGuiColors.DalamudYellow, HellionStrings.ChatInput_PluginDisclosure_Warning); + } } // TEST-MIRROR: ../_Helpers/CompactInputSubmitter.cs - private void SubmitCompact(Tab tab) => + private void SubmitCompact(Tab tab) + { + if (Plugin.Config.NotifyPluginDisclosure + && _state.Buffer != _disclosureArmedBuffer + && PluginDisclosureScanner.ContainsPrivateUseGlyph(_state.Buffer)) + { + // First send attempt on this exact buffer: arm and hold. The buffer + // is kept, the warning renders, the user can press Enter again. + _disclosureArmedBuffer = _state.Buffer; + return; + } + + _disclosureArmedBuffer = null; CompactInputSubmitter.TrySubmit(_state, tab, _host.SendChatBoxFromExternal); + } // History navigation callback. Cursor math delegated to // CompactInputHistoryNavigator; ImGui buffer splice stays here. diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 227124b..c3ccd90 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Interface.Colors; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; @@ -15,6 +16,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using HellionChat._Helpers; using HellionChat.Code; using HellionChat.GameFunctions; using HellionChat.GameFunctions.Types; @@ -55,6 +57,11 @@ public sealed class ChatLogWindow : Window private int ActivatePos = -1; internal string Chat = string.Empty; + // UI-11: the main-window input buffer for which a plugin-disclosure + // warning was already shown. Mirrors _disclosureArmedBuffer in + // ChatInputBar — a second Enter on the same buffer sends it anyway. + private string? _disclosureArmedBufferMain; + // Input history extracted into InputHistoryService so pop-out windows share // the same Up/Down history. Cursor stays window-local (independent navigation). private int InputBacklogIdx = -1; @@ -1081,6 +1088,10 @@ public sealed class ChatLogWindow : Window { Chat = chatCopy; + // UI-11: Escape cancels the input — drop any pending + // disclosure arming so the warning does not linger. + _disclosureArmedBufferMain = null; + if (activeTab.CurrentChannel.UseTempChannel) { activeTab.CurrentChannel.ResetTempChannel(); @@ -1090,17 +1101,39 @@ public sealed class ChatLogWindow : Window if (ImGui.IsKeyDown(ImGuiKey.Enter) || ImGui.IsKeyDown(ImGuiKey.KeypadEnter)) { - Plugin.CommandHelpWindow.IsOpen = false; - SendChatBox(activeTab); - - if (activeTab.CurrentChannel.UseTempChannel) + if (Plugin.Config.NotifyPluginDisclosure + && Chat != _disclosureArmedBufferMain + && PluginDisclosureScanner.ContainsPrivateUseGlyph(Chat)) { - activeTab.CurrentChannel.ResetTempChannel(); - SetChannel(activeTab.CurrentChannel.Channel); + // First send attempt on this exact buffer: arm and hold. + // The warning renders below the input. + _disclosureArmedBufferMain = Chat; + } + else + { + _disclosureArmedBufferMain = null; + Plugin.CommandHelpWindow.IsOpen = false; + SendChatBox(activeTab); + + if (activeTab.CurrentChannel.UseTempChannel) + { + activeTab.CurrentChannel.ResetTempChannel(); + SetChannel(activeTab.CurrentChannel.Channel); + } } } } + // UI-11: disclosure warning for the main-window input, mirrors the + // ChatInputBar path. Visible only while the armed buffer is held + // unchanged; editing the buffer clears the condition. + if (Plugin.Config.NotifyPluginDisclosure + && _disclosureArmedBufferMain is not null + && Chat == _disclosureArmedBufferMain) + { + ImGui.TextColored(ImGuiColors.DalamudYellow, HellionStrings.ChatInput_PluginDisclosure_Warning); + } + // Process keybinds that have modifiers while the chat is focused. if (inputActive) { diff --git a/HellionChat/Ui/SettingsTabs/Chat.cs b/HellionChat/Ui/SettingsTabs/Chat.cs index d7fc96b..65b75d4 100644 --- a/HellionChat/Ui/SettingsTabs/Chat.cs +++ b/HellionChat/Ui/SettingsTabs/Chat.cs @@ -151,6 +151,12 @@ internal sealed class Chat : ISettingsTab ref Mutable.NotifyFailedTell ); ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyFailedTell_Description); + + ImGui.Checkbox( + HellionStrings.Settings_Chat_NotifyPluginDisclosure_Name, + ref Mutable.NotifyPluginDisclosure + ); + ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyPluginDisclosure_Description); } } diff --git a/HellionChat/_Helpers/PluginDisclosureScanner.cs b/HellionChat/_Helpers/PluginDisclosureScanner.cs new file mode 100644 index 0000000..5f4029e --- /dev/null +++ b/HellionChat/_Helpers/PluginDisclosureScanner.cs @@ -0,0 +1,30 @@ +namespace HellionChat._Helpers; + +// UI-11 pure decision helper: does a message about to be sent carry a glyph +// that only renders correctly for players running HellionChat or a similar +// plugin? Those are FFXIV Private-Use-Area icon codepoints (the same range +// SeIconChar covers); a recipient without a plugin sees an empty box. +// +// Works on raw char codepoints on purpose: SeIconChar is a Dalamud type, and a +// helper that touched it could not run in the xUnit AppDomain +// (feedback_dalamud_test_isolation, point 7). +// TEST-MIRROR: ../../../Hellion Build test/Ui/PluginDisclosureScannerTests.cs +public static class PluginDisclosureScanner +{ + // FFXIV packs its icon glyphs into this slice of the Unicode Private Use + // Area. The whole range is inside the BMP, so a single char per codepoint + // is enough — no surrogate-pair handling needed. + private const char PrivateUseFirst = ''; + private const char PrivateUseLast = ''; + + public static bool ContainsPrivateUseGlyph(string text) + { + foreach (var c in text) + { + if (c >= PrivateUseFirst && c <= PrivateUseLast) + return true; + } + + return false; + } +}