feat(ui): warn before sending plugin-only symbols
This commit is contained in:
@@ -199,6 +199,9 @@ public class Configuration : IPluginConfiguration
|
|||||||
|
|
||||||
// Toast when a tell the user sent could not be delivered.
|
// Toast when a tell the user sent could not be delivered.
|
||||||
public bool NotifyFailedTell = true;
|
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 bool KeepInputFocus = true;
|
||||||
public int MaxLinesToRender = 2_500; // 1-10000
|
public int MaxLinesToRender = 2_500; // 1-10000
|
||||||
public bool Use24HourClock = true;
|
public bool Use24HourClock = true;
|
||||||
@@ -296,6 +299,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
PlaySounds = other.PlaySounds;
|
PlaySounds = other.PlaySounds;
|
||||||
CustomSoundVolume = other.CustomSoundVolume;
|
CustomSoundVolume = other.CustomSoundVolume;
|
||||||
NotifyFailedTell = other.NotifyFailedTell;
|
NotifyFailedTell = other.NotifyFailedTell;
|
||||||
|
NotifyPluginDisclosure = other.NotifyPluginDisclosure;
|
||||||
KeepInputFocus = other.KeepInputFocus;
|
KeepInputFocus = other.KeepInputFocus;
|
||||||
MaxLinesToRender = other.MaxLinesToRender;
|
MaxLinesToRender = other.MaxLinesToRender;
|
||||||
Use24HourClock = other.Use24HourClock;
|
Use24HourClock = other.Use24HourClock;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using HellionChat._Helpers;
|
using HellionChat._Helpers;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
@@ -19,6 +21,11 @@ public sealed class ChatInputBar
|
|||||||
private readonly Func<Tab?> _activeTabAccessor;
|
private readonly Func<Tab?> _activeTabAccessor;
|
||||||
private readonly InputState _state = new();
|
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<Tab?> activeTabAccessor)
|
public ChatInputBar(Plugin plugin, ChatLogWindow host, Func<Tab?> activeTabAccessor)
|
||||||
{
|
{
|
||||||
_plugin = plugin;
|
_plugin = plugin;
|
||||||
@@ -80,11 +87,33 @@ public sealed class ChatInputBar
|
|||||||
{
|
{
|
||||||
SubmitCompact(tab);
|
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
|
// 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);
|
CompactInputSubmitter.TrySubmit(_state, tab, _host.SendChatBoxFromExternal);
|
||||||
|
}
|
||||||
|
|
||||||
// History navigation callback. Cursor math delegated to
|
// History navigation callback. Cursor math delegated to
|
||||||
// CompactInputHistoryNavigator; ImGui buffer splice stays here.
|
// CompactInputHistoryNavigator; ImGui buffer splice stays here.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Game.Addon.Lifecycle;
|
using Dalamud.Game.Addon.Lifecycle;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
@@ -15,6 +16,7 @@ using Dalamud.Interface.Windowing;
|
|||||||
using Dalamud.Memory;
|
using Dalamud.Memory;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
|
using HellionChat._Helpers;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.GameFunctions;
|
using HellionChat.GameFunctions;
|
||||||
using HellionChat.GameFunctions.Types;
|
using HellionChat.GameFunctions.Types;
|
||||||
@@ -55,6 +57,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
private int ActivatePos = -1;
|
private int ActivatePos = -1;
|
||||||
internal string Chat = string.Empty;
|
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
|
// Input history extracted into InputHistoryService so pop-out windows share
|
||||||
// the same Up/Down history. Cursor stays window-local (independent navigation).
|
// the same Up/Down history. Cursor stays window-local (independent navigation).
|
||||||
private int InputBacklogIdx = -1;
|
private int InputBacklogIdx = -1;
|
||||||
@@ -1081,6 +1088,10 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
Chat = chatCopy;
|
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)
|
if (activeTab.CurrentChannel.UseTempChannel)
|
||||||
{
|
{
|
||||||
activeTab.CurrentChannel.ResetTempChannel();
|
activeTab.CurrentChannel.ResetTempChannel();
|
||||||
@@ -1090,17 +1101,39 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
if (ImGui.IsKeyDown(ImGuiKey.Enter) || ImGui.IsKeyDown(ImGuiKey.KeypadEnter))
|
if (ImGui.IsKeyDown(ImGuiKey.Enter) || ImGui.IsKeyDown(ImGuiKey.KeypadEnter))
|
||||||
{
|
{
|
||||||
Plugin.CommandHelpWindow.IsOpen = false;
|
if (Plugin.Config.NotifyPluginDisclosure
|
||||||
SendChatBox(activeTab);
|
&& Chat != _disclosureArmedBufferMain
|
||||||
|
&& PluginDisclosureScanner.ContainsPrivateUseGlyph(Chat))
|
||||||
if (activeTab.CurrentChannel.UseTempChannel)
|
|
||||||
{
|
{
|
||||||
activeTab.CurrentChannel.ResetTempChannel();
|
// First send attempt on this exact buffer: arm and hold.
|
||||||
SetChannel(activeTab.CurrentChannel.Channel);
|
// 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.
|
// Process keybinds that have modifiers while the chat is focused.
|
||||||
if (inputActive)
|
if (inputActive)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -151,6 +151,12 @@ internal sealed class Chat : ISettingsTab
|
|||||||
ref Mutable.NotifyFailedTell
|
ref Mutable.NotifyFailedTell
|
||||||
);
|
);
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyFailedTell_Description);
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyFailedTell_Description);
|
||||||
|
|
||||||
|
ImGui.Checkbox(
|
||||||
|
HellionStrings.Settings_Chat_NotifyPluginDisclosure_Name,
|
||||||
|
ref Mutable.NotifyPluginDisclosure
|
||||||
|
);
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyPluginDisclosure_Description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user