fractal/session_view/create_direct_chat_dialog/
user_list.rs1use 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 list: RefCell<Vec<DirectChatUser>>,
20 #[property(get, construct_only)]
22 session: glib::WeakRef<Session>,
23 #[property(get, builder(LoadingState::default()))]
25 loading_state: Cell<LoadingState>,
26 #[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 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 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 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 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 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 return;
136 };
137
138 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 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 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 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}