Skip to content

Commit

Permalink
Make app notifications appear in new workspaces + dismiss on all work…
Browse files Browse the repository at this point in the history
…spaces

The keymap error notifications got convoluted to support displaying the notification on startup. This change addresses it systemically for all future app notifications
  • Loading branch information
mgsloan committed Jan 21, 2025
1 parent c450cd5 commit f670953
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 190 deletions.
205 changes: 138 additions & 67 deletions crates/workspace/src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ use crate::{Toast, Workspace};
use anyhow::Context;
use anyhow::{anyhow, Result};
use gpui::{
svg, AppContext, AsyncWindowContext, ClipboardItem, DismissEvent, EventEmitter, PromptLevel,
Render, ScrollHandle, Task, View, ViewContext, VisualContext, WindowContext,
svg, AnyView, AppContext, AsyncWindowContext, ClipboardItem, DismissEvent, EventEmitter,
Global, PromptLevel, Render, ScrollHandle, Task, View, ViewContext, VisualContext,
WindowContext,
};
use std::rc::Rc;
use std::{any::TypeId, time::Duration};
use ui::{prelude::*, Tooltip};
use util::ResultExt;

pub fn init(cx: &mut AppContext) {
cx.set_global(GlobalAppNotifications {
app_notifications: Vec::new(),
})
}

#[derive(Debug, PartialEq, Clone)]
pub enum NotificationId {
Unique(TypeId),
Expand Down Expand Up @@ -54,17 +62,28 @@ impl Workspace {
cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
) {
self.dismiss_notification_internal(&id, cx);
self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
let notification = build_notification(cx);
cx.subscribe(&notification, {
let id = id.clone();
move |this, _, _: &DismissEvent, cx| {
this.dismiss_notification(&id, cx);
}
})
.detach();
notification.into()
});
}

let notification = build_notification(cx);
cx.subscribe(&notification, {
let id = id.clone();
move |this, _, _: &DismissEvent, cx| {
this.dismiss_notification_internal(&id, cx);
}
})
.detach();
self.notifications.push((id, notification.into()));
pub(crate) fn show_notification_without_handling_dismiss_events(
&mut self,
id: &NotificationId,
cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> AnyView,
) {
self.dismiss_notification(id, cx);
self.notifications
.push((id.clone(), build_notification(cx)));
cx.notify();
}

Expand All @@ -91,7 +110,14 @@ impl Workspace {
}

pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
self.dismiss_notification_internal(id, cx)
self.notifications.retain(|(existing_id, _)| {
if existing_id == id {
cx.notify();
false
} else {
true
}
});
}

pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
Expand Down Expand Up @@ -131,15 +157,16 @@ impl Workspace {
cx.notify();
}

fn dismiss_notification_internal(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
self.notifications.retain(|(existing_id, _)| {
if existing_id == id {
cx.notify();
false
} else {
true
}
});
pub fn show_initial_notifications(&mut self, cx: &mut ViewContext<Self>) {
for (id, build_notification) in cx
.global::<GlobalAppNotifications>()
.app_notifications
.clone()
{
self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
build_notification(cx)
});
}
}
}

Expand Down Expand Up @@ -467,66 +494,103 @@ pub mod simple_message_notification {
}
}

