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