// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings}; // use anyhow::Result; // use channel::ChannelStore; // use client::{Client, Notification, User, UserStore}; // use collections::HashMap; // use db::kvp::KEY_VALUE_STORE; // use futures::StreamExt; // use gpui::{ // actions, // elements::*, // platform::{CursorStyle, MouseButton}, // serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, // ViewContext, ViewHandle, WeakViewHandle, WindowContext, // }; // use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; // use project::Fs; // use rpc::proto; // use serde::{Deserialize, Serialize}; // use settings::SettingsStore; // use std::{sync::Arc, time::Duration}; // use theme::{ui, Theme}; // use time::{OffsetDateTime, UtcOffset}; // use util::{ResultExt, TryFutureExt}; // use workspace::{ // dock::{DockPosition, Panel}, // Workspace, // }; // const LOADING_THRESHOLD: usize = 30; // const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1); // const TOAST_DURATION: Duration = Duration::from_secs(5); // const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel"; // pub struct NotificationPanel { // client: Arc, // user_store: ModelHandle, // channel_store: ModelHandle, // notification_store: ModelHandle, // fs: Arc, // width: Option, // active: bool, // notification_list: ListState, // pending_serialization: Task>, // subscriptions: Vec, // workspace: WeakViewHandle, // current_notification_toast: Option<(u64, Task<()>)>, // local_timezone: UtcOffset, // has_focus: bool, // mark_as_read_tasks: HashMap>>, // } // #[derive(Serialize, Deserialize)] // struct SerializedNotificationPanel { // width: Option, // } // #[derive(Debug)] // pub enum Event { // DockPositionChanged, // Focus, // Dismissed, // } // pub struct NotificationPresenter { // pub actor: Option>, // pub text: String, // pub icon: &'static str, // pub needs_response: bool, // pub can_navigate: bool, // } // actions!(notification_panel, [ToggleFocus]); // pub fn init(_cx: &mut AppContext) {} // impl NotificationPanel { // pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { // let fs = workspace.app_state().fs.clone(); // let client = workspace.app_state().client.clone(); // let user_store = workspace.app_state().user_store.clone(); // let workspace_handle = workspace.weak_handle(); // cx.add_view(|cx| { // let mut status = client.status(); // cx.spawn(|this, mut cx| async move { // while let Some(_) = status.next().await { // if this // .update(&mut cx, |_, cx| { // cx.notify(); // }) // .is_err() // { // break; // } // } // }) // .detach(); // let mut notification_list = // ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { // this.render_notification(ix, cx) // .unwrap_or_else(|| Empty::new().into_any()) // }); // notification_list.set_scroll_handler(|visible_range, count, this, cx| { // if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD { // if let Some(task) = this // .notification_store // .update(cx, |store, cx| store.load_more_notifications(false, cx)) // { // task.detach(); // } // } // }); // let mut this = Self { // fs, // client, // user_store, // local_timezone: cx.platform().local_timezone(), // channel_store: ChannelStore::global(cx), // notification_store: NotificationStore::global(cx), // notification_list, // pending_serialization: Task::ready(None), // workspace: workspace_handle, // has_focus: false, // current_notification_toast: None, // subscriptions: Vec::new(), // active: false, // mark_as_read_tasks: HashMap::default(), // width: None, // }; // let mut old_dock_position = this.position(cx); // this.subscriptions.extend([ // cx.observe(&this.notification_store, |_, _, cx| cx.notify()), // cx.subscribe(&this.notification_store, Self::on_notification_event), // cx.observe_global::(move |this: &mut Self, cx| { // let new_dock_position = this.position(cx); // if new_dock_position != old_dock_position { // old_dock_position = new_dock_position; // cx.emit(Event::DockPositionChanged); // } // cx.notify(); // }), // ]); // this // }) // } // pub fn load( // workspace: WeakViewHandle, // cx: AsyncAppContext, // ) -> Task>> { // cx.spawn(|mut cx| async move { // let serialized_panel = if let Some(panel) = cx // .background() // .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) }) // .await // .log_err() // .flatten() // { // Some(serde_json::from_str::(&panel)?) // } else { // None // }; // workspace.update(&mut cx, |workspace, cx| { // let panel = Self::new(workspace, cx); // if let Some(serialized_panel) = serialized_panel { // panel.update(cx, |panel, cx| { // panel.width = serialized_panel.width; // cx.notify(); // }); // } // panel // }) // }) // } // fn serialize(&mut self, cx: &mut ViewContext) { // let width = self.width; // self.pending_serialization = cx.background().spawn( // async move { // KEY_VALUE_STORE // .write_kvp( // NOTIFICATION_PANEL_KEY.into(), // serde_json::to_string(&SerializedNotificationPanel { width })?, // ) // .await?; // anyhow::Ok(()) // } // .log_err(), // ); // } // fn render_notification( // &mut self, // ix: usize, // cx: &mut ViewContext, // ) -> Option> { // let entry = self.notification_store.read(cx).notification_at(ix)?; // let notification_id = entry.id; // let now = OffsetDateTime::now_utc(); // let timestamp = entry.timestamp; // let NotificationPresenter { // actor, // text, // needs_response, // can_navigate, // .. // } = self.present_notification(entry, cx)?; // let theme = theme::current(cx); // let style = &theme.notification_panel; // let response = entry.response; // let notification = entry.notification.clone(); // let message_style = if entry.is_read { // style.read_text.clone() // } else { // style.unread_text.clone() // }; // if self.active && !entry.is_read { // self.did_render_notification(notification_id, ¬ification, cx); // } // enum Decline {} // enum Accept {} // Some( // MouseEventHandler::new::(ix, cx, |_, cx| { // let container = message_style.container; // Flex::row() // .with_children(actor.map(|actor| { // render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container) // })) // .with_child( // Flex::column() // .with_child(Text::new(text, message_style.text.clone())) // .with_child( // Flex::row() // .with_child( // Label::new( // format_timestamp(timestamp, now, self.local_timezone), // style.timestamp.text.clone(), // ) // .contained() // .with_style(style.timestamp.container), // ) // .with_children(if let Some(is_accepted) = response { // Some( // Label::new( // if is_accepted { // "You accepted" // } else { // "You declined" // }, // style.read_text.text.clone(), // ) // .flex_float() // .into_any(), // ) // } else if needs_response { // Some( // Flex::row() // .with_children([ // MouseEventHandler::new::( // ix, // cx, // |state, _| { // let button = // style.button.style_for(state); // Label::new( // "Decline", // button.text.clone(), // ) // .contained() // .with_style(button.container) // }, // ) // .with_cursor_style(CursorStyle::PointingHand) // .on_click(MouseButton::Left, { // let notification = notification.clone(); // move |_, view, cx| { // view.respond_to_notification( // notification.clone(), // false, // cx, // ); // } // }), // MouseEventHandler::new::( // ix, // cx, // |state, _| { // let button = // style.button.style_for(state); // Label::new( // "Accept", // button.text.clone(), // ) // .contained() // .with_style(button.container) // }, // ) // .with_cursor_style(CursorStyle::PointingHand) // .on_click(MouseButton::Left, { // let notification = notification.clone(); // move |_, view, cx| { // view.respond_to_notification( // notification.clone(), // true, // cx, // ); // } // }), // ]) // .flex_float() // .into_any(), // ) // } else { // None // }), // ) // .flex(1.0, true), // ) // .contained() // .with_style(container) // .into_any() // }) // .with_cursor_style(if can_navigate { // CursorStyle::PointingHand // } else { // CursorStyle::default() // }) // .on_click(MouseButton::Left, { // let notification = notification.clone(); // move |_, this, cx| this.did_click_notification(¬ification, cx) // }) // .into_any(), // ) // } // fn present_notification( // &self, // entry: &NotificationEntry, // cx: &AppContext, // ) -> Option { // let user_store = self.user_store.read(cx); // let channel_store = self.channel_store.read(cx); // match entry.notification { // Notification::ContactRequest { sender_id } => { // let requester = user_store.get_cached_user(sender_id)?; // Some(NotificationPresenter { // icon: "icons/plus.svg", // text: format!("{} wants to add you as a contact", requester.github_login), // needs_response: user_store.has_incoming_contact_request(requester.id), // actor: Some(requester), // can_navigate: false, // }) // } // Notification::ContactRequestAccepted { responder_id } => { // let responder = user_store.get_cached_user(responder_id)?; // Some(NotificationPresenter { // icon: "icons/plus.svg", // text: format!("{} accepted your contact invite", responder.github_login), // needs_response: false, // actor: Some(responder), // can_navigate: false, // }) // } // Notification::ChannelInvitation { // ref channel_name, // channel_id, // inviter_id, // } => { // let inviter = user_store.get_cached_user(inviter_id)?; // Some(NotificationPresenter { // icon: "icons/hash.svg", // text: format!( // "{} invited you to join the #{channel_name} channel", // inviter.github_login // ), // needs_response: channel_store.has_channel_invitation(channel_id), // actor: Some(inviter), // can_navigate: false, // }) // } // Notification::ChannelMessageMention { // sender_id, // channel_id, // message_id, // } => { // let sender = user_store.get_cached_user(sender_id)?; // let channel = channel_store.channel_for_id(channel_id)?; // let message = self // .notification_store // .read(cx) // .channel_message_for_id(message_id)?; // Some(NotificationPresenter { // icon: "icons/conversations.svg", // text: format!( // "{} mentioned you in #{}:\n{}", // sender.github_login, channel.name, message.body, // ), // needs_response: false, // actor: Some(sender), // can_navigate: true, // }) // } // } // } // fn did_render_notification( // &mut self, // notification_id: u64, // notification: &Notification, // cx: &mut ViewContext, // ) { // let should_mark_as_read = match notification { // Notification::ContactRequestAccepted { .. } => true, // Notification::ContactRequest { .. } // | Notification::ChannelInvitation { .. } // | Notification::ChannelMessageMention { .. } => false, // }; // if should_mark_as_read { // self.mark_as_read_tasks // .entry(notification_id) // .or_insert_with(|| { // let client = self.client.clone(); // cx.spawn(|this, mut cx| async move { // cx.background().timer(MARK_AS_READ_DELAY).await; // client // .request(proto::MarkNotificationRead { notification_id }) // .await?; // this.update(&mut cx, |this, _| { // this.mark_as_read_tasks.remove(¬ification_id); // })?; // Ok(()) // }) // }); // } // } // fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext) { // if let Notification::ChannelMessageMention { // message_id, // channel_id, // .. // } = notification.clone() // { // if let Some(workspace) = self.workspace.upgrade(cx) { // cx.app_context().defer(move |cx| { // workspace.update(cx, |workspace, cx| { // if let Some(panel) = workspace.focus_panel::(cx) { // panel.update(cx, |panel, cx| { // panel // .select_channel(channel_id, Some(message_id), cx) // .detach_and_log_err(cx); // }); // } // }); // }); // } // } // } // fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool { // if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification { // if let Some(workspace) = self.workspace.upgrade(cx) { // return workspace // .read_with(cx, |workspace, cx| { // if let Some(panel) = workspace.panel::(cx) { // return panel.read_with(cx, |panel, cx| { // panel.is_scrolled_to_bottom() // && panel.active_chat().map_or(false, |chat| { // chat.read(cx).channel_id == *channel_id // }) // }); // } // false // }) // .unwrap_or_default(); // } // } // false // } // fn render_sign_in_prompt( // &self, // theme: &Arc, // cx: &mut ViewContext, // ) -> AnyElement { // enum SignInPromptLabel {} // MouseEventHandler::new::(0, cx, |mouse_state, _| { // Label::new( // "Sign in to view your notifications".to_string(), // theme // .chat_panel // .sign_in_prompt // .style_for(mouse_state) // .clone(), // ) // }) // .with_cursor_style(CursorStyle::PointingHand) // .on_click(MouseButton::Left, move |_, this, cx| { // let client = this.client.clone(); // cx.spawn(|_, cx| async move { // client.authenticate_and_connect(true, &cx).log_err().await; // }) // .detach(); // }) // .aligned() // .into_any() // } // fn render_empty_state( // &self, // theme: &Arc, // _cx: &mut ViewContext, // ) -> AnyElement { // Label::new( // "You have no notifications".to_string(), // theme.chat_panel.sign_in_prompt.default.clone(), // ) // .aligned() // .into_any() // } // fn on_notification_event( // &mut self, // _: ModelHandle, // event: &NotificationEvent, // cx: &mut ViewContext, // ) { // match event { // NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx), // NotificationEvent::NotificationRemoved { entry } // | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx), // NotificationEvent::NotificationsUpdated { // old_range, // new_count, // } => { // self.notification_list.splice(old_range.clone(), *new_count); // cx.notify(); // } // } // } // fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { // if self.is_showing_notification(&entry.notification, cx) { // return; // } // let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx) // else { // return; // }; // let notification_id = entry.id; // self.current_notification_toast = Some(( // notification_id, // cx.spawn(|this, mut cx| async move { // cx.background().timer(TOAST_DURATION).await; // this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx)) // .ok(); // }), // )); // self.workspace // .update(cx, |workspace, cx| { // workspace.dismiss_notification::(0, cx); // workspace.show_notification(0, cx, |cx| { // let workspace = cx.weak_handle(); // cx.add_view(|_| NotificationToast { // notification_id, // actor, // text, // workspace, // }) // }) // }) // .ok(); // } // fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext) { // if let Some((current_id, _)) = &self.current_notification_toast { // if *current_id == notification_id { // self.current_notification_toast.take(); // self.workspace // .update(cx, |workspace, cx| { // workspace.dismiss_notification::(0, cx) // }) // .ok(); // } // } // } // fn respond_to_notification( // &mut self, // notification: Notification, // response: bool, // cx: &mut ViewContext, // ) { // self.notification_store.update(cx, |store, cx| { // store.respond_to_notification(notification, response, cx); // }); // } // } // impl Entity for NotificationPanel { // type Event = Event; // } // impl View for NotificationPanel { // fn ui_name() -> &'static str { // "NotificationPanel" // } // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { // let theme = theme::current(cx); // let style = &theme.notification_panel; // let element = if self.client.user_id().is_none() { // self.render_sign_in_prompt(&theme, cx) // } else if self.notification_list.item_count() == 0 { // self.render_empty_state(&theme, cx) // } else { // Flex::column() // .with_child( // Flex::row() // .with_child(Label::new("Notifications", style.title.text.clone())) // .with_child(ui::svg(&style.title_icon).flex_float()) // .align_children_center() // .contained() // .with_style(style.title.container) // .constrained() // .with_height(style.title_height), // ) // .with_child( // List::new(self.notification_list.clone()) // .contained() // .with_style(style.list) // .flex(1., true), // ) // .into_any() // }; // element // .contained() // .with_style(style.container) // .constrained() // .with_min_width(150.) // .into_any() // } // fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext) { // self.has_focus = true; // } // fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { // self.has_focus = false; // } // } // impl Panel for NotificationPanel { // fn position(&self, cx: &gpui::WindowContext) -> DockPosition { // settings::get::(cx).dock // } // fn position_is_valid(&self, position: DockPosition) -> bool { // matches!(position, DockPosition::Left | DockPosition::Right) // } // fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { // settings::update_settings_file::( // self.fs.clone(), // cx, // move |settings| settings.dock = Some(position), // ); // } // fn size(&self, cx: &gpui::WindowContext) -> f32 { // self.width // .unwrap_or_else(|| settings::get::(cx).default_width) // } // fn set_size(&mut self, size: Option, cx: &mut ViewContext) { // self.width = size; // self.serialize(cx); // cx.notify(); // } // fn set_active(&mut self, active: bool, cx: &mut ViewContext) { // self.active = active; // if self.notification_store.read(cx).notification_count() == 0 { // cx.emit(Event::Dismissed); // } // } // fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { // (settings::get::(cx).button // && self.notification_store.read(cx).notification_count() > 0) // .then(|| "icons/bell.svg") // } // fn icon_tooltip(&self) -> (String, Option>) { // ( // "Notification Panel".to_string(), // Some(Box::new(ToggleFocus)), // ) // } // fn icon_label(&self, cx: &WindowContext) -> Option { // let count = self.notification_store.read(cx).unread_notification_count(); // if count == 0 { // None // } else { // Some(count.to_string()) // } // } // fn should_change_position_on_event(event: &Self::Event) -> bool { // matches!(event, Event::DockPositionChanged) // } // fn should_close_on_event(event: &Self::Event) -> bool { // matches!(event, Event::Dismissed) // } // fn has_focus(&self, _cx: &gpui::WindowContext) -> bool { // self.has_focus // } // fn is_focus_event(event: &Self::Event) -> bool { // matches!(event, Event::Focus) // } // } // pub struct NotificationToast { // notification_id: u64, // actor: Option>, // text: String, // workspace: WeakViewHandle, // } // pub enum ToastEvent { // Dismiss, // } // impl NotificationToast { // fn focus_notification_panel(&self, cx: &mut AppContext) { // let workspace = self.workspace.clone(); // let notification_id = self.notification_id; // cx.defer(move |cx| { // workspace // .update(cx, |workspace, cx| { // if let Some(panel) = workspace.focus_panel::(cx) { // panel.update(cx, |panel, cx| { // let store = panel.notification_store.read(cx); // if let Some(entry) = store.notification_for_id(notification_id) { // panel.did_click_notification(&entry.clone().notification, cx); // } // }); // } // }) // .ok(); // }) // } // } // impl Entity for NotificationToast { // type Event = ToastEvent; // } // impl View for NotificationToast { // fn ui_name() -> &'static str { // "ContactNotification" // } // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { // let user = self.actor.clone(); // let theme = theme::current(cx).clone(); // let theme = &theme.contact_notification; // MouseEventHandler::new::(0, cx, |_, cx| { // Flex::row() // .with_children(user.and_then(|user| { // Some( // Image::from_data(user.avatar.clone()?) // .with_style(theme.header_avatar) // .aligned() // .constrained() // .with_height( // cx.font_cache() // .line_height(theme.header_message.text.font_size), // ) // .aligned() // .top(), // ) // })) // .with_child( // Text::new(self.text.clone(), theme.header_message.text.clone()) // .contained() // .with_style(theme.header_message.container) // .aligned() // .top() // .left() // .flex(1., true), // ) // .with_child( // MouseEventHandler::new::(0, cx, |state, _| { // let style = theme.dismiss_button.style_for(state); // Svg::new("icons/x.svg") // .with_color(style.color) // .constrained() // .with_width(style.icon_width) // .aligned() // .contained() // .with_style(style.container) // .constrained() // .with_width(style.button_width) // .with_height(style.button_width) // }) // .with_cursor_style(CursorStyle::PointingHand) // .with_padding(Padding::uniform(5.)) // .on_click(MouseButton::Left, move |_, _, cx| { // cx.emit(ToastEvent::Dismiss) // }) // .aligned() // .constrained() // .with_height( // cx.font_cache() // .line_height(theme.header_message.text.font_size), // ) // .aligned() // .top() // .flex_float(), // ) // .contained() // }) // .with_cursor_style(CursorStyle::PointingHand) // .on_click(MouseButton::Left, move |_, this, cx| { // this.focus_notification_panel(cx); // cx.emit(ToastEvent::Dismiss); // }) // .into_any() // } // } // impl workspace::notifications::Notification for NotificationToast { // fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { // matches!(event, ToastEvent::Dismiss) // } // } // fn format_timestamp( // mut timestamp: OffsetDateTime, // mut now: OffsetDateTime, // local_timezone: UtcOffset, // ) -> String { // timestamp = timestamp.to_offset(local_timezone); // now = now.to_offset(local_timezone); // let today = now.date(); // let date = timestamp.date(); // if date == today { // let difference = now - timestamp; // if difference >= Duration::from_secs(3600) { // format!("{}h", difference.whole_seconds() / 3600) // } else if difference >= Duration::from_secs(60) { // format!("{}m", difference.whole_seconds() / 60) // } else { // "just now".to_string() // } // } else if date.next_day() == Some(today) { // format!("yesterday") // } else { // format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) // } // }