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, ¬ification, 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(¬ification, 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))
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(¬ification_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 {
473 window.defer(cx, move |window, cx| {
474 workspace.update(cx, |workspace, cx| {
475 if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
476 panel.update(cx, |panel, cx| {
477 panel
478 .select_channel(ChannelId(channel_id), Some(message_id), cx)
479 .detach_and_log_err(cx);
480 });
481 }
482 });
483 });
484 }
485 }
486
487 fn is_showing_notification(&self, notification: &Notification, cx: &mut Context<Self>) -> bool {
488 if !self.active {
489 return false;
490 }
491
492 if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification
493 && let Some(workspace) = self.workspace.upgrade()
494 {
495 return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
496 let panel = panel.read(cx);
497 panel.is_scrolled_to_bottom()
498 && panel
499 .active_chat()
500 .is_some_and(|chat| chat.read(cx).channel_id.0 == *channel_id)
501 } else {
502 false
503 };
504 }
505
506 false
507 }
508
509 fn on_notification_event(
510 &mut self,
511 _: &Entity<NotificationStore>,
512 event: &NotificationEvent,
513 window: &mut Window,
514 cx: &mut Context<Self>,
515 ) {
516 match event {
517 NotificationEvent::NewNotification { entry } => {
518 if !self.is_showing_notification(&entry.notification, cx) {
519 self.unseen_notifications.push(entry.clone());
520 }
521 self.add_toast(entry, window, cx);
522 }
523 NotificationEvent::NotificationRemoved { entry }
524 | NotificationEvent::NotificationRead { entry } => {
525 self.unseen_notifications.retain(|n| n.id != entry.id);
526 self.remove_toast(entry.id, cx);
527 }
528 NotificationEvent::NotificationsUpdated {
529 old_range,
530 new_count,
531 } => {
532 self.notification_list.splice(old_range.clone(), *new_count);
533 cx.notify();
534 }
535 }
536 }
537
538 fn add_toast(
539 &mut self,
540 entry: &NotificationEntry,
541 window: &mut Window,
542 cx: &mut Context<Self>,
543 ) {
544 if self.is_showing_notification(&entry.notification, cx) {
545 return;
546 }
547
548 let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
549 else {
550 return;
551 };
552
553 let notification_id = entry.id;
554 self.current_notification_toast = Some((
555 notification_id,
556 cx.spawn_in(window, async move |this, cx| {
557 cx.background_executor().timer(TOAST_DURATION).await;
558 this.update(cx, |this, cx| this.remove_toast(notification_id, cx))
559 .ok();
560 }),
561 ));
562
563 self.workspace
564 .update(cx, |workspace, cx| {
565 let id = NotificationId::unique::<NotificationToast>();
566
567 workspace.dismiss_notification(&id, cx);
568 workspace.show_notification(id, cx, |cx| {
569 let workspace = cx.entity().downgrade();
570 cx.new(|cx| NotificationToast {
571 notification_id,
572 actor,
573 text,
574 workspace,
575 focus_handle: cx.focus_handle(),
576 })
577 })
578 })
579 .ok();
580 }
581
582 fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) {
583 if let Some((current_id, _)) = &self.current_notification_toast
584 && *current_id == notification_id
585 {
586 self.current_notification_toast.take();
587 self.workspace
588 .update(cx, |workspace, cx| {
589 let id = NotificationId::unique::<NotificationToast>();
590 workspace.dismiss_notification(&id, cx)
591 })
592 .ok();
593 }
594 }
595
596 fn respond_to_notification(
597 &mut self,
598 notification: Notification,
599 response: bool,
600
601 cx: &mut Context<Self>,
602 ) {
603 self.notification_store.update(cx, |store, cx| {
604 store.respond_to_notification(notification, response, cx);
605 });
606 }
607}
608
609impl Render for NotificationPanel {
610 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
611 v_flex()
612 .size_full()
613 .child(
614 h_flex()
615 .justify_between()
616 .px_2()
617 .py_1()
618 // Match the height of the tab bar so they line up.
619 .h(Tab::container_height(cx))
620 .border_b_1()
621 .border_color(cx.theme().colors().border)
622 .child(Label::new("Notifications"))
623 .child(Icon::new(IconName::Envelope)),
624 )
625 .map(|this| {
626 if !self.client.status().borrow().is_connected() {
627 this.child(
628 v_flex()
629 .gap_2()
630 .p_4()
631 .child(
632 Button::new("connect_prompt_button", "Connect")
633 .icon_color(Color::Muted)
634 .icon(IconName::Github)
635 .icon_position(IconPosition::Start)
636 .style(ButtonStyle::Filled)
637 .full_width()
638 .on_click({
639 let client = self.client.clone();
640 move |_, window, cx| {
641 let client = client.clone();
642 window
643 .spawn(cx, async move |cx| {
644 match client.connect(true, cx).await {
645 util::ConnectionResult::Timeout => {
646 log::error!("Connection timeout");
647 }
648 util::ConnectionResult::ConnectionReset => {
649 log::error!("Connection reset");
650 }
651 util::ConnectionResult::Result(r) => {
652 r.log_err();
653 }
654 }
655 })
656 .detach()
657 }
658 }),
659 )
660 .child(
661 div().flex().w_full().items_center().child(
662 Label::new("Connect to view notifications.")
663 .color(Color::Muted)
664 .size(LabelSize::Small),
665 ),
666 ),
667 )
668 } else if self.notification_list.item_count() == 0 {
669 this.child(
670 v_flex().p_4().child(
671 div().flex().w_full().items_center().child(
672 Label::new("You have no notifications.")
673 .color(Color::Muted)
674 .size(LabelSize::Small),
675 ),
676 ),
677 )
678 } else {
679 this.child(
680 list(
681 self.notification_list.clone(),
682 cx.processor(|this, ix, window, cx| {
683 this.render_notification(ix, window, cx)
684 .unwrap_or_else(|| div().into_any())
685 }),
686 )
687 .size_full(),
688 )
689 }
690 })
691 }
692}
693
694impl Focusable for NotificationPanel {
695 fn focus_handle(&self, _: &App) -> FocusHandle {
696 self.focus_handle.clone()
697 }
698}
699
700impl EventEmitter<Event> for NotificationPanel {}
701impl EventEmitter<PanelEvent> for NotificationPanel {}
702
703impl Panel for NotificationPanel {
704 fn persistent_name() -> &'static str {
705 "NotificationPanel"
706 }
707
708 fn position(&self, _: &Window, cx: &App) -> DockPosition {
709 NotificationPanelSettings::get_global(cx).dock
710 }
711
712 fn position_is_valid(&self, position: DockPosition) -> bool {
713 matches!(position, DockPosition::Left | DockPosition::Right)
714 }
715
716 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
717 settings::update_settings_file::<NotificationPanelSettings>(
718 self.fs.clone(),
719 cx,
720 move |settings, _| settings.dock = Some(position),
721 );
722 }
723
724 fn size(&self, _: &Window, cx: &App) -> Pixels {
725 self.width
726 .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
727 }
728
729 fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
730 self.width = size;
731 self.serialize(cx);
732 cx.notify();
733 }
734
735 fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context<Self>) {
736 self.active = active;
737
738 if self.active {
739 self.unseen_notifications = Vec::new();
740 cx.notify();
741 }
742
743 if self.notification_store.read(cx).notification_count() == 0 {
744 cx.emit(Event::Dismissed);
745 }
746 }
747
748 fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
749 let show_button = NotificationPanelSettings::get_global(cx).button;
750 if !show_button {
751 return None;
752 }
753
754 if self.unseen_notifications.is_empty() {
755 return Some(IconName::Bell);
756 }
757
758 Some(IconName::BellDot)
759 }
760
761 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
762 Some("Notification Panel")
763 }
764
765 fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
766 let count = self.notification_store.read(cx).unread_notification_count();
767 if count == 0 {
768 None
769 } else {
770 Some(count.to_string())
771 }
772 }
773
774 fn toggle_action(&self) -> Box<dyn gpui::Action> {
775 Box::new(ToggleFocus)
776 }
777
778 fn activation_priority(&self) -> u32 {
779 8
780 }
781}
782
783pub struct NotificationToast {
784 notification_id: u64,
785 actor: Option<Arc<User>>,
786 text: String,
787 workspace: WeakEntity<Workspace>,
788 focus_handle: FocusHandle,
789}
790
791impl Focusable for NotificationToast {
792 fn focus_handle(&self, _cx: &App) -> FocusHandle {
793 self.focus_handle.clone()
794 }
795}
796
797impl WorkspaceNotification for NotificationToast {}
798
799impl NotificationToast {
800 fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
801 let workspace = self.workspace.clone();
802 let notification_id = self.notification_id;
803 window.defer(cx, move |window, cx| {
804 workspace
805 .update(cx, |workspace, cx| {
806 if let Some(panel) = workspace.focus_panel::<NotificationPanel>(window, cx) {
807 panel.update(cx, |panel, cx| {
808 let store = panel.notification_store.read(cx);
809 if let Some(entry) = store.notification_for_id(notification_id) {
810 panel.did_click_notification(
811 &entry.clone().notification,
812 window,
813 cx,
814 );
815 }
816 });
817 }
818 })
819 .ok();
820 })
821 }
822}
823
824impl Render for NotificationToast {
825 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
826 let user = self.actor.clone();
827
828 let suppress = window.modifiers().shift;
829 let (close_id, close_icon) = if suppress {
830 ("suppress", IconName::Minimize)
831 } else {
832 ("close", IconName::Close)
833 };
834
835 h_flex()
836 .id("notification_panel_toast")
837 .elevation_3(cx)
838 .p_2()
839 .justify_between()
840 .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
841 .child(Label::new(self.text.clone()))
842 .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
843 .child(
844 IconButton::new(close_id, close_icon)
845 .tooltip(move |window, cx| {
846 if suppress {
847 Tooltip::for_action(
848 "Suppress.\nClose with click.",
849 &workspace::SuppressNotification,
850 window,
851 cx,
852 )
853 } else {
854 Tooltip::for_action(
855 "Close.\nSuppress with shift-click",
856 &menu::Cancel,
857 window,
858 cx,
859 )
860 }
861 })
862 .on_click(cx.listener(move |_, _: &ClickEvent, _, cx| {
863 if suppress {
864 cx.emit(SuppressEvent);
865 } else {
866 cx.emit(DismissEvent);
867 }
868 })),
869 )
870 .on_click(cx.listener(|this, _, window, cx| {
871 this.focus_notification_panel(window, cx);
872 cx.emit(DismissEvent);
873 }))
874 }
875}
876
877impl EventEmitter<DismissEvent> for NotificationToast {}
878impl EventEmitter<SuppressEvent> for NotificationToast {}