Added user notifications

Mikayla Maki created

Change summary

crates/auto_update/src/update_notification.rs     |   4 
crates/collab_ui/src/contact_notification.rs      |   2 
crates/theme/src/theme.rs                         |   8 
crates/workspace/src/notifications.rs             | 280 +++++++++++++++++
crates/workspace/src/workspace.rs                 | 116 +++---
styles/src/styleTree/app.ts                       |   2 
styles/src/styleTree/simpleMessageNotification.ts |  31 +
7 files changed, 375 insertions(+), 68 deletions(-)

Detailed changes

crates/auto_update/src/update_notification.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
 use menu::Cancel;
 use settings::Settings;
 use util::channel::ReleaseChannel;
-use workspace::Notification;
+use workspace::notifications::Notification;
 
 pub struct UpdateNotification {
     version: AppVersion,
@@ -28,7 +28,7 @@ impl View for UpdateNotification {
 
     fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
         let theme = cx.global::<Settings>().theme.clone();
-        let theme = &theme.update_notification;
+        let theme = &theme.simple_message_notification;
 
         let app_name = cx.global::<ReleaseChannel>().display_name();
 

crates/collab_ui/src/contact_notification.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
     elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext,
     View, ViewContext,
 };
-use workspace::Notification;
+use workspace::notifications::Notification;
 
 impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
 

crates/theme/src/theme.rs 🔗

@@ -31,6 +31,7 @@ pub struct Theme {
     pub shared_screen: ContainerStyle,
     pub contact_notification: ContactNotification,
     pub update_notification: UpdateNotification,
+    pub simple_message_notification: MessageNotification,
     pub project_shared_notification: ProjectSharedNotification,
     pub incoming_call_notification: IncomingCallNotification,
     pub tooltip: TooltipStyle,
@@ -478,6 +479,13 @@ pub struct UpdateNotification {
     pub dismiss_button: Interactive<IconButton>,
 }
 
+#[derive(Deserialize, Default)]
+pub struct MessageNotification {
+    pub message: ContainedText,
+    pub action_message: Interactive<ContainedText>,
+    pub dismiss_button: Interactive<IconButton>,
+}
+
 #[derive(Deserialize, Default)]
 pub struct ProjectSharedNotification {
     pub window_height: f32,

crates/workspace/src/notifications.rs 🔗

@@ -0,0 +1,280 @@
+use std::{any::TypeId, ops::DerefMut};
+
+use collections::HashSet;
+use gpui::{AnyViewHandle, Entity, MutableAppContext, View, ViewContext, ViewHandle};
+
+use crate::Workspace;
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.set_global(NotificationTracker::new());
+    simple_message_notification::init(cx);
+}
+
+pub trait Notification: View {
+    fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
+}
+
+pub trait NotificationHandle {
+    fn id(&self) -> usize;
+    fn to_any(&self) -> AnyViewHandle;
+}
+
+impl<T: Notification> NotificationHandle for ViewHandle<T> {
+    fn id(&self) -> usize {
+        self.id()
+    }
+
+    fn to_any(&self) -> AnyViewHandle {
+        self.into()
+    }
+}
+
+impl From<&dyn NotificationHandle> for AnyViewHandle {
+    fn from(val: &dyn NotificationHandle) -> Self {
+        val.to_any()
+    }
+}
+
+struct NotificationTracker {
+    notifications_sent: HashSet<TypeId>,
+}
+
+impl std::ops::Deref for NotificationTracker {
+    type Target = HashSet<TypeId>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.notifications_sent
+    }
+}
+
+impl DerefMut for NotificationTracker {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.notifications_sent
+    }
+}
+
+impl NotificationTracker {
+    fn new() -> Self {
+        Self {
+            notifications_sent: HashSet::default(),
+        }
+    }
+}
+
+impl Workspace {
+    pub fn show_notification_once<V: Notification>(
+        &mut self,
+        id: usize,
+        cx: &mut ViewContext<Self>,
+        build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
+    ) {
+        if !cx
+            .global::<NotificationTracker>()
+            .contains(&TypeId::of::<V>())
+        {
+            cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
+                tracker.insert(TypeId::of::<V>())
+            });
+
+            self.show_notification::<V>(id, cx, build_notification)
+        }
+    }
+
+    pub fn show_notification<V: Notification>(
+        &mut self,
+        id: usize,
+        cx: &mut ViewContext<Self>,
+        build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
+    ) {
+        let type_id = TypeId::of::<V>();
+        if self
+            .notifications
+            .iter()
+            .all(|(existing_type_id, existing_id, _)| {
+                (*existing_type_id, *existing_id) != (type_id, id)
+            })
+        {
+            let notification = build_notification(cx);
+            cx.subscribe(&notification, move |this, handle, event, cx| {
+                if handle.read(cx).should_dismiss_notification_on_event(event) {
+                    this.dismiss_notification(type_id, id, cx);
+                }
+            })
+            .detach();
+            self.notifications
+                .push((type_id, id, Box::new(notification)));
+            cx.notify();
+        }
+    }
+
+    fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
+        self.notifications
+            .retain(|(existing_type_id, existing_id, _)| {
+                if (*existing_type_id, *existing_id) == (type_id, id) {
+                    cx.notify();
+                    false
+                } else {
+                    true
+                }
+            });
+    }
+}
+
+pub mod simple_message_notification {
+    use std::process::Command;
+
+    use gpui::{
+        actions,
+        elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
+        impl_actions, Action, CursorStyle, Element, Entity, MouseButton, MutableAppContext, View,
+        ViewContext,
+    };
+    use menu::Cancel;
+    use serde::Deserialize;
+    use settings::Settings;
+
+    use crate::Workspace;
+
+    use super::Notification;
+
+    actions!(message_notifications, [CancelMessageNotification]);
+
+    #[derive(Clone, Default, Deserialize, PartialEq)]
+    pub struct OsOpen(pub String);
+
+    impl_actions!(message_notifications, [OsOpen]);
+
+    pub fn init(cx: &mut MutableAppContext) {
+        cx.add_action(MessageNotification::dismiss);
+        cx.add_action(
+            |_workspace: &mut Workspace, open_action: &OsOpen, _cx: &mut ViewContext<Workspace>| {
+                #[cfg(target_os = "macos")]
+                {
+                    let mut command = Command::new("open");
+                    command.arg(open_action.0.clone());
+
+                    command.spawn().ok();
+                }
+            },
+        )
+    }
+
+    pub struct MessageNotification {
+        message: String,
+        click_action: Box<dyn Action>,
+        click_message: String,
+    }
+
+    pub enum MessageNotificationEvent {
+        Dismiss,
+    }
+
+    impl Entity for MessageNotification {
+        type Event = MessageNotificationEvent;
+    }
+
+    impl MessageNotification {
+        pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
+            message: S1,
+            click_action: A,
+            click_message: S2,
+        ) -> Self {
+            Self {
+                message: message.as_ref().to_string(),
+                click_action: Box::new(click_action) as Box<dyn Action>,
+                click_message: click_message.as_ref().to_string(),
+            }
+        }
+
+        pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
+            cx.emit(MessageNotificationEvent::Dismiss);
+        }
+    }
+
+    impl View for MessageNotification {
+        fn ui_name() -> &'static str {
+            "MessageNotification"
+        }
+
+        fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+            let theme = cx.global::<Settings>().theme.clone();
+            let theme = &theme.update_notification;
+
+            enum MessageNotificationTag {}
+
+            let click_action = self.click_action.boxed_clone();
+            let click_message = self.click_message.clone();
+            let message = self.message.clone();
+
+            MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
+                Flex::column()
+                    .with_child(
+                        Flex::row()
+                            .with_child(
+                                Text::new(message, theme.message.text.clone())
+                                    .contained()
+                                    .with_style(theme.message.container)
+                                    .aligned()
+                                    .top()
+                                    .left()
+                                    .flex(1., true)
+                                    .boxed(),
+                            )
+                            .with_child(
+                                MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
+                                    let style = theme.dismiss_button.style_for(state, false);
+                                    Svg::new("icons/x_mark_8.svg")
+                                        .with_color(style.color)
+                                        .constrained()
+                                        .with_width(style.icon_width)
+                                        .aligned()
+                                        .contained()
+                                        .with_style(style.container)
+                                        .constrained()
+                                        .with_width(style.button_width)
+                                        .with_height(style.button_width)
+                                        .boxed()
+                                })
+                                .with_padding(Padding::uniform(5.))
+                                .on_click(MouseButton::Left, move |_, cx| {
+                                    cx.dispatch_action(CancelMessageNotification)
+                                })
+                                .aligned()
+                                .constrained()
+                                .with_height(
+                                    cx.font_cache().line_height(theme.message.text.font_size),
+                                )
+                                .aligned()
+                                .top()
+                                .flex_float()
+                                .boxed(),
+                            )
+                            .boxed(),
+                    )
+                    .with_child({
+                        let style = theme.action_message.style_for(state, false);
+
+                        Text::new(click_message, style.text.clone())
+                            .contained()
+                            .with_style(style.container)
+                            .boxed()
+                    })
+                    .contained()
+                    .boxed()
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, cx| {
+                cx.dispatch_any_action(click_action.boxed_clone())
+            })
+            .boxed()
+        }
+    }
+
+    impl Notification for MessageNotification {
+        fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
+            match event {
+                MessageNotificationEvent::Dismiss => true,
+            }
+        }
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -4,6 +4,7 @@
 /// specific locations.
 pub mod dock;
 pub mod item;
