fractal/session/user_sessions_list/
user_session.rs1use gettextrs::gettext;
2use gtk::{
3 glib,
4 glib::{clone, closure_local},
5 prelude::*,
6 subclass::prelude::*,
7};
8use matrix_sdk::{HttpError, encryption::identities::Device as CryptoDevice};
9use ruma::{DeviceId, OwnedDeviceId, api::client::device::Device as DeviceData};
10use tracing::{debug, error};
11
12use crate::{
13 Application,
14 components::{AuthDialog, AuthError},
15 prelude::*,
16 session::Session,
17 spawn_tokio,
18 system_settings::ClockFormat,
19 utils::matrix::timestamp_to_date,
20};
21
22#[derive(Debug, Clone)]
24pub(super) enum UserSessionData {
25 DevicesApi(DeviceData),
27 Crypto(CryptoDevice),
29 Both {
31 api: DeviceData,
32 crypto: CryptoDevice,
33 },
34}
35
36impl UserSessionData {
37 pub(super) fn device_id(&self) -> &DeviceId {
39 match self {
40 UserSessionData::DevicesApi(api) | UserSessionData::Both { api, .. } => &api.device_id,
41 UserSessionData::Crypto(crypto) => crypto.device_id(),
42 }
43 }
44
45 fn set_display_name(&mut self, name: String) {
47 match self {
48 UserSessionData::DevicesApi(api) | UserSessionData::Both { api, .. } => {
49 api.display_name = Some(name);
50 }
51 UserSessionData::Crypto(crypto) => {
52 *self = UserSessionData::Both {
53 api: DeviceData::new(crypto.device_id().into()),
54 crypto: crypto.to_owned(),
55 }
56 }
57 }
58 }
59
60 fn api(&self) -> Option<&DeviceData> {
62 match self {
63 UserSessionData::DevicesApi(api) | UserSessionData::Both { api, .. } => Some(api),
64 UserSessionData::Crypto(_) => None,
65 }
66 }
67
68 fn crypto(&self) -> Option<&CryptoDevice> {
70 match self {
71 UserSessionData::Crypto(crypto) | UserSessionData::Both { crypto, .. } => Some(crypto),
72 UserSessionData::DevicesApi(_) => None,
73 }
74 }
75}
76
77mod imp {
78 use std::{
79 cell::{Cell, OnceCell, RefCell},
80 marker::PhantomData,
81 sync::LazyLock,
82 };
83
84 use glib::subclass::Signal;
85
86 use super::*;
87
88 #[derive(Debug, Default, glib::Properties)]
89 #[properties(wrapper_type = super::UserSession)]
90 pub struct UserSession {
91 #[property(get, construct_only)]
93 session: glib::WeakRef<Session>,
94 device_id: OnceCell<OwnedDeviceId>,
96 data: RefCell<Option<UserSessionData>>,
98 #[property(get)]
100 is_current: Cell<bool>,
101 #[property(get = Self::device_id_string)]
103 device_id_string: PhantomData<String>,
104 #[property(get = Self::display_name)]
106 display_name: PhantomData<String>,
107 #[property(get = Self::display_name_or_device_id)]
109 display_name_or_device_id: PhantomData<String>,
110 #[property(get = Self::last_seen_ip)]
112 last_seen_ip: PhantomData<Option<String>>,
113 #[property(get = Self::last_seen_ts)]
116 last_seen_ts: PhantomData<u64>,
117 #[property(get = Self::last_seen_datetime)]
119 last_seen_datetime: PhantomData<Option<glib::DateTime>>,
120 #[property(get = Self::last_seen_datetime_string)]
122 last_seen_datetime_string: PhantomData<Option<String>>,
123 #[property(get = Self::verified)]
125 verified: PhantomData<bool>,
126 system_settings_handler: RefCell<Option<glib::SignalHandlerId>>,
127 }
128
129 #[glib::object_subclass]
130 impl ObjectSubclass for UserSession {
131 const NAME: &'static str = "UserSession";
132 type Type = super::UserSession;
133 }
134
135 #[glib::derived_properties]
136 impl ObjectImpl for UserSession {
137 fn constructed(&self) {
138 self.parent_constructed();
139
140 let obj = self.obj();
141 let system_settings = Application::default().system_settings();
142 let system_settings_handler = system_settings.connect_clock_format_notify(clone!(
143 #[weak]
144 obj,
145 move |_| {
146 obj.notify_last_seen_datetime_string();
147 }
148 ));
149 self.system_settings_handler
150 .replace(Some(system_settings_handler));
151 }
152
153 fn dispose(&self) {
154 if let Some(handler) = self.system_settings_handler.take() {
155 Application::default().system_settings().disconnect(handler);
156 }
157 }
158
159 fn signals() -> &'static [Signal] {
160 static SIGNALS: LazyLock<Vec<Signal>> =
161 LazyLock::new(|| vec![Signal::builder("disconnected").build()]);
162 SIGNALS.as_ref()
163 }
164 }
165
166 impl UserSession {
167 pub(super) fn set_device_id(&self, device_id: OwnedDeviceId) {
169 let device_id = self.device_id.get_or_init(|| device_id);
170
171 if let Some(session) = self.session.upgrade() {
172 let is_current = session.device_id() == device_id;
173 self.is_current.set(is_current);
174 }
175 }
176
177 pub(super) fn device_id(&self) -> &OwnedDeviceId {
179 self.device_id
180 .get()
181 .expect("device ID should be initialized")
182 }
183
184 pub(super) fn set_data(&self, data: UserSessionData) {
186 let old_display_name = self.display_name();
187 let old_last_seen_ip = self.last_seen_ip();
188 let old_last_seen_ts = self.last_seen_ts();
189 let old_verified = self.verified();
190
191 self.data.replace(Some(data));
192
193 let obj = self.obj();
194 if self.display_name() != old_display_name {
195 obj.notify_display_name();
196 obj.notify_display_name_or_device_id();
197 }
198 if self.last_seen_ip() != old_last_seen_ip {
199 obj.notify_last_seen_ip();
200 }
201 if self.last_seen_ts() != old_last_seen_ts {
202 obj.notify_last_seen_ts();
203 obj.notify_last_seen_datetime();
204 obj.notify_last_seen_datetime_string();
205 }
206 if self.verified() != old_verified {
207 obj.notify_verified();
208 }
209 }
210
211 fn device_id_string(&self) -> String {
213 self.device_id().to_string()
214 }
215
216 fn display_name(&self) -> String {
218 self.data
219 .borrow()
220 .as_ref()
221 .and_then(UserSessionData::api)
222 .and_then(|d| d.display_name.clone())
223 .unwrap_or_default()
224 }
225
226 pub(super) fn set_display_name(&self, name: String) {
228 if let Some(data) = &mut *self.data.borrow_mut() {
229 data.set_display_name(name);
230 }
231
232 self.obj().notify_display_name();
233 self.obj().notify_display_name_or_device_id();
234 }
235
236 fn display_name_or_device_id(&self) -> String {
238 if let Some(display_name) = self
239 .data
240 .borrow()
241 .as_ref()
242 .and_then(UserSessionData::api)
243 .and_then(|d| d.display_name.as_ref().map(|s| s.trim()))
244 .filter(|s| !s.is_empty())
245 .map(ToOwned::to_owned)
246 {
247 display_name
248 } else {
249 self.device_id_string()
250 }
251 }
252
253 fn last_seen_ip(&self) -> Option<String> {
255 self.data.borrow().as_ref()?.api()?.last_seen_ip.clone()
256 }
257
258 fn last_seen_ts(&self) -> u64 {
263 self.data
264 .borrow()
265 .as_ref()
266 .and_then(UserSessionData::api)
267 .and_then(|s| s.last_seen_ts)
268 .map(|ts| ts.0.into())
269 .unwrap_or_default()
270 }
271
272 fn last_seen_datetime(&self) -> Option<glib::DateTime> {
274 self.data
275 .borrow()
276 .as_ref()?
277 .api()?
278 .last_seen_ts
279 .map(timestamp_to_date)
280 }
281
282 pub(super) fn last_seen_datetime_string(&self) -> Option<String> {
285 let datetime = self.last_seen_datetime()?;
286
287 let clock_format = Application::default().system_settings().clock_format();
288 let use_24 = clock_format == ClockFormat::TwentyFourHours;
289
290 let now = glib::DateTime::now_local().unwrap();
293 let format;
294 let days_ago = {
295 let today_midnight = glib::DateTime::from_local(
296 now.year(),
297 now.month(),
298 now.day_of_month(),
299 0,
300 0,
301 0f64,
302 )
303 .expect("constructing GDateTime works");
304
305 let date = glib::DateTime::from_local(
306 datetime.year(),
307 datetime.month(),
308 datetime.day_of_month(),
309 0,
310 0,
311 0f64,
312 )
313 .expect("constructing GDateTime works");
314
315 today_midnight.difference(&date).as_days()
316 };
317
318 if days_ago == 0 {
320 if use_24 {
321 format = gettext("Last seen at %H:%M");
325 } else {
326 format = gettext("Last seen at %I:%M %p");
330 }
331 }
332 else if days_ago == 1 {
334 if use_24 {
335 format = gettext("Last seen yesterday at %H:%M");
340 } else {
341 format = gettext("Last seen yesterday at %I:%M %p");
347 }
348 }
349 else if days_ago > 1 && days_ago < 7 {
351 if use_24 {
352 format = gettext("Last seen %A at %H:%M");
358 } else {
359 format = gettext("Last seen %A at %I:%M %p");
365 }
366 } else if datetime.year() == now.year() {
367 if use_24 {
368 format = gettext("Last seen %B %-e at %H:%M");
374 } else {
375 format = gettext("Last seen %B %-e at %I:%M %p");
381 }
382 } else if use_24 {
383 format = gettext("Last seen %B %-e %Y at %H:%M");
389 } else {
390 format = gettext("Last seen %B %-e %Y at %I:%M %p");
396 }
397
398 Some(
399 datetime
400 .format(&format)
401 .expect("formatting GDateTime works")
402 .into(),
403 )
404 }
405
406 fn verified(&self) -> bool {
408 self.data
409 .borrow()
410 .as_ref()
411 .and_then(UserSessionData::crypto)
412 .is_some_and(CryptoDevice::is_verified)
413 }
414 }
415}
416
417glib::wrapper! {
418 pub struct UserSession(ObjectSubclass<imp::UserSession>);
420}
421
422impl UserSession {
423 pub(super) fn new(session: &Session, device_id: OwnedDeviceId) -> Self {
424 let obj = glib::Object::builder::<Self>()
425 .property("session", session)
426 .build();
427
428 obj.imp().set_device_id(device_id);
429
430 obj
431 }
432
433 pub(crate) fn device_id(&self) -> &OwnedDeviceId {
435 self.imp().device_id()
436 }
437
438 pub(super) fn set_data(&self, data: UserSessionData) {
440 self.imp().set_data(data);
441 }
442
443 pub(crate) async fn rename(&self, display_name: String) -> Result<(), HttpError> {
445 let Some(client) = self.session().map(|s| s.client()) else {
446 return Ok(());
447 };
448 let device_id = self.imp().device_id().clone();
449
450 let cloned_display_name = display_name.clone();
451 let res =
452 spawn_tokio!(
453 async move { client.rename_device(&device_id, &cloned_display_name).await }
454 )
455 .await
456 .expect("task was not aborted");
457
458 match res {
459 Ok(_) => {
460 self.imp().set_display_name(display_name);
461 Ok(())
462 }
463 Err(error) => {
464 let device_id = self.device_id();
465 error!("Could not rename user session {device_id}: {error}");
466 Err(error)
467 }
468 }
469 }
470
471 pub(crate) async fn delete(&self, parent: &impl IsA<gtk::Widget>) -> Result<(), AuthError> {
475 let Some(session) = self.session() else {
476 return Err(AuthError::Unknown);
477 };
478 let device_id = self.imp().device_id().clone();
479
480 let dialog = AuthDialog::new(&session);
481
482 let res = dialog
483 .authenticate(parent, move |client, auth| {
484 let device_id = device_id.clone();
485 async move {
486 client
487 .delete_devices(&[device_id], auth)
488 .await
489 .map_err(Into::into)
490 }
491 })
492 .await;
493
494 match res {
495 Ok(_) => Ok(()),
496 Err(error) => {
497 let device_id = self.imp().device_id();
498
499 if matches!(error, AuthError::UserCancelled) {
500 debug!("Deletion of user session {device_id} cancelled by user");
501 } else {
502 error!("Could not delete user session {device_id}: {error}");
503 }
504 Err(error)
505 }
506 }
507 }
508
509 pub(super) fn emit_disconnected(&self) {
511 self.emit_by_name::<()>("disconnected", &[]);
512 }
513
514 pub fn connect_disconnected<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
516 self.connect_closure(
517 "disconnected",
518 true,
519 closure_local!(|obj: Self| {
520 f(&obj);
521 }),
522 )
523 }
524}