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