diff --git a/data/resources/icons/scalable/apps/org.gnome.Fractal.svg b/data/resources/icons/scalable/apps/org.gnome.Fractal.svg
new file mode 100644
index 00000000..4ca612d0
--- /dev/null
+++ b/data/resources/icons/scalable/apps/org.gnome.Fractal.svg
@@ -0,0 +1,55 @@
+
+
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 60d8ddd3..e7813411 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -42,6 +42,7 @@
icons/scalable/actions/settings-symbolic.svg
icons/scalable/actions/system-search-symbolic.svg
icons/scalable/actions/user-add-symbolic.svg
+ icons/scalable/apps/org.gnome.Fractal.svg
icons/scalable/status/audio-symbolic.svg
icons/scalable/status/blocked-symbolic.svg
icons/scalable/status/checkmark-symbolic.svg
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 22bfed4d..4e2a061a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -65,6 +65,7 @@ src/login/advanced_dialog.ui
src/login/greeter.ui
src/login/homeserver_page.rs
src/login/homeserver_page.ui
+src/login/in_browser_page.rs
src/login/in_browser_page.ui
src/login/method_page.rs
src/login/method_page.ui
diff --git a/src/application.rs b/src/application.rs
index 2ccc0c6c..e76c1abc 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -20,6 +20,10 @@ use crate::{
/// The key for the current session setting.
pub(crate) const SETTINGS_KEY_CURRENT_SESSION: &str = "current-session";
+/// The name of the application.
+pub(crate) const APP_NAME: &str = "Fractal";
+/// The URL of the homepage of the application.
+pub(crate) const APP_HOMEPAGE_URL: &str = "https://gitlab.gnome.org/World/fractal/";
mod imp {
use std::cell::Cell;
@@ -231,11 +235,11 @@ mod imp {
/// Show the dialog with information about the application.
fn show_about_dialog(&self) {
let dialog = adw::AboutDialog::builder()
- .application_name("Fractal")
+ .application_name(APP_NAME)
.application_icon(config::APP_ID)
.developer_name(gettext("The Fractal Team"))
.license_type(gtk::License::Gpl30)
- .website("https://gitlab.gnome.org/World/fractal/")
+ .website(APP_HOMEPAGE_URL)
.issue_url("https://gitlab.gnome.org/World/fractal/-/issues")
.support_url("https://matrix.to/#/#fractal:gnome.org")
.version(config::VERSION)
diff --git a/src/login/homeserver_page.rs b/src/login/homeserver_page.rs
index a77f012a..0db1809a 100644
--- a/src/login/homeserver_page.rs
+++ b/src/login/homeserver_page.rs
@@ -1,6 +1,6 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
-use gtk::{self, glib, glib::clone, CompositeTemplate};
+use gtk::{glib, glib::clone, CompositeTemplate};
use matrix_sdk::{
config::RequestConfig, sanitize_server_name, Client, ClientBuildError, ClientBuilder,
};
@@ -124,12 +124,17 @@ mod imp {
self.update_next_state();
}
+ /// The current text from the homeserver entry.
+ pub(super) fn homeserver(&self) -> glib::GString {
+ self.homeserver_entry.text()
+ }
+
/// Whether the current state allows to go to the next step.
fn can_go_next(&self) -> bool {
let Some(login) = self.login.obj() else {
return false;
};
- let homeserver = self.homeserver_entry.text();
+ let homeserver = self.homeserver();
if login.autodiscovery() {
sanitize_server_name(homeserver.as_str()).is_ok()
@@ -144,14 +149,9 @@ mod imp {
self.next_button.set_sensitive(self.can_go_next());
}
- /// Fetch the login details of the homeserver.
- #[template_callback]
- async fn fetch_homeserver_details(&self) {
- self.check_homeserver().await;
- }
-
/// Check if the homeserver that was entered is valid.
- pub(super) async fn check_homeserver(&self) {
+ #[template_callback]
+ async fn check_homeserver(&self) {
if !self.can_go_next() {
return;
}
@@ -164,27 +164,15 @@ mod imp {
login.freeze();
let autodiscovery = login.autodiscovery();
-
- let res = if autodiscovery {
- self.discover_homeserver().await
- } else {
- self.detect_homeserver().await
- };
+ let res = self.build_client(autodiscovery).await;
match res {
Ok(client) => {
- let server_name = autodiscovery
- .then(|| self.homeserver_entry.text())
- .and_then(|s| sanitize_server_name(&s).ok());
-
- login.set_domain(server_name);
login.set_client(Some(client.clone()));
-
- self.homeserver_login_types(client).await;
+ self.discover_login_api(client).await;
}
Err(error) => {
- let obj = self.obj();
- toast!(obj, error.to_user_facing());
+ self.abort_on_error(&error.to_user_facing());
}
}
@@ -192,9 +180,21 @@ mod imp {
login.unfreeze();
}
- /// Try to discover the homeserver.
- async fn discover_homeserver(&self) -> Result {
- let homeserver = self.homeserver_entry.text();
+ /// Try to build a client with the current homeserver.
+ pub(super) async fn build_client(
+ &self,
+ autodiscovery: bool,
+ ) -> Result {
+ if autodiscovery {
+ self.build_client_with_autodiscovery().await
+ } else {
+ self.build_client_with_url().await
+ }
+ }
+
+ /// Try to build a client by using homeserver autodiscovery.
+ async fn build_client_with_autodiscovery(&self) -> Result {
+ let homeserver = self.homeserver();
let handle = spawn_tokio!(async move {
Self::client_builder()
.server_name_or_homeserver_url(homeserver)
@@ -211,9 +211,9 @@ mod imp {
}
}
- /// Check if the URL points to a homeserver.
- async fn detect_homeserver(&self) -> Result {
- let homeserver = self.homeserver_entry.text();
+ /// Try to build a client by using the homeserver's URL.
+ async fn build_client_with_url(&self) -> Result {
+ let homeserver = self.homeserver();
spawn_tokio!(async move {
let client = Self::client_builder()
.respect_login_well_known(false)
@@ -221,9 +221,9 @@ mod imp {
.build()
.await?;
- // This method calls the `GET /versions` endpoint if it was not called
- // previously.
- client.unstable_features().await?;
+ // Call the `GET /versions` endpoint to make sure that the URL belongs to a
+ // Matrix homeserver.
+ client.server_versions().await?;
Ok(client)
})
@@ -231,26 +231,28 @@ mod imp {
.expect("task was not aborted")
}
- /// Fetch the login types supported by the homeserver.
- async fn homeserver_login_types(&self, client: Client) {
+ /// Discover the login API supported by the homeserver.
+ async fn discover_login_api(&self, client: Client) {
let Some(login) = self.login.obj() else {
return;
};
- let handle = spawn_tokio!(async move { client.matrix_auth().get_login_types().await });
+ // Check if the server supports the OAuth 2.0 API.
+ let oauth = client.oauth();
+ let handle = spawn_tokio!(async move { oauth.server_metadata().await });
match handle.await.expect("task was not aborted") {
- Ok(res) => {
- login.set_login_types(res.flows);
- login.show_login_page();
+ Ok(_) => {
+ login.init_oauth_login().await;
}
Err(error) => {
- warn!("Could not get available login types: {error}");
- let obj = self.obj();
- toast!(obj, "Could not get available login types");
-
- // Drop the client because it is bound to the homeserver.
- login.drop_client();
+ if error.is_not_supported() {
+ // Fallback to the Matrix native API.
+ login.init_matrix_login().await;
+ } else {
+ warn!("Could not get authorization server metadata: {error}");
+ self.abort_on_error(&gettext("Could not set up login"));
+ }
}
}
}
@@ -259,6 +261,17 @@ mod imp {
fn client_builder() -> ClientBuilder {
Client::builder().request_config(RequestConfig::new().retry_limit(2))
}
+
+ /// Show the given error and abort the current login.
+ fn abort_on_error(&self, error: &str) {
+ let obj = self.obj();
+ toast!(obj, error);
+
+ // Drop the client because it is bound to the homeserver.
+ if let Some(login) = self.login.obj() {
+ login.drop_client();
+ }
+ }
}
}
@@ -273,13 +286,21 @@ impl LoginHomeserverPage {
glib::Object::new()
}
+ /// The current text from the homeserver entry.
+ pub(super) fn homeserver(&self) -> glib::GString {
+ self.imp().homeserver()
+ }
+
/// Reset this page.
- pub(crate) fn clean(&self) {
+ pub(super) fn clean(&self) {
self.imp().clean();
}
- /// Check if the homeserver that was entered is valid.
- pub(crate) async fn check_homeserver(&self) {
- self.imp().check_homeserver().await;
+ /// Try to build a client with the current homeserver.
+ pub(super) async fn build_client(
+ &self,
+ autodiscovery: bool,
+ ) -> Result {
+ self.imp().build_client(autodiscovery).await
}
}
diff --git a/src/login/homeserver_page.ui b/src/login/homeserver_page.ui
index 58e5dbb0..dd9b73b8 100644
--- a/src/login/homeserver_page.ui
+++ b/src/login/homeserver_page.ui
@@ -71,7 +71,7 @@
@@ -140,7 +140,7 @@
Try Again
-
+
diff --git a/src/session/view/account_settings/general_page/mod.rs b/src/session/view/account_settings/general_page/mod.rs
index a5e9726b..b2fac496 100644
--- a/src/session/view/account_settings/general_page/mod.rs
+++ b/src/session/view/account_settings/general_page/mod.rs
@@ -55,6 +55,8 @@ mod imp {
pub user_id: TemplateChild,
#[template_child]
pub session_id: TemplateChild,
+ #[template_child]
+ deactivate_account_button: TemplateChild,
/// The current session.
#[property(get, set = Self::set_session, nullable)]
session: glib::WeakRef,
@@ -209,6 +211,11 @@ mod imp {
/// Update the possible changes on the user account with the current
/// state.
fn update_capabilities(&self) {
+ let Some(session) = self.session.upgrade() else {
+ return;
+ };
+
+ let uses_oauth_api = session.uses_oauth_api();
let has_account_management_url = self.account_management_url_builder().is_some();
let capabilities = self.capabilities.borrow();
@@ -220,6 +227,8 @@ mod imp {
.set_visible(!has_account_management_url && capabilities.change_password.enabled);
self.manage_account_group
.set_visible(has_account_management_url);
+ self.deactivate_account_button
+ .set_visible(!uses_oauth_api || has_account_management_url);
}
/// Open the URL to manage the account.
diff --git a/src/session/view/account_settings/general_page/mod.ui b/src/session/view/account_settings/general_page/mod.ui
index 84d18778..7db9eee8 100644
--- a/src/session/view/account_settings/general_page/mod.ui
+++ b/src/session/view/account_settings/general_page/mod.ui
@@ -122,7 +122,7 @@
-
+
diff --git a/src/session/view/account_settings/user_sessions_page/user_session_subpage.rs b/src/session/view/account_settings/user_sessions_page/user_session_subpage.rs
index cbf2edd0..a5def297 100644
--- a/src/session/view/account_settings/user_sessions_page/user_session_subpage.rs
+++ b/src/session/view/account_settings/user_sessions_page/user_session_subpage.rs
@@ -8,6 +8,7 @@ use super::AccountSettings;
use crate::{
components::{ActionButton, ActionState, AuthError, LoadingButtonRow},
gettext_f,
+ prelude::*,
session::model::UserSession,
toast,
utils::{template_callbacks::TemplateCallbacks, BoundConstructOnlyObject, BoundObject},
@@ -149,10 +150,19 @@ mod imp {
self.log_out_button.set_visible(true);
self.loading_disconnect_button.set_visible(false);
self.open_url_disconnect_button.set_visible(false);
- } else if self.account_management_url_builder().is_some() {
+ return;
+ }
+
+ let Some(session) = user_session.session() else {
+ return;
+ };
+
+ if session.uses_oauth_api() {
+ let has_account_management_url = self.account_management_url_builder().is_some();
self.log_out_button.set_visible(false);
self.loading_disconnect_button.set_visible(false);
- self.open_url_disconnect_button.set_visible(true);
+ self.open_url_disconnect_button
+ .set_visible(has_account_management_url);
} else {
self.log_out_button.set_visible(false);
self.loading_disconnect_button.set_visible(true);
diff --git a/src/session_list/session_info.rs b/src/session_list/session_info.rs
index dc787186..28922841 100644
--- a/src/session_list/session_info.rs
+++ b/src/session_list/session_info.rs
@@ -1,4 +1,5 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
+use matrix_sdk::authentication::oauth::ClientId;
use ruma::{OwnedDeviceId, OwnedUserId};
use url::Url;
@@ -117,6 +118,16 @@ pub trait SessionInfoExt: 'static {
&self.info().homeserver
}
+ /// The OAuth 2.0 client ID, if any.
+ fn client_id(&self) -> Option<&ClientId> {
+ self.info().client_id.as_ref()
+ }
+
+ /// Whether this session uses the OAuth 2.0 API.
+ fn uses_oauth_api(&self) -> bool {
+ self.client_id().is_some()
+ }
+
/// The Matrix session's device ID.
fn device_id(&self) -> &OwnedDeviceId {
&self.info().device_id
diff --git a/src/utils/matrix/mod.rs b/src/utils/matrix/mod.rs
index b1cad474..de6f4a85 100644
--- a/src/utils/matrix/mod.rs
+++ b/src/utils/matrix/mod.rs
@@ -5,11 +5,14 @@ use std::{borrow::Cow, fmt, str::FromStr};
use gettextrs::gettext;
use gtk::{glib, prelude::*};
use matrix_sdk::{
- authentication::matrix::MatrixSession,
+ authentication::{
+ matrix::MatrixSession,
+ oauth::{OAuthSession, UserSession},
+ },
config::RequestConfig,
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
encryption::{BackupDownloadStrategy, EncryptionSettings},
- Client, ClientBuildError, SessionMeta, SessionTokens,
+ AuthSession, Client, ClientBuildError, SessionMeta, SessionTokens,
};
use ruma::{
events::{
@@ -298,12 +301,19 @@ pub async fn client_with_stored_session(
user_id,
device_id,
passphrase,
+ client_id,
..
} = session;
- let session_data = MatrixSession {
- meta: SessionMeta { user_id, device_id },
- tokens,
+ let meta = SessionMeta { user_id, device_id };
+ let session_data: AuthSession = if let Some(client_id) = client_id {
+ OAuthSession {
+ user: UserSession { meta, tokens },
+ client_id,
+ }
+ .into()
+ } else {
+ MatrixSession { meta, tokens }.into()
};
let encryption_settings = EncryptionSettings {