Skip to main content

fractal/session/sidebar_data/section/
mod.rs

1use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
2
3mod name;
4mod room_category_filter;
5
6pub use self::name::SidebarSectionName;
7use self::room_category_filter::RoomCategoryFilter;
8use crate::{
9    session::{
10        Room, RoomCategory, RoomList, SessionSettings, VerificationList, room::HighlightFlags,
11    },
12    utils::ExpressionListModel,
13};
14
15mod imp {
16    use std::{
17        cell::{Cell, OnceCell},
18        marker::PhantomData,
19    };
20
21    use super::*;
22
23    #[derive(Debug, Default, glib::Properties)]
24    #[properties(wrapper_type = super::SidebarSection)]
25    pub struct SidebarSection {
26        /// The source model of this section.
27        #[property(get, set = Self::set_model, construct_only)]
28        model: OnceCell<gio::ListModel>,
29        /// The inner model of this section.
30        inner_model: OnceCell<gio::ListModel>,
31        /// The filter of this section.
32        filter: RoomCategoryFilter,
33        /// The name of this section.
34        #[property(get, set = Self::set_name, construct_only, builder(SidebarSectionName::default()))]
35        name: Cell<SidebarSectionName>,
36        /// The display name of this section.
37        #[property(get = Self::display_name)]
38        display_name: PhantomData<String>,
39        /// Whether this section is empty.
40        #[property(get)]
41        is_empty: Cell<bool>,
42        /// Whether this section is expanded.
43        #[property(get, set = Self::set_is_expanded, explicit_notify)]
44        is_expanded: Cell<bool>,
45        /// Whether any of the rooms in this section have unread notifications.
46        #[property(get)]
47        has_notifications: Cell<bool>,
48        /// Total number of unread notifications over all the rooms in this
49        /// section.
50        #[property(get)]
51        notification_count: Cell<u64>,
52        /// Whether all the messages of all the rooms in this section are read.
53        #[property(get)]
54        is_read: Cell<bool>,
55        /// The highlight state of the section.
56        #[property(get)]
57        highlight: Cell<HighlightFlags>,
58    }
59
60    #[glib::object_subclass]
61    impl ObjectSubclass for SidebarSection {
62        const NAME: &'static str = "SidebarSection";
63        type Type = super::SidebarSection;
64        type Interfaces = (gio::ListModel,);
65    }
66
67    #[glib::derived_properties]
68    impl ObjectImpl for SidebarSection {
69        fn constructed(&self) {
70            self.parent_constructed();
71
72            let Some(settings) = self.session_settings() else {
73                return;
74            };
75
76            let is_expanded = settings.is_section_expanded(self.name.get());
77            self.set_is_expanded(is_expanded);
78        }
79    }
80
81    impl ListModelImpl for SidebarSection {
82        fn item_type(&self) -> glib::Type {
83            glib::Object::static_type()
84        }
85
86        fn n_items(&self) -> u32 {
87            self.inner_model().n_items()
88        }
89
90        fn item(&self, position: u32) -> Option<glib::Object> {
91            self.inner_model().item(position)
92        }
93    }
94
95    impl SidebarSection {
96        /// The source model of this section.
97        fn model(&self) -> &gio::ListModel {
98            self.model.get().expect("model should be initialized")
99        }
100
101        /// Set the source model of this section.
102        fn set_model(&self, model: gio::ListModel) {
103            let model = self.model.get_or_init(|| model).clone();
104            let obj = self.obj();
105
106            // Special-case room lists so that they are sorted and in the right section.
107            let inner_model = if model.is::<RoomList>() {
108                // Filter the list to only show rooms for the proper category.
109                self.filter
110                    .set_expression(Some(Room::this_expression("category").upcast()));
111                let filter_model = gtk::FilterListModel::builder()
112                    .model(&model)
113                    .filter(&self.filter)
114                    .watch_items(true)
115                    .build();
116
117                // Sort the list by activity.
118                let room_latest_activity = Room::this_expression("latest-activity");
119                let sorter = gtk::NumericSorter::builder()
120                    .expression(&room_latest_activity)
121                    .sort_order(gtk::SortType::Descending)
122                    .build();
123
124                let latest_activity_expr_model = ExpressionListModel::new();
125                latest_activity_expr_model.set_expressions(vec![room_latest_activity.upcast()]);
126                latest_activity_expr_model.set_model(Some(filter_model.clone()));
127
128                let sort_model =
129                    gtk::SortListModel::new(Some(latest_activity_expr_model), Some(sorter));
130
131                // Watch for notification count and highlight changes in the filtered room list.
132                let room_notification_count = Room::this_expression("notification-count");
133                let room_highlight = Room::this_expression("highlight");
134                let notification_and_highlight_expr_model = ExpressionListModel::new();
135                notification_and_highlight_expr_model.set_expressions(vec![
136                    room_notification_count.upcast(),
137                    room_highlight.upcast(),
138                ]);
139                notification_and_highlight_expr_model.connect_items_changed(clone!(
140                    #[weak(rename_to = imp)]
141                    self,
142                    move |model, _, _, _| {
143                        imp.update_notification_count_and_highlight(model);
144                    }
145                ));
146                notification_and_highlight_expr_model.set_model(Some(filter_model));
147
148                sort_model.upcast()
149            } else {
150                model
151            };
152
153            inner_model.connect_items_changed(clone!(
154                #[weak]
155                obj,
156                move |model, pos, removed, added| {
157                    obj.items_changed(pos, removed, added);
158                    obj.imp().set_is_empty(model.n_items() == 0);
159                }
160            ));
161
162            self.set_is_empty(inner_model.n_items() == 0);
163            self.inner_model
164                .set(inner_model)
165                .expect("inner model should be uninitialized");
166        }
167
168        /// Update each of the properties if needed and emit corresponding
169        /// signals.
170        fn update_notification_count_and_highlight(&self, model: &ExpressionListModel) {
171            // Aggregate properties over rooms in the section.
172
173            // property:notification-count
174            let mut notification_count = 0;
175            // property:is-read
176            let mut is_read = true;
177            // property:highlight
178            let mut highlight = HighlightFlags::empty();
179
180            for room in model.iter::<glib::Object>() {
181                if let Some(room) = room.ok().and_downcast::<Room>() {
182                    notification_count += room.notification_count();
183                    is_read &= room.is_read();
184                    highlight |= room.highlight();
185                }
186            }
187
188            // property:has-notification
189            let has_notifications = notification_count > 0;
190
191            if self.notification_count.get() != notification_count {
192                self.notification_count.set(notification_count);
193                self.obj().notify_notification_count();
194            }
195
196            if self.has_notifications.get() != has_notifications {
197                self.has_notifications.set(has_notifications);
198                self.obj().notify_has_notifications();
199            }
200
201            if self.highlight.get() != highlight {
202                self.highlight.set(highlight);
203                self.obj().notify_highlight();
204            }
205
206            if self.is_read.get() != is_read {
207                self.is_read.set(is_read);
208                self.obj().notify_is_read();
209            }
210        }
211
212        /// The inner model of this section.
213        fn inner_model(&self) -> &gio::ListModel {
214            self.inner_model.get().unwrap()
215        }
216
217        /// Set the name of this section.
218        fn set_name(&self, name: SidebarSectionName) {
219            if let Some(room_category) = name.into_room_category() {
220                self.filter.set_room_category(room_category);
221            }
222
223            self.name.set(name);
224            self.obj().notify_name();
225        }
226
227        /// The display name of this section.
228        fn display_name(&self) -> String {
229            self.name.get().to_string()
230        }
231
232        /// Set whether this section is empty.
233        fn set_is_empty(&self, is_empty: bool) {
234            if is_empty == self.is_empty.get() {
235                return;
236            }
237
238            self.is_empty.set(is_empty);
239            self.obj().notify_is_empty();
240        }
241
242        /// Set whether this section is expanded.
243        fn set_is_expanded(&self, expanded: bool) {
244            if self.is_expanded.get() == expanded {
245                return;
246            }
247
248            self.is_expanded.set(expanded);
249            self.obj().notify_is_expanded();
250
251            if let Some(settings) = self.session_settings() {
252                settings.set_section_expanded(self.name.get(), expanded);
253            }
254        }
255
256        /// The settings of the current session.
257        fn session_settings(&self) -> Option<SessionSettings> {
258            let model = self.model();
259            let session = model
260                .downcast_ref::<RoomList>()
261                .and_then(RoomList::session)
262                .or_else(|| {
263                    model
264                        .downcast_ref::<VerificationList>()
265                        .and_then(VerificationList::session)
266                })?;
267            Some(session.settings())
268        }
269    }
270}
271
272glib::wrapper! {
273    /// A list of items in the same section of the sidebar.
274    pub struct SidebarSection(ObjectSubclass<imp::SidebarSection>)
275        @implements gio::ListModel;
276}
277
278impl SidebarSection {
279    /// Constructs a new `SidebarSection` with the given name and source model.
280    pub fn new(name: SidebarSectionName, model: &impl IsA<gio::ListModel>) -> Self {
281        glib::Object::builder()
282            .property("name", name)
283            .property("model", model)
284            .build()
285    }
286
287    /// Whether this section should be shown for the drag-n-drop of a room with
288    /// the given category.
289    pub(crate) fn visible_for_room_category(&self, source_category: Option<RoomCategory>) -> bool {
290        if !self.is_empty() {
291            return true;
292        }
293
294        source_category
295            .zip(self.name().into_target_room_category())
296            .is_some_and(|(source_category, target_category)| {
297                source_category.can_change_to(target_category)
298            })
299    }
300}