fractal/session/sidebar_data/section/
mod.rs1use 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 #[property(get, set = Self::set_model, construct_only)]
28 model: OnceCell<gio::ListModel>,
29 inner_model: OnceCell<gio::ListModel>,
31 filter: RoomCategoryFilter,
33 #[property(get, set = Self::set_name, construct_only, builder(SidebarSectionName::default()))]
35 name: Cell<SidebarSectionName>,
36 #[property(get = Self::display_name)]
38 display_name: PhantomData<String>,
39 #[property(get)]
41 is_empty: Cell<bool>,
42 #[property(get, set = Self::set_is_expanded, explicit_notify)]
44 is_expanded: Cell<bool>,
45 #[property(get)]
47 has_notifications: Cell<bool>,
48 #[property(get)]
51 notification_count: Cell<u64>,
52 #[property(get)]
54 is_read: Cell<bool>,
55 #[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 fn model(&self) -> &gio::ListModel {
98 self.model.get().expect("model should be initialized")
99 }
100
101 fn set_model(&self, model: gio::ListModel) {
103 let model = self.model.get_or_init(|| model).clone();
104 let obj = self.obj();
105
106 let inner_model = if model.is::<RoomList>() {
108 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 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 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 fn update_notification_count_and_highlight(&self, model: &ExpressionListModel) {
171 let mut notification_count = 0;
175 let mut is_read = true;
177 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 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 fn inner_model(&self) -> &gio::ListModel {
214 self.inner_model.get().unwrap()
215 }
216
217 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 fn display_name(&self) -> String {
229 self.name.get().to_string()
230 }
231
232 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 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 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 pub struct SidebarSection(ObjectSubclass<imp::SidebarSection>)
275 @implements gio::ListModel;
276}
277
278impl SidebarSection {
279 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 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}