ZIm/crates/collab_ui2/src/notification_panel.rs
2023-11-15 16:09:21 -07:00

884 lines
35 KiB
Rust

// 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, &notification, 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(&notification, 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(&notification_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, .. } = &notification {
// 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())
// }
// }