Add first-run wizard with three privacy profiles

Fresh installs now open a setup window on first plugin load that
asks the user to pick one of three starting profiles. Existing
ChatTwo users keep skipping the wizard because the v6→v7 migration
sets Configuration.FirstRunCompleted = true on the same pass that
seeds the Privacy-First defaults — they already saw the migration
notification and can reopen the wizard from the Privacy tab if
they want to choose differently.

The three profiles map to concrete configuration sets:

  Privacy-First (recommended): own-conversation whitelist (30
  channels), retention enabled with the spec defaults (Tells 365
  days, own-conversation channels 90, fallback 30).

  Casual: Privacy-First plus public chat (Say/Shout/Yell, both
  emote types, Novice Network) with a 1-day retention window so
  RP players can scroll back the last scene without keeping
  third-party speech forever.

  Full History: filter off, retention off, GDPR warning shown
  inline. Behaves like upstream Chat 2.

The wizard window is non-modal but covers a wide layout (three
side-by-side cards) and closing it without picking anything is
treated as accepting whatever defaults are already in place. The
Privacy tab gains a "show wizard again" button at the top so the
choice is reversible.
This commit is contained in:
2026-05-01 20:30:25 +02:00
parent 5b33a21d15
commit 33cfc7effa
8 changed files with 289 additions and 0 deletions
+6
View File
@@ -62,6 +62,10 @@ public class Configuration : IPluginConfiguration
public Dictionary<ChatType, int> RetentionPerChannelDays = []; public Dictionary<ChatType, int> RetentionPerChannelDays = [];
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue; public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
// Hellion Chat first-run wizard — opens once on a fresh install. Existing
// ChatTwo users skip it because the v6→v7 migration sets the flag.
public bool FirstRunCompleted;
public int GetRetentionDays(ChatType type) public int GetRetentionDays(ChatType type)
{ {
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride)) if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
@@ -238,6 +242,8 @@ public class Configuration : IPluginConfiguration
RetentionDefaultDays = other.RetentionDefaultDays; RetentionDefaultDays = other.RetentionDefaultDays;
RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value); RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value);
RetentionLastRunAt = other.RetentionLastRunAt; RetentionLastRunAt = other.RetentionLastRunAt;
FirstRunCompleted = other.FirstRunCompleted;
} }
} }
+13
View File
@@ -52,6 +52,7 @@ public sealed class Plugin : IDalamudPlugin
public InputPreview InputPreview { get; } public InputPreview InputPreview { get; }
public CommandHelpWindow CommandHelpWindow { get; } public CommandHelpWindow CommandHelpWindow { get; }
public SeStringDebugger SeStringDebugger { get; } public SeStringDebugger SeStringDebugger { get; }
public FirstRunWizard FirstRunWizard { get; }
public DebuggerWindow DebuggerWindow { get; } public DebuggerWindow DebuggerWindow { get; }
internal Commands Commands { get; } internal Commands Commands { get; }
@@ -124,6 +125,11 @@ public sealed class Plugin : IDalamudPlugin
Config.PrivacyFilterEnabled = true; Config.PrivacyFilterEnabled = true;
Config.PrivacyPersistChannels = [..Privacy.PrivacyDefaults.PrivacyFirstWhitelist]; Config.PrivacyPersistChannels = [..Privacy.PrivacyDefaults.PrivacyFirstWhitelist];
Config.PrivacyPersistUnknownChannels = false; Config.PrivacyPersistUnknownChannels = false;
// Existing ChatTwo users skip the first-run wizard — the
// migration toast already explains what changed and they
// can reopen the wizard from Settings → Privacy if they
// want to pick a different profile.
Config.FirstRunCompleted = true;
Config.Version = 7; Config.Version = 7;
SaveConfig(); SaveConfig();
@@ -168,6 +174,7 @@ public sealed class Plugin : IDalamudPlugin
CommandHelpWindow = new CommandHelpWindow(ChatLogWindow); CommandHelpWindow = new CommandHelpWindow(ChatLogWindow);
SeStringDebugger = new SeStringDebugger(this); SeStringDebugger = new SeStringDebugger(this);
DebuggerWindow = new DebuggerWindow(this); DebuggerWindow = new DebuggerWindow(this);
FirstRunWizard = new FirstRunWizard(this);
WindowSystem.AddWindow(ChatLogWindow); WindowSystem.AddWindow(ChatLogWindow);
WindowSystem.AddWindow(SettingsWindow); WindowSystem.AddWindow(SettingsWindow);
@@ -176,6 +183,12 @@ public sealed class Plugin : IDalamudPlugin
WindowSystem.AddWindow(CommandHelpWindow); WindowSystem.AddWindow(CommandHelpWindow);
WindowSystem.AddWindow(SeStringDebugger); WindowSystem.AddWindow(SeStringDebugger);
WindowSystem.AddWindow(DebuggerWindow); WindowSystem.AddWindow(DebuggerWindow);
WindowSystem.AddWindow(FirstRunWizard);
// Open the wizard on a fresh install. Existing ChatTwo users have
// FirstRunCompleted set to true by the v6→v7 migration above.
if (!Config.FirstRunCompleted)
FirstRunWizard.IsOpen = true;
FontManager.BuildFonts(); FontManager.BuildFonts();
+24
View File
@@ -84,4 +84,28 @@ internal static class PrivacyDefaults
[ChatType.ExtraChatLinkshell7] = 90, [ChatType.ExtraChatLinkshell7] = 90,
[ChatType.ExtraChatLinkshell8] = 90, [ChatType.ExtraChatLinkshell8] = 90,
}; };
// Casual profile = Privacy-First plus public chat (Say/Shout/Yell, both
// emote types, Novice Network), kept for a short 24-hour window so the
// last RP scene or shout trade is still searchable but third-party data
// doesn't accumulate forever.
internal static readonly IReadOnlySet<ChatType> CasualWhitelist = new HashSet<ChatType>(PrivacyFirstWhitelist)
{
ChatType.Say,
ChatType.Shout,
ChatType.Yell,
ChatType.CustomEmote,
ChatType.StandardEmote,
ChatType.NoviceNetwork,
};
internal static readonly IReadOnlyDictionary<ChatType, int> CasualRetentionOverrides = new Dictionary<ChatType, int>
{
[ChatType.Say] = 1,
[ChatType.Shout] = 1,
[ChatType.Yell] = 1,
[ChatType.CustomEmote] = 1,
[ChatType.StandardEmote] = 1,
[ChatType.NoviceNetwork] = 1,
};
} }
+14
View File
@@ -99,4 +99,18 @@ internal class HellionStrings
internal static string Migration_Notification_Title => Get(nameof(Migration_Notification_Title)); internal static string Migration_Notification_Title => Get(nameof(Migration_Notification_Title));
internal static string Migration_Notification_Content => Get(nameof(Migration_Notification_Content)); internal static string Migration_Notification_Content => Get(nameof(Migration_Notification_Content));
internal static string Wizard_Title => Get(nameof(Wizard_Title));
internal static string Wizard_Intro => Get(nameof(Wizard_Intro));
internal static string Wizard_Profile_PrivacyFirst_Heading => Get(nameof(Wizard_Profile_PrivacyFirst_Heading));
internal static string Wizard_Profile_PrivacyFirst_Description => Get(nameof(Wizard_Profile_PrivacyFirst_Description));
internal static string Wizard_Profile_PrivacyFirst_Apply => Get(nameof(Wizard_Profile_PrivacyFirst_Apply));
internal static string Wizard_Profile_Casual_Heading => Get(nameof(Wizard_Profile_Casual_Heading));
internal static string Wizard_Profile_Casual_Description => Get(nameof(Wizard_Profile_Casual_Description));
internal static string Wizard_Profile_Casual_Apply => Get(nameof(Wizard_Profile_Casual_Apply));
internal static string Wizard_Profile_FullHistory_Heading => Get(nameof(Wizard_Profile_FullHistory_Heading));
internal static string Wizard_Profile_FullHistory_Description => Get(nameof(Wizard_Profile_FullHistory_Description));
internal static string Wizard_Profile_FullHistory_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
} }
+39
View File
@@ -177,4 +177,43 @@
<data name="Migration_Notification_Content" xml:space="preserve"> <data name="Migration_Notification_Content" xml:space="preserve">
<value>Datenschutz-Filter ist standardmäßig aktiviert. Einstellungen → Datenschutz zum Anpassen.</value> <value>Datenschutz-Filter ist standardmäßig aktiviert. Einstellungen → Datenschutz zum Anpassen.</value>
</data> </data>
<data name="Wizard_Title" xml:space="preserve">
<value>Hellion Chat — Willkommen</value>
</data>
<data name="Wizard_Intro" xml:space="preserve">
<value>Wähle ein Start-Profil. Du kannst später alles unter Einstellungen → Datenschutz anpassen.</value>
</data>
<data name="Wizard_Profile_PrivacyFirst_Heading" xml:space="preserve">
<value>Datensparsamkeit (empfohlen)</value>
</data>
<data name="Wizard_Profile_PrivacyFirst_Description" xml:space="preserve">
<value>Es werden nur deine eigenen Konversationen gespeichert: Tells, Gruppe, FC, Linkshells, Cross-World-Linkshells, Allianz und ExtraChat. Öffentlicher Chat, NPC-Dialoge und System-Spam werden auf der Storage-Ebene verworfen. Aufbewahrung nach Spec-Defaults (Tells 365 Tage, eigene Konversations-Kanäle 90 Tage).</value>
</data>
<data name="Wizard_Profile_PrivacyFirst_Apply" xml:space="preserve">
<value>Datensparsamkeit übernehmen</value>
</data>
<data name="Wizard_Profile_Casual_Heading" xml:space="preserve">
<value>Locker</value>
</data>
<data name="Wizard_Profile_Casual_Description" xml:space="preserve">
<value>Datensparsamkeit plus ein 24-Stunden-Fenster für öffentlichen Chat (Sagen, Schreien, Rufen, beide Emote-Typen, Anfänger-Netzwerk). Für RP-Spieler, die die letzte Szene nochmal nachlesen wollen, ohne öffentlichen Chat ewig zu behalten.</value>
</data>
<data name="Wizard_Profile_Casual_Apply" xml:space="preserve">
<value>Locker übernehmen</value>
</data>
<data name="Wizard_Profile_FullHistory_Heading" xml:space="preserve">
<value>Volle Historie</value>
</data>
<data name="Wizard_Profile_FullHistory_Description" xml:space="preserve">
<value>Deaktiviert den Datenschutz-Filter komplett. Speichert alles außer Battle-Logs, wie das Original-Chat 2. Aufbewahrung ist AUS, die Historie wächst dauerhaft.</value>
</data>
<data name="Wizard_Profile_FullHistory_GdprWarning" xml:space="preserve">
<value>DSGVO-Hinweis: Wenn du Nachrichten Dritter (Sagen/Schreien/Rufen fremder Spieler, NPC-Dialoge mit Spielernamen usw.) zeitlich unbegrenzt speicherst, kann das die Ausnahme für rein persönliche oder familiäre Tätigkeiten (Art. 2 Abs. 2 Buchst. c) sprengen. Nutze dieses Profil nur, wenn du einen klaren Grund hast, das volle Archiv zu behalten.</value>
</data>
<data name="Wizard_Profile_FullHistory_Apply" xml:space="preserve">
<value>Volle Historie übernehmen</value>
</data>
<data name="Wizard_Reopen_Button" xml:space="preserve">
<value>Wizard erneut zeigen</value>
</data>
</root> </root>
+39
View File
@@ -177,4 +177,43 @@
<data name="Migration_Notification_Content" xml:space="preserve"> <data name="Migration_Notification_Content" xml:space="preserve">
<value>Privacy filter activated by default. Settings → Privacy to adjust.</value> <value>Privacy filter activated by default. Settings → Privacy to adjust.</value>
</data> </data>
<data name="Wizard_Title" xml:space="preserve">
<value>Hellion Chat — Welcome</value>
</data>
<data name="Wizard_Intro" xml:space="preserve">
<value>Pick a starting profile. You can change anything later under Settings → Privacy.</value>
</data>
<data name="Wizard_Profile_PrivacyFirst_Heading" xml:space="preserve">
<value>Privacy-First (recommended)</value>
</data>
<data name="Wizard_Profile_PrivacyFirst_Description" xml:space="preserve">
<value>Only your own conversations are stored: Tells, Party, FC, Linkshells, Cross-World Linkshells, Alliance and ExtraChat. Public chat, NPC dialogue and system spam are dropped at the storage layer. Retention follows the spec defaults (Tells 365 days, own-conversation channels 90 days).</value>
</data>
<data name="Wizard_Profile_PrivacyFirst_Apply" xml:space="preserve">
<value>Use Privacy-First</value>
</data>
<data name="Wizard_Profile_Casual_Heading" xml:space="preserve">
<value>Casual</value>
</data>
<data name="Wizard_Profile_Casual_Description" xml:space="preserve">
<value>Privacy-First plus a 24-hour window for public chat (Say, Shout, Yell, both emote types, Novice Network). For RP players who want to look up the last scene without keeping public chat forever.</value>
</data>
<data name="Wizard_Profile_Casual_Apply" xml:space="preserve">
<value>Use Casual</value>
</data>
<data name="Wizard_Profile_FullHistory_Heading" xml:space="preserve">
<value>Full History</value>
</data>
<data name="Wizard_Profile_FullHistory_Description" xml:space="preserve">
<value>Disables the privacy filter entirely. Stores everything except battle logs, just like upstream Chat 2. Retention is OFF, history grows forever.</value>
</data>
<data name="Wizard_Profile_FullHistory_GdprWarning" xml:space="preserve">
<value>GDPR notice: storing third-party messages (Say/Shout/Yell of strangers, NPC dialogue with player names, etc.) for an unlimited time may exceed the personal/household exemption (Art. 2(2)(c)). Use this profile only if you have a clear reason to keep the full archive.</value>
</data>
<data name="Wizard_Profile_FullHistory_Apply" xml:space="preserve">
<value>Use Full History</value>
</data>
<data name="Wizard_Reopen_Button" xml:space="preserve">
<value>Show wizard again</value>
</data>
</root> </root>
+150
View File
@@ -0,0 +1,150 @@
using System.Numerics;
using ChatTwo.Code;
using ChatTwo.Privacy;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui;
public sealed class FirstRunWizard : Window
{
private readonly Plugin Plugin;
internal FirstRunWizard(Plugin plugin) : base($"{HellionStrings.Wizard_Title}###hellion-firstrun")
{
Plugin = plugin;
Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking;
SizeCondition = ImGuiCond.Appearing;
Size = new Vector2(900, 560);
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(720, 480),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
};
}
public override void OnClose()
{
// Closing the wizard without picking anything = the user accepts
// whatever defaults are already in place. Mark as complete so we
// don't pester them again on the next launch.
if (!Plugin.Config.FirstRunCompleted)
{
Plugin.Config.FirstRunCompleted = true;
Plugin.SaveConfig();
}
}
public override void Draw()
{
ImGui.TextWrapped(HellionStrings.Wizard_Intro);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
var avail = ImGui.GetContentRegionAvail();
var cardWidth = (avail.X - ImGui.GetStyle().ItemSpacing.X * 2) / 3f;
var cardHeight = avail.Y - ImGui.GetTextLineHeightWithSpacing();
DrawCard("privacy-first", cardWidth, cardHeight,
HellionStrings.Wizard_Profile_PrivacyFirst_Heading,
HellionStrings.Wizard_Profile_PrivacyFirst_Description,
null,
HellionStrings.Wizard_Profile_PrivacyFirst_Apply,
ApplyPrivacyFirst);
ImGui.SameLine();
DrawCard("casual", cardWidth, cardHeight,
HellionStrings.Wizard_Profile_Casual_Heading,
HellionStrings.Wizard_Profile_Casual_Description,
null,
HellionStrings.Wizard_Profile_Casual_Apply,
ApplyCasual);
ImGui.SameLine();
DrawCard("full-history", cardWidth, cardHeight,
HellionStrings.Wizard_Profile_FullHistory_Heading,
HellionStrings.Wizard_Profile_FullHistory_Description,
HellionStrings.Wizard_Profile_FullHistory_GdprWarning,
HellionStrings.Wizard_Profile_FullHistory_Apply,
ApplyFullHistory);
}
private void DrawCard(string id, float width, float height, string heading, string description, string? warning, string buttonLabel, Action onApply)
{
using var child = ImRaii.Child($"##wizard-card-{id}", new Vector2(width, height), true);
if (!child.Success)
return;
ImGui.TextUnformatted(heading);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextWrapped(description);
if (warning is not null)
{
ImGui.Spacing();
ImGuiUtil.WarningText(warning);
}
// Push the button to the bottom of the card.
var lineHeight = ImGui.GetFrameHeightWithSpacing();
var remaining = ImGui.GetContentRegionAvail().Y - lineHeight;
if (remaining > 0)
ImGui.Dummy(new Vector2(0, remaining));
if (ImGui.Button($"{buttonLabel}##{id}", new Vector2(-1, 0)))
{
onApply();
Plugin.Config.FirstRunCompleted = true;
Plugin.SaveConfig();
IsOpen = false;
}
}
private void ApplyPrivacyFirst()
{
Plugin.Config.PrivacyFilterEnabled = true;
Plugin.Config.PrivacyPersistChannels = [..PrivacyDefaults.PrivacyFirstWhitelist];
Plugin.Config.PrivacyPersistUnknownChannels = false;
Plugin.Config.RetentionEnabled = true;
Plugin.Config.RetentionDefaultDays = 30;
Plugin.Config.RetentionPerChannelDays =
PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value);
}
private void ApplyCasual()
{
Plugin.Config.PrivacyFilterEnabled = true;
Plugin.Config.PrivacyPersistChannels = [..PrivacyDefaults.CasualWhitelist];
Plugin.Config.PrivacyPersistUnknownChannels = false;
Plugin.Config.RetentionEnabled = true;
Plugin.Config.RetentionDefaultDays = 30;
var policy = PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value);
foreach (var (type, days) in PrivacyDefaults.CasualRetentionOverrides)
policy[type] = days;
Plugin.Config.RetentionPerChannelDays = policy;
}
private void ApplyFullHistory()
{
// Full history = upstream Chat 2 behavior. Filter off, retention off,
// everything (except battle messages, which Chat 2 itself controls)
// accumulates indefinitely.
Plugin.Config.PrivacyFilterEnabled = false;
Plugin.Config.PrivacyPersistUnknownChannels = true;
Plugin.Config.RetentionEnabled = false;
Plugin.Config.RetentionPerChannelDays.Clear();
}
}
+4
View File
@@ -58,6 +58,10 @@ internal sealed class Privacy : ISettingsTab
public void Draw(bool changed) public void Draw(bool changed)
{ {
if (ImGui.Button(HellionStrings.Wizard_Reopen_Button))
Plugin.FirstRunWizard.IsOpen = true;
ImGui.Spacing();
ImGuiUtil.OptionCheckbox( ImGuiUtil.OptionCheckbox(
ref Mutable.PrivacyFilterEnabled, ref Mutable.PrivacyFilterEnabled,
HellionStrings.Privacy_FilterEnabled_Name, HellionStrings.Privacy_FilterEnabled_Name,