You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

410 lines
14 KiB

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<Sidebar>,
#[template_child]
pub content: TemplateChild<Content>,
pub homeserver: OnceCell<String>,
/// Contains the error if something went wrong
pub error: RefCell<Option<matrix_sdk::Error>>,
pub client: OnceCell<Client>,
pub rooms: RefCell<HashMap<RoomId, room::Room>>,
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<Self>) {
obj.init_template();
}
}
impl ObjectImpl for Session {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = 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<Vec<Signal>> = 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<imp::Session>)
@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<matrix_sdk::Result<Option<login::Response>>> {
let (sender, receiver) = glib::MainContext::sync_channel::<
matrix_sdk::Result<Option<login::Response>>,
>(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<SyncResponse> {
let (sender, receiver) =
glib::MainContext::sync_channel::<SyncResponse>(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<matrix_sdk::Error> {
let priv_ = &imp::Session::from_instance(self);
priv_.error.take()
}
pub fn connect_ready<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("ready", true, move |values| {
let obj = values[0].get::<Self>().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<RoomId> = {
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
}
}