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