From 1ab7ba83771850a32557689c4f8084ebab4d1b55 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Tue, 18 Aug 2026 21:38:34 +0200 Subject: [PATCH] feat(privacy): reconnect the retroactive cleanup The privacy filter only decides what gets written from now on. Whatever was stored before the user narrowed their channels stays there until something removes it, and that something has had no button since May. Two rules shape the section, both because this deletes history and cannot be undone: - 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. - A preview that no longer matches the settings counts 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. The mapping from the live rule to CleanupRetainOnly is the part worth reading twice. CleanupRetainOnly takes one set and deletes everything else, so it can only stand in for the live rule where that rule narrows something: filter off means nothing is filtered, and an empty list means a full wipe, which has its own button and its own confirmation. Both cases now say so instead of offering a destructive action that does not mean what it looks like. Inside that, the allowlist is the whitelist itself plus any stored channel this build does not recognise while the unknown-channel failsafe is on. Deriving it from the counts instead would delete messages that arrive on a whitelisted but currently empty channel between the preview and the apply, and dropping the unrecognised ones would defeat the failsafe, which exists to hold on to a new patch's channel until the user has decided about it. The preview runs on a worker over its own connection. It is a GROUP BY across every stored row, the old version ran it inline on the draw thread, and holding the read lock for it would stall UpsertMessage on the framework thread for the length of the scan. Cleanup_Help_SavedNote stays unused: it tells the reader to press Save first, and the window it was written for had a Save button. --- HellionChat/MessageStore.cs | 31 +- HellionChat/Privacy/StorageRule.cs | 19 + .../Resources/HellionStrings.Designer.cs | 2 + HellionChat/Resources/HellionStrings.ca.resx | 6 + HellionChat/Resources/HellionStrings.cs.resx | 6 + HellionChat/Resources/HellionStrings.da.resx | 6 + HellionChat/Resources/HellionStrings.de.resx | 6 + HellionChat/Resources/HellionStrings.el.resx | 6 + HellionChat/Resources/HellionStrings.es.resx | 6 + HellionChat/Resources/HellionStrings.fi.resx | 6 + HellionChat/Resources/HellionStrings.fr.resx | 6 + HellionChat/Resources/HellionStrings.hu.resx | 6 + HellionChat/Resources/HellionStrings.it.resx | 6 + HellionChat/Resources/HellionStrings.ja.resx | 6 + HellionChat/Resources/HellionStrings.ko.resx | 6 + HellionChat/Resources/HellionStrings.nb.resx | 6 + HellionChat/Resources/HellionStrings.nl.resx | 6 + HellionChat/Resources/HellionStrings.pl.resx | 6 + .../Resources/HellionStrings.pt-BR.resx | 6 + .../Resources/HellionStrings.pt-PT.resx | 6 + HellionChat/Resources/HellionStrings.resx | 6 + HellionChat/Resources/HellionStrings.ro.resx | 6 + HellionChat/Resources/HellionStrings.ru.resx | 6 + HellionChat/Resources/HellionStrings.sv.resx | 6 + HellionChat/Resources/HellionStrings.tr.resx | 6 + HellionChat/Resources/HellionStrings.uk.resx | 6 + .../Resources/HellionStrings.zh-Hans.resx | 6 + .../Resources/HellionStrings.zh-Hant.resx | 6 + .../Settings/Tabs/DataPrivacyTab.cs | 337 ++++++++++++++++++ 29 files changed, 524 insertions(+), 15 deletions(-) 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.