fractal/session/model/
ignored_users.rs

1use futures_util::StreamExt;
2use gtk::{
3    gio,
4    glib::{self, clone},
5    prelude::*,
6    subclass::prelude::*,
7};
8use indexmap::IndexSet;
9use ruma::{events::ignored_user_list::IgnoredUserListEventContent, OwnedUserId};
10use tracing::{debug, error, warn};
11
12use super::Session;
13use crate::{spawn, spawn_tokio};
14
15mod imp {
16    use std::cell::RefCell;
17
18    use super::*;
19
20    #[derive(Debug, Default, glib::Properties)]
21    #[properties(wrapper_type = super::IgnoredUsers)]
22    pub struct IgnoredUsers {
23        /// The current session.
24        #[property(get, set = Self::set_session, explicit_notify, nullable)]
25        pub session: glib::WeakRef<Session>,
26        /// The content of the ignored user list event.
27        pub list: RefCell<IndexSet<OwnedUserId>>,
28        abort_handle: RefCell<Option<tokio::task::AbortHandle>>,
29    }
30
31    #[glib::object_subclass]
32    impl ObjectSubclass for IgnoredUsers {
33        const NAME: &'static str = "IgnoredUsers";
34        type Type = super::IgnoredUsers;
35        type Interfaces = (gio::ListModel,);
36    }
37
38    #[glib::derived_properties]
39    impl ObjectImpl for IgnoredUsers {
40        fn dispose(&self) {
41            if let Some(abort_handle) = self.abort_handle.take() {
42                abort_handle.abort();
43            }
44        }
45    }
46
47    impl ListModelImpl for IgnoredUsers {
48        fn item_type(&self) -> glib::Type {
49            gtk::StringObject::static_type()
50        }
51
52        fn n_items(&self) -> u32 {
53            self.list.borrow().len() as u32
54        }
55
56        fn item(&self, position: u32) -> Option<glib::Object> {
57            self.list
58                .borrow()
59                .get_index(position as usize)
60                .map(|user_id| gtk::StringObject::new(user_id.as_str()).upcast())
61        }
62    }
63
64    impl IgnoredUsers {
65        /// Set the current session.
66        fn set_session(&self, session: Option<&Session>) {
67            if self.session.upgrade().as_ref() == session {
68                return;
69            }
70
71            self.session.set(session);
72
73            self.init();
74            self.obj().notify_session();
75        }
76
77        /// Listen to changes of the ignored users list.
78        fn init(&self) {
79            if let Some(abort_handle) = self.abort_handle.take() {
80                abort_handle.abort();
81            }
82
83            let Some(session) = self.session.upgrade() else {
84                return;
85            };
86            let obj = self.obj();
87
88            let obj_weak = glib::SendWeakRef::from(obj.downgrade());
89            let subscriber = session.client().subscribe_to_ignore_user_list_changes();
90            let fut = subscriber.for_each(move |_| {
91                let obj_weak = obj_weak.clone();
92                async move {
93                    let ctx = glib::MainContext::default();
94                    ctx.spawn(async move {
95                        spawn!(async move {
96                            if let Some(obj) = obj_weak.upgrade() {
97                                obj.imp().load_list().await;
98                            }
99                        });
100                    });
101                }
102            });
103
104            let abort_handle = spawn_tokio!(fut).abort_handle();
105            self.abort_handle.replace(Some(abort_handle));
106
107            spawn!(clone!(
108                #[weak(rename_to = imp)]
109                self,
110                async move {
111                    imp.load_list().await;
112                }
113            ));
114        }
115
116        /// Load the list from the store and update it.
117        async fn load_list(&self) {
118            let Some(session) = self.session.upgrade() else {
119                return;
120            };
121
122            let client = session.client();
123            let handle = spawn_tokio!(async move {
124                client
125                    .account()
126                    .account_data::<IgnoredUserListEventContent>()
127                    .await
128            });
129
130            let raw = match handle.await.unwrap() {
131                Ok(Some(raw)) => raw,
132                Ok(None) => {
133                    debug!("Got no ignored users list");
134                    self.update_list(IndexSet::new());
135                    return;
136                }
137                Err(error) => {
138                    error!("Could not get ignored users list: {error}");
139                    return;
140                }
141            };
142
143            match raw.deserialize() {
144                Ok(content) => self.update_list(content.ignored_users.into_keys().collect()),
145                Err(error) => {
146                    error!("Could not deserialize ignored users list: {error}");
147                }
148            }
149        }
150
151        /// Update the list with the given new list.
152        fn update_list(&self, new_list: IndexSet<OwnedUserId>) {
153            if *self.list.borrow() == new_list {
154                return;
155            }
156
157            let old_len = self.n_items();
158            let new_len = new_list.len() as u32;
159
160            let mut pos = 0;
161            {
162                let old_list = self.list.borrow();
163
164                for old_item in old_list.iter() {
165                    let Some(new_item) = new_list.get_index(pos as usize) else {
166                        break;
167                    };
168
169                    if old_item != new_item {
170                        break;
171                    }
172
173                    pos += 1;
174                }
175            }
176
177            if old_len == new_len && pos == new_len {
178                // Nothing changed.
179                return;
180            }
181
182            self.list.replace(new_list);
183
184            self.obj().items_changed(
185                pos,
186                old_len.saturating_sub(pos),
187                new_len.saturating_sub(pos),
188            );
189        }
190    }
191}
192
193glib::wrapper! {
194    /// The list of ignored users of a `Session`.
195    pub struct IgnoredUsers(ObjectSubclass<imp::IgnoredUsers>)
196        @implements gio::ListModel;
197}
198
199impl IgnoredUsers {
200    pub fn new() -> Self {
201        glib::Object::new()
202    }
203
204    /// Whether this list contains the given user ID.
205    pub fn contains(&self, user_id: &OwnedUserId) -> bool {
206        self.imp().list.borrow().contains(user_id)
207    }
208
209    /// Add the user with the given ID to the list.
210    pub async fn add(&self, user_id: &OwnedUserId) -> Result<(), ()> {
211        let Some(session) = self.session() else {
212            return Err(());
213        };
214
215        if self.contains(user_id) {
216            warn!("Trying to add `{user_id}` to the ignored users but they are already in the list, ignoring");
217            return Ok(());
218        }
219
220        let client = session.client();
221        let user_id_clone = user_id.clone();
222        let handle =
223            spawn_tokio!(async move { client.account().ignore_user(&user_id_clone).await });
224
225        match handle.await.unwrap() {
226            Ok(()) => {
227                let (pos, added) = self.imp().list.borrow_mut().insert_full(user_id.clone());
228
229                if added {
230                    self.items_changed(pos as u32, 0, 1);
231                }
232                Ok(())
233            }
234            Err(error) => {
235                error!("Could not add `{user_id}` to the ignored users: {error}");
236                Err(())
237            }
238        }
239    }
240
241    /// Remove the user with the given ID from the list.
242    pub async fn remove(&self, user_id: &OwnedUserId) -> Result<(), ()> {
243        let Some(session) = self.session() else {
244            return Err(());
245        };
246
247        if !self.contains(user_id) {
248            warn!("Trying to remove `{user_id}` from the ignored users but they are not in the list, ignoring");
249            return Ok(());
250        }
251
252        let client = session.client();
253        let user_id_clone = user_id.clone();
254        let handle =
255            spawn_tokio!(async move { client.account().unignore_user(&user_id_clone).await });
256
257        match handle.await.unwrap() {
258            Ok(()) => {
259                let removed = self.imp().list.borrow_mut().shift_remove_full(user_id);
260
261                if let Some((pos, _)) = removed {
262                    self.items_changed(pos as u32, 1, 0);
263                }
264                Ok(())
265            }
266            Err(error) => {
267                error!("Could not remove `{user_id}` from the ignored users: {error}");
268                Err(())
269            }
270        }
271    }
272}
273
274impl Default for IgnoredUsers {
275    fn default() -> Self {
276        Self::new()
277    }
278}