members-list: Open user details page on click

Replaces the toggle button with the menu.
This commit is contained in:
Kévin Commaille 2023-12-07 12:05:29 +01:00
parent 81660826c2
commit c6c3c73c3b
No known key found for this signature in database
GPG key ID: 29A48C1F03620416
18 changed files with 432 additions and 361 deletions

View file

@ -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

View file

@ -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,

View file

@ -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::<Option<glib::Object>>().unwrap())
}
_ => unimplemented!(),
}
}
@ -228,8 +230,9 @@ impl Selection {
}
/// Set the selected item.
pub fn set_selected_item(&self, item: Option<glib::Object>) {
pub fn set_selected_item(&self, item: Option<impl IsA<glib::Object>>) {
let imp = self.imp();
let item = item.and_upcast();
let selected_item = self.selected_item();
if selected_item == item {

View file

@ -165,7 +165,7 @@ impl Invite {
let selection = session.sidebar_list_model().selection_model();
if let Some(selected_room) = selection.selected_item().and_downcast::<Room>() {
if selected_room == *room {
selection.set_selected_item(None);
selection.set_selected_item(Option::<glib::Object>::None);
}
}
}

View file

@ -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<Option<Member>>,
pub popover: OnceCell<gtk::PopoverMenu>,
pub destroy_handler: RefCell<Option<glib::signal::SignalHandlerId>>,
pub actions_handler: RefCell<Option<glib::signal::SignalHandlerId>>,
}
#[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<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<Member>("member")
.explicit_notify()
.build(),
glib::ParamSpecFlags::builder::<UserActions>("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<imp::MemberMenu>);
}
impl MemberMenu {
pub fn new() -> Self {
glib::Object::new()
}
/// The member to apply actions to.
pub fn member(&self) -> Option<Member> {
self.imp().member.borrow().clone()
}
/// Set the member to apply actions to.
pub fn set_member(&self, member: Option<Member>) {
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) -> &gtk::PopoverMenu {
self.imp().popover.get_or_init(|| {
gtk::PopoverMenu::from_model(Some(
&gtk::Builder::from_resource("/org/gnome/Fractal/ui/session/view/content/room_details/members_page/member_menu.ui")
.object::<gio::MenuModel>("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: &gtk::ToggleButton, member: Option<Member>) {
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::<gtk::ToggleButton>()
.expect("The parent of a MemberMenu needs to be a gtk::ToggleButton")
.set_active(false);
}
}
}

View file

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu_model">
<section>
<item>
<attribute name="label" translatable="yes">_Verify</attribute>
<attribute name="action">member.verify</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<attribute name="hidden-when">action-missing</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Make _Mod</attribute>
<attribute name="action">member.make-mod</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<attribute name="hidden-when">action-missing</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Make _Admin</attribute>
<attribute name="action">member.make-admin</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<attribute name="hidden-when">action-missing</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Kick</attribute>
<attribute name="action">member.kick</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<attribute name="hidden-when">action-missing</attribute>
</item>
</section>
</menu>
</interface>

View file

@ -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<Option<Member>>,
#[template_child]
pub menu_btn: TemplateChild<gtk::ToggleButton>,
}
#[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<MemberMenu> {
let member_page = self
.ancestor(MembersPage::static_type())
.and_downcast::<MembersPage>()?;
Some(member_page.member_menu().clone())
}
}

View file

@ -42,12 +42,7 @@
<child>
<object class="GtkImage" id="verified_icon">
<property name="icon-name">verified-symbolic</property>
<!-- Translators: As in "Verified room member". -->
<property name="tooltip-text" translatable="yes">Verified</property>
<accessibility>
<!-- Translators: As in "Verified room member". -->
<property name="label" translatable="yes">Verified</property>
</accessibility>
<property name="tooltip-text" translatable="yes">Identity verified</property>
<binding name="visible">
<lookup name="verified" type="User">
<lookup name="member">ContentMemberRow</lookup>
@ -83,15 +78,5 @@
</child>
</object>
</child>
<child>
<object class="GtkToggleButton" id="menu_btn">
<property name="has-frame">False</property>
<property name="icon-name">menu-secondary-symbolic</property>
<property name="tooltip-text" translatable="yes">Member Menu</property>
<accessibility>
<property name="label" translatable="yes">Member Menu</property>
</accessibility>
</object>
</child>
</template>
</interface>

View file

@ -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 {}

View file

@ -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(&gtk::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::<Member>() {
obj.activate_action(
"members.show-member",
Some(&member.user_id().as_str().to_variant()),
)
.unwrap();
} else if let Some(item) = item.downcast_ref::<MembershipSubpageItem>() {
obj.activate_action(
"members.show-membership-list",
Some(&item.state().to_variant()),
)
.unwrap();
}
}));
}
}

