room: Add dialog to create new rooms

This commit is contained in:
Julian Sparber 2021-09-29 12:39:23 +02:00
parent f3f570fc7e
commit f01d402dd6
9 changed files with 594 additions and 0 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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;

26
src/matrix_error.rs Normal file
View file

@ -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."),
}
}
}

View file

@ -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',

View file

@ -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 {

View file

@ -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!("Couldnt 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();
}
}
}