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, ¬ification, 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(¬ification, 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(¬ification_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, .. } = ¬ification {
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.user_id().is_none() {
638 this.child(
639 v_flex()
640 .gap_2()
641 .p_4()
642 .child(
643 Button::new("sign_in_prompt_button", "Sign in")
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
656 .authenticate_and_connect(true, &cx)
657 .await
658 {
659 util::ConnectionResult::Timeout => {
660 log::error!("Connection timeout");
661 }
662 util::ConnectionResult::ConnectionReset => {
663 log::error!("Connection reset");
664 }
665 util::ConnectionResult::Result(r) => {
666 r.log_err();
667 }
668 }
669 })
670 .detach()
671 }
672 }),
673 )
674 .child(
675 div().flex().w_full().items_center().child(
676 Label::new("Sign in to view notifications.")
677 .color(Color::Muted)
678 .size(LabelSize::Small),
679 ),
680 ),
681 )
682 } else if self.notification_list.item_count() == 0 {
683 this.child(
684 v_flex().p_4().child(
685 div().flex().w_full().items_center().child(
686 Label::new("You have no notifications.")
687 .color(Color::Muted)
688 .size(LabelSize::Small),
689 ),
690 ),
691 )
692 } else {
693 this.child(list(self.notification_list.clone()).size_full())
694 }
695 })
696 }
697}
698
699impl Focusable for NotificationPanel {
700 fn focus_handle(&self, _: &App) -> FocusHandle {
701 self.focus_handle.clone()
702 }
703}
704
705impl EventEmitter<Event> for NotificationPanel {}
706impl EventEmitter<PanelEvent> for NotificationPanel {}
707
708impl Panel for NotificationPanel {
709 fn persistent_name() -> &'static str {
710 "NotificationPanel"
711 }
712
713 fn position(&self, _: &Window, cx: &App) -> DockPosition {
714 NotificationPanelSettings::get_global(cx).dock
715 }
716
717 fn position_is_valid(&self, position: DockPosition) -> bool {
718 matches!(position, DockPosition::Left | DockPosition::Right)
719 }
720
721 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
722 settings::update_settings_file::<NotificationPanelSettings>(
723 self.fs.clone(),
724 cx,
725 move |settings, _| settings.dock = Some(position),
726 );
727 }
728
729 fn size(&self, _: &Window, cx: &App) -> Pixels {
730 self.width
731 .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
732 }
733
734 fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
735 self.width = size;
736 self.serialize(cx);
737 cx.notify();
738 }
739
740 fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context<Self>) {
741 self.active = active;
742
743 if self.active {
744 self.unseen_notifications = Vec::new();
745 cx.notify();
746 }
747
748 if self.notification_store.read(cx).notification_count() == 0 {
749 cx.emit(Event::Dismissed);
750 }
751 }
752
753 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
754 let show_button = NotificationPanelSettings::get_global(cx).button;
755 if !show_button {
756 return None;
757 }
758
759 if self.unseen_notifications.is_empty() {
760 return Some(IconName::Bell);
761 }
762
763 Some(IconName::BellDot)
764 }
765
766 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
767 Some("Notification Panel")
768 }
769
770 fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
771 let count = self.notification_store.read(cx).unread_notification_count();
772 if count == 0 {
773 None
774 } else {
775 Some(count.to_string())
776 }
777 }
778
779 fn toggle_action(&self) -> Box<dyn gpui::Action> {
780 Box::new(ToggleFocus)
781 }
782
783 fn activation_priority(&self) -> u32 {
784 8
785 }
786}
787
788pub struct NotificationToast {
789 notification_id: u64,
790 actor: Option<Arc<User>>,
791 text: String,
792 workspace: WeakEntity<Workspace>,
793 focus_handle: FocusHandle,
794}
795
796impl Focusable for NotificationToast {
797 fn focus_handle(&self, _cx: &App) -> FocusHandle {
798 self.focus_handle.clone()
799 }
800}
801
802impl WorkspaceNotification for NotificationToast {}
803
804impl NotificationToast {
805 fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
806 let workspace = self.workspace.clone();
807 let notification_id = self.notification_id;
808 window.defer(cx, move |window, cx| {
809 workspace
810 .update(cx, |workspace, cx| {
811 if let Some(panel) = workspace.focus_panel::<NotificationPanel>(window, cx) {
812 panel.update(cx, |panel, cx| {
813 let store = panel.notification_store.read(cx);
814 if let Some(entry) = store.notification_for_id(notification_id) {
815 panel.did_click_notification(
816 &entry.clone().notification,
817 window,
818 cx,
819 );
820 }
821 });
822 }
823 })
824 .ok();
825 })
826 }
827}
828
829impl Render for NotificationToast {
830 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
831 let user = self.actor.clone();
832
833 let suppress = window.modifiers().shift;
834 let (close_id, close_icon) = if suppress {
835 ("suppress", IconName::Minimize)
836 } else {
837 ("close", IconName::Close)
838 };
839
840 h_flex()
841 .id("notification_panel_toast")
842 .elevation_3(cx)
843 .p_2()
844 .justify_between()
845 .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
846 .child(Label::new(self.text.clone()))
847 .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
848 .child(
849 IconButton::new(close_id, close_icon)
850 .tooltip(move |window, cx| {
851 if suppress {
852 Tooltip::for_action(
853 "Suppress.\nClose with click.",
854 &workspace::SuppressNotification,
855 window,
856 cx,
857 )
858 } else {
859 Tooltip::for_action(
860 "Close.\nSuppress with shift-click",
861 &menu::Cancel,
862 window,
863 cx,
864 )
865 }
866 })
867 .on_click(cx.listener(move |_, _: &ClickEvent, _, cx| {
868 if suppress {
869 cx.emit(SuppressEvent);
870 } else {
871 cx.emit(DismissEvent);
872 }
873 })),
874 )
875 .on_click(cx.listener(|this, _, window, cx| {
876 this.focus_notification_panel(window, cx);
877 cx.emit(DismissEvent);
878 }))
879 }
880}
881
882impl EventEmitter<DismissEvent> for NotificationToast {}
883impl EventEmitter<SuppressEvent> for NotificationToast {}