diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs index 21c3bd0..2afee3b 100644 --- a/HellionChat/MessageStore.cs +++ b/HellionChat/MessageStore.cs @@ -499,24 +499,25 @@ internal class MessageStore : IDisposable // Returns a (ChatType, count) snapshot over non-deleted messages. // Used by the Privacy tab to preview retroactive cleanup impact. - internal Dictionary GetMessageCountsByChatType() + // + // Caller-owned connection, same reasoning as StreamForExport: this is a + // GROUP BY over every row, and holding _readLock for it would stall + // UpsertMessage on the framework thread for as long as the scan takes. + internal Dictionary GetMessageCountsByChatType(SqliteConnection conn) { - lock (_readLock) + var result = new Dictionary(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = + "SELECT ChatType, COUNT(*) FROM messages WHERE deleted = false GROUP BY ChatType;"; + cmd.CommandTimeout = 120; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) { - var result = new Dictionary(); - using var cmd = Connection.CreateCommand(); - cmd.CommandText = - "SELECT ChatType, COUNT(*) FROM messages WHERE deleted = false GROUP BY ChatType;"; - cmd.CommandTimeout = 120; - using var reader = cmd.ExecuteReader(); - while (reader.Read()) - { - var chatType = reader.GetInt32(0); - var count = reader.GetInt64(1); - result[chatType] = count; - } - return result; + var chatType = reader.GetInt32(0); + var count = reader.GetInt64(1); + result[chatType] = count; } + return result; } // Deletes messages older than the per-channel retention window, with a global diff --git a/HellionChat/Privacy/StorageRule.cs b/HellionChat/Privacy/StorageRule.cs index 5fb561f..8615364 100644 --- a/HellionChat/Privacy/StorageRule.cs +++ b/HellionChat/Privacy/StorageRule.cs @@ -29,4 +29,23 @@ internal static class StorageRule bool persistUnknownTypes, int listedCount ) => filterEnabled && persistUnknownTypes && listedCount == 0; + + // Why a retroactive cleanup cannot be offered, or that it can. + internal enum CleanupAvailability + { + Available, + + // Nothing is filtered, so nothing in the database contradicts the rule. + FilterDisabled, + + // The rule keeps no channel at all. CleanupRetainOnly refuses an empty + // allowlist on purpose -- that request is a full wipe, and a full wipe + // has its own button with its own confirmation. + NothingListed, + } + + internal static CleanupAvailability CleanupState(bool filterEnabled, int listedCount) => + !filterEnabled ? CleanupAvailability.FilterDisabled + : listedCount == 0 ? CleanupAvailability.NothingListed + : CleanupAvailability.Available; } diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index c57dec5..2ca6de7 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -60,6 +60,8 @@ internal class HellionStrings internal static string Privacy_PersistUnknown_Description => Get(nameof(Privacy_PersistUnknown_Description)); internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading)); + internal static string Cleanup_Unavailable_FilterOff => Get(nameof(Cleanup_Unavailable_FilterOff)); + internal static string Cleanup_Unavailable_NothingListed => Get(nameof(Cleanup_Unavailable_NothingListed)); internal static string Cleanup_Help_Intro => Get(nameof(Cleanup_Help_Intro)); internal static string Cleanup_Help_SavedNote => Get(nameof(Cleanup_Help_SavedNote)); internal static string Cleanup_Preview_Stale => Get(nameof(Cleanup_Preview_Stale)); diff --git a/HellionChat/Resources/HellionStrings.ca.resx b/HellionChat/Resources/HellionStrings.ca.resx index 73d9adb..2095e6f 100644 --- a/HellionChat/Resources/HellionStrings.ca.resx +++ b/HellionChat/Resources/HellionStrings.ca.resx @@ -1172,4 +1172,10 @@ esborrat de l'historial + + El filtre de privadesa està desactivat, així que es desa cada canal i res de la base de dades no contradiu la teva configuració. Activa el filtre i tria els canals primer. + + + No hi ha cap canal seleccionat, així que una neteja esborraria tot l'historial. Tria els canals que vols conservar, o fes servir el botó d'esborrar si de debò ho vols eliminar tot. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.cs.resx b/HellionChat/Resources/HellionStrings.cs.resx index 08fa793..42a7fdf 100644 --- a/HellionChat/Resources/HellionStrings.cs.resx +++ b/HellionChat/Resources/HellionStrings.cs.resx @@ -1171,4 +1171,10 @@ mazání historie + + Filtr soukromí je vypnutý, takže se ukládá každý kanál a nic v databázi neodporuje vašemu nastavení. Nejprve filtr zapněte a vyberte kanály. + + + Není vybrán žádný kanál, takže úklid by smazal celou historii. Vyberte kanály, které chcete zachovat, nebo použijte tlačítko pro vymazání, pokud opravdu chcete smazat vše. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.da.resx b/HellionChat/Resources/HellionStrings.da.resx index 9f028c9..f4bd284 100644 --- a/HellionChat/Resources/HellionStrings.da.resx +++ b/HellionChat/Resources/HellionStrings.da.resx @@ -1171,4 +1171,10 @@ sletning af historikken + + Privatlivsfilteret er slået fra, så alle kanaler gemmes, og intet i databasen strider mod dine indstillinger. Slå filteret til, og vælg kanaler først. + + + Ingen kanal er valgt, så en oprydning ville slette hele historikken. Vælg de kanaler, du vil beholde, eller brug sletteknappen, hvis du virkelig vil af med alt. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 59b1471..54c4595 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -1166,4 +1166,10 @@ Löschen des Verlaufs + + Der Datenschutzfilter ist aus, deshalb wird jeder Kanal gespeichert und nichts in der Datenbank widerspricht deinen Einstellungen. Schalte den Filter zuerst ein und wähle Kanäle aus. + + + Es ist kein Kanal ausgewählt, eine Bereinigung würde also den gesamten Verlauf löschen. Wähle die Kanäle aus, die du behalten willst, oder nimm den Löschen-Knopf, wenn wirklich alles weg soll. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.el.resx b/HellionChat/Resources/HellionStrings.el.resx index 35e5dfb..af91bc3 100644 --- a/HellionChat/Resources/HellionStrings.el.resx +++ b/HellionChat/Resources/HellionStrings.el.resx @@ -1171,4 +1171,10 @@ διαγραφή ιστορικού + + Το φίλτρο απορρήτου είναι απενεργοποιημένο, άρα αποθηκεύεται κάθε κανάλι και τίποτα στη βάση δεδομένων δεν έρχεται σε αντίθεση με τις ρυθμίσεις σας. Ενεργοποιήστε πρώτα το φίλτρο και επιλέξτε κανάλια. + + + Δεν έχει επιλεγεί κανένα κανάλι, οπότε μια εκκαθάριση θα διέγραφε όλο το ιστορικό. Επιλέξτε τα κανάλια που θέλετε να κρατήσετε ή χρησιμοποιήστε το κουμπί διαγραφής αν θέλετε πραγματικά να φύγουν όλα. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.es.resx b/HellionChat/Resources/HellionStrings.es.resx index ed433c9..1ff51f0 100644 --- a/HellionChat/Resources/HellionStrings.es.resx +++ b/HellionChat/Resources/HellionStrings.es.resx @@ -1172,4 +1172,10 @@ borrado del historial + + El filtro de privacidad está desactivado, así que se guarda cada canal y nada en la base de datos contradice tu configuración. Activa el filtro y elige canales primero. + + + No hay ningún canal seleccionado, así que una limpieza borraría todo el historial. Elige los canales que quieras conservar, o usa el botón de borrar si de verdad quieres eliminarlo todo. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fi.resx b/HellionChat/Resources/HellionStrings.fi.resx index f28a7ee..b9a4d51 100644 --- a/HellionChat/Resources/HellionStrings.fi.resx +++ b/HellionChat/Resources/HellionStrings.fi.resx @@ -1171,4 +1171,10 @@ historian tyhjennys + + Yksityisyyssuodatin on pois päältä, joten jokainen kanava tallennetaan eikä mikään tietokannassa ole ristiriidassa asetustesi kanssa. Kytke suodatin päälle ja valitse kanavat ensin. + + + Yhtään kanavaa ei ole valittu, joten siivous poistaisi koko historian. Valitse säilytettävät kanavat tai käytä tyhjennyspainiketta, jos haluat todella poistaa kaiken. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.fr.resx b/HellionChat/Resources/HellionStrings.fr.resx index f6a634f..0442588 100644 --- a/HellionChat/Resources/HellionStrings.fr.resx +++ b/HellionChat/Resources/HellionStrings.fr.resx @@ -1172,4 +1172,10 @@ effacement de l'historique + + Le filtre de confidentialité est désactivé, donc chaque canal est enregistré et rien dans la base de données ne contredit vos réglages. Activez d'abord le filtre et choisissez des canaux. + + + Aucun canal n'est sélectionné, un nettoyage supprimerait donc tout l'historique. Choisissez les canaux à conserver, ou utilisez le bouton d'effacement si vous voulez vraiment tout supprimer. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.hu.resx b/HellionChat/Resources/HellionStrings.hu.resx index 2583443..083c79b 100644 --- a/HellionChat/Resources/HellionStrings.hu.resx +++ b/HellionChat/Resources/HellionStrings.hu.resx @@ -1171,4 +1171,10 @@ az előzmények törlése + + Az adatvédelmi szűrő ki van kapcsolva, így minden csatorna mentésre kerül, és semmi sem mond ellent a beállításaidnak az adatbázisban. Előbb kapcsold be a szűrőt, és válassz csatornákat. + + + Nincs kiválasztva csatorna, így a takarítás a teljes előzményt törölné. Válaszd ki a megtartandó csatornákat, vagy használd a törlés gombot, ha tényleg mindent el akarsz távolítani. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.it.resx b/HellionChat/Resources/HellionStrings.it.resx index a55e756..89b963a 100644 --- a/HellionChat/Resources/HellionStrings.it.resx +++ b/HellionChat/Resources/HellionStrings.it.resx @@ -1172,4 +1172,10 @@ cancellazione della cronologia + + Il filtro privacy è disattivato, quindi ogni canale viene salvato e nulla nel database contraddice le tue impostazioni. Attiva prima il filtro e scegli i canali. + + + Nessun canale è selezionato, quindi una pulizia cancellerebbe l'intera cronologia. Scegli i canali da conservare, oppure usa il pulsante di cancellazione se vuoi davvero eliminare tutto. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ja.resx b/HellionChat/Resources/HellionStrings.ja.resx index 4fda299..e1329e1 100644 --- a/HellionChat/Resources/HellionStrings.ja.resx +++ b/HellionChat/Resources/HellionStrings.ja.resx @@ -1172,4 +1172,10 @@ 履歴の削除 + + プライバシーフィルターがオフのため、すべてのチャンネルが保存されており、設定と矛盾するデータはありません。まずフィルターをオンにしてチャンネルを選んでください。 + + + チャンネルが一つも選ばれていないため、クリーンアップは履歴をすべて削除します。残したいチャンネルを選ぶか、本当にすべて消したい場合は削除ボタンを使ってください。 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ko.resx b/HellionChat/Resources/HellionStrings.ko.resx index ce3f4cf..3344e1f 100644 --- a/HellionChat/Resources/HellionStrings.ko.resx +++ b/HellionChat/Resources/HellionStrings.ko.resx @@ -1172,4 +1172,10 @@ 기록 삭제 + + 개인정보 필터가 꺼져 있어 모든 채널이 저장되며, 데이터베이스에 설정과 어긋나는 내용이 없습니다. 먼저 필터를 켜고 채널을 선택하세요. + + + 선택된 채널이 없어 정리를 실행하면 기록 전체가 삭제됩니다. 남길 채널을 선택하거나, 정말 모두 지우려면 삭제 버튼을 사용하세요. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nb.resx b/HellionChat/Resources/HellionStrings.nb.resx index b4e659c..4d7cb89 100644 --- a/HellionChat/Resources/HellionStrings.nb.resx +++ b/HellionChat/Resources/HellionStrings.nb.resx @@ -1171,4 +1171,10 @@ sletting av historikken + + Personvernfilteret er av, så alle kanaler lagres, og ingenting i databasen strider mot innstillingene dine. Slå på filteret og velg kanaler først. + + + Ingen kanal er valgt, så en opprydding ville slette hele historikken. Velg kanalene du vil beholde, eller bruk sletteknappen hvis du virkelig vil fjerne alt. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.nl.resx b/HellionChat/Resources/HellionStrings.nl.resx index 13ec0ac..64c7335 100644 --- a/HellionChat/Resources/HellionStrings.nl.resx +++ b/HellionChat/Resources/HellionStrings.nl.resx @@ -1172,4 +1172,10 @@ wissen van de geschiedenis + + Het privacyfilter staat uit, dus elk kanaal wordt opgeslagen en niets in de database spreekt je instellingen tegen. Zet het filter eerst aan en kies kanalen. + + + Er is geen kanaal geselecteerd, dus een opschoning zou de hele geschiedenis wissen. Kies de kanalen die je wilt bewaren, of gebruik de wisknop als je echt alles kwijt wilt. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pl.resx b/HellionChat/Resources/HellionStrings.pl.resx index 783f642..ea72b2c 100644 --- a/HellionChat/Resources/HellionStrings.pl.resx +++ b/HellionChat/Resources/HellionStrings.pl.resx @@ -1171,4 +1171,10 @@ usuwanie historii + + Filtr prywatności jest wyłączony, więc zapisywany jest każdy kanał i nic w bazie danych nie kłóci się z twoimi ustawieniami. Najpierw włącz filtr i wybierz kanały. + + + Nie wybrano żadnego kanału, więc porządkowanie usunęłoby całą historię. Wybierz kanały, które chcesz zachować, albo użyj przycisku czyszczenia, jeśli naprawdę chcesz usunąć wszystko. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-BR.resx b/HellionChat/Resources/HellionStrings.pt-BR.resx index 4f23d63..dd698da 100644 --- a/HellionChat/Resources/HellionStrings.pt-BR.resx +++ b/HellionChat/Resources/HellionStrings.pt-BR.resx @@ -1172,4 +1172,10 @@ exclusão do histórico + + O filtro de privacidade está desligado, então todos os canais são salvos e nada no banco de dados contradiz suas configurações. Ligue o filtro e escolha os canais primeiro. + + + Nenhum canal está selecionado, então uma limpeza apagaria todo o histórico. Escolha os canais que quer manter, ou use o botão de apagar se realmente quiser remover tudo. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.pt-PT.resx b/HellionChat/Resources/HellionStrings.pt-PT.resx index 07a382b..10b880a 100644 --- a/HellionChat/Resources/HellionStrings.pt-PT.resx +++ b/HellionChat/Resources/HellionStrings.pt-PT.resx @@ -1171,4 +1171,10 @@ eliminação do histórico + + O filtro de privacidade está desligado, por isso todos os canais são guardados e nada na base de dados contradiz as suas definições. Ligue primeiro o filtro e escolha os canais. + + + Não está selecionado nenhum canal, por isso uma limpeza apagaria todo o histórico. Escolha os canais que quer manter, ou use o botão de apagar se quiser mesmo remover tudo. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index fc01aec..aa47fe1 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -1192,4 +1192,10 @@ clearing the history + + The privacy filter is off, so every channel is stored and nothing in the database contradicts your settings. Switch the filter on and pick channels first. + + + No channel is selected, so a cleanup would delete the entire history. Pick the channels you want to keep, or use the clear button if you really want everything gone. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ro.resx b/HellionChat/Resources/HellionStrings.ro.resx index fc18940..6fc0885 100644 --- a/HellionChat/Resources/HellionStrings.ro.resx +++ b/HellionChat/Resources/HellionStrings.ro.resx @@ -1172,4 +1172,10 @@ ștergerea istoricului + + Filtrul de confidențialitate este oprit, deci fiecare canal este salvat și nimic din baza de date nu contrazice setările tale. Pornește mai întâi filtrul și alege canalele. + + + Niciun canal nu este selectat, deci o curățare ar șterge tot istoricul. Alege canalele pe care vrei să le păstrezi sau folosește butonul de ștergere dacă chiar vrei să dispară tot. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.ru.resx b/HellionChat/Resources/HellionStrings.ru.resx index 2ea927c..597df69 100644 --- a/HellionChat/Resources/HellionStrings.ru.resx +++ b/HellionChat/Resources/HellionStrings.ru.resx @@ -1172,4 +1172,10 @@ удаление истории + + Фильтр конфиденциальности выключен, поэтому сохраняются все каналы и ничто в базе данных не противоречит вашим настройкам. Сначала включите фильтр и выберите каналы. + + + Не выбран ни один канал, поэтому очистка удалит всю историю. Выберите каналы, которые хотите сохранить, или воспользуйтесь кнопкой удаления, если действительно хотите стереть всё. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.sv.resx b/HellionChat/Resources/HellionStrings.sv.resx index 3c3e042..ccd76bc 100644 --- a/HellionChat/Resources/HellionStrings.sv.resx +++ b/HellionChat/Resources/HellionStrings.sv.resx @@ -1172,4 +1172,10 @@ radering av historiken + + Integritetsfiltret är avstängt, så alla kanaler sparas och inget i databasen strider mot dina inställningar. Slå på filtret och välj kanaler först. + + + Ingen kanal är vald, så en rensning skulle radera hela historiken. Välj de kanaler du vill behålla, eller använd raderingsknappen om du verkligen vill ta bort allt. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.tr.resx b/HellionChat/Resources/HellionStrings.tr.resx index 587bd23..9ad83af 100644 --- a/HellionChat/Resources/HellionStrings.tr.resx +++ b/HellionChat/Resources/HellionStrings.tr.resx @@ -1171,4 +1171,10 @@ geçmişin silinmesi + + Gizlilik filtresi kapalı, bu yüzden her kanal saklanıyor ve veritabanında ayarlarınızla çelişen bir şey yok. Önce filtreyi açın ve kanalları seçin. + + + Hiçbir kanal seçili değil, bu yüzden temizlik tüm geçmişi siler. Saklamak istediğiniz kanalları seçin veya gerçekten her şeyin gitmesini istiyorsanız silme düğmesini kullanın. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.uk.resx b/HellionChat/Resources/HellionStrings.uk.resx index 06c1328..2c74872 100644 --- a/HellionChat/Resources/HellionStrings.uk.resx +++ b/HellionChat/Resources/HellionStrings.uk.resx @@ -1171,4 +1171,10 @@ видалення історії + + Фільтр приватності вимкнено, тому зберігаються всі канали і ніщо в базі даних не суперечить вашим налаштуванням. Спершу увімкніть фільтр і виберіть канали. + + + Не вибрано жодного каналу, тому очищення видалить усю історію. Виберіть канали, які хочете зберегти, або скористайтеся кнопкою видалення, якщо справді хочете стерти все. + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hans.resx b/HellionChat/Resources/HellionStrings.zh-Hans.resx index c74a6dc..9d5a775 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hans.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hans.resx @@ -1172,4 +1172,10 @@ 清空历史记录 + + 隐私过滤器已关闭,因此所有频道都会保存,数据库中没有与设置冲突的内容。请先开启过滤器并选择频道。 + + + 未选择任何频道,因此清理会删除全部历史记录。请选择要保留的频道,若确实想全部删除,请使用清空按钮。 + \ No newline at end of file diff --git a/HellionChat/Resources/HellionStrings.zh-Hant.resx b/HellionChat/Resources/HellionStrings.zh-Hant.resx index ee01999..64a1eb1 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hant.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hant.resx @@ -1172,4 +1172,10 @@ 清除歷史紀錄 + + 隱私篩選器已關閉,因此所有頻道都會保存,資料庫中沒有與設定衝突的內容。請先開啟篩選器並選擇頻道。 + + + 未選擇任何頻道,因此清理會刪除全部歷史紀錄。請選擇要保留的頻道,若確實想全部刪除,請使用清除按鈕。 + \ No newline at end of file diff --git a/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs b/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs index 25578fd..c1f1988 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/DataPrivacyTab.cs @@ -32,6 +32,31 @@ internal sealed class DataPrivacyTab // Written by the export thread, read by the draw thread every frame. private volatile bool _exportRunning; + // One immutable object, published in a single write. The preview is built on + // a worker and read by the draw thread, and a half-filled set of counters is + // exactly the kind of thing a user would act on. + private volatile CleanupPreview? _cleanupPreview; + private volatile bool _cleanupPreviewRunning; + private volatile bool _cleanupRunning; + + // What the preview was computed against. All three inputs decide the outcome, + // so all three decide whether it is still valid. + private sealed record CleanupPreview( + IReadOnlyList<(ChatType Type, long Count, bool Keep)> Rows, + long KeepCount, + long DeleteCount, + IReadOnlyCollection AllowedTypes, + HashSet Listed, + bool FilterEnabled, + bool PersistUnknown + ) + { + internal bool MatchesConfig() => + FilterEnabled == Plugin.Config.PrivacyFilterEnabled + && PersistUnknown == Plugin.Config.PrivacyPersistUnknownChannels + && Listed.SetEquals(Plugin.Config.PrivacyPersistChannels); + } + private static readonly ExportFormat[] FormatValues = EnumValues.All; // Five years. The old form had no upper bound at all, and retention caps at @@ -114,6 +139,17 @@ internal sealed class DataPrivacyTab DrawPrivacyPersistChannelsGrid(); } + if ( + _w.Section( + ImGui.GetID("privacy.cleanup"u8), + HellionStrings.Settings_Section_Cleanup, + open: false + ) + ) + { + DrawCleanupSection(); + } + if ( _w.Section( ImGui.GetID("privacy.export"u8), @@ -180,6 +216,307 @@ internal sealed class DataPrivacyTab } } + // The privacy filter only decides what gets written from now on. Everything + // stored before the user narrowed their channels stays until something goes + // and removes it, and that is what this does. + // + // Two rules shape the layout, both because this deletes history and cannot + // be undone: + // + // 1. Without a preview the apply button does not exist. Not greyed out -- + // absent. A disabled button is something a user waits for; a missing + // one is something they have to go and earn. + // 2. A preview that no longer matches the settings is the same as no + // preview. The old version only recoloured the number and left the + // button live, so a changed whitelist could be applied against counts + // computed for the previous one. + private void DrawCleanupSection() + { + ImGuiUtil.HelpText(HellionStrings.Cleanup_Help_Intro); + + var availability = StorageRule.CleanupState( + Plugin.Config.PrivacyFilterEnabled, + Plugin.Config.PrivacyPersistChannels.Count + ); + + if (availability != StorageRule.CleanupAvailability.Available) + { + ImGui.Spacing(); + ImGuiUtil.HelpText( + availability == StorageRule.CleanupAvailability.FilterDisabled + ? HellionStrings.Cleanup_Unavailable_FilterOff + : HellionStrings.Cleanup_Unavailable_NothingListed + ); + return; + } + + var current = _plugin.DbOperations.Current; + var busy = _cleanupPreviewRunning || _cleanupRunning || current != DbOperation.None; + + ImGui.Spacing(); + using (ImRaii.Disabled(busy)) + { + if (ImGui.Button(HellionStrings.Cleanup_RefreshPreview)) + StartCleanupPreview(); + } + + var preview = _cleanupPreview; + if (preview is null) + { + ImGuiUtil.HelpText(HellionStrings.Cleanup_NoPreview); + } + else if (!preview.MatchesConfig()) + { + ImGuiUtil.HelpText(HellionStrings.Cleanup_Preview_Stale); + } + else + { + DrawCleanupNumbers(preview); + + // Only reachable with a preview that still describes the current + // settings. Everything above returns before this point. + if (preview.DeleteCount > 0) + { + ImGui.Spacing(); + using (ImRaii.Disabled(busy)) + { + if ( + ImGuiUtil.CtrlShiftButton( + HellionStrings.Cleanup_Apply_Label, + string.Format(HellionStrings.Cleanup_Apply_Tooltip, preview.DeleteCount) + ) + ) + StartCleanup(preview); + } + } + } + + if (_cleanupRunning) + ImGuiUtil.HelpText(HellionStrings.Cleanup_Running); + else if (current != DbOperation.None) + ImGuiUtil.HelpText( + string.Format(HellionStrings.Settings_Database_Busy, OperationName(current)) + ); + } + + private void DrawCleanupNumbers(CleanupPreview preview) + { + ImGui.Spacing(); + ImGui.TextUnformatted( + string.Format( + HellionStrings.Cleanup_TotalStored, + preview.KeepCount + preview.DeleteCount + ) + ); + ImGui.TextUnformatted(string.Format(HellionStrings.Cleanup_WillKeep, preview.KeepCount)); + ImGui.TextUnformatted( + string.Format(HellionStrings.Cleanup_WillDelete, preview.DeleteCount) + ); + + using var tree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown); + if (!tree.Success) + return; + + using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false); + foreach (var (type, count, keep) in preview.Rows) + { + var marker = keep + ? HellionStrings.Cleanup_Marker_Keep + : HellionStrings.Cleanup_Marker_Delete; + ImGui.TextDisabled($"{marker} {type.Name()}: {count:N0}"); + } + } + + // On a worker: the count is a GROUP BY over every stored row, and the old + // version ran it inline on the draw thread. + private void StartCleanupPreview() + { + if (_cleanupPreviewRunning) + return; + + _cleanupPreviewRunning = true; + + // Snapshotted here, on the draw thread, so the worker cannot read the + // config while the settings UI is writing it. + var listed = new HashSet(Plugin.Config.PrivacyPersistChannels); + var filterEnabled = Plugin.Config.PrivacyFilterEnabled; + var persistUnknown = Plugin.Config.PrivacyPersistUnknownChannels; + + var worker = new Thread(() => + { + try + { + using var conn = _plugin.MessageManager.Store.OpenSecondaryConnection(); + var counts = _plugin.MessageManager.Store.GetMessageCountsByChatType(conn); + + var rows = new List<(ChatType, long, bool)>(counts.Count); + long keepCount = 0; + long deleteCount = 0; + + // The allowlist starts from the whitelist itself, not from what + // happens to be stored right now. A channel with zero messages + // at preview time can receive one before the apply lands, and + // deriving the list from the counts would delete it. + var allowed = new HashSet(listed.Select(t => (int)(ushort)t)); + + foreach (var (raw, count) in counts) + { + var type = (ChatType)(ushort)raw; + var known = Enum.IsDefined(type); + var keep = StorageRule.Allows(listed.Contains(type), known, persistUnknown); + + // A stored channel this build does not recognise survives if + // the failsafe says so. That failsafe exists to hold on to a + // new patch's channel until the user decides, and a cleanup + // that deleted it anyway would defeat exactly that. + if (keep) + { + keepCount += count; + allowed.Add(raw); + } + else + { + deleteCount += count; + } + + rows.Add((type, count, keep)); + } + + rows.Sort((a, b) => b.Item2.CompareTo(a.Item2)); + + _cleanupPreview = new CleanupPreview( + rows, + keepCount, + deleteCount, + allowed, + listed, + filterEnabled, + persistUnknown + ); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to compute cleanup preview"); + Notify(HellionStrings.Cleanup_PreviewError, NotificationType.Error); + } + finally + { + _cleanupPreviewRunning = false; + } + }) + { + IsBackground = true, + Name = "HellionChat Cleanup Preview", + }; + + try + { + worker.Start(); + } + catch (Exception e) + { + _cleanupPreviewRunning = false; + _logger.LogError(e, "Could not start the cleanup preview thread"); + WrapperUtil.AddNotification( + HellionStrings.Cleanup_PreviewError, + NotificationType.Error + ); + } + } + + private void StartCleanup(CleanupPreview preview) + { + if (_cleanupRunning) + return; + + // Carried from the preview the user actually read, not recomputed from + // the config. Between the frame that drew the number and the frame that + // took the click, nothing can have changed -- but the next revision of + // this method should not have to prove that again. + var allowed = preview.AllowedTypes; + if (allowed.Count == 0) + return; + + _cleanupRunning = true; + + var worker = new Thread(() => + { + try + { + if (!_plugin.DbOperations.TryBegin(DbOperation.Cleanup)) + { + Notify( + string.Format( + HellionStrings.Settings_Database_Busy, + OperationName(_plugin.DbOperations.Current) + ), + NotificationType.Warning + ); + return; + } + + try + { + var deleted = _plugin.MessageManager.Store.CleanupRetainOnly(allowed); + _logger.LogInformation($"Privacy cleanup: deleted {deleted} messages"); + + // The tabs still hold the rows that just left the database. + if ( + !Plugin + .Framework.Run(() => + { + _plugin.MessageManager.ClearAllTabs(); + _plugin.MessageManager.FilterAllTabs(); + }) + .Wait(TimeSpan.FromSeconds(5)) + ) + { + _logger.LogWarning( + "Privacy cleanup: framework refresh timed out after 5s." + ); + } + + Notify( + string.Format(HellionStrings.Cleanup_Success, deleted), + NotificationType.Success + ); + } + finally + { + _plugin.DbOperations.End(); + } + } + catch (Exception e) + { + _logger.LogError(e, "Privacy cleanup failed"); + Notify(HellionStrings.Cleanup_Error, NotificationType.Error); + } + finally + { + // Dropped either way: after a successful run the numbers describe + // a database that no longer exists, and after a failure they + // describe one nobody should act on. + _cleanupPreview = null; + _cleanupRunning = false; + } + }) + { + IsBackground = true, + Name = "HellionChat Cleanup", + }; + + try + { + worker.Start(); + } + catch (Exception e) + { + _cleanupRunning = false; + _logger.LogError(e, "Could not start the cleanup thread"); + WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error); + } + } + // GDPR Art. 15. The backend has been able to do this since v1.4.8; the form // that drives it was removed with the old settings window in May, which left // the promise in PRIVACY.md without a way to keep it.