diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 23a37a07..f106321d 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -20,6 +20,7 @@ ui/context-menu-bin.ui ui/pill.ui ui/spinner-button.ui + ui/in-app-notification.ui style.css icons/scalable/actions/send-symbolic.svg icons/scalable/status/welcome.svg diff --git a/data/resources/style.css b/data/resources/style.css index 4f385eea..8eedf99e 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -15,7 +15,7 @@ } .app-notification .pill { - background-color: alpha(@theme_fg_color, 0.35); + background-color: alpha(@theme_bg_color, 0.2); } /* Login */ @@ -153,3 +153,8 @@ headerbar.flat { .invite-room-name { font-size: 24px; } + +.app-notification { + border-radius: 9999px; + padding-left: 24px; +} diff --git a/data/resources/ui/in-app-notification.ui b/data/resources/ui/in-app-notification.ui new file mode 100644 index 00000000..6b695182 --- /dev/null +++ b/data/resources/ui/in-app-notification.ui @@ -0,0 +1,35 @@ + + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index bcbe1a39..93ff03cc 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -16,6 +16,7 @@ data/resources/ui/content-state-row.ui data/resources/ui/content.ui data/resources/ui/context-menu-bin.ui data/resources/ui/login.ui +data/resources/ui/in-app-notification.ui data/resources/ui/session.ui data/resources/ui/shortcuts.ui data/resources/ui/sidebar-category-row.ui @@ -30,6 +31,7 @@ data/resources/ui/window.ui src/application.rs src/components/context_menu_bin.rs src/components/label_with_widgets.rs +src/components/in_app_notification.rs src/components/mod.rs src/components/spinner_button.rs src/components/pill.rs diff --git a/src/components/in_app_notification.rs b/src/components/in_app_notification.rs new file mode 100644 index 00000000..821f4022 --- /dev/null +++ b/src/components/in_app_notification.rs @@ -0,0 +1,185 @@ +use crate::Error; +use adw::subclass::prelude::*; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{gio, glib, glib::clone, CompositeTemplate}; + +mod imp { + use super::*; + use glib::{signal::SignalHandlerId, subclass::InitializingObject}; + use std::cell::{Cell, RefCell}; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/FractalNext/in-app-notification.ui")] + pub struct InAppNotification { + pub error_list: RefCell>, + pub handler: RefCell>, + #[template_child] + pub revealer: TemplateChild, + #[template_child] + pub box_: TemplateChild, + pub current_widget: RefCell>, + pub shows_error: Cell, + } + + #[glib::object_subclass] + impl ObjectSubclass for InAppNotification { + const NAME: &'static str = "InAppNotification"; + type Type = super::InAppNotification; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + + klass.install_action("in-app-notification.close", None, move |widget, _, _| { + widget.dismiss() + }); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for InAppNotification { + fn properties() -> &'static [glib::ParamSpec] { + use once_cell::sync::Lazy; + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpec::new_object( + "error-list", + "Error List", + "The list of errors to display", + gio::ListStore::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() { + "error-list" => obj.set_error_list(value.get().unwrap()), + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "error-list" => obj.error_list().to_value(), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + self.revealer + .connect_child_revealed_notify(clone!(@weak obj => move |revealer| { + let priv_ = imp::InAppNotification::from_instance(&obj); + revealer.set_visible(priv_.shows_error.get()); + })); + } + + fn dispose(&self, _obj: &Self::Type) { + if let Some(id) = self.handler.take() { + self.error_list.borrow().as_ref().unwrap().disconnect(id); + } + } + } + + impl WidgetImpl for InAppNotification {} + + impl BinImpl for InAppNotification {} +} + +glib::wrapper! { + pub struct InAppNotification(ObjectSubclass) + @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; +} + +impl InAppNotification { + pub fn new(error_list: &gio::ListStore) -> Self { + glib::Object::new(&[("error-list", &error_list)]) + .expect("Failed to create InAppNotification") + } + + pub fn set_error_list(&self, error_list: Option) { + let priv_ = imp::InAppNotification::from_instance(self); + if self.error_list() == error_list { + return; + } + + if let Some(id) = priv_.handler.take() { + priv_.error_list.borrow().as_ref().unwrap().disconnect(id); + } + + if let Some(ref error_list) = error_list { + let handler = error_list.connect_items_changed( + clone!(@weak self as obj => move |_, position, removed, added| { + let priv_ = imp::InAppNotification::from_instance(&obj); + // If the first error is removed we need to display the next error + if position == 0 && removed > 0 { + obj.next(); + } + + if added > 0 && !priv_.shows_error.get() { + obj.next(); + } + + }), + ); + priv_.handler.replace(Some(handler)); + } + priv_.error_list.replace(error_list); + + self.next(); + self.notify("error-list"); + } + + pub fn error_list(&self) -> Option { + let priv_ = imp::InAppNotification::from_instance(self); + priv_.error_list.borrow().to_owned() + } + + /// Show the next message in the `error-list` + fn next(&self) { + let priv_ = imp::InAppNotification::from_instance(self); + + let shows_error = if let Some(widget) = priv_ + .error_list + .borrow() + .as_ref() + .and_then(|error_list| error_list.item(0)) + .and_then(|obj| obj.downcast::().ok()) + .and_then(|error| error.widget()) + { + if let Some(current_widget) = priv_.current_widget.take() { + priv_.box_.remove(¤t_widget); + } + priv_.box_.prepend(&widget); + priv_.current_widget.replace(Some(widget)); + true + } else { + false + }; + + priv_.shows_error.set(shows_error); + if shows_error { + priv_.revealer.show(); + } + priv_.revealer.set_reveal_child(shows_error); + } + + fn dismiss(&self) { + let priv_ = imp::InAppNotification::from_instance(self); + if let Some(error_list) = &*priv_.error_list.borrow() { + error_list.remove(0); + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 04674049..158f2a50 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,9 +1,11 @@ mod context_menu_bin; +mod in_app_notification; mod label_with_widgets; mod pill; mod spinner_button; pub use self::context_menu_bin::{ContextMenuBin, ContextMenuBinImpl}; +pub use self::in_app_notification::InAppNotification; pub use self::label_with_widgets::LabelWithWidgets; pub use self::pill::Pill; pub use self::spinner_button::SpinnerButton; diff --git a/src/meson.build b/src/meson.build index b2c8fd8c..e149e2bc 100644 --- a/src/meson.build +++ b/src/meson.build @@ -24,6 +24,7 @@ sources = files( 'components/label_with_widgets.rs', 'components/mod.rs', 'components/pill.rs', + 'components/in_app_notification.rs', 'components/spinner_button.rs', 'config.rs', 'error.rs',