+pub mod notifications;
 pub mod pane;
 pub mod pane_group;
 mod persistence;
@@ -41,7 +42,9 @@ use gpui::{
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use language::LanguageRegistry;
+
 use log::{error, warn};
+use notifications::NotificationHandle;
 pub use pane::*;
 pub use pane_group::*;
 use persistence::{model::SerializedItem, DB};
@@ -61,7 +64,10 @@ use theme::{Theme, ThemeRegistry};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
 use util::ResultExt;
 
-use crate::persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace};
+use crate::{
+    notifications::simple_message_notification::{MessageNotification, OsOpen},
+    persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
+};
 
 #[derive(Clone, PartialEq)]
 pub struct RemoveWorktreeFromProject(pub WorktreeId);
@@ -151,6 +157,7 @@ impl_actions!(workspace, [ActivatePane]);
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     pane::init(cx);
     dock::init(cx);
+    notifications::init(cx);
 
     cx.add_global_action(open);
     cx.add_global_action({
@@ -453,31 +460,6 @@ impl DelayedDebouncedEditAction {
     }
 }
 
-pub trait Notification: View {
-    fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
-}
-
-pub trait NotificationHandle {
-    fn id(&self) -> usize;
-    fn to_any(&self) -> AnyViewHandle;
-}
-
-impl<T: Notification> NotificationHandle for ViewHandle<T> {
-    fn id(&self) -> usize {
-        self.id()
-    }
-
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
-    }
-}
-
-impl From<&dyn NotificationHandle> for AnyViewHandle {
-    fn from(val: &dyn NotificationHandle) -> Self {
-        val.to_any()
-    }
-}
-
 #[derive(Default)]
 struct LeaderState {
     followers: HashSet<PeerId>,
@@ -732,6 +714,8 @@ impl Workspace {
                 workspace
             });
 
