use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, clone},
CompositeTemplate,
};
use ruma::OwnedMxcUri;
use tracing::error;
mod change_password_subpage;
mod deactivate_account_subpage;
mod log_out_subpage;
pub use self::{
change_password_subpage::ChangePasswordSubpage,
deactivate_account_subpage::DeactivateAccountSubpage, log_out_subpage::LogOutSubpage,
};
use crate::{
components::{ActionButton, ActionState, ButtonRow, CopyableRow, EditableAvatar},
prelude::*,
session::model::Session,
spawn, spawn_tokio, toast,
utils::{media::load_file, template_callbacks::TemplateCallbacks, OngoingAsyncAction},
};
mod imp {
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/account_settings/general_page/mod.ui"
)]
#[properties(wrapper_type = super::GeneralPage)]
pub struct GeneralPage {
#[property(get, set = Self::set_session, nullable)]
pub session: glib::WeakRef<Session>,
#[template_child]
pub avatar: TemplateChild<EditableAvatar>,
#[template_child]
pub display_name: TemplateChild<adw::EntryRow>,
#[template_child]
pub display_name_button: TemplateChild<ActionButton>,
#[template_child]
pub change_password_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub homeserver: TemplateChild<CopyableRow>,
#[template_child]
pub user_id: TemplateChild<CopyableRow>,
#[template_child]
pub session_id: TemplateChild<CopyableRow>,
pub changing_avatar: RefCell<Option<OngoingAsyncAction<OwnedMxcUri>>>,
pub changing_display_name: RefCell<Option<OngoingAsyncAction<String>>>,
pub avatar_uri_handler: RefCell<Option<glib::SignalHandlerId>>,
pub display_name_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for GeneralPage {
const NAME: &'static str = "AccountSettingsGeneralPage";
type Type = super::GeneralPage;
type ParentType = adw::PreferencesPage;
fn class_init(klass: &mut Self::Class) {
ButtonRow::ensure_type();
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
TemplateCallbacks::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for GeneralPage {}
impl WidgetImpl for GeneralPage {}
impl PreferencesPageImpl for GeneralPage {}
impl GeneralPage {
fn set_session(&self, session: Option<Session>) {
let prev_session = self.session.upgrade();
if prev_session == session {
return;
}
let obj = self.obj();
if let Some(session) = prev_session {
let user = session.user();
if let Some(handler) = self.avatar_uri_handler.take() {
user.avatar_data().image().unwrap().disconnect(handler)
}
if let Some(handler) = self.display_name_handler.take() {
user.disconnect(handler);
}
}
self.session.set(session.as_ref());
obj.notify_session();
let Some(session) = session else {
return;
};
self.user_id.set_subtitle(session.user_id().as_str());
self.homeserver.set_subtitle(session.homeserver().as_str());
self.session_id.set_subtitle(session.device_id().as_str());
let user = session.user();
let avatar_uri_handler = user
.avatar_data()
.image()
.unwrap()
.connect_uri_string_notify(clone!(
#[weak]
obj,
move |avatar_image| {
obj.user_avatar_changed(avatar_image.uri());
}
));
self.avatar_uri_handler.replace(Some(avatar_uri_handler));
let display_name_handler = user.connect_display_name_notify(clone!(
#[weak]
obj,
move |user| {
obj.user_display_name_changed(user.display_name());
}
));
self.display_name_handler
.replace(Some(display_name_handler));
spawn!(
glib::Priority::LOW,
clone!(
#[weak(rename_to = imp)]
self,
async move {
imp.load_can_change_password().await;
}
)
);
}
async fn load_can_change_password(&self) {
let Some(session) = self.session.upgrade() else {
return;
};
let client = session.client();
let handle = spawn_tokio!(async move { client.get_capabilities().await });
let can_change_password = match handle.await.unwrap() {
Ok(capabilities) => capabilities.change_password.enabled,
Err(error) => {
error!("Could not get server capabilities: {error}");
false
}
};
self.change_password_group.set_visible(can_change_password);
}
}
}
glib::wrapper! {
pub struct GeneralPage(ObjectSubclass<imp::GeneralPage>)
@extends gtk::Widget, adw::PreferencesPage, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
impl GeneralPage {
pub fn new(session: &Session) -> Self {
glib::Object::builder().property("session", session).build()
}
fn user_avatar_changed(&self, uri: Option<OwnedMxcUri>) {
let imp = self.imp();
if let Some(action) = imp.changing_avatar.borrow().as_ref() {
if uri.as_ref() != action.as_value() {
return;
}
} else {
return;
};
imp.changing_avatar.take();
imp.avatar.success();
if uri.is_none() {
toast!(self, gettext("Avatar removed successfully"));
} else {
toast!(self, gettext("Avatar changed successfully"));
}
}
#[template_callback]
async fn change_avatar(&self, file: gio::File) {
let Some(session) = self.session() else {
return;
};
let imp = self.imp();
let avatar = &imp.avatar;
avatar.edit_in_progress();
let (data, info) = match load_file(&file).await {
Ok(res) => res,
Err(error) => {
error!("Could not load user avatar file: {error}");
toast!(self, gettext("Could not load file"));
avatar.reset();
return;
}
};
let client = session.client();
let client_clone = client.clone();
let handle =
spawn_tokio!(async move { client_clone.media().upload(&info.mime, data).await });
let uri = match handle.await.unwrap() {
Ok(res) => res.content_uri,
Err(error) => {
error!("Could not upload user avatar: {error}");
toast!(self, gettext("Could not upload avatar"));
avatar.reset();
return;
}
};
let (action, weak_action) = OngoingAsyncAction::set(uri.clone());
imp.changing_avatar.replace(Some(action));
let uri_clone = uri.clone();
let handle =
spawn_tokio!(async move { client.account().set_avatar_url(Some(&uri_clone)).await });
match handle.await.unwrap() {
Ok(_) => {
if weak_action.is_ongoing() {
session.user().set_avatar_url(Some(uri))
}
}
Err(error) => {
if weak_action.is_ongoing() {
imp.changing_avatar.take();
error!("Could not change user avatar: {error}");
toast!(self, gettext("Could not change avatar"));
avatar.reset();
}
}
}
}
#[template_callback]
async fn remove_avatar(&self) {
let Some(session) = self.session() else {
return;
};
let confirm_dialog = adw::AlertDialog::builder()
.default_response("cancel")
.heading(gettext("Remove Avatar?"))
.body(gettext("Do you really want to remove your avatar?"))
.build();
confirm_dialog.add_responses(&[
("cancel", &gettext("Cancel")),
("remove", &gettext("Remove")),
]);
confirm_dialog.set_response_appearance("remove", adw::ResponseAppearance::Destructive);
if confirm_dialog.choose_future(self).await != "remove" {
return;
}
let imp = self.imp();
let avatar = &*imp.avatar;
avatar.removal_in_progress();
let (action, weak_action) = OngoingAsyncAction::remove();
imp.changing_avatar.replace(Some(action));
let client = session.client();
let handle = spawn_tokio!(async move { client.account().set_avatar_url(None).await });
match handle.await.unwrap() {
Ok(_) => {
if weak_action.is_ongoing() {
session.user().set_avatar_url(None)
}
}
Err(error) => {
if weak_action.is_ongoing() {
imp.changing_avatar.take();
error!("Could not remove user avatar: {error}");
toast!(self, gettext("Could not remove avatar"));
avatar.reset();
}
}
}
}
#[template_callback]
fn display_name_changed(&self) {
self.imp()
.display_name_button
.set_visible(self.has_display_name_changed());
}
fn has_display_name_changed(&self) -> bool {
let Some(session) = self.session() else {
return false;
};
let imp = self.imp();
let text = imp.display_name.text();
let display_name = session.user().display_name();
text != display_name
}
fn user_display_name_changed(&self, name: String) {
let imp = self.imp();
if let Some(action) = imp.changing_display_name.borrow().as_ref() {
if action.as_value() == Some(&name) {
return;
}
} else {
return;
}
imp.changing_display_name.take();
let entry = &imp.display_name;
let button = &imp.display_name_button;
entry.remove_css_class("error");
entry.set_sensitive(true);
button.set_visible(false);
button.set_state(ActionState::Confirm);
toast!(self, gettext("Name changed successfully"));
}
#[template_callback]
async fn change_display_name(&self) {
if !self.has_display_name_changed() {
return;
}
let Some(session) = self.session() else {
return;
};
let imp = self.imp();
let entry = &imp.display_name;
let button = &imp.display_name_button;
entry.set_sensitive(false);
button.set_state(ActionState::Loading);
let display_name = entry.text().trim().to_string();
let (action, weak_action) = OngoingAsyncAction::set(display_name.clone());
imp.changing_display_name.replace(Some(action));
let client = session.client();
let display_name_clone = display_name.clone();
let handle = spawn_tokio!(async move {
client
.account()
.set_display_name(Some(&display_name_clone))
.await
});
match handle.await.unwrap() {
Ok(_) => {
if weak_action.is_ongoing() {
session.user().set_name(Some(display_name));
}
}
Err(error) => {
if weak_action.is_ongoing() {
imp.changing_display_name.take();
error!("Could not change user display name: {error}");
toast!(self, gettext("Could not change display name"));
button.set_state(ActionState::Retry);
entry.add_css_class("error");
entry.set_sensitive(true);
}
}
}
}
}