Skip to main content

fractal/login/
method_page.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::glib;
4use ruma::{OwnedServerName, api::client::session::get_login_types::v3::LoginType};
5use tracing::warn;
6use url::Url;
7
8use super::{Login, sso_idp_button::SsoIdpButton};
9use crate::{components::LoadingButton, gettext_f, prelude::*, spawn_tokio, toast};
10
11mod imp {
12    use std::cell::RefCell;
13
14    use glib::subclass::InitializingObject;
15
16    use super::*;
17
18    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
19    #[template(resource = "/org/gnome/Fractal/ui/login/method_page.ui")]
20    #[properties(wrapper_type = super::LoginMethodPage)]
21    pub struct LoginMethodPage {
22        #[template_child]
23        title: TemplateChild<gtk::Label>,
24        #[template_child]
25        homeserver_url: TemplateChild<gtk::Label>,
26        #[template_child]
27        username_entry: TemplateChild<adw::EntryRow>,
28        #[template_child]
29        password_entry: TemplateChild<adw::PasswordEntryRow>,
30        #[template_child]
31        sso_idp_box: TemplateChild<gtk::Box>,
32        sso_idp_box_children: RefCell<Vec<SsoIdpButton>>,
33        #[template_child]
34        more_sso_btn: TemplateChild<gtk::Button>,
35        #[template_child]
36        next_button: TemplateChild<LoadingButton>,
37        /// The parent `Login` object.
38        #[property(get, set, nullable)]
39        login: glib::WeakRef<Login>,
40    }
41
42    #[glib::object_subclass]
43    impl ObjectSubclass for LoginMethodPage {
44        const NAME: &'static str = "LoginMethodPage";
45        type Type = super::LoginMethodPage;
46        type ParentType = adw::NavigationPage;
47
48        fn class_init(klass: &mut Self::Class) {
49            Self::bind_template(klass);
50            Self::bind_template_callbacks(klass);
51        }
52
53        fn instance_init(obj: &InitializingObject<Self>) {
54            obj.init_template();
55        }
56    }
57
58    #[glib::derived_properties]
59    impl ObjectImpl for LoginMethodPage {}
60
61    impl WidgetImpl for LoginMethodPage {
62        fn grab_focus(&self) -> bool {
63            self.username_entry.grab_focus()
64        }
65    }
66
67    impl NavigationPageImpl for LoginMethodPage {
68        fn shown(&self) {
69            self.grab_focus();
70        }
71    }
72
73    #[gtk::template_callbacks]
74    impl LoginMethodPage {
75        /// The username entered by the user.
76        fn username(&self) -> glib::GString {
77            self.username_entry.text()
78        }
79
80        /// The password entered by the user.
81        fn password(&self) -> glib::GString {
82            self.password_entry.text()
83        }
84
85        /// Update the domain name and URL displayed in the title.
86        pub(super) fn update_title(
87            &self,
88            homeserver_url: &Url,
89            server_name: Option<&OwnedServerName>,
90        ) {
91            let title = if let Some(server_name) = server_name {
92                gettext_f(
93                    // Translators: Do NOT translate the content between '{' and '}', this is a
94                    // variable name.
95                    "Log in to {domain_name}",
96                    &[(
97                        "domain_name",
98                        &format!("<span segment=\"word\">{server_name}</span>"),
99                    )],
100                )
101            } else {
102                gettext("Log in")
103            };
104            self.title.set_markup(&title);
105
106            let homeserver_url = homeserver_url.as_str().trim_end_matches('/');
107            self.homeserver_url.set_label(homeserver_url);
108        }
109
110        /// Update the SSO group.
111        pub(super) fn update_sso(&self, login_types: Vec<LoginType>) {
112            let Some(sso_login) = login_types.into_iter().find_map(|t| match t {
113                LoginType::Sso(sso) => Some(sso),
114                _ => None,
115            }) else {
116                self.sso_idp_box.set_visible(false);
117                self.more_sso_btn.set_visible(false);
118                return;
119            };
120
121            self.clean_idp_box();
122
123            let mut has_unknown_methods = false;
124            let mut has_known_methods = false;
125
126            if !sso_login.identity_providers.is_empty() {
127                let mut sso_idp_box_children = self.sso_idp_box_children.borrow_mut();
128                sso_idp_box_children.reserve(sso_login.identity_providers.len());
129
130                for identity_provider in sso_login.identity_providers {
131                    if let Some(btn) = SsoIdpButton::new(identity_provider) {
132                        self.sso_idp_box.append(&btn);
133                        sso_idp_box_children.push(btn);
134
135                        has_known_methods = true;
136                    } else {
137                        has_unknown_methods = true;
138                    }
139                }
140            }
141            self.sso_idp_box.set_visible(has_known_methods);
142
143            if has_known_methods {
144                self.more_sso_btn.set_label(&gettext("More SSO Providers"));
145                self.more_sso_btn.set_visible(has_unknown_methods);
146            } else {
147                self.more_sso_btn.set_label(&gettext("Login via SSO"));
148                self.more_sso_btn.set_visible(true);
149            }
150        }
151
152        /// Whether the current state allows to login with a password.
153        fn can_login_with_password(&self) -> bool {
154            let username_length = self.username().len();
155            let password_length = self.password().len();
156            username_length != 0 && password_length != 0
157        }
158
159        /// Update the state of the "Next" button.
160        #[template_callback]
161        pub(super) fn update_next_state(&self) {
162            self.next_button
163                .set_sensitive(self.can_login_with_password());
164        }
165
166        /// Login with the password login type.
167        #[template_callback]
168        async fn login_with_password(&self) {
169            if !self.can_login_with_password() {
170                return;
171            }
172
173            let Some(login) = self.login.upgrade() else {
174                return;
175            };
176
177            self.next_button.set_is_loading(true);
178            login.freeze();
179
180            let username = self.username();
181            let password = self.password();
182
183            let client = login.client().await.unwrap();
184            let handle = spawn_tokio!(async move {
185                client
186                    .matrix_auth()
187                    .login_username(&username, &password)
188                    .initial_device_display_name("Fractal")
189                    .send()
190                    .await
191            });
192
193            match handle.await.expect("task was not aborted") {
194                Ok(_) => {
195                    login.create_session().await;
196                }
197                Err(error) => {
198                    warn!("Could not log in: {error}");
199                    toast!(self.obj(), error.to_user_facing());
200                }
201            }
202
203            self.next_button.set_is_loading(false);
204            login.unfreeze();
205        }
206
207        /// Reset this page.
208        pub(super) fn clean(&self) {
209            self.username_entry.set_text("");
210            self.password_entry.set_text("");
211            self.next_button.set_is_loading(false);
212            self.update_next_state();
213            self.clean_idp_box();
214        }
215
216        /// Empty the identity providers box.
217        fn clean_idp_box(&self) {
218            for child in self.sso_idp_box_children.borrow_mut().drain(..) {
219                self.sso_idp_box.remove(&child);
220            }
221        }
222    }
223}
224
225glib::wrapper! {
226    /// The login page allowing to login via password or to choose a SSO provider.
227    pub struct LoginMethodPage(ObjectSubclass<imp::LoginMethodPage>)
228        @extends gtk::Widget, adw::NavigationPage,
229        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
230}
231
232impl LoginMethodPage {
233    pub fn new() -> Self {
234        glib::Object::new()
235    }
236
237    /// Update this page with the given data.
238    pub(crate) fn update(
239        &self,
240        homeserver_url: &Url,
241        domain_name: Option<&OwnedServerName>,
242        login_types: Vec<LoginType>,
243    ) {
244        let imp = self.imp();
245        imp.update_title(homeserver_url, domain_name);
246        imp.update_sso(login_types);
247        imp.update_next_state();
248    }
249
250    /// Reset this page.
251    pub(crate) fn clean(&self) {
252        self.imp().clean();
253    }
254}