+            notify_if_database_failed(&workspace, &mut cx);
+
             // Call open path for each of the project paths
             // (this will bring them to the front if they were in the serialized workspace)
             debug_assert!(paths_to_open.len() == project_paths.len());
@@ -1115,45 +1099,6 @@ impl Workspace {
         }
     }
 
-    pub fn show_notification<V: Notification>(
-        &mut self,
-        id: usize,
-        cx: &mut ViewContext<Self>,
-        build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
-    ) {
-        let type_id = TypeId::of::<V>();
-        if self
-            .notifications
-            .iter()
-            .all(|(existing_type_id, existing_id, _)| {
-                (*existing_type_id, *existing_id) != (type_id, id)
-            })
-        {
-            let notification = build_notification(cx);
-            cx.subscribe(&notification, move |this, handle, event, cx| {
-                if handle.read(cx).should_dismiss_notification_on_event(event) {
-                    this.dismiss_notification(type_id, id, cx);
-                }
-            })
-            .detach();
-            self.notifications
-                .push((type_id, id, Box::new(notification)));
-            cx.notify();
-        }
-    }
-
-    fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
-        self.notifications
-            .retain(|(existing_type_id, existing_id, _)| {
-                if (*existing_type_id, *existing_id) == (type_id, id) {
-                    cx.notify();
-                    false
-                } else {
-                    true
-                }
-            });
-    }
-
     pub fn items<'a>(
         &'a self,
         cx: &'a AppContext,
