Skip to main content

fractal/session_view/create_direct_chat_dialog/
user_list.rs

1use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
2use ruma::UserId;
3use tracing::error;
4
5use super::DirectChatUser;
6use crate::{prelude::*, session::Session, spawn, spawn_tokio, utils::LoadingState};
7
8mod imp {
9    use std::cell::{Cell, RefCell};
10
11    use tokio::task::AbortHandle;
12
13    use super::*;
14
15    #[derive(Debug, Default, glib::Properties)]
16    #[properties(wrapper_type = super::DirectChatUserList)]
17    pub struct DirectChatUserList {
18        /// The current list of results.
19        list: RefCell<Vec<DirectChatUser>>,
20        /// The current session.
21        #[property(get, construct_only)]
22        session: glib::WeakRef<Session>,
23        /// The state of the list.
24        #[property(get, builder(LoadingState::default()))]
25        loading_state: Cell<LoadingState>,
26        /// The search term.
27        #[property(get, set = Self::set_search_term, explicit_notify, nullable)]
28        search_term: RefCell<Option<String>>,
29        abort_handle: RefCell<Option<AbortHandle>>,
30    }
31
32    #[glib::object_subclass]
33    impl ObjectSubclass for DirectChatUserList {
34        const NAME: &'static str = "DirectChatUserList";
35        type Type = super::DirectChatUserList;
36        type Interfaces = (gio::ListModel,);
37    }
38
39    #[glib::derived_properties]
40    impl ObjectImpl for DirectChatUserList {}
41
42    impl ListModelImpl for DirectChatUserList {
43        fn item_type(&self) -> glib::Type {
44            DirectChatUser::static_type()
45        }
46
47        fn n_items(&self) -> u32 {
48            self.list.borrow().len() as u32
49        }
50
51        fn item(&self, position: u32) -> Option<glib::Object> {
52            self.list
53                .borrow()
54                .get(position as usize)
55                .cloned()
56                .and_upcast()
57        }
58    }
59
60    impl DirectChatUserList {
61        /// Set the search term.
62        fn set_search_term(&self, search_term: Option<String>) {
63            let search_term = search_term.filter(|s| !s.is_empty());
64
65            if search_term == *self.search_term.borrow() {
66                return;
67            }
68
69            self.search_term.replace(search_term.clone());
70
71            spawn!(clone!(
72                #[weak(rename_to = imp)]
73                self,
74                async move {
75                    imp.search_users(search_term).await;
76                }
77            ));
78
79            self.obj().notify_search_term();
80        }
81
82        /// Set the loading state of the list.
83        fn set_loading_state(&self, state: LoadingState) {
84            if self.loading_state.get() == state {
85                return;
86            }
87
88            self.loading_state.set(state);
89            self.obj().notify_loading_state();
90        }
91
92        /// Replace the list of results.
93        fn set_list(&self, users: Vec<DirectChatUser>) {
94            let removed = self.n_items();
95            self.list.replace(users);
96            let added = self.n_items();
97
98            self.obj().items_changed(0, removed, added);
99        }
100
101        /// Clear the list of results.
102        fn clear_list(&self) {
103            let removed = self.n_items();
104            self.list.borrow_mut().clear();
105
106            self.obj().items_changed(0, removed, 0);
107        }
108
109        /// Update the list of users for the given search term.
110        async fn search_users(&self, search_term: Option<String>) {
111            let Some(search_term) = search_term else {
112                self.set_loading_state(LoadingState::Initial);
113                return;
114            };
115
116            let Some(session) = self.session.upgrade() else {
117                return;
118            };
119            let client = session.client();
120
121            self.set_loading_state(LoadingState::Loading);
122            self.clear_list();
123
124            let search_term_clone = search_term.clone();
125            let handle =
126                spawn_tokio!(async move { client.search_users(&search_term_clone, 20).await });
127
128            if let Some(abort_handle) = self.abort_handle.replace(Some(handle.abort_handle())) {
129                abort_handle.abort();
130            }
131
132            let Ok(result) = handle.await else {
133                // The future was aborted, which means that there is a new search term, we have
134                // nothing to do.
135                return;
136            };
137
138            // Check that the search term is the current one, in case the future was not
139            // aborted in time.
140            if self
141                .search_term
142                .borrow()
143                .as_ref()
144                .is_none_or(|term| *term != search_term)
145            {
146                return;
147            }
148
149            self.abort_handle.take();
150
151            let response = match result {
152                Ok(response) => response,
153                Err(error) => {
154                    error!("Could not search users: {error}");
155                    self.set_loading_state(LoadingState::Error);
156                    return;
157                }
158            };
159
160            let mut list = Vec::with_capacity(response.results.len());
161
162            // If the search term looks like a UserId and is not already in the response,
163            // insert it.
164            if let Ok(user_id) = UserId::parse(&search_term)
165                && !response.results.iter().any(|item| item.user_id == user_id)
166            {
167                let user = DirectChatUser::new(&session, user_id, None, None);
168
169                // Fetch the avatar and display name.
170                spawn!(clone!(
171                    #[weak]
172                    user,
173                    async move {
174                        let _ = user.load_profile().await;
175                    }
176                ));
177
178                list.push(user);
179            }
180
181            list.extend(response.results.into_iter().map(|user| {
182                DirectChatUser::new(
183                    &session,
184                    user.user_id,
185                    user.display_name.as_deref(),
186                    user.avatar_url,
187                )
188            }));
189
190            self.set_list(list);
191            self.set_loading_state(LoadingState::Ready);
192        }
193    }
194}
195
196glib::wrapper! {
197    /// List of users in the server's user directory matching a search term.
198    pub struct DirectChatUserList(ObjectSubclass<imp::DirectChatUserList>)
199        @implements gio::ListModel;
200}
201
202impl DirectChatUserList {
203    pub fn new(session: &Session) -> Self {
204        glib::Object::builder().property("session", session).build()
205    }
206}