use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{self, gio, glib, glib::clone, CompositeTemplate};
use matrix_sdk::Client;
use ruma::{
api::client::session::{get_login_types::v3::LoginType, login},
OwnedServerName,
};
use tracing::{error, warn};
use url::Url;
mod advanced_dialog;
mod greeter;
mod homeserver_page;
mod idp_button;
mod method_page;
mod session_setup_view;
mod sso_page;
use self::{
advanced_dialog::LoginAdvancedDialog, greeter::Greeter, homeserver_page::LoginHomeserverPage,
method_page::LoginMethodPage, session_setup_view::SessionSetupView, sso_page::LoginSsoPage,
};
use crate::{
components::OfflineBanner, prelude::*, secret::store_session, session::model::Session, spawn,
spawn_tokio, toast, Application, Window, RUNTIME, SETTINGS_KEY_CURRENT_SESSION,
};
#[derive(Clone, Debug, Default, glib::Boxed)]
#[boxed_type(name = "BoxedLoginTypes")]
pub struct BoxedLoginTypes(Vec<LoginType>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString, strum::AsRefStr)]
#[strum(serialize_all = "kebab-case")]
enum LoginPage {
Greeter,
Homeserver,
Method,
Sso,
Loading,
SessionSetup,
Completed,
}
mod imp {
use std::cell::{Cell, RefCell};
use glib::{subclass::InitializingObject, SignalHandlerId};
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/login/mod.ui")]
#[properties(wrapper_type = super::Login)]
pub struct Login {
#[template_child]
pub navigation: TemplateChild<adw::NavigationView>,
#[template_child]
pub greeter: TemplateChild<Greeter>,
#[template_child]
pub homeserver_page: TemplateChild<LoginHomeserverPage>,
#[template_child]
pub method_page: TemplateChild<LoginMethodPage>,
#[template_child]
pub sso_page: TemplateChild<LoginSsoPage>,
#[template_child]
pub done_button: TemplateChild<gtk::Button>,
pub prepared_source_id: RefCell<Option<SignalHandlerId>>,
pub logged_out_source_id: RefCell<Option<SignalHandlerId>>,
pub ready_source_id: RefCell<Option<SignalHandlerId>>,
#[property(get, set = Self::set_autodiscovery, construct, explicit_notify, default = true)]
pub autodiscovery: Cell<bool>,
#[property(get)]
pub login_types: RefCell<BoxedLoginTypes>,
#[property(get = Self::domain, type = Option<String>)]
pub domain: RefCell<Option<OwnedServerName>>,
#[property(get = Self::homeserver, type = Option<String>)]
pub homeserver: RefCell<Option<Url>>,
pub client: RefCell<Option<Client>>,
pub session: RefCell<Option<Session>>,
}
#[glib::object_subclass]
impl ObjectSubclass for Login {
const NAME: &'static str = "Login";
type Type = super::Login;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
OfflineBanner::ensure_type();
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.set_css_name("login");
klass.set_accessible_role(gtk::AccessibleRole::Group);
klass.install_action_async(
"login.sso",
Some(&Option::<String>::static_variant_type()),
|obj, _, variant| async move {
let idp_id = variant.and_then(|v| v.get::<Option<String>>()).flatten();
obj.login_with_sso(idp_id).await;
},
);
klass.install_action_async("login.open-advanced", None, |obj, _, _| async move {
obj.open_advanced_dialog().await;
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for Login {
fn constructed(&self) {
let obj = self.obj();
obj.action_set_enabled("login.next", false);
self.parent_constructed();
let monitor = gio::NetworkMonitor::default();
monitor.connect_network_changed(clone!(
#[weak]
obj,
move |_, available| {
obj.action_set_enabled("login.sso", available);
}
));
obj.action_set_enabled("login.sso", monitor.is_network_available());
self.navigation.connect_visible_page_notify(clone!(
#[weak]
obj,
move |_| {
obj.visible_page_changed();
}
));
}
fn dispose(&self) {
let obj = self.obj();
obj.drop_client();
obj.drop_session();
}
}
impl WidgetImpl for Login {
fn grab_focus(&self) -> bool {
match self.visible_page() {
LoginPage::Greeter => self.greeter.grab_focus(),
LoginPage::Homeserver => self.homeserver_page.grab_focus(),
LoginPage::Method => self.method_page.grab_focus(),
LoginPage::Sso | LoginPage::Loading => false,
LoginPage::SessionSetup => {
if let Some(session_setup) = self.session_setup() {
session_setup.grab_focus()
} else {
false
}
}
LoginPage::Completed => self.done_button.grab_focus(),
}
}
}
impl BinImpl for Login {}
impl AccessibleImpl for Login {}
impl Login {
pub(super) fn visible_page(&self) -> LoginPage {
self.navigation
.visible_page()
.and_then(|p| p.tag())
.and_then(|s| s.as_str().try_into().ok())
.unwrap()
}
pub fn set_autodiscovery(&self, autodiscovery: bool) {
if self.autodiscovery.get() == autodiscovery {
return;
}
self.autodiscovery.set(autodiscovery);
self.obj().notify_autodiscovery();
}
fn domain(&self) -> Option<String> {
if self.autodiscovery.get() {
self.domain.borrow().clone().map(Into::into)
} else {
self.homeserver()
}
}
fn homeserver(&self) -> Option<String> {
self.homeserver
.borrow()
.as_ref()
.map(|url| url.as_ref().trim_end_matches('/').to_owned())
}
pub(super) fn session_setup(&self) -> Option<SessionSetupView> {
self.navigation
.find_page(LoginPage::SessionSetup.as_ref())
.and_downcast()
}
}
}
glib::wrapper! {
pub struct Login(ObjectSubclass<imp::Login>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl Login {
pub fn new() -> Self {
glib::Object::new()
}
fn visible_page_changed(&self) {
let imp = self.imp();
match imp.visible_page() {
LoginPage::Greeter => {
self.clean();
}
LoginPage::Homeserver => {
self.drop_client();
self.drop_session();
self.imp().method_page.clean();
}
LoginPage::Method => {
self.drop_session();
}
_ => {}
}
}
pub async fn client(&self) -> Option<Client> {
if let Some(client) = self.imp().client.borrow().clone() {
return Some(client);
}
self.imp().homeserver_page.check_homeserver().await;
if let Some(client) = self.imp().client.borrow().clone() {
return Some(client);
}
None
}
fn set_client(&self, client: Option<Client>) {
let homeserver = client.as_ref().map(Client::homeserver);
self.set_homeserver_url(homeserver);
self.imp().client.replace(client);
}
pub fn drop_client(&self) {
if let Some(client) = self.imp().client.take() {
let _guard = RUNTIME.enter();
drop(client);
}
}
fn drop_session(&self) {
if let Some(session) = self.imp().session.take() {
spawn!(async move {
let _ = session.logout().await;
});
}
}
fn set_domain(&self, domain: Option<OwnedServerName>) {
let imp = self.imp();
if *imp.domain.borrow() == domain {
return;
}
imp.domain.replace(domain);
self.notify_domain();
}
pub fn homeserver_url(&self) -> Option<Url> {
self.imp().homeserver.borrow().clone()
}
pub fn set_homeserver_url(&self, homeserver: Option<Url>) {
let imp = self.imp();
if self.homeserver_url() == homeserver {
return;
}
imp.homeserver.replace(homeserver);
self.notify_homeserver();
if !self.autodiscovery() {
self.notify_domain();
}
}
fn set_login_types(&self, types: Vec<LoginType>) {
self.imp().login_types.replace(BoxedLoginTypes(types));
self.notify_login_types();
}
pub fn supports_password(&self) -> bool {
self.imp()
.login_types
.borrow()
.0
.iter()
.any(|t| matches!(t, LoginType::Password(_)))
}
async fn open_advanced_dialog(&self) {
let dialog = LoginAdvancedDialog::new();
self.bind_property("autodiscovery", &dialog, "autodiscovery")
.sync_create()
.bidirectional()
.build();
dialog.run_future(self).await;
}
fn show_login_screen(&self) {
if self.supports_password() {
self.imp()
.navigation
.push_by_tag(LoginPage::Method.as_ref());
} else {
spawn!(clone!(
#[weak(rename_to = obj)]
self,
async move {
obj.login_with_sso(None).await;
}
));
}
}
async fn login_with_sso(&self, idp_id: Option<String>) {
let imp = self.imp();
imp.navigation.push_by_tag(LoginPage::Sso.as_ref());
let client = self.client().await.unwrap();
let handle = spawn_tokio!(async move {
let mut login = client
.matrix_auth()
.login_sso(|sso_url| async move {
let ctx = glib::MainContext::default();
ctx.spawn(async move {
spawn!(async move {
if let Err(error) = gtk::UriLauncher::new(&sso_url)
.launch_future(gtk::Window::NONE)
.await
{
error!("Could not launch URI: {error}");
}
});
});
Ok(())
})
.initial_device_display_name("Fractal");
if let Some(idp_id) = idp_id.as_deref() {
login = login.identity_provider_id(idp_id);
}
login.send().await
});
match handle.await.unwrap() {
Ok(response) => {
self.handle_login_response(response).await;
}
Err(error) => {
warn!("Could not log in: {error}");
toast!(self, error.to_user_facing());
imp.navigation.pop();
}
}
}
async fn handle_login_response(&self, response: login::v3::Response) {
let client = self.client().await.unwrap();
let homeserver = client.homeserver();
match Session::new(homeserver, (&response).into()).await {
Ok(session) => {
self.init_session(session).await;
}
Err(error) => {
warn!("Could not create session: {error}");
toast!(self, error.to_user_facing());
self.imp().navigation.pop();
}
}
}
async fn init_session(&self, session: Session) {
let imp = self.imp();
let setup_view = SessionSetupView::new(&session);
setup_view.connect_completed(clone!(
#[weak]
imp,
move |_| {
imp.navigation.push_by_tag(LoginPage::Completed.as_ref());
}
));
imp.navigation.push(&setup_view);
self.drop_client();
imp.session.replace(Some(session.clone()));
let settings = Application::default().settings();
if let Err(err) = settings.set_string(SETTINGS_KEY_CURRENT_SESSION, session.session_id()) {
warn!("Could not save current session: {err}");
}
let session_info = session.info().clone();
let handle = spawn_tokio!(async move { store_session(session_info).await });
if handle.await.unwrap().is_err() {
toast!(self, gettext("Could not store session"));
}
session.prepare().await;
}
#[template_callback]
fn finish_login(&self) {
let Some(window) = self.root().and_downcast::<Window>() else {
return;
};
if let Some(session) = self.imp().session.take() {
window.add_session(session);
}
self.clean();
}
pub fn clean(&self) {
let imp = self.imp();
imp.homeserver_page.clean();
imp.method_page.clean();
self.set_autodiscovery(true);
self.set_login_types(vec![]);
self.set_domain(None);
self.set_homeserver_url(None);
self.drop_client();
self.drop_session();
imp.navigation.pop_to_tag(LoginPage::Greeter.as_ref());
self.unfreeze();
}
fn freeze(&self) {
self.imp().navigation.set_sensitive(false);
}
fn unfreeze(&self) {
self.imp().navigation.set_sensitive(true);
}
}