diff --git a/po/POTFILES.in b/po/POTFILES.in index 4fcb71fa..4ad457b8 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -178,12 +178,12 @@ src/session/view/content/room_history/typing_row.rs src/session/view/content/room_history/verification_info_bar.rs src/session/view/create_dm_dialog/mod.rs src/session/view/create_dm_dialog/mod.ui +src/session/view/create_room_dialog.rs +src/session/view/create_room_dialog.ui src/session/view/event_details_dialog.rs src/session/view/event_details_dialog.ui src/session/view/media_viewer.rs src/session/view/media_viewer.ui -src/session/view/room_creation.rs -src/session/view/room_creation.ui src/session/view/sidebar/mod.rs src/session/view/sidebar/mod.ui src/session/view/sidebar/room_row.rs diff --git a/src/session/view/create_room_dialog.rs b/src/session/view/create_room_dialog.rs new file mode 100644 index 00000000..6ba8f259 --- /dev/null +++ b/src/session/view/create_room_dialog.rs @@ -0,0 +1,257 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::gettext; +use gtk::{glib, CompositeTemplate}; +use matrix_sdk::{ + ruma::{ + api::client::{ + error::ErrorKind, + room::{create_room, Visibility}, + }, + assign, + }, + Error, +}; +use ruma::events::{room::encryption::RoomEncryptionEventContent, InitialStateEvent}; +use tracing::error; + +use crate::{ + components::{LoadingButton, SubstringEntryRow, ToastableDialog}, + prelude::*, + session::model::Session, + spawn_tokio, toast, Window, +}; + +// MAX length of room addresses +const MAX_BYTES: usize = 255; + +mod imp { + use glib::subclass::InitializingObject; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] + #[template(resource = "/org/gnome/Fractal/ui/session/view/create_room_dialog.ui")] + #[properties(wrapper_type = super::CreateRoomDialog)] + pub struct CreateRoomDialog { + #[template_child] + create_button: TemplateChild, + #[template_child] + content: TemplateChild, + #[template_child] + room_name: TemplateChild, + #[template_child] + topic_text_view: TemplateChild, + #[template_child] + visibility_private: TemplateChild, + #[template_child] + encryption: TemplateChild, + #[template_child] + room_address: TemplateChild, + #[template_child] + room_address_error_revealer: TemplateChild, + #[template_child] + room_address_error: TemplateChild, + /// The current session. + #[property(get, set = Self::set_session, explicit_notify, nullable)] + session: glib::WeakRef, + } + + #[glib::object_subclass] + impl ObjectSubclass for CreateRoomDialog { + const NAME: &'static str = "CreateRoomDialog"; + type Type = super::CreateRoomDialog; + type ParentType = ToastableDialog; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + Self::bind_template_callbacks(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for CreateRoomDialog {} + + impl WidgetImpl for CreateRoomDialog {} + impl AdwDialogImpl for CreateRoomDialog {} + impl ToastableDialogImpl for CreateRoomDialog {} + + #[gtk::template_callbacks] + impl CreateRoomDialog { + /// Set the current session. + fn set_session(&self, session: Option<&Session>) { + if self.session.upgrade().as_ref() == session { + return; + } + + if let Some(session) = session { + let server_name = session.user_id().server_name(); + self.room_address.set_suffix_text(format!(":{server_name}")); + } + + self.session.set(session); + self.obj().notify_session(); + } + + /// Check whether a room can be created with the current input. + /// + /// This will also change the UI elements to reflect why the room can't + /// be created. + fn can_create_room(&self) -> bool { + if self.room_name.text().trim().is_empty() { + return false; + } + + // Only public rooms have an address. + if self.visibility_private.is_active() { + return true; + } + + let mut can_create = true; + let room_address = self.room_address.text(); + + // We don't allow #, : in the room address + let address_error = if room_address.contains(':') { + can_create = false; + Some(gettext("Cannot contain “:”")) + } else if room_address.contains('#') { + can_create = false; + Some(gettext("Cannot contain “#”")) + } else if room_address.len() > MAX_BYTES { + can_create = false; + Some(gettext("Too long. Use a shorter address.")) + } else if room_address.trim().is_empty() { + can_create = false; + None + } else { + None + }; + + let reveal_address_error = address_error.is_some(); + + if let Some(error) = address_error { + self.room_address_error.set_text(&error); + self.room_address.add_css_class("error"); + } else { + self.room_address.remove_css_class("error"); + } + self.room_address_error_revealer + .set_reveal_child(reveal_address_error); + + can_create + } + + /// Validate the form and change the corresponding UI elements. + #[template_callback] + fn validate_form(&self) { + self.create_button.set_sensitive(self.can_create_room()); + } + + /// Create the room, if it is allowed. + #[template_callback] + async fn create_room(&self) { + if !self.can_create_room() { + return; + } + + let Some(session) = self.session.upgrade() else { + return; + }; + + self.create_button.set_is_loading(true); + self.content.set_sensitive(false); + + let name = Some(self.room_name.text().trim()) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned); + + let buffer = self.topic_text_view.buffer(); + let (start_iter, end_iter) = buffer.bounds(); + let topic = Some(buffer.text(&start_iter, &end_iter, false).trim()) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned); + + let mut request = assign!( + create_room::v3::Request::new(), + { + name, + topic, + } + ); + + if self.visibility_private.is_active() { + // The room is private. + request.visibility = Visibility::Private; + + if self.encryption.is_active() { + let event = InitialStateEvent::new( + RoomEncryptionEventContent::with_recommended_defaults(), + ); + request.initial_state = vec![event.to_raw_any()]; + } + } else { + // The room is public. + request.visibility = Visibility::Public; + request.room_alias_name = Some(self.room_address.text().trim().to_owned()); + } + + let client = session.client(); + let handle = spawn_tokio!(async move { client.create_room(request).await }); + + match handle.await.expect("task was not aborted") { + Ok(matrix_room) => { + let obj = self.obj(); + + let Some(window) = obj.root().and_downcast::() else { + return; + }; + if let Some(room) = session.room_list().get_wait(matrix_room.room_id()).await { + window.session_view().select_room(room); + } + + obj.close(); + } + Err(error) => { + error!("Could not create a new room: {error}"); + self.handle_error(&error); + } + } + } + + /// Display the error that occurred during creation. + fn handle_error(&self, error: &Error) { + self.create_button.set_is_loading(false); + self.content.set_sensitive(true); + + // Handle the room address already taken error. + if let Some(kind) = error.client_api_error_kind() { + if *kind == ErrorKind::RoomInUse { + self.room_address.add_css_class("error"); + self.room_address_error + .set_text(&gettext("The address is already taken.")); + self.room_address_error_revealer.set_reveal_child(true); + + return; + } + } + + let obj = self.obj(); + toast!(obj, error.to_user_facing()); + } + } +} + +glib::wrapper! { + /// Dialog to create a new room. + pub struct CreateRoomDialog(ObjectSubclass) + @extends gtk::Widget, adw::Dialog, ToastableDialog, @implements gtk::Accessible; +} + +impl CreateRoomDialog { + pub fn new(session: &Session) -> Self { + glib::Object::builder().property("session", session).build() + } +} diff --git a/src/session/view/room_creation.ui b/src/session/view/create_room_dialog.ui similarity index 99% rename from src/session/view/room_creation.ui rename to src/session/view/create_room_dialog.ui index 2b33de6a..708713b6 100644 --- a/src/session/view/room_creation.ui +++ b/src/session/view/create_room_dialog.ui @@ -1,6 +1,6 @@ -