/// Shows a notification in the active workspace if there is one, otherwise shows it in all workspaces.
pub fn show_app_notification<V: Notification>(
/// Stores app notifications so that they can be shown in new workspaces.
struct GlobalAppNotifications {
app_notifications: Vec<(
NotificationId,
Rc<dyn Fn(&mut ViewContext<Workspace>) -> AnyView>,
)>,
}

impl Global for GlobalAppNotifications {}

impl GlobalAppNotifications {
pub fn insert(
&mut self,
id: NotificationId,
build_notification: Rc<dyn Fn(&mut ViewContext<Workspace>) -> AnyView>,
) {
self.remove(&id);
self.app_notifications.push((id, build_notification))
}

pub fn remove(&mut self, id: &NotificationId) {
self.app_notifications
.retain(|(existing_id, _)| existing_id != id);
}
}

/// Shows a notification in all workspaces. New workspaces will also receive the notification - this
/// is particularly to handle notifications that occur on initialization before any workspaces
/// exist. If the notification is dismissed within any workspace, it will be removed from all.
pub fn show_app_notification<V: Notification + 'static>(
id: NotificationId,
cx: &mut AppContext,
build_notification: impl Fn(&mut ViewContext<Workspace>) -> View<V>,
build_notification: impl Fn(&mut ViewContext<Workspace>) -> View<V> + 'static,
) -> Result<()> {
let workspaces_to_notify = if let Some(active_workspace_window) = cx
.active_window()
.and_then(|window| window.downcast::<Workspace>())
{
vec![active_workspace_window]
} else {
let mut workspaces_to_notify = Vec::new();
for window in cx.windows() {
if let Some(workspace_window) = window.downcast::<Workspace>() {
workspaces_to_notify.push(workspace_window);
}
// Handle dismiss events by removing the notification from all workspaces.
let build_notification: Rc<dyn Fn(&mut ViewContext<Workspace>) -> AnyView> = Rc::new({
let id = id.clone();
move |cx| {
let notification = build_notification(cx);
cx.subscribe(&notification, {
let id = id.clone();
move |_, _, _: &DismissEvent, cx| {
dismiss_app_notification(&id, cx);
}
})
.detach();
notification.into()
}
workspaces_to_notify
};
});

// Store the notification so that new workspaces also receive it.
cx.global_mut::<GlobalAppNotifications>()
.insert(id.clone(), build_notification.clone());

let mut notified = false;
let mut notify_errors = Vec::new();

for workspace_window in workspaces_to_notify {
let notify_result = workspace_window.update(cx, |workspace, cx| {
workspace.show_notification(id.clone(), cx, &build_notification);
});
match notify_result {
Ok(()) => notified = true,
Err(notify_err) => notify_errors.push(notify_err),
for window in cx.windows() {
if let Some(workspace_window) = window.downcast::<Workspace>() {
let notify_result = workspace_window.update(cx, |workspace, cx| {
workspace.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
build_notification(cx)
});
});
match notify_result {
Ok(()) => {}
Err(notify_err) => notify_errors.push(notify_err),
}
}
}

if notified {
if notify_errors.is_empty() {
Ok(())
} else {
if notify_errors.is_empty() {
Err(anyhow!("Found no workspaces to show notification."))
} else {
Err(anyhow!(
"No workspaces were able to show notification. Errors:\n\n{}",
notify_errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n\n")
))
}
Err(anyhow!(
"No workspaces were able to show notification. Errors:\n\n{}",
notify_errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n\n")
))
}
}

pub fn dismiss_app_notification(id: &NotificationId, cx: &mut AppContext) {
cx.global_mut::<GlobalAppNotifications>().remove(id);
for window in cx.windows() {
if let Some(workspace_window) = window.downcast::<Workspace>() {
workspace_window
.update(cx, |workspace, cx| {
workspace.dismiss_notification(&id, cx);
let id = id.clone();
// This spawn is necessary in order to dismiss the notification on which the click
// occurred, because in that case we're already in the middle of an update.
cx.spawn(move |mut cx| async move {
workspace_window.update(&mut cx, |workspace, cx| {
workspace.dismiss_notification(&id, cx)
})
.ok();
})
.detach_and_log_err(cx);
}
}
}
Expand Down Expand Up @@ -556,7 +620,7 @@ where
match self {
Ok(value) => Some(value),
Err(err) => {
log::error!("TODO {err:?}");
log::error!("Showing error notification in workspace: {err:?}");
workspace.show_error(&err, cx);
None
}
Expand Down Expand Up @@ -584,10 +648,17 @@ where
Ok(value) => Some(value),
Err(err) => {
let message: SharedString = format!("Error: {err}").into();
show_app_notification(workspace_error_notification_id(), cx, |cx| {
cx.new_view(|_cx| ErrorMessagePrompt::new(message.clone()))
log::error!("Showing error notification in app: {message}");
show_app_notification(workspace_error_notification_id(), cx, {
let message = message.clone();
move |cx| {
cx.new_view({
let message = message.clone();
move |_cx| ErrorMessagePrompt::new(message)
})
}
})
.with_context(|| format!("Showing error notification: {message}"))
.with_context(|| format!("Error while showing error notification: {message}"))
.log_err();
None
}
Expand Down
2 changes: 2 additions & 0 deletions crates/workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ fn prompt_and_open_paths(

pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
notifications::init(cx);
theme_preview::init(cx);

cx.on_action(Workspace::close_global);
Expand Down Expand Up @@ -1056,6 +1057,7 @@ impl Workspace {

cx.defer(|this, cx| {
this.update_window_title(cx);
this.show_initial_notifications(cx);
});
Workspace {
weak_self: weak_handle.clone(),
Expand Down
Loading

0 comments on commit f670953

Please sign in to comment.