diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index 8b4d18f..1b21657 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -62,6 +62,10 @@ public class Configuration : IPluginConfiguration public Dictionary RetentionPerChannelDays = []; 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) { if (RetentionPerChannelDays.TryGetValue(type, out var userOverride)) @@ -238,6 +242,8 @@ public class Configuration : IPluginConfiguration RetentionDefaultDays = other.RetentionDefaultDays; RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value); RetentionLastRunAt = other.RetentionLastRunAt; + + FirstRunCompleted = other.FirstRunCompleted; } } diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 28c1af0..25474d9 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -52,6 +52,7 @@ public sealed class Plugin : IDalamudPlugin public InputPreview InputPreview { get; } public CommandHelpWindow CommandHelpWindow { get; } public SeStringDebugger SeStringDebugger { get; } + public FirstRunWizard FirstRunWizard { get; } public DebuggerWindow DebuggerWindow { get; } internal Commands Commands { get; } @@ -124,6 +125,11 @@ public sealed class Plugin : IDalamudPlugin Config.PrivacyFilterEnabled = true; Config.PrivacyPersistChannels = [..Privacy.PrivacyDefaults.PrivacyFirstWhitelist]; 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; SaveConfig(); @@ -168,6 +174,7 @@ public sealed class Plugin : IDalamudPlugin CommandHelpWindow = new CommandHelpWindow(ChatLogWindow); SeStringDebugger = new SeStringDebugger(this); DebuggerWindow = new DebuggerWindow(this); + FirstRunWizard = new FirstRunWizard(this); WindowSystem.AddWindow(ChatLogWindow); WindowSystem.AddWindow(SettingsWindow); @@ -176,6 +183,12 @@ public sealed class Plugin : IDalamudPlugin WindowSystem.AddWindow(CommandHelpWindow); WindowSystem.AddWindow(SeStringDebugger); 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(); diff --git a/ChatTwo/Privacy/PrivacyDefaults.cs b/ChatTwo/Privacy/PrivacyDefaults.cs index 0007686..6e932ca 100644 --- a/ChatTwo/Privacy/PrivacyDefaults.cs +++ b/ChatTwo/Privacy/PrivacyDefaults.cs @@ -84,4 +84,28 @@ internal static class PrivacyDefaults [ChatType.ExtraChatLinkshell7] = 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 CasualWhitelist = new HashSet(PrivacyFirstWhitelist) + { + ChatType.Say, + ChatType.Shout, + ChatType.Yell, + ChatType.CustomEmote, + ChatType.StandardEmote, + ChatType.NoviceNetwork, + }; + + internal static readonly IReadOnlyDictionary CasualRetentionOverrides = new Dictionary + { + [ChatType.Say] = 1, + [ChatType.Shout] = 1, + [ChatType.Yell] = 1, + [ChatType.CustomEmote] = 1, + [ChatType.StandardEmote] = 1, + [ChatType.NoviceNetwork] = 1, + }; } diff --git a/ChatTwo/Resources/HellionStrings.Designer.cs b/ChatTwo/Resources/HellionStrings.Designer.cs index a8e9864..5fc0b9e 100644 --- a/ChatTwo/Resources/HellionStrings.Designer.cs +++ b/ChatTwo/Resources/HellionStrings.Designer.cs @@ -99,4 +99,18 @@ internal class HellionStrings 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 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)); } diff --git a/ChatTwo/Resources/HellionStrings.de.resx b/ChatTwo/Resources/HellionStrings.de.resx index 067db01..9c6f948 100644 --- a/ChatTwo/Resources/HellionStrings.de.resx +++ b/ChatTwo/Resources/HellionStrings.de.resx @@ -177,4 +177,43 @@ Datenschutz-Filter ist standardmäßig aktiviert. Einstellungen → Datenschutz zum Anpassen. + + Hellion Chat — Willkommen + + + Wähle ein Start-Profil. Du kannst später alles unter Einstellungen → Datenschutz anpassen. + + + Datensparsamkeit (empfohlen) + + + 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). + + + Datensparsamkeit übernehmen + + + Locker + + + 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. + + + Locker übernehmen + + + Volle Historie + + + Deaktiviert den Datenschutz-Filter komplett. Speichert alles außer Battle-Logs, wie das Original-Chat 2. Aufbewahrung ist AUS, die Historie wächst dauerhaft. + + + 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. + + + Volle Historie übernehmen + + + Wizard erneut zeigen + diff --git a/ChatTwo/Resources/HellionStrings.resx b/ChatTwo/Resources/HellionStrings.resx index c8a791d..6814810 100644 --- a/ChatTwo/Resources/HellionStrings.resx +++ b/ChatTwo/Resources/HellionStrings.resx @@ -177,4 +177,43 @@ Privacy filter activated by default. Settings → Privacy to adjust. + + Hellion Chat — Welcome + + + Pick a starting profile. You can change anything later under Settings → Privacy. + + + Privacy-First (recommended) + + + 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). + + + Use Privacy-First + + + Casual + + + 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. + + + Use Casual + + + Full History + + + Disables the privacy filter entirely. Stores everything except battle logs, just like upstream Chat 2. Retention is OFF, history grows forever. + + + 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. + + + Use Full History + + + Show wizard again + diff --git a/ChatTwo/Ui/FirstRunWizard.cs b/ChatTwo/Ui/FirstRunWizard.cs new file mode 100644 index 0000000..99928e7 --- /dev/null +++ b/ChatTwo/Ui/FirstRunWizard.cs @@ -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(); + } +} diff --git a/ChatTwo/Ui/SettingsTabs/Privacy.cs b/ChatTwo/Ui/SettingsTabs/Privacy.cs index c2eefca..4ed8a97 100644 --- a/ChatTwo/Ui/SettingsTabs/Privacy.cs +++ b/ChatTwo/Ui/SettingsTabs/Privacy.cs @@ -58,6 +58,10 @@ internal sealed class Privacy : ISettingsTab public void Draw(bool changed) { + if (ImGui.Button(HellionStrings.Wizard_Reopen_Button)) + Plugin.FirstRunWizard.IsOpen = true; + ImGui.Spacing(); + ImGuiUtil.OptionCheckbox( ref Mutable.PrivacyFilterEnabled, HellionStrings.Privacy_FilterEnabled_Name,