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.
491 lines
17 KiB
491 lines
17 KiB
mod avatar; |
|
mod content; |
|
mod event_source_dialog; |
|
mod room; |
|
mod room_list; |
|
mod sidebar; |
|
mod user; |
|
|
|
pub use self::avatar::Avatar; |
|
use self::content::Content; |
|
pub use self::room::Room; |
|
use self::room_list::RoomList; |
|
use self::sidebar::Sidebar; |
|
pub use self::user::{User, UserExt}; |
|
|
|
use crate::components::InAppNotification; |
|
use crate::secret; |
|
use crate::secret::StoredSession; |
|
use crate::utils::do_async; |
|
use crate::Error; |
|
use crate::RUNTIME; |
|
|
|
use crate::login::LoginError; |
|
use crate::session::content::ContentType; |
|
use adw::subclass::prelude::BinImpl; |
|
use gtk::subclass::prelude::*; |
|
use gtk::{self, prelude::*}; |
|
use gtk::{gdk, gio, glib, glib::clone, glib::SyncSender, CompositeTemplate, SelectionModel}; |
|
use gtk_macros::send; |
|
use log::error; |
|
use matrix_sdk::ruma::{ |
|
api::client::r0::filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter}, |
|
assign, |
|
}; |
|
use matrix_sdk::{ |
|
config::{ClientConfig, RequestConfig, SyncSettings}, |
|
deserialized_responses::SyncResponse, |
|
uuid::Uuid, |
|
Client, |
|
}; |
|
use rand::{distributions::Alphanumeric, thread_rng, Rng}; |
|
use std::fs; |
|
use std::time::Duration; |
|
use url::Url; |
|
|
|
mod imp { |
|
use super::*; |
|
use glib::subclass::{InitializingObject, Signal}; |
|
use once_cell::sync::{Lazy, OnceCell}; |
|
use std::cell::{Cell, RefCell}; |
|
|
|
#[derive(Debug, Default, CompositeTemplate)] |
|
#[template(resource = "/org/gnome/FractalNext/session.ui")] |
|
pub struct Session { |
|
#[template_child] |
|
pub error_list: TemplateChild<gio::ListStore>, |
|
#[template_child] |
|
pub stack: TemplateChild<gtk::Stack>, |
|
#[template_child] |
|
pub content: TemplateChild<adw::Leaflet>, |
|
#[template_child] |
|
pub sidebar: TemplateChild<Sidebar>, |
|
/// Contains the error if something went wrong |
|
pub error: RefCell<Option<matrix_sdk::Error>>, |
|
pub client: OnceCell<Client>, |
|
pub room_list: OnceCell<RoomList>, |
|
pub user: OnceCell<User>, |
|
pub selected_room: RefCell<Option<Room>>, |
|
pub selected_content_type: Cell<ContentType>, |
|
pub is_ready: OnceCell<bool>, |
|
} |
|
|
|
#[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.close-room", None, move |session, _, _| { |
|
session.set_selected_room(None); |
|
}); |
|
|
|
klass.add_binding_action( |
|
gdk::keys::constants::Escape, |
|
gdk::ModifierType::empty(), |
|
"session.close-room", |
|
None, |
|
); |
|
|
|
klass.install_action("session.toggle-room-search", None, move |session, _, _| { |
|
session.toggle_room_search(); |
|
}); |
|
|
|
klass.add_binding_action( |
|
gdk::keys::constants::k, |
|
gdk::ModifierType::CONTROL_MASK, |
|
"session.toggle-room-search", |
|
None, |
|
); |
|
} |
|
|
|
fn instance_init(obj: &InitializingObject<Self>) { |
|
Sidebar::static_type(); |
|
Content::static_type(); |
|
Error::static_type(); |
|
InAppNotification::static_type(); |
|
obj.init_template(); |
|
} |
|
} |
|
|
|
impl ObjectImpl for Session { |
|
fn properties() -> &'static [glib::ParamSpec] { |
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
|
vec![ |
|
glib::ParamSpec::new_object( |
|
"room-list", |
|
"Room List", |
|
"The list of rooms", |
|
RoomList::static_type(), |
|
glib::ParamFlags::READABLE, |
|
), |
|
glib::ParamSpec::new_object( |
|
"selected-room", |
|
"Selected Room", |
|
"The selected room in this session", |
|
Room::static_type(), |
|
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, |
|
), |
|
glib::ParamSpec::new_enum( |
|
"selected-content-type", |
|
"Selected Content Type", |
|
"The current content type selected", |
|
ContentType::static_type(), |
|
ContentType::default() as i32, |
|
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, |
|
), |
|
glib::ParamSpec::new_object( |
|
"user", |
|
"User", |
|
"The user of this session", |
|
User::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() { |
|
"selected-room" => { |
|
let selected_room = value.get().unwrap(); |
|
obj.set_selected_room(selected_room); |
|
} |
|
"selected-content-type" => obj.set_selected_content_type(value.get().unwrap()), |
|
_ => unimplemented!(), |
|
} |
|
} |
|
|
|
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
|
match pspec.name() { |
|
"room-list" => obj.room_list().to_value(), |
|
"selected-room" => obj.selected_room().to_value(), |
|
"user" => obj.user().to_value(), |
|
"selected-content-type" => obj.selected_content_type().to_value(), |
|
_ => unimplemented!(), |
|
} |
|
} |
|
|
|
fn signals() -> &'static [Signal] { |
|
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| { |
|
vec![Signal::builder("prepared", &[], <()>::static_type().into()).build()] |
|
}); |
|
SIGNALS.as_ref() |
|
} |
|
} |
|
impl WidgetImpl for Session {} |
|
impl BinImpl for Session {} |
|
} |
|
|
|
glib::wrapper! { |
|
pub struct Session(ObjectSubclass<imp::Session>) |
|
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible; |
|
} |
|
|
|
impl Session { |
|
pub fn new() -> Self { |
|
glib::Object::new(&[]).expect("Failed to create Session") |
|
} |
|
|
|
pub fn selected_content_type(&self) -> ContentType { |
|
let priv_ = imp::Session::from_instance(self); |
|
priv_.selected_content_type.get() |
|
} |
|
|
|
pub fn set_selected_content_type(&self, selected_type: ContentType) { |
|
let priv_ = imp::Session::from_instance(self); |
|
|
|
if self.selected_content_type() == selected_type { |
|
return; |
|
} |
|
|
|
if selected_type == ContentType::None { |
|
priv_.content.navigate(adw::NavigationDirection::Back); |
|
} else { |
|
priv_.content.navigate(adw::NavigationDirection::Forward); |
|
} |
|
|
|
priv_.selected_content_type.set(selected_type); |
|
|
|
self.notify("selected-content-type"); |
|
} |
|
|
|
pub fn selected_room(&self) -> Option<Room> { |
|
let priv_ = imp::Session::from_instance(self); |
|
priv_.selected_room.borrow().clone() |
|
} |
|
|
|
pub fn set_selected_room(&self, selected_room: Option<Room>) { |
|
let priv_ = imp::Session::from_instance(self); |
|
|
|
if self.selected_room() == selected_room { |
|
return; |
|
} |
|
|
|
priv_.selected_room.replace(selected_room); |
|
|
|
self.notify("selected-room"); |
|
} |
|
|
|
pub fn login_with_password(&self, homeserver: Url, username: String, password: String) { |
|
let mut path = glib::user_data_dir(); |
|
path.push( |
|
&Uuid::new_v4() |
|
.to_hyphenated() |
|
.encode_lower(&mut Uuid::encode_buffer()), |
|
); |
|
|
|
do_async( |
|
glib::PRIORITY_DEFAULT_IDLE, |
|
async move { |
|
let passphrase: String = { |
|
let mut rng = thread_rng(); |
|
(&mut rng) |
|
.sample_iter(Alphanumeric) |
|
.take(30) |
|
.map(char::from) |
|
.collect() |
|
}; |
|
let config = ClientConfig::new() |
|
.request_config(RequestConfig::new().retry_limit(2)) |
|
.passphrase(passphrase.clone()) |
|
.store_path(path.clone()); |
|
|
|
let client = Client::new_with_config(homeserver.clone(), config).unwrap(); |
|
let response = client |
|
.login(&username, &password, None, Some("Fractal Next")) |
|
.await; |
|
match response { |
|
Ok(response) => Ok(( |
|
client, |
|
StoredSession { |
|
homeserver, |
|
path, |
|
passphrase, |
|
access_token: response.access_token, |
|
user_id: response.user_id, |
|
device_id: response.device_id, |
|
}, |
|
)), |
|
Err(error) => { |
|
// Remove the store created by Client::new() |
|
fs::remove_dir_all(path).unwrap(); |
|
Err(error) |
|
} |
|
} |
|
}, |
|
clone!(@weak self as obj => move |result| async move { |
|
obj.handle_login_result(result, true); |
|
}), |
|
); |
|
} |
|
|
|
fn toggle_room_search(&self) { |
|
let priv_ = imp::Session::from_instance(self); |
|
let room_search = priv_.sidebar.room_search_bar(); |
|
room_search.set_search_mode(!room_search.is_search_mode()); |
|
} |
|
|
|
pub fn login_with_previous_session(&self, session: StoredSession) { |
|
do_async( |
|
glib::PRIORITY_DEFAULT_IDLE, |
|
async move { |
|
let config = ClientConfig::new() |
|
.request_config(RequestConfig::new().retry_limit(2)) |
|
.passphrase(session.passphrase.clone()) |
|
.store_path(session.path.clone()); |
|
|
|
let client = Client::new_with_config(session.homeserver.clone(), config).unwrap(); |
|
client |
|
.restore_login(matrix_sdk::Session { |
|
user_id: session.user_id.clone(), |
|
device_id: session.device_id.clone(), |
|
access_token: session.access_token.clone(), |
|
}) |
|
.await |
|
.map(|_| (client, session)) |
|
}, |
|
clone!(@weak self as obj => move |result| async move { |
|
obj.handle_login_result(result, false); |
|
}), |
|
); |
|
} |
|
|
|
fn handle_login_result( |
|
&self, |
|
result: Result<(Client, StoredSession), matrix_sdk::Error>, |
|
store_session: bool, |
|
) { |
|
let priv_ = imp::Session::from_instance(self); |
|
match result { |
|
Ok((client, session)) => { |
|
priv_.client.set(client.clone()).unwrap(); |
|
let user = User::new(self, &session.user_id); |
|
priv_.user.set(user.clone()).unwrap(); |
|
self.notify("user"); |
|
|
|
do_async( |
|
glib::PRIORITY_LOW, |
|
async move { |
|
let display_name = client.display_name().await?; |
|
let avatar_url = client.avatar_url().await?; |
|
Ok((display_name, avatar_url)) |
|
}, |
|
move |result: matrix_sdk::Result<_>| async move { |
|
match result { |
|
Ok((display_name, avatar_url)) => { |
|
user.set_display_name(display_name); |
|
user.set_avatar_url(avatar_url); |
|
} |
|
Err(error) => error!("Couldn’t fetch account metadata: {}", error), |
|
}; |
|
}, |
|
); |
|
|
|
if store_session { |
|
// TODO: report secret service errors |
|
secret::store_session(session).unwrap(); |
|
} |
|
|
|
self.room_list().load(); |
|
self.sync(); |
|
} |
|
Err(error) => { |
|
priv_.error.replace(Some(error)); |
|
} |
|
} |
|
self.emit_by_name("prepared", &[]).unwrap(); |
|
} |
|
|
|
fn sync(&self) { |
|
let priv_ = imp::Session::from_instance(self); |
|
let sender = self.create_new_sync_response_sender(); |
|
let client = priv_.client.get().unwrap().clone(); |
|
RUNTIME.spawn(async move { |
|
// TODO: only create the filter once and reuse it in the future |
|
let room_event_filter = assign!(RoomEventFilter::default(), { |
|
lazy_load_options: LazyLoadOptions::Enabled {include_redundant_members: false}, |
|
}); |
|
let filter = assign!(FilterDefinition::default(), { |
|
room: assign!(RoomFilter::empty(), { |
|
include_leave: true, |
|
state: room_event_filter, |
|
}), |
|
}); |
|
|
|
let sync_settings = SyncSettings::new() |
|
.timeout(Duration::from_secs(30)) |
|
.filter(filter.into()); |
|
client |
|
.sync_with_callback(sync_settings, |response| { |
|
let sender = sender.clone(); |
|
async move { |
|
// Using the event handler 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. |
|
send!(sender, response); |
|
|
|
matrix_sdk::LoopCtrl::Continue |
|
} |
|
}) |
|
.await; |
|
}); |
|
} |
|
|
|
fn mark_ready(&self) { |
|
let priv_ = &imp::Session::from_instance(self); |
|
priv_.stack.set_visible_child(&*priv_.content); |
|
priv_.is_ready.set(true).unwrap(); |
|
} |
|
|
|
fn is_ready(&self) -> bool { |
|
let priv_ = &imp::Session::from_instance(self); |
|
priv_.is_ready.get().copied().unwrap_or_default() |
|
} |
|
|
|
pub fn room_list(&self) -> &RoomList { |
|
let priv_ = &imp::Session::from_instance(self); |
|
priv_.room_list.get_or_init(|| RoomList::new(self)) |
|
} |
|
|
|
pub fn user(&self) -> Option<&User> { |
|
let priv_ = &imp::Session::from_instance(self); |
|
priv_.user.get() |
|
} |
|
|
|
pub fn client(&self) -> &Client { |
|
let priv_ = &imp::Session::from_instance(self); |
|
priv_.client.get().unwrap() |
|
} |
|
|
|
/// 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| { |
|
if !obj.is_ready() { |
|
obj.mark_ready(); |
|
} |
|
obj.handle_sync_response(response); |
|
|
|
glib::Continue(true) |
|
}), |
|
); |
|
|
|
sender |
|
} |
|
|
|
/// This appends a new error to the list of errors |
|
pub fn append_error(&self, error: &Error) { |
|
let priv_ = imp::Session::from_instance(self); |
|
priv_.error_list.append(error); |
|
} |
|
|
|
/// Returns and consumes the `error` that was generated when the session failed to login, |
|
/// on a successful login this will be `None`. |
|
/// Unfortunately it's not possible to connect the Error directly to the `prepared` signals. |
|
pub fn get_error(&self) -> Option<LoginError> { |
|
let priv_ = &imp::Session::from_instance(self); |
|
priv_.error.take().map(LoginError::from) |
|
} |
|
|
|
pub fn connect_prepared<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId { |
|
self.connect_local("prepared", true, move |values| { |
|
let obj = values[0].get::<Self>().unwrap(); |
|
|
|
f(&obj); |
|
|
|
None |
|
}) |
|
.unwrap() |
|
} |
|
|
|
fn handle_sync_response(&self, response: SyncResponse) { |
|
self.room_list().handle_response_rooms(response.rooms); |
|
} |
|
|
|
pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) { |
|
let priv_ = &imp::Session::from_instance(self); |
|
priv_ |
|
.sidebar |
|
.set_logged_in_users(sessions_stack_pages, self); |
|
} |
|
} |
|
|
|
impl Default for Session { |
|
fn default() -> Self { |
|
Self::new() |
|
} |
|
}
|
|
|