@@ -1,884 +1,711 @@
-// 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<Client>,
-// user_store: ModelHandle<UserStore>,
-// channel_store: ModelHandle<ChannelStore>,
-// notification_store: ModelHandle<NotificationStore>,
-// fs: Arc<dyn Fs>,
-// width: Option<f32>,
-// active: bool,
-// notification_list: ListState<Self>,
-// pending_serialization: Task<Option<()>>,
-// subscriptions: Vec<gpui::Subscription>,
-// workspace: WeakViewHandle<Workspace>,
-// current_notification_toast: Option<(u64, Task<()>)>,
-// local_timezone: UtcOffset,
-// has_focus: bool,
-// mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
-// }
-
-// #[derive(Serialize, Deserialize)]
-// struct SerializedNotificationPanel {
-// width: Option<f32>,
-// }
-
-// #[derive(Debug)]
-// pub enum Event {
-// DockPositionChanged,
-// Focus,
-// Dismissed,
-// }
-
-// pub struct NotificationPresenter {
-// pub actor: Option<Arc<client::User>>,
-// 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<Workspace>) -> ViewHandle<Self> {
-// 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::<Self>::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::<SettingsStore, _>(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<Workspace>,
-// cx: AsyncAppContext,
-// ) -> Task<Result<ViewHandle<Self>>> {
-// 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::<SerializedNotificationPanel>(&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<Self>) {
-// 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<Self>,
-// ) -> Option<AnyElement<Self>> {
-// 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::<NotificationEntry, _>(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::<Decline, _>(
-// 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::<Accept, _>(
-// 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<NotificationPresenter> {
-// 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<Self>,
-// ) {
-// 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<Self>) {
-// 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::<ChatPanel>(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::<ChatPanel>(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<Theme>,
-// cx: &mut ViewContext<Self>,
-// ) -> AnyElement<Self> {
-// enum SignInPromptLabel {}
-
-// MouseEventHandler::new::<SignInPromptLabel, _>(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<Theme>,
-// _cx: &mut ViewContext<Self>,
-// ) -> AnyElement<Self> {
-// 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<NotificationStore>,
-// event: &NotificationEvent,
-// cx: &mut ViewContext<Self>,
-// ) {
-// 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<Self>) {
-// 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::<NotificationToast>(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<Self>) {
-// 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::<NotificationToast>(0, cx)
-// })
-// .ok();
-// }
-// }
-// }
-
-// fn respond_to_notification(
-// &mut self,
-// notification: Notification,
-// response: bool,
-// cx: &mut ViewContext<Self>,
-// ) {
-// 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<Self>) -> AnyElement<Self> {
-// 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>) {
-// self.has_focus = true;
-// }
-
-// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-// self.has_focus = false;
-// }
-// }
-
-// impl Panel for NotificationPanel {
-// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-// settings::get::<NotificationPanelSettings>(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<Self>) {
-// settings::update_settings_file::<NotificationPanelSettings>(
-// self.fs.clone(),
-// cx,
-// move |settings| settings.dock = Some(position),
-// );
-// }
-
-// fn size(&self, cx: &gpui::WindowContext) -> f32 {
-// self.width
-// .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
-// }
-
-// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
-// self.width = size;
-// self.serialize(cx);
-// cx.notify();
-// }
-
-// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-// 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::<NotificationPanelSettings>(cx).button
-// && self.notification_store.read(cx).notification_count() > 0)
-// .then(|| "icons/bell.svg")
-// }
-
-// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-// (
-// "Notification Panel".to_string(),
-// Some(Box::new(ToggleFocus)),
-// )
-// }
-
-// fn icon_label(&self, cx: &WindowContext) -> Option<String> {
-// 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<Arc<User>>,
-// text: String,
-// workspace: WeakViewHandle<Workspace>,
-// }
-
-// 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::<NotificationPanel>(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<Self>) -> AnyElement<Self> {
-// let user = self.actor.clone();
-// let theme = theme::current(cx).clone();
-// let theme = &theme.contact_notification;
-
-// MouseEventHandler::new::<Self, _>(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::<ToastEvent, _>(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: &<Self as Entity>::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())
-// }
-// }
+use crate::{chat_panel::ChatPanel, 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, div, img, serde_json, svg, AnyElement, AnyView, AppContext, AsyncAppContext, Context,
+ CursorStyle, Div, Entity, EventEmitter, Flatten, FocusHandle, FocusableView,
+ InteractiveElement, IntoElement, ListAlignment, ListState, Model, MouseButton, ParentElement,
+ Render, Stateful, StatefulInteractiveElement, Task, View, ViewContext, VisualContext, WeakView,
+ WindowContext,
+};
+use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
+use project::Fs;
+use rpc::proto;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
+use std::{sync::Arc, time::Duration};
+use time::{OffsetDateTime, UtcOffset};
+use ui::{h_stack, v_stack, Avatar, Button, Clickable, Icon, IconButton, IconElement, Label, List};
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+ dock::{DockPosition, Panel, PanelEvent},
+ 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<Client>,
+ user_store: Model<UserStore>,
+ channel_store: Model<ChannelStore>,
+ notification_store: Model<NotificationStore>,
+ fs: Arc<dyn Fs>,
+ width: Option<f32>,
+ active: bool,
+ notification_list: ListState,
+ pending_serialization: Task<Option<()>>,
+ subscriptions: Vec<gpui::Subscription>,
+ workspace: WeakView<Workspace>,
+ current_notification_toast: Option<(u64, Task<()>)>,
+ local_timezone: UtcOffset,
+ focus_handle: FocusHandle,
+ mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedNotificationPanel {
+ width: Option<f32>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+ DockPositionChanged,
+ Focus,
+ Dismissed,
+}
+
+pub struct NotificationPresenter {
+ pub actor: Option<Arc<client::User>>,
+ 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<Workspace>) -> View<Self> {
+ 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.build_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, ListAlignment::Top, 1000., move |this, ix, cx| {
+ this.render_notification(ix, cx).unwrap_or_else(|| div())
+ });
+ 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,
+ focus_handle: cx.focus_handle(),
+ 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::<SettingsStore>(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: WeakView<Workspace>, cx: AsyncAppContext) -> Task<Result<View<Self>>> {
+ cx.spawn(|mut cx| async move {
+ let serialized_panel = if let Some(panel) = cx
+ .background_executor()
+ .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
+ .await
+ .log_err()
+ .flatten()
+ {
+ Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
+ } else {
+ None
+ };
+
+ Flatten::flatten(cx.update(|cx| {
+ workspace.update(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<Self>) {
+ 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<Self>) -> Option<AnyElement> {
+ 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 response = entry.response;
+ let notification = entry.notification.clone();
+
+ if self.active && !entry.is_read {
+ self.did_render_notification(notification_id, ¬ification, cx);
+ }
+
+ Some(
+ h_stack()
+ .children(actor.map(|actor| Avatar::from(actor.avatar.clone())))
+ .child(
+ v_stack().child(Label::new(text)).child(
+ h_stack()
+ .child(Label::from(format_timestamp(
+ timestamp,
+ now,
+ self.local_timezone,
+ )))
+ .children(if let Some(is_accepted) = response {
+ Some(Label::new(if is_accepted {
+ "You accepted"
+ } else {
+ "You declined"
+ }))
+ } else if needs_response {
+ Some(
+ h_stack()
+ .child(Button::new("decline", "Decline").on_click({
+ let notification = notification.clone();
+ let view = cx.view().clone();
+ move |_, cx| {
+ view.update(cx, |this, cx| {
+ this.respond_to_notification(
+ notification.clone(),
+ false,
+ cx,
+ )
+ });
+ }
+ }))
+ .child(Button::new("accept", "Accept").on_click({
+ let notification = notification.clone();
+ let view = cx.view().clone();
+ move |_, cx| {
+ view.update(cx, |this, cx| {
+ this.respond_to_notification(
+ notification.clone(),
+ true,
+ cx,
+ )
+ });
+ }
+ })),
+ )
+ } else {
+ None
+ }),
+ ),
+ ),
+ )
+ }
+
+ fn present_notification(
+ &self,
+ entry: &NotificationEntry,
+ cx: &AppContext,
+ ) -> Option<NotificationPresenter> {
+ 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<Self>,
+ ) {
+ 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<Self>) {
+ if let Notification::ChannelMessageMention {
+ message_id,
+ channel_id,
+ ..
+ } = notification.clone()
+ {
+ if let Some(workspace) = self.workspace.upgrade() {
+ cx.app_context().defer(move |cx| {
+ workspace.update(cx, |workspace, cx| {
+ if let Some(panel) = workspace.focus_panel::<ChatPanel>(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() {
+ return workspace
+ .read_with(cx, |workspace, cx| {
+ if let Some(panel) = workspace.panel::<ChatPanel>(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, cx: &mut ViewContext<Self>) -> AnyElement {
+ Button::new(
+ "sign_in_prompt_button",
+ "Sign in to view your notifications",
+ )
+ .on_click({
+ let client = self.client.clone();
+ |_, cx| {
+ cx.spawn(|cx| async move {
+ client.authenticate_and_connect(true, &cx).log_err().await;
+ })
+ .detach()
+ }
+ })
+ .into_any_element()
+ }
+
+ fn render_empty_state(&self) -> AnyElement {
+ Label::new("You have no notifications").into_any_element()
+ }
+
+ fn on_notification_event(
+ &mut self,
+ _: Model<NotificationStore>,
+ event: &NotificationEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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<Self>) {
+ 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::<NotificationToast>(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<Self>) {
+ 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::<NotificationToast>(0, cx)
+ })
+ .ok();
+ }
+ }
+ }
+
+ fn respond_to_notification(
+ &mut self,
+ notification: Notification,
+ response: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.notification_store.update(cx, |store, cx| {
+ store.respond_to_notification(notification, response, cx);
+ });
+ }
+}
+
+impl Render for NotificationPanel {
+ type Element = AnyElement;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement {
+ if self.client.user_id().is_none() {
+ self.render_sign_in_prompt(cx)
+ } else if self.notification_list.item_count() == 0 {
+ self.render_empty_state()
+ } else {
+ v_stack()
+ .child(
+ h_stack()
+ .child(Label::new("Notifications"))
+ .child(IconElement::new(Icon::Envelope)),
+ )
+ // todo!()
+ // .child(
+ // List::new()
+ // )
+ .into_any_element()
+
+ // 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()
+ }
+ }
+}
+
+impl FocusableView for NotificationPanel {
+ fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+ self.focus_handle
+ }
+}
+
+impl EventEmitter<Event> for NotificationPanel {}
+impl EventEmitter<PanelEvent> for NotificationPanel {}
+
+impl Panel for NotificationPanel {
+ fn persistent_name() -> &'static str {
+ "NotificationPanel"
+ }
+
+ fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+ NotificationPanelSettings::get_global(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<Self>) {
+ settings::update_settings_file::<NotificationPanelSettings>(
+ self.fs.clone(),
+ cx,
+ move |settings| settings.dock = Some(position),
+ );
+ }
+
+ fn size(&self, cx: &gpui::WindowContext) -> f32 {
+ self.width
+ .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
+ }
+
+ fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+ self.width = size;
+ self.serialize(cx);
+ cx.notify();
+ }
+
+ fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+ self.active = active;
+ if self.notification_store.read(cx).notification_count() == 0 {
+ cx.emit(Event::Dismissed);
+ }
+ }
+
+ fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
+ (NotificationPanelSettings::get_global(cx).button
+ && self.notification_store.read(cx).notification_count() > 0)
+ .then(|| Icon::Bell)
+ }
+
+ fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+ let count = self.notification_store.read(cx).unread_notification_count();
+ if count == 0 {
+ None
+ } else {
+ Some(count.to_string())
+ }
+ }
+
+ fn toggle_action(&self) -> Box<dyn gpui::Action> {
+ Box::new(ToggleFocus)
+ }
+}
+
+pub struct NotificationToast {
+ notification_id: u64,
+ actor: Option<Arc<User>>,
+ text: String,
+ workspace: WeakView<Workspace>,
+}
+
+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::<NotificationPanel>(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 Render for NotificationToast {
+ type Element = Stateful<Div>;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let user = self.actor.clone();
+
+ h_stack()
+ .id("notification_panel_toast")
+ .children(user.and_then(|user| Some(img(user.avatar.clone()?))))
+ .child(Label::new(self.text.clone()))
+ .child(
+ IconButton::new("close", Icon::Close)
+ .on_click(|_, cx| cx.emit(ToastEvent::Dismiss)),
+ )
+ .on_click({
+ let this = cx.view().clone();
+ |_, cx| {
+ this.update(cx, |this, cx| this.focus_notification_panel(cx));
+ cx.emit(ToastEvent::Dismiss);
+ }
+ })
+ }
+}
+
+impl EventEmitter<ToastEvent> for NotificationToast {}
+
+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())
+ }
+}