fractal/session/view/content/room_details/permissions/
members_subpage.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{
3    glib,
4    glib::{clone, closure},
5    CompositeTemplate,
6};
7use tracing::error;
8
9use super::{MemberPowerLevel, PermissionsMemberRow, PrivilegedMembers};
10use crate::{session::model::User, utils::expression};
11
12mod imp {
13    use std::{cell::Cell, marker::PhantomData};
14
15    use glib::subclass::InitializingObject;
16
17    use super::*;
18
19    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
20    #[template(
21        resource = "/org/gnome/Fractal/ui/session/view/content/room_details/permissions/members_subpage.ui"
22    )]
23    #[properties(wrapper_type = super::PermissionsMembersSubpage)]
24    pub struct PermissionsMembersSubpage {
25        #[template_child]
26        search_bar: TemplateChild<gtk::SearchBar>,
27        #[template_child]
28        search_entry: TemplateChild<gtk::SearchEntry>,
29        #[template_child]
30        list_view: TemplateChild<gtk::ListView>,
31        filtered_model: gtk::FilterListModel,
32        /// The list used for this view.
33        #[property(get = Self::list, set = Self::set_list, explicit_notify, nullable)]
34        list: PhantomData<Option<PrivilegedMembers>>,
35        /// Whether our own user can change the power levels in this room.
36        #[property(get, set = Self::set_editable, explicit_notify)]
37        editable: Cell<bool>,
38    }
39
40    #[glib::object_subclass]
41    impl ObjectSubclass for PermissionsMembersSubpage {
42        const NAME: &'static str = "RoomDetailsPermissionsMembersSubpage";
43        type Type = super::PermissionsMembersSubpage;
44        type ParentType = adw::NavigationPage;
45
46        fn class_init(klass: &mut Self::Class) {
47            Self::bind_template(klass);
48        }
49
50        fn instance_init(obj: &InitializingObject<Self>) {
51            obj.init_template();
52        }
53    }
54
55    #[glib::derived_properties]
56    impl ObjectImpl for PermissionsMembersSubpage {
57        fn constructed(&self) {
58            self.parent_constructed();
59
60            // Needed because the GtkSearchEntry is not the direct child of the
61            // GtkSearchBar.
62            self.search_bar.connect_entry(&*self.search_entry);
63
64            let user_expr = gtk::ClosureExpression::new::<String>(
65                &[] as &[gtk::Expression],
66                closure!(|item: Option<glib::Object>| {
67                    item.and_downcast_ref()
68                        .map(MemberPowerLevel::search_string)
69                        .unwrap_or_default()
70                }),
71            );
72            let search_filter = gtk::StringFilter::builder()
73                .match_mode(gtk::StringFilterMatchMode::Substring)
74                .expression(expression::normalize_string(user_expr))
75                .ignore_case(true)
76                .build();
77
78            expression::normalize_string(self.search_entry.property_expression("text")).bind(
79                &search_filter,
80                "search",
81                None::<&glib::Object>,
82            );
83
84            self.filtered_model.set_filter(Some(&search_filter));
85
86            // Sort members by power level, then display name, then user ID.
87            let power_level_expr = MemberPowerLevel::this_expression("power-level");
88            let power_level_sorter = gtk::NumericSorter::builder()
89                .expression(power_level_expr)
90                .sort_order(gtk::SortType::Descending)
91                .build();
92
93            let display_name_expr =
94                MemberPowerLevel::this_expression("user").chain_property::<User>("display-name");
95            let display_name_sorter = gtk::StringSorter::new(Some(display_name_expr));
96
97            let user_id_expr =
98                MemberPowerLevel::this_expression("user").chain_property::<User>("user-id-string");
99            let user_id_sorter = gtk::StringSorter::new(Some(user_id_expr));
100
101            let sorter = gtk::MultiSorter::new();
102            sorter.append(power_level_sorter);
103            sorter.append(display_name_sorter);
104            sorter.append(user_id_sorter);
105
106            let sorted_model =
107                gtk::SortListModel::new(Some(self.filtered_model.clone()), Some(sorter));
108
109            self.list_view
110                .set_model(Some(&gtk::NoSelection::new(Some(sorted_model))));
111
112            let factory = gtk::SignalListItemFactory::new();
113            factory.connect_setup(clone!(
114                #[weak(rename_to = imp)]
115                self,
116                move |_, item| {
117                    let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
118                        error!("List item factory did not receive a list item: {item:?}");
119                        return;
120                    };
121                    let Some(permissions) = imp.list().and_then(|l| l.permissions()) else {
122                        return;
123                    };
124                    let row = PermissionsMemberRow::new(&permissions);
125                    item.set_child(Some(&row));
126                    item.bind_property("item", &row, "member")
127                        .sync_create()
128                        .build();
129                    item.set_activatable(false);
130                    item.set_selectable(false);
131                }
132            ));
133            self.list_view.set_factory(Some(&factory));
134        }
135    }
136
137    impl WidgetImpl for PermissionsMembersSubpage {}
138    impl NavigationPageImpl for PermissionsMembersSubpage {}
139
140    impl PermissionsMembersSubpage {
141        /// The list used for this view.
142        fn list(&self) -> Option<PrivilegedMembers> {
143            self.filtered_model.model().and_downcast()
144        }
145
146        /// Set the list used for this view.
147        fn set_list(&self, list: Option<&PrivilegedMembers>) {
148            if self.list().as_ref() == list {
149                return;
150            }
151
152            self.filtered_model.set_model(list);
153            self.obj().notify_list();
154        }
155
156        /// Set whether our own user can edit the list.
157        fn set_editable(&self, editable: bool) {
158            if self.editable.get() == editable {
159                return;
160            }
161
162            self.editable.set(editable);
163            self.obj().notify_editable();
164        }
165    }
166}
167
168glib::wrapper! {
169    /// A subpage to see and possibly edit the room members with custom power levels.
170    pub struct PermissionsMembersSubpage(ObjectSubclass<imp::PermissionsMembersSubpage>)
171        @extends gtk::Widget, adw::NavigationPage;
172}
173
174impl PermissionsMembersSubpage {
175    pub fn new() -> Self {
176        glib::Object::new()
177    }
178}