feat(ui): notify on failed tell via RaptureLogModule hook
This commit is contained in:
@@ -187,6 +187,8 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool CollapseKeepUniqueLinks;
|
public bool CollapseKeepUniqueLinks;
|
||||||
public bool SymbolPickerEnabled = true;
|
public bool SymbolPickerEnabled = true;
|
||||||
public bool PlaySounds = true;
|
public bool PlaySounds = true;
|
||||||
|
// UI-2: toast when a tell the user sent could not be delivered.
|
||||||
|
public bool NotifyFailedTell = 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;
|
||||||
@@ -282,6 +284,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
||||||
SymbolPickerEnabled = other.SymbolPickerEnabled;
|
SymbolPickerEnabled = other.SymbolPickerEnabled;
|
||||||
PlaySounds = other.PlaySounds;
|
PlaySounds = other.PlaySounds;
|
||||||
|
NotifyFailedTell = other.NotifyFailedTell;
|
||||||
KeepInputFocus = other.KeepInputFocus;
|
KeepInputFocus = other.KeepInputFocus;
|
||||||
MaxLinesToRender = other.MaxLinesToRender;
|
MaxLinesToRender = other.MaxLinesToRender;
|
||||||
Use24HourClock = other.Use24HourClock;
|
Use24HourClock = other.Use24HourClock;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
using HellionChat.Integrations;
|
||||||
using HellionChat.Ipc;
|
using HellionChat.Ipc;
|
||||||
using HellionChat.Themes;
|
using HellionChat.Themes;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -85,3 +86,18 @@ internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService s
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eager-resolve trigger: resolving FailedTellNotifier in this adapter's ctor
|
||||||
|
// enables its game hook during host startup. StartAsync itself is a no-op.
|
||||||
|
internal sealed class FailedTellNotifierInitHostedService(
|
||||||
|
FailedTellNotifier notifier) : IHostedService
|
||||||
|
{
|
||||||
|
// No-op adapter: the ctor dependency above is the actual eager-resolve
|
||||||
|
// trigger. Field kept to match the IpcManager/TypingIpc/ExtraChat no-op
|
||||||
|
// adapters and to avoid the CS9113 unread-parameter warning.
|
||||||
|
private readonly FailedTellNotifier _notifier = notifier;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
using HellionChat._Helpers;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Integrations;
|
||||||
|
|
||||||
|
// UI-2: a minimal, failed-tell-specific game hook. A locale-robust "tell
|
||||||
|
// failed" signal is not reachable over the processed message stream (Message
|
||||||
|
// carries no LogMessage row id, ChatCode 60 is too broad). This hooks the one
|
||||||
|
// ShowLogMessageString overload and toasts on a pinned id set. It is NOT the
|
||||||
|
// broad ad-block hook layer — that stays v1.5.6.
|
||||||
|
internal sealed class FailedTellNotifier : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<FailedTellNotifier> _logger;
|
||||||
|
private readonly Hook<RaptureLogModule.Delegates.ShowLogMessageString>? _hook;
|
||||||
|
|
||||||
|
public unsafe FailedTellNotifier(ILogger<FailedTellNotifier> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Creating/enabling a hook is safe off the framework thread (the
|
||||||
|
// ctor runs during host startup on the framework thread,
|
||||||
|
// eager-resolved via FailedTellNotifierInitHostedService).
|
||||||
|
_hook = Plugin.GameInteropProvider
|
||||||
|
.HookFromAddress<RaptureLogModule.Delegates.ShowLogMessageString>(
|
||||||
|
RaptureLogModule.MemberFunctionPointers.ShowLogMessageString,
|
||||||
|
ShowLogMessageStringDetour);
|
||||||
|
_hook.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void ShowLogMessageStringDetour(
|
||||||
|
RaptureLogModule* module, uint logMessageId, Utf8String* value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// DISCOVERY (Task 2 Step 9): while the id set is empty, log every
|
||||||
|
// call so a failed tell can be triggered in-game and its id read.
|
||||||
|
// Remove this block once FailedTellLogMessageIds is pinned.
|
||||||
|
_logger.LogInformation(
|
||||||
|
"ShowLogMessageString id={Id} value={Value}",
|
||||||
|
logMessageId,
|
||||||
|
value is null ? "<null>" : value->ToString());
|
||||||
|
|
||||||
|
if (FailedTellMatcher.ShouldNotify(
|
||||||
|
logMessageId, Plugin.Config.NotifyFailedTell,
|
||||||
|
FailedTellMatcher.FailedTellLogMessageIds))
|
||||||
|
{
|
||||||
|
var recipient = value is null ? string.Empty : value->ToString();
|
||||||
|
var content = string.IsNullOrEmpty(recipient)
|
||||||
|
? HellionStrings.FailedTell_Notification_Generic
|
||||||
|
: string.Format(HellionStrings.FailedTell_Notification_Named, recipient);
|
||||||
|
WrapperUtil.AddNotification(content, NotificationType.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "FailedTellNotifier detour threw");
|
||||||
|
}
|
||||||
|
|
||||||
|
_hook!.Original(module, logMessageId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_hook?.Disable();
|
||||||
|
_hook?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,6 +107,8 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<ILogger<Integrations.HonorificService>>(),
|
sp.GetRequiredService<ILogger<Integrations.HonorificService>>(),
|
||||||
sp.GetRequiredService<IFramework>()
|
sp.GetRequiredService<IFramework>()
|
||||||
));
|
));
|
||||||
|
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
||||||
|
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new MessageManager(
|
services.AddSingleton(sp => new MessageManager(
|
||||||
sp.GetRequiredService<Plugin>(),
|
sp.GetRequiredService<Plugin>(),
|
||||||
@@ -172,6 +174,8 @@ internal static class PluginHostFactory
|
|||||||
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
||||||
sp.GetRequiredService<AutoTellTabsService>()
|
sp.GetRequiredService<AutoTellTabsService>()
|
||||||
));
|
));
|
||||||
|
services.AddHostedService(sp => new Infrastructure.Hosting.FailedTellNotifierInitHostedService(
|
||||||
|
sp.GetRequiredService<Integrations.FailedTellNotifier>()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
@@ -451,4 +451,20 @@ internal class HellionStrings
|
|||||||
internal static string Settings_QuickPicker_Tabs_Header => Get(nameof(Settings_QuickPicker_Tabs_Header));
|
internal static string Settings_QuickPicker_Tabs_Header => Get(nameof(Settings_QuickPicker_Tabs_Header));
|
||||||
internal static string Settings_ThemeAndLayout_ReduceMotion_Name => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Name));
|
internal static string Settings_ThemeAndLayout_ReduceMotion_Name => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Name));
|
||||||
internal static string Settings_ThemeAndLayout_ReduceMotion_Description => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Description));
|
internal static string Settings_ThemeAndLayout_ReduceMotion_Description => Get(nameof(Settings_ThemeAndLayout_ReduceMotion_Description));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.5.5 UI-2 Failed Tell Notification
|
||||||
|
internal static string FailedTell_Notification_Generic => Get(nameof(FailedTell_Notification_Generic));
|
||||||
|
internal static string FailedTell_Notification_Named => Get(nameof(FailedTell_Notification_Named));
|
||||||
|
internal static string Settings_Chat_NotifyFailedTell_Name => Get(nameof(Settings_Chat_NotifyFailedTell_Name));
|
||||||
|
internal static string Settings_Chat_NotifyFailedTell_Description => Get(nameof(Settings_Chat_NotifyFailedTell_Description));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.5.5 UI-3 Per-Tab Notification Sound
|
||||||
|
internal static string Tabs_NotificationSound_Enable_Name => Get(nameof(Tabs_NotificationSound_Enable_Name));
|
||||||
|
internal static string Tabs_NotificationSound_Description => Get(nameof(Tabs_NotificationSound_Description));
|
||||||
|
internal static string Tabs_NotificationSound_Option => Get(nameof(Tabs_NotificationSound_Option));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.5.5 UI-5a Scroll-to-bottom + UI-6 Linking
|
||||||
|
internal static string ChatLog_ScrollToBottom_Tooltip => Get(nameof(ChatLog_ScrollToBottom_Tooltip));
|
||||||
|
internal static string ChatLog_Insert_MapFlag => Get(nameof(ChatLog_Insert_MapFlag));
|
||||||
|
internal static string ChatLog_Insert_ItemLink => Get(nameof(ChatLog_Insert_ItemLink));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1048,4 +1048,40 @@
|
|||||||
<data name="Settings_ThemeAndLayout_ReduceMotion_Description" xml:space="preserve">
|
<data name="Settings_ThemeAndLayout_ReduceMotion_Description" xml:space="preserve">
|
||||||
<value>Disables the theme crossfade, the sidebar and card-row hover animations, and the unread-tab pulse. Theme switches and hover states apply instantly instead.</value>
|
<value>Disables the theme crossfade, the sidebar and card-row hover animations, and the unread-tab pulse. Theme switches and hover states apply instantly instead.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
|
<!-- Hellion Chat — v1.5.5 UI-2 Failed Tell Notification -->
|
||||||
|
<data name="FailedTell_Notification_Generic" xml:space="preserve">
|
||||||
|
<value>A tell could not be delivered.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FailedTell_Notification_Named" xml:space="preserve">
|
||||||
|
<value>Tell to {0} could not be delivered.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Chat_NotifyFailedTell_Name" xml:space="preserve">
|
||||||
|
<value>Notify on failed tell</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Chat_NotifyFailedTell_Description" xml:space="preserve">
|
||||||
|
<value>Show a toast when a tell you sent could not be delivered (recipient offline, in an instance, or blocking you).</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<!-- Hellion Chat — v1.5.5 UI-3 Per-Tab Notification Sound -->
|
||||||
|
<data name="Tabs_NotificationSound_Enable_Name" xml:space="preserve">
|
||||||
|
<value>Notification sound</value>
|
||||||
|
</data>
|
||||||
|
<data name="Tabs_NotificationSound_Description" xml:space="preserve">
|
||||||
|
<value>Play a sound when a message arrives in this tab while you are looking at a different tab. Respects the global sound toggle.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Tabs_NotificationSound_Option" xml:space="preserve">
|
||||||
|
<value>Sound</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<!-- Hellion Chat — v1.5.5 UI-5a Scroll-to-bottom + UI-6 Linking -->
|
||||||
|
<data name="ChatLog_ScrollToBottom_Tooltip" xml:space="preserve">
|
||||||
|
<value>Jump to the latest message</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatLog_Insert_MapFlag" xml:space="preserve">
|
||||||
|
<value>Insert map flag <flag></value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatLog_Insert_ItemLink" xml:space="preserve">
|
||||||
|
<value>Insert linked item <item></value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -145,6 +145,12 @@ internal sealed class Chat : ISettingsTab
|
|||||||
ref Mutable.SymbolPickerEnabled
|
ref Mutable.SymbolPickerEnabled
|
||||||
);
|
);
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_SymbolPicker_Enable_Description);
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_SymbolPicker_Enable_Description);
|
||||||
|
|
||||||
|
ImGui.Checkbox(
|
||||||
|
HellionStrings.Settings_Chat_NotifyFailedTell_Name,
|
||||||
|
ref Mutable.NotifyFailedTell
|
||||||
|
);
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyFailedTell_Description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace HellionChat._Helpers;
|
||||||
|
|
||||||
|
// UI-2 pure decision helper for failed-tell detection. The processed message
|
||||||
|
// stream carries no LogMessage row id, so detection happens at the
|
||||||
|
// RaptureLogModule level (see FailedTellNotifier). This POCO stays free of
|
||||||
|
// Dalamud types so the "known id AND enabled" rule is Build-Suite testable.
|
||||||
|
// TEST-MIRROR: ../../../Hellion Build test/Ui/FailedTellMatcherTests.cs
|
||||||
|
public static class FailedTellMatcher
|
||||||
|
{
|
||||||
|
// Log-message ids the game raises for a tell that could not be delivered.
|
||||||
|
// Pinned from discovery (see plan Task 2, Step 9). An empty set means the
|
||||||
|
// hook never toasts — "no toast over a false toast" until ids are pinned.
|
||||||
|
public static readonly IReadOnlySet<uint> FailedTellLogMessageIds = new HashSet<uint>
|
||||||
|
{
|
||||||
|
// pinned in Task 2 Step 9
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool ShouldNotify(uint logMessageId, bool notifyEnabled, IReadOnlySet<uint> failedTellIds)
|
||||||
|
=> notifyEnabled && failedTellIds.Contains(logMessageId);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user