notification_panel.rs

  1// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
  2// use anyhow::Result;
  3// use channel::ChannelStore;
  4// use client::{Client, Notification, User, UserStore};
  5// use collections::HashMap;
  6// use db::kvp::KEY_VALUE_STORE;
  7// use futures::StreamExt;
  8// use gpui::{
  9//     actions,
 10//     elements::*,
 11//     platform::{CursorStyle, MouseButton},
 12//     serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
 13//     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 14// };
 15// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
 16// use project::Fs;
 17// use rpc::proto;
 18// use serde::{Deserialize, Serialize};
 19// use settings::SettingsStore;
 20// use std::{sync::Arc, time::Duration};
 21// use theme::{ui, Theme};
 22// use time::{OffsetDateTime, UtcOffset};
 23// use util::{ResultExt, TryFutureExt};
 24// use workspace::{
 25//     dock::{DockPosition, Panel},
 26//     Workspace,
 27// };
 28
 29// const LOADING_THRESHOLD: usize = 30;
 30// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
 31// const TOAST_DURATION: Duration = Duration::from_secs(5);
 32// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
 33
 34// pub struct NotificationPanel {
 35//     client: Arc<Client>,
 36//     user_store: ModelHandle<UserStore>,
 37//     channel_store: ModelHandle<ChannelStore>,
 38//     notification_store: ModelHandle<NotificationStore>,
 39//     fs: Arc<dyn Fs>,
 40//     width: Option<f32>,
 41//     active: bool,
 42//     notification_list: ListState<Self>,
 43//     pending_serialization: Task<Option<()>>,
 44//     subscriptions: Vec<gpui::Subscription>,
 45//     workspace: WeakViewHandle<Workspace>,
 46//     current_notification_toast: Option<(u64, Task<()>)>,
 47//     local_timezone: UtcOffset,
 48//     has_focus: bool,
 49//     mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
 50// }
 51
 52// #[derive(Serialize, Deserialize)]
 53// struct SerializedNotificationPanel {
 54//     width: Option<f32>,
 55// }
 56
 57// #[derive(Debug)]
 58// pub enum Event {
 59//     DockPositionChanged,
 60//     Focus,
 61//     Dismissed,
 62// }
 63
 64// pub struct NotificationPresenter {
 65//     pub actor: Option<Arc<client::User>>,
 66//     pub text: String,
 67//     pub icon: &'static str,
 68//     pub needs_response: bool,
 69//     pub can_navigate: bool,
 70// }
 71
 72// actions!(notification_panel, [ToggleFocus]);
 73
 74// pub fn init(_cx: &mut AppContext) {}
 75
 76// impl NotificationPanel {
 77//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
 78//         let fs = workspace.app_state().fs.clone();
 79//         let client = workspace.app_state().client.clone();
 80//         let user_store = workspace.app_state().user_store.clone();
 81//         let workspace_handle = workspace.weak_handle();
 82
 83//         cx.add_view(|cx| {
 84//             let mut status = client.status();
 85//             cx.spawn(|this, mut cx| async move {
 86//                 while let Some(_) = status.next().await {
 87//                     if this
 88//                         .update(&mut cx, |_, cx| {
 89//                             cx.notify();
 90//                         })
 91//                         .is_err()
 92//                     {
 93//                         break;
 94//                     }
 95//                 }
 96//             })
 97//             .detach();
 98
 99//             let mut notification_list =
