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").on_click({
323                                                let notification = notification.clone();
324                                                let entity = cx.entity();
325                                                move |_, _, cx| {
326                                                    entity.update(cx, |this, cx| {
327                                                        this.respond_to_notification(
328                                                            notification.clone(),
329                                                            false,
330                                                            cx,
331                                                        )
332                                                    });
333                                                }
334                                            }))
335                                            .child(Button::new("accept", "Accept").on_click({
336                                                let notification = notification.clone();
337                                                let entity = cx.entity();
338                                                move |_, _, cx| {
339                                                    entity.update(cx, |this, cx| {
340                                                        this.respond_to_notification(
341                                                            notification.clone(),
342                                                            true,
343                                                            cx,
344                                                        )
345                                                    });
346                                                }
347                                            })),
348                                    )
349                                } else {
350                                    None
351                                }),
352                        ),
353                )
354                .into_any(),
355        )
356    }
357
358    fn present_notification(
359        &self,
360        entry: &NotificationEntry,
361        cx: &App,
362    ) -> Option<NotificationPresenter> {
363        let user_store = self.user_store.read(cx);
364        let channel_store = self.channel_store.read(cx);
365        match entry.notification {
366            Notification::ContactRequest { sender_id } => {
367                let requester = user_store.get_cached_user(sender_id)?;
368                Some(NotificationPresenter {
369                    icon: "icons/plus.svg",
370                    text: format!("{} wants to add you as a contact", requester.github_login),
371                    needs_response: user_store.has_incoming_contact_request(requester.id),
372                    actor: Some(requester),
373                    can_navigate: false,
374                })
375            }
376            Notification::ContactRequestAccepted { responder_id } => {
377                let responder = user_store.get_cached_user(responder_id)?;
378                Some(NotificationPresenter {
379                    icon: "icons/plus.svg",
380                    text: format!("{} accepted your contact invite", responder.github_login),
381                    needs_response: false,
382                    actor: Some(responder),
383                    can_navigate: false,
384                })
385            }
386            Notification::ChannelInvitation {
387                ref channel_name,
388                channel_id,
389                inviter_id,
390            } => {
391                let inviter = user_store.get_cached_user(inviter_id)?;
392                Some(NotificationPresenter {
393                    icon: "icons/hash.svg",
394                    text: format!(
395                        "{} invited you to join the #{channel_name} channel",
396                        inviter.github_login
397                    ),
398                    needs_response: channel_store.has_channel_invitation(ChannelId(channel_id)),
399                    actor: Some(inviter),
400                    can_navigate: false,
401                })
402            }
403            Notification::ChannelMessageMention {
404                sender_id,
405                channel_id,
406                message_id,
407            } => {
408                let sender = user_store.get_cached_user(sender_id)?;
409                let channel = channel_store.channel_for_id(ChannelId(channel_id))?;
410                let message = self
411                    .notification_store
412                    .read(cx)
413                    .channel_message_for_id(message_id)?;
414                Some(NotificationPresenter {
415                    icon: "icons/conversations.svg",
416                    text: format!(
417                        "{} mentioned you in #{}:\n{}",
418                        sender.github_login, channel.name, message.body,
419                    ),
420                    needs_response: false,
421                    actor: Some(sender),
422                    can_navigate: true,
423                })
424            }
425        }
426    }
427
428    fn did_render_notification(
429        &mut self,
430        notification_id: u64,
431        notification: &Notification,
432        window: &mut Window,
433        cx: &mut Context<Self>,
434    ) {
435        let should_mark_as_read = match notification {
436            Notification::ContactRequestAccepted { .. } => true,
437            Notification::ContactRequest { .. }
438            | Notification::ChannelInvitation { .. }
439            | Notification::ChannelMessageMention { .. } => false,
440        };
441
442        if should_mark_as_read {
443            self.mark_as_read_tasks
444                .entry(notification_id)
445                .or_insert_with(|| {
446                    let client = self.client.clone();
447                    cx.spawn_in(window, async move |this, cx| {
448                        cx.background_executor().timer(MARK_AS_READ_DELAY).await;
449                        client
450                            .request(proto::MarkNotificationRead { notification_id })
451                            .await?;
452                        this.update(cx, |this, _| {
453                            this.mark_as_read_tasks.remove(&notification_id);
454                        })?;
455                        Ok(())
456                    })
457                });
458        }
459    }
460
461    fn did_click_notification(
462        &mut self,
463        notification: &Notification,
464        window: &mut Window,
465        cx: &mut Context<Self>,
466    ) {
467        if let Notification::ChannelMessageMention {
468            message_id,
469            channel_id,
470            ..
471        } = notification.clone()
472        {
473            if let Some(workspace) = self.workspace.upgrade() {
474                window.defer(cx, move |window, cx| {
475                    workspace.update(cx, |workspace, cx| {
476                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
477                            panel.update(cx, |panel, cx| {
478                                panel
479                                    .select_channel(ChannelId(channel_id), Some(message_id), cx)
480                                    .detach_and_log_err(cx);
481                            });
482                        }
483                    });
484                });
485            }
486        }
487    }
488
489    fn is_showing_notification(&self, notification: &Notification, cx: &mut Context<Self>) -> bool {
490        if !self.active {
491            return false;
492        }
493
494        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
495            if let Some(workspace) = self.workspace.upgrade() {
496                return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
497                    let panel = panel.read(cx);
498                    panel.is_scrolled_to_bottom()
499                        && panel
500                            .active_chat()
501                            .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id)
502                } else {
503                    false
504                };
505            }
506        }
507
508        false
509    }
510
511    fn on_notification_event(
512        &mut self,
513        _: &Entity<NotificationStore>,
514        event: &NotificationEvent,
515        window: &mut Window,
516        cx: &mut Context<Self>,
517    ) {
518        match event {
519            NotificationEvent::NewNotification { entry } => {
520                if !self.is_showing_notification(&entry.notification, cx) {
521                    self.unseen_notifications.push(entry.clone());
522                }
523                self.add_toast(entry, window, cx);
524            }
525            NotificationEvent::NotificationRemoved { entry }
526            | NotificationEvent::NotificationRead { entry } => {
527                self.unseen_notifications.retain(|n| n.id != entry.id);
528                self.remove_toast(entry.id, cx);
529            }
530            NotificationEvent::NotificationsUpdated {
531                old_range,
532                new_count,
533            } => {
534                self.notification_list.splice(old_range.clone(), *new_count);
535                cx.notify();
536            }
537        }
538    }
539
540    fn add_toast(
541        &mut self,
542        entry: &NotificationEntry,
543        window: &mut Window,
544        cx: &mut Context<Self>,
545    ) {
546        if self.is_showing_notification(&entry.notification, cx) {
547            return;
548        }
549
550        let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
551        else {
552            return;
553        };
554
555        let notification_id = entry.id;
556        self.current_notification_toast = Some((
557            notification_id,
558            cx.spawn_in(window, async move |this, cx| {
559                cx.background_executor().timer(TOAST_DURATION).await;
560                this.update(cx, |this, cx| this.remove_toast(notification_id, cx))
561                    .ok();
562            }),
563        ));
564
565        self.workspace
566            .update(cx, |workspace, cx| {
567                let id = NotificationId::unique::<NotificationToast>();
568
569                workspace.dismiss_notification(&id, cx);
570                workspace.show_notification(id, cx, |cx| {
571                    let workspace = cx.entity().downgrade();
572                    cx.new(|cx| NotificationToast {
573                        notification_id,
574                        actor,
575                        text,
576                        workspace,
577                        focus_handle: cx.focus_handle(),
578                    })
579                })
580            })
581            .ok();
582    }
583
584    fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) {
585        if let Some((current_id, _)) = &self.current_notification_toast {
586            if *current_id == notification_id {
587                self.current_notification_toast.take();
588                self.workspace
589                    .update(cx, |workspace, cx| {
590                        let id = NotificationId::unique::<NotificationToast>();
591                        workspace.dismiss_notification(&id, cx)
592                    })
593                    .ok();
594            }
595        }
596    }
597
598    fn respond_to_notification(
599        &mut self,
600        notification: Notification,
601        response: bool,
602
603        cx: &mut Context<Self>,
604    ) {
605        self.notification_store.update(cx, |store, cx| {
606            store.respond_to_notification(notification, response, cx);
607        });
608    }
609}
610
611impl Render for NotificationPanel {
612    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
613        v_flex()
614            .size_full()
615            .child(
616                h_flex()
617                    .justify_between()
618                    .px_2()
619                    .py_1()
620                    // Match the height of the tab bar so they line up.
621                    .h(Tab::container_height(cx))
622                    .border_b_1()
623                    .border_color(cx.theme().colors().border)
624                    .child(Label::new("Notifications"))
625                    .child(Icon::new(IconName::Envelope)),
626            )
627            .map(|this| {
628                if !self.client.status().borrow().is_connected() {
629                    this.child(
630                        v_flex()
631                            .gap_2()
632                            .p_4()
633                            .child(
634                                Button::new("connect_prompt_button", "Connect")
635                                    .icon_color(Color::Muted)
636                                    .icon(IconName::Github)
637                                    .icon_position(IconPosition::Start)
638                                    .style(ButtonStyle::Filled)
639                                    .full_width()
640                                    .on_click({
641                                        let client = self.client.clone();
642                                        move |_, window, cx| {
643                                            let client = client.clone();
644                                            window
645                                                .spawn(cx, async move |cx| {
646                                                    match client.connect(true, &cx).await {
647                                                        util::ConnectionResult::Timeout => {
648                                                            log::error!("Connection timeout");
649                                                        }
650                                                        util::ConnectionResult::ConnectionReset => {
651                                                            log::error!("Connection reset");
652                                                        }
653                                                        util::ConnectionResult::Result(r) => {
654                                                            r.log_err();
655                                                        }
656                                                    }
657                                                })
658                                                .detach()
659                                        }
660                                    }),
661                            )
662                            .child(
663                                div().flex().w_full().items_center().child(
664                                    Label::new("Connect to view notifications.")
665                                        .color(Color::Muted)
666                                        .size(LabelSize::Small),
667                                ),
668                            ),
669                    )
670                } else if self.notification_list.item_count() == 0 {
671                    this.child(
672                        v_flex().p_4().child(
673                            div().flex().w_full().items_center().child(
674                                Label::new("You have no notifications.")
675                                    .color(Color::Muted)
676                                    .size(LabelSize::Small),
677                            ),
678                        ),
679                    )
680                } else {
681                    this.child(
682                        list(
683                            self.notification_list.clone(),
684                            cx.processor(|this, ix, window, cx| {
685                                this.render_notification(ix, window, cx)
686                                    .unwrap_or_else(|| div().into_any())
687                            }),
688                        )
689                        .size_full(),
690                    )
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 {}