diff --git a/crates/cordiale-ui/lang/de/LC_MESSAGES/cordiale-ui.po b/crates/cordiale-ui/lang/de/LC_MESSAGES/cordiale-ui.po index 8cde361..f35c394 100644 --- a/crates/cordiale-ui/lang/de/LC_MESSAGES/cordiale-ui.po +++ b/crates/cordiale-ui/lang/de/LC_MESSAGES/cordiale-ui.po @@ -2729,8 +2729,8 @@ msgid "Delete {} for good? Its whole scrollback goes with it, for every account. msgstr "{} endgültig löschen? Der gesamte Verlauf geht damit verloren, für jedes Konto. Gelöschte Nachrichten: {}" msgctxt "AppWindow" -msgid "Delete {} for good? Its whole scrollback goes with it, for every account; the server couldn't say how many messages that is." -msgstr "{} endgültig löschen? Der gesamte Verlauf geht damit verloren, für jedes Konto; der Server konnte nicht sagen, wie viele Nachrichten das sind." +msgid "Can't delete {}: the server couldn't say how many messages that would remove. Cancel and try again." +msgstr "{} kann nicht gelöscht werden: Der Server konnte nicht sagen, wie viele Nachrichten entfernt würden. Abbrechen und erneut versuchen." msgctxt "AppWindow" msgid "Delete network and scrollback" diff --git a/crates/cordiale-ui/lang/es/LC_MESSAGES/cordiale-ui.po b/crates/cordiale-ui/lang/es/LC_MESSAGES/cordiale-ui.po index 686a9bf..90ee4c9 100644 --- a/crates/cordiale-ui/lang/es/LC_MESSAGES/cordiale-ui.po +++ b/crates/cordiale-ui/lang/es/LC_MESSAGES/cordiale-ui.po @@ -2729,8 +2729,8 @@ msgid "Delete {} for good? Its whole scrollback goes with it, for every account. msgstr "¿Eliminar {} definitivamente? Todo su historial se va con ella, para todas las cuentas. Mensajes eliminados: {}" msgctxt "AppWindow" -msgid "Delete {} for good? Its whole scrollback goes with it, for every account; the server couldn't say how many messages that is." -msgstr "¿Eliminar {} definitivamente? Todo su historial se va con ella, para todas las cuentas; el servidor no pudo decir cuántos mensajes son." +msgid "Can't delete {}: the server couldn't say how many messages that would remove. Cancel and try again." +msgstr "No se puede eliminar {}: el servidor no pudo decir cuántos mensajes se eliminarían. Cancela e inténtalo de nuevo." msgctxt "AppWindow" msgid "Delete network and scrollback" diff --git a/crates/cordiale-ui/lang/fr/LC_MESSAGES/cordiale-ui.po b/crates/cordiale-ui/lang/fr/LC_MESSAGES/cordiale-ui.po index d6a8067..8e6087c 100644 --- a/crates/cordiale-ui/lang/fr/LC_MESSAGES/cordiale-ui.po +++ b/crates/cordiale-ui/lang/fr/LC_MESSAGES/cordiale-ui.po @@ -2729,8 +2729,8 @@ msgid "Delete {} for good? Its whole scrollback goes with it, for every account. msgstr "Supprimer {} définitivement ? Tout son historique disparaît avec, pour chaque compte. Messages supprimés : {}" msgctxt "AppWindow" -msgid "Delete {} for good? Its whole scrollback goes with it, for every account; the server couldn't say how many messages that is." -msgstr "Supprimer {} définitivement ? Tout son historique disparaît avec, pour chaque compte ; le serveur n'a pas pu dire combien de messages cela représente." +msgid "Can't delete {}: the server couldn't say how many messages that would remove. Cancel and try again." +msgstr "Impossible de supprimer {} : le serveur n'a pas pu dire combien de messages seraient supprimés. Annulez et réessayez." msgctxt "AppWindow" msgid "Delete network and scrollback" diff --git a/crates/cordiale-ui/lang/it/LC_MESSAGES/cordiale-ui.po b/crates/cordiale-ui/lang/it/LC_MESSAGES/cordiale-ui.po index 04e8e71..148ff24 100644 --- a/crates/cordiale-ui/lang/it/LC_MESSAGES/cordiale-ui.po +++ b/crates/cordiale-ui/lang/it/LC_MESSAGES/cordiale-ui.po @@ -2729,8 +2729,8 @@ msgid "Delete {} for good? Its whole scrollback goes with it, for every account. msgstr "Eliminare {} definitivamente? Con la rete va via tutto lo storico, per ogni account. Messaggi eliminati: {}" msgctxt "AppWindow" -msgid "Delete {} for good? Its whole scrollback goes with it, for every account; the server couldn't say how many messages that is." -msgstr "Eliminare {} definitivamente? Con la rete va via tutto lo storico, per ogni account; il server non ha saputo dire quanti messaggi siano." +msgid "Can't delete {}: the server couldn't say how many messages that would remove. Cancel and try again." +msgstr "Impossibile eliminare {}: il server non ha saputo dire quanti messaggi verrebbero rimossi. Annulla e riprova." msgctxt "AppWindow" msgid "Delete network and scrollback" diff --git a/crates/cordiale-ui/src/admin_handlers.rs b/crates/cordiale-ui/src/admin_handlers.rs index 8da0cd9..784a7dd 100644 --- a/crates/cordiale-ui/src/admin_handlers.rs +++ b/crates/cordiale-ui/src/admin_handlers.rs @@ -546,10 +546,11 @@ pub(crate) async fn push_admin_featured( /// confirmation. A 404 or any failure is "can't say", never zero, and the /// answer is dropped if the confirmation moved on to another network. pub(crate) async fn handle_admin_network_count( - state: &WorkerState, + state: &mut WorkerState, ui: &slint::Weak, network_id: String, ) { + state.panels.admin_network_count_for = None; let (Some(client), Some(token)) = (&state.conn.client, &state.conn.token) else { return; }; @@ -563,6 +564,9 @@ pub(crate) async fn handle_admin_network_count( None } }; + if count.is_some() { + state.panels.admin_network_count_for = Some(network_id.clone()); + } let _ = ui.upgrade_in_event_loop(move |ui| { if ui.get_admin_network_confirm_id() != network_id.as_str() { return; @@ -577,6 +581,12 @@ pub(crate) async fn handle_admin_network_count( }); } +/// A network may only be deleted once the server has answered its +/// scrollback count (zero included); a stale or forged request is refused. +pub(crate) fn admin_network_delete_armed(state: &WorkerState, network_id: &str) -> bool { + state.panels.admin_network_count_for.as_deref() == Some(network_id) +} + pub(crate) fn admin_overview_text(overview: &cordiale_core::admin::AdminOverview) -> String { format!( "{} session(s) · {}/{} visitors live · {} · v{}", diff --git a/crates/cordiale-ui/src/main.rs b/crates/cordiale-ui/src/main.rs index 925d9bb..ad72a23 100644 --- a/crates/cordiale-ui/src/main.rs +++ b/crates/cordiale-ui/src/main.rs @@ -1095,6 +1095,9 @@ struct PanelState { /// The last `GET /admin/uploads` answer, which decides what may be /// deleted. admin_uploads: Option, + /// The network id whose scrollback count the server last answered; only + /// that network may be deleted. + admin_network_count_for: Option, /// `/kb` requests waiting for their `resolve_userhost` reply, by ref. pending_kickbans: HashMap, /// Identity-recovery panel driven entirely by server pushes; `None` @@ -1448,6 +1451,7 @@ impl WorkerState { admin_events: Vec::new(), admin_settings: None, admin_uploads: None, + admin_network_count_for: None, pending_kickbans: HashMap::new(), recover_panel: None, reply_view: None, @@ -2116,7 +2120,7 @@ async fn run_worker( run_admin_featured_delete(&mut state, &ui, network_id, featured_id).await; } Some(WorkerCommand::AdminNetworkCount(network_id)) => { - handle_admin_network_count(&state, &ui, network_id).await; + handle_admin_network_count(&mut state, &ui, network_id).await; } Some(WorkerCommand::AdminCredentialEdit { user_id, @@ -2168,7 +2172,13 @@ async fn run_worker( run_admin_settings_save(&mut state, &ui, form).await; } Some(WorkerCommand::AdminNetworkDelete(network_id)) => { - handle_admin_write(&state, &ui, AdminWrite::DeleteNetwork(network_id)).await; + if admin_network_delete_armed(&state, &network_id) { + state.panels.admin_network_count_for = None; + handle_admin_write(&state, &ui, AdminWrite::DeleteNetwork(network_id)) + .await; + } else { + persistence::log_line("admin network delete refused: no count"); + } } Some(WorkerCommand::AdminNetworkSave { slug, diff --git a/crates/cordiale-ui/src/tests.rs b/crates/cordiale-ui/src/tests.rs index 1f467fb..ad04e0c 100644 --- a/crates/cordiale-ui/src/tests.rs +++ b/crates/cordiale-ui/src/tests.rs @@ -4657,6 +4657,20 @@ fn a_refused_admin_write_is_told_apart() { assert_eq!(admin_failure_kind(None, true), "admin-action-failed"); } +#[test] +fn network_delete_needs_a_count_for_that_network() { + let mut state = WorkerState::new(); + // No count answered yet (counting, 404 or any failure): refused. + assert!(!admin_network_delete_armed(&state, "7")); + // A successful count, zero included, arms only its own network. + state.panels.admin_network_count_for = Some("7".to_string()); + assert!(admin_network_delete_armed(&state, "7")); + assert!(!admin_network_delete_armed(&state, "8")); + // A new count request clears the previous answer. + state.panels.admin_network_count_for = None; + assert!(!admin_network_delete_armed(&state, "7")); +} + fn pending_kickban(ban_type: cordiale_core::ban::BanType) -> PendingKickBan { PendingKickBan { network: "libera".to_string(), diff --git a/crates/cordiale-ui/ui/appwindow.slint b/crates/cordiale-ui/ui/appwindow.slint index 7090f42..3db9b23 100644 --- a/crates/cordiale-ui/ui/appwindow.slint +++ b/crates/cordiale-ui/ui/appwindow.slint @@ -6223,7 +6223,7 @@ export component AppWindow inherits Window { ? @tr("Counting the messages of {}…", admin-network-confirm-slug) : (admin-network-count-state == "known" ? @tr("Delete {} for good? Its whole scrollback goes with it, for every account. Messages deleted: {}", admin-network-confirm-slug, admin-network-count) - : @tr("Delete {} for good? Its whole scrollback goes with it, for every account; the server couldn't say how many messages that is.", admin-network-confirm-slug)); + : @tr("Can't delete {}: the server couldn't say how many messages that would remove. Cancel and try again.", admin-network-confirm-slug)); wrap: word-wrap; horizontal-stretch: 1; vertical-alignment: center; @@ -6238,7 +6238,7 @@ export component AppWindow inherits Window { } Button { text: @tr("Delete network and scrollback"); - enabled: admin-network-count-state != "counting"; + enabled: admin-network-count-state == "known"; clicked => { admin-network-delete(admin-network-confirm-id); admin-network-confirm-id = "";