From c6c3c73c3b33f20d3e4b121c9d2e3161adeb2fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Thu, 7 Dec 2023 12:05:29 +0100 Subject: [PATCH] members-list: Open user details page on click Replaces the toggle button with the menu. --- po/POTFILES.in | 3 +- src/session/model/mod.rs | 2 +- src/session/model/sidebar/selection.rs | 7 +- src/session/view/content/invite.rs | 2 +- .../room_details/members_page/member_menu.rs | 186 ----------------- .../room_details/members_page/member_menu.ui | 34 ---- .../members_list_view/member_row.rs | 34 +--- .../members_list_view/member_row.ui | 17 +- .../membership_subpage_row.rs | 25 +-- .../members_page/members_list_view/mod.rs | 27 ++- .../content/room_details/members_page/mod.rs | 90 ++++----- .../room_history/verification_info_bar.rs | 2 +- src/session/view/mod.rs | 3 +- src/session/view/session_view.rs | 22 +- src/session/view/user_page.rs | 188 ++++++++++++++++++ src/session/view/user_page.ui | 135 +++++++++++++ src/ui-resources.gresource.xml | 2 +- src/window.rs | 14 +- 18 files changed, 432 insertions(+), 361 deletions(-) delete mode 100644 src/session/view/content/room_details/members_page/member_menu.rs delete mode 100644 src/session/view/content/room_details/members_page/member_menu.ui create mode 100644 src/session/view/user_page.rs create mode 100644 src/session/view/user_page.ui diff --git a/po/POTFILES.in b/po/POTFILES.in index 80845eb6..8b8819a6 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -75,7 +75,6 @@ src/session/view/content/room_details/members_page/members_list_view/member_row. src/session/view/content/room_details/members_page/members_list_view/membership_subpage_row.rs src/session/view/content/room_details/members_page/members_list_view/mod.rs src/session/view/content/room_details/members_page/members_list_view/mod.ui -src/session/view/content/room_details/members_page/member_menu.ui src/session/view/content/room_details/mod.ui src/session/view/content/room_history/event_actions.ui src/session/view/content/room_history/item_row.rs @@ -117,6 +116,8 @@ src/session/view/sidebar/category_row.rs src/session/view/sidebar/mod.ui src/session/view/sidebar/room_row.rs src/session/view/sidebar/row.rs +src/session/view/user_page.rs +src/session/view/user_page.ui src/session_list/mod.rs src/shortcuts.ui src/user_facing_error.rs diff --git a/src/session/model/mod.rs b/src/session/model/mod.rs index 35e42a8a..af8d0037 100644 --- a/src/session/model/mod.rs +++ b/src/session/model/mod.rs @@ -24,7 +24,7 @@ pub use self::{ Category, CategoryType, IconItem, ItemList, ItemType, Selection, SidebarItem, SidebarItemImpl, SidebarListModel, }, - user::{User, UserActions, UserExt}, + user::{User, UserExt}, verification::{ IdentityVerification, SasData, VerificationList, VerificationMode, VerificationState, VerificationSupportedMethods, diff --git a/src/session/model/sidebar/selection.rs b/src/session/model/sidebar/selection.rs index 310ec9e7..ad047a3f 100644 --- a/src/session/model/sidebar/selection.rs +++ b/src/session/model/sidebar/selection.rs @@ -58,7 +58,9 @@ mod imp { obj.set_model(model.as_ref()); } "selected" => obj.set_selected(value.get().unwrap()), - "selected-item" => obj.set_selected_item(value.get().unwrap()), + "selected-item" => { + obj.set_selected_item(value.get::>().unwrap()) + } _ => unimplemented!(), } } @@ -228,8 +230,9 @@ impl Selection { } /// Set the selected item. - pub fn set_selected_item(&self, item: Option) { + pub fn set_selected_item(&self, item: Option>) { let imp = self.imp(); + let item = item.and_upcast(); let selected_item = self.selected_item(); if selected_item == item { diff --git a/src/session/view/content/invite.rs b/src/session/view/content/invite.rs index 35bb44e0..89f7c91b 100644 --- a/src/session/view/content/invite.rs +++ b/src/session/view/content/invite.rs @@ -165,7 +165,7 @@ impl Invite { let selection = session.sidebar_list_model().selection_model(); if let Some(selected_room) = selection.selected_item().and_downcast::() { if selected_room == *room { - selection.set_selected_item(None); + selection.set_selected_item(Option::::None); } } } diff --git a/src/session/view/content/room_details/members_page/member_menu.rs b/src/session/view/content/room_details/members_page/member_menu.rs deleted file mode 100644 index 02486bfe..00000000 --- a/src/session/view/content/room_details/members_page/member_menu.rs +++ /dev/null @@ -1,186 +0,0 @@ -use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; - -use crate::{ - prelude::*, - session::model::{Member, UserActions}, -}; - -mod imp { - use std::cell::RefCell; - - use once_cell::{sync::Lazy, unsync::OnceCell}; - - use super::*; - - #[derive(Debug, Default)] - pub struct MemberMenu { - pub member: RefCell>, - pub popover: OnceCell, - pub destroy_handler: RefCell>, - pub actions_handler: RefCell>, - } - - #[glib::object_subclass] - impl ObjectSubclass for MemberMenu { - const NAME: &'static str = "ContentMemberMenu"; - type Type = super::MemberMenu; - } - - impl ObjectImpl for MemberMenu { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("member") - .explicit_notify() - .build(), - glib::ParamSpecFlags::builder::("allowed-actions") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "member" => self.obj().set_member(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "member" => obj.member().to_value(), - "allowed-actions" => obj.allowed_actions().to_value(), - _ => unimplemented!(), - } - } - - fn constructed(&self) { - self.parent_constructed(); - let obj = self.obj(); - - obj.popover_menu() - .connect_closed(clone!(@weak obj => move |_| { - obj.close_popover(); - })); - } - } -} - -glib::wrapper! { - pub struct MemberMenu(ObjectSubclass); -} - -impl MemberMenu { - pub fn new() -> Self { - glib::Object::new() - } - - /// The member to apply actions to. - pub fn member(&self) -> Option { - self.imp().member.borrow().clone() - } - - /// Set the member to apply actions to. - pub fn set_member(&self, member: Option) { - let imp = self.imp(); - let prev_member = self.member(); - - if prev_member == member { - return; - } - - if let Some(member) = prev_member { - if let Some(handler) = imp.actions_handler.take() { - member.disconnect(handler); - } - } - - if let Some(ref member) = member { - let handler = member.connect_notify_local( - Some("allowed-actions"), - clone!(@weak self as obj => move |_, _| { - obj.notify("allowed-actions"); - }), - ); - - imp.actions_handler.replace(Some(handler)); - } - - imp.member.replace(member); - self.notify("member"); - self.notify("allowed-actions"); - } - - /// The actions the logged-in user is allowed to perform on the member. - pub fn allowed_actions(&self) -> UserActions { - self.member() - .map(|member| member.allowed_actions()) - .unwrap_or_default() - } - - fn popover_menu(&self) -> >k::PopoverMenu { - self.imp().popover.get_or_init(|| { - gtk::PopoverMenu::from_model(Some( - >k::Builder::from_resource("/org/gnome/Fractal/ui/session/view/content/room_details/members_page/member_menu.ui") - .object::("menu_model") - .unwrap(), - )) - }) - } - - /// Show the menu on the specific button - /// - /// For convenience it allows to set the member for which the popover is - /// shown - pub fn present_popover(&self, button: >k::ToggleButton, member: Option) { - let popover = self.popover_menu(); - let _guard = popover.freeze_notify(); - - self.close_popover(); - self.unparent_popover(); - - self.set_member(member); - - let handler = button.connect_destroy(clone!(@weak self as obj => move |_| { - obj.unparent_popover(); - })); - - self.imp().destroy_handler.replace(Some(handler)); - - popover.set_parent(button); - popover.popup(); - } - - fn unparent_popover(&self) { - let popover = self.popover_menu(); - - if let Some(parent) = popover.parent() { - if let Some(handler) = self.imp().destroy_handler.take() { - parent.disconnect(handler); - } - - popover.unparent(); - } - } - - /// Closes the popover - pub fn close_popover(&self) { - let popover = self.popover_menu(); - let _guard = popover.freeze_notify(); - - if let Some(button) = popover.parent() { - if popover.is_visible() { - popover.popdown(); - } - button - .downcast::() - .expect("The parent of a MemberMenu needs to be a gtk::ToggleButton") - .set_active(false); - } - } -} diff --git a/src/session/view/content/room_details/members_page/member_menu.ui b/src/session/view/content/room_details/members_page/member_menu.ui deleted file mode 100644 index e64e1511..00000000 --- a/src/session/view/content/room_details/members_page/member_menu.ui +++ /dev/null @@ -1,34 +0,0 @@ - - - -
- - _Verify - member.verify - action-disabled - action-missing - -
-
- - Make _Mod - member.make-mod - action-disabled - action-missing - - - Make _Admin - member.make-admin - action-disabled - action-missing - - - _Kick - member.kick - action-disabled - action-missing - -
-
-
- diff --git a/src/session/view/content/room_details/members_page/members_list_view/member_row.rs b/src/session/view/content/room_details/members_page/members_list_view/member_row.rs index 18cdd789..d9905bed 100644 --- a/src/session/view/content/room_details/members_page/members_list_view/member_row.rs +++ b/src/session/view/content/room_details/members_page/members_list_view/member_row.rs @@ -1,6 +1,5 @@ -use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate}; +use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate}; -use super::super::{MemberMenu, MembersPage}; use crate::{ components::{Avatar, Badge}, session::model::Member, @@ -20,8 +19,6 @@ mod imp { )] pub struct MemberRow { pub member: RefCell>, - #[template_child] - pub menu_btn: TemplateChild, } #[glib::object_subclass] @@ -68,21 +65,8 @@ mod imp { _ => unimplemented!(), } } - - fn constructed(&self) { - self.parent_constructed(); - let obj = self.obj(); - - self.menu_btn - .connect_toggled(clone!(@weak obj => move |btn| { - if btn.is_active() { - if let Some(menu) = obj.member_menu() { - menu.present_popover(btn, obj.member()); - } - } - })); - } } + impl WidgetImpl for MemberRow {} impl BoxImpl for MemberRow {} } @@ -110,21 +94,7 @@ impl MemberRow { return; } - // We need to update the member of the menu if it's shown for this row - if imp.menu_btn.is_active() { - if let Some(menu) = self.member_menu() { - menu.set_member(member.clone()); - } - } - imp.member.replace(member); self.notify("member"); } - - fn member_menu(&self) -> Option { - let member_page = self - .ancestor(MembersPage::static_type()) - .and_downcast::()?; - Some(member_page.member_menu().clone()) - } } diff --git a/src/session/view/content/room_details/members_page/members_list_view/member_row.ui b/src/session/view/content/room_details/members_page/members_list_view/member_row.ui index 5b51910f..07ceb76b 100644 --- a/src/session/view/content/room_details/members_page/members_list_view/member_row.ui +++ b/src/session/view/content/room_details/members_page/members_list_view/member_row.ui @@ -42,12 +42,7 @@ verified-symbolic - - Verified - - - Verified - + Identity verified ContentMemberRow @@ -83,15 +78,5 @@ - - - False - menu-secondary-symbolic - Member Menu - - Member Menu - - - diff --git a/src/session/view/content/room_details/members_page/members_list_view/membership_subpage_row.rs b/src/session/view/content/room_details/members_page/members_list_view/membership_subpage_row.rs index bd9c3eed..50122f06 100644 --- a/src/session/view/content/room_details/members_page/members_list_view/membership_subpage_row.rs +++ b/src/session/view/content/room_details/members_page/members_list_view/membership_subpage_row.rs @@ -1,6 +1,6 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; -use gtk::{gdk, glib, glib::clone, CompositeTemplate}; +use gtk::{glib, glib::clone, CompositeTemplate}; use super::MembershipSubpageItem; use crate::session::model::Membership; @@ -71,29 +71,6 @@ mod imp { _ => unimplemented!(), } } - - fn constructed(&self) { - self.parent_constructed(); - let obj = self.obj(); - - self.gesture.set_touch_only(false); - self.gesture.set_button(gdk::BUTTON_PRIMARY); - - self.gesture - .connect_released(clone!(@weak obj => move |_, _, _, _| { - if let Some(item) = obj.item() { - obj.activate_action( - "members.subpage", - Some(&item.state().to_variant()), - ) - .unwrap(); - } - })); - - self.gesture - .set_propagation_phase(gtk::PropagationPhase::Capture); - obj.add_controller(self.gesture.clone()); - } } impl WidgetImpl for MembershipSubpageRow {} diff --git a/src/session/view/content/room_details/members_page/members_list_view/mod.rs b/src/session/view/content/room_details/members_page/members_list_view/mod.rs index e0f34d7f..5b21d130 100644 --- a/src/session/view/content/room_details/members_page/members_list_view/mod.rs +++ b/src/session/view/content/room_details/members_page/members_list_view/mod.rs @@ -1,6 +1,10 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; -use gtk::{gio, glib, glib::closure, CompositeTemplate}; +use gtk::{ + gio, glib, + glib::{clone, closure}, + CompositeTemplate, +}; mod extra_lists; mod item_row; @@ -93,6 +97,7 @@ mod imp { fn constructed(&self) { self.parent_constructed(); + let obj = self.obj(); // Needed because the GtkSearchEntry is not the direct child of the // GtkSearchBear. @@ -130,6 +135,26 @@ mod imp { self.list_view.set_model(Some(>k::NoSelection::new(Some( self.filtered_model.clone(), )))); + self.list_view + .connect_activate(clone!(@weak obj => move |_, pos| { + let Some(item) = obj.imp().filtered_model.item(pos) else { + return; + }; + + if let Some(member) = item.downcast_ref::() { + obj.activate_action( + "members.show-member", + Some(&member.user_id().as_str().to_variant()), + ) + .unwrap(); + } else if let Some(item) = item.downcast_ref::() { + obj.activate_action( + "members.show-membership-list", + Some(&item.state().to_variant()), + ) + .unwrap(); + } + })); } } diff --git a/src/session/view/content/room_details/members_page/mod.rs b/src/session/view/content/room_details/members_page/mod.rs index 56b24d3e..c9a3d109 100644 --- a/src/session/view/content/room_details/members_page/mod.rs +++ b/src/session/view/content/room_details/members_page/mod.rs @@ -4,25 +4,22 @@ use gtk::{ glib::{self, clone, closure}, CompositeTemplate, }; -use tracing::warn; -mod member_menu; mod members_list_view; use members_list_view::{ExtraLists, MembersListView, MembershipSubpageItem}; -use ruma::events::room::power_levels::PowerLevelAction; +use ruma::{events::room::power_levels::PowerLevelAction, UserId}; -use self::member_menu::MemberMenu; -use crate::{ - session::model::{Member, Membership, Room, User, UserActions}, - spawn, +use crate::session::{ + model::{Member, Membership, Room}, + view::UserPage, }; mod imp { use std::cell::RefCell; use glib::subclass::InitializingObject; - use once_cell::{sync::Lazy, unsync::OnceCell}; + use once_cell::sync::Lazy; use super::*; @@ -34,7 +31,6 @@ mod imp { pub room: glib::WeakRef, #[template_child] pub navigation_view: TemplateChild, - pub member_menu: OnceCell, pub can_invite_watch: RefCell>, } @@ -47,27 +43,41 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); - klass.install_action("member.verify", None, move |widget, _, _| { - if let Some(member) = widget.member_menu().member() { - widget.verify_member(member); - } else { - warn!("No member was selected to be verified"); - } - }); + klass.install_action( + "members.show-membership-list", + Some("u"), + move |widget, _, param| { + let Some(membership) = param.and_then(|variant| variant.get::()) + else { + return; + }; - klass.install_action("members.subpage", Some("u"), move |widget, _, param| { - let Some(membership) = param.and_then(|variant| variant.get::()) else { + let subpage = match membership { + Membership::Join => "joined", + Membership::Invite => "invited", + Membership::Ban => "banned", + _ => return, + }; + + widget.imp().navigation_view.push_by_tag(subpage); + }, + ); + + klass.install_action("members.show-member", Some("s"), move |widget, _, param| { + let Some(user_id) = param + .and_then(|variant| variant.get::()) + .and_then(|s| UserId::parse(s).ok()) + else { + return; + }; + let Some(room) = widget.room() else { return; }; - let subpage = match membership { - Membership::Join => "joined", - Membership::Invite => "invited", - Membership::Ban => "banned", - _ => return, - }; + let member = room.get_or_create_members().get_or_create(user_id); + let user_page = UserPage::new(&member); - widget.imp().navigation_view.push_by_tag(subpage); + widget.imp().navigation_view.push(&user_page); }); } @@ -207,36 +217,6 @@ impl MembersPage { imp.navigation_view.add(&banned_view); } - /// The object holding information needed for the menu of each `MemberRow`. - pub fn member_menu(&self) -> &MemberMenu { - self.imp().member_menu.get_or_init(|| { - let menu = MemberMenu::new(); - - menu.connect_notify_local( - Some("allowed-actions"), - clone!(@weak self as obj => move |menu, _| { - obj.update_actions(menu.allowed_actions()); - }), - ); - self.update_actions(menu.allowed_actions()); - menu - }) - } - - fn update_actions(&self, allowed_actions: UserActions) { - self.action_set_enabled( - "member.verify", - allowed_actions.contains(UserActions::VERIFY), - ); - } - - fn verify_member(&self, member: Member) { - // TODO: show the verification immediately when started - spawn!(clone!(@weak self as obj => async move { - member.upcast::().verify_identity().await; - })); - } - fn build_filtered_list( &self, model: impl IsA, diff --git a/src/session/view/content/room_history/verification_info_bar.rs b/src/session/view/content/room_history/verification_info_bar.rs index daec9094..a0a8241e 100644 --- a/src/session/view/content/room_history/verification_info_bar.rs +++ b/src/session/view/content/room_history/verification_info_bar.rs @@ -53,7 +53,7 @@ mod imp { let request = obj.request().unwrap(); request.accept(); - window.session_view().select_item(Some(request.upcast())); + window.session_view().select_item(Some(request)); }); klass.install_action("verification.decline", None, move |widget, _, _| { diff --git a/src/session/view/mod.rs b/src/session/view/mod.rs index e02e4e9d..fada78ab 100644 --- a/src/session/view/mod.rs +++ b/src/session/view/mod.rs @@ -7,6 +7,7 @@ mod media_viewer; mod room_creation; mod session_view; mod sidebar; +mod user_page; pub use self::{ account_settings::AccountSettings, content::verification::SessionVerification, @@ -15,5 +16,5 @@ pub use self::{ use self::{ content::Content, create_dm_dialog::CreateDmDialog, event_source_dialog::EventSourceDialog, join_room_dialog::JoinRoomDialog, media_viewer::MediaViewer, room_creation::RoomCreation, - sidebar::Sidebar, + sidebar::Sidebar, user_page::UserPage, }; diff --git a/src/session/view/session_view.rs b/src/session/view/session_view.rs index 69ac66ca..2eb90a40 100644 --- a/src/session/view/session_view.rs +++ b/src/session/view/session_view.rs @@ -4,7 +4,7 @@ use gtk::{ glib::{clone, signal::SignalHandlerId}, CompositeTemplate, }; -use ruma::RoomId; +use ruma::{RoomId, UserId}; use tracing::{error, warn}; use super::{Content, CreateDmDialog, JoinRoomDialog, MediaViewer, RoomCreation, Sidebar}; @@ -241,10 +241,10 @@ impl SessionView { } pub fn select_room(&self, room: Option) { - self.select_item(room.map(|item| item.upcast())); + self.select_item(room); } - pub fn select_item(&self, item: Option) { + pub fn select_item(&self, item: Option>) { let Some(session) = self.session() else { return; }; @@ -255,11 +255,25 @@ impl SessionView { .set_selected_item(item); } + /// Select the room with the given ID in this view. pub fn select_room_by_id(&self, room_id: &RoomId) { if let Some(room) = self.session().and_then(|s| s.room_list().get(room_id)) { self.select_room(Some(room)); } else { - warn!("A room with id {room_id} couldn't be found"); + warn!("A room with ID {room_id} could not be found"); + } + } + + /// Select the verification with the given flow ID for the user with the + /// given ID in this view. + pub fn select_verification_by_id(&self, user_id: &UserId, flow_id: &str) { + if let Some(verification) = self + .session() + .and_then(|s| s.verification_list().get_by_id(user_id, flow_id)) + { + self.select_item(Some(verification)); + } else { + warn!("A verification with flow ID {flow_id} could not be found"); } } diff --git a/src/session/view/user_page.rs b/src/session/view/user_page.rs new file mode 100644 index 00000000..04a9bd05 --- /dev/null +++ b/src/session/view/user_page.rs @@ -0,0 +1,188 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::gettext; +use gtk::{glib, glib::clone, CompositeTemplate}; + +use crate::{ + components::{Avatar, SpinnerButton}, + prelude::*, + session::model::User, + toast, + utils::BoundObject, + Window, +}; + +mod imp { + use std::cell::RefCell; + + use glib::subclass::InitializingObject; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/Fractal/ui/session/view/user_page.ui")] + pub struct UserPage { + #[template_child] + pub avatar: TemplateChild, + #[template_child] + pub verified_row: TemplateChild, + #[template_child] + pub verified_stack: TemplateChild, + #[template_child] + pub verify_button: TemplateChild, + /// The current user. + pub user: BoundObject, + pub bindings: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for UserPage { + const NAME: &'static str = "UserPage"; + type Type = super::UserPage; + type ParentType = adw::NavigationPage; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + + klass.install_action_async("user-page.verify-user", None, |widget, _, _| async move { + widget.verify_user().await; + }); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for UserPage { + fn properties() -> &'static [glib::ParamSpec] { + use once_cell::sync::Lazy; + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpecObject::builder::("user") + .construct_only() + .build()] + }); + + PROPERTIES.as_ref() + } + + fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + match pspec.name() { + "user" => self.obj().set_user(value.get().unwrap()), + _ => unimplemented!(), + } + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "user" => self.obj().user().to_value(), + _ => unimplemented!(), + } + } + + fn dispose(&self) { + for binding in self.bindings.take() { + binding.unbind(); + } + } + } + + impl WidgetImpl for UserPage {} + impl NavigationPageImpl for UserPage {} +} + +glib::wrapper! { + /// Page to view details about a user. + pub struct UserPage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; +} + +#[gtk::template_callbacks] +impl UserPage { + /// Construct a new `UserPage` for the given user. + pub fn new(user: &impl IsA) -> Self { + glib::Object::builder().property("user", user).build() + } + + /// The current user. + pub fn user(&self) -> Option { + self.imp().user.obj() + } + + /// Set the current user. + fn set_user(&self, user: Option) { + let Some(user) = user else { + // Ignore missing user. + return; + }; + let imp = self.imp(); + + let title_binding = user + .bind_property("display-name", self, "title") + .sync_create() + .build(); + let avatar_binding = user + .bind_property("avatar-data", &*imp.avatar, "data") + .sync_create() + .build(); + imp.bindings.replace(vec![title_binding, avatar_binding]); + + let is_verified_handler = user.connect_notify_local( + Some("is-verified"), + clone!(@weak self as obj => move |_, _| { + obj.update_verified(); + }), + ); + + self.imp().user.set(user, vec![is_verified_handler]); + self.update_verified(); + } + + /// Update the verified row. + fn update_verified(&self) { + let Some(user) = self.user() else { + return; + }; + let imp = self.imp(); + + if user.is_verified() { + imp.verified_row.set_title(&gettext("Identity verified")); + imp.verified_stack.set_visible_child_name("icon"); + self.action_set_enabled("user-page.verify-user", false); + } else { + self.action_set_enabled("user-page.verify-user", true); + imp.verified_stack.set_visible_child_name("button"); + imp.verified_row + .set_title(&gettext("Identity not verified")); + } + } + + /// Launch the verification for the current user. + async fn verify_user(&self) { + let Some(user) = self.user() else { + return; + }; + let imp = self.imp(); + + self.action_set_enabled("user-page.verify-user", false); + imp.verify_button.set_loading(true); + let verification = user.verify_identity().await; + + let Some(flow_id) = verification.flow_id() else { + toast!(self, gettext("Failed to start user verification")); + self.action_set_enabled("user-page.verify-user", true); + imp.verify_button.set_loading(false); + + return; + }; + + let Some(parent_window) = self.root().and_downcast::() else { + return; + }; + + if let Some(main_window) = parent_window.transient_for().and_downcast::() { + main_window.show_verification(user.session().session_id(), &user.user_id(), flow_id); + } + + parent_window.close(); + } +} diff --git a/src/session/view/user_page.ui b/src/session/view/user_page.ui new file mode 100644 index 00000000..00d1d4a6 --- /dev/null +++ b/src/session/view/user_page.ui @@ -0,0 +1,135 @@ + + + + diff --git a/src/ui-resources.gresource.xml b/src/ui-resources.gresource.xml index 821d0a60..32ce4b89 100644 --- a/src/ui-resources.gresource.xml +++ b/src/ui-resources.gresource.xml @@ -55,7 +55,6 @@ session/view/content/room_details/history_viewer/media_item.ui session/view/content/room_details/invite_subpage/invitee_row.ui session/view/content/room_details/invite_subpage/mod.ui - session/view/content/room_details/members_page/member_menu.ui session/view/content/room_details/members_page/members_list_view/member_row.ui session/view/content/room_details/members_page/members_list_view/membership_subpage_row.ui session/view/content/room_details/members_page/members_list_view/mod.ui @@ -101,6 +100,7 @@ session/view/sidebar/mod.ui session/view/sidebar/room_row.ui session/view/sidebar/verification_row.ui + session/view/user_page.ui shortcuts.ui window.ui diff --git a/src/window.rs b/src/window.rs index 2c32b9f8..156ffcee 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use adw::subclass::prelude::AdwApplicationWindowImpl; use gettextrs::gettext; use gtk::{self, gdk, gio, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate}; -use ruma::RoomId; +use ruma::{RoomId, UserId}; use tracing::{error, warn}; use crate::{ @@ -515,4 +515,16 @@ impl Window { imp.error_page.display_secret_error(message); imp.main_stack.set_visible_child(&*imp.error_page); } + + /// Show the verification with the given flow ID for the user with the given + /// ID for the given session. + pub fn show_verification(&self, session_id: &str, user_id: &UserId, flow_id: &str) { + if self.set_current_session_by_id(session_id) { + self.imp() + .session + .select_verification_by_id(user_id, flow_id); + + self.present(); + } + } }