fractal/login/
method_page.rs1use 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 #[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 fn username(&self) -> glib::GString {
77 self.username_entry.text()
78 }
79
80 fn password(&self) -> glib::GString {
82 self.password_entry.text()
83 }
84
85 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 "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 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 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 #[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 #[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 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 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 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 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 pub(crate) fn clean(&self) {
252 self.imp().clean();
253 }
254}