1use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
2use matrix_sdk::encryption::identities::UserIdentity;
3use ruma::{
4 MatrixToUri, OwnedMxcUri, OwnedUserId,
5 api::client::profile::{AvatarUrl, DisplayName},
6};
7use tracing::{debug, error};
8
9use super::{IdentityVerification, Room, Session};
10use crate::{
11 components::{AvatarImage, AvatarUriSource, PillSource},
12 prelude::*,
13 spawn, spawn_tokio,
14};
15
16#[glib::flags(name = "UserActions")]
17pub enum UserActions {
18 VERIFY = 0b0000_0001,
19}
20
21impl Default for UserActions {
22 fn default() -> Self {
23 Self::empty()
24 }
25}
26
27mod imp {
28 use std::{
29 cell::{Cell, OnceCell, RefCell},
30 marker::PhantomData,
31 };
32
33 use super::*;
34
35 #[derive(Debug, Default, glib::Properties)]
36 #[properties(wrapper_type = super::User)]
37 pub struct User {
38 user_id: OnceCell<OwnedUserId>,
40 #[property(get = Self::user_id_string)]
42 user_id_string: PhantomData<String>,
43 #[property(get, construct_only)]
45 session: OnceCell<Session>,
46 #[property(get)]
48 is_own_user: Cell<bool>,
49 #[property(get)]
54 pub(super) has_display_name: Cell<bool>,
55 #[property(get)]
57 is_verified: Cell<bool>,
58 #[property(get)]
60 is_ignored: Cell<bool>,
61 ignored_handler: RefCell<Option<glib::SignalHandlerId>>,
62 }
63
64 #[glib::object_subclass]
65 impl ObjectSubclass for User {
66 const NAME: &'static str = "User";
67 type Type = super::User;
68 type ParentType = PillSource;
69 }
70
71 #[glib::derived_properties]
72 impl ObjectImpl for User {
73 fn constructed(&self) {
74 self.parent_constructed();
75 let obj = self.obj();
76
77 let avatar_image = AvatarImage::new(&obj.session(), AvatarUriSource::User, None, None);
78 obj.avatar_data().set_image(Some(avatar_image));
79 }
80
81 fn dispose(&self) {
82 if let Some(session) = self.session.get()
83 && let Some(handler) = self.ignored_handler.take()
84 {
85 session.ignored_users().disconnect(handler);
86 }
87 }
88 }
89
90 impl PillSourceImpl for User {
91 fn identifier(&self) -> String {
92 self.user_id_string()
93 }
94 }
95
96 impl User {
97 pub(super) fn user_id(&self) -> &OwnedUserId {
99 self.user_id.get().expect("user ID should be initialized")
100 }
101
102 fn user_id_string(&self) -> String {
104 self.user_id().to_string()
105 }
106
107 fn session(&self) -> &Session {
109 self.session.get().expect("session should be initialized")
110 }
111
112 pub(crate) fn set_user_id(&self, user_id: OwnedUserId) {
114 let user_id = self.user_id.get_or_init(|| user_id);
115
116 let obj = self.obj();
117 obj.set_name(None);
118 obj.bind_property("display-name", &obj.avatar_data(), "display-name")
119 .sync_create()
120 .build();
121
122 let session = self.session();
123 self.is_own_user.set(session.user_id() == user_id);
124
125 let ignored_users = session.ignored_users();
126 let ignored_handler = ignored_users.connect_items_changed(clone!(
127 #[weak(rename_to = imp)]
128 self,
129 move |ignored_users, _, _, _| {
130 let user_id = imp.user_id.get().expect("user ID is initialized");
131 let is_ignored = ignored_users.contains(user_id);
132
133 if imp.is_ignored.get() != is_ignored {
134 imp.is_ignored.set(is_ignored);
135 imp.obj().notify_is_ignored();
136 }
137 }
138 ));
139 self.is_ignored.set(ignored_users.contains(user_id));
140 self.ignored_handler.replace(Some(ignored_handler));
141
142 spawn!(clone!(
143 #[weak(rename_to = imp)]
144 self,
145 async move {
146 imp.init_is_verified().await;
147 }
148 ));
149 }
150
151 pub(super) fn set_has_display_name(&self, has_display_name: bool) {
153 if self.has_display_name.get() == has_display_name {
154 return;
155 }
156
157 self.has_display_name.set(has_display_name);
158 self.obj().notify_has_display_name();
159 }
160
161 pub(super) async fn local_crypto_identity(&self) -> Option<UserIdentity> {
167 let encryption = self.session().client().encryption();
168 let user_id = self.user_id().clone();
169 let handle = spawn_tokio!(async move { encryption.get_user_identity(&user_id).await });
170
171 match handle.await.expect("task was not aborted") {
172 Ok(identity) => identity,
173 Err(error) => {
174 error!("Could not get local crypto identity: {error}");
175 None
176 }
177 }
178 }
179
180 async fn init_is_verified(&self) {
182 let is_verified = self
184 .local_crypto_identity()
185 .await
186 .is_some_and(|i| i.is_verified());
187
188 if self.is_verified.get() == is_verified {
189 return;
190 }
191
192 self.is_verified.set(is_verified);
193 self.obj().notify_is_verified();
194 }
195
196 pub(super) async fn create_direct_chat(&self) -> Result<Room, matrix_sdk::Error> {
198 let user_id = self.user_id().clone();
199 let client = self.session().client();
200 let handle = spawn_tokio!(async move { client.create_dm(&user_id).await });
201
202 match handle.await.expect("task was not aborted") {
203 Ok(matrix_room) => {
204 let room = self
205 .session()
206 .room_list()
207 .get_wait(matrix_room.room_id(), None)
208 .await
209 .expect("The newly created room was not found");
210 Ok(room)
211 }
212 Err(error) => {
213 error!("Could not create direct chat: {error}");
214 Err(error)
215 }
216 }
217 }
218 }
219}
220
221glib::wrapper! {
222 pub struct User(ObjectSubclass<imp::User>) @extends PillSource;
224}
225
226impl User {
227 pub fn new(session: &Session, user_id: OwnedUserId) -> Self {
229 let obj = glib::Object::builder::<Self>()
230 .property("session", session)
231 .build();
232
233 obj.imp().set_user_id(user_id);
234 obj
235 }
236
237 pub(crate) async fn ensure_crypto_identity(&self) -> Option<UserIdentity> {
244 let session = self.session();
245 let encryption = session.client().encryption();
246 let user_id = self.user_id();
247
248 let should_have_local = if user_id == session.user_id() {
253 true
254 } else {
255 let encryption_clone = encryption.clone();
257 let handle = spawn_tokio!(async move { encryption_clone.tracked_users().await });
258
259 match handle.await.expect("task was not aborted") {
260 Ok(tracked_users) => tracked_users.contains(user_id),
261 Err(error) => {
262 error!("Could not get tracked users: {error}");
263 true
265 }
266 }
267 };
268
269 if should_have_local && let Some(identity) = self.imp().local_crypto_identity().await {
271 return Some(identity);
272 }
273
274 let user_id_clone = user_id.clone();
276 let handle =
277 spawn_tokio!(async move { encryption.request_user_identity(&user_id_clone).await });
278
279 match handle.await.expect("task was not aborted") {
280 Ok(identity) => identity,
281 Err(error) => {
282 error!("Could not request remote crypto identity: {error}");
283 None
284 }
285 }
286 }
287
288 pub(crate) async fn verify_identity(&self) -> Result<IdentityVerification, ()> {
290 self.session()
291 .verification_list()
292 .create(Some(self.clone()))
293 .await
294 }
295
296 pub(crate) fn direct_chat(&self) -> Option<Room> {
301 self.session().room_list().direct_chat(self.user_id())
302 }
303
304 pub(crate) async fn get_or_create_direct_chat(&self) -> Result<Room, ()> {
308 let user_id = self.user_id();
309
310 if let Some(room) = self.direct_chat() {
311 debug!("Using existing direct chat with {user_id}…");
312 return Ok(room);
313 }
314
315 debug!("Creating direct chat with {user_id}…");
316 self.imp().create_direct_chat().await.map_err(|_| ())
317 }
318
319 pub(crate) async fn ignore(&self) -> Result<(), ()> {
321 self.session().ignored_users().add(self.user_id()).await
322 }
323
324 pub(crate) async fn stop_ignoring(&self) -> Result<(), ()> {
326 self.session().ignored_users().remove(self.user_id()).await
327 }
328}
329
330pub trait UserExt: IsA<User> {
331 fn session(&self) -> Session {
333 self.upcast_ref().session()
334 }
335
336 fn user_id(&self) -> &OwnedUserId {
338 self.upcast_ref().imp().user_id()
339 }
340
341 fn is_own_user(&self) -> bool {
343 self.upcast_ref().is_own_user()
344 }
345
346 fn set_name(&self, name: Option<String>) {
348 let user = self.upcast_ref();
349 let name = name.into_clean_string();
350
351 user.imp().set_has_display_name(name.is_some());
352
353 let display_name = name.unwrap_or_else(|| user.user_id().localpart().to_owned());
354 user.set_display_name(display_name);
355 }
356
357 fn has_display_name(&self) -> bool {
362 self.upcast_ref().imp().has_display_name.get()
363 }
364
365 fn set_avatar_url(&self, uri: Option<OwnedMxcUri>) {
367 self.upcast_ref()
368 .avatar_data()
369 .image()
370 .expect("avatar data should have an image")
371 .set_uri_and_info(uri, None);
373 }
374
375 fn matrix_to_uri(&self) -> MatrixToUri {
377 self.user_id().matrix_to_uri()
378 }
379
380 async fn load_profile(&self) -> Result<(), ()> {
384 let user_id = self.user_id();
385
386 let client = self.session().client();
387 let user_id_clone = user_id.clone();
388 let handle =
389 spawn_tokio!(
390 async move { client.account().fetch_user_profile_of(&user_id_clone).await }
391 );
392
393 match handle.await.expect("task was not aborted") {
394 Ok(response) => {
395 let user = self.upcast_ref::<User>();
396
397 match response.get_static::<DisplayName>() {
398 Ok(display_name) => user.set_name(display_name),
399 Err(error) => {
400 error!(%user_id, "Could not deserialize user display name: {error}");
401 }
402 }
403
404 match response.get_static::<AvatarUrl>() {
405 Ok(avatar_url) => user.set_avatar_url(avatar_url),
406 Err(error) => {
407 error!(%user_id, "Could not deserialize user avatar URL: {error}");
408 }
409 }
410
411 Ok(())
412 }
413 Err(error) => {
414 error!(%user_id, "Could not load user profile: {error}");
415 Err(())
416 }
417 }
418 }
419
420 fn is_ignored(&self) -> bool {
422 self.upcast_ref().is_ignored()
423 }
424
425 fn connect_is_ignored_notify<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
427 self.upcast_ref().connect_is_ignored_notify(move |user| {
428 f(user
429 .downcast_ref()
430 .expect("downcasting to own type should succeed"));
431 })
432 }
433}
434
435impl<T: IsA<PillSource> + IsA<User>> UserExt for T {}
436
437unsafe impl<T> IsSubclassable<T> for User
438where
439 T: PillSourceImpl,
440 T::Type: IsA<PillSource>,
441{
442 fn class_init(class: &mut glib::Class<Self>) {
443 <glib::Object as IsSubclassable<T>>::class_init(class.upcast_ref_mut());
444 }
445
446 fn instance_init(instance: &mut glib::subclass::InitializingObject<T>) {
447 <glib::Object as IsSubclassable<T>>::instance_init(instance);
448 }
449}