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