From 376716254f6af33e7702e188e72835027b5d8c5b Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 11 Dec 2023 20:40:48 -0500 Subject: [PATCH 1/8] Start port of notification panel --- crates/collab_ui2/src/notification_panel.rs | 1595 +++++++++---------- 1 file changed, 711 insertions(+), 884 deletions(-) diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index 4b5a99a0ed..2e5058d791 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -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, -// 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()) -// } -// } +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, + user_store: Model, + channel_store: Model, + notification_store: Model, + fs: Arc, + width: Option, + active: bool, + notification_list: ListState, + pending_serialization: Task>, + subscriptions: Vec, + workspace: WeakView, + current_notification_toast: Option<(u64, Task<()>)>, + local_timezone: UtcOffset, + focus_handle: FocusHandle, + 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) -> View { + 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::(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, cx: AsyncAppContext) -> Task>> { + 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::(&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) { + 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 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 { + 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.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() { + 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, cx: &mut ViewContext) -> 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, + 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 Render for NotificationPanel { + type Element = AnyElement; + + fn render(&mut self, cx: &mut ViewContext) -> 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 for NotificationPanel {} +impl EventEmitter 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) { + 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(|| NotificationPanelSettings::get_global(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(&self, cx: &gpui::WindowContext) -> Option { + (NotificationPanelSettings::get_global(cx).button + && self.notification_store.read(cx).notification_count() > 0) + .then(|| Icon::Bell) + } + + 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 toggle_action(&self) -> Box { + Box::new(ToggleFocus) + } +} + +pub struct NotificationToast { + notification_id: u64, + actor: Option>, + text: String, + workspace: WeakView, +} + +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 Render for NotificationToast { + type Element = Stateful
; + + fn render(&mut self, cx: &mut ViewContext) -> 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 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()) + } +} From 44d40625fee03966164352ac5874526de6153b71 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Dec 2023 11:26:37 -0500 Subject: [PATCH 2/8] Start changing Avatar to use URI Co-Authored-By: Antonio Scandurra --- crates/channel2/src/channel_store_tests.rs | 2 +- crates/client2/src/test.rs | 4 +- crates/client2/src/user.rs | 32 ++--- crates/collab2/src/tests/integration_tests.rs | 4 +- crates/collab2/src/tests/test_server.rs | 2 +- crates/collab_ui2/src/chat_panel.rs | 10 +- .../src/chat_panel/message_editor.rs | 6 +- crates/collab_ui2/src/collab_panel.rs | 85 ++++++------- .../src/collab_panel/contact_finder.rs | 4 +- crates/collab_ui2/src/collab_titlebar_item.rs | 112 ++++++++---------- crates/collab_ui2/src/face_pile.rs | 13 +- crates/collab_ui2/src/notification_panel.rs | 83 +++++++------ crates/project2/src/project2.rs | 2 +- crates/ui2/src/components/avatar.rs | 12 +- crates/ui2/src/components/list/list_item.rs | 2 +- crates/ui2/src/components/stories/avatar.rs | 8 +- crates/workspace2/src/workspace2.rs | 2 +- crates/zed2/src/main.rs | 2 +- crates/zed2/src/zed2.rs | 8 +- 19 files changed, 190 insertions(+), 203 deletions(-) diff --git a/crates/channel2/src/channel_store_tests.rs b/crates/channel2/src/channel_store_tests.rs index e193917b76..7f392032cd 100644 --- a/crates/channel2/src/channel_store_tests.rs +++ b/crates/channel2/src/channel_store_tests.rs @@ -345,7 +345,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { fn init_test(cx: &mut AppContext) -> Model { let http = FakeHttpClient::with_404_response(); let client = Client::new(http.clone(), cx); - let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx)); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx)); let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); diff --git a/crates/client2/src/test.rs b/crates/client2/src/test.rs index 086cbd570e..ce3ea740b6 100644 --- a/crates/client2/src/test.rs +++ b/crates/client2/src/test.rs @@ -8,7 +8,6 @@ use rpc::{ ConnectionId, Peer, Receipt, TypedEnvelope, }; use std::sync::Arc; -use util::http::FakeHttpClient; pub struct FakeServer { peer: Arc, @@ -195,8 +194,7 @@ impl FakeServer { client: Arc, cx: &mut TestAppContext, ) -> Model { - let http_client = FakeHttpClient::with_404_response(); - let user_store = cx.build_model(|cx| UserStore::new(client, http_client, cx)); + let user_store = cx.build_model(|cx| UserStore::new(client, cx)); assert_eq!( self.receive::() .await diff --git a/crates/client2/src/user.rs b/crates/client2/src/user.rs index a5dba03d2d..766861fbf8 100644 --- a/crates/client2/src/user.rs +++ b/crates/client2/src/user.rs @@ -2,8 +2,8 @@ use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use feature_flags::FeatureFlagAppExt; -use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; -use gpui::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, Task}; +use futures::{channel::mpsc, AsyncReadExt, Future, StreamExt}; +use gpui::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, SharedString, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; @@ -20,7 +20,7 @@ pub struct ParticipantIndex(pub u32); pub struct User { pub id: UserId, pub github_login: String, - pub avatar: Option>, + pub avatar_uri: SharedString, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -76,7 +76,6 @@ pub struct UserStore { pending_contact_requests: HashMap, invite_info: Option, client: Weak, - http: Arc, _maintain_contacts: Task<()>, _maintain_current_user: Task>, } @@ -114,7 +113,6 @@ enum UpdateContacts { impl UserStore { pub fn new( client: Arc, - http: Arc, cx: &mut ModelContext, ) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); @@ -134,7 +132,6 @@ impl UserStore { invite_info: None, client: Arc::downgrade(&client), update_contacts_tx, - http, _maintain_contacts: cx.spawn(|this, mut cx| async move { let _subscriptions = rpc_subscriptions; while let Some(message) = update_contacts_rx.next().await { @@ -445,6 +442,12 @@ impl UserStore { self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx) } + pub fn has_incoming_contact_request(&self, user_id: u64) -> bool { + self.incoming_contact_requests + .iter() + .any(|user| user.id == user_id) + } + pub fn respond_to_contact_request( &mut self, requester_id: u64, @@ -616,17 +619,14 @@ impl UserStore { cx: &mut ModelContext, ) -> Task>>> { let client = self.client.clone(); - let http = self.http.clone(); cx.spawn(|this, mut cx| async move { if let Some(rpc) = client.upgrade() { let response = rpc.request(request).await.context("error loading users")?; - let users = future::join_all( - response - .users - .into_iter() - .map(|user| User::new(user, http.as_ref())), - ) - .await; + let users = response + .users + .into_iter() + .map(|user| User::new(user)) + .collect::>(); this.update(&mut cx, |this, _| { for user in &users { @@ -659,11 +659,11 @@ impl UserStore { } impl User { - async fn new(message: proto::User, http: &dyn HttpClient) -> Arc { + fn new(message: proto::User) -> Arc { Arc::new(User { id: message.id, github_login: message.github_login, - avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await, + avatar_uri: message.avatar_url.into(), }) } } diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index 823c8e9045..201ba07dbb 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -1823,7 +1823,7 @@ async fn test_active_call_events( owner: Arc::new(User { id: client_a.user_id().unwrap(), github_login: "user_a".to_string(), - avatar: None, + avatar_uri: "avatar_a".into(), }), project_id: project_a_id, worktree_root_names: vec!["a".to_string()], @@ -1841,7 +1841,7 @@ async fn test_active_call_events( owner: Arc::new(User { id: client_b.user_id().unwrap(), github_login: "user_b".to_string(), - avatar: None, + avatar_uri: "avatar_b".into(), }), project_id: project_b_id, worktree_root_names: vec!["b".to_string()] diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index f751736971..cfecb4880d 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -209,7 +209,7 @@ impl TestServer { }); let fs = FakeFs::new(cx.executor()); - let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx)); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); let mut language_registry = LanguageRegistry::test(); language_registry.set_executor(cx.executor()); diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs index c484016aae..6715ddf513 100644 --- a/crates/collab_ui2/src/chat_panel.rs +++ b/crates/collab_ui2/src/chat_panel.rs @@ -364,13 +364,7 @@ impl ChatPanel { if !is_continuation { result = result.child( h_stack() - .children( - message - .sender - .avatar - .clone() - .map(|avatar| Avatar::data(avatar)), - ) + .child(Avatar::new(message.sender.avatar_uri.clone())) .child(Label::new(message.sender.github_login.clone())) .child(Label::new(format_timestamp( message.timestamp, @@ -659,7 +653,7 @@ mod tests { timestamp: OffsetDateTime::now_utc(), sender: Arc::new(client::User { github_login: "fgh".into(), - avatar: None, + avatar_uri: "avatar_fgh".into(), id: 103, }), nonce: 5, diff --git a/crates/collab_ui2/src/chat_panel/message_editor.rs b/crates/collab_ui2/src/chat_panel/message_editor.rs index 61e621525f..0b9d0be0b4 100644 --- a/crates/collab_ui2/src/chat_panel/message_editor.rs +++ b/crates/collab_ui2/src/chat_panel/message_editor.rs @@ -234,7 +234,7 @@ mod tests { user: Arc::new(User { github_login: "a-b".into(), id: 101, - avatar: None, + avatar_uri: "avatar_a-b".into(), }), kind: proto::channel_member::Kind::Member, role: proto::ChannelRole::Member, @@ -243,7 +243,7 @@ mod tests { user: Arc::new(User { github_login: "C_D".into(), id: 102, - avatar: None, + avatar_uri: "avatar_C_D".into(), }), kind: proto::channel_member::Kind::Member, role: proto::ChannelRole::Member, @@ -275,7 +275,7 @@ mod tests { cx.update(|cx| { let http = FakeHttpClient::with_404_response(); let client = Client::new(http.clone(), cx); - let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx)); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx)); let settings = SettingsStore::test(cx); cx.set_global(settings); theme::init(theme::LoadThemes::JustBase, cx); diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 0b917d72b0..bae1779d74 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1155,7 +1155,7 @@ impl CollabPanel { let tooltip = format!("Follow {}", user.github_login); ListItem::new(SharedString::from(user.github_login.clone())) - .left_child(Avatar::data(user.avatar.clone().unwrap())) + .left_child(Avatar::new(user.avatar_uri.clone())) .child( h_stack() .w_full() @@ -2365,44 +2365,45 @@ impl CollabPanel { let busy = contact.busy || calling; let user_id = contact.user.id; let github_login = SharedString::from(contact.user.github_login.clone()); - let mut item = ListItem::new(github_login.clone()) - .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) - .child( - h_stack() - .w_full() - .justify_between() - .child(Label::new(github_login.clone())) - .when(calling, |el| { - el.child(Label::new("Calling").color(Color::Muted)) - }) - .when(!calling, |el| { - el.child( - div() - .id("remove_contact") - .invisible() - .group_hover("", |style| style.visible()) - .child( - IconButton::new("remove_contact", Icon::Close) - .icon_color(Color::Muted) - .tooltip(|cx| Tooltip::text("Remove Contact", cx)) - .on_click(cx.listener({ - let github_login = github_login.clone(); - move |this, _, cx| { - this.remove_contact(user_id, &github_login, cx); - } - })), - ), - ) - }), - ) - .left_child( - // todo!() handle contacts with no avatar - Avatar::data(contact.user.avatar.clone().unwrap()) - .availability_indicator(if online { Some(!busy) } else { None }), - ) - .when(online && !busy, |el| { - el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) - }); + let mut item = + ListItem::new(github_login.clone()) + .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) + .child( + h_stack() + .w_full() + .justify_between() + .child(Label::new(github_login.clone())) + .when(calling, |el| { + el.child(Label::new("Calling").color(Color::Muted)) + }) + .when(!calling, |el| { + el.child( + div() + .id("remove_contact") + .invisible() + .group_hover("", |style| style.visible()) + .child( + IconButton::new("remove_contact", Icon::Close) + .icon_color(Color::Muted) + .tooltip(|cx| Tooltip::text("Remove Contact", cx)) + .on_click(cx.listener({ + let github_login = github_login.clone(); + move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + } + })), + ), + ) + }), + ) + .left_child( + // todo!() handle contacts with no avatar + Avatar::new(contact.user.avatar_uri.clone()) + .availability_indicator(if online { Some(!busy) } else { None }), + ) + .when(online && !busy, |el| { + el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) + }); div() .id(github_login.clone()) @@ -2474,7 +2475,7 @@ impl CollabPanel { .child(Label::new(github_login.clone())) .child(h_stack().children(controls)), ) - .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar)) + .left_avatar(user.avatar_uri.clone()) } fn render_contact_placeholder( @@ -2532,7 +2533,9 @@ impl CollabPanel { let result = FacePile { faces: participants .iter() - .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element())) + .filter_map(|user| { + Some(Avatar::new(user.avatar_uri.clone()).into_any_element()) + }) .take(FACEPILE_LIMIT) .chain(if extra_count > 0 { // todo!() @nate - this label looks wrong. diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index 742c25d148..bd0c5d4b07 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui2/src/collab_panel/contact_finder.rs @@ -7,7 +7,7 @@ use gpui::{ use picker::{Picker, PickerDelegate}; use std::sync::Arc; use theme::ActiveTheme as _; -use ui::prelude::*; +use ui::{prelude::*, Avatar}; use util::{ResultExt as _, TryFutureExt}; use workspace::ModalView; @@ -187,7 +187,7 @@ impl PickerDelegate for ContactFinderDelegate { div() .flex_1() .justify_between() - .children(user.avatar.clone().map(|avatar| img(avatar))) + .child(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) .children(icon_path.map(|icon_path| svg().path(icon_path))), ) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 4940893a6c..8c2d112f09 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -232,43 +232,41 @@ impl Render for CollabTitlebarItem { }) .child(h_stack().px_1p5().map(|this| { if let Some(user) = current_user { - this.when_some(user.avatar.clone(), |this, avatar| { - // TODO: Finish implementing user menu popover - // - this.child( - popover_menu("user-menu") - .menu(|cx| { - ContextMenu::build(cx, |menu, _| menu.header("ADADA")) - }) - .trigger( - ButtonLike::new("user-menu") - .child( - h_stack() - .gap_0p5() - .child(Avatar::data(avatar)) - .child( - IconElement::new(Icon::ChevronDown) - .color(Color::Muted), - ), - ) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| { - Tooltip::text("Toggle User Menu", cx) - }), - ) - .anchor(gpui::AnchorCorner::TopRight), - ) - // this.child( - // ButtonLike::new("user-menu") - // .child( - // h_stack().gap_0p5().child(Avatar::data(avatar)).child( - // IconElement::new(Icon::ChevronDown).color(Color::Muted), - // ), - // ) - // .style(ButtonStyle::Subtle) - // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), - // ) - }) + // TODO: Finish implementing user menu popover + // + this.child( + popover_menu("user-menu") + .menu(|cx| { + ContextMenu::build(cx, |menu, _| menu.header("ADADA")) + }) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack() + .gap_0p5() + .child(Avatar::new(user.avatar_uri.clone())) + .child( + IconElement::new(Icon::ChevronDown) + .color(Color::Muted), + ), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| { + Tooltip::text("Toggle User Menu", cx) + }), + ) + .anchor(gpui::AnchorCorner::TopRight), + ) + // this.child( + // ButtonLike::new("user-menu") + // .child( + // h_stack().gap_0p5().child(Avatar::data(avatar)).child( + // IconElement::new(Icon::ChevronDown).color(Color::Muted), + // ), + // ) + // .style(ButtonStyle::Subtle) + // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + // ) } else { this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { let client = client.clone(); @@ -425,26 +423,20 @@ impl CollabTitlebarItem { ) -> Option { let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); let mut pile = FacePile::default(); - pile.extend( - user.avatar - .clone() - .map(|avatar| { - div() - .child( - Avatar::data(avatar.clone()) - .grayscale(!is_present) - .border_color(if is_speaking { - gpui::blue() - } else if is_muted { - gpui::red() - } else { - Hsla::default() - }), - ) - .into_any_element() - }) - .into_iter() - .chain(followers.iter().filter_map(|follower_peer_id| { + pile.child( + div() + .child( + Avatar::new(user.avatar_uri.clone()) + .grayscale(!is_present) + .border_color(if is_speaking { + gpui::blue() + } else if is_muted { + gpui::red() + } else { + Hsla::default() + }), + ) + .children(followers.iter().filter_map(|follower_peer_id| { let follower = room .remote_participants() .values() @@ -454,10 +446,8 @@ impl CollabTitlebarItem { .then_some(current_user) })? .clone(); - follower - .avatar - .clone() - .map(|avatar| div().child(Avatar::data(avatar.clone())).into_any_element()) + + Some(div().child(Avatar::new(follower.avatar_uri.clone()))) })), ); Some(pile) diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index 52b30062ea..9b2406024f 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/crates/collab_ui2/src/face_pile.rs @@ -1,11 +1,12 @@ use gpui::{ - div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled, - WindowContext, + div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, ParentElement, RenderOnce, + Styled, WindowContext, }; +use smallvec::SmallVec; #[derive(Default, IntoElement)] pub struct FacePile { - pub faces: Vec, + pub faces: SmallVec<[AnyElement; 2]>, } impl RenderOnce for FacePile { @@ -25,8 +26,8 @@ impl RenderOnce for FacePile { } } -impl Extend for FacePile { - fn extend>(&mut self, children: T) { - self.faces.extend(children); +impl ParentElement for FacePile { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.faces } } diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index 2e5058d791..279c3cbd90 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -6,11 +6,11 @@ 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, + actions, div, img, px, serde_json, svg, AnyElement, AnyView, AppContext, AsyncAppContext, + AsyncWindowContext, Context, CursorStyle, Div, Element, Entity, EventEmitter, Flatten, + FocusHandle, FocusableView, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, + ListState, Model, MouseButton, ParentElement, Render, Stateful, StatefulInteractiveElement, + Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; @@ -80,7 +80,9 @@ impl NotificationPanel { let user_store = workspace.app_state().user_store.clone(); let workspace_handle = workspace.weak_handle(); - cx.build_view(|cx| { + cx.build_view(|cx: &mut ViewContext| { + let view = cx.view().clone(); + let mut status = client.status(); cx.spawn(|this, mut cx| async move { while let Some(_) = status.next().await { @@ -97,25 +99,30 @@ impl NotificationPanel { .detach(); let mut notification_list = - ListState::new(0, ListAlignment::Top, 1000., move |this, ix, cx| { - this.render_notification(ix, cx).unwrap_or_else(|| div()) + ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| { + view.update(cx, |this, cx| { + this.render_notification(ix, cx) + .unwrap_or_else(|| div().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(); + notification_list.set_scroll_handler(cx.listener( + |this, event: &ListScrollEvent, cx| { + if event.count.saturating_sub(event.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(), + local_timezone: cx.local_timezone(), channel_store: ChannelStore::global(cx), notification_store: NotificationStore::global(cx), notification_list, @@ -146,7 +153,10 @@ impl NotificationPanel { }) } - pub fn load(workspace: WeakView, cx: AsyncAppContext) -> Task>> { + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { cx.spawn(|mut cx| async move { let serialized_panel = if let Some(panel) = cx .background_executor() @@ -160,24 +170,22 @@ impl NotificationPanel { 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 - }) - })) + 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( + self.pending_serialization = cx.background_executor().spawn( async move { KEY_VALUE_STORE .write_kvp( @@ -217,17 +225,17 @@ impl NotificationPanel { .child( v_stack().child(Label::new(text)).child( h_stack() - .child(Label::from(format_timestamp( + .child(Label::new(format_timestamp( timestamp, now, self.local_timezone, ))) .children(if let Some(is_accepted) = response { - Some(Label::new(if is_accepted { + Some(div().child(Label::new(if is_accepted { "You accepted" } else { "You declined" - })) + }))) } else if needs_response { Some( h_stack() @@ -262,7 +270,8 @@ impl NotificationPanel { None }), ), - ), + ) + .into_any(), ) } @@ -355,7 +364,7 @@ impl NotificationPanel { .or_insert_with(|| { let client = self.client.clone(); cx.spawn(|this, mut cx| async move { - cx.background().timer(MARK_AS_READ_DELAY).await; + cx.background_executor().timer(MARK_AS_READ_DELAY).await; client .request(proto::MarkNotificationRead { notification_id }) .await?; diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index fe3498b930..4e8a95fcfd 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -867,7 +867,7 @@ impl Project { languages.set_executor(cx.executor()); let http_client = util::http::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); - let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx)); let project = cx.update(|cx| { Project::local( client, diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 454daacc42..4162ee2256 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -58,16 +58,8 @@ impl RenderOnce for Avatar { } impl Avatar { - pub fn uri(src: impl Into) -> Self { - Self::source(src.into().into()) - } - - pub fn data(src: Arc) -> Self { - Self::source(src.into()) - } - - pub fn source(src: ImageSource) -> Self { - Self { + pub fn new(src: impl Into) -> Self { + Avatar { image: img(src), is_available: None, border_color: None, diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 529f2c2a58..e089a2b479 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -107,7 +107,7 @@ impl ListItem { } pub fn left_avatar(mut self, left_avatar: impl Into) -> Self { - self.left_slot = Some(Avatar::source(left_avatar.into()).into_any_element()); + self.left_slot = Some(Avatar::new(left_avatar).into_any_element()); self } } diff --git a/crates/ui2/src/components/stories/avatar.rs b/crates/ui2/src/components/stories/avatar.rs index 1b5ceec2fd..09adfe480a 100644 --- a/crates/ui2/src/components/stories/avatar.rs +++ b/crates/ui2/src/components/stories/avatar.rs @@ -13,18 +13,18 @@ impl Render for AvatarStory { Story::container() .child(Story::title_for::()) .child(Story::label("Default")) - .child(Avatar::uri( + .child(Avatar::new( "https://avatars.githubusercontent.com/u/1714999?v=4", )) - .child(Avatar::uri( + .child(Avatar::new( "https://avatars.githubusercontent.com/u/326587?v=4", )) .child( - Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4") + Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") .availability_indicator(true), ) .child( - Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4") + Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") .availability_indicator(false), ) } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index eb846fbea8..adc8b0a38c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -360,7 +360,7 @@ impl AppState { let languages = Arc::new(LanguageRegistry::test()); let http_client = util::http::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); - let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); theme::init(theme::LoadThemes::JustBase, cx); diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index b97c4f40c3..92b20bf271 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -143,7 +143,7 @@ fn main() { language::init(cx); languages::init(languages.clone(), node_runtime.clone(), cx); - let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); + let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); cx.set_global(client.clone()); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 0a44bf6ec0..1414bbaa13 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -165,10 +165,10 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); let chat_panel = collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); - // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( - // workspace_handle.clone(), - // cx.clone(), - // ); + let notification_panel = collab_ui::notification_panel::NotificationPanel::load( + workspace_handle.clone(), + cx.clone(), + ); let ( project_panel, terminal_panel, From fd6320b136ab8d84a360ae3a5d3746ccd5467018 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Dec 2023 11:49:04 -0500 Subject: [PATCH 3/8] Finish fixing up Avatar using URI --- crates/client2/src/user.rs | 32 ++----------------- crates/collab_ui2/src/collab_panel.rs | 3 +- crates/collab_ui2/src/notification_panel.rs | 4 +-- .../incoming_call_notification.rs | 9 +----- .../project_shared_notification.rs | 7 +--- crates/ui2/src/components/avatar.rs | 3 +- 6 files changed, 10 insertions(+), 48 deletions(-) diff --git a/crates/client2/src/user.rs b/crates/client2/src/user.rs index 766861fbf8..b08d423cae 100644 --- a/crates/client2/src/user.rs +++ b/crates/client2/src/user.rs @@ -2,13 +2,12 @@ use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use feature_flags::FeatureFlagAppExt; -use futures::{channel::mpsc, AsyncReadExt, Future, StreamExt}; -use gpui::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, SharedString, Task}; +use futures::{channel::mpsc, Future, StreamExt}; +use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; use text::ReplicaId; -use util::http::HttpClient; use util::TryFutureExt as _; pub type UserId = u64; @@ -111,10 +110,7 @@ enum UpdateContacts { } impl UserStore { - pub fn new( - client: Arc, - cx: &mut ModelContext, - ) -> Self { + pub fn new(client: Arc, cx: &mut ModelContext) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscriptions = vec![ @@ -696,25 +692,3 @@ impl Collaborator { }) } } - -// todo!("we probably don't need this now that we fetch") -async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { - let mut response = http - .get(url, Default::default(), true) - .await - .map_err(|e| anyhow!("failed to send user avatar request: {}", e))?; - - if !response.status().is_success() { - return Err(anyhow!("avatar request failed {:?}", response.status())); - } - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?; - let format = image::guess_format(&body)?; - let image = image::load_from_memory_with_format(&body, format)?.into_bgra8(); - Ok(Arc::new(ImageData::new(image))) -} diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index bae1779d74..8967308dd3 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -19,6 +19,7 @@ mod contact_finder; use contact_finder::ContactFinder; use menu::{Cancel, Confirm, SelectNext, SelectPrev}; use rpc::proto::{self, PeerId}; +use smallvec::SmallVec; use theme::{ActiveTheme, ThemeSettings}; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; @@ -2543,7 +2544,7 @@ impl CollabPanel { } else { None }) - .collect::>(), + .collect::>(), }; Some(result) diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index 279c3cbd90..75c7cb3404 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -221,7 +221,7 @@ impl NotificationPanel { Some( h_stack() - .children(actor.map(|actor| Avatar::from(actor.avatar.clone()))) + .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) .child( v_stack().child(Label::new(text)).child( h_stack() @@ -675,7 +675,7 @@ impl Render for NotificationToast { h_stack() .id("notification_panel_toast") - .children(user.and_then(|user| Some(img(user.avatar.clone()?)))) + .children(user.map(|user| Avatar::new(user.avatar_uri.clone()))) .child(Label::new(self.text.clone())) .child( IconButton::new("close", Icon::Close) diff --git a/crates/collab_ui2/src/notifications/incoming_call_notification.rs b/crates/collab_ui2/src/notifications/incoming_call_notification.rs index b277e6fe94..53ff51d1cd 100644 --- a/crates/collab_ui2/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui2/src/notifications/incoming_call_notification.rs @@ -114,14 +114,7 @@ impl IncomingCallNotification { } fn render_caller(&self, cx: &mut ViewContext) -> impl Element { h_stack() - .children( - self.state - .call - .calling_user - .avatar - .as_ref() - .map(|avatar| Avatar::data(avatar.clone())), - ) + .child(Avatar::new(self.state.call.calling_user.avatar_uri.clone())) .child( v_stack() .child(Label::new(format!( diff --git a/crates/collab_ui2/src/notifications/project_shared_notification.rs b/crates/collab_ui2/src/notifications/project_shared_notification.rs index c7e667e284..e130f09b16 100644 --- a/crates/collab_ui2/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui2/src/notifications/project_shared_notification.rs @@ -119,12 +119,7 @@ impl ProjectSharedNotification { fn render_owner(&self) -> impl Element { h_stack() - .children( - self.owner - .avatar - .clone() - .map(|avatar| Avatar::data(avatar.clone())), - ) + .child(Avatar::new(self.owner.avatar_uri.clone())) .child( v_stack() .child(Label::new(self.owner.github_login.clone())) diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 4162ee2256..33c8d71625 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -use gpui::{img, Div, Hsla, ImageData, ImageSource, Img, IntoElement, Styled}; -use std::sync::Arc; +use gpui::{img, Div, Hsla, ImageSource, Img, IntoElement, Styled}; #[derive(Debug, Default, PartialEq, Clone)] pub enum Shape { From 52e72d9648eed1a38d31e0ba60167689f45f2706 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Dec 2023 13:07:17 -0500 Subject: [PATCH 4/8] Builds and runs --- Cargo.lock | 1 + crates/collab_ui2/src/collab_titlebar_item.rs | 5 +- crates/collab_ui2/src/face_pile.rs | 3 +- crates/collab_ui2/src/notification_panel.rs | 189 +++++++++--------- crates/zed2/Cargo.toml | 1 + crates/zed2/src/main.rs | 1 + crates/zed2/src/zed2.rs | 6 +- 7 files changed, 108 insertions(+), 98 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 849eaab25f..73123915d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12046,6 +12046,7 @@ dependencies = [ "lsp2", "menu2", "node_runtime", + "notifications2", "num_cpus", "outline2", "parking_lot 0.11.2", diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 8c2d112f09..2b931f7085 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -422,8 +422,8 @@ impl CollabTitlebarItem { current_user: &Arc, ) -> Option { let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); - let mut pile = FacePile::default(); - pile.child( + + let pile = FacePile::default().child( div() .child( Avatar::new(user.avatar_uri.clone()) @@ -450,6 +450,7 @@ impl CollabTitlebarItem { Some(div().child(Avatar::new(follower.avatar_uri.clone()))) })), ); + Some(pile) } diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index 9b2406024f..d181509c46 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/crates/collab_ui2/src/face_pile.rs @@ -1,6 +1,5 @@ use gpui::{ - div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, ParentElement, RenderOnce, - Styled, WindowContext, + div, AnyElement, Div, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext, }; use smallvec::SmallVec; diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index 75c7cb3404..8053337078 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -6,11 +6,10 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ - actions, div, img, px, serde_json, svg, AnyElement, AnyView, AppContext, AsyncAppContext, - AsyncWindowContext, Context, CursorStyle, Div, Element, Entity, EventEmitter, Flatten, - FocusHandle, FocusableView, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, - ListState, Model, MouseButton, ParentElement, Render, Stateful, StatefulInteractiveElement, - Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, div, px, serde_json, AnyElement, AppContext, AsyncWindowContext, DismissEvent, Div, + Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, + ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful, + StatefulInteractiveElement, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; @@ -19,7 +18,10 @@ 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 ui::{ + h_stack, v_stack, Avatar, Button, ButtonLike, Clickable, Disableable, Icon, IconButton, + IconElement, Label, +}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -98,7 +100,7 @@ impl NotificationPanel { }) .detach(); - let mut notification_list = + let notification_list = ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| { view.update(cx, |this, cx| { this.render_notification(ix, cx) @@ -220,58 +222,68 @@ impl NotificationPanel { } Some( - h_stack() - .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) + ButtonLike::new(ix) .child( - v_stack().child(Label::new(text)).child( - h_stack() - .child(Label::new(format_timestamp( - timestamp, - now, - self.local_timezone, - ))) - .children(if let Some(is_accepted) = response { - Some(div().child(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 - }), - ), + h_stack() + .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) + .child( + v_stack().child(Label::new(text)).child( + h_stack() + .child(Label::new(format_timestamp( + timestamp, + now, + self.local_timezone, + ))) + .children(if let Some(is_accepted) = response { + Some(div().child(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 + }), + ), + ), ) - .into_any(), + .disabled(!can_navigate) + .on_click({ + let notification = notification.clone(); + cx.listener(move |this, _, cx| this.did_click_notification(¬ification, cx)) + }) + .into_any_element(), ) } @@ -385,7 +397,7 @@ impl NotificationPanel { } = notification.clone() { if let Some(workspace) = self.workspace.upgrade() { - cx.app_context().defer(move |cx| { + cx.defer(move |_, cx| { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.focus_panel::(cx) { panel.update(cx, |panel, cx| { @@ -400,37 +412,34 @@ impl NotificationPanel { } } - fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool { + fn is_showing_notification(&self, notification: &Notification, cx: &ViewContext) -> 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::(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(); + return if let Some(panel) = workspace.read(cx).panel::(cx) { + let panel = panel.read(cx); + panel.is_scrolled_to_bottom() + && panel + .active_chat() + .map_or(false, |chat| chat.read(cx).channel_id == *channel_id) + } else { + false + }; } } false } - fn render_sign_in_prompt(&self, cx: &mut ViewContext) -> AnyElement { + fn render_sign_in_prompt(&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 { + move |_, cx| { + let client = client.clone(); + cx.spawn(move |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await; }) .detach() @@ -477,7 +486,7 @@ impl NotificationPanel { self.current_notification_toast = Some(( notification_id, cx.spawn(|this, mut cx| async move { - cx.background().timer(TOAST_DURATION).await; + cx.background_executor().timer(TOAST_DURATION).await; this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx)) .ok(); }), @@ -487,8 +496,8 @@ impl NotificationPanel { .update(cx, |workspace, cx| { workspace.dismiss_notification::(0, cx); workspace.show_notification(0, cx, |cx| { - let workspace = cx.weak_handle(); - cx.add_view(|_| NotificationToast { + let workspace = cx.view().downgrade(); + cx.build_view(|_| NotificationToast { notification_id, actor, text, @@ -527,9 +536,9 @@ impl NotificationPanel { impl Render for NotificationPanel { type Element = AnyElement; - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + fn render(&mut self, _: &mut ViewContext) -> AnyElement { if self.client.user_id().is_none() { - self.render_sign_in_prompt(cx) + self.render_sign_in_prompt() } else if self.notification_list.item_count() == 0 { self.render_empty_state() } else { @@ -569,7 +578,7 @@ impl Render for NotificationPanel { impl FocusableView for NotificationPanel { fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus_handle + self.focus_handle.clone() } } @@ -647,10 +656,10 @@ pub enum ToastEvent { } impl NotificationToast { - fn focus_notification_panel(&self, cx: &mut AppContext) { + fn focus_notification_panel(&self, cx: &mut ViewContext) { let workspace = self.workspace.clone(); let notification_id = self.notification_id; - cx.defer(move |cx| { + cx.defer(move |_, cx| { workspace .update(cx, |workspace, cx| { if let Some(panel) = workspace.focus_panel::(cx) { @@ -679,19 +688,17 @@ impl Render for NotificationToast { .child(Label::new(self.text.clone())) .child( IconButton::new("close", Icon::Close) - .on_click(|_, cx| cx.emit(ToastEvent::Dismiss)), + .on_click(cx.listener(|_, _, 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); - } - }) + .on_click(cx.listener(|this, _, cx| { + this.focus_notification_panel(cx); + cx.emit(ToastEvent::Dismiss); + })) } } impl EventEmitter for NotificationToast {} +impl EventEmitter for NotificationToast {} fn format_timestamp( mut timestamp: OffsetDateTime, diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index ed7ed180f5..4dad72c302 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -49,6 +49,7 @@ lsp = { package = "lsp2", path = "../lsp2" } menu = { package = "menu2", path = "../menu2" } # language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } +notifications = { package = "notifications2", path = "../notifications2" } assistant = { package = "assistant2", path = "../assistant2" } outline = { package = "outline2", path = "../outline2" } # plugin_runtime = { path = "../plugin_runtime",optional = true } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 92b20bf271..22be566bf9 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -220,6 +220,7 @@ fn main() { // activity_indicator::init(cx); // language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); + notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); feedback::init(cx); welcome::init(cx); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 1414bbaa13..65331a05a7 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -175,14 +175,14 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { assistant_panel, channels_panel, chat_panel, - // notification_panel, + notification_panel, ) = futures::try_join!( project_panel, terminal_panel, assistant_panel, channels_panel, chat_panel, - // notification_panel, + notification_panel, )?; workspace_handle.update(&mut cx, |workspace, cx| { @@ -192,7 +192,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace.add_panel(assistant_panel, cx); workspace.add_panel(channels_panel, cx); workspace.add_panel(chat_panel, cx); - // workspace.add_panel(notification_panel, cx); + workspace.add_panel(notification_panel, cx); // if !was_deserialized // && workspace From d3eff6371ed7156e1b9ebcd3e67775f7a022d811 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Dec 2023 16:14:43 -0500 Subject: [PATCH 5/8] Panel showing up --- crates/collab_ui2/src/collab_ui.rs | 1 + crates/collab_ui2/src/notification_panel.rs | 50 +++++++++------------ 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index b23971b3f2..df81af3e57 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -39,6 +39,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { collab_panel::init(cx); channel_view::init(cx); chat_panel::init(cx); + notification_panel::init(cx); notifications::init(&app_state, cx); // cx.add_global_action(toggle_screen_sharing); diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index 8053337078..df45228d1a 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -6,10 +6,10 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ - actions, div, px, serde_json, AnyElement, AppContext, AsyncWindowContext, DismissEvent, Div, - Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, + actions, div, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, DismissEvent, + Div, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful, - StatefulInteractiveElement, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + StatefulInteractiveElement, Task, View, ViewContext, VisualContext, WeakView, WindowContext, Styled, }; use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; @@ -73,7 +73,14 @@ pub struct NotificationPresenter { actions!(notification_panel, [ToggleFocus]); -pub fn init(_cx: &mut AppContext) {} +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }) + .detach(); +} impl NotificationPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { @@ -102,9 +109,10 @@ impl NotificationPanel { let notification_list = ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| { + dbg!(); view.update(cx, |this, cx| { - this.render_notification(ix, cx) - .unwrap_or_else(|| div().into_any()) + this.render_notification(ix, cx).unwrap() + // .unwrap_or_else(|| div().into_any()) }) }); notification_list.set_scroll_handler(cx.listener( @@ -221,6 +229,8 @@ impl NotificationPanel { self.did_render_notification(notification_id, ¬ification, cx); } + println!("rendering a notification"); + return Some(div().bg(gpui::red()).size_full().into_any()); Some( ButtonLike::new(ix) .child( @@ -466,6 +476,7 @@ impl NotificationPanel { old_range, new_count, } => { + dbg!(new_count); self.notification_list.splice(old_range.clone(), *new_count); cx.notify(); } @@ -538,40 +549,21 @@ impl Render for NotificationPanel { fn render(&mut self, _: &mut ViewContext) -> AnyElement { if self.client.user_id().is_none() { + dbg!(); self.render_sign_in_prompt() } else if self.notification_list.item_count() == 0 { + dbg!(); self.render_empty_state() } else { + dbg!(self.notification_list.item_count()); v_stack() .child( h_stack() .child(Label::new("Notifications")) .child(IconElement::new(Icon::Envelope)), ) - // todo!() - // .child( - // List::new() - // ) + .child(list(self.notification_list.clone()).full()) .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() } } } From 01d4e711d80bc9913e8f941a9270cd7a511ff402 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Dec 2023 16:31:26 -0500 Subject: [PATCH 6/8] Get notifications rendering in panel --- crates/collab_ui2/src/notification_panel.rs | 123 +++++++++++--------- 1 file changed, 65 insertions(+), 58 deletions(-) diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index df45228d1a..f03bc55c73 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -9,7 +9,8 @@ use gpui::{ actions, div, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful, - StatefulInteractiveElement, Task, View, ViewContext, VisualContext, WeakView, WindowContext, Styled, + StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; @@ -109,10 +110,9 @@ impl NotificationPanel { let notification_list = ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| { - dbg!(); view.update(cx, |this, cx| { - this.render_notification(ix, cx).unwrap() - // .unwrap_or_else(|| div().into_any()) + this.render_notification(ix, cx) + .unwrap_or_else(|| div().into_any()) }) }); notification_list.set_scroll_handler(cx.listener( @@ -229,71 +229,80 @@ impl NotificationPanel { self.did_render_notification(notification_id, ¬ification, cx); } - println!("rendering a notification"); - return Some(div().bg(gpui::red()).size_full().into_any()); Some( - ButtonLike::new(ix) + div() .child( - h_stack() - .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) + ButtonLike::new(ix) .child( - v_stack().child(Label::new(text)).child( - h_stack() - .child(Label::new(format_timestamp( - timestamp, - now, - self.local_timezone, - ))) - .children(if let Some(is_accepted) = response { - Some(div().child(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| { + h_stack() + .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) + .child( + v_stack().child(Label::new(text)).child( + h_stack() + .child(Label::new(format_timestamp( + timestamp, + now, + self.local_timezone, + ))) + .children(if let Some(is_accepted) = response { + Some(div().child(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| { + } + }), + ) + .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 - }), - ), - ), + } + }), + ), + ) + } else { + None + }), + ), + ), + ) + .disabled(!can_navigate) + .on_click({ + let notification = notification.clone(); + cx.listener(move |this, _, cx| { + this.did_click_notification(¬ification, cx) + }) + }), ) - .disabled(!can_navigate) - .on_click({ - let notification = notification.clone(); - cx.listener(move |this, _, cx| this.did_click_notification(¬ification, cx)) - }) - .into_any_element(), + .into_any(), ) } @@ -476,7 +485,6 @@ impl NotificationPanel { old_range, new_count, } => { - dbg!(new_count); self.notification_list.splice(old_range.clone(), *new_count); cx.notify(); } @@ -549,20 +557,19 @@ impl Render for NotificationPanel { fn render(&mut self, _: &mut ViewContext) -> AnyElement { if self.client.user_id().is_none() { - dbg!(); self.render_sign_in_prompt() } else if self.notification_list.item_count() == 0 { - dbg!(); self.render_empty_state() } else { - dbg!(self.notification_list.item_count()); v_stack() + .bg(gpui::red()) .child( h_stack() .child(Label::new("Notifications")) .child(IconElement::new(Icon::Envelope)), ) - .child(list(self.notification_list.clone()).full()) + .child(list(self.notification_list.clone()).size_full()) + .size_full() .into_any_element() } } From 51ceb52931b0b6464253ce2fc0e35e641f215499 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Dec 2023 17:22:49 -0500 Subject: [PATCH 7/8] Fix borked layout --- crates/collab_ui2/src/notification_panel.rs | 101 +++++++++----------- 1 file changed, 45 insertions(+), 56 deletions(-) diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index f03bc55c73..b347614830 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -231,77 +231,66 @@ impl NotificationPanel { Some( div() + .id(ix) .child( - ButtonLike::new(ix) + h_stack() + .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) .child( - h_stack() - .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) - .child( - v_stack().child(Label::new(text)).child( - h_stack() - .child(Label::new(format_timestamp( - timestamp, - now, - self.local_timezone, - ))) - .children(if let Some(is_accepted) = response { - Some(div().child(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| { + v_stack().child(Label::new(text)).child( + h_stack() + .child(Label::new(format_timestamp( + timestamp, + now, + self.local_timezone, + ))) + .children(if let Some(is_accepted) = response { + Some(div().child(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| { + } + }, + )) + .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 - }), - ), - ), - ) - .disabled(!can_navigate) - .on_click({ - let notification = notification.clone(); - cx.listener(move |this, _, cx| { - this.did_click_notification(¬ification, cx) - }) - }), + } + })), + ) + } else { + None + }), + ), + ), ) + .on_click({ + let notification = notification.clone(); + cx.listener(move |this, _, cx| this.did_click_notification(¬ification, cx)) + }) .into_any(), ) } From 29413bc72c6a5b1dbb68ebdd7ce05ff6ca4c9571 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 12 Dec 2023 17:47:12 -0500 Subject: [PATCH 8/8] Fix defer handle double lease Co-Authored-By: Conrad Irwin --- crates/collab_ui2/src/notification_panel.rs | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index b347614830..35288e810d 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -6,9 +6,9 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ - actions, div, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, DismissEvent, - Div, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, - ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful, + actions, div, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, CursorStyle, + DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -19,10 +19,7 @@ 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, ButtonLike, Clickable, Disableable, Icon, IconButton, - IconElement, Label, -}; +use ui::{h_stack, v_stack, Avatar, Button, Clickable, Icon, IconButton, IconElement, Label}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -287,9 +284,13 @@ impl NotificationPanel { ), ), ) - .on_click({ - let notification = notification.clone(); - cx.listener(move |this, _, cx| this.did_click_notification(¬ification, cx)) + .when(can_navigate, |el| { + el.cursor(CursorStyle::PointingHand).on_click({ + let notification = notification.clone(); + cx.listener(move |this, _, cx| { + this.did_click_notification(¬ification, cx) + }) + }) }) .into_any(), ) @@ -405,7 +406,7 @@ impl NotificationPanel { } = notification.clone() { if let Some(workspace) = self.workspace.upgrade() { - cx.defer(move |_, cx| { + cx.window_context().defer(move |cx| { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.focus_panel::(cx) { panel.update(cx, |panel, cx| { @@ -647,7 +648,7 @@ impl NotificationToast { fn focus_notification_panel(&self, cx: &mut ViewContext) { let workspace = self.workspace.clone(); let notification_id = self.notification_id; - cx.defer(move |_, cx| { + cx.window_context().defer(move |cx| { workspace .update(cx, |workspace, cx| { if let Some(panel) = workspace.focus_panel::(cx) {