100//                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
101//                     this.render_notification(ix, cx)
102//                         .unwrap_or_else(|| Empty::new().into_any())
103//                 });
104//             notification_list.set_scroll_handler(|visible_range, count, this, cx| {
105//                 if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
106//                     if let Some(task) = this
107//                         .notification_store
108//                         .update(cx, |store, cx| store.load_more_notifications(false, cx))
109//                     {
110//                         task.detach();
111//                     }
112//                 }
113//             });
114
115//             let mut this = Self {
116//                 fs,
117//                 client,
118//                 user_store,
119//                 local_timezone: cx.platform().local_timezone(),
120//                 channel_store: ChannelStore::global(cx),
121//                 notification_store: NotificationStore::global(cx),
122//                 notification_list,
123//                 pending_serialization: Task::ready(None),
124//                 workspace: workspace_handle,
125//                 has_focus: false,
126//                 current_notification_toast: None,
127//                 subscriptions: Vec::new(),
128//                 active: false,
129//                 mark_as_read_tasks: HashMap::default(),
130//                 width: None,
131//             };
132
133//             let mut old_dock_position = this.position(cx);
134//             this.subscriptions.extend([
135//                 cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
136//                 cx.subscribe(&this.notification_store, Self::on_notification_event),
137//                 cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
138//                     let new_dock_position = this.position(cx);
139//                     if new_dock_position != old_dock_position {
140//                         old_dock_position = new_dock_position;
141//                         cx.emit(Event::DockPositionChanged);
142//                     }
143//                     cx.notify();
144//                 }),
145//             ]);
146//             this
147//         })
148//     }
149
150//     pub fn load(
151//         workspace: WeakViewHandle<Workspace>,
152//         cx: AsyncAppContext,
153//     ) -> Task<Result<ViewHandle<Self>>> {
154//         cx.spawn(|mut cx| async move {
155//             let serialized_panel = if let Some(panel) = cx
156//                 .background()
157//                 .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
158//                 .await
159//                 .log_err()
160//                 .flatten()
161//             {
162//                 Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
163//             } else {
164//                 None
165//             };
166
167//             workspace.update(&mut cx, |workspace, cx| {
168//                 let panel = Self::new(workspace, cx);
169//                 if let Some(serialized_panel) = serialized_panel {
170//                     panel.update(cx, |panel, cx| {
171//                         panel.width = serialized_panel.width;
172//                         cx.notify();
173//                     });
174//                 }
175//                 panel
176//             })
177//         })
178//     }
179
180//     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
181//         let width = self.width;
182//         self.pending_serialization = cx.background().spawn(
183//             async move {
184//                 KEY_VALUE_STORE
185//                     .write_kvp(
186//                         NOTIFICATION_PANEL_KEY.into(),
187//                         serde_json::to_string(&SerializedNotificationPanel { width })?,
188//                     )
189//                     .await?;
190//                 anyhow::Ok(())
191//             }
192//             .log_err(),
193//         );
194//     }
195
196//     fn render_notification(
197//         &mut self,
198//         ix: usize,
199//         cx: &mut ViewContext<Self>,
200//     ) -> Option<AnyElement<Self>> {
201//         let entry = self.notification_store.read(cx).notification_at(ix)?;
202//         let notification_id = entry.id;
203//         let now = OffsetDateTime::now_utc();
204//         let timestamp = entry.timestamp;
205//         let NotificationPresenter {
206//             actor,
207//             text,
208//             needs_response,
209//             can_navigate,
210//             ..
211//         } = self.present_notification(entry, cx)?;
212
213//         let theme = theme::current(cx);
214//         let style = &theme.notification_panel;
215//         let response = entry.response;
216//         let notification = entry.notification.clone();
217
218//         let message_style = if entry.is_read {
219//             style.read_text.clone()
220//         } else {
221//             style.unread_text.clone()
222//         };
223
224//         if self.active && !entry.is_read {
225//             self.did_render_notification(notification_id, &notification, cx);
226//         }
227
228//         enum Decline {}
229//         enum Accept {}
230
231//         Some(
232//             MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
233//                 let container = message_style.container;
234
235//                 Flex::row()
236//                     .with_children(actor.map(|actor| {
237//                         render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
238//                     }))
239//                     .with_child(
240//                         Flex::column()
241//                             .with_child(Text::new(text, message_style.text.clone()))
242//                             .with_child(
243//                                 Flex::row()
244//                                     .with_child(
245//                                         Label::new(
246//                                             format_timestamp(timestamp, now, self.local_timezone),
247//                                             style.timestamp.text.clone(),
248//                                         )
249//                                         .contained()
250//                                         .with_style(style.timestamp.container),
251//                                     )
252//                                     .with_children(if let Some(is_accepted) = response {
253//                                         Some(
254//                                             Label::new(
255//                                                 if is_accepted {
256//                                                     "You accepted"
257//                                                 } else {
258//                                                     "You declined"
259//                                                 },
260//                                                 style.read_text.text.clone(),
261//                                             )
262//                                             .flex_float()
263//                                             .into_any(),
264//                                         )
265//                                     } else if needs_response {
266//                                         Some(
267//                                             Flex::row()
268//                                                 .with_children([
269//                                                     MouseEventHandler::new::<Decline, _>(
270//                                                         ix,
271//                                                         cx,
272//                                                         |state, _| {
273//                                                             let button =
274//                                                                 style.button.style_for(state);
275//                                                             Label::new(
276//                                                                 "Decline",
277//                                                                 button.text.clone(),
278//                                                             )
279//                                                             .contained()
280//                                                             .with_style(button.container)
281//                                                         },
282//                                                     )
283//                                                     .with_cursor_style(CursorStyle::PointingHand)
284//                                                     .on_click(MouseButton::Left, {
285//                                                         let notification = notification.clone();
286//                                                         move |_, view, cx| {
287//                                                             view.respond_to_notification(
288//                                                                 notification.clone(),
289//                                                                 false,
290//                                                                 cx,
291//                                                             );
292//                                                         }
293//                                                     }),
294//                                                     MouseEventHandler::new::<Accept, _>(
295//                                                         ix,
296//                                                         cx,
297//                                                         |state, _| {
298//                                                             let button =
299//                                                                 style.button.style_for(state);
300//                                                             Label::new(
301//                                                                 "Accept",
302//                                                                 button.text.clone(),
303//                                                             )
304//                                                             .contained()
305//                                                             .with_style(button.container)
306//                                                         },
307//                                                     )
308//                                                     .with_cursor_style(CursorStyle::PointingHand)
309//                                                     .on_click(MouseButton::Left, {
310//                                                         let notification = notification.clone();
311//                                                         move |_, view, cx| {
312//                                                             view.respond_to_notification(
313//                                                                 notification.clone(),
314//                                                                 true,
315//                                                                 cx,
316//                                                             );
317//                                                         }
318//                                                     }),
319//                                                 ])
320//                                                 .flex_float()
321//                                                 .into_any(),
322//                                         )
323//                                     } else {
324//                                         None
325//                                     }),
326//                             )
327//                             .flex(1.0, true),
328//                     )
329//                     .contained()
330//                     .with_style(container)
331//                     .into_any()
332//             })
333//             .with_cursor_style(if can_navigate {
334//                 CursorStyle::PointingHand
335//             } else {
336//                 CursorStyle::default()
337//             })
338//             .on_click(MouseButton::Left, {
339//                 let notification = notification.clone();
340//                 move |_, this, cx| this.did_click_notification(&notification, cx)
341//             })
342//             .into_any(),
343//         )
344//     }
345
346//     fn present_notification(
347//         &self,
348//         entry: &NotificationEntry,
349//         cx: &AppContext,
350//     ) -> Option<NotificationPresenter> {
351//         let user_store = self.user_store.read(cx);
352//         let channel_store = self.channel_store.read(cx);
353//         match entry.notification {
354//             Notification::ContactRequest { sender_id } => {
355//                 let requester = user_store.get_cached_user(sender_id)?;
356//                 Some(NotificationPresenter {
357//                     icon: "icons/plus.svg",
358//                     text: format!("{} wants to add you as a contact", requester.github_login),
359//                     needs_response: user_store.has_incoming_contact_request(requester.id),
360//                     actor: Some(requester),
361//                     can_navigate: false,
362//                 })
363//             }
364//             Notification::ContactRequestAccepted { responder_id } => {
365//                 let responder = user_store.get_cached_user(responder_id)?;
366//                 Some(NotificationPresenter {
367//                     icon: "icons/plus.svg",
368//                     text: format!("{} accepted your contact invite", responder.github_login),
369//                     needs_response: false,
370//                     actor: Some(responder),
371//                     can_navigate: false,
372//                 })
373//             }
374//             Notification::ChannelInvitation {
375//                 ref channel_name,
376//                 channel_id,
377//                 inviter_id,
378//             } => {
379//                 let inviter = user_store.get_cached_user(inviter_id)?;
380//                 Some(NotificationPresenter {
381//                     icon: "icons/hash.svg",
382//                     text: format!(
383//                         "{} invited you to join the #{channel_name} channel",
384//                         inviter.github_login
385//                     ),
386//                     needs_response: channel_store.has_channel_invitation(channel_id),
387//                     actor: Some(inviter),
388//                     can_navigate: false,
389//                 })
390//             }
391//             Notification::ChannelMessageMention {
392//                 sender_id,
393//                 channel_id,
394//                 message_id,
395//             } => {
396//                 let sender = user_store.get_cached_user(sender_id)?;
397//                 let channel = channel_store.channel_for_id(channel_id)?;
398//                 let message = self
399//                     .notification_store
400//                     .read(cx)
401//                     .channel_message_for_id(message_id)?;
402//                 Some(NotificationPresenter {
403//                     icon: "icons/conversations.svg",
404//                     text: format!(
405//                         "{} mentioned you in #{}:\n{}",
406//                         sender.github_login, channel.name, message.body,
407//                     ),
408//                     needs_response: false,
409//                     actor: Some(sender),
410//                     can_navigate: true,
411//                 })
412//             }
413//         }
414//     }
415
416//     fn did_render_notification(
417//         &mut self,
418//         notification_id: u64,
419//         notification: &Notification,
420//         cx: &mut ViewContext<Self>,
421//     ) {
422//         let should_mark_as_read = match notification {
423//             Notification::ContactRequestAccepted { .. } => true,
424//             Notification::ContactRequest { .. }
425//             | Notification::ChannelInvitation { .. }
426//             | Notification::ChannelMessageMention { .. } => false,
427//         };
428
429//         if should_mark_as_read {
430//             self.mark_as_read_tasks
431//                 .entry(notification_id)
432//                 .or_insert_with(|| {
433//                     let client = self.client.clone();
434//                     cx.spawn(|this, mut cx| async move {
435//                         cx.background().timer(MARK_AS_READ_DELAY).await;
436//                         client
437//                             .request(proto::MarkNotificationRead { notification_id })
438//                             .await?;
439//                         this.update(&mut cx, |this, _| {
440//                             this.mark_as_read_tasks.remove(&notification_id);
441//                         })?;
442//                         Ok(())
443//                     })
444//                 });
445//         }
446//     }
447
448//     fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
449//         if let Notification::ChannelMessageMention {
450//             message_id,
451//             channel_id,
452//             ..
453//         } = notification.clone()
454//         {
455//             if let Some(workspace) = self.workspace.upgrade(cx) {
456//                 cx.app_context().defer(move |cx| {
457//                     workspace.update(cx, |workspace, cx| {
458//                         if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
459//                             panel.update(cx, |panel, cx| {
460//                                 panel
461//                                     .select_channel(channel_id, Some(message_id), cx)
462//                                     .detach_and_log_err(cx);
463//                             });
464//                         }
465//                     });
466//                 });
467//             }
468//         }
469//     }
470
471//     fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
472//         if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
473//             if let Some(workspace) = self.workspace.upgrade(cx) {
474//                 return workspace
475//                     .read_with(cx, |workspace, cx| {
476//                         if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
477//                             return panel.read_with(cx, |panel, cx| {
478//                                 panel.is_scrolled_to_bottom()
479//                                     && panel.active_chat().map_or(false, |chat| {
480//                                         chat.read(cx).channel_id == *channel_id
481//                                     })
482//                             });
483//                         }
484//                         false
485//                     })
486//                     .unwrap_or_default();
487//             }
488//         }
489
490//         false
491//     }
492
493//     fn render_sign_in_prompt(
494//         &self,
495//         theme: &Arc<Theme>,
496//         cx: &mut ViewContext<Self>,
497//     ) -> AnyElement<Self> {
498//         enum SignInPromptLabel {}
499
500//         MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
501//             Label::new(
502//                 "Sign in to view your notifications".to_string(),
503//                 theme
504//                     .chat_panel
505//                     .sign_in_prompt
506//                     .style_for(mouse_state)
507//                     .clone(),
508//             )
509//         })
510//         .with_cursor_style(CursorStyle::PointingHand)
511//         .on_click(MouseButton::Left, move |_, this, cx| {
512//             let client = this.client.clone();
513//             cx.spawn(|_, cx| async move {
514//                 client.authenticate_and_connect(true, &cx).log_err().await;
515//             })
516//             .detach();
517//         })
518//         .aligned()
519//         .into_any()
520//     }
521
522//     fn render_empty_state(
523//         &self,
524//         theme: &Arc<Theme>,
525//         _cx: &mut ViewContext<Self>,
526//     ) -> AnyElement<Self> {
527//         Label::new(
528//             "You have no notifications".to_string(),
529//             theme.chat_panel.sign_in_prompt.default.clone(),
530//         )
531//         .aligned()
532//         .into_any()
533//     }
534
535//     fn on_notification_event(
536//         &mut self,
537//         _: ModelHandle<NotificationStore>,
538//         event: &NotificationEvent,
539//         cx: &mut ViewContext<Self>,
540//     ) {
541//         match event {
542//             NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
543//             NotificationEvent::NotificationRemoved { entry }
544//             | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
545//             NotificationEvent::NotificationsUpdated {
546//                 old_range,
547//                 new_count,
548//             } => {
549//                 self.notification_list.splice(old_range.clone(), *new_count);
550//                 cx.notify();
551//             }
552//         }
553//     }
554
555//     fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
556//         if self.is_showing_notification(&entry.notification, cx) {
557//             return;
558//         }
559
560//         let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
561//         else {
562//             return;
563//         };
564
565//         let notification_id = entry.id;
566//         self.current_notification_toast = Some((
567//             notification_id,
568//             cx.spawn(|this, mut cx| async move {
569//                 cx.background().timer(TOAST_DURATION).await;
570//                 this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
571//                     .ok();
572//             }),
573//         ));
574
575//         self.workspace
576//             .update(cx, |workspace, cx| {
577//                 workspace.dismiss_notification::<NotificationToast>(0, cx);
578//                 workspace.show_notification(0, cx, |cx| {
579//                     let workspace = cx.weak_handle();
580//                     cx.add_view(|_| NotificationToast {
581//                         notification_id,
582//                         actor,
583//                         text,
584//                         workspace,
585//                     })
586//                 })
587//             })
588//             .ok();
589//     }
590
591//     fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
592//         if let Some((current_id, _)) = &self.current_notification_toast {
593//             if *current_id == notification_id {
594//                 self.current_notification_toast.take();
595//                 self.workspace
596//                     .update(cx, |workspace, cx| {
597//                         workspace.dismiss_notification::<NotificationToast>(0, cx)
598//                     })
599//                     .ok();
600//             }
601//         }
602//     }
603
604//     fn respond_to_notification(
605//         &mut self,
606//         notification: Notification,
607//         response: bool,
608//         cx: &mut ViewContext<Self>,
609//     ) {
610//         self.notification_store.update(cx, |store, cx| {
611//             store.respond_to_notification(notification, response, cx);
612//         });
613//     }
614// }
615
616// impl Entity for NotificationPanel {
617//     type Event = Event;
618// }
619
620// impl View for NotificationPanel {
621//     fn ui_name() -> &'static str {
622//         "NotificationPanel"
623//     }
624
625//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
626//         let theme = theme::current(cx);
627//         let style = &theme.notification_panel;
628//         let element = if self.client.user_id().is_none() {
629//             self.render_sign_in_prompt(&theme, cx)
630//         } else if self.notification_list.item_count() == 0 {
631//             self.render_empty_state(&theme, cx)
632//         } else {
633//             Flex::column()
634//                 .with_child(
635//                     Flex::row()
636//                         .with_child(Label::new("Notifications", style.title.text.clone()))
637//                         .with_child(ui::svg(&style.title_icon).flex_float())
638//                         .align_children_center()
639//                         .contained()
640//                         .with_style(style.title.container)
641//                         .constrained()
642//                         .with_height(style.title_height),
643//                 )
644//                 .with_child(
645//                     List::new(self.notification_list.clone())
646//                         .contained()
647//                         .with_style(style.list)
648//                         .flex(1., true),
649//                 )
650//                 .into_any()
651//         };
652//         element
653//             .contained()
654//             .with_style(style.container)
655//             .constrained()
656//             .with_min_width(150.)
657//             .into_any()
658//     }
659
660//     fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
661//         self.has_focus = true;
662//     }
663
664//     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
665//         self.has_focus = false;
666//     }
667// }
668
669// impl Panel for NotificationPanel {
670//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
671//         settings::get::<NotificationPanelSettings>(cx).dock
672//     }
673
674//     fn position_is_valid(&self, position: DockPosition) -> bool {
675//         matches!(position, DockPosition::Left | DockPosition::Right)
676//     }
677
678//     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
679//         settings::update_settings_file::<NotificationPanelSettings>(
680//             self.fs.clone(),
681//             cx,
682//             move |settings| settings.dock = Some(position),
683//         );
684//     }
685
686//     fn size(&self, cx: &gpui::WindowContext) -> f32 {
687//         self.width
688//             .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
689//     }
690
691//     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
692//         self.width = size;
693//         self.serialize(cx);
694//         cx.notify();
695//     }
696
697//     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
698//         self.active = active;
699//         if self.notification_store.read(cx).notification_count() == 0 {
700//             cx.emit(Event::Dismissed);
701//         }
702//     }
703
704//     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
705//         (settings::get::<NotificationPanelSettings>(cx).button
706//             && self.notification_store.read(cx).notification_count() > 0)
707//             .then(|| "icons/bell.svg")
708//     }
709
710//     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
711//         (
712//             "Notification Panel".to_string(),
713//             Some(Box::new(ToggleFocus)),
714//         )
715//     }
716
717//     fn icon_label(&self, cx: &WindowContext) -> Option<String> {
718//         let count = self.notification_store.read(cx).unread_notification_count();
719//         if count == 0 {
720//             None
721//         } else {
722//             Some(count.to_string())
723//         }
724//     }
725
726//     fn should_change_position_on_event(event: &Self::Event) -> bool {
727//         matches!(event, Event::DockPositionChanged)
728//     }
729
730//     fn should_close_on_event(event: &Self::Event) -> bool {
731//         matches!(event, Event::Dismissed)
732//     }
733
734//     fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
735//         self.has_focus
736//     }
737
738//     fn is_focus_event(event: &Self::Event) -> bool {
739//         matches!(event, Event::Focus)
740//     }
741// }
742
743// pub struct NotificationToast {
744//     notification_id: u64,
745//     actor: Option<Arc<User>>,
746//     text: String,
747//     workspace: WeakViewHandle<Workspace>,
748// }
749
750// pub enum ToastEvent {
751//     Dismiss,
752// }
753
754// impl NotificationToast {
755//     fn focus_notification_panel(&self, cx: &mut AppContext) {
756//         let workspace = self.workspace.clone();
757//         let notification_id = self.notification_id;
758//         cx.defer(move |cx| {
759//             workspace
760//                 .update(cx, |workspace, cx| {
761//                     if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
762//                         panel.update(cx, |panel, cx| {
763//                             let store = panel.notification_store.read(cx);
764//                             if let Some(entry) = store.notification_for_id(notification_id) {
765//                                 panel.did_click_notification(&entry.clone().notification, cx);
766//                             }
767//                         });
768//                     }
769//                 })
770//                 .ok();
771//         })
772//     }
773// }
774
775// impl Entity for NotificationToast {
776//     type Event = ToastEvent;
777// }
778
779// impl View for NotificationToast {
780//     fn ui_name() -> &'static str {
781//         "ContactNotification"
782//     }
783
784//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
785//         let user = self.actor.clone();
786//         let theme = theme::current(cx).clone();
787//         let theme = &theme.contact_notification;
788
789//         MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
790//             Flex::row()
791//                 .with_children(user.and_then(|user| {
792//                     Some(
793//                         Image::from_data(user.avatar.clone()?)
794//                             .with_style(theme.header_avatar)
795//                             .aligned()
796//                             .constrained()
797//                             .with_height(
798//                                 cx.font_cache()
799//                                     .line_height(theme.header_message.text.font_size),
800//                             )
801//                             .aligned()
802//                             .top(),
803//                     )
804//                 }))
805//                 .with_child(
806//                     Text::new(self.text.clone(), theme.header_message.text.clone())
807//                         .contained()
808//                         .with_style(theme.header_message.container)
809//                         .aligned()
810//                         .top()
811//                         .left()
812//                         .flex(1., true),
813//                 )
814//                 .with_child(
815//                     MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
816//                         let style = theme.dismiss_button.style_for(state);
817//                         Svg::new("icons/x.svg")
818//                             .with_color(style.color)
819//                             .constrained()
820//                             .with_width(style.icon_width)
821//                             .aligned()
822//                             .contained()
823//                             .with_style(style.container)
824//                             .constrained()
825//                             .with_width(style.button_width)
826//                             .with_height(style.button_width)
827//                     })
828//                     .with_cursor_style(CursorStyle::PointingHand)
829//                     .with_padding(Padding::uniform(5.))
830//                     .on_click(MouseButton::Left, move |_, _, cx| {
831//                         cx.emit(ToastEvent::Dismiss)
832//                     })
833//                     .aligned()
834//                     .constrained()
835//                     .with_height(
836//                         cx.font_cache()
837//                             .line_height(theme.header_message.text.font_size),
838//                     )
839//                     .aligned()
840//                     .top()
841//                     .flex_float(),
842//                 )
843//                 .contained()
844//         })
845//         .with_cursor_style(CursorStyle::PointingHand)
846//         .on_click(MouseButton::Left, move |_, this, cx| {
847//             this.focus_notification_panel(cx);
848//             cx.emit(ToastEvent::Dismiss);
849//         })
850//         .into_any()
851//     }
852// }
853
854// impl workspace::notifications::Notification for NotificationToast {
855//     fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
856//         matches!(event, ToastEvent::Dismiss)
857//     }
858// }
859
860// fn format_timestamp(
861//     mut timestamp: OffsetDateTime,
862//     mut now: OffsetDateTime,
863//     local_timezone: UtcOffset,
864// ) -> String {
865//     timestamp = timestamp.to_offset(local_timezone);
866//     now = now.to_offset(local_timezone);
867
868//     let today = now.date();
869//     let date = timestamp.date();
870//     if date == today {
871//         let difference = now - timestamp;
872//         if difference >= Duration::from_secs(3600) {
873//             format!("{}h", difference.whole_seconds() / 3600)
874//         } else if difference >= Duration::from_secs(60) {
875//             format!("{}m", difference.whole_seconds() / 60)
876//         } else {
877//             "just now".to_string()
878//         }
879//     } else if date.next_day() == Some(today) {
880//         format!("yesterday")
881//     } else {
882//         format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
883//     }
884// }