[fractal/fractal-next] room: Add dialog to create new rooms
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] room: Add dialog to create new rooms
- Date: Thu, 30 Sep 2021 08:52:12 +0000 (UTC)
commit f01d402dd63543e0e54221cc2c096fb993d79d00
Author: Julian Sparber <julian sparber net>
Date: Wed Sep 29 12:39:23 2021 +0200
room: Add dialog to create new rooms
data/resources/resources.gresource.xml | 1 +
data/resources/ui/room-creation.ui | 182 +++++++++++++++++
data/resources/ui/sidebar.ui | 6 +
po/POTFILES.in | 3 +
src/main.rs | 2 +
src/matrix_error.rs | 26 +++
src/meson.build | 2 +
src/session/mod.rs | 11 +
src/session/room_creation/mod.rs | 361 +++++++++++++++++++++++++++++++++
9 files changed, 594 insertions(+)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 182b4cfc..60e0b34e 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -39,6 +39,7 @@
<file compressed="true" preprocess="xml-stripblanks"
alias="account-settings-device-row.ui">ui/account-settings-device-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="account-settings-devices-page.ui">ui/account-settings-devices-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="components-loading-listbox-row.ui">ui/components-loading-listbox-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks" alias="room-creation.ui">ui/room-creation.ui</file>
<file compressed="true">style.css</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
diff --git a/data/resources/ui/room-creation.ui b/data/resources/ui/room-creation.ui
new file mode 100644
index 00000000..6b6bb6cf
--- /dev/null
+++ b/data/resources/ui/room-creation.ui
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="RoomCreation" parent="AdwWindow">
+ <property name="title" translatable="yes">Create new Room</property>
+ <property name="default-widget">create_button</property>
+ <property name="modal">True</property>
+ <property name="default-width">380</property>
+ <property name="content">
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkHeaderBar">
+ <property name="show-title-buttons">False</property>
+ <child type="start">
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child type="end">
+ <object class="SpinnerButton" id="create_button">
+ <property name="label" translatable="yes">C_reate</property>
+ <property name="use_underline">True</property>
+ <property name="sensitive">False</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="error_label_revealer">
+ <property name="child">
+ <object class="GtkLabel" id="error_label">
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="margin-top">24</property>
+ <style>
+ <class name="error"/>
+ </style>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="content">
+ <property name="selection-mode">none</property>
+ <property name="margin-top">24</property>
+ <property name="margin-bottom">24</property>
+ <property name="margin-start">24</property>
+ <property name="margin-end">24</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="AdwActionRow">
+ <property name="title" translatable="yes">Room Name</property>
+ <property name="selectable">False</property>
+ <property name="use_underline">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="margin-top">6</property>
+ <property name="margin-bottom">6</property>
+ <child>
+ <object class="GtkEntry" id="room_name">
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="room_name_error_revealer">
+ <property name="child">
+ <object class="GtkLabel" id="room_name_error">
+ <property name="valign">start</property>
+ <property name="xalign">0.0</property>
+ <property name="margin-top">6</property>
+ <style>
+ <class name="error"/>
+ <class name="caption"/>
+ </style>
+ </object>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow">
+ <property name="title" translatable="yes">Visibility</property>
+ <property name="selectable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="valign">center</property>
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="GtkToggleButton" id="private_button">
+ <property name="label" translatable="yes">_Private</property>
+ <property name="use_underline">True</property>
+ <property name="active">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="public_button">
+ <property name="label" translatable="yes">P_ublic</property>
+ <property name="use_underline">True</property>
+ <property name="group">private_button</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow">
+ <property name="visible" bind-source="public_button" bind-property="active"
bind-flags="sync-create"/>
+ <property name="title" translatable="yes">Room Address</property>
+ <property name="selectable">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="margin-top">6</property>
+ <property name="margin-bottom">6</property>
+ <child>
+ <object class="GtkBox">
+ <property name="valign">center</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label">#</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="room_address">
+ <property name="valign">center</property>
+ <property name="max-width-chars">10</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="server_name">
+ <property name="label">:gnome.org</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="room_address_error_revealer">
+ <property name="child">
+ <object class="GtkLabel" id="room_address_error">
+ <property name="valign">start</property>
+ <property name="xalign">0.0</property>
+ <property name="margin-top">6</property>
+ <style>
+ <class name="error"/>
+ <class name="caption"/>
+ </style>
+ </object>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+</interface>
+
diff --git a/data/resources/ui/sidebar.ui b/data/resources/ui/sidebar.ui
index 90eb8f62..668ef7ba 100644
--- a/data/resources/ui/sidebar.ui
+++ b/data/resources/ui/sidebar.ui
@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="primary_menu">
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_New Room</attribute>
+ <attribute name="action">session.room-creation</attribute>
+ </item>
+ </section>
<section>
<item>
<attribute name="label" translatable="yes">_Preferences</attribute>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 902998b2..04cb0556 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -27,6 +27,7 @@ data/resources/ui/context-menu-bin.ui
data/resources/ui/event-source-dialog.ui
data/resources/ui/login.ui
data/resources/ui/in-app-notification.ui
+data/resources/ui/room-creation.ui
data/resources/ui/session.ui
data/resources/ui/shortcuts.ui
data/resources/ui/sidebar-category-row.ui
@@ -54,6 +55,7 @@ src/components/pill.rs
src/error.rs
src/login.rs
src/main.rs
+src/matrix_error.rs
src/secret.rs
src/session/account_settings/devices_page/device.rs
src/session/account_settings/devices_page/device_item.rs
@@ -74,6 +76,7 @@ src/session/content/room_details/mod.rs
src/session/content/room_history.rs
src/session/content/state_row.rs
src/session/mod.rs
+src/session/room_creation/mod.rs
src/session/room_list.rs
src/session/room/event.rs
src/session/room/highlight_flags.rs
diff --git a/src/main.rs b/src/main.rs
index c90124c0..60984048 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,6 +10,7 @@ mod prelude;
mod components;
mod error;
mod login;
+mod matrix_error;
mod secret;
mod session;
mod utils;
@@ -18,6 +19,7 @@ mod window;
use self::application::Application;
use self::error::Error;
use self::login::Login;
+use self::matrix_error::UserFacingMatrixError;
use self::session::Session;
use self::window::Window;
diff --git a/src/matrix_error.rs b/src/matrix_error.rs
new file mode 100644
index 00000000..53f1faed
--- /dev/null
+++ b/src/matrix_error.rs
@@ -0,0 +1,26 @@
+use matrix_sdk::{
+ ruma::api::error::{FromHttpResponseError, ServerError},
+ HttpError,
+};
+
+use gettextrs::gettext;
+
+pub trait UserFacingMatrixError {
+ fn to_user_facing(self) -> String;
+}
+
+impl UserFacingMatrixError for HttpError {
+ fn to_user_facing(self) -> String {
+ match self {
+ HttpError::Reqwest(_) => {
+ // TODO: Add more information based on the error
+ gettext("Couldn't connect to the server.")
+ }
+ HttpError::ClientApi(FromHttpResponseError::Http(ServerError::Known(error))) => {
+ // TODO: The server may not give us pretty enough error message. We should add our own error
message.
+ error.message
+ }
+ _ => gettext("An Unknown error occurred."),
+ }
+ }
+}
diff --git a/src/meson.build b/src/meson.build
index 84177de3..2e74b35f 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -34,6 +34,7 @@ sources = files(
'config.rs',
'error.rs',
'main.rs',
+ 'matrix_error.rs',
'window.rs',
'login.rs',
'secret.rs',
@@ -69,6 +70,7 @@ sources = files(
'session/room/room_type.rs',
'session/room_list.rs',
'session/room/timeline.rs',
+ 'session/room_creation/mod.rs',
'session/sidebar/item_list.rs',
'session/sidebar/category.rs',
'session/sidebar/category_row.rs',
diff --git a/src/session/mod.rs b/src/session/mod.rs
index f2a6b480..409be32b 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -3,6 +3,7 @@ mod avatar;
mod content;
mod event_source_dialog;
mod room;
+mod room_creation;
mod room_list;
mod sidebar;
mod user;
@@ -11,6 +12,7 @@ use self::account_settings::AccountSettings;
pub use self::avatar::Avatar;
use self::content::Content;
pub use self::room::Room;
+pub use self::room_creation::RoomCreation;
use self::room_list::RoomList;
use self::sidebar::Sidebar;
pub use self::user::{User, UserExt};
@@ -82,6 +84,10 @@ mod imp {
session.set_selected_room(None);
});
+ klass.install_action("session.room-creation", None, move |session, _, _| {
+ session.show_room_creation_dialog();
+ });
+
klass.add_binding_action(
gdk::keys::constants::Escape,
gdk::ModifierType::empty(),
@@ -493,6 +499,11 @@ impl Session {
window.show();
}
}
+
+ fn show_room_creation_dialog(&self) {
+ let window = RoomCreation::new(self.parent_window().as_ref(), &self);
+ window.show();
+ }
}
impl Default for Session {
diff --git a/src/session/room_creation/mod.rs b/src/session/room_creation/mod.rs
new file mode 100644
index 00000000..d1115d78
--- /dev/null
+++ b/src/session/room_creation/mod.rs
@@ -0,0 +1,361 @@
+use adw::subclass::prelude::*;
+use gettextrs::gettext;
+use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+use log::error;
+use std::convert::{TryFrom, TryInto};
+
+use crate::components::SpinnerButton;
+use crate::session::user::UserExt;
+use crate::session::Session;
+use crate::utils::do_async;
+use matrix_sdk::{
+ ruma::{
+ api::{
+ client::{
+ error::ErrorKind as RumaClientErrorKind,
+ r0::room::{create_room, Visibility},
+ },
+ error::{FromHttpResponseError, ServerError},
+ },
+ assign,
+ identifiers::{Error, RoomName},
+ },
+ HttpError,
+};
+
+use crate::UserFacingMatrixError;
+
+// MAX length of room addresses
+const MAX_BYTES: usize = 255;
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+ use std::cell::RefCell;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/room-creation.ui")]
+ pub struct RoomCreation {
+ pub session: RefCell<Option<Session>>,
+ #[template_child]
+ pub content: TemplateChild<gtk::ListBox>,
+ #[template_child]
+ pub create_button: TemplateChild<SpinnerButton>,
+ #[template_child]
+ pub cancel_button: TemplateChild<gtk::Button>,
+ #[template_child]
+ pub room_name: TemplateChild<gtk::Entry>,
+ #[template_child]
+ pub private_button: TemplateChild<gtk::ToggleButton>,
+ #[template_child]
+ pub room_address: TemplateChild<gtk::Entry>,
+ #[template_child]
+ pub room_name_error_revealer: TemplateChild<gtk::Revealer>,
+ #[template_child]
+ pub room_name_error: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub room_address_error_revealer: TemplateChild<gtk::Revealer>,
+ #[template_child]
+ pub room_address_error: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub server_name: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub error_label: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub error_label_revealer: TemplateChild<gtk::Revealer>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for RoomCreation {
+ const NAME: &'static str = "RoomCreation";
+ type Type = super::RoomCreation;
+ type ParentType = adw::Window;
+
+ fn class_init(klass: &mut Self::Class) {
+ SpinnerButton::static_type();
+ Self::bind_template(klass);
+
+ klass.add_binding(
+ gdk::keys::constants::Escape,
+ gdk::ModifierType::empty(),
+ |obj, _| {
+ obj.cancel();
+ true
+ },
+ None,
+ );
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for RoomCreation {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_object(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "session" => obj.set_session(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "session" => obj.session().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ self.cancel_button
+ .connect_clicked(clone!(@weak obj => move |_| {
+ obj.cancel();
+ }));
+
+ self.create_button
+ .connect_clicked(clone!(@weak obj => move |_| {
+ obj.create_room();
+ }));
+
+ self.room_name
+ .connect_text_notify(clone!(@weak obj = > move |_| {
+ obj.validate_input();
+ }));
+
+ self.room_address
+ .connect_text_notify(clone!(@weak obj = > move |_| {
+ obj.validate_input();
+ }));
+ }
+ }
+
+ impl WidgetImpl for RoomCreation {}
+ impl WindowImpl for RoomCreation {}
+ impl AdwWindowImpl for RoomCreation {}
+}
+
+glib::wrapper! {
+ /// Preference Window to display and update room details.
+ pub struct RoomCreation(ObjectSubclass<imp::RoomCreation>)
+ @extends gtk::Widget, gtk::Window, adw::Window, @implements gtk::Accessible;
+}
+
+impl RoomCreation {
+ pub fn new(parent_window: Option<&impl IsA<gtk::Window>>, session: &Session) -> Self {
+ glib::Object::new(&[("transient-for", &parent_window), ("session", session)])
+ .expect("Failed to create RoomCreation")
+ }
+
+ pub fn session(&self) -> Option<Session> {
+ let priv_ = imp::RoomCreation::from_instance(self);
+ priv_.session.borrow().clone()
+ }
+
+ fn set_session(&self, session: Option<Session>) {
+ let priv_ = imp::RoomCreation::from_instance(self);
+
+ if self.session() == session {
+ return;
+ }
+
+ if let Some(user) = session.as_ref().and_then(|session| session.user()) {
+ priv_
+ .server_name
+ .set_label(&[":", user.user_id().server_name().as_str()].concat());
+ }
+
+ priv_.session.replace(session);
+ self.notify("session");
+ }
+
+ fn create_room(&self) -> Option<()> {
+ let priv_ = imp::RoomCreation::from_instance(self);
+
+ priv_.create_button.set_loading(true);
+ priv_.content.set_sensitive(false);
+ priv_.cancel_button.set_sensitive(false);
+ priv_.error_label_revealer.set_reveal_child(false);
+
+ let client = self.session()?.client().clone();
+
+ let room_name = priv_.room_name.text().to_string();
+
+ let visibility = if priv_.private_button.is_active() {
+ Visibility::Private
+ } else {
+ Visibility::Public
+ };
+
+ let room_address = if !priv_.private_button.is_active() {
+ Some(format!("#{}", priv_.room_address.text().as_str()))
+ } else {
+ None
+ };
+
+ do_async(
+ glib::PRIORITY_DEFAULT_IDLE,
+ async move {
+ // We don't allow invalid room names to be entered by the user
+ let name = room_name.as_str().try_into().unwrap();
+
+ let request = assign!(create_room::Request::new(),
+ {
+ name: Some(name),
+ visibility,
+ room_alias_name: room_address.as_deref()
+ });
+ client.create_room(request).await
+ },
+ clone!(@weak self as obj => move |result| async move {
+ match result {
+ Ok(response) => {
+ if let Some(session) = obj.session() {
+ let room = session.room_list().get_wait(response.room_id).await;
+ session.set_selected_room(room);
+ }
+ obj.close();
+ },
+ Err(error) => {
+ error!("Couldn’t create a new room: {}", error);
+ obj.handle_error(error);
+ },
+ };
+ }),
+ );
+
+ None
+ }
+
+ /// Display the error that occured during creation
+ fn handle_error(&self, error: HttpError) {
+ let priv_ = imp::RoomCreation::from_instance(self);
+
+ priv_.create_button.set_loading(false);
+ priv_.content.set_sensitive(true);
+ priv_.cancel_button.set_sensitive(true);
+
+ // Treat the room address already taken error special
+ if let HttpError::ClientApi(FromHttpResponseError::Http(ServerError::Known(
+ ref client_error,
+ ))) = error
+ {
+ if client_error.kind == RumaClientErrorKind::RoomInUse {
+ priv_.room_address.add_css_class("error");
+ priv_
+ .room_address_error
+ .set_text(&gettext("The address is already taken."));
+ priv_.room_address_error_revealer.set_reveal_child(true);
+
+ return;
+ }
+ }
+
+ priv_.error_label.set_label(&error.to_user_facing());
+
+ priv_.error_label_revealer.set_reveal_child(true);
+ }
+
+ fn validate_input(&self) {
+ let priv_ = imp::RoomCreation::from_instance(self);
+
+ // Validate room name
+ let (is_name_valid, has_error) =
+ match <&RoomName>::try_from(priv_.room_name.text().as_str()) {
+ Ok(_) => (true, false),
+ Err(Error::EmptyRoomName) => (false, false),
+ Err(Error::MaximumLengthExceeded) => {
+ priv_
+ .room_name_error
+ .set_text(&gettext("Too long. Use a shorter name."));
+ (false, true)
+ }
+ Err(_) => unimplemented!(),
+ };
+
+ if has_error {
+ priv_.room_name.add_css_class("error");
+ } else {
+ priv_.room_name.remove_css_class("error");
+ }
+
+ priv_.room_name_error_revealer.set_reveal_child(has_error);
+
+ // Validate room address
+
+ // Only public rooms have a address
+ if priv_.private_button.is_active() {
+ priv_.create_button.set_sensitive(is_name_valid);
+ return;
+ }
+
+ let room_address = priv_.room_address.text();
+
+ // We don't allow #, : in the room address
+ let (is_address_valid, has_error) = if room_address.find(':').is_some() {
+ priv_
+ .room_address_error
+ .set_text(&gettext("Can't contain `:`"));
+ (false, true)
+ } else if room_address.find('#').is_some() {
+ priv_
+ .room_address_error
+ .set_text(&gettext("Can't contain `#`"));
+ (false, true)
+ } else if room_address.len() > MAX_BYTES {
+ priv_
+ .room_address_error
+ .set_text(&gettext("Too long. Use a shorter address."));
+ (false, true)
+ } else if room_address.is_empty() {
+ (false, false)
+ } else {
+ (true, false)
+ };
+
+ // TODO: should we immediately check if the address is available, like element is doing?
+
+ if has_error {
+ priv_.room_address.add_css_class("error");
+ } else {
+ priv_.room_address.remove_css_class("error");
+ }
+
+ priv_
+ .room_address_error_revealer
+ .set_reveal_child(has_error);
+ priv_
+ .create_button
+ .set_sensitive(is_name_valid && is_address_valid);
+ }
+
+ fn cancel(&self) {
+ let priv_ = imp::RoomCreation::from_instance(self);
+
+ if priv_.cancel_button.is_sensitive() {
+ self.close();
+ }
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]