notification_panel.rs

  1use crate::{NotificationPanelSettings, chat_panel::ChatPanel};
  2use anyhow::Result;
  3use channel::ChannelStore;
  4use client::{ChannelId, Client, Notification, User, UserStore};
  5use collections::HashMap;
  6use db::kvp::KEY_VALUE_STORE;
  7use futures::StreamExt;
  8use gpui::{
  9    AnyElement, App, AsyncWindowContext, Context, CursorStyle, DismissEvent, Element, Entity,
 10    EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
 11    ListScrollEvent, ListState, ParentElement, Render, StatefulInteractiveElement, Styled, Task,
 12    WeakEntity, Window, actions, div, img, list, px,
 13};
 14use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
 15use project::Fs;
 16use rpc::proto;
 17use serde::{Deserialize, Serialize};
 18use settings::{Settings, SettingsStore};
 19use std::{sync::Arc, time::Duration};
 20use time::{OffsetDateTime, UtcOffset};
 21use ui::{
 22    Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
 23};
 24use util::{ResultExt, TryFutureExt};
 25use workspace::SuppressNotification;
 26use workspace::notifications::{
 27    Notification as WorkspaceNotification, NotificationId, SuppressEvent,
 28};
 29use workspace::{
 30    Workspace,
 31    dock::{DockPosition, Panel, PanelEvent},
 32};
 33
 34const LOADING_THRESHOLD: usize = 30;
 35const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
 36const TOAST_DURATION: Duration = Duration::from_secs(5);
 37const NOTIFICATION_PANEL_KEY: &str = "NotificationPanel";
 38
 39pub struct NotificationPanel {
 40    client: Arc<Client>,
 41    user_store: Entity<UserStore>,
 42    channel_store: Entity<ChannelStore>,
 43    notification_store: Entity<NotificationStore>,
 44    fs: Arc<dyn Fs>,
 45    width: Option<Pixels>,
 46    active: bool,
 47    notification_list: ListState,
 48    pending_serialization: Task<Option<()>>,
 49    subscriptions: Vec<gpui::Subscription>,
 50    workspace: WeakEntity<Workspace>,
 51    current_notification_toast: Option<(u64, Task<()>)>,
 52    local_timezone: UtcOffset,
 53    focus_handle: FocusHandle,
 54    mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
 55    unseen_notifications: Vec<NotificationEntry>,
 56}
 57
 58#[derive(Serialize, Deserialize)]
 59struct SerializedNotificationPanel {
 60    width: Option<Pixels>,
 61}
 62
 63#[derive(Debug)]
 64pub enum Event {
 65    DockPositionChanged,
 66    Focus,
 67    Dismissed,
 68}
 69
 70pub struct NotificationPresenter {
 71    pub actor: Option<Arc<client::User>>,
 72    pub text: String,
 73    pub icon: &'static str,
 74    pub needs_response: bool,
 75    pub can_navigate: bool,
 76}
 77
 78actions!(notification_panel, [ToggleFocus]);
 79
 80pub fn init(cx: &mut App) {
 81    cx.observe_new(|workspace: &mut Workspace, _, _| {
 82        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
 83            workspace.toggle_panel_focus::<NotificationPanel>(window, cx);
 84        });
 85    })
 86    .detach();
 87}
 88
 89impl NotificationPanel {
 90    pub fn new(
 91        workspace: &mut Workspace,
 92        window: &mut Window,
 93        cx: &mut Context<Workspace>,
 94    ) -> Entity<Self> {
 95        let fs = workspace.app_state().fs.clone();
 96        let client = workspace.app_state().client.clone();
 97        let user_store = workspace.app_state().user_store.clone();
 98        let workspace_handle = workspace.weak_handle();
 99
100        cx.new(|cx| {
101            let mut status = client.status();
102            cx.spawn_in(window, async move |this, cx| {
103                while (status.next().await).is_some() {
104                    if this
105                        .update(cx, |_: &mut Self, cx| {
106                            cx.notify();
107                        })
108                        .is_err()
109                    {
110                        break;
111                    }
112                }
113            })
114            .detach();
115
116            let entity = cx.entity().downgrade();
117            let notification_list =
118                ListState::new(0, ListAlignment::Top, px(1000.), move |ix, window, cx| {
119                    entity
120                        .upgrade()
121                        .and_then(|entity| {
122                            entity.update(cx, |this, cx| this.render_notification(ix, window, cx))
123                        })
124                        .unwrap_or_else(|| div().into_any())
125                });
126            notification_list.set_scroll_handler(cx.listener(
127                |this, event: &ListScrollEvent, _, cx| {
128                    if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
129                        if let Some(task) = this
130                            .notification_store
131                            .update(cx, |store, cx| store.load_more_notifications(false, cx))
132                        {
133                            task.detach();
134                        }
135                    }
136                },
137            ));
138
139            let local_offset = chrono::Local::now().offset().local_minus_utc();
140            let mut this = Self {
141                fs,
142                client,
143                user_store,
144                local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
145                channel_store: ChannelStore::global(cx),
146                notification_store: NotificationStore::global(cx),
147                notification_list,
148                pending_serialization: Task::ready(None),
149                workspace: workspace_handle,
150                focus_handle: cx.focus_handle(),
151                current_notification_toast: None,
152                subscriptions: Vec::new(),
153                active: false,
154                mark_as_read_tasks: HashMap::default(),
155                width: None,
156                unseen_notifications: Vec::new(),
157            };
158
159            let mut old_dock_position = this.position(window, cx);
160            this.subscriptions.extend([
161                cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
162                cx.subscribe_in(
163                    &this.notification_store,
164                    window,
165                    Self::on_notification_event,
166                ),
167                cx.observe_global_in::<SettingsStore>(
168                    window,
169                    move |this: &mut Self, window, cx| {
170                        let new_dock_position = this.position(window, cx);
171                        if new_dock_position != old_dock_position {
172                            old_dock_position = new_dock_position;
173                            cx.emit(Event::DockPositionChanged);
174                        }
175                        cx.notify();
176                    },
177                ),
178            ]);
179            this
180        })
181    }
182
183    pub fn load(
184        workspace: WeakEntity<Workspace>,
185        cx: AsyncWindowContext,
186    ) -> Task<Result<Entity<Self>>> {
187        cx.spawn(async move |cx| {
188            let serialized_panel = if let Some(panel) = cx
189                .background_spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
190                .await
191                .log_err()
192                .flatten()
193            {
194                Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
195            } else {
196                None
197            };
198
199            workspace.update_in(cx, |workspace, window, cx| {
200                let panel = Self::new(workspace, window, cx);
201                if let Some(serialized_panel) = serialized_panel {
202                    panel.update(cx, |panel, cx| {
203                        panel.width = serialized_panel.width.map(|w| w.round());
204                        cx.notify();
205                    });
206                }
207                panel
208            })
209        })
210    }
211
212    fn serialize(&mut self, cx: &mut Context<Self>) {
213        let width = self.width;
214        self.pending_serialization = cx.background_spawn(
215            async move {
216                KEY_VALUE_STORE
217                    .write_kvp(
218                        NOTIFICATION_PANEL_KEY.into(),
219                        serde_json::to_string(&SerializedNotificationPanel { width })?,
220                    )
221                    .await?;
222                anyhow::Ok(())
223            }
224            .log_err(),
225        );
226    }
227
228    fn render_notification(
229        &mut self,
230        ix: usize,
231        window: &mut Window,
232        cx: &mut Context<Self>,
233    ) -> Option<AnyElement> {
234        let entry = self.notification_store.read(cx).notification_at(ix)?;
235        let notification_id = entry.id;
236        let now = OffsetDateTime::now_utc();
237        let timestamp = entry.timestamp;
238        let NotificationPresenter {
239            actor,
240            text,
241            needs_response,
242            can_navigate,
243            ..
244        } = self.present_notification(entry, cx)?;
245
246        let response = entry.response;
247        let notification = entry.notification.clone();
248
249        if self.active && !entry.is_read {
250            self.did_render_notification(notification_id, &notification, window, cx);
251        }
252
253        let relative_timestamp = time_format::format_localized_timestamp(
254            timestamp,
255            now,
256            self.local_timezone,
257            time_format::TimestampFormat::Relative,
258        );
259
260        let absolute_timestamp = time_format::format_localized_timestamp(
261            timestamp,
262            now,
263            self.local_timezone,
264            time_format::TimestampFormat::Absolute,
265        );
266
267        Some(
268            div()
269                .id(ix)
270                .flex()
271                .flex_row()
272                .size_full()
273                .px_2()
274                .py_1()
275                .gap_2()
276                .hover(|style| style.bg(cx.theme().colors().element_hover))
277                .when(can_navigate, |el| {
278                    el.cursor(CursorStyle::PointingHand).on_click({
279                        let notification = notification.clone();
280                        cx.listener(move |this, _, window, cx| {
281                            this.did_click_notification(&notification, window, cx)
282                        })
283                    })
284                })
285                .children(actor.map(|actor| {
286                    img(actor.avatar_uri.clone())
287                        .flex_none()
288                        .w_8()
289                        .h_8()
290                        .rounded_full()
291                }))
292                .child(
293                    v_flex()
294                        .gap_1()
295                        .size_full()
296                        .overflow_hidden()
297                        .child(Label::new(text.clone()))
298                        .child(
299                            h_flex()
300                                .child(
301                                    div()
302                                        .id("notification_timestamp")
303                                        .hover(|style| {
304                                            style
305                                                .bg(cx.theme().colors().element_selected)
306                                                .rounded_sm()
307                                        })
308                                        .child(Label::new(relative_timestamp).color(Color::Muted))
309                                        .tooltip(move |_, cx| {
310                                            Tooltip::simple(absolute_timestamp.clone(), cx)
311                                        }),
312                                )
313                                .children(if let Some(is_accepted) = response {
314                                    Some(div().flex().flex_grow().justify_end().child(Label::new(
315                                        if is_accepted {
316                                            "You accepted"
317                                        } else {
318                                            "You declined"
319                                        },
320                                    )))
321                                } else if needs_response {
322                                    Some(
323                                        h_flex()
324                                            .flex_grow()
325                                            .justify_end()
326                                            .child(Button::new("decline", "Decline").on_click({
327                                                let notification = notification.clone();
328                                                let entity = cx.entity().clone();
329                                                move |_, _, cx| {
330                                                    entity.update(cx, |this, cx| {
331                                                        this.respond_to_notification(
332                                                            notification.clone(),
333                                                            false,
334                                                            cx,
335                                                        )
336                                                    });
337                                                }
338                                            }))
339                                            .child(Button::new("accept", "Accept").on_click({
340                                                let notification = notification.clone();
341                                                let entity = cx.entity().clone();
342                                                move |_, _, cx| {
343                                                    entity.update(cx, |this, cx| {
344                                                        this.respond_to_notification(
345                                                            notification.clone(),
346                                                            true,
347                                                            cx,
348                                                        )
349                                                    });
350                                                }
351                                            })),
352                                    )
353                                } else {
354                                    None
355                                }),
356                        ),
357                )
358                .into_any(),
359        )
360    }
361
362    fn present_notification(
363        &self,
364        entry: &NotificationEntry,
365        cx: &App,
366    ) -> Option<NotificationPresenter> {
367        let user_store = self.user_store.read(cx);
368        let channel_store = self.channel_store.read(cx);
369        match entry.notification {
370            Notification::ContactRequest { sender_id } => {
371                let requester = user_store.get_cached_user(sender_id)?;
372                Some(NotificationPresenter {
373                    icon: "icons/plus.svg",
374                    text: format!("{} wants to add you as a contact", requester.github_login),
375                    needs_response: user_store.has_incoming_contact_request(requester.id),
376                    actor: Some(requester),
377                    can_navigate: false,
378                })
379            }
380            Notification::ContactRequestAccepted { responder_id } => {
381                let responder = user_store.get_cached_user(responder_id)?;
382                Some(NotificationPresenter {
383                    icon: "icons/plus.svg",
384                    text: format!("{} accepted your contact invite", responder.github_login),
385                    needs_response: false,
386                    actor: Some(responder),
387                    can_navigate: false,
388                })
389            }
390            Notification::ChannelInvitation {
391                ref channel_name,
392                channel_id,
393                inviter_id,
394            } => {
395                let inviter = user_store.get_cached_user(inviter_id)?;
396                Some(NotificationPresenter {
397                    icon: "icons/hash.svg",
398                    text: format!(
399                        "{} invited you to join the #{channel_name} channel",
400                        inviter.github_login
401                    ),
402                    needs_response: channel_store.has_channel_invitation(ChannelId(channel_id)),
403                    actor: Some(inviter),
404                    can_navigate: false,
405                })
406            }
407            Notification::ChannelMessageMention {
408                sender_id,
409                channel_id,
410                message_id,
411            } => {
412                let sender = user_store.get_cached_user(sender_id)?;
413                let channel = channel_store.channel_for_id(ChannelId(channel_id))?;
414                let message = self
415                    .notification_store
416                    .read(cx)
417                    .channel_message_for_id(message_id)?;
418                Some(NotificationPresenter {
419                    icon: "icons/conversations.svg",
420                    text: format!(
421                        "{} mentioned you in #{}:\n{}",
422                        sender.github_login, channel.name, message.body,
423                    ),
424                    needs_response: false,
425                    actor: Some(sender),
426                    can_navigate: true,
427                })
428            }
429        }
430    }
431
432    fn did_render_notification(
433        &mut self,
434        notification_id: u64,
435        notification: &Notification,
436        window: &mut Window,
437        cx: &mut Context<Self>,
438    ) {
439        let should_mark_as_read = match notification {
440            Notification::ContactRequestAccepted { .. } => true,
441            Notification::ContactRequest { .. }
442            | Notification::ChannelInvitation { .. }
443            | Notification::ChannelMessageMention { .. } => false,
444        };
445
446        if should_mark_as_read {
447            self.mark_as_read_tasks
448                .entry(notification_id)
449                .or_insert_with(|| {
450                    let client = self.client.clone();
451                    cx.spawn_in(window, async move |this, cx| {
452                        cx.background_executor().timer(MARK_AS_READ_DELAY).await;
453                        client
454                            .request(proto::MarkNotificationRead { notification_id })
455                            .await?;
456                        this.update(cx, |this, _| {
457                            this.mark_as_read_tasks.remove(&notification_id);
458                        })?;
459                        Ok(())
460                    })
461                });
462        }
463    }
464
465    fn did_click_notification(
466        &mut self,
467        notification: &Notification,
468        window: &mut Window,
469        cx: &mut Context<Self>,
470    ) {
471        if let Notification::ChannelMessageMention {
472            message_id,
473            channel_id,
474            ..
475        } = notification.clone()
476        {
477            if let Some(workspace) = self.workspace.upgrade() {
478                window.defer(cx, move |window, cx| {
479                    workspace.update(cx, |workspace, cx| {
480                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
481                            panel.update(cx, |panel, cx| {
482                                panel
483                                    .select_channel(ChannelId(channel_id), Some(message_id), cx)
484                                    .detach_and_log_err(cx);
485                            });
486                        }
487                    });
488                });
489            }
490        }
491    }
492
493    fn is_showing_notification(&self, notification: &Notification, cx: &mut Context<Self>) -> bool {
494        if !self.active {
495            return false;
496        }
497
498        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
499            if let Some(workspace) = self.workspace.upgrade() {
500                return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
501                    let panel = panel.read(cx);
502                    panel.is_scrolled_to_bottom()
503                        && panel
504                            .active_chat()
505                            .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id)
506                } else {
507                    false
508                };
509            }
510        }
511
512        false
513    }
514
515    fn on_notification_event(
516        &mut self,
517        _: &Entity<NotificationStore>,
518        event: &NotificationEvent,
519        window: &mut Window,
520        cx: &mut Context<Self>,
521    ) {
522        match event {
523            NotificationEvent::NewNotification { entry } => {
524                if !self.is_showing_notification(&entry.notification, cx) {
525                    self.unseen_notifications.push(entry.clone());
526                }
527                self.add_toast(entry, window, cx);
528            }
529            NotificationEvent::NotificationRemoved { entry }
530            | NotificationEvent::NotificationRead { entry } => {
531                self.unseen_notifications.retain(|n| n.id != entry.id);
532                self.remove_toast(entry.id, cx);
533            }
534            NotificationEvent::NotificationsUpdated {
535                old_range,
536                new_count,
537            } => {
538                self.notification_list.splice(old_range.clone(), *new_count);
539                cx.notify();
540            }
541        }
542    }
543
544    fn add_toast(
545        &mut self,
546        entry: &NotificationEntry,
547        window: &mut Window,
548        cx: &mut Context<Self>,
549    ) {
550        if self.is_showing_notification(&entry.notification, cx) {
551            return;
552        }
553
554        let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
555        else {
556            return;
557        };
558
559        let notification_id = entry.id;
560        self.current_notification_toast = Some((
561            notification_id,
562            cx.spawn_in(window, async move |this, cx| {
563                cx.background_executor().timer(TOAST_DURATION).await;
564                this.update(cx, |this, cx| this.remove_toast(notification_id, cx))
565                    .ok();
566            }),
567        ));
568
569        self.workspace
570            .update(cx, |workspace, cx| {
571                let id = NotificationId::unique::<NotificationToast>();
572
573                workspace.dismiss_notification(&id, cx);
574                workspace.show_notification(id, cx, |cx| {
575                    let workspace = cx.entity().downgrade();
576                    cx.new(|cx| NotificationToast {
577                        notification_id,
578                        actor,
579                        text,
580                        workspace,
581                        focus_handle: cx.focus_handle(),
582                    })
583                })
584            })
585            .ok();
586    }
587
588    fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) {
589        if let Some((current_id, _)) = &self.current_notification_toast {
590            if *current_id == notification_id {
591                self.current_notification_toast.take();
592                self.workspace
593                    .update(cx, |workspace, cx| {
594                        let id = NotificationId::unique::<NotificationToast>();
595                        workspace.dismiss_notification(&id, cx)
596                    })
597                    .ok();
598            }
599        }
600    }
601
602    fn respond_to_notification(
603        &mut self,
604        notification: Notification,
605        response: bool,
606
607        cx: &mut Context<Self>,
608    ) {
609        self.notification_store.update(cx, |store, cx| {
610            store.respond_to_notification(notification, response, cx);
611        });
612    }
613}
614
615impl Render for NotificationPanel {
616    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
617        v_flex()
618            .size_full()
619            .child(
620                h_flex()
621                    .justify_between()
622                    .px_2()
623                    .py_1()
624                    // Match the height of the tab bar so they line up.
625                    .h(Tab::container_height(cx))
626                    .border_b_1()
627                    .border_color(cx.theme().colors().border)
628                    .child(Label::new("Notifications"))
629                    .child(Icon::new(IconName::Envelope)),
630            )
631            .map(|this| {
632                if self.client.user_id().is_none() {
633                    this.child(
634                        v_flex()
635                            .gap_2()
636                            .p_4()
637                            .child(
638                                Button::new("sign_in_prompt_button", "Sign in")
639                                    .icon_color(Color::Muted)
640                                    .icon(IconName::Github)
641                                    .icon_position(IconPosition::Start)
642                                    .style(ButtonStyle::Filled)
643                                    .full_width()
644                                    .on_click({
645                                        let client = self.client.clone();
646                                        move |_, window, cx| {
647                                            let client = client.clone();
648                                            window
649                                                .spawn(cx, async move |cx| {
650                                                    client
651                                                        .authenticate_and_connect(true, &cx)
652                                                        .log_err()
653                                                        .await;
654                                                })
655                                                .detach()
656                                        }
657                                    }),
658                            )
659                            .child(
660                                div().flex().w_full().items_center().child(
661                                    Label::new("Sign in to view notifications.")
662                                        .color(Color::Muted)
663                                        .size(LabelSize::Small),
664                                ),
665                            ),
666                    )
667                } else if self.notification_list.item_count() == 0 {
668                    this.child(
669                        v_flex().p_4().child(
670                            div().flex().w_full().items_center().child(
671                                Label::new("You have no notifications.")
672                                    .color(Color::Muted)
673                                    .size(LabelSize::Small),
674                            ),
675                        ),
676                    )
677                } else {
678                    this.child(list(self.notification_list.clone()).size_full())
679                }
680            })
681    }
682}
683
684impl Focusable for NotificationPanel {
685    fn focus_handle(&self, _: &App) -> FocusHandle {
686        self.focus_handle.clone()
687    }
688}
689
690impl EventEmitter<Event> for NotificationPanel {}
691impl EventEmitter<PanelEvent> for NotificationPanel {}
692
693impl Panel for NotificationPanel {
694    fn persistent_name() -> &'static str {
695        "NotificationPanel"
696    }
697
698    fn position(&self, _: &Window, cx: &App) -> DockPosition {
699        NotificationPanelSettings::get_global(cx).dock
700    }
701
702    fn position_is_valid(&self, position: DockPosition) -> bool {
703        matches!(position, DockPosition::Left | DockPosition::Right)
704    }
705
706    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
707        settings::update_settings_file::<NotificationPanelSettings>(
708            self.fs.clone(),
709            cx,
710            move |settings, _| settings.dock = Some(position),
711        );
712    }
713
714    fn size(&self, _: &Window, cx: &App) -> Pixels {
715        self.width
716            .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
717    }
718
719    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
720        self.width = size;
721        self.serialize(cx);
722        cx.notify();
723    }
724
725    fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context<Self>) {
726        self.active = active;
727
728        if self.active {
729            self.unseen_notifications = Vec::new();
730            cx.notify();
731        }
732
733        if self.notification_store.read(cx).notification_count() == 0 {
734            cx.emit(Event::Dismissed);
735        }
736    }
737
738    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
739        let show_button = NotificationPanelSettings::get_global(cx).button;
740        if !show_button {
741            return None;
742        }
743
744        if self.unseen_notifications.is_empty() {
745            return Some(IconName::Bell);
746        }
747
748        Some(IconName::BellDot)
749    }
750
751    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
752        Some("Notification Panel")
753    }
754
755    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
756        let count = self.notification_store.read(cx).unread_notification_count();
757        if count == 0 {
758            None
759        } else {
760            Some(count.to_string())
761        }
762    }
763
764    fn toggle_action(&self) -> Box<dyn gpui::Action> {
765        Box::new(ToggleFocus)
766    }
767
768    fn activation_priority(&self) -> u32 {
769        8
770    }
771}
772
773pub struct NotificationToast {
774    notification_id: u64,
775    actor: Option<Arc<User>>,
776    text: String,
777    workspace: WeakEntity<Workspace>,
778    focus_handle: FocusHandle,
779}
780
781impl Focusable for NotificationToast {
782    fn focus_handle(&self, _cx: &App) -> FocusHandle {
783        self.focus_handle.clone()
784    }
785}
786
787impl WorkspaceNotification for NotificationToast {}
788
789impl NotificationToast {
790    fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
791        let workspace = self.workspace.clone();
792        let notification_id = self.notification_id;
793        window.defer(cx, move |window, cx| {
794            workspace
795                .update(cx, |workspace, cx| {
796                    if let Some(panel) = workspace.focus_panel::<NotificationPanel>(window, cx) {
797                        panel.update(cx, |panel, cx| {
798                            let store = panel.notification_store.read(cx);
799                            if let Some(entry) = store.notification_for_id(notification_id) {
800                                panel.did_click_notification(
801                                    &entry.clone().notification,
802                                    window,
803                                    cx,
804                                );
805                            }
806                        });
807                    }
808                })
809                .ok();
810        })
811    }
812}
813
814impl Render for NotificationToast {
815    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
816        let user = self.actor.clone();
817
818        h_flex()
819            .id("notification_panel_toast")
820            .elevation_3(cx)
821            .p_2()
822            .gap_2()
823            .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
824            .child(Label::new(self.text.clone()))
825            .child(
826                IconButton::new("close", IconName::Close)
827                    .tooltip(|window, cx| Tooltip::for_action("Close", &menu::Cancel, window, cx))
828                    .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
829            )
830            .child(
831                IconButton::new("suppress", IconName::SquareMinus)
832                    .tooltip(|window, cx| {
833                        Tooltip::for_action(
834                            "Do not show until restart",
835                            &SuppressNotification,
836                            window,
837                            cx,
838                        )
839                    })
840                    .on_click(cx.listener(|_, _, _, cx| cx.emit(SuppressEvent))),
841            )
842            .on_click(cx.listener(|this, _, window, cx| {
843                this.focus_notification_panel(window, cx);
844                cx.emit(DismissEvent);
845            }))
846    }
847}
848
849impl EventEmitter<DismissEvent> for NotificationToast {}
850impl EventEmitter<SuppressEvent> for NotificationToast {}