mod categories; mod content; mod room; mod sidebar; mod user; use self::content::Content; use self::sidebar::Sidebar; use self::user::User; use crate::event_from_sync_event; use crate::secret; use crate::RUNTIME; use adw; use adw::subclass::prelude::BinImpl; use gtk::subclass::prelude::*; use gtk::{self, prelude::*}; use gtk::{glib, glib::clone, glib::SyncSender, CompositeTemplate}; use gtk_macros::send; use log::{error, warn}; use matrix_sdk::api::r0::{ filter::{FilterDefinition, RoomFilter}, session::login, }; use matrix_sdk::{ self, deserialized_responses::SyncResponse, events::{AnyRoomEvent, AnySyncRoomEvent}, identifiers::RoomId, Client, ClientConfig, RequestConfig, SyncSettings, }; use std::time::Duration; use crate::session::categories::Categories; mod imp { use super::*; use glib::subclass::{InitializingObject, Signal}; use once_cell::sync::{Lazy, OnceCell}; use std::cell::RefCell; use std::collections::HashMap; #[derive(Debug, Default, CompositeTemplate)] #[template(resource = "/org/gnome/FractalNext/session.ui")] pub struct Session { #[template_child] pub sidebar: TemplateChild, #[template_child] pub content: TemplateChild, pub homeserver: OnceCell, /// Contains the error if something went wrong pub error: RefCell>, pub client: OnceCell, pub rooms: RefCell>, pub categories: Categories, } #[glib::object_subclass] impl ObjectSubclass for Session { const NAME: &'static str = "Session"; type Type = super::Session; type ParentType = adw::Bin; fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); klass.install_action( "session.show-room", Some("s"), move |widget, _, parameter| { use std::convert::TryInto; if let Some(room_id) = parameter .and_then(|p| p.str()) .and_then(|s| s.try_into().ok()) { widget.handle_show_room_action(room_id); } else { warn!("Not a valid room id: {:?}", parameter); } }, ); } fn instance_init(obj: &InitializingObject) { obj.init_template(); } } impl ObjectImpl for Session { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpec::new_string( "homeserver", "Homeserver", "The matrix homeserver of this session", None, glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY, ), glib::ParamSpec::new_object( "categories", "Categories", "A list of rooms grouped into categories", Categories::static_type(), glib::ParamFlags::READABLE, ), ] }); PROPERTIES.as_ref() } fn set_property( &self, _obj: &Self::Type, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec, ) { match pspec.name() { "homeserver" => { let homeserver = value .get() .expect("type conformity checked by `Object::set_property`"); let _ = self.homeserver.set(homeserver); } _ => unimplemented!(), } } fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { "homeserver" => self.homeserver.get().to_value(), "categories" => self.categories.to_value(), _ => unimplemented!(), } } fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![Signal::builder("ready", &[], <()>::static_type().into()).build()] }); SIGNALS.as_ref() } fn constructed(&self, obj: &Self::Type) { self.parent_constructed(obj); } } impl WidgetImpl for Session {} impl BinImpl for Session {} } /// Enum containing the supported methods to create a `Session`. #[derive(Clone, Debug)] enum CreationMethod { /// Restore a previous session: `matrix_sdk::Session` SessionRestore(matrix_sdk::Session), /// Password Login: `username`, 'password` Password(String, String), } glib::wrapper! { pub struct Session(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } impl Session { pub fn new(homeserver: String) -> Self { glib::Object::new(&[("homeserver", &homeserver)]).expect("Failed to create Session") } pub fn login_with_password(&self, username: String, password: String) { let method = CreationMethod::Password(username, password); self.login(method); } pub fn login_with_previous_session(&self, session: matrix_sdk::Session) { let method = CreationMethod::SessionRestore(session); self.login(method); } fn login(&self, method: CreationMethod) { let priv_ = imp::Session::from_instance(self); let homeserver = priv_.homeserver.get().unwrap(); let sender = self.setup(); let config = ClientConfig::new().request_config(RequestConfig::new().retry_limit(2)); // Please note the homeserver needs to be a valid url or the client will panic! let client = Client::new_with_config(homeserver.as_str(), config); if let Err(error) = client { send!(sender, Err(error)); return; } let client = client.unwrap(); priv_.client.set(client.clone()).unwrap(); let room_sender = self.create_new_sync_response_sender(); RUNTIME.spawn(async move { let success = match method { CreationMethod::SessionRestore(session) => { let res = client.restore_login(session).await; let success = res.is_ok(); send!(sender, res.map(|_| None)); success } CreationMethod::Password(username, password) => { let response = client .login(&username, &password, None, Some("Fractal Next")) .await; let success = response.is_ok(); send!(sender, response.map(|r| Some(r))); success } }; if success { // We need the filter or else left rooms won't be shown let mut room_filter = RoomFilter::empty(); room_filter.include_leave = true; let mut filter = FilterDefinition::empty(); filter.room = room_filter; let sync_settings = SyncSettings::new() .timeout(Duration::from_secs(30)) .filter(filter.into()); client .sync_with_callback(sync_settings, |response| { let room_sender = room_sender.clone(); async move { // Using the event hanlder doesn't make a lot of sense for us since we want every room event // Eventually we should contribute a better EventHandler interface so that it makes sense to use it. room_sender.send(response).unwrap(); matrix_sdk::LoopCtrl::Continue } }) .await; } }); } fn setup(&self) -> glib::SyncSender>> { let (sender, receiver) = glib::MainContext::sync_channel::< matrix_sdk::Result>, >(Default::default(), 100); receiver.attach( None, clone!(@weak self as obj => @default-return glib::Continue(false), move |result| { match result { Err(error) => { let priv_ = &imp::Session::from_instance(&obj); priv_.error.replace(Some(error)); } Ok(Some(response)) => { let session = matrix_sdk::Session { access_token: response.access_token, user_id: response.user_id, device_id: response.device_id, }; //TODO: set error to this error obj.store_session(session).unwrap(); } Ok(None) => {} } obj.load(); obj.emit_by_name("ready", &[]).unwrap(); glib::Continue(false) }), ); sender } /// Sets up the required channel to receive new room events fn create_new_sync_response_sender(&self) -> SyncSender { let (sender, receiver) = glib::MainContext::sync_channel::(Default::default(), 100); receiver.attach( None, clone!(@weak self as obj => @default-return glib::Continue(false), move |response| { obj.handle_sync_reposne(response); glib::Continue(true) }), ); sender } /// Loads the state from the `Store` /// Note that the `Store` currently doesn't store all events, therefore, we arn't really /// loading much via this function. pub fn load(&self) { // TODO: load rooms from the store before the sync completes } /// Returns and consumes the `error` that was generated when the session failed to login, /// on a successful login this will be `None`. /// Unfortunatly it's not possible to connect the Error direclty to the `ready` signals. pub fn get_error(&self) -> Option { let priv_ = &imp::Session::from_instance(self); priv_.error.take() } pub fn connect_ready(&self, f: F) -> glib::SignalHandlerId { self.connect_local("ready", true, move |values| { let obj = values[0].get::().unwrap(); f(&obj); None }) .unwrap() } fn store_session(&self, session: matrix_sdk::Session) -> Result<(), secret_service::Error> { let priv_ = &imp::Session::from_instance(self); let homeserver = priv_.homeserver.get().unwrap(); secret::store_session(homeserver, session) } fn handle_show_room_action(&self, room_id: RoomId) { let priv_ = imp::Session::from_instance(self); if let Some(room) = priv_.rooms.borrow().get(&room_id) { priv_.content.set_room(room); } else { warn!("No room with {} was found", room_id); } } fn handle_sync_reposne(&self, response: SyncResponse) { let priv_ = imp::Session::from_instance(self); let new_rooms_id: Vec = { let rooms_map = priv_.rooms.borrow(); let new_joined_rooms = response.rooms.leave.iter().filter_map(|(room_id, _)| { if rooms_map.contains_key(room_id) { Some(room_id) } else { None } }); let new_left_rooms = response.rooms.join.iter().filter_map(|(room_id, _)| { if rooms_map.contains_key(room_id) { Some(room_id) } else { None } }); new_joined_rooms.chain(new_left_rooms).cloned().collect() }; let mut new_rooms = Vec::new(); let mut rooms_map = priv_.rooms.borrow_mut(); for room_id in new_rooms_id { if let Some(matrix_room) = priv_.client.get().unwrap().get_room(&room_id) { let room = room::Room::new(matrix_room); rooms_map.insert(room_id.clone(), room.clone()); new_rooms.push(room.clone()); } } priv_.categories.append(new_rooms); for (room_id, matrix_room) in response.rooms.leave { if matrix_room.timeline.events.is_empty() { continue; } if let Some(room) = rooms_map.get(&room_id) { room.append_events( matrix_room .timeline .events .into_iter() .map(|event| event_from_sync_event!(event, room_id)) .collect(), ); } } for (room_id, matrix_room) in response.rooms.join { if matrix_room.timeline.events.is_empty() { continue; } if let Some(room) = rooms_map.get(&room_id) { room.append_events( matrix_room .timeline .events .into_iter() .map(|event| event_from_sync_event!(event, room_id)) .collect(), ); } } // TODO: handle StrippedStateEvents for invited rooms } }