View file

@ -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<Room>,
#[template_child]
pub navigation_view: TemplateChild<adw::NavigationView>,
pub member_menu: OnceCell<MemberMenu>,
pub can_invite_watch: RefCell<Option<gtk::ExpressionWatch>>,
}
@ -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::<Membership>())
else {
return;
};
klass.install_action("members.subpage", Some("u"), move |widget, _, param| {
let Some(membership) = param.and_then(|variant| variant.get::<Membership>()) 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::<String>())
.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::<User>().verify_identity().await;
}));
}
fn build_filtered_list(
&self,
model: impl IsA<gio::ListModel>,

View file

@ -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, _, _| {

View file

@ -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,
};

View file

@ -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<Room>) {
self.select_item(room.map(|item| item.upcast()));
self.select_item(room);
}
pub fn select_item(&self, item: Option<glib::Object>) {
pub fn select_item(&self, item: Option<impl IsA<glib::Object>>) {
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");
}
}

View file

@ -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<Avatar>,
#[template_child]
pub verified_row: TemplateChild<adw::ActionRow>,
#[template_child]
pub verified_stack: TemplateChild<gtk::Stack>,
#[template_child]
pub verify_button: TemplateChild<SpinnerButton>,
/// The current user.
pub user: BoundObject<User>,
pub bindings: RefCell<Vec<glib::Binding>>,
}
#[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<Self>) {
obj.init_template();
}
}
impl ObjectImpl for UserPage {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<User>("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<imp::UserPage>)
@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<User>) -> Self {
glib::Object::builder().property("user", user).build()
}
/// The current user.
pub fn user(&self) -> Option<User> {
self.imp().user.obj()
}
/// Set the current user.
fn set_user(&self, user: Option<User>) {
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::<gtk::Window>() else {
return;
};
if let Some(main_window) = parent_window.transient_for().and_downcast::<Window>() {
main_window.show_verification(user.session().session_id(), &user.user_id(), flow_id);
}
parent_window.close();
}
}

View file

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="UserPage" parent="AdwNavigationPage">
<style>
<class name="form-page"/>
</style>
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar"/>
</child>
<property name="content">
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="hscrollbar-policy">never</property>
<property name="propagate-natural-height">True</property>
<property name="vexpand">True</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">444</property>
<property name="child">
<object class="GtkBox" id="box">
<property name="orientation">vertical</property>
<child>
<object class="ComponentsAvatar" id="avatar">
<property name="size">128</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="wrap">true</property>
<property name="wrap-mode">word-char</property>
<binding name="label">
<lookup name="display-name">
<lookup name="user">UserPage</lookup>
</lookup>
</binding>
<style>
<class name="title-1"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="user_id">
<property name="selectable">true</property>
<property name="wrap">true</property>
<property name="wrap-mode">word-char</property>
<binding name="label">
<lookup name="user-id">
<lookup name="user">UserPage</lookup>
</lookup>
</binding>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Security</property>
<property name="ellipsize">end</property>
<property name="xalign">0.0</property>
<style>
<class name="heading"/>
<class name="h4"/>
</style>
</object>
</child>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
</style>
<child>
<object class="AdwActionRow" id="verified_row">
<property name="activatable-widget">verify_button</property>
<child>
<object class="GtkStack" id="verified_stack">
<property name="transition-type">crossfade</property>
<property name="valign">center</property>
<child>
<object class="GtkStackPage">
<property name="name">button</property>
<property name="child">
<object class="SpinnerButton" id="verify_button">
<property name="sensitive">False</property>
<property name="label" translatable="yes">Verify</property>
<property name="action-name">user-page.verify-user</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">icon</property>
<property name="child">
<object class="GtkImage">
<property name="icon-name">verified-symbolic</property>
<property name="accessible-role">presentation</property>
<property name="halign">end</property>
<property name="margin-end">6</property>
<style>
<class name="success"/>
</style>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</property>
</template>
</interface>

View file

@ -55,7 +55,6 @@
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/history_viewer/media_item.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/invite_subpage/invitee_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/invite_subpage/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/members_page/member_menu.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/members_page/members_list_view/member_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/members_page/members_list_view/membership_subpage_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/members_page/members_list_view/mod.ui</file>
@ -101,6 +100,7 @@
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/room_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/verification_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/user_page.ui</file>
<file compressed="true" preprocess="xml-stripblanks">shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
</gresource>

View file

@ -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();
}
}
}