Merge restoration block 2 (settings without UI) into v1.8.x track
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||||
<Version>1.8.2</Version>
|
<Version>1.8.3</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Use lock file to pin exact versions -->
|
<!-- Use lock file to pin exact versions -->
|
||||||
|
|||||||
@@ -385,6 +385,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
new SelfTests.PerformanceBaselineStep(this),
|
new SelfTests.PerformanceBaselineStep(this),
|
||||||
new SelfTests.MainWindowFocusOpacityStep(this),
|
new SelfTests.MainWindowFocusOpacityStep(this),
|
||||||
new SelfTests.MainWindowFlagsStep(this),
|
new SelfTests.MainWindowFlagsStep(this),
|
||||||
|
new SelfTests.SenderNameReformatStep(this),
|
||||||
|
new SelfTests.DisclosureArmStep(this),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Re-surface the wizard for existing users when a major UX
|
// Re-surface the wizard for existing users when a major UX
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
using HellionChat._Helpers;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// B2-3: proves the plugin-disclosure arm-and-hold wires the (otherwise verwaist)
|
||||||
|
// scanner into the REAL send entry InputBar.TrySend. Drives TrySend via the
|
||||||
|
// arm-test-hook with a PUA glyph in the buffer and NotifyPluginDisclosure on:
|
||||||
|
// the first send must ARM and HOLD (no send), so PendingMessage stays the probe
|
||||||
|
// string and the armed flag is set. Arm-case ONLY (seiteneffektfrei): a real
|
||||||
|
// send fires ChatBox.SendMessageUnsafe (a real in-game chat line), so the
|
||||||
|
// second-Enter-sends + ASCII-passthrough legs are in-game smoke only, never
|
||||||
|
// headless. Does NOT call PluginDisclosureScanner.ContainsPrivateUseGlyph in
|
||||||
|
// isolation (the false-green trap — it has no other production caller).
|
||||||
|
internal sealed class DisclosureArmStep : ISelfTestStep
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
public DisclosureArmStep(Plugin plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "Hellion Chat - plugin disclosure arm";
|
||||||
|
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var input = this.plugin.InputBar;
|
||||||
|
if (input is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("Plugin.InputBar is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The SymbolPicker inserts exactly these FFXIV Private-Use-Area glyphs;
|
||||||
|
// HighQuality is inside PluginDisclosureScanner's PUA range by
|
||||||
|
// construction (the scanner range IS the SeIconChar range).
|
||||||
|
var probe = $"test {SeIconChar.HighQuality.ToIconString()} msg";
|
||||||
|
|
||||||
|
var savedPending = input.PendingMessage;
|
||||||
|
var savedNotify = Plugin.Config.NotifyPluginDisclosure;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Plugin.Config.NotifyPluginDisclosure = true;
|
||||||
|
input.TestResetDisclosureForSelfTest();
|
||||||
|
input.TestSetPendingMessageForSelfTest(probe);
|
||||||
|
|
||||||
|
// Precondition guard: refuse to drive the real TrySend unless the
|
||||||
|
// toggle is on AND the scanner sees the probe glyph. If the scanner
|
||||||
|
// regressed, this bails with Fail WITHOUT ever calling TrySend, so a
|
||||||
|
// broken scanner can never leak a real chat line. (The remaining
|
||||||
|
// risk — TrySend not calling the scanner at all — is the wiring this
|
||||||
|
// step exists to catch and is covered by the documented residual-leak
|
||||||
|
// note + the mandatory mid-cycle smoke; see the Step 4.8 warning box.)
|
||||||
|
if (
|
||||||
|
!Plugin.Config.NotifyPluginDisclosure
|
||||||
|
|| !PluginDisclosureScanner.ContainsPrivateUseGlyph(input.PendingMessage)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ImGui.Text(
|
||||||
|
"Disclosure precondition not met (toggle off or probe glyph not in the scanner's PUA range) — refusing to drive TrySend to avoid an unintended real send"
|
||||||
|
);
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First send with a PUA glyph + toggle on must ARM, not send. Pass a
|
||||||
|
// null Tab — the arm branch returns before any channel/send use.
|
||||||
|
var armed = input.TestTryArmDisclosureForSelfTest(null);
|
||||||
|
|
||||||
|
if (!armed)
|
||||||
|
{
|
||||||
|
ImGui.Text(
|
||||||
|
"First send did not arm disclosure for a PUA-glyph buffer (scanner not wired into TrySend?)"
|
||||||
|
);
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer must be HELD: TrySend clears _pendingMessage to empty only on
|
||||||
|
// a real send, so an unchanged probe proves nothing was transmitted.
|
||||||
|
if (input.PendingMessage != probe)
|
||||||
|
{
|
||||||
|
ImGui.Text(
|
||||||
|
$"Buffer not held on arm: PendingMessage = '{input.PendingMessage}', expected the unchanged probe (a cleared buffer means it actually sent)"
|
||||||
|
);
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
input.TestResetDisclosureForSelfTest();
|
||||||
|
input.TestSetPendingMessageForSelfTest(savedPending);
|
||||||
|
Plugin.Config.NotifyPluginDisclosure = savedNotify;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUp() { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// B2-1/B2-2: proves the WorldSuffixMode/NameFormMode reformat reaches the REAL
|
||||||
|
// render entry. Drives ChunkRenderer.DrawChunks (a SelfTests/README-sanctioned
|
||||||
|
// real entry that wires SenderNameDisplay.ForDisplay at ChunkRenderer.cs:54)
|
||||||
|
// with a synthetic ChunkSource.Sender chunk carrying a PlayerPayload, at a
|
||||||
|
// non-neutral NameFormMode, and reads the LastRenderedSenderText observability
|
||||||
|
// the real draw produced. NameFormMode.Initials + WorldSuffixMode.Never is
|
||||||
|
// world-independent ("Test Tester" -> "T. T."), so the assertion is
|
||||||
|
// deterministic without a live world lookup. The MessageList routing (its row
|
||||||
|
// methods pass message.Sender to DrawChunks) is gated by the reviewer-grep
|
||||||
|
// (Step 2.6) + in-game smoke, since the visible sender change needs real chat +
|
||||||
|
// the world sheet. Does NOT call SenderNameFormatter/ForDisplay in isolation
|
||||||
|
// (the false-green trap — both are green today on a path the message list never
|
||||||
|
// takes for the sender).
|
||||||
|
internal sealed class SenderNameReformatStep : ISelfTestStep
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
public SenderNameReformatStep(Plugin plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "Hellion Chat - sender name reformat";
|
||||||
|
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var renderer = this.plugin.ChunkRenderer;
|
||||||
|
if (renderer is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("Plugin.ChunkRenderer is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedForm = Plugin.Config.NameFormMode;
|
||||||
|
var savedSuffix = Plugin.Config.WorldSuffixMode;
|
||||||
|
var savedScreenshot = Plugin.Config.ScreenshotMode;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Initials (non-neutral) so ForDisplay reformats; Never + screenshot
|
||||||
|
// off so the result is world-independent and the reformat is not
|
||||||
|
// skipped.
|
||||||
|
Plugin.Config.NameFormMode = NameFormMode.Initials;
|
||||||
|
Plugin.Config.WorldSuffixMode = WorldSuffixMode.Never;
|
||||||
|
Plugin.Config.ScreenshotMode = false;
|
||||||
|
|
||||||
|
// ForDisplay formats payload.PlayerName, not the chunk text.
|
||||||
|
var payload = new PlayerPayload("Test Tester", 1u);
|
||||||
|
var senderChunks = new List<Chunk>
|
||||||
|
{
|
||||||
|
new TextChunk(ChunkSource.Sender, payload, "Test Tester"),
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.DrawChunks(senderChunks);
|
||||||
|
|
||||||
|
if (renderer.LastRenderedSenderText != "T. T.")
|
||||||
|
{
|
||||||
|
ImGui.Text(
|
||||||
|
$"LastRenderedSenderText = '{renderer.LastRenderedSenderText}', expected 'T. T.' (Initials reformat through the real render path)"
|
||||||
|
);
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Plugin.Config.NameFormMode = savedForm;
|
||||||
|
Plugin.Config.WorldSuffixMode = savedSuffix;
|
||||||
|
Plugin.Config.ScreenshotMode = savedScreenshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUp() { }
|
||||||
|
}
|
||||||
@@ -38,6 +38,13 @@ internal sealed class ChunkRenderer
|
|||||||
_ = _logger;
|
_ = _logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// B2-1/B2-2 render-observability: the formatted sender text the real draw
|
||||||
|
// path actually produced (post-ForDisplay). A SelfTest reads this after
|
||||||
|
// driving DrawChunks to prove the WorldSuffixMode/NameFormMode reformat
|
||||||
|
// reached the real render entry — never the helper in isolation. null until
|
||||||
|
// a sender span is reformatted for display.
|
||||||
|
internal string? LastRenderedSenderText { get; private set; }
|
||||||
|
|
||||||
public void DrawChunks(
|
public void DrawChunks(
|
||||||
IReadOnlyList<Chunk> chunks,
|
IReadOnlyList<Chunk> chunks,
|
||||||
bool wrap = true,
|
bool wrap = true,
|
||||||
@@ -51,7 +58,25 @@ internal sealed class ChunkRenderer
|
|||||||
// the list unchanged when nothing applies, so non-sender lists and the
|
// the list unchanged when nothing applies, so non-sender lists and the
|
||||||
// neutral default cost only a quick scan.
|
// neutral default cost only a quick scan.
|
||||||
if (!Plugin.Config.ScreenshotMode)
|
if (!Plugin.Config.ScreenshotMode)
|
||||||
chunks = SenderNameDisplay.ForDisplay(chunks);
|
{
|
||||||
|
var displayed = SenderNameDisplay.ForDisplay(chunks);
|
||||||
|
// ForDisplay only allocates a NEW list when it actually reformatted
|
||||||
|
// a sender span (same reference on the neutral default / non-sender
|
||||||
|
// lists). So this scan runs only when a sender name was reformatted
|
||||||
|
// for display — zero overhead on the neutral-default hot path.
|
||||||
|
if (!ReferenceEquals(displayed, chunks))
|
||||||
|
{
|
||||||
|
chunks = displayed;
|
||||||
|
foreach (var c in chunks)
|
||||||
|
{
|
||||||
|
if (c.Source == ChunkSource.Sender && c is TextChunk reformatted)
|
||||||
|
{
|
||||||
|
LastRenderedSenderText = reformatted.Content;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ using System.Numerics;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using HellionChat._Helpers;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.GameFunctions;
|
using HellionChat.GameFunctions;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
@@ -42,6 +44,12 @@ internal sealed class InputBar
|
|||||||
private bool _wasInputTextHovered;
|
private bool _wasInputTextHovered;
|
||||||
private bool? _isFocusedOverride; // Test-only; null = honour per-frame Draw() value.
|
private bool? _isFocusedOverride; // Test-only; null = honour per-frame Draw() value.
|
||||||
|
|
||||||
|
// UI-11 plugin-disclosure arm-and-hold: holds the buffer that armed the
|
||||||
|
// disclosure warning. null = not armed. Compared by value so an edit
|
||||||
|
// re-arms and a resend on the identical buffer goes through. 1.5.6 parity
|
||||||
|
// (ChatInputBar 1d3b429:27).
|
||||||
|
private string? _disclosureArmedBuffer;
|
||||||
|
|
||||||
// Auto-translate popup state — lives here because the popup lifecycle is
|
// Auto-translate popup state — lives here because the popup lifecycle is
|
||||||
// tightly coupled to the input callback and the pending message buffer.
|
// tightly coupled to the input callback and the pending message buffer.
|
||||||
private const string AutoCompleteId = "##hellion-at-complete";
|
private const string AutoCompleteId = "##hellion-at-complete";
|
||||||
@@ -158,6 +166,21 @@ internal sealed class InputBar
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
DrawQuickButtons();
|
DrawQuickButtons();
|
||||||
|
|
||||||
|
// UI-11: yellow inline warning while a plugin-only-glyph message is
|
||||||
|
// armed-and-held (buffer unchanged since it armed). Renders on its own
|
||||||
|
// line below the input row. 1.5.6 parity (ChatInputBar 1d3b429:93-103).
|
||||||
|
if (
|
||||||
|
Plugin.Config.NotifyPluginDisclosure
|
||||||
|
&& _disclosureArmedBuffer is not null
|
||||||
|
&& _pendingMessage == _disclosureArmedBuffer
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ImGui.TextColored(
|
||||||
|
ImGuiColors.DalamudYellow,
|
||||||
|
HellionStrings.ChatInput_PluginDisclosure_Warning
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// SymbolPicker popup is rendered last so it can splice its fragment
|
// SymbolPicker popup is rendered last so it can splice its fragment
|
||||||
// straight into the pending buffer.
|
// straight into the pending buffer.
|
||||||
var inserted = _symbolPicker.DrawAndConsume();
|
var inserted = _symbolPicker.DrawAndConsume();
|
||||||
@@ -334,6 +357,31 @@ internal sealed class InputBar
|
|||||||
if (string.IsNullOrEmpty(text))
|
if (string.IsNullOrEmpty(text))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// UI-11: plugin-disclosure arm-and-hold. Arm + scan on the RAW
|
||||||
|
// _pendingMessage (NOT the trimmed `text`) so the Draw warning gate
|
||||||
|
// (_pendingMessage == _disclosureArmedBuffer) matches byte-for-byte even
|
||||||
|
// when the buffer has leading/trailing whitespace. 1.5.6 armed/held/
|
||||||
|
// warned on the raw buffer and only trimmed at SendChatBox; storing the
|
||||||
|
// trimmed value here would silently kill the warning for a padded buffer
|
||||||
|
// (the Draw gate compares the untrimmed _pendingMessage). Runs BEFORE the
|
||||||
|
// channel prefix + AutoTranslate.ReplaceWithPayload (the resolved <at:>
|
||||||
|
// macro carries its own non-ASCII bytes and would false-positive;
|
||||||
|
// whitespace is never a PUA codepoint, so scanning the raw buffer is
|
||||||
|
// equivalent for detection). First Enter on a buffer with a plugin-only
|
||||||
|
// PUA glyph arms + HOLDS (returns without sending, buffer kept); a second
|
||||||
|
// Enter on the same unchanged buffer sends; editing re-checks. 1.5.6
|
||||||
|
// parity (ChatInputBar.SubmitCompact 1d3b429:108-118).
|
||||||
|
if (
|
||||||
|
Plugin.Config.NotifyPluginDisclosure
|
||||||
|
&& _disclosureArmedBuffer != _pendingMessage
|
||||||
|
&& PluginDisclosureScanner.ContainsPrivateUseGlyph(_pendingMessage)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_disclosureArmedBuffer = _pendingMessage;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_disclosureArmedBuffer = null;
|
||||||
|
|
||||||
// Slash commands route through verbatim — the game's chat parser
|
// Slash commands route through verbatim — the game's chat parser
|
||||||
// handles /tell, /fc, /hellion etc. on its own. Other text gets
|
// handles /tell, /fc, /hellion etc. on its own. Other text gets
|
||||||
// the active channel's prefix so the line lands on the channel
|
// the active channel's prefix so the line lands on the channel
|
||||||
@@ -417,6 +465,21 @@ internal sealed class InputBar
|
|||||||
// override and let Draw()'s ImGui.IsItemFocused() result take over again.
|
// override and let Draw()'s ImGui.IsItemFocused() result take over again.
|
||||||
internal void TestSetFocusedForSelfTest(bool? value) => _isFocusedOverride = value;
|
internal void TestSetFocusedForSelfTest(bool? value) => _isFocusedOverride = value;
|
||||||
|
|
||||||
|
// Test-only hook; do not call from production code. Drives the REAL TrySend
|
||||||
|
// arm path: with NotifyPluginDisclosure on and a PUA glyph in the buffer the
|
||||||
|
// first call arms and HOLDS (no send). Returns whether the buffer is armed.
|
||||||
|
// The caller asserts PendingMessage is unchanged (held) so a regressed wiring
|
||||||
|
// that fell through to ChatBox.SendMessageUnsafe is caught.
|
||||||
|
internal bool TestTryArmDisclosureForSelfTest(Tab? activeTab)
|
||||||
|
{
|
||||||
|
TrySend(activeTab);
|
||||||
|
return _disclosureArmedBuffer is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-only hook; do not call from production code. Clears the armed buffer
|
||||||
|
// so a SelfTest leaves no residual arm state.
|
||||||
|
internal void TestResetDisclosureForSelfTest() => _disclosureArmedBuffer = null;
|
||||||
|
|
||||||
private void DrawAutoCompletePopup()
|
private void DrawAutoCompletePopup()
|
||||||
{
|
{
|
||||||
if (_autoCompleteInfo == null)
|
if (_autoCompleteInfo == null)
|
||||||
|
|||||||
@@ -88,12 +88,25 @@ internal sealed class MessageList
|
|||||||
|
|
||||||
private void DrawCompactRow(Message message)
|
private void DrawCompactRow(Message message)
|
||||||
{
|
{
|
||||||
|
// B2-1/B2-2: render the sender through DrawChunks (the name-aware path
|
||||||
|
// that applies WorldSuffixMode/NameFormMode via ForDisplay), not as a
|
||||||
|
// flat SenderSource.TextValue string. message.Sender already carries the
|
||||||
|
// channel brackets/colon as ChunkSource.None wrappers (MessageManager
|
||||||
|
// .cs:300-314), so the separator is rendered by the chunks. 1.5.6 parity
|
||||||
|
// (ChatLogWindow.cs:1965: DrawChunks(message.Sender) + SameLine).
|
||||||
var timestamp = FormatTimestamp(message.Date);
|
var timestamp = FormatTimestamp(message.Date);
|
||||||
var sender = message.SenderSource.TextValue;
|
if (message.Sender.Count > 0)
|
||||||
ImGui.TextUnformatted(
|
{
|
||||||
string.IsNullOrEmpty(sender) ? timestamp : $"{timestamp} {sender}: "
|
ImGui.TextUnformatted($"{timestamp} ");
|
||||||
);
|
ImGui.SameLine(0f, 0f);
|
||||||
ImGui.SameLine(0f, 0f);
|
_chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f);
|
||||||
|
ImGui.SameLine(0f, 0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(timestamp);
|
||||||
|
ImGui.SameLine(0f, 0f);
|
||||||
|
}
|
||||||
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
|
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,9 +141,22 @@ internal sealed class MessageList
|
|||||||
|
|
||||||
private void DrawCardRow(Message message)
|
private void DrawCardRow(Message message)
|
||||||
{
|
{
|
||||||
|
// B2-1/B2-2: sender via DrawChunks (name-aware path), on its own line
|
||||||
|
// with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no
|
||||||
|
// SameLine after the sender). The 1.5.6 channel-colour push on the
|
||||||
|
// sender is deferred styling polish (masterplan §6 -> v1.9.0); plain
|
||||||
|
// text here.
|
||||||
var timestamp = FormatTimestamp(message.Date);
|
var timestamp = FormatTimestamp(message.Date);
|
||||||
var sender = message.SenderSource.TextValue;
|
if (message.Sender.Count > 0)
|
||||||
ImGui.TextUnformatted(string.IsNullOrEmpty(sender) ? timestamp : $"{timestamp} {sender}");
|
{
|
||||||
|
ImGui.TextUnformatted($"{timestamp} ");
|
||||||
|
ImGui.SameLine(0f, 0f);
|
||||||
|
_chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(timestamp);
|
||||||
|
}
|
||||||
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
|
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.Ui.Components.Settings.Tabs;
|
namespace HellionChat.Ui.Components.Settings.Tabs;
|
||||||
|
|
||||||
@@ -36,6 +38,8 @@ internal sealed class ChatTab
|
|||||||
() => Plugin.Config.HideSameTimestamps,
|
() => Plugin.Config.HideSameTimestamps,
|
||||||
v => Plugin.Config.HideSameTimestamps = v
|
v => Plugin.Config.HideSameTimestamps = v
|
||||||
);
|
);
|
||||||
|
DrawWorldSuffixCombo();
|
||||||
|
DrawNameFormCombo();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.CollapsingHeader("Channel filter"))
|
if (ImGui.CollapsingHeader("Channel filter"))
|
||||||
@@ -52,6 +56,16 @@ internal sealed class ChatTab
|
|||||||
{
|
{
|
||||||
DrawCommandHelpSideCombo();
|
DrawCommandHelpSideCombo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ImGui.CollapsingHeader("Plugin disclosure"))
|
||||||
|
{
|
||||||
|
DrawToggle(
|
||||||
|
HellionStrings.Settings_Chat_NotifyPluginDisclosure_Name,
|
||||||
|
() => Plugin.Config.NotifyPluginDisclosure,
|
||||||
|
v => Plugin.Config.NotifyPluginDisclosure = v
|
||||||
|
);
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyPluginDisclosure_Description);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawPrivacyPersistChannels()
|
private void DrawPrivacyPersistChannels()
|
||||||
@@ -101,6 +115,68 @@ internal sealed class ChatTab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawWorldSuffixCombo()
|
||||||
|
{
|
||||||
|
var current = Plugin.Config.WorldSuffixMode;
|
||||||
|
var values = Enum.GetValues<WorldSuffixMode>();
|
||||||
|
var labels = new string[values.Length];
|
||||||
|
var selected = 0;
|
||||||
|
for (var i = 0; i < values.Length; i++)
|
||||||
|
{
|
||||||
|
labels[i] = values[i].Name();
|
||||||
|
if (values[i] == current)
|
||||||
|
{
|
||||||
|
selected = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetNextItemWidth(200);
|
||||||
|
if (
|
||||||
|
ImGui.Combo(
|
||||||
|
HellionStrings.Settings_Chat_WorldSuffix_Name,
|
||||||
|
ref selected,
|
||||||
|
labels,
|
||||||
|
labels.Length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Plugin.Config.WorldSuffixMode = values[selected];
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_WorldSuffix_Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNameFormCombo()
|
||||||
|
{
|
||||||
|
var current = Plugin.Config.NameFormMode;
|
||||||
|
var values = Enum.GetValues<NameFormMode>();
|
||||||
|
var labels = new string[values.Length];
|
||||||
|
var selected = 0;
|
||||||
|
for (var i = 0; i < values.Length; i++)
|
||||||
|
{
|
||||||
|
labels[i] = values[i].Name();
|
||||||
|
if (values[i] == current)
|
||||||
|
{
|
||||||
|
selected = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetNextItemWidth(200);
|
||||||
|
if (
|
||||||
|
ImGui.Combo(
|
||||||
|
HellionStrings.Settings_Chat_NameForm_Name,
|
||||||
|
ref selected,
|
||||||
|
labels,
|
||||||
|
labels.Length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Plugin.Config.NameFormMode = values[selected];
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NameForm_Description);
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawToggle(string label, Func<bool> get, Action<bool> set)
|
private void DrawToggle(string label, Func<bool> get, Action<bool> set)
|
||||||
{
|
{
|
||||||
var current = get();
|
var current = get();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"Author": "Jon Kazama (Hellion Forge)",
|
"Author": "Jon Kazama (Hellion Forge)",
|
||||||
"Name": "Hellion Chat",
|
"Name": "Hellion Chat",
|
||||||
"InternalName": "HellionChat",
|
"InternalName": "HellionChat",
|
||||||
"AssemblyVersion": "1.8.2.0",
|
"AssemblyVersion": "1.8.3.0",
|
||||||
"Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with four profiles: Privacy-First, Casual, Roleplay, Full History\n- Multi-language UI (24 locales) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR",
|
"Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with four profiles: Privacy-First, Casual, Roleplay, Full History\n- Multi-language UI (24 locales) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
|
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
||||||
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
||||||
"DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
"DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.5.6/latest.zip",
|
||||||
"TestingAssemblyVersion": "1.8.2.0",
|
"TestingAssemblyVersion": "1.8.3.0",
|
||||||
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
|
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
|
||||||
"ImageUrls": [
|
"ImageUrls": [
|
||||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
|
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
|
||||||
|
|||||||
Reference in New Issue
Block a user