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.
This commit is contained in:
+16
-15
@@ -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<int, long> 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<int, long> GetMessageCountsByChatType(SqliteConnection conn)
|
||||
{
|
||||
lock (_readLock)
|
||||
var result = new Dictionary<int, long>();
|
||||
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<int, long>();
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>esborrat de l'historial</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>mazání historie</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>sletning af historikken</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1166,4 +1166,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>Löschen des Verlaufs</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>διαγραφή ιστορικού</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>Το φίλτρο απορρήτου είναι απενεργοποιημένο, άρα αποθηκεύεται κάθε κανάλι και τίποτα στη βάση δεδομένων δεν έρχεται σε αντίθεση με τις ρυθμίσεις σας. Ενεργοποιήστε πρώτα το φίλτρο και επιλέξτε κανάλια.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Δεν έχει επιλεγεί κανένα κανάλι, οπότε μια εκκαθάριση θα διέγραφε όλο το ιστορικό. Επιλέξτε τα κανάλια που θέλετε να κρατήσετε ή χρησιμοποιήστε το κουμπί διαγραφής αν θέλετε πραγματικά να φύγουν όλα.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>borrado del historial</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>historian tyhjennys</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>effacement de l'historique</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>az előzmények törlése</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>cancellazione della cronologia</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>履歴の削除</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>プライバシーフィルターがオフのため、すべてのチャンネルが保存されており、設定と矛盾するデータはありません。まずフィルターをオンにしてチャンネルを選んでください。</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>チャンネルが一つも選ばれていないため、クリーンアップは履歴をすべて削除します。残したいチャンネルを選ぶか、本当にすべて消したい場合は削除ボタンを使ってください。</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>기록 삭제</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>개인정보 필터가 꺼져 있어 모든 채널이 저장되며, 데이터베이스에 설정과 어긋나는 내용이 없습니다. 먼저 필터를 켜고 채널을 선택하세요.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>선택된 채널이 없어 정리를 실행하면 기록 전체가 삭제됩니다. 남길 채널을 선택하거나, 정말 모두 지우려면 삭제 버튼을 사용하세요.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>sletting av historikken</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>Personvernfilteret er av, så alle kanaler lagres, og ingenting i databasen strider mot innstillingene dine. Slå på filteret og velg kanaler først.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>wissen van de geschiedenis</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>usuwanie historii</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>exclusão do histórico</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>eliminação do histórico</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1192,4 +1192,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>clearing the history</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>ștergerea istoricului</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>удаление истории</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>Фильтр конфиденциальности выключен, поэтому сохраняются все каналы и ничто в базе данных не противоречит вашим настройкам. Сначала включите фильтр и выберите каналы.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Не выбран ни один канал, поэтому очистка удалит всю историю. Выберите каналы, которые хотите сохранить, или воспользуйтесь кнопкой удаления, если действительно хотите стереть всё.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>radering av historiken</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>geçmişin silinmesi</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1171,4 +1171,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>видалення історії</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>Фільтр приватності вимкнено, тому зберігаються всі канали і ніщо в базі даних не суперечить вашим налаштуванням. Спершу увімкніть фільтр і виберіть канали.</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>Не вибрано жодного каналу, тому очищення видалить усю історію. Виберіть канали, які хочете зберегти, або скористайтеся кнопкою видалення, якщо справді хочете стерти все.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>清空历史记录</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>隐私过滤器已关闭,因此所有频道都会保存,数据库中没有与设置冲突的内容。请先开启过滤器并选择频道。</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>未选择任何频道,因此清理会删除全部历史记录。请选择要保留的频道,若确实想全部删除,请使用清空按钮。</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1172,4 +1172,10 @@
|
||||
<data name="Settings_Database_Op_Clear" xml:space="preserve">
|
||||
<value>清除歷史紀錄</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_FilterOff" xml:space="preserve">
|
||||
<value>隱私篩選器已關閉,因此所有頻道都會保存,資料庫中沒有與設定衝突的內容。請先開啟篩選器並選擇頻道。</value>
|
||||
</data>
|
||||
<data name="Cleanup_Unavailable_NothingListed" xml:space="preserve">
|
||||
<value>未選擇任何頻道,因此清理會刪除全部歷史紀錄。請選擇要保留的頻道,若確實想全部刪除,請使用清除按鈕。</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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<int> AllowedTypes,
|
||||
HashSet<ChatType> 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<ExportFormat>.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<ChatType>(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<int>(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.
|
||||
|
||||
Reference in New Issue
Block a user