From 4afba6e7367a24e3cc904f09e91227690ee22635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Sat, 20 May 2023 23:40:36 +0200 Subject: [PATCH] session: Do not use Session to show toasts This usually means we are trying to show a toast from a model Object, which is not right. --- po/POTFILES.in | 3 + src/login/mod.rs | 4 +- .../user_page/deactivate_account_subpage.rs | 8 +- .../user_page/log_out_subpage.rs | 9 +- src/session/content/explore/public_room.rs | 12 +- .../content/explore/public_room_row.rs | 29 ++- src/session/content/invite.rs | 23 ++- .../invite_subpage/invitee_list.rs | 6 +- .../room_details/invite_subpage/mod.rs | 49 ++++- src/session/content/room_history/mod.rs | 39 +++- .../room_history/state_row/tombstone.rs | 19 +- src/session/create_dm_dialog/dm_user.rs | 9 +- src/session/create_dm_dialog/mod.rs | 36 ++-- src/session/join_room_dialog.rs | 24 ++- src/session/mod.rs | 13 +- src/session/room/mod.rs | 178 +++++------------- src/session/room_list.rs | 52 +++-- src/session/sidebar/room_row.rs | 119 ++++++++++-- src/session/sidebar/row.rs | 40 +++- 19 files changed, 426 insertions(+), 246 deletions(-) diff --git a/po/POTFILES.in b/po/POTFILES.in index 6f4c605c..22f844cd 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -79,6 +79,7 @@ src/session/content/room_details/general_page/mod.rs src/session/content/room_details/history_viewer/audio_row.rs src/session/content/room_details/history_viewer/file_row.rs src/session/content/room_details/invite_subpage/invitee_list.rs +src/session/content/room_details/invite_subpage/mod.rs src/session/content/room_details/member_page/mod.rs src/session/content/room_details/mod.rs src/session/content/room_history/item_row.rs @@ -107,6 +108,8 @@ src/session/room_list.rs src/session/sidebar/category/category_row.rs src/session/sidebar/category/category_type.rs src/session/sidebar/entry_type.rs +src/session/sidebar/room_row.rs +src/session/sidebar/row.rs src/session/verification/identity_verification.rs src/user_facing_error.rs src/utils/media.rs diff --git a/src/login/mod.rs b/src/login/mod.rs index cb1af105..82de6aca 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -257,7 +257,9 @@ impl Login { /// Drop the session and clean up its data from the system. fn drop_session(&self) { if let Some(session) = self.imp().session.take() { - glib::MainContext::default().block_on(session.logout()); + glib::MainContext::default().block_on(async move { + let _ = session.logout().await; + }); } } 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 28b26019..74318bb7 100644 --- a/src/session/account_settings/user_page/deactivate_account_subpage.rs +++ b/src/session/account_settings/user_page/deactivate_account_subpage.rs @@ -169,7 +169,13 @@ impl DeactivateAccountSubpage { match result { Ok(_) => { if let Some(session) = self.session() { - toast!(session, gettext("Account successfully deactivated")); + if let Some(window) = self + .root() + .and_downcast_ref::() + .and_then(|w| w.transient_for()) + { + toast!(window, gettext("Account successfully deactivated")); + } session.handle_logged_out(); } self.activate_action("account-settings.close", None) diff --git a/src/session/account_settings/user_page/log_out_subpage.rs b/src/session/account_settings/user_page/log_out_subpage.rs index 67a768d6..1cc61b46 100644 --- a/src/session/account_settings/user_page/log_out_subpage.rs +++ b/src/session/account_settings/user_page/log_out_subpage.rs @@ -4,7 +4,7 @@ use gtk::{ CompositeTemplate, }; -use crate::{components::SpinnerButton, session::Session, spawn}; +use crate::{components::SpinnerButton, session::Session, spawn, toast}; mod imp { use glib::{subclass::InitializingObject, WeakRef}; @@ -100,8 +100,11 @@ impl LogOutSubpage { make_backup_button.set_sensitive(false); spawn!( - clone!(@weak logout_button, @weak make_backup_button, @weak session => async move { - session.logout().await; + clone!(@weak self as obj, @weak logout_button, @weak make_backup_button, @weak session => async move { + if let Err(error) = session.logout().await { + toast!(obj, error); + } + logout_button.set_loading(false); make_backup_button.set_sensitive(true); }) diff --git a/src/session/content/explore/public_room.rs b/src/session/content/explore/public_room.rs index 9e35b81a..6e20e1d0 100644 --- a/src/session/content/explore/public_room.rs +++ b/src/session/content/explore/public_room.rs @@ -1,5 +1,5 @@ use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; -use matrix_sdk::ruma::{directory::PublicRoomsChunk, RoomId, RoomOrAliasId}; +use matrix_sdk::ruma::directory::PublicRoomsChunk; use crate::session::{room::Room, AvatarData, AvatarImage, AvatarUriSource, RoomList}; @@ -181,14 +181,4 @@ impl PublicRoom { pub fn matrix_public_room(&self) -> Option<&PublicRoomsChunk> { self.imp().matrix_public_room.get() } - - pub fn join_or_view(&self) { - if let Some(room) = self.room() { - self.room_list().session().select_room(Some(room.clone())); - } else if let Some(matrix_public_room) = self.matrix_public_room() { - let room_id: &RoomId = matrix_public_room.room_id.as_ref(); - self.room_list() - .join_by_id_or_alias(<&RoomOrAliasId>::from(room_id).to_owned(), vec![]); - } - } } diff --git a/src/session/content/explore/public_room_row.rs b/src/session/content/explore/public_room_row.rs index 8118e63c..08e59278 100644 --- a/src/session/content/explore/public_room_row.rs +++ b/src/session/content/explore/public_room_row.rs @@ -5,6 +5,8 @@ use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate use crate::{ components::{Avatar, Spinner, SpinnerButton}, session::content::explore::PublicRoom, + spawn, toast, + window::Window, }; mod imp { @@ -79,9 +81,7 @@ mod imp { self.parent_constructed(); self.button .connect_clicked(clone!(@weak self as imp => move |_| { - if let Some(public_room) = &*imp.public_room.borrow() { - public_room.join_or_view(); - }; + imp.obj().join_or_view(); })); } @@ -213,4 +213,27 @@ impl PublicRoomRow { button.set_loading(public_room.is_pending()); } + + /// Join or view the public room. + pub fn join_or_view(&self) { + let Some(public_room) = self.public_room() else { + return; + }; + let room_list = public_room.room_list(); + + if let Some(room) = public_room.room() { + if let Some(window) = self.root().and_downcast::() { + let session = room_list.session(); + window.show_room(session.session_id(), room.room_id()); + } + } else if let Some(matrix_public_room) = public_room.matrix_public_room() { + let room_id = matrix_public_room.room_id.clone(); + + spawn!(clone!(@weak self as obj, @weak room_list => async move { + if let Err(error) = room_list.join_by_id_or_alias(room_id.into(), vec![]).await { + toast!(obj, error); + } + })); + } + } } diff --git a/src/session/content/invite.rs b/src/session/content/invite.rs index 6e2f5518..a288e372 100644 --- a/src/session/content/invite.rs +++ b/src/session/content/invite.rs @@ -1,11 +1,12 @@ use adw::subclass::prelude::*; +use gettextrs::gettext; use gtk::{glib, glib::clone, prelude::*, CompositeTemplate}; use crate::{ components::{Avatar, LabelWithWidgets, Pill, SpinnerButton}, gettext_f, session::room::{Room, RoomType}, - spawn, + spawn, toast, }; mod imp { @@ -217,6 +218,16 @@ impl Invite { clone!(@weak self as obj, @strong room => move || async move { let result = room.accept_invite().await; if result.is_err() { + toast!( + obj, + gettext( + // Translators: Do NOT translate the content between '{' and '}', this + // is a variable name. + "Failed to accept invitation for {room}. Try again later.", + ), + @room, + ); + obj.imp().accept_requests.borrow_mut().remove(&room); obj.reset(); } @@ -239,6 +250,16 @@ impl Invite { clone!(@weak self as obj, @strong room => move || async move { let result = room.reject_invite().await; if result.is_err() { + toast!( + obj, + gettext( + // Translators: Do NOT translate the content between '{' and '}', this + // is a variable name. + "Failed to reject invitation for {room}. Try again later.", + ), + @room, + ); + obj.imp().reject_requests.borrow_mut().remove(&room); obj.reset(); } diff --git a/src/session/content/room_details/invite_subpage/invitee_list.rs b/src/session/content/room_details/invite_subpage/invitee_list.rs index a81962ee..325f4063 100644 --- a/src/session/content/room_details/invite_subpage/invitee_list.rs +++ b/src/session/content/room_details/invite_subpage/invitee_list.rs @@ -246,7 +246,7 @@ impl InviteeList { if user.is_invited() && user.invite_exception().is_none() { obj.add_invitee(user.clone()); } else { - obj.remove_invitee(user.user_id()) + obj.remove_invitee(&user.user_id()) } }), ); @@ -365,8 +365,8 @@ impl InviteeList { .collect() } - fn remove_invitee(&self, user_id: OwnedUserId) { - let removed = self.imp().invitee_list.borrow_mut().remove(&user_id); + pub fn remove_invitee(&self, user_id: &UserId) { + let removed = self.imp().invitee_list.borrow_mut().remove(user_id); if let Some(user) = removed { user.set_invited(false); self.emit_by_name::<()>("invitee-removed", &[&user]); diff --git a/src/session/content/room_details/invite_subpage/mod.rs b/src/session/content/room_details/invite_subpage/mod.rs index d5d831bf..57126b41 100644 --- a/src/session/content/room_details/invite_subpage/mod.rs +++ b/src/session/content/room_details/invite_subpage/mod.rs @@ -1,4 +1,5 @@ use adw::subclass::prelude::*; +use gettextrs::ngettext; use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate}; mod invitee; @@ -11,8 +12,8 @@ use self::{ }; use crate::{ components::{Pill, Spinner, SpinnerButton}, - session::{Room, User}, - spawn, + session::{Room, User, UserExt}, + spawn, toast, }; mod imp { @@ -297,9 +298,16 @@ impl InviteSubpage { .ok() } + /// Invite the selected users to the room. fn invite(&self) { self.imp().invite_button.set_loading(true); + spawn!(clone!(@weak self as obj => async move { + obj.invite_inner().await; + })); + } + + async fn invite_inner(&self) { let Some(room) = self.room() else { return; }; @@ -312,11 +320,38 @@ impl InviteSubpage { .into_iter() .map(glib::object::Cast::upcast) .collect(); - spawn!(clone!(@weak self as obj => async move { - room.invite(invitees.as_slice()).await; - obj.close(); - obj.imp().invite_button.set_loading(false); - })); + + match room.invite(&invitees).await { + Ok(()) => { + self.close(); + } + Err(failed_users) => { + for invitee in &invitees { + if !failed_users.contains(&invitee) { + user_list.remove_invitee(&invitee.user_id()) + } + } + + let n = failed_users.len(); + let first_failed = failed_users.first().unwrap(); + + toast!( + self, + ngettext( + // Translators: Do NOT translate the content between '{' and '}', these + // are variable names. + "Failed to invite {user} to {room}. Try again later.", + "Failed to invite {n} users to {room}. Try again later.", + n as u32, + ), + @user = first_failed, + @room, + n = n.to_string(), + ); + } + } + + self.imp().invite_button.set_loading(false); } fn update_view(&self) { diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs index ff2f98de..853a8e63 100644 --- a/src/session/content/room_history/mod.rs +++ b/src/session/content/room_history/mod.rs @@ -178,8 +178,10 @@ mod imp { widget.send_text_message(); }, ); - klass.install_action("room-history.leave", None, move |widget, _, _| { - widget.leave(); + klass.install_action("room-history.leave", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.leave().await; + })); }); klass.install_action("room-history.try-again", None, move |widget, _, _| { @@ -779,9 +781,24 @@ impl RoomHistory { self.clear_related_event(); } - pub fn leave(&self) { - if let Some(room) = &*self.imp().room.borrow() { - room.set_category(RoomType::Left); + /// Leave the room. + pub async fn leave(&self) { + let Some(room) = self.room() else { + return; + }; + let previous_category = room.category(); + + if room.set_category(RoomType::Left).await.is_err() { + toast!( + self, + 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, + previous_category = previous_category.to_string(), + new_category = RoomType::Left.to_string(), + ); } } @@ -1440,10 +1457,16 @@ impl RoomHistory { let Some(successor) = room.successor() else { return; }; + let successor = successor.to_owned(); - room.session() - .room_list() - .join_by_id_or_alias(successor.to_owned().into(), vec![]); + spawn!(clone!(@weak self as obj, @weak room => async move { + if let Err(error) = room.session() + .room_list() + .join_by_id_or_alias(successor.into(), vec![]).await + { + toast!(obj, error); + } + })); } } } diff --git a/src/session/content/room_history/state_row/tombstone.rs b/src/session/content/room_history/state_row/tombstone.rs index 087b46a4..8d7e7a86 100644 --- a/src/session/content/room_history/state_row/tombstone.rs +++ b/src/session/content/room_history/state_row/tombstone.rs @@ -2,7 +2,7 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; use gtk::{glib, glib::clone, CompositeTemplate}; -use crate::{session::Room, utils::BoundObjectWeakRef}; +use crate::{session::Room, spawn, toast, utils::BoundObjectWeakRef}; mod imp { use glib::subclass::InitializingObject; @@ -133,9 +133,20 @@ impl StateTombstone { let Some(successor) = room.successor() else { return; }; + let session = room.session(); + let room_list = session.room_list(); - room.session() - .room_list() - .join_or_view(successor.into(), vec![]); + // Join or view the room with the given identifier. + if let Some(successor_room) = room_list.joined_room(successor.into()) { + session.select_room(Some(successor_room)); + } else { + let successor = successor.to_owned(); + + spawn!(clone!(@weak self as obj, @weak room_list => async move { + if let Err(error) = room_list.join_by_id_or_alias(successor.into(), vec![]).await { + toast!(obj, error); + } + })); + } } } diff --git a/src/session/create_dm_dialog/dm_user.rs b/src/session/create_dm_dialog/dm_user.rs index 23620573..bf2220b6 100644 --- a/src/session/create_dm_dialog/dm_user.rs +++ b/src/session/create_dm_dialog/dm_user.rs @@ -103,7 +103,7 @@ impl DmUser { //// /// If A DM chat exists already no new room is created and the existing one /// is returned. - pub async fn start_chat(&self) -> Result { + pub async fn start_chat(&self) -> Result { let session = self.session(); let client = session.client(); let other_user = self.user_id(); @@ -116,8 +116,11 @@ impl DmUser { // We can be sure that this room has only ourself and maybe the other user as // member. if room.matrix_room().active_members_count() < 2 { - room.invite(&[self.clone().upcast()]).await; debug!("{other_user} left the chat, re-invite them"); + + if room.invite(&[self.clone().upcast()]).await.is_err() { + return Err(()); + } } return Ok(room); @@ -137,7 +140,7 @@ impl DmUser { } Err(error) => { error!("Couldn’t create a new Direct Chat: {error}"); - Err(error) + Err(()) } } } diff --git a/src/session/create_dm_dialog/mod.rs b/src/session/create_dm_dialog/mod.rs index 7981840e..ec3a8699 100644 --- a/src/session/create_dm_dialog/mod.rs +++ b/src/session/create_dm_dialog/mod.rs @@ -2,10 +2,11 @@ use adw::subclass::prelude::*; use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate}; mod dm_user; -use self::dm_user::DmUser; mod dm_user_list; mod dm_user_row; + use self::{ + dm_user::DmUser, dm_user_list::{DmUserList, DmUserListState}, dm_user_row::DmUserRow, }; @@ -177,23 +178,32 @@ impl CreateDmDialog { #[template_callback] fn row_activated_cb(&self, row: gtk::ListBoxRow) { - let Some(user): Option = row.downcast::().ok().and_then(|r| r.user()) else { return; }; + let Some(user) = row.downcast_ref::().and_then(|r| r.user()) else { + return; + }; // TODO: For now we show the loading page while we create the room, // ideally we would like to have the same behavior as Element: // Create the room only once the user sends a message - self.imp().stack.set_visible_child_name("loading-page"); - self.imp().search_entry.set_sensitive(false); + let imp = self.imp(); + imp.stack.set_visible_child_name("loading-page"); + imp.search_entry.set_sensitive(false); + spawn!(clone!(@weak self as obj, @weak user => async move { - match user.start_chat().await { - Ok(room) => { - user.session().select_room(Some(room)); - obj.close(); - } - Err(_) => { - obj.show_error(&gettext("Failed to create a new Direct Chat")); - } - } + obj.start_chat(&user).await; })); } + + async fn start_chat(&self, user: &DmUser) { + match user.start_chat().await { + Ok(room) => { + user.session().select_room(Some(room)); + self.close(); + } + Err(_) => { + self.show_error(&gettext("Failed to create a new Direct Chat")); + self.imp().search_entry.set_sensitive(true); + } + } + } } diff --git a/src/session/join_room_dialog.rs b/src/session/join_room_dialog.rs index 61c5cf16..1d780d8b 100644 --- a/src/session/join_room_dialog.rs +++ b/src/session/join_room_dialog.rs @@ -1,12 +1,12 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; -use gtk::{gdk, glib, CompositeTemplate}; +use gtk::{gdk, glib, glib::clone, CompositeTemplate}; use ruma::{ matrix_uri::MatrixId, MatrixToUri, MatrixUri, OwnedRoomOrAliasId, OwnedServerName, RoomOrAliasId, }; -use crate::session::Session; +use crate::{session::Session, spawn, toast}; mod imp { use glib::{object::WeakRef, subclass::InitializingObject}; @@ -148,15 +148,25 @@ impl JoinRoomDialog { /// Join the room that was entered, if it is valid. fn join_room(&self) { - let Some(session) = self.session() else { - return; - }; - let Some((room_id, via)) = parse_room(&self.imp().entry.text()) else { return; }; - session.room_list().join_or_view((&*room_id).into(), via); + let Some(session) = self.session() else { + return; + }; + let room_list = session.room_list(); + + // Join or view the room with the given identifier. + if let Some(room) = room_list.joined_room((&*room_id).into()) { + session.select_room(Some(room)); + } else { + spawn!(clone!(@weak self as obj, @weak room_list => async move { + if let Err(error) = room_list.join_by_id_or_alias(room_id, via).await { + toast!(obj, error); + } + })); + } } } diff --git a/src/session/mod.rs b/src/session/mod.rs index 6ba7df62..bfd4f01e 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -64,7 +64,7 @@ pub use self::{ use crate::{ secret::StoredSession, session::sidebar::ItemList, - spawn, spawn_tokio, toast, + spawn, spawn_tokio, utils::{ check_if_reachable, matrix::{self, ClientSetupError}, @@ -681,7 +681,7 @@ impl Session { dialog.present(); } - pub async fn logout(&self) { + pub async fn logout(&self) -> Result<(), String> { let stack = &self.imp().stack; debug!("The session is about to be logged out"); @@ -698,10 +698,15 @@ impl Session { }); match handle.await.unwrap() { - Ok(_) => self.cleanup_session().await, + Ok(_) => { + self.cleanup_session().await; + + Ok(()) + } Err(error) => { error!("Couldn’t logout the session: {error}"); - toast!(self, gettext("Failed to logout the session.")); + + Err(gettext("Failed to logout the session.")) } } } diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs index 2bb69ead..0afe45f8 100644 --- a/src/session/room/mod.rs +++ b/src/session/room/mod.rs @@ -10,7 +10,7 @@ mod typing_list; use std::{cell::RefCell, io::Cursor}; -use gettextrs::{gettext, ngettext}; +use gettextrs::gettext; use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*}; use log::{debug, error, warn}; use matrix_sdk::{ @@ -58,7 +58,7 @@ use crate::{ sidebar::{SidebarItem, SidebarItemImpl}, AvatarData, AvatarImage, AvatarUriSource, Session, User, }, - spawn, spawn_tokio, toast, + spawn, spawn_tokio, }; mod imp { @@ -143,7 +143,7 @@ mod imp { .read_only() .build(), glib::ParamSpecEnum::builder::("category") - .explicit_notify() + .read_only() .build(), glib::ParamSpecString::builder("topic").read_only().build(), glib::ParamSpecUInt64::builder("latest-unread") @@ -184,7 +184,6 @@ mod imp { match pspec.name() { "session" => self.session.set(value.get().ok().as_ref()), - "category" => obj.set_category(value.get().unwrap()), "room-id" => self .room_id .set(RoomId::parse(value.get::<&str>().unwrap()).unwrap()) @@ -299,18 +298,13 @@ impl Room { self.imp().room_id.get().unwrap() } - /// Set the proper category for this joined room. - pub fn set_joined(&self) { + /// Whether this room is direct or not. + pub async fn is_direct(&self) -> bool { let matrix_room = self.matrix_room(); - let handle = spawn_tokio!(async move { matrix_room.is_direct().await.unwrap_or_default() }); - spawn!(clone!(@weak self as obj => async move { - if handle.await.unwrap() { - obj.set_category(RoomType::Direct); - } else { - obj.set_category(RoomType::Normal); - } - })); + spawn_tokio!(async move { matrix_room.is_direct().await.unwrap_or_default() }) + .await + .unwrap() } pub fn matrix_room(&self) -> MatrixRoom { @@ -346,10 +340,10 @@ impl Room { } /// Forget a room that is left. - pub fn forget(&self) { + pub async fn forget(&self) -> MatrixResult<()> { if self.category() != RoomType::Left { warn!("Cannot forget a room that is not left"); - return; + return Ok(()); } let matrix_room = self.matrix_room(); @@ -361,29 +355,20 @@ impl Room { } }); - spawn!( - glib::PRIORITY_DEFAULT_IDLE, - clone!(@weak self as obj => async move { - match handle.await.unwrap() { - Ok(_) => { - obj.emit_by_name::<()>("room-forgotten", &[]); - } - Err(error) => { - error!("Couldn’t forget the room: {error}"); + match handle.await.unwrap() { + Ok(_) => { + self.emit_by_name::<()>("room-forgotten", &[]); + Ok(()) + } + Err(error) => { + error!("Couldn’t forget the room: {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 + self.load_category(); - // Load the previous category - obj.load_category(); - }, - }; - }) - ); + Err(error) + } + } } pub fn is_joined(&self) -> bool { @@ -417,32 +402,34 @@ impl Room { /// /// Note: Rooms can't be moved to the invite category and they can't be /// moved once they are upgraded. - pub fn set_category(&self, category: RoomType) { + pub async fn set_category(&self, category: RoomType) -> MatrixResult<()> { let matrix_room = self.matrix_room(); let previous_category = self.category(); if previous_category == category { - return; + return Ok(()); } if previous_category == RoomType::Outdated { warn!("Can't set the category of an upgraded room"); - return; + return Ok(()); } match category { RoomType::Invited => { warn!("Rooms can’t be moved to the invite Category"); - return; + return Ok(()); } RoomType::Outdated => { // Outdated rooms don't need to propagate anything to the server self.set_category_internal(category); - return; + return Ok(()); } _ => {} } + self.set_category_internal(category); + let handle = spawn_tokio!(async move { match matrix_room { MatrixRoom::Invited(room) => match category { @@ -615,33 +602,17 @@ impl Room { Result::<_, matrix_sdk::Error>::Ok(()) }); - spawn!( - glib::PRIORITY_DEFAULT_IDLE, - clone!(@weak self as obj => async move { - match handle.await.unwrap() { - Ok(_) => {}, - Err(error) => { - error!("Couldn’t set the room category: {error}"); + match handle.await.unwrap() { + Ok(_) => Ok(()), + Err(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 + self.load_category(); - // Load the previous category - obj.load_category(); - }, - }; - }) - ); - - self.set_category_internal(category); + Err(error) + } + } } pub fn load_category(&self) { @@ -1303,17 +1274,6 @@ impl Room { Ok(_) => Ok(()), Err(error) => { error!("Accepting invitation failed: {error}"); - - 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 = self, - ); - Err(error) } } @@ -1333,16 +1293,6 @@ impl Room { Err(error) => { error!("Rejecting invitation failed: {error}"); - 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 = self, - ); - Err(error) } } @@ -1525,21 +1475,25 @@ impl Room { }); } - pub async fn invite(&self, users: &[User]) { + /// Invite the given users to this room. + /// + /// Returns `Ok(())` if all the invites are sent successfully, otherwise + /// returns the list of users who could not be invited. + pub async fn invite<'a>(&self, users: &'a [User]) -> Result<(), Vec<&'a User>> { let MatrixRoom::Joined(matrix_room) = self.matrix_room() else { error!("Can’t invite users, because this room isn’t a joined room"); - return; + return Ok(()); }; let user_ids: Vec = users.iter().map(|user| user.user_id()).collect(); let handle = spawn_tokio!(async move { - let invitiations = user_ids + let invitations = user_ids .iter() .map(|user_id| matrix_room.invite_user_by_id(user_id)); - futures::future::join_all(invitiations).await + futures::future::join_all(invitations).await }); - let mut failed_invites: Vec = Vec::new(); + let mut failed_invites = Vec::new(); for (index, result) in handle.await.unwrap().iter().enumerate() { match result { Ok(_) => {} @@ -1548,43 +1502,15 @@ impl Room { "Failed to invite user with id {}: {error}", users[index].user_id(), ); - failed_invites.push(users[index].clone()); + failed_invites.push(&users[index]); } } } - if !failed_invites.is_empty() { - let no_failed = failed_invites.len(); - let first_failed = failed_invites.first().unwrap(); - - // TODO: should we show all the failed users? - 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 = 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(), - ); - }; + if failed_invites.is_empty() { + Ok(()) + } else { + Err(failed_invites) } } diff --git a/src/session/room_list.rs b/src/session/room_list.rs index 846e0d7a..ea4d072c 100644 --- a/src/session/room_list.rs +++ b/src/session/room_list.rs @@ -14,7 +14,7 @@ use matrix_sdk::{ use crate::{ gettext_f, session::{room::Room, Session}, - spawn, spawn_tokio, toast, + spawn_tokio, }; mod imp { @@ -314,7 +314,11 @@ impl RoomList { } /// Join the room with the given identifier. - pub fn join_by_id_or_alias(&self, identifier: OwnedRoomOrAliasId, via: Vec) { + pub async fn join_by_id_or_alias( + &self, + identifier: OwnedRoomOrAliasId, + via: Vec, + ) -> Result<(), String> { let client = self.session().client(); let identifier_clone = identifier.clone(); @@ -326,26 +330,25 @@ impl RoomList { .await }); - spawn!( - glib::PRIORITY_DEFAULT_IDLE, - clone!(@weak self as obj => async move { - match handle.await.unwrap() { - Ok(matrix_room) => obj.pending_rooms_replace_or_remove(&identifier, matrix_room.room_id()), - Err(error) => { - obj.pending_rooms_remove(&identifier); - error!("Joining room {identifier} failed: {error}"); + match handle.await.unwrap() { + Ok(matrix_room) => { + self.pending_rooms_replace_or_remove(&identifier, matrix_room.room_id()); + Ok(()) + } + Err(error) => { + self.pending_rooms_remove(&identifier); + error!("Joining room {identifier} failed: {error}"); - let error = gettext_f( - // Translators: Do NOT translate the content between '{' and '}', this is a variable name. - "Failed to join room {room_name}. Try again later.", - &[("room_name", identifier.as_str())] - ); + let error = gettext_f( + // Translators: Do NOT translate the content between '{' and '}', this is a + // variable name. + "Failed to join room {room_name}. Try again later.", + &[("room_name", identifier.as_str())], + ); - toast!(obj.session(), error); - } - } - }) - ); + Err(error) + } + } } pub fn connect_pending_rooms_changed( @@ -367,15 +370,6 @@ impl RoomList { .filter(|room| room.is_joined()) } - /// Join or view the room with the given identifier. - pub fn join_or_view(&self, identifier: RoomIdentifier, via: Vec) { - if let Some(room) = self.joined_room(identifier) { - self.session().select_room(Some(room)); - } else { - self.join_by_id_or_alias(identifier.into(), via); - } - } - /// Add a room that was tombstoned but for which we haven't joined the /// successor yet. pub fn add_tombstoned_room(&self, room_id: OwnedRoomId) { diff --git a/src/session/sidebar/room_row.rs b/src/session/sidebar/room_row.rs index 9daf9191..1951776d 100644 --- a/src/session/sidebar/room_row.rs +++ b/src/session/sidebar/room_row.rs @@ -1,10 +1,12 @@ use adw::subclass::prelude::BinImpl; +use gettextrs::gettext; use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate}; use super::Row; use crate::{ components::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl}, session::room::{HighlightFlags, Room, RoomType}, + spawn, toast, }; mod imp { @@ -39,34 +41,52 @@ mod imp { klass.set_accessible_role(gtk::AccessibleRole::Group); - klass.install_action("room-row.accept-invite", None, move |widget, _, _| { - widget.set_room_as_normal_or_direct(); + klass.install_action("room-row.accept-invite", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.set_room_as_normal_or_direct().await; + })); }); - klass.install_action("room-row.reject-invite", None, move |widget, _, _| { - widget.room().unwrap().set_category(RoomType::Left) + klass.install_action("room-row.reject-invite", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.set_category(RoomType::Left).await + })); }); - klass.install_action("room-row.set-favorite", None, move |widget, _, _| { - widget.room().unwrap().set_category(RoomType::Favorite); + klass.install_action("room-row.set-favorite", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.set_category(RoomType::Favorite).await + })); }); - klass.install_action("room-row.set-normal", None, move |widget, _, _| { - widget.room().unwrap().set_category(RoomType::Normal); + klass.install_action("room-row.set-normal", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.set_category(RoomType::Normal).await + })); }); - klass.install_action("room-row.set-lowpriority", None, move |widget, _, _| { - widget.room().unwrap().set_category(RoomType::LowPriority); + klass.install_action("room-row.set-lowpriority", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.set_category(RoomType::LowPriority).await + })); }); - klass.install_action("room-row.set-direct", None, move |widget, _, _| { - widget.room().unwrap().set_category(RoomType::Direct); + klass.install_action("room-row.set-direct", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.set_category(RoomType::Direct).await + })); }); - klass.install_action("room-row.leave", None, move |widget, _, _| { - widget.room().unwrap().set_category(RoomType::Left); + klass.install_action("room-row.leave", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.set_category(RoomType::Left).await + })); }); - klass.install_action("room-row.join", None, move |widget, _, _| { - widget.set_room_as_normal_or_direct(); + klass.install_action("room-row.join", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.set_room_as_normal_or_direct().await; + })); }); - klass.install_action("room-row.forget", None, move |widget, _, _| { - widget.room().unwrap().forget(); + klass.install_action("room-row.forget", None, move |obj, _, _| { + spawn!(clone!(@weak obj => async move { + obj.forget().await + })); }); } @@ -350,7 +370,66 @@ impl RoomRow { row.remove_css_class("drag"); } - fn set_room_as_normal_or_direct(&self) { - self.room().unwrap().set_joined(); + async fn set_room_as_normal_or_direct(&self) { + let Some(room) = self.room() else { + return; + }; + let previous_category = room.category(); + + let category = if room.is_direct().await { + RoomType::Direct + } else { + RoomType::Normal + }; + + if room.set_category(category).await.is_err() { + toast!( + self, + 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, + previous_category = previous_category.to_string(), + new_category = category.to_string(), + ); + } + } + + /// Change the category of this room. + async fn set_category(&self, category: RoomType) { + let Some(room) = self.room() else { + return; + }; + let previous_category = room.category(); + + if room.set_category(category).await.is_err() { + toast!( + self, + 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, + previous_category = previous_category.to_string(), + new_category = category.to_string(), + ); + } + } + + /// Forget this room. + async fn forget(&self) { + let Some(room) = self.room() else { + return; + }; + + if room.forget().await.is_err() { + toast!( + self, + // Translators: Do NOT translate the content between '{' and '}', this is a variable name. + gettext("Failed to forget {room}."), + @room, + ); + } } } diff --git a/src/session/sidebar/row.rs b/src/session/sidebar/row.rs index 2dc0d9db..8ff38f62 100644 --- a/src/session/sidebar/row.rs +++ b/src/session/sidebar/row.rs @@ -1,4 +1,5 @@ use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::gettext; use gtk::{gdk, glib, glib::clone}; use super::{CategoryType, EntryType}; @@ -10,6 +11,7 @@ use crate::{ }, verification::IdentityVerification, }, + spawn, toast, utils::BoundObjectWeakRef, }; @@ -312,12 +314,16 @@ impl Row { if let Ok(room) = value.get::() { if let Some(target_type) = self.room_type() { if room.category().can_change_to(target_type) { - room.set_category(target_type); + spawn!(clone!(@weak self as obj, @weak room => async move { + obj.set_room_category(&room, target_type).await; + })); ret = true; } } else if let Some(entry_type) = self.entry_type() { if room.category() == RoomType::Left && entry_type == EntryType::Forget { - room.forget(); + spawn!(clone!(@weak self as obj, @weak room => async move { + obj.forget_room(&room).await; + })); ret = true; } } @@ -326,6 +332,36 @@ impl Row { ret } + /// Change the category of the given room room. + async fn set_room_category(&self, room: &Room, category: RoomType) { + let previous_category = room.category(); + + if room.set_category(category).await.is_err() { + toast!( + self, + 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, + previous_category = previous_category.to_string(), + new_category = category.to_string(), + ); + } + } + + /// Forget the given room. + async fn forget_room(&self, room: &Room) { + if room.forget().await.is_err() { + toast!( + self, + // Translators: Do NOT translate the content between '{' and '}', this is a variable name. + gettext("Failed to forget {room}."), + @room, + ); + } + } + /// Update the disabled or empty state of this drop target. fn update_for_drop_source_type(&self) { let source_type = self.sidebar().drop_source_type();