members-list: Open user details page on click
Replaces the toggle button with the menu.
This commit is contained in:
parent
81660826c2
commit
c6c3c73c3b
18 changed files with 432 additions and 361 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) -> >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::<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: >k::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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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::<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();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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, _, _| {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
188
src/session/view/user_page.rs
Normal file
188
src/session/view/user_page.rs
Normal 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();
|
||||
}
|
||||
}
|
135
src/session/view/user_page.ui
Normal file
135
src/session/view/user_page.ui
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue