diff --git a/src/components/editable_avatar.rs b/src/components/editable_avatar.rs index c03a01e0cfcdb6945f1f70887d776bf5d1f41ecf..7582d859d38034b58804dfd84eabc6da76402d44 100644 --- a/src/components/editable_avatar.rs +++ b/src/components/editable_avatar.rs @@ -10,7 +10,7 @@ use gtk::{ use log::error; use super::{ActionButton, ActionState}; -use crate::{session::Avatar, spawn}; +use crate::{session::Avatar, spawn, toast}; mod imp { use std::cell::{Cell, RefCell}; @@ -361,27 +361,18 @@ impl EditableAvatar { self.emit_by_name::<()>("edit-avatar", &[&file]); } else { error!("The chosen file is not an image"); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("The chosen file is not an image").to_variant()), - ); + toast!(self, gettext("The chosen file is not an image")); } } else { error!("Could not get the content type of the file"); - let _ = self.activate_action( - "win.add-toast", - Some( - &gettext("Could not determine the type of the chosen file") - .to_variant(), - ), + toast!( + self, + gettext("Could not determine the type of the chosen file") ); } } else { error!("No file chosen"); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("No file was chosen").to_variant()), - ); + toast!(self, gettext("No file was chosen")); } } } diff --git a/src/components/label_with_widgets.rs b/src/components/label_with_widgets.rs index 737de3cd1a16825e4c98b4d3498f9ad96b267774..ae3dea646928c4dd49adc45a1c9744f616543595 100644 --- a/src/components/label_with_widgets.rs +++ b/src/components/label_with_widgets.rs @@ -2,7 +2,7 @@ use std::cmp::max; use gtk::{glib, glib::clone, pango, prelude::*, subclass::prelude::*}; -const DEFAULT_PLACEHOLDER: &str = ""; +pub const DEFAULT_PLACEHOLDER: &str = ""; const PANGO_SCALE: i32 = 1024; const OBJECT_REPLACEMENT_CHARACTER: &str = "\u{FFFC}"; fn pango_pixels(d: i32) -> i32 { diff --git a/src/components/mod.rs b/src/components/mod.rs index 4c151b617265a84b9063adbf16814501132a55cb..ffa03ef06a62c46526636de20a58739db9a02082 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -36,7 +36,7 @@ pub use self::{ editable_avatar::EditableAvatar, entry_row::EntryRow, in_app_notification::InAppNotification, - label_with_widgets::LabelWithWidgets, + label_with_widgets::{LabelWithWidgets, DEFAULT_PLACEHOLDER}, loading_listbox_row::LoadingListBoxRow, location_viewer::LocationViewer, media_content_viewer::{ContentType, MediaContentViewer}, diff --git a/src/components/toast.rs b/src/components/toast.rs index 6581445a29fc30d177608227f2426a477a4894d0..e7b2c6223e36998d4b210cb53e41daa9e431f68d 100644 --- a/src/components/toast.rs +++ b/src/components/toast.rs @@ -131,12 +131,12 @@ impl ToastBuilder { Self::default() } - pub fn title(mut self, title: &str) -> Self { - self.title = Some(title.to_owned()); + pub fn title(mut self, title: String) -> Self { + self.title = Some(title); self } - pub fn widgets(mut self, widgets: &[&impl IsA]) -> Self { + pub fn widgets(mut self, widgets: &[impl IsA]) -> Self { self.widgets = Some(widgets.iter().map(|w| w.upcast_ref().clone()).collect()); self } diff --git a/src/error_page.rs b/src/error_page.rs index 5ed7a188e3e51f39dc360f68399f82ac3bf270bd..86766db38e1048d9de1226198a3faf120d137289 100644 --- a/src/error_page.rs +++ b/src/error_page.rs @@ -3,7 +3,7 @@ use gettextrs::gettext; use gtk::{self, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate}; use log::error; -use crate::{components::Toast, secret, secret::SecretError, spawn, window::Window}; +use crate::{secret, secret::SecretError, spawn, toast, window::Window}; pub enum ErrorSubpage { SecretErrorSession, @@ -105,21 +105,16 @@ impl ErrorPage { .as_ref() .and_then(|root| root.downcast_ref::()) { - window.add_toast(&Toast::new(&gettext("Session removed successfully."))); + toast!(self, gettext("Session removed successfully.")); window.restore_sessions().await; } } Err(err) => { error!("Could not remove session from secret storage: {:?}", err); - if let Some(window) = self - .root() - .as_ref() - .and_then(|root| root.downcast_ref::()) - { - window.add_toast(&Toast::new(&gettext( - "Could not remove session from secret storage", - ))); - } + toast!( + self, + gettext("Could not remove session from secret storage") + ); } } } diff --git a/src/i18n.rs b/src/i18n.rs index ea704f949f077d88ca4ba915eaf45fc16a38be06..2daef5f01c2dd98f24ea50d980c5879c5e1c1a1f 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -1,14 +1,6 @@ use gettextrs::{gettext, ngettext}; -fn freplace(s: String, args: &[(&str, &str)]) -> String { - let mut s = s; - - for (k, v) in args { - s = s.replace(&format!("{{{}}}", k), v); - } - - s -} +use crate::utils::freplace; /// Like `gettext`, but replaces named variables with the given dictionary. /// diff --git a/src/login/mod.rs b/src/login/mod.rs index 5ec3a5c0483f217dab2e488c12e893efd4c0f9fd..d093e26ee405fabebf7d723c040718db8a177cd3 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -22,8 +22,8 @@ use idp_button::IdpButton; use login_advanced_dialog::LoginAdvancedDialog; use crate::{ - components::{EntryRow, PasswordEntryRow, SpinnerButton, Toast}, - gettext_f, spawn, spawn_tokio, + components::{EntryRow, PasswordEntryRow, SpinnerButton}, + gettext_f, spawn, spawn_tokio, toast, user_facing_error::UserFacingError, Session, }; @@ -380,7 +380,7 @@ impl Login { } Err(error) => { warn!("Failed to discover homeserver: {}", error); - obj.parent_window().add_toast(&Toast::new(&error.to_user_facing())); + toast!(obj, error.to_user_facing()); } }; obj.unfreeze(); @@ -448,7 +448,7 @@ impl Login { } Err(error) => { warn!("Failed to check homeserver: {}", error); - obj.parent_window().add_toast(&Toast::new(&error.to_user_facing())); + toast!(obj, error.to_user_facing()); } }; obj.unfreeze(); @@ -600,7 +600,7 @@ impl Login { clone!(@weak self as login => move |session, error| { match error { Some(e) => { - login.parent_window().add_toast(&e); + toast!(login, e); login.unfreeze(); }, None => { diff --git a/src/session/account_settings/devices_page/device_row.rs b/src/session/account_settings/devices_page/device_row.rs index ba11ced14b099dd985de5cb7877c72b2a2459bde..6d657b33f97c5cb508dc24e71afbcc7c05ae6e0b 100644 --- a/src/session/account_settings/devices_page/device_row.rs +++ b/src/session/account_settings/devices_page/device_row.rs @@ -5,8 +5,8 @@ use log::error; use super::Device; use crate::{ - components::{AuthError, SpinnerButton, Toast}, - gettext_f, spawn, + components::{AuthError, SpinnerButton}, + gettext_f, spawn, toast, }; mod imp { @@ -210,12 +210,10 @@ impl DeviceRow { Err(AuthError::UserCancelled) => {}, Err(err) => { error!("Failed to disconnect device {}: {err:?}", device.device_id()); - if let Some(adw_window) = window.and_then(|w| w.downcast::().ok()) { - let device_name = device.display_name(); - // Translators: Do NOT translate the content between '{' and '}', this is a variable name. - let error_message = gettext_f("Failed to disconnect device “{device_name}”", &[("device_name", device_name)]); - adw_window.add_toast(&Toast::new(&error_message).into()); - } + let device_name = device.display_name(); + // Translators: Do NOT translate the content between '{' and '}', this is a variable name. + let error_message = gettext_f("Failed to disconnect device “{device_name}”", &[("device_name", device_name)]); + toast!(obj, error_message); }, } obj.imp().delete_logout_button.set_loading(false); diff --git a/src/session/account_settings/user_page/change_password_subpage.rs b/src/session/account_settings/user_page/change_password_subpage.rs index 5008d7a599927327d5ebd7fe37b3a55c7d2a8260..0e2a71f1a8ca5e89dd88a739a0388160560ed474 100644 --- a/src/session/account_settings/user_page/change_password_subpage.rs +++ b/src/session/account_settings/user_page/change_password_subpage.rs @@ -20,7 +20,7 @@ use matrix_sdk::{ use crate::{ components::{AuthDialog, AuthError, PasswordEntryRow, SpinnerButton}, session::Session, - spawn, + spawn, toast, utils::validate_password, }; @@ -308,10 +308,7 @@ impl ChangePasswordSubpage { match result { Ok(_) => { - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Password changed successfully").to_variant()), - ); + toast!(self, gettext("Password changed successfully")); priv_.password.set_text(""); priv_.confirm_password.set_text(""); self.activate_action("win.close-subpage", None).unwrap(); @@ -326,17 +323,11 @@ impl ChangePasswordSubpage { )) if error.kind == ErrorKind::WeakPassword) => { error!("Weak password: {:?}", error); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Password rejected for being too weak").to_variant()), - ); + toast!(self, gettext("Password rejected for being too weak")); } _ => { error!("Failed to change the password: {:?}", err); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Could not change password").to_variant()), - ); + toast!(self, gettext("Could not change password")); } }, } diff --git a/src/session/account_settings/user_page/deactivate_account_subpage.rs b/src/session/account_settings/user_page/deactivate_account_subpage.rs index 6d8751c89120eb916f543e49be7e988b2019209d..a7816b1ac4affdb45410aaaa4ad6523038848053 100644 --- a/src/session/account_settings/user_page/deactivate_account_subpage.rs +++ b/src/session/account_settings/user_page/deactivate_account_subpage.rs @@ -9,9 +9,9 @@ use log::error; use matrix_sdk::ruma::{api::client::account::deactivate, assign}; use crate::{ - components::{AuthDialog, EntryRow, SpinnerButton, Toast}, + components::{AuthDialog, EntryRow, SpinnerButton}, session::{Session, UserExt}, - spawn, + spawn, toast, }; mod imp { @@ -194,10 +194,7 @@ impl DeactivateAccountSubpage { match result { Ok(_) => { if let Some(session) = self.session() { - session - .parent_window() - .unwrap() - .add_toast(&Toast::new(&gettext("Account successfully deactivated"))); + toast!(session, gettext("Account successfully deactivated")); session.handle_logged_out(); } self.activate_action("account-settings.close", None) @@ -205,10 +202,7 @@ impl DeactivateAccountSubpage { } Err(err) => { error!("Failed to deactivate account: {:?}", err); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Could not deactivate account").to_variant()), - ); + toast!(self, gettext("Could not deactivate account")); } } priv_.button.set_loading(false); diff --git a/src/session/account_settings/user_page/mod.rs b/src/session/account_settings/user_page/mod.rs index c10891c76fbb2afd2c192776b44014c70f0b03f1..f2ff309c1a9b37e39c60f9c62dd4b300bd6b06f3 100644 --- a/src/session/account_settings/user_page/mod.rs +++ b/src/session/account_settings/user_page/mod.rs @@ -20,7 +20,7 @@ use deactivate_account_subpage::DeactivateAccountSubpage; use crate::{ components::{ActionState, ButtonRow, EditableAvatar, EntryRow}, session::{Session, User, UserExt}, - spawn, spawn_tokio, + spawn, spawn_tokio, toast, utils::TemplateCallbacks, }; @@ -222,10 +222,7 @@ impl UserPage { avatar.show_temp_image(false); avatar.set_remove_state(ActionState::Success); avatar.set_edit_sensitive(true); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Avatar removed successfully").to_variant()), - ); + toast!(self, gettext("Avatar removed successfully")); glib::timeout_add_local_once( Duration::from_secs(2), clone!(@weak avatar => move || { @@ -240,10 +237,7 @@ impl UserPage { avatar.show_temp_image(false); avatar.set_temp_image_from_file(None); avatar.set_remove_sensitive(true); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Avatar changed successfully").to_variant()), - ); + toast!(self, gettext("Avatar changed successfully")); glib::timeout_add_local_once( Duration::from_secs(2), clone!(@weak avatar => move || { @@ -286,10 +280,7 @@ impl UserPage { Ok(res) => res.content_uri, Err(error) => { error!("Could not upload user avatar: {}", error); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Could not upload avatar").to_variant()), - ); + toast!(self, gettext("Could not upload avatar")); avatar.show_temp_image(false); avatar.set_temp_image_from_file(None); avatar.set_edit_state(ActionState::Default); @@ -311,10 +302,7 @@ impl UserPage { Err(error) => { if priv_.changing_avatar_to.take().is_some() { error!("Could not change user avatar: {}", error); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Could not change avatar").to_variant()), - ); + toast!(self, gettext("Could not change avatar")); avatar.show_temp_image(false); avatar.set_temp_image_from_file(None); avatar.set_edit_state(ActionState::Default); @@ -343,10 +331,7 @@ impl UserPage { if priv_.removing_avatar.get() { priv_.removing_avatar.set(false); error!("Couldn’t remove user avatar: {}", error); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Could not remove avatar").to_variant()), - ); + toast!(self, gettext("Could not remove avatar")); avatar.show_temp_image(false); avatar.set_remove_state(ActionState::Default); avatar.set_edit_sensitive(true); @@ -392,10 +377,7 @@ impl UserPage { entry.remove_css_class("error"); entry.set_action_state(ActionState::Success); entry.set_entry_sensitive(true); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Name changed successfully").to_variant()), - ); + toast!(self, gettext("Name changed successfully")); glib::timeout_add_local_once( Duration::from_secs(2), clone!(@weak entry => move || { @@ -431,10 +413,7 @@ impl UserPage { } Err(err) => { error!("Couldn’t change user display name: {}", err); - let _ = self.activate_action( - "win.add-toast", - Some(&gettext("Could not change display name").to_variant()), - ); + toast!(self, gettext("Could not change display name")); entry.set_action_state(ActionState::Retry); entry.add_css_class("error"); entry.set_entry_sensitive(true); diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs index 304a2fd94fefa5bf5ba3083e0932f8a579c569e0..15bf3ae8fbf13ad5300453477b4760e852abfd07 100644 --- a/src/session/content/room_history/mod.rs +++ b/src/session/content/room_history/mod.rs @@ -34,16 +34,15 @@ use self::{ state_row::StateRow, verification_info_bar::VerificationInfoBar, }; use crate::{ - components::{CustomEntry, DragOverlay, Pill, ReactionChooser, RoomTitle, Toast}, + components::{CustomEntry, DragOverlay, Pill, ReactionChooser, RoomTitle}, i18n::gettext_f, session::{ content::{MarkdownPopover, RoomDetails}, room::{Event, Room, RoomType, Timeline, TimelineItem, TimelineState}, user::UserExt, }, - spawn, + spawn, toast, utils::filename_for_mime, - window::Window, }; mod imp { @@ -53,7 +52,7 @@ mod imp { use once_cell::unsync::OnceCell; use super::*; - use crate::{components::Toast, window::Window, Application}; + use crate::Application; #[derive(Debug, Default, CompositeTemplate)] #[template(resource = "/org/gnome/Fractal/content-room-history.ui")] @@ -162,14 +161,8 @@ mod imp { _ => None, }; - if let Some(window) = widget - .root() - .as_ref() - .and_then(|root| root.downcast_ref::()) - { - if let Some(message) = toast_error { - window.add_toast(&Toast::new(&message)); - } + if let Some(message) = toast_error { + toast!(widget, message); } })); }); @@ -843,14 +836,7 @@ impl RoomHistory { } Err(err) => { warn!("Could not read file: {}", err); - - if let Some(window) = self - .root() - .as_ref() - .and_then(|root| root.downcast_ref::()) - { - window.add_toast(&Toast::new(&gettext("Error reading file"))); - } + toast!(self, gettext("Error reading file")); } } } @@ -874,16 +860,10 @@ impl RoomHistory { } Err(error) => { warn!("Could not get file from drop: {error:?}"); - - if let Some(window) = obj - .root() - .as_ref() - .and_then(|root| root.downcast_ref::()) - { - window.add_toast( - &Toast::new(&gettext("Error getting file from drop")) - ); - } + toast!( + obj, + gettext("Error getting file from drop") + ); false } @@ -914,13 +894,7 @@ impl RoomHistory { Err(error) => warn!("Could not get GdkTexture from the clipboard: {error:?}"), } - if let Some(window) = self - .root() - .as_ref() - .and_then(|root| root.downcast_ref::()) - { - window.add_toast(&Toast::new(&gettext("Error getting image from clipboard"))); - } + toast!(self, gettext("Error getting image from clipboard")); } else if formats.contains_type(gio::File::static_type()) { // There is a file in the clipboard. match clipboard @@ -937,13 +911,7 @@ impl RoomHistory { Err(error) => warn!("Could not get file from the clipboard: {error:?}"), } - if let Some(window) = self - .root() - .as_ref() - .and_then(|root| root.downcast_ref::()) - { - window.add_toast(&Toast::new(&gettext("Error getting file from clipboard"))); - } + toast!(self, gettext("Error getting file from clipboard")); } } diff --git a/src/session/content/verification/session_verification.rs b/src/session/content/verification/session_verification.rs index 335f7833e9616b05c2841814d7146f869a2c06e7..dcc970e93157baf746075248f4df46ac9a2541a8 100644 --- a/src/session/content/verification/session_verification.rs +++ b/src/session/content/verification/session_verification.rs @@ -5,9 +5,9 @@ use log::{debug, error}; use super::IdentityVerificationWidget; use crate::{ - components::{AuthDialog, AuthError, SpinnerButton, Toast}, + components::{AuthDialog, AuthError, SpinnerButton}, session::verification::{IdentityVerification, VerificationState}, - spawn, Session, Window, + spawn, toast, Session, Window, }; mod imp { @@ -326,9 +326,7 @@ impl SessionVerification { }; if let Some(error_message) = error_message { - if let Some(window) = obj.parent_window() { - window.add_toast(&Toast::new(&error_message)); - } + toast!(obj, error_message); } else { // TODO tell user that the a crypto identity was created obj.activate_action("session.show-content", None).unwrap(); diff --git a/src/session/mod.rs b/src/session/mod.rs index 0f748fe69354a6a4584eccbfaed2f884e71d2ad5..5c4e244bf82a56ecbec91579e8b5dc4b6b3f3294 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -66,11 +66,10 @@ pub use self::{ user::{User, UserActions, UserExt}, }; use crate::{ - components::Toast, secret, secret::{Secret, StoredSession}, session::sidebar::ItemList, - spawn, spawn_tokio, UserFacingError, Window, + spawn, spawn_tokio, toast, UserFacingError, Window, }; #[derive(Error, Debug)] @@ -235,7 +234,7 @@ mod imp { vec![ Signal::builder( "prepared", - &[Option::::static_type().into()], + &[Option::::static_type().into()], <()>::static_type().into(), ) .build(), @@ -491,7 +490,7 @@ impl Session { priv_.logout_on_dispose.set(false); - Some(Toast::new(&error.to_user_facing())) + Some(error.to_user_facing()) } }; @@ -658,13 +657,13 @@ impl Session { } /// Connects the prepared signals to the function f given in input - pub fn connect_prepared) + 'static>( + pub fn connect_prepared) + 'static>( &self, f: F, ) -> glib::SignalHandlerId { self.connect_local("prepared", true, move |values| { let obj = values[0].get::().unwrap(); - let err = values[1].get::>().unwrap(); + let err = values[1].get::>().unwrap(); f(&obj, err); @@ -758,9 +757,7 @@ impl Session { } Err(error) => { error!("Couldn’t logout the session {}", error); - if let Some(window) = self.parent_window() { - window.add_toast(&Toast::new(&gettext("Failed to logout the session."))); - } + toast!(self, gettext("Failed to logout the session.")); } } } diff --git a/src/session/room/event_actions.rs b/src/session/room/event_actions.rs index ac38d9ff2bc23b8b96f5887bc70fec0bf35b9b6e..a8c894e08a2b1019691f58132e2722c0b9e0370b 100644 --- a/src/session/room/event_actions.rs +++ b/src/session/room/event_actions.rs @@ -5,13 +5,12 @@ use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageLikeEventCo use once_cell::sync::Lazy; use crate::{ - components::Toast, session::{ event_source_dialog::EventSourceDialog, room::{Event, RoomAction}, user::UserExt, }, - spawn, + spawn, toast, utils::cache_dir, UserFacingError, Window, }; @@ -190,12 +189,12 @@ where let window: Window = self.root().unwrap().downcast().unwrap(); spawn!( glib::PRIORITY_LOW, - clone!(@weak window => async move { + clone!(@weak self as obj, @weak window => async move { let (_, filename, data) = match event.get_media_content().await { Ok(res) => res, Err(err) => { error!("Could not get file: {}", err); - window.add_toast(&Toast::new(&err.to_user_facing())); + toast!(obj, err.to_user_facing()); return; } @@ -236,15 +235,14 @@ where /// See `Event::get_media_content` for compatible events. Panics on an /// incompatible event. fn open_event_file(&self, event: Event) { - let window: Window = self.root().unwrap().downcast().unwrap(); spawn!( glib::PRIORITY_LOW, - clone!(@weak window => async move { + clone!(@weak self as obj => async move { let (uid, filename, data) = match event.get_media_content().await { Ok(res) => res, Err(err) => { error!("Could not get file: {}", err); - window.add_toast(&Toast::new(&err.to_user_facing())); + toast!(obj, err.to_user_facing()); return; } diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs index 412af38dfa8fc06dbb7f556ca54cefff04daf8fa..fc2065fde854a271cfd78a5ce41d4cd964ab73d0 100644 --- a/src/session/room/mod.rs +++ b/src/session/room/mod.rs @@ -12,7 +12,7 @@ mod timeline; use std::{cell::RefCell, convert::TryInto, path::PathBuf}; -use gettextrs::gettext; +use gettextrs::{gettext, ngettext}; use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; use log::{debug, error, info, warn}; use matrix_sdk::{ @@ -42,7 +42,7 @@ use matrix_sdk::{ serde::Raw, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, }, - DisplayName, + DisplayName, Result as MatrixResult, }; use ruma::events::SyncEphemeralRoomEvent; @@ -63,8 +63,8 @@ pub use self::{ }; use super::verification::IdentityVerification; use crate::{ - components::{Pill, Toast}, - gettext_f, ngettext_f, + components::Pill, + gettext_f, prelude::*, session::{ avatar::update_room_avatar_from_file, @@ -72,7 +72,7 @@ use crate::{ sidebar::{SidebarItem, SidebarItemImpl}, Avatar, Session, User, }, - spawn, spawn_tokio, + spawn, spawn_tokio, toast, utils::pending_event_ids, }; @@ -458,21 +458,17 @@ impl Room { obj.emit_by_name::<()>("room-forgotten", &[]); } Err(error) => { - error!("Couldn’t forget the room: {}", error); + error!("Couldn’t forget the room: {}", error); - let room_pill = Pill::for_room(&obj); - let error = Toast::builder() - // Translators: Do NOT translate the content between '{' and '}', this is a variable name. - .title(&gettext_f("Failed to forget {room}.", &[("room", "")])) - .widgets(&[&room_pill]) - .build(); - - if let Some(window) = obj.session().parent_window() { - window.add_toast(&error); - } + toast!( + obj.session(), + // Translators: Do NOT translate the content between '{' and '}', this is a variable name. + gettext("Failed to forget {room}."), + @room = &obj, + ); - // Load the previous category - obj.load_category(); + // Load the previous category + obj.load_category(); }, }; }) @@ -699,24 +695,21 @@ impl Room { match handle.await.unwrap() { Ok(_) => {}, Err(error) => { - error!("Couldn’t set the room category: {}", error); - - let room_pill = Pill::for_room(&obj); - let error = Toast::builder() - .title(&gettext_f( - // Translators: Do NOT translate the content between '{' and '}', this is a variable name. - "Failed to move {room} from {previous_category} to {new_category}.", - &[("room", ""),("previous_category", &previous_category.to_string()), ("new_category", &category.to_string())], - )) - .widgets(&[&room_pill]) - .build(); - - if let Some(window) = obj.session().parent_window() { - window.add_toast(&error); - } + error!("Couldn’t set the room category: {}", error); + + toast!( + obj.session(), + gettext( + // Translators: Do NOT translate the content between '{' and '}', this is a variable name. + "Failed to move {room} from {previous_category} to {new_category}.", + ), + @room = obj, + previous_category = previous_category.to_string(), + new_category = category.to_string(), + ); - // Load the previous category - obj.load_category(); + // Load the previous category + obj.load_category(); }, }; }) @@ -1426,7 +1419,7 @@ impl Room { ); } - pub async fn accept_invite(&self) -> Result<(), Toast> { + pub async fn accept_invite(&self) -> MatrixResult<()> { let matrix_room = self.matrix_room(); if let MatrixRoom::Invited(matrix_room) = matrix_room { @@ -1436,20 +1429,15 @@ impl Room { Err(error) => { error!("Accepting invitation failed: {}", error); - let room_pill = Pill::for_room(self); - let error = Toast::builder() - .title(&gettext_f( + toast!( + self.session(), + gettext( // Translators: Do NOT translate the content between '{' and '}', this // is a variable name. "Failed to accept invitation for {room}. Try again later.", - &[("room", "")], - )) - .widgets(&[&room_pill]) - .build(); - - if let Some(window) = self.session().parent_window() { - window.add_toast(&error); - } + ), + @room = self, + ); Err(error) } @@ -1460,7 +1448,7 @@ impl Room { } } - pub async fn reject_invite(&self) -> Result<(), Toast> { + pub async fn reject_invite(&self) -> MatrixResult<()> { let matrix_room = self.matrix_room(); if let MatrixRoom::Invited(matrix_room) = matrix_room { @@ -1470,20 +1458,15 @@ impl Room { Err(error) => { error!("Rejecting invitation failed: {}", error); - let room_pill = Pill::for_room(self); - let error = Toast::builder() - .title(&gettext_f( + toast!( + self.session(), + gettext( // Translators: Do NOT translate the content between '{' and '}', this // is a variable name. "Failed to reject invitation for {room}. Try again later.", - &[("room", "")], - )) - .widgets(&[&room_pill]) - .build(); - - if let Some(window) = self.session().parent_window() { - window.add_toast(&error); - } + ), + @room = self, + ); Err(error) } @@ -1640,35 +1623,33 @@ impl Room { let first_failed = failed_invites.first().unwrap(); // TODO: should we show all the failed users? - let error_message = - if no_failed == 1 { - gettext_f( + if no_failed == 1 { + toast!( + self.session(), + gettext( // Translators: Do NOT translate the content between '{' and '}', this // is a variable name. "Failed to invite {user} to {room}. Try again later.", - &[("user", ""), ("room", "")], - ) - } else { - let n = (no_failed - 1) as u32; - ngettext_f( - // Translators: Do NOT translate the content between '{' and '}', this - // is a variable name. - "Failed to invite {user} and 1 other user to {room}. Try again later.", - "Failed to invite {user} and {n} other users to {room}. Try again later.", - n, - &[("user", ""), ("room", ""), ("n", &n.to_string())], - ) - }; - let user_pill = Pill::for_user(first_failed); - let room_pill = Pill::for_room(self); - let error = Toast::builder() - .title(&error_message) - .widgets(&[&user_pill, &room_pill]) - .build(); - - if let Some(window) = self.session().parent_window() { - window.add_toast(&error); - } + ), + @user = first_failed, + @room = self, + ); + } else { + let n = (no_failed - 1) as u32; + toast!( + self.session(), + ngettext( + // Translators: Do NOT translate the content between '{' and '}', this + // is a variable name. + "Failed to invite {user} and 1 other user to {room}. Try again later.", + "Failed to invite {user} and {n} other users to {room}. Try again later.", + n, + ), + @user = first_failed, + @room = self, + n = n.to_string(), + ); + }; } } else { error!("Can’t invite users, because this room isn’t a joined room"); @@ -1769,6 +1750,11 @@ impl Room { None }) } + + /// Get a `Pill` representing this `Room`. + pub fn to_pill(&self) -> Pill { + Pill::for_room(self) + } } /// Whether the given event can count as an unread message. diff --git a/src/session/room_list.rs b/src/session/room_list.rs index 48f2b98788c0f7a97d29608a37a29dd3bebb83df..8bb57d918333e254dba0c4c0ac9d3365a8b80bbd 100644 --- a/src/session/room_list.rs +++ b/src/session/room_list.rs @@ -9,10 +9,9 @@ use matrix_sdk::{ }; use crate::{ - components::Toast, gettext_f, session::{room::Room, Session}, - spawn, spawn_tokio, + spawn, spawn_tokio, toast, }; mod imp { @@ -321,14 +320,14 @@ impl RoomList { Err(error) => { obj.pending_rooms_remove(&identifier); error!("Joining room {} failed: {}", identifier, error); - let error = Toast::new( + + let error = gettext_f( // Translators: Do NOT translate the content between '{' and '}', this is a variable name. - &gettext_f("Failed to join room {room_name}. Try again later.", &[("room_name", identifier.as_str())]) + "Failed to join room {room_name}. Try again later.", + &[("room_name", identifier.as_str())] ); - if let Some(window) = obj.session().parent_window() { - window.add_toast(&error); - } + toast!(obj.session(), error); } } }) diff --git a/src/session/user.rs b/src/session/user.rs index b94eb6436c14e97402c8f7b624a89735d5781b50..0c62129f8cf693a0e0af2f94a08e60345592de67 100644 --- a/src/session/user.rs +++ b/src/session/user.rs @@ -6,6 +6,7 @@ use matrix_sdk::{ }; use crate::{ + components::Pill, session::{ verification::{IdentityVerification, VerificationState}, Avatar, Session, @@ -265,6 +266,12 @@ pub trait UserExt: IsA { UserActions::NONE } } + + /// Get a `Pill` representing this `User`. + fn to_pill(&self) -> Pill { + let user = self.upcast_ref(); + Pill::for_user(user) + } } impl> UserExt for T {} diff --git a/src/session/verification/identity_verification.rs b/src/session/verification/identity_verification.rs index b0281efc5056646f231a253f8d8d8d817237d003..8ca9ec275e823096faf14e9fc06bb70d45d98aa4 100644 --- a/src/session/verification/identity_verification.rs +++ b/src/session/verification/identity_verification.rs @@ -22,14 +22,13 @@ use tokio::sync::mpsc; use super::{VERIFICATION_CREATION_TIMEOUT, VERIFICATION_RECEIVE_TIMEOUT}; use crate::{ - components::Toast, contrib::Camera, session::{ sidebar::{SidebarItem, SidebarItemImpl}, user::UserExt, Session, User, }, - spawn, spawn_tokio, + spawn, spawn_tokio, toast, }; #[derive(Debug, Eq, PartialEq, Clone, Copy, glib::Enum)] @@ -697,9 +696,7 @@ impl IdentityVerification { gettext("An unknown error occurred during the verification process.") }); - if let Some(window) = self.session().parent_window() { - window.add_toast(&Toast::new(&error_message)); - } + toast!(self.session(), error_message); } pub fn display_name(&self) -> String { diff --git a/src/utils.rs b/src/utils.rs index b5505b5ba00b0d768254ac6681cee80bc025eed3..745aa3ad2813bcb96eeba5f00ddf5a92073293a0 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -26,6 +26,150 @@ macro_rules! spawn_tokio { }; } +/// 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`. +/// +/// ```ignore +/// 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. +/// +/// ```ignore +/// 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 +/// `@`. +/// +/// ```ignore +/// 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::room::Room +/// [`User`]: crate::session::user::User +#[macro_export] +macro_rules! toast { + ($widget:expr, $message:expr) => { + { + let message = $message; + if let Some(root) = $widget.root() { + if let Some(window) = root.downcast_ref::<$crate::Window>() { + window.add_toast(&$crate::components::Toast::new(message.as_ref())); + } else if let Some(window) = root.downcast_ref::() { + use adw::prelude::PreferencesWindowExt; + window.add_toast(&adw::Toast::new(message.as_ref())); + } else { + log::error!("Trying to display a toast when the parent doesn't support it"); + } + } else { + log::warn!("Could not display toast with message: {message}"); + } + } + }; + ($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); + + if let Some(root) = $widget.root() { + if pill_vars.is_empty() { + if let Some(window) = root.downcast_ref::<$crate::Window>() { + window.add_toast(&$crate::components::Toast::new(&message)); + } else if let Some(window) = root.downcast_ref::() { + use adw::prelude::PreferencesWindowExt; + window.add_toast(&adw::Toast::new(&message)); + } else { + log::error!("Trying to display a toast when the parent doesn't support it"); + } + } else if let Some(window) = root.downcast_ref::<$crate::Window>() { + 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::DEFAULT_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 toast = $crate::components::Toast::builder() + .title(swapped_label) + .widgets(&widgets) + .build(); + window.add_toast(&toast); + } else { + log::error!("Trying to display a toast with pills when the parent doesn't support it"); + } + } else { + log::warn!("Could not display toast with message: {message}"); + } + } + }; +} +#[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)*) => { + { + 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)*) => { + { + 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)*]) }; +} + use std::{convert::TryInto, path::PathBuf, str::FromStr}; use gettextrs::gettext; @@ -284,3 +428,17 @@ pub fn validate_password(password: &str) -> PasswordValidity { validity } + +/// Replace variables in the given string with the given dictionary. +/// +/// The expected format to replace is `{name}`, where `name` is the first string +/// in the dictionary entry tuple. +pub fn freplace(s: String, args: &[(&str, &str)]) -> String { + let mut s = s; + + for (k, v) in args { + s = s.replace(&format!("{{{}}}", k), v); + } + + s +}