Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make app notifications appear in new workspaces + dismiss on all workspaces #23432

Merged
merged 2 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 145 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,33 @@ 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()));
/// Shows a notification in this workspace's window. Caller must handle dismiss.
///
/// This exists so that the `build_notification` closures stored for app notifications can
/// return `AnyView`. Subscribing to events from an `AnyView` is not supported, so instead that
/// responsibility is pushed to the caller where the `V` type is known.
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 +115,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 +162,18 @@ 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>) {
// Allow absence of the global so that tests don't need to initialize it.
let app_notifications = cx
.try_global::<GlobalAppNotifications>()
.iter()
.flat_map(|global| global.app_notifications.iter().cloned())
.collect::<Vec<_>>();
for (id, build_notification) in app_notifications {
self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
build_notification(cx)
});
}
}
}

Expand Down Expand Up @@ -467,66 +501,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 +627,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 +655,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
Loading