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, ¬ification, 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(¬ification, 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(¬ification_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, .. } = ¬ification {
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// }