From c46404cbbf2a929f8525c27d54ac356b396df846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sat, 5 Apr 2025 19:04:34 +0200 Subject: [PATCH] utils: Refactor toast macro Simplify the rules for using the macro: - The message must always implement `AsRef`, - The variables values must implement `ToString`, - Trailing commas are always optional. Use functions for code that doesn't actually need to be in the macro, it allows to have linting of the code. --- po/POTFILES.skip | 2 +- src/components/crypto/identity_setup_view.rs | 8 +- src/components/dialogs/room_preview.rs | 3 +- src/components/user_page.rs | 20 +- src/i18n.rs | 4 +- src/login/homeserver_page.rs | 3 +- src/login/in_browser_page.rs | 5 +- src/login/method_page.rs | 3 +- src/login/mod.rs | 18 +- .../general_page/log_out_subpage.rs | 3 +- .../view/account_settings/general_page/mod.rs | 18 +- .../account_settings/notifications_page.rs | 11 +- .../ignored_users_subpage/ignored_user_row.rs | 3 +- .../import_export_keys_subpage.rs | 14 +- .../room_details/addresses_subpage/mod.rs | 15 +- .../room_details/edit_details_subpage.rs | 12 +- .../view/content/room_details/general_page.rs | 15 +- .../room_details/invite_subpage/mod.rs | 5 +- src/session/view/content/room_details/mod.rs | 2 +- .../permissions/permissions_subpage.rs | 3 +- .../view/content/room_history/item_row.rs | 6 +- .../room_history/message_toolbar/mod.rs | 18 +- .../content/room_history/sender_avatar/mod.rs | 16 +- src/session/view/create_room_dialog.rs | 3 +- src/session/view/sidebar/row.rs | 7 +- src/utils/macros.rs | 186 +------------- src/utils/mod.rs | 8 +- src/utils/toast.rs | 241 ++++++++++++++++++ 28 files changed, 333 insertions(+), 319 deletions(-) create mode 100644 src/utils/toast.rs diff --git a/po/POTFILES.skip b/po/POTFILES.skip index 9b21fec9..4ccb0194 100644 --- a/po/POTFILES.skip +++ b/po/POTFILES.skip @@ -1,4 +1,4 @@ # These are files that we don't want to translate # Please keep this file sorted alphabetically. src/i18n.rs -src/utils/macros.rs +src/utils/toast.rs diff --git a/src/components/crypto/identity_setup_view.rs b/src/components/crypto/identity_setup_view.rs index e78c2cb3..5ab5b11c 100644 --- a/src/components/crypto/identity_setup_view.rs +++ b/src/components/crypto/identity_setup_view.rs @@ -380,8 +380,10 @@ mod imp { self.send_request_btn.set_is_loading(true); if let Err(()) = session.verification_list().create(None).await { - let obj = self.obj(); - toast!(obj, gettext("Could not send a new verification request")); + toast!( + self.obj(), + gettext("Could not send a new verification request") + ); } // On success, the verification should be shown automatically. @@ -432,7 +434,7 @@ mod imp { } Err(error) => { error!("Could not bootstrap cross-signing: {error:?}"); - toast!(obj, gettext("Could not create the crypto identity",)); + toast!(obj, gettext("Could not create the crypto identity")); } } diff --git a/src/components/dialogs/room_preview.rs b/src/components/dialogs/room_preview.rs index a242cad4..f122b871 100644 --- a/src/components/dialogs/room_preview.rs +++ b/src/components/dialogs/room_preview.rs @@ -334,8 +334,7 @@ mod imp { obj.close(); } Err(error) => { - let obj = self.obj(); - toast!(obj, error); + toast!(self.obj(), error); self.join_btn.set_is_loading(false); self.go_back_btn.set_sensitive(true); diff --git a/src/components/user_page.rs b/src/components/user_page.rs index 2f5eddfc..c0ff6aa2 100644 --- a/src/components/user_page.rs +++ b/src/components/user_page.rs @@ -1,5 +1,5 @@ use adw::{prelude::*, subclass::prelude::*}; -use gettextrs::{gettext, pgettext}; +use gettextrs::{gettext, ngettext, pgettext}; use gtk::{ glib, glib::{clone, closure_local}, @@ -13,8 +13,7 @@ use crate::{ confirm_mute_room_member_dialog, confirm_room_member_destructive_action_dialog, confirm_set_room_member_power_level_same_as_own_dialog, RoomMemberDestructiveAction, }, - i18n::gettext_f, - ngettext_f, + gettext_f, prelude::*, session::model::{Member, Membership, Permissions, Room, User}, toast, @@ -496,8 +495,7 @@ mod imp { let user_id = member.user_id().clone(); if room.invite(&[user_id]).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not invite user")); + toast!(self.obj(), gettext("Could not invite user")); } self.reset_room(); @@ -610,8 +608,7 @@ mod imp { let user_id = member.user_id().clone(); if room.unban(&[(user_id, None)]).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not unban user")); + toast!(self.obj(), gettext("Could not unban user")); } self.reset_room(); @@ -653,18 +650,17 @@ mod imp { ) { if let Err(events) = room.redact(&events, reason).await { let n = u32::try_from(events.len()).unwrap_or(u32::MAX); - let obj = self.obj(); toast!( - obj, - ngettext_f( + self.obj(), + ngettext( // Translators: Do NOT translate the content between '{' and '}', // this is a variable name. "Could not remove 1 message sent by the user", "Could not remove {n} messages sent by the user", n, - &[("n", &n.to_string())] - ) + ), + n, ); } } diff --git a/src/i18n.rs b/src/i18n.rs index 2daef5f0..d5bd7da2 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -8,7 +8,7 @@ use crate::utils::freplace; /// in the dictionary entry tuple. pub fn gettext_f(msgid: &str, args: &[(&str, &str)]) -> String { let s = gettext(msgid); - freplace(s, args) + freplace(&s, args).into_owned() } /// Like `ngettext`, but replaces named variables with the given dictionary. @@ -17,7 +17,7 @@ pub fn gettext_f(msgid: &str, args: &[(&str, &str)]) -> String { /// in the dictionary entry tuple. pub fn ngettext_f(msgid: &str, msgid_plural: &str, n: u32, args: &[(&str, &str)]) -> String { let s = ngettext(msgid, msgid_plural, n); - freplace(s, args) + freplace(&s, args).into_owned() } #[cfg(test)] diff --git a/src/login/homeserver_page.rs b/src/login/homeserver_page.rs index 0db1809a..835386d4 100644 --- a/src/login/homeserver_page.rs +++ b/src/login/homeserver_page.rs @@ -264,8 +264,7 @@ mod imp { /// Show the given error and abort the current login. fn abort_on_error(&self, error: &str) { - let obj = self.obj(); - toast!(obj, error); + toast!(self.obj(), error); // Drop the client because it is bound to the homeserver. if let Some(login) = self.login.obj() { diff --git a/src/login/in_browser_page.rs b/src/login/in_browser_page.rs index 4bf5173b..b080cbc5 100644 --- a/src/login/in_browser_page.rs +++ b/src/login/in_browser_page.rs @@ -97,8 +97,7 @@ mod imp { .await { error!("Could not launch URI: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not open URL")); + toast!(self.obj(), gettext("Could not open URL")); return; } @@ -225,7 +224,7 @@ mod imp { // We need to restart the server if the user wants to try again, so let's go // back to the previous screen. - let _ = self.obj().activate_action("navigation.pop", None); + let _ = obj.activate_action("navigation.pop", None); } /// Reset this page. diff --git a/src/login/method_page.rs b/src/login/method_page.rs index 8916b94d..90353a79 100644 --- a/src/login/method_page.rs +++ b/src/login/method_page.rs @@ -196,8 +196,7 @@ mod imp { } Err(error) => { warn!("Could not log in: {error}"); - let obj = self.obj(); - toast!(obj, error.to_user_facing()); + toast!(self.obj(), error.to_user_facing()); } } diff --git a/src/login/mod.rs b/src/login/mod.rs index c6a55be6..6246b845 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -297,8 +297,7 @@ mod imp { Ok(authorization_data) => authorization_data, Err(error) => { warn!("Could not construct OAuth 2.0 authorization URL: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not set up login")); + toast!(self.obj(), gettext("Could not set up login")); return; } }; @@ -322,8 +321,7 @@ mod imp { Ok(response) => response.flows, Err(error) => { warn!("Could not get available Matrix login types: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not set up login")); + toast!(self.obj(), gettext("Could not set up login")); return; } }; @@ -369,8 +367,7 @@ mod imp { } Err(error) => { warn!("Could not build Matrix SSO URL: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not set up login")); + toast!(self.obj(), gettext("Could not set up login")); } } } @@ -387,8 +384,7 @@ mod imp { .expect("task was not aborted") .map_err(|error| { warn!("Could not spawn local server: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not set up login")); + toast!(self.obj(), gettext("Could not set up login")); }) } @@ -424,8 +420,7 @@ mod imp { } Err(error) => { warn!("Could not create session: {error}"); - let obj = self.obj(); - toast!(obj, error.to_user_facing()); + toast!(self.obj(), error.to_user_facing()); self.navigation.pop(); } @@ -458,8 +453,7 @@ mod imp { let session_info = session.info().clone(); if Secret::store_session(session_info).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not store session")); + toast!(self.obj(), gettext("Could not store session")); } session.prepare().await; diff --git a/src/session/view/account_settings/general_page/log_out_subpage.rs b/src/session/view/account_settings/general_page/log_out_subpage.rs index e08f826f..8ffd43b0 100644 --- a/src/session/view/account_settings/general_page/log_out_subpage.rs +++ b/src/session/view/account_settings/general_page/log_out_subpage.rs @@ -139,8 +139,7 @@ mod imp { if is_logout_page { self.stack.set_visible_child_name("failed"); } else { - let obj = self.obj(); - toast!(obj, error); + toast!(self.obj(), error); } } diff --git a/src/session/view/account_settings/general_page/mod.rs b/src/session/view/account_settings/general_page/mod.rs index c0a2c317..40b25c61 100644 --- a/src/session/view/account_settings/general_page/mod.rs +++ b/src/session/view/account_settings/general_page/mod.rs @@ -287,8 +287,7 @@ mod imp { Ok(info) => info, Err(error) => { error!("Could not load user avatar file info: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not load file")); + toast!(self.obj(), gettext("Could not load file")); avatar.reset(); return; } @@ -298,8 +297,7 @@ mod imp { Ok((data, _)) => data, Err(error) => { error!("Could not load user avatar file: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not load file")); + toast!(self.obj(), gettext("Could not load file")); avatar.reset(); return; } @@ -318,8 +316,7 @@ mod imp { Ok(res) => res.content_uri, Err(error) => { error!("Could not upload user avatar: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not upload avatar")); + toast!(self.obj(), gettext("Could not upload avatar")); avatar.reset(); return; } @@ -350,8 +347,7 @@ mod imp { if weak_action.is_ongoing() { self.changing_avatar.take(); error!("Could not change user avatar: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not change avatar")); + toast!(self.obj(), gettext("Could not change avatar")); avatar.reset(); } } @@ -456,8 +452,7 @@ mod imp { entry.set_sensitive(true); button.set_visible(false); button.set_state(ActionState::Confirm); - let obj = self.obj(); - toast!(obj, gettext("Name changed successfully")); + toast!(self.obj(), gettext("Name changed successfully")); } /// Change the display name of the user. @@ -507,8 +502,7 @@ mod imp { if weak_action.is_ongoing() { self.changing_display_name.take(); error!("Could not change user display name: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not change display name")); + toast!(self.obj(), gettext("Could not change display name")); button.set_state(ActionState::Retry); entry.add_css_class("error"); entry.set_sensitive(true); diff --git a/src/session/view/account_settings/notifications_page.rs b/src/session/view/account_settings/notifications_page.rs index 8ba29330..2be95480 100644 --- a/src/session/view/account_settings/notifications_page.rs +++ b/src/session/view/account_settings/notifications_page.rs @@ -271,8 +271,7 @@ mod imp { } else { gettext("Could not disable account notifications") }; - let obj = self.obj(); - toast!(obj, msg); + toast!(self.obj(), msg); } self.set_account_loading(false); @@ -317,10 +316,9 @@ mod imp { self.set_global_loading(true, setting); if settings.set_global_setting(setting).await.is_err() { - let obj = self.obj(); toast!( - obj, - gettext("Could not change global notifications setting") + self.obj(), + gettext("Could not change global notifications setting"), ); } @@ -438,8 +436,7 @@ mod imp { let keyword = self.keywords_add_row.text().into(); if settings.add_keyword(keyword).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not add notification keyword")); + toast!(self.obj(), gettext("Could not add notification keyword")); } else { // Adding the keyword was successful, reset the entry. self.keywords_add_row.set_text(""); diff --git a/src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs b/src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs index 42a7fbbb..f867ac75 100644 --- a/src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs +++ b/src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs @@ -82,8 +82,7 @@ mod imp { self.stop_ignoring_button.set_is_loading(true); if ignored_users.remove(&user_id).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not stop ignoring user")); + toast!(self.obj(), gettext("Could not stop ignoring user")); self.stop_ignoring_button.set_is_loading(false); } } diff --git a/src/session/view/account_settings/security_page/import_export_keys_subpage.rs b/src/session/view/account_settings/security_page/import_export_keys_subpage.rs index c294f14c..1c089c53 100644 --- a/src/session/view/account_settings/security_page/import_export_keys_subpage.rs +++ b/src/session/view/account_settings/security_page/import_export_keys_subpage.rs @@ -1,12 +1,10 @@ use adw::{prelude::*, subclass::prelude::*}; -use gettextrs::gettext; +use gettextrs::{gettext, ngettext}; use gtk::{gio, glib, CompositeTemplate}; use matrix_sdk::encryption::{KeyExportError, RoomKeyImportError}; use tracing::{debug, error}; -use crate::{ - components::LoadingButtonRow, ngettext_f, session::model::Session, spawn_tokio, toast, -}; +use crate::{components::LoadingButtonRow, session::model::Session, spawn_tokio, toast}; #[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)] #[repr(u32)] @@ -325,12 +323,12 @@ mod imp { let n = nb.try_into().unwrap_or(u32::MAX); toast!( obj, - ngettext_f( + ngettext( "Imported 1 room encryption key", "Imported {n} room encryption keys", n, - &[("n", &n.to_string())] - ) + ), + n, ); } @@ -355,7 +353,7 @@ mod imp { obj, gettext( "The passphrase doesn't match the one used to export the keys." - ) + ), ); } else { error!("Could not import the keys: {error}"); diff --git a/src/session/view/content/room_details/addresses_subpage/mod.rs b/src/session/view/content/room_details/addresses_subpage/mod.rs index 40a57cf2..2337ba14 100644 --- a/src/session/view/content/room_details/addresses_subpage/mod.rs +++ b/src/session/view/content/room_details/addresses_subpage/mod.rs @@ -501,8 +501,7 @@ mod imp { }; if result.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not remove public address")); + toast!(self.obj(), gettext("Could not remove public address")); self.public_addresses_list.set_sensitive(true); row.set_is_loading(false); } @@ -527,8 +526,7 @@ mod imp { button.set_is_loading(true); if aliases.set_canonical_alias(alias).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not set main public address")); + toast!(self.obj(), gettext("Could not set main public address")); self.public_addresses_list.set_sensitive(true); button.set_is_loading(false); } @@ -576,8 +574,7 @@ mod imp { row.set_text(""); } Err(error) => { - let obj = self.obj(); - toast!(obj, gettext("Could not add public address")); + toast!(self.obj(), gettext("Could not add public address")); let label = match error { AddAltAliasError::NotRegistered => { @@ -679,8 +676,7 @@ mod imp { row.set_is_loading(true); if aliases.unregister_local_alias(alias).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not unregister local address")); + toast!(self.obj(), gettext("Could not unregister local address")); } self.update_local_addresses().await; @@ -750,8 +746,7 @@ mod imp { row.set_text(""); } Err(error) => { - let obj = self.obj(); - toast!(obj, gettext("Could not register local address")); + toast!(self.obj(), gettext("Could not register local address")); if let RegisterLocalAliasError::AlreadyInUse = error { self.local_addresses_error diff --git a/src/session/view/content/room_details/edit_details_subpage.rs b/src/session/view/content/room_details/edit_details_subpage.rs index c74086e5..d9bd1835 100644 --- a/src/session/view/content/room_details/edit_details_subpage.rs +++ b/src/session/view/content/room_details/edit_details_subpage.rs @@ -322,8 +322,7 @@ mod imp { return; } - let obj = self.obj(); - toast!(obj, gettext("Room name saved successfully")); + toast!(self.obj(), gettext("Room name saved successfully")); // Reset state. self.changing_name.take(); @@ -394,8 +393,7 @@ mod imp { if weak_action.is_ongoing() { self.changing_name.take(); error!("Could not change room name: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not change room name")); + toast!(self.obj(), gettext("Could not change room name")); self.name_entry_row.set_sensitive(true); self.name_button.set_state(ActionState::Retry); } @@ -433,8 +431,7 @@ mod imp { return; } - let obj = self.obj(); - toast!(obj, gettext("Room description saved successfully")); + toast!(self.obj(), gettext("Room description saved successfully")); // Reset state. self.changing_topic.take(); @@ -510,8 +507,7 @@ mod imp { if weak_action.is_ongoing() { self.changing_topic.take(); error!("Could not change room description: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not change room description")); + toast!(self.obj(), gettext("Could not change room description")); self.topic_text_view.set_sensitive(true); self.save_topic_button.set_is_loading(false); } diff --git a/src/session/view/content/room_details/general_page.rs b/src/session/view/content/room_details/general_page.rs index 34d7a35b..02ce3b4a 100644 --- a/src/session/view/content/room_details/general_page.rs +++ b/src/session/view/content/room_details/general_page.rs @@ -626,8 +626,7 @@ mod imp { .await .is_err() { - let obj = imp.obj(); - toast!(obj, gettext("Could not change notifications setting")); + toast!(imp.obj(), gettext("Could not change notifications setting")); } imp.set_notifications_loading(false, setting); @@ -802,8 +801,7 @@ mod imp { row.set_read_only(true); if join_rule.set_value(value).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not change who can join")); + toast!(self.obj(), gettext("Could not change who can join")); self.update_join_rule(); } } @@ -851,8 +849,7 @@ mod imp { if let Err(error) = handle.await.unwrap() { error!("Could not change guest access: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not change guest access")); + toast!(self.obj(), gettext("Could not change guest access")); self.update_guest_access(); } } @@ -948,8 +945,7 @@ mod imp { } else { gettext("Could not unpublish room from directory") }; - let obj = self.obj(); - toast!(obj, text); + toast!(self.obj(), text); } self.update_publish().await; @@ -1024,8 +1020,7 @@ mod imp { if let Err(error) = handle.await.unwrap() { error!("Could not change room history visibility: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not change who can read history")); + toast!(self.obj(), gettext("Could not change who can read history")); self.update_history_visibility(); } diff --git a/src/session/view/content/room_details/invite_subpage/mod.rs b/src/session/view/content/room_details/invite_subpage/mod.rs index 72e77ee4..c6154f69 100644 --- a/src/session/view/content/room_details/invite_subpage/mod.rs +++ b/src/session/view/content/room_details/invite_subpage/mod.rs @@ -220,9 +220,8 @@ mod imp { let first_failed = invite_list.first_invitee().map(|item| item.user()).unwrap(); - let obj = self.obj(); toast!( - obj, + self.obj(), ngettext( // Translators: Do NOT translate the content between '{' and '}', these // are variable names. @@ -232,7 +231,7 @@ mod imp { ), @user = first_failed, @room, - n = n.to_string(), + n, ); } } diff --git a/src/session/view/content/room_details/mod.rs b/src/session/view/content/room_details/mod.rs index f9f53bbd..cb1b7039 100644 --- a/src/session/view/content/room_details/mod.rs +++ b/src/session/view/content/room_details/mod.rs @@ -129,7 +129,7 @@ mod imp { obj.pop_subpage(); toast!( obj, - gettext("The user is not in the room members list anymore") + gettext("The user is not in the room members list anymore"), ); } )); diff --git a/src/session/view/content/room_details/permissions/permissions_subpage.rs b/src/session/view/content/room_details/permissions/permissions_subpage.rs index 84ad1a13..0751a3c2 100644 --- a/src/session/view/content/room_details/permissions/permissions_subpage.rs +++ b/src/session/view/content/room_details/permissions/permissions_subpage.rs @@ -593,8 +593,7 @@ mod imp { }; if permissions.set_power_levels(power_levels).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not save permissions")); + toast!(self.obj(), gettext("Could not save permissions")); self.save_button.set_is_loading(false); } } diff --git a/src/session/view/content/room_history/item_row.rs b/src/session/view/content/room_history/item_row.rs index 38d9297a..dde927b5 100644 --- a/src/session/view/content/room_history/item_row.rs +++ b/src/session/view/content/room_history/item_row.rs @@ -936,8 +936,7 @@ mod imp { }; if event.room().toggle_reaction(key, &event).await.is_err() { - let obj = self.obj(); - toast!(obj, gettext("Could not toggle reaction")); + toast!(self.obj(), gettext("Could not toggle reaction")); } } @@ -1011,8 +1010,7 @@ mod imp { if let Err(error) = handle.await.unwrap() { error!("Could not discard local event: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not discard message")); + toast!(self.obj(), gettext("Could not discard message")); } } } diff --git a/src/session/view/content/room_history/message_toolbar/mod.rs b/src/session/view/content/room_history/message_toolbar/mod.rs index 7b2b65a6..57f46853 100644 --- a/src/session/view/content/room_history/message_toolbar/mod.rs +++ b/src/session/view/content/room_history/message_toolbar/mod.rs @@ -520,8 +520,7 @@ mod imp { if let Err(error) = handle.await.expect("task was not aborted") { error!("Could not send reply: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not send reply")); + toast!(self.obj(), gettext("Could not send reply")); } } Some(RelationInfo::Edit(event_id)) => { @@ -537,8 +536,7 @@ mod imp { }); if let Err(error) = handle.await.unwrap() { error!("Could not send edit: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not send edit")); + toast!(self.obj(), gettext("Could not send edit")); } } _ => { @@ -549,8 +547,7 @@ mod imp { }); if let Err(error) = handle.await.unwrap() { error!("Could not send message: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not send message")); + toast!(self.obj(), gettext("Could not send message")); } } } @@ -679,8 +676,7 @@ mod imp { if let Err(error) = handle.await.unwrap() { error!("Could not send location: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not send location")); + toast!(self.obj(), gettext("Could not send location")); } } @@ -692,8 +688,7 @@ mod imp { LocationError::Other => gettext("Could not retrieve current location"), }; - let obj = self.obj(); - toast!(obj, msg); + toast!(self.obj(), msg); } /// Send the attachment with the given data. @@ -721,8 +716,7 @@ mod imp { if let Err(error) = handle.await.unwrap() { error!("Could not send file: {error}"); - let obj = self.obj(); - toast!(obj, gettext("Could not send file")); + toast!(self.obj(), gettext("Could not send file")); } } diff --git a/src/session/view/content/room_history/sender_avatar/mod.rs b/src/session/view/content/room_history/sender_avatar/mod.rs index 2c6ba1a9..86c6b050 100644 --- a/src/session/view/content/room_history/sender_avatar/mod.rs +++ b/src/session/view/content/room_history/sender_avatar/mod.rs @@ -1,5 +1,5 @@ use adw::{prelude::*, subclass::prelude::*}; -use gettextrs::gettext; +use gettextrs::{gettext, ngettext}; use gtk::{gdk, glib, glib::clone, CompositeTemplate}; use ruma::{events::room::power_levels::PowerLevelUserAction, OwnedEventId}; @@ -8,7 +8,7 @@ use crate::{ confirm_mute_room_member_dialog, confirm_room_member_destructive_action_dialog, Avatar, RoomMemberDestructiveAction, UserProfileDialog, }, - gettext_f, ngettext_f, + gettext_f, prelude::*, session::{ model::{Member, MemberRole, Membership, User}, @@ -753,14 +753,14 @@ mod imp { let n = u32::try_from(events.len()).unwrap_or(u32::MAX); toast!( obj, - ngettext_f( + ngettext( // Translators: Do NOT translate the content between '{' and '}', // this is a variable name. "Removing 1 message sent by the user…", "Removing {n} messages sent by the user…", n, - &[("n", &n.to_string())] - ) + ), + n, ); let room = sender.room(); @@ -769,14 +769,14 @@ mod imp { let n = u32::try_from(failed_events.len()).unwrap_or(u32::MAX); toast!( obj, - ngettext_f( + ngettext( // Translators: Do NOT translate the content between '{' and '}', // this is a variable name. "Could not remove 1 message sent by the user", "Could not remove {n} messages sent by the user", n, - &[("n", &n.to_string())] - ) + ), + n, ); } } diff --git a/src/session/view/create_room_dialog.rs b/src/session/view/create_room_dialog.rs index 6ba8f259..c1f23077 100644 --- a/src/session/view/create_room_dialog.rs +++ b/src/session/view/create_room_dialog.rs @@ -238,8 +238,7 @@ mod imp { } } - let obj = self.obj(); - toast!(obj, error.to_user_facing()); + toast!(self.obj(), error.to_user_facing()); } } } diff --git a/src/session/view/sidebar/row.rs b/src/session/view/sidebar/row.rs index 0f8d1881..2294e873 100644 --- a/src/session/view/sidebar/row.rs +++ b/src/session/view/sidebar/row.rs @@ -766,9 +766,8 @@ mod imp { /// Forget the given room. async fn forget_room(&self, room: &Room) { if room.forget().await.is_err() { - let obj = self.obj(); toast!( - obj, + self.obj(), // Translators: Do NOT translate the content between '{' and '}', this is a variable name. gettext("Could not forget {room}"), @room, @@ -788,12 +787,12 @@ mod imp { error!("Could not mark room as direct chat: {error}"); // Translators: Do NOT translate the content between '{' and '}', this is a // variable name. - toast!(obj, gettext("Could not mark {room} as direct chat"), @room,); + toast!(obj, gettext("Could not mark {room} as direct chat"), @room); } else { error!("Could not unmark room as direct chat: {error}"); // Translators: Do NOT translate the content between '{' and '}', this is a // variable name. - toast!(obj, gettext("Could not unmark {room} as direct chat"), @room,); + toast!(obj, gettext("Could not unmark {room} as direct chat"), @room); } } } diff --git a/src/utils/macros.rs b/src/utils/macros.rs index a885f9ef..d4f113ae 100644 --- a/src/utils/macros.rs +++ b/src/utils/macros.rs @@ -1,11 +1,10 @@ //! Collection of macros. -/// Spawn a future on the default `MainContext` +/// Spawn a local future on the default `GMainContext`. /// -/// This was taken from `gtk-macros` -/// but allows setting optionally the priority +/// A custom [`glib::Priority`] can be set as the first argument. /// -/// FIXME: this should maybe be upstreamed +/// [`glib::Priority`]: gtk::glib::Priority #[macro_export] macro_rules! spawn { ($future:expr) => { @@ -18,187 +17,10 @@ macro_rules! spawn { }; } -/// Spawn a future on the tokio runtime +/// Spawn a future on the tokio runtime. #[macro_export] macro_rules! spawn_tokio { ($future:expr) => { $crate::RUNTIME.spawn($future) }; } - -/// Show a toast with the given message on the ancestor window of `widget`. -/// -/// The simplest way to use this macros is for displaying a simple message. It -/// can be anything that implements `AsRef`. -/// -/// ```no_run -/// use gettextts::gettext; -/// -/// use crate::toast; -/// -/// # let widget = unimplemented!(); -/// toast!(widget, gettext("Something happened")); -/// ``` -/// -/// This macro also supports replacing named variables with their value. It -/// supports both the `var` and the `var = expr` syntax. In this case the -/// message and the variables must be `String`s. -/// -/// ```no_run -/// use gettextts::gettext; -/// -/// use crate::toast; -/// -/// # let widget = unimplemented!(); -/// # let error_nb = 0; -/// toast!( -/// widget, -/// gettext("Error number {n}: {msg}"), -/// n = error_nb.to_string(), -/// msg, -/// ); -/// ``` -/// -/// To add `Pill`s to the toast, you can precede a [`Room`] or [`User`] with -/// `@`. -/// -/// ```no_run -/// use gettextts::gettext; -/// use crate::toast; -/// use crate::session::model::{Room, User}; -/// -/// # let session = unimplemented!(); -/// # let room_id = unimplemented!(); -/// # let user_id = unimplemented!(); -/// let room = Room::new(session, room_id); -/// let member = Member::new(room, user_id); -/// -/// toast!( -/// widget, -/// gettext("Could not contact {user} in {room}"), -/// @user = member, -/// @room, -/// ); -/// ``` -/// -/// For this macro to work, the ancestor window be a [`Window`](crate::Window) -/// or an [`adw::PreferencesWindow`]. -/// -/// [`Room`]: crate::session::model::Room -/// [`User`]: crate::session::model::User -#[macro_export] -macro_rules! toast { - ($widget:expr, $message:expr) => { - { - $crate::_add_toast!($widget, adw::Toast::new($message.as_ref())); - } - }; - ($widget:expr, $message:expr, $($tail:tt)+) => { - { - let (string_vars, pill_vars) = $crate::_toast_accum!([], [], $($tail)+); - let string_dict: Vec<_> = string_vars - .iter() - .map(|(key, val): &(&str, String)| (key.as_ref(), val.as_ref())) - .collect(); - let message = $crate::utils::freplace($message.into(), &*string_dict); - - let toast = if pill_vars.is_empty() { - adw::Toast::new($message.as_ref()) - } else { - let pill_vars = std::collections::HashMap::<&str, $crate::components::Pill>::from(pill_vars); - let mut swapped_label = String::new(); - let mut widgets = Vec::with_capacity(pill_vars.len()); - let mut last_end = 0; - - let mut matches = pill_vars - .keys() - .map(|key: &&str| { - message - .match_indices(&format!("{{{key}}}")) - .map(|(start, _)| (start, key)) - .collect::>() - }) - .flatten() - .collect::>(); - matches.sort_unstable(); - - for (start, key) in matches { - swapped_label.push_str(&message[last_end..start]); - swapped_label.push_str($crate::components::LabelWithWidgets::PLACEHOLDER); - last_end = start + key.len() + 2; - widgets.push(pill_vars.get(key).unwrap().clone()) - } - swapped_label.push_str(&message[last_end..message.len()]); - - let widget = $crate::components::LabelWithWidgets::new(); - widget.set_label_and_widgets( - swapped_label, - widgets, - ); - - adw::Toast::builder() - .custom_title(&widget) - .build() - }; - - $crate::_add_toast!($widget, toast); - } - }; -} -#[doc(hidden)] -#[macro_export] -macro_rules! _toast_accum { - ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident, $($tail:tt)*) => { - $crate::_toast_accum!([$($string_vars)* (stringify!($var), $var),], [$($pill_vars)*], $($tail)*) - }; - ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr, $($tail:tt)*) => { - $crate::_toast_accum!([$($string_vars)* (stringify!($var), $val),], [$($pill_vars)*], $($tail)*) - }; - ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident, $($tail:tt)*) => { - { - use $crate::components::PillSourceExt; - let pill: $crate::components::Pill = $var.to_pill(); - $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*) - } - }; - ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr, $($tail:tt)*) => { - { - use $crate::components::PillSourceExt; - let pill: $crate::components::Pill = $val.to_pill(); - $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*) - } - }; - ([$($string_vars:tt)*], [$($pill_vars:tt)*],) => { ([$($string_vars)*], [$($pill_vars)*]) }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! _add_toast { - ($widget:expr, $toast:expr) => {{ - use gtk::prelude::WidgetExt; - if let Some(dialog) = $widget - .ancestor($crate::components::ToastableDialog::static_type()) - .and_downcast::<$crate::components::ToastableDialog>() - { - use $crate::prelude::ToastableDialogExt; - dialog.add_toast($toast); - } else if let Some(dialog) = $widget - .ancestor(adw::PreferencesDialog::static_type()) - .and_downcast::() - { - use adw::prelude::PreferencesDialogExt; - dialog.add_toast($toast); - } else if let Some(root) = $widget.root() { - // FIXME: AdwPreferencesWindow is deprecated but RoomDetails uses it. - #[allow(deprecated)] - if let Some(window) = root.downcast_ref::() { - use adw::prelude::PreferencesWindowExt; - window.add_toast($toast); - } else if let Some(window) = root.downcast_ref::<$crate::Window>() { - window.add_toast($toast); - } else { - panic!("Trying to display a toast when the parent doesn't support it"); - } - } - }}; -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index bfb71215..7f4b2b1f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,7 @@ //! Collection of common methods and types. use std::{ + borrow::Cow, cell::{Cell, OnceCell, RefCell}, fmt, fs, io::{self, Write}, @@ -30,6 +31,7 @@ mod single_item_list_model; pub(crate) mod sourceview; pub(crate) mod string; mod template_callbacks; +pub(crate) mod toast; pub(crate) use self::{ dummy_object::DummyObject, @@ -86,11 +88,11 @@ pub(crate) async fn timeout_future( /// /// The expected format to replace is `{name}`, where `name` is the first string /// in the dictionary entry tuple. -pub(crate) fn freplace(s: String, args: &[(&str, &str)]) -> String { - let mut s = s; +pub(crate) fn freplace<'a>(s: &'a str, args: &[(&str, &str)]) -> Cow<'a, str> { + let mut s = Cow::Borrowed(s); for (k, v) in args { - s = s.replace(&format!("{{{k}}}"), v); + s = Cow::Owned(s.replace(&format!("{{{k}}}"), v)); } s diff --git a/src/utils/toast.rs b/src/utils/toast.rs new file mode 100644 index 00000000..4934a8a3 --- /dev/null +++ b/src/utils/toast.rs @@ -0,0 +1,241 @@ +//! Macros and methods to display toasts in the interface. + +use std::collections::HashMap; + +use adw::prelude::*; + +use super::freplace; +use crate::{ + components::{LabelWithWidgets, Pill, ToastableDialog}, + prelude::*, + Window, +}; + +/// Show a toast with the given message on an ancestor of `widget`. +/// +/// The simplest way to use this macro is for displaying a simple message. It +/// can be anything that implements `AsRef`. +/// +/// ```no_run +/// use gettextts::gettext; +/// +/// use crate::toast; +/// +/// # let widget = unimplemented!(); +/// toast!(widget, gettext("Something happened")); +/// ``` +/// +/// This macro also supports replacing named variables with their value. It +/// supports both the `var` and the `var = expr` syntax. The variable value must +/// implement `ToString`. +/// +/// ```no_run +/// use gettextts::gettext; +/// +/// use crate::toast; +/// +/// # let widget = unimplemented!(); +/// # let error_nb = 0; +/// toast!( +/// widget, +/// gettext("Error number {n}: {msg}"), +/// n = error_nb.to_string(), +/// msg, +/// ); +/// ``` +/// +/// To add [`Pill`]s to the toast, you can precede a type that implements +/// [`PillSource`] with `@`. +/// +/// ```no_run +/// use gettextts::gettext; +/// use crate::toast; +/// use crate::session::model::{Room, User}; +/// +/// # let session = unimplemented!(); +/// # let room_id = unimplemented!(); +/// # let user_id = unimplemented!(); +/// let room = Room::new(session, room_id); +/// let member = Member::new(room, user_id); +/// +/// toast!( +/// widget, +/// gettext("Could not contact {user} in {room}"), +/// @user = member, +/// @room, +/// ); +/// ``` +/// +/// For this macro to work, the widget must have one of these ancestors that can +/// show toasts: +/// +/// - `ToastableDialog` +/// - `AdwPreferencesDialog` +/// - `AdwPreferencesWindow` +/// - `Window` +/// +/// [`PillSource`]: crate::components::PillSource +#[macro_export] +macro_rules! toast { + // Without vars, with or without a trailing comma. + ($widget:expr, $message:expr $(,)?) => { + { + $crate::utils::toast::add_toast( + $widget.upcast_ref(), + adw::Toast::new($message.as_ref()) + ); + } + }; + // With vars. + ($widget:expr, $message:expr, $($tail:tt)+) => { + { + let (string_vars, pill_vars) = $crate::_toast_accum!([], [], $($tail)+); + $crate::utils::toast::add_toast_with_vars( + $widget.upcast_ref(), + $message.as_ref(), + &string_vars, + &pill_vars.into() + ); + } + }; +} + +/// Macro to accumulate the variables passed to `toast!`. +/// +/// Returns a `([(&str, String)],[(&str, Pill)])` tuple. The items in the first +/// array are `(var_name, var_value)` tuples, and the ones in the second array +/// are `(var_name, pill)` tuples. +#[doc(hidden)] +#[macro_export] +macro_rules! _toast_accum { + // `var = val` syntax, without anything after. + ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr) => { + $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)*], $var = $val,) + }; + // `var = val` syntax, with a trailing comma or other vars after. + ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr, $($tail:tt)*) => { + $crate::_toast_accum!([$($string_vars)* (stringify!($var), $val.to_string()),], [$($pill_vars)*], $($tail)*) + }; + // `var` syntax, with or without a trailing comma and other vars after. + ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident $($tail:tt)*) => { + $crate::_toast_accum!([$($string_vars)* (stringify!($var), $var.to_string()),], [$($pill_vars)*] $($tail)*) + }; + // `@var = val` syntax, without anything after. + ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr) => { + $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)*], @$var = $val,) + }; + // `@var = val` syntax, with a trailing comma or other vars after. + ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr, $($tail:tt)*) => { + { + use $crate::components::PillSourceExt; + let pill: $crate::components::Pill = $val.to_pill(); + $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*) + } + }; + // `@var` syntax, with or without a trailing comma and other vars after. + ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident $($tail:tt)*) => { + { + use $crate::components::PillSourceExt; + let pill: $crate::components::Pill = $var.to_pill(); + $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),] $($tail)*) + } + }; + // No more vars, with or without trailing comma. + ([$($string_vars:tt)*], [$($pill_vars:tt)*] $(,)?) => { ([$($string_vars)*], [$($pill_vars)*]) }; +} + +/// Add the given `AdwToast` to the ancestor of the given widget. +/// +/// The widget must have one of these ancestors that can show toasts: +/// +/// - `ToastableDialog` +/// - `AdwPreferencesDialog` +/// - `AdwPreferencesWindow` +/// - `Window` +pub(crate) fn add_toast(widget: >k::Widget, toast: adw::Toast) { + if let Some(dialog) = widget + .ancestor(ToastableDialog::static_type()) + .and_downcast::() + { + dialog.add_toast(toast); + } else if let Some(dialog) = widget + .ancestor(adw::PreferencesDialog::static_type()) + .and_downcast::() + { + dialog.add_toast(toast); + } else if let Some(root) = widget.root() { + // FIXME: AdwPreferencesWindow is deprecated but RoomDetails uses it. + #[allow(deprecated)] + if let Some(window) = root.downcast_ref::() { + use adw::prelude::PreferencesWindowExt; + window.add_toast(toast); + } else if let Some(window) = root.downcast_ref::() { + window.add_toast(toast); + } else { + panic!("Trying to display a toast when the parent doesn't support it"); + } + } +} + +/// Add a toast with the given message and variables to the ancestor of the +/// given widget. +/// +/// The widget must have one of these ancestors that can show toasts: +/// +/// - `ToastableDialog` +/// - `AdwPreferencesDialog` +/// - `AdwPreferencesWindow` +/// - `Window` +pub(crate) fn add_toast_with_vars( + widget: >k::Widget, + message: &str, + string_vars: &[(&str, String)], + pill_vars: &HashMap<&str, Pill>, +) { + let string_dict: Vec<_> = string_vars + .iter() + .map(|(key, val)| (*key, val.as_ref())) + .collect(); + let message = freplace(message, &string_dict); + + let toast = if pill_vars.is_empty() { + adw::Toast::new(&message) + } else { + let mut swapped_label = String::new(); + let mut widgets = Vec::with_capacity(pill_vars.len()); + let mut last_end = 0; + + // Find the locations of the pills in the message. + let mut matches = pill_vars + .keys() + .flat_map(|key| { + message + .match_indices(&format!("{{{key}}}")) + .map(|(start, _)| (start, *key)) + .collect::>() + }) + .collect::>(); + // Sort the locations, so we can insert the pills in the right order. + matches.sort_unstable(); + + for (start, key) in matches { + swapped_label.push_str(&message[last_end..start]); + swapped_label.push_str(LabelWithWidgets::PLACEHOLDER); + last_end = start + key.len() + 2; + widgets.push( + pill_vars + .get(key) + .expect("match key should be in map") + .clone(), + ); + } + swapped_label.push_str(&message[last_end..message.len()]); + + let widget = LabelWithWidgets::new(); + widget.set_label_and_widgets(swapped_label, widgets); + + adw::Toast::builder().custom_title(&widget).build() + }; + + add_toast(widget, toast); +}