use std::cmp;
use futures_util::StreamExt;
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use indexmap::{IndexMap, IndexSet};
use matrix_sdk::Client;
use ruma::OwnedUserId;
use tokio::task::AbortHandle;
use tracing::error;
mod user_session;
pub use self::user_session::UserSession;
use self::user_session::UserSessionData;
use super::Session;
use crate::{spawn, spawn_tokio, utils::LoadingState};
mod imp {
use std::{
cell::{Cell, OnceCell, RefCell},
marker::PhantomData,
};
use super::*;
#[derive(Debug, glib::Properties)]
#[properties(wrapper_type = super::UserSessionsList)]
pub struct UserSessionsList {
#[property(get)]
pub session: glib::WeakRef<Session>,
pub user_id: OnceCell<OwnedUserId>,
#[property(get)]
pub other_sessions: gio::ListStore,
#[property(get)]
current_session: RefCell<Option<UserSession>>,
#[property(get, builder(LoadingState::default()))]
pub loading_state: Cell<LoadingState>,
#[property(get = Self::is_empty)]
pub is_empty: PhantomData<bool>,
pub sessions_watch_abort_handle: RefCell<Option<AbortHandle>>,
}
impl Default for UserSessionsList {
fn default() -> Self {
Self {
session: Default::default(),
user_id: Default::default(),
other_sessions: gio::ListStore::new::<UserSession>(),
current_session: Default::default(),
loading_state: Default::default(),
is_empty: Default::default(),
sessions_watch_abort_handle: Default::default(),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for UserSessionsList {
const NAME: &'static str = "UserSessionsList";
type Type = super::UserSessionsList;
}
#[glib::derived_properties]
impl ObjectImpl for UserSessionsList {
fn dispose(&self) {
if let Some(abort_handle) = self.sessions_watch_abort_handle.take() {
abort_handle.abort();
}
}
}
impl UserSessionsList {
pub(super) fn set_current_session(&self, user_session: Option<UserSession>) {
if *self.current_session.borrow() == user_session {
return;
}
let was_empty = self.is_empty();
self.current_session.replace(user_session);
let obj = self.obj();
obj.notify_current_session();
if self.is_empty() != was_empty {
obj.notify_is_empty();
}
}
pub(super) fn set_loading_state(&self, loading_state: LoadingState) {
if self.loading_state.get() == loading_state {
return;
}
self.loading_state.set(loading_state);
self.obj().notify_loading_state();
}
pub(super) fn is_empty(&self) -> bool {
self.current_session.borrow().is_none() && self.other_sessions.n_items() == 0
}
}
}
glib::wrapper! {
pub struct UserSessionsList(ObjectSubclass<imp::UserSessionsList>);
}
impl UserSessionsList {
pub fn new() -> Self {
glib::Object::new()
}
pub fn init(&self, session: &Session, user_id: OwnedUserId) {
let imp = self.imp();
imp.session.set(Some(session));
imp.user_id.set(user_id).unwrap();
spawn!(clone!(
#[weak(rename_to = obj)]
self,
async move {
obj.load().await;
}
));
spawn!(clone!(
#[weak(rename_to = obj)]
self,
#[weak]
session,
async move {
obj.init_sessions_watch(session.client()).await;
}
));
}
async fn init_sessions_watch(&self, client: Client) {
let stream = match client.encryption().devices_stream().await {
Ok(stream) => stream,
Err(error) => {
error!("Could not access the user sessions stream: {error}");
return;
}
};
let obj_weak = glib::SendWeakRef::from(self.downgrade());
let user_id = self.user_id().clone();
let fut = stream.for_each(move |updates| {
let user_id = user_id.clone();
let obj_weak = obj_weak.clone();
async move {
if !updates.new.contains_key(&user_id) && !updates.changed.contains_key(&user_id) {
return;
}
let ctx = glib::MainContext::default();
ctx.spawn(async move {
spawn!(async move {
if let Some(obj) = obj_weak.upgrade() {
obj.load().await;
}
});
});
}
});
let abort_handle = spawn_tokio!(fut).abort_handle();
self.imp()
.sessions_watch_abort_handle
.replace(Some(abort_handle));
}
pub fn user_id(&self) -> &OwnedUserId {
self.imp().user_id.get().unwrap()
}
pub async fn load(&self) {
if self.loading_state() == LoadingState::Loading {
return;
}
let Some(session) = self.session() else {
return;
};
let imp = self.imp();
imp.set_loading_state(LoadingState::Loading);
let user_id = self.user_id().clone();
let client = session.client();
let handle = spawn_tokio!(async move {
let crypto_sessions = match client.encryption().get_user_devices(&user_id).await {
Ok(crypto_sessions) => Some(crypto_sessions),
Err(error) => {
error!("Could not get crypto sessions for user {user_id}: {error}");
None
}
};
let is_own_user = client.user_id().unwrap() == user_id;
let mut api_sessions = None;
if is_own_user {
match client.devices().await {
Ok(response) => {
api_sessions = Some(response.devices);
}
Err(error) => {
error!("Could not get sessions list for user {user_id}: {error}");
}
}
}
(api_sessions, crypto_sessions)
});
let (api_sessions, crypto_sessions) = handle.await.unwrap();
if api_sessions.is_none() && crypto_sessions.is_none() {
imp.set_loading_state(LoadingState::Error);
return;
};
let mut api_sessions = api_sessions
.into_iter()
.flatten()
.map(|d| (d.device_id.clone(), d))
.collect::<IndexMap<_, _>>();
api_sessions.sort_by(|_key_a, val_a, _key_b, val_b| {
match val_b.last_seen_ts.cmp(&val_a.last_seen_ts) {
cmp::Ordering::Equal => val_a.device_id.cmp(&val_b.device_id),
cmp => cmp,
}
});
let ids = api_sessions
.keys()
.cloned()
.chain(
crypto_sessions
.iter()
.flat_map(|s| s.keys())
.map(ToOwned::to_owned),
)
.collect::<IndexSet<_>>();
let (current, others) = ids
.into_iter()
.filter_map(|id| {
let data = match (
api_sessions.shift_remove(&id),
crypto_sessions.as_ref().and_then(|s| s.get(&id)),
) {
(Some(api), Some(crypto)) => UserSessionData::Both { api, crypto },
(Some(api), None) => UserSessionData::DevicesApi(api),
(None, Some(crypto)) => UserSessionData::Crypto(crypto),
_ => return None,
};
Some(UserSession::new(&session, data))
})
.partition::<Vec<_>, _>(|s| s.is_current());
if let Some(current) = current.into_iter().next() {
imp.set_current_session(Some(current));
}
let was_empty = imp.is_empty();
let removed = imp.other_sessions.n_items();
imp.other_sessions.splice(0, removed, &others);
if imp.is_empty() != was_empty {
self.notify_is_empty();
}
imp.set_loading_state(LoadingState::Ready);
}
}
impl Default for UserSessionsList {
fn default() -> Self {
Self::new()
}
}