fractal/session/model/
user.rs1use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
2use matrix_sdk::encryption::identities::UserIdentity;
3use ruma::{MatrixToUri, OwnedMxcUri, OwnedUserId};
4use tracing::{debug, error};
5
6use super::{IdentityVerification, Room, Session};
7use crate::{
8 components::{AvatarImage, AvatarUriSource, PillSource},
9 prelude::*,
10 spawn, spawn_tokio,
11};
12
13#[glib::flags(name = "UserActions")]
14pub enum UserActions {
15 VERIFY = 0b0000_0001,
16}
17
18impl Default for UserActions {
19 fn default() -> Self {
20 Self::empty()
21 }
22}
23
24mod imp {
25 use std::{
26 cell::{Cell, OnceCell, RefCell},
27 marker::PhantomData,
28 };
29
30 use super::*;
31
32 #[derive(Debug, Default, glib::Properties)]
33 #[properties(wrapper_type = super::User)]
34 pub struct User {
35 user_id: OnceCell<OwnedUserId>,
37 #[property(get = Self::user_id_string)]
39 user_id_string: PhantomData<String>,
40 #[property(get, construct_only)]
42 session: OnceCell<Session>,
43 #[property(get)]
45 is_own_user: Cell<bool>,
46 #[property(get)]
48 is_verified: Cell<bool>,
49 #[property(get)]
51 is_ignored: Cell<bool>,
52 ignored_handler: RefCell<Option<glib::SignalHandlerId>>,
53 }
54
55 #[glib::object_subclass]
56 impl ObjectSubclass for User {
57 const NAME: &'static str = "User";
58 type Type = super::User;
59 type ParentType = PillSource;
60 }
61
62 #[glib::derived_properties]
63 impl ObjectImpl for User {
64 fn constructed(&self) {
65 self.parent_constructed();
66 let obj = self.obj();
67
68 let avatar_image = AvatarImage::new(&obj.session(), AvatarUriSource::User, None, None);
69 obj.avatar_data().set_image(Some(avatar_image));
70 }
71
72 fn dispose(&self) {
73 if let Some(session) = self.session.get() {
74 if let Some(handler) = self.ignored_handler.take() {
75 session.ignored_users().disconnect(handler);
76 }
77 }
78 }
79 }
80
81 impl PillSourceImpl for User {
82 fn identifier(&self) -> String {
83 self.user_id_string()
84 }
85 }
86
87 impl User {
88 pub(super) fn user_id(&self) -> &OwnedUserId {
90 self.user_id.get().expect("user ID should be initialized")
91 }
92
93 fn user_id_string(&self) -> String {
95 self.user_id().to_string()
96 }
97
98 fn session(&self) -> &Session {
100 self.session.get().expect("session should be initialized")
101 }
102
103 pub(crate) fn set_user_id(&self, user_id: OwnedUserId) {
105 let user_id = self.user_id.get_or_init(|| user_id);
106
107 let obj = self.obj();
108 obj.set_name(None);
109 obj.bind_property("display-name", &obj.avatar_data(), "display-name")
110 .sync_create()
111 .build();
112
113 let session = self.session();
114 self.is_own_user.set(session.user_id() == user_id);
115
116 let ignored_users = session.ignored_users();
117 let ignored_handler = ignored_users.connect_items_changed(clone!(
118 #[weak(rename_to = imp)]
119 self,
120 move |ignored_users, _, _, _| {
121 let user_id = imp.user_id.get().expect("user ID is initialized");
122 let is_ignored = ignored_users.contains(user_id);
123
124 if imp.is_ignored.get() != is_ignored {
125 imp.is_ignored.set(is_ignored);
126 imp.obj().notify_is_ignored();
127 }
128 }
129 ));
130 self.is_ignored.set(ignored_users.contains(user_id));
131 self.ignored_handler.replace(Some(ignored_handler));
132
133 spawn!(clone!(
134 #[weak(rename_to = imp)]
135 self,
136 async move {
137 imp.init_is_verified().await;
138 }
139 ));
140 }
141
142 pub(super) async fn local_crypto_identity(&self) -> Option<UserIdentity> {
148 let encryption = self.session().client().encryption();
149 let user_id = self.user_id().clone();
150 let handle = spawn_tokio!(async move { encryption.get_user_identity(&user_id).await });
151
152 match handle.await.expect("task was not aborted") {
153 Ok(identity) => identity,
154 Err(error) => {
155 error!("Could not get local crypto identity: {error}");
156 None
157 }
158 }
159 }
160
161 async fn init_is_verified(&self) {
163 let is_verified = self
165 .local_crypto_identity()
166 .await
167 .is_some_and(|i| i.is_verified());
168
169 if self.is_verified.get() == is_verified {
170 return;
171 }
172
173 self.is_verified.set(is_verified);
174 self.obj().notify_is_verified();
175 }
176
177 pub(super) async fn create_direct_chat(&self) -> Result<Room, matrix_sdk::Error> {
179 let user_id = self.user_id().clone();
180 let client = self.session().client();
181 let handle = spawn_tokio!(async move { client.create_dm(&user_id).await });
182
183 match handle.await.expect("task was not aborted") {
184 Ok(matrix_room) => {
185 let room = self
186 .session()
187 .room_list()
188 .get_wait(matrix_room.room_id(), None)
189 .await
190 .expect("The newly created room was not found");
191 Ok(room)
192 }
193 Err(error) => {
194 error!("Could not create direct chat: {error}");
195 Err(error)
196 }
197 }
198 }
199 }
200}
201
202glib::wrapper! {
203 pub struct User(ObjectSubclass<imp::User>) @extends PillSource;
205}
206
207impl User {
208 pub fn new(session: &Session, user_id: OwnedUserId) -> Self {
210 let obj = glib::Object::builder::<Self>()
211 .property("session", session)
212 .build();
213
214 obj.imp().set_user_id(user_id);
215 obj
216 }
217
218 pub(crate) async fn ensure_crypto_identity(&self) -> Option<UserIdentity> {
225 let session = self.session();
226 let encryption = session.client().encryption();
227 let user_id = self.user_id();
228
229 let should_have_local = if user_id == session.user_id() {
234 true
235 } else {
236 let encryption_clone = encryption.clone();
238 let handle = spawn_tokio!(async move { encryption_clone.tracked_users().await });
239
240 match handle.await.expect("task was not aborted") {
241 Ok(tracked_users) => tracked_users.contains(user_id),
242 Err(error) => {
243 error!("Could not get tracked users: {error}");
244 true
246 }
247 }
248 };
249
250 if should_have_local {
252 if let Some(identity) = self.imp().local_crypto_identity().await {
253 return Some(identity);
254 }
255 }
256
257 let user_id_clone = user_id.clone();
259 let handle =
260 spawn_tokio!(async move { encryption.request_user_identity(&user_id_clone).await });
261
262 match handle.await.expect("task was not aborted") {
263 Ok(identity) => identity,
264 Err(error) => {
265 error!("Could not request remote crypto identity: {error}");
266 None
267 }
268 }
269 }
270
271 pub(crate) async fn verify_identity(&self) -> Result<IdentityVerification, ()> {
273 self.session()
274 .verification_list()
275 .create(Some(self.clone()))
276 .await
277 }
278
279 pub(crate) fn direct_chat(&self) -> Option<Room> {
284 self.session().room_list().direct_chat(self.user_id())
285 }
286
287 pub(crate) async fn get_or_create_direct_chat(&self) -> Result<Room, ()> {
291 let user_id = self.user_id();
292
293 if let Some(room) = self.direct_chat() {
294 debug!("Using existing direct chat with {user_id}…");
295 return Ok(room);
296 }
297
298 debug!("Creating direct chat with {user_id}…");
299 self.imp().create_direct_chat().await.map_err(|_| ())
300 }
301
302 pub(crate) async fn ignore(&self) -> Result<(), ()> {
304 self.session().ignored_users().add(self.user_id()).await
305 }
306
307 pub(crate) async fn stop_ignoring(&self) -> Result<(), ()> {
309 self.session().ignored_users().remove(self.user_id()).await
310 }
311}
312
313pub trait UserExt: IsA<User> {
314 fn session(&self) -> Session {
316 self.upcast_ref().session()
317 }
318
319 fn user_id(&self) -> &OwnedUserId {
321 self.upcast_ref().imp().user_id()
322 }
323
324 fn is_own_user(&self) -> bool {
326 self.upcast_ref().is_own_user()
327 }
328
329 fn set_name(&self, name: Option<String>) {
331 let user = self.upcast_ref();
332
333 let display_name = if let Some(name) = name.filter(|n| !n.is_empty()) {
334 name
335 } else {
336 user.user_id().localpart().to_owned()
337 };
338
339 user.set_display_name(display_name);
340 }
341
342 fn set_avatar_url(&self, uri: Option<OwnedMxcUri>) {
344 self.upcast_ref()
345 .avatar_data()
346 .image()
347 .expect("avatar data should have an image")
348 .set_uri_and_info(uri, None);
350 }
351
352 fn matrix_to_uri(&self) -> MatrixToUri {
354 self.user_id().matrix_to_uri()
355 }
356
357 async fn load_profile(&self) -> Result<(), ()> {
361 let user_id = self.user_id();
362
363 let client = self.session().client();
364 let user_id_clone = user_id.clone();
365 let handle =
366 spawn_tokio!(
367 async move { client.account().fetch_user_profile_of(&user_id_clone).await }
368 );
369
370 match handle.await.expect("task was not aborted") {
371 Ok(response) => {
372 let user = self.upcast_ref::<User>();
373
374 user.set_name(response.displayname);
375 user.set_avatar_url(response.avatar_url);
376 Ok(())
377 }
378 Err(error) => {
379 error!("Could not load user profile for `{user_id}`: {error}");
380 Err(())
381 }
382 }
383 }
384
385 fn is_ignored(&self) -> bool {
387 self.upcast_ref().is_ignored()
388 }
389
390 fn connect_is_ignored_notify<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
392 self.upcast_ref().connect_is_ignored_notify(move |user| {
393 f(user
394 .downcast_ref()
395 .expect("downcasting to own type should succeed"));
396 })
397 }
398}
399
400impl<T: IsA<PillSource> + IsA<User>> UserExt for T {}
401
402unsafe impl<T> IsSubclassable<T> for User
403where
404 T: PillSourceImpl,
405 T::Type: IsA<PillSource>,
406{
407 fn class_init(class: &mut glib::Class<Self>) {
408 <glib::Object as IsSubclassable<T>>::class_init(class.upcast_ref_mut());
409 }
410
411 fn instance_init(instance: &mut glib::subclass::InitializingObject<T>) {
412 <glib::Object as IsSubclassable<T>>::instance_init(instance);
413 }
414}