Handle all user errors in add_subscription_dialog

This commit is contained in:
ranfdev
2023-10-24 18:44:25 +02:00
parent 98a6a0ad47
commit 933247e564
6 changed files with 92 additions and 48 deletions

2
Cargo.lock generated
View File

@ -1641,7 +1641,7 @@ dependencies = [
[[package]] [[package]]
name = "notify" name = "notify"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ashpd", "ashpd",

View File

@ -21,6 +21,8 @@ pub struct SharedEnv {
pub enum Error { pub enum Error {
#[error("topic {0} must not be empty and must contain only alphanumeric characters and _ (underscore)")] #[error("topic {0} must not be empty and must contain only alphanumeric characters and _ (underscore)")]
InvalidTopic(String), InvalidTopic(String),
#[error("invalid server base url {0:?}")]
InvalidServer(#[from] url::ParseError),
#[error("duplicate message")] #[error("duplicate message")]
DuplicateMessage, DuplicateMessage,
#[error("can't parse the minimum set of required fields from the message {0}")] #[error("can't parse the minimum set of required fields from the message {0}")]

View File

@ -121,23 +121,31 @@ pub struct Subscription {
} }
impl Subscription { impl Subscription {
pub fn build_url(server: &str, topic: &str, since: u64) -> anyhow::Result<url::Url> { pub fn build_url(server: &str, topic: &str, since: u64) -> Result<url::Url, crate::Error> {
let mut url = url::Url::parse(server)?; let mut url = url::Url::parse(server)?;
url.path_segments_mut() url.path_segments_mut()
.map_err(|_| anyhow::anyhow!("url can't be base"))? .map_err(|_| url::ParseError::RelativeUrlWithCannotBeABaseBase)?
.push(&topic) .push(&topic)
.push("json"); .push("json");
url.query_pairs_mut() url.query_pairs_mut()
.append_pair("since", &since.to_string()); .append_pair("since", &since.to_string());
Ok(url) Ok(url)
} }
pub fn validate(self) -> anyhow::Result<Self> { pub fn validate(self) -> Result<Self, Vec<crate::Error>> {
validate_topic(&self.topic)?; let mut errs = vec![];
Self::build_url(&self.server, &self.topic, 0)?; if let Err(e) = validate_topic(&self.topic) {
errs.push(e);
};
if let Err(e) = Self::build_url(&self.server, &self.topic, 0) {
errs.push(e);
};
if errs.len() > 0 {
return Err(errs);
}
Ok(self) Ok(self)
} }
pub fn builder(server: String, topic: String) -> SubscriptionBuilder { pub fn builder(topic: String) -> SubscriptionBuilder {
SubscriptionBuilder::new(server, topic) SubscriptionBuilder::new(topic)
} }
} }
@ -153,9 +161,9 @@ pub struct SubscriptionBuilder {
} }
impl SubscriptionBuilder { impl SubscriptionBuilder {
pub fn new(server: String, topic: String) -> Self { pub fn new(topic: String) -> Self {
Self { Self {
server, server: "https://ntfy.sh".to_string(),
topic, topic,
muted: false, muted: false,
archived: false, archived: false,
@ -165,6 +173,11 @@ impl SubscriptionBuilder {
} }
} }
pub fn server(mut self, server: String) -> Self {
self.server = server;
self
}
pub fn muted(mut self, muted: bool) -> Self { pub fn muted(mut self, muted: bool) -> Self {
self.muted = muted; self.muted = muted;
self self
@ -190,7 +203,7 @@ impl SubscriptionBuilder {
self self
} }
pub fn build(self) -> anyhow::Result<Subscription> { pub fn build(self) -> Result<Subscription, Vec<Error>> {
let res = Subscription { let res = Subscription {
server: self.server, server: self.server,
topic: self.topic, topic: self.topic,

View File

@ -401,17 +401,11 @@ impl system_notifier::Server for SystemNotifier {
) -> capnp::capability::Promise<(), capnp::Error> { ) -> capnp::capability::Promise<(), capnp::Error> {
let topic = pry!(pry!(params.get()).get_topic()); let topic = pry!(pry!(params.get()).get_topic());
let server: &str = pry!(pry!(params.get()).get_server()); let server: &str = pry!(pry!(params.get()).get_server());
let server = if server.is_empty() {
"https://ntfy.sh"
} else {
""
};
let subscription = pry!( let subscription = pry!(models::Subscription::builder(topic.to_owned())
models::Subscription::builder(server.to_owned(), topic.to_owned()) .server(server.to_string())
.build() .build()
.map_err(|e| capnp::Error::failed(e.to_string())) .map_err(|e| capnp::Error::failed(format!("{:?}", e))));
);
let sub: Promise<subscription::Client, capnp::Error> = self.watch(subscription.clone()); let sub: Promise<subscription::Client, capnp::Error> = self.watch(subscription.clone());
let mut db = self.env.db.clone(); let mut db = self.env.db.clone();

View File

@ -8,13 +8,18 @@ use gtk::gio;
use gtk::glib; use gtk::glib;
use ntfy_daemon::models; use ntfy_daemon::models;
#[derive(Default, Debug, Clone)]
pub struct Widgets {
pub topic_entry: adw::EntryRow,
pub server_entry: adw::EntryRow,
pub server_expander: adw::ExpanderRow,
pub sub_btn: gtk::Button,
}
mod imp { mod imp {
pub use super::*; pub use super::*;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct AddSubscriptionDialog { pub struct AddSubscriptionDialog {
pub topic_entry: RefCell<adw::EntryRow>, pub widgets: RefCell<Widgets>,
pub server_entry: RefCell<adw::EntryRow>,
pub sub_btn: RefCell<gtk::Button>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -32,7 +37,6 @@ mod imp {
); );
klass.install_action("default.activate", None, |this, _, _| { klass.install_action("default.activate", None, |this, _, _| {
this.emit_subscribe_request(); this.emit_subscribe_request();
this.close();
}); });
} }
} }
@ -112,7 +116,7 @@ impl AddSubscriptionDialog {
} }
} }
}, },
append = &adw::ExpanderRow { append: server_expander = &adw::ExpanderRow {
set_title: "Custom server...", set_title: "Custom server...",
set_enable_expansion: false, set_enable_expansion: false,
set_show_enable_switch: true, set_show_enable_switch: true,
@ -129,7 +133,6 @@ impl AddSubscriptionDialog {
set_sensitive: false, set_sensitive: false,
connect_clicked[obj] => move |_| { connect_clicked[obj] => move |_| {
obj.emit_subscribe_request(); obj.emit_subscribe_request();
obj.close();
} }
} }
}, },
@ -142,34 +145,58 @@ impl AddSubscriptionDialog {
topic_entry.delegate().unwrap().connect_changed(move |_| { topic_entry.delegate().unwrap().connect_changed(move |_| {
txc.send(()).unwrap(); txc.send(()).unwrap();
}); });
let txc = tx.clone();
server_entry.delegate().unwrap().connect_changed(move |_| {
txc.send(()).unwrap();
});
server_expander.connect_enable_expansion_notify(move |_| {
tx.send(()).unwrap();
});
let rx = crate::async_utils::debounce_channel(std::time::Duration::from_millis(500), rx); let rx = crate::async_utils::debounce_channel(std::time::Duration::from_millis(500), rx);
let objc = obj.clone(); let objc = obj.clone();
rx.attach(None, move |_| { rx.attach(None, move |_| {
objc.check_errors(); objc.check_errors();
glib::ControlFlow::Continue glib::ControlFlow::Continue
}); });
imp.topic_entry.replace(topic_entry); imp.widgets.replace(Widgets {
imp.server_entry.replace(server_entry); topic_entry,
imp.sub_btn.replace(sub_btn); server_expander,
server_entry,
sub_btn,
});
obj.set_content(Some(&toolbar_view)); obj.set_content(Some(&toolbar_view));
} }
pub fn topic(&self) -> String { pub fn subscription(&self) -> Result<models::Subscription, Vec<ntfy_daemon::Error>> {
self.imp().topic_entry.borrow().text().to_string() let w = { self.imp().widgets.borrow().clone() };
let mut sub = models::Subscription::builder(w.topic_entry.text().to_string());
if w.server_expander.enables_expansion() {
sub = sub.server(w.server_entry.text().to_string());
} }
pub fn server(&self) -> String {
self.imp().server_entry.borrow().text().to_string() sub.build()
} }
fn check_errors(&self) { fn check_errors(&self) {
let imp = self.imp(); let w = { self.imp().widgets.borrow().clone() };
let topic_entry = imp.topic_entry.borrow().clone(); let sub = self.subscription();
let sub_btn = imp.sub_btn.borrow().clone();
if let Err(_) = models::validate_topic(&topic_entry.delegate().unwrap().text()) { w.server_entry.remove_css_class("error");
topic_entry.add_css_class("error"); w.topic_entry.remove_css_class("error");
sub_btn.set_sensitive(false); w.sub_btn.set_sensitive(true);
} else {
topic_entry.remove_css_class("error"); if let Err(errs) = sub {
sub_btn.set_sensitive(true); w.sub_btn.set_sensitive(false);
for e in errs {
match e {
ntfy_daemon::Error::InvalidTopic(_) => {
w.topic_entry.add_css_class("error");
}
ntfy_daemon::Error::InvalidServer(_) => {
w.server_entry.add_css_class("error");
}
_ => {}
}
}
} }
} }
fn emit_subscribe_request(&self) { fn emit_subscribe_request(&self) {

View File

@ -119,7 +119,15 @@ mod imp {
let this = self.obj().clone(); let this = self.obj().clone();
let dc = dialog.clone(); let dc = dialog.clone();
dialog.connect_local("subscribe-request", true, move |_| { dialog.connect_local("subscribe-request", true, move |_| {
this.add_subscription(&dc.server(), &dc.topic()); let sub = match dc.subscription() {
Ok(sub) => sub,
Err(e) => {
warn!(errors = ?e, "trying to add invalid subscription");
return None;
}
};
this.add_subscription(sub);
dc.close();
None None
}); });
} }
@ -246,11 +254,11 @@ impl NotifyWindow {
}); });
} }
fn add_subscription(&self, server: &str, topic: &str) { fn add_subscription(&self, sub: models::Subscription) {
let mut req = self.notifier().subscribe_request(); let mut req = self.notifier().subscribe_request();
req.get().set_server(server); req.get().set_server(&sub.server);
req.get().set_topic(topic); req.get().set_topic(&sub.topic);
let res = req.send(); let res = req.send();
let this = self.clone(); let this = self.clone();
self.spawn_with_near_toast(async move { self.spawn_with_near_toast(async move {