@@ -2436,6 +2381,47 @@ impl Workspace {
     }
 }
 
+fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
+    if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
+        workspace.update(cx, |workspace, cx| {
+            workspace.show_notification_once(0, cx, |cx| {
+                cx.add_view(|_| {
+                    MessageNotification::new(
+                        indoc::indoc! {"
+                            Failed to load any database file :(
+                        "},
+                        OsOpen("https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
+                        "Click to let us know about this error"
+                    )
+                })
+            });
+        });
+    } else {
+        let backup_path = (*db::BACKUP_DB_PATH).read();
+        if let Some(backup_path) = &*backup_path {
+            workspace.update(cx, |workspace, cx| {
+                workspace.show_notification_once(0, cx, |cx| {
+                    cx.add_view(|_| {
+                        let backup_path = backup_path.to_string_lossy();
+                        MessageNotification::new(
+                            format!(
+                                indoc::indoc! {"
+                                Database file was corrupted :(
+                                Old database backed up to:
+                                {}
+                                "},
+                                backup_path
+                            ),
+                            OsOpen(backup_path.to_string()),
+                            "Click to show old database in finder",
+                        )
+                    })
+                });
+            });
+        }
+    }
+}
+
 impl Entity for Workspace {
     type Event = Event;
 }

styles/src/styleTree/app.ts 🔗

@@ -12,6 +12,7 @@ import sharedScreen from "./sharedScreen";
 import projectDiagnostics from "./projectDiagnostics";
 import contactNotification from "./contactNotification";
 import updateNotification from "./updateNotification";
+import simpleMessageNotification from "./simpleMessageNotification";
 import projectSharedNotification from "./projectSharedNotification";
 import tooltip from "./tooltip";
 import terminal from "./terminal";
@@ -47,6 +48,7 @@ export default function app(colorScheme: ColorScheme): Object {
       },
     },
     updateNotification: updateNotification(colorScheme),
+    simpleMessageNotification: simpleMessageNotification(colorScheme),
     tooltip: tooltip(colorScheme),
     terminal: terminal(colorScheme),
     colorScheme: {

styles/src/styleTree/simpleMessageNotification.ts 🔗

@@ -0,0 +1,31 @@
+import { ColorScheme } from "../themes/common/colorScheme";
+import { foreground, text } from "./components";
+
+const headerPadding = 8;
+
+export default function simpleMessageNotification(colorScheme: ColorScheme): Object {
+  let layer = colorScheme.middle;
+  return {
+    message: {
+      ...text(layer, "sans", { size: "md" }),
+      margin: { left: headerPadding, right: headerPadding },
+    },
+    actionMessage: {
+      ...text(layer, "sans", { size: "md" }),
+      margin: { left: headerPadding, top: 6, bottom: 6 },
+      hover: {
+        color: foreground(layer, "hovered"),
+      },
+    },
+    dismissButton: {
+      color: foreground(layer),
+      iconWidth: 8,
+      iconHeight: 8,
+      buttonWidth: 8,
+      buttonHeight: 8,
+      hover: {
+        color: foreground(layer, "hovered"),
+      },
+    },
+  };
+}