From 9c68c3e8a9f5bb0c61dbf58d14f91556b9851800 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 May 2022 16:46:53 -0600 Subject: [PATCH 01/12] Put context parameter last in toggle_modal callback This is more consistent with our treatment of context params everywhere else. --- crates/command_palette/src/command_palette.rs | 2 +- crates/contacts_panel/src/contact_finder.rs | 2 +- crates/file_finder/src/file_finder.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/outline/src/outline.rs | 2 +- crates/project_symbols/src/project_symbols.rs | 2 +- crates/theme_selector/src/theme_selector.rs | 2 +- crates/workspace/src/workspace.rs | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index b6058c01e5..f724cc19a6 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -71,7 +71,7 @@ impl CommandPalette { cx.as_mut().defer(move |cx| { let this = cx.add_view(window_id, |cx| Self::new(focused_view_id, cx)); workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx, _| { + workspace.toggle_modal(cx, |_, cx| { cx.subscribe(&this, Self::on_event).detach(); this }); diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs index 5a480911d4..3b88eaf117 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/contacts_panel/src/contact_finder.rs @@ -159,7 +159,7 @@ impl PickerDelegate for ContactFinder { impl ContactFinder { fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |cx, workspace| { + workspace.toggle_modal(cx, |workspace, cx| { let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx)); cx.subscribe(&finder, Self::on_event).detach(); finder diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index a63ff7b0bd..e85147d7e2 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -85,7 +85,7 @@ impl FileFinder { } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |cx, workspace| { + workspace.toggle_modal(cx, |workspace, cx| { let project = workspace.project().clone(); let finder = cx.add_view(|cx| Self::new(project, cx)); cx.subscribe(&finder, Self::on_event).detach(); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 3f8aa933ba..9e2c79c5dc 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -62,7 +62,7 @@ impl GoToLine { .active_item(cx) .and_then(|active_item| active_item.downcast::()) { - workspace.toggle_modal(cx, |cx, _| { + workspace.toggle_modal(cx, |_, cx| { let view = cx.add_view(|cx| GoToLine::new(editor, cx)); cx.subscribe(&view, Self::on_event).detach(); view diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 5658cf2011..f5057ba39d 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -87,7 +87,7 @@ impl OutlineView { .read(cx) .outline(Some(cx.global::().theme.editor.syntax.as_ref())); if let Some(outline) = buffer { - workspace.toggle_modal(cx, |cx, _| { + workspace.toggle_modal(cx, |_, cx| { let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx)); cx.subscribe(&view, Self::on_event).detach(); view diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 157ea8ef73..5322a8924a 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -71,7 +71,7 @@ impl ProjectSymbolsView { } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |cx, workspace| { + workspace.toggle_modal(cx, |workspace, cx| { let project = workspace.project().clone(); let symbols = cx.add_view(|cx| Self::new(project, cx)); cx.subscribe(&symbols, Self::on_event).detach(); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 1904ed89d9..718268788c 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -66,7 +66,7 @@ impl ThemeSelector { fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { let themes = workspace.themes(); - workspace.toggle_modal(cx, |cx, _| { + workspace.toggle_modal(cx, |_, cx| { let this = cx.add_view(|cx| Self::new(themes, cx)); cx.subscribe(&this, Self::on_event).detach(); this diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d5b0bf2ed5..688accfcb9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -943,7 +943,7 @@ impl Workspace { ) -> Option> where V: 'static + View, - F: FnOnce(&mut ViewContext, &mut Self) -> ViewHandle, + F: FnOnce(&mut Self, &mut ViewContext) -> ViewHandle, { cx.notify(); // Whatever modal was visible is getting clobbered. If its the same type as V, then return @@ -953,7 +953,7 @@ impl Workspace { cx.focus_self(); Some(already_open_modal) } else { - let modal = add_view(cx, self); + let modal = add_view(self, cx); cx.focus(&modal); self.modal = Some(modal.into()); None From bd2ae304fa9084c4539dad7d222630370784ba34 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 May 2022 17:46:46 -0600 Subject: [PATCH 02/12] Start on workspace notifications --- assets/themes/cave-dark.json | 13 +++++++ assets/themes/cave-light.json | 13 +++++++ assets/themes/dark.json | 13 +++++++ assets/themes/light.json | 13 +++++++ assets/themes/solarized-dark.json | 13 +++++++ assets/themes/solarized-light.json | 13 +++++++ assets/themes/sulphurpool-dark.json | 13 +++++++ assets/themes/sulphurpool-light.json | 13 +++++++ crates/chat_panel/src/chat_panel.rs | 2 +- crates/theme/src/theme.rs | 9 +++++ crates/workspace/src/workspace.rs | 58 ++++++++++++++++++++++++++++ styles/src/styleTree/workspace.ts | 8 ++++ 12 files changed, 180 insertions(+), 1 deletion(-) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index cb1208a1db..3ced053621 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -470,6 +470,19 @@ "color": "#efecf4", "size": 14, "background": "#000000aa" + }, + "notification": { + "margin": { + "top": 10 + } + }, + "notifications": { + "width": 256, + "margin": { + "right": 10, + "bottom": 10 + }, + "background": "#ff0000" } }, "editor": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index f0b3f5bd43..a0a19149df 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -470,6 +470,19 @@ "color": "#19171c", "size": 14, "background": "#000000aa" + }, + "notification": { + "margin": { + "top": 10 + } + }, + "notifications": { + "width": 256, + "margin": { + "right": 10, + "bottom": 10 + }, + "background": "#ff0000" } }, "editor": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 9cc3badc81..cbff53933e 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -470,6 +470,19 @@ "color": "#ffffff", "size": 14, "background": "#000000aa" + }, + "notification": { + "margin": { + "top": 10 + } + }, + "notifications": { + "width": 256, + "margin": { + "right": 10, + "bottom": 10 + }, + "background": "#ff0000" } }, "editor": { diff --git a/assets/themes/light.json b/assets/themes/light.json index e2563fadad..c7331a083b 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -470,6 +470,19 @@ "color": "#000000", "size": 14, "background": "#000000aa" + }, + "notification": { + "margin": { + "top": 10 + } + }, + "notifications": { + "width": 256, + "margin": { + "right": 10, + "bottom": 10 + }, + "background": "#ff0000" } }, "editor": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 6e8c405b6c..d3367a3a3f 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -470,6 +470,19 @@ "color": "#fdf6e3", "size": 14, "background": "#000000aa" + }, + "notification": { + "margin": { + "top": 10 + } + }, + "notifications": { + "width": 256, + "margin": { + "right": 10, + "bottom": 10 + }, + "background": "#ff0000" } }, "editor": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 3f5b26ee56..d0c1f4e6e8 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -470,6 +470,19 @@ "color": "#002b36", "size": 14, "background": "#000000aa" + }, + "notification": { + "margin": { + "top": 10 + } + }, + "notifications": { + "width": 256, + "margin": { + "right": 10, + "bottom": 10 + }, + "background": "#ff0000" } }, "editor": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 0f2a868f24..05541f91f8 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -470,6 +470,19 @@ "color": "#f5f7ff", "size": 14, "background": "#000000aa" + }, + "notification": { + "margin": { + "top": 10 + } + }, + "notifications": { + "width": 256, + "margin": { + "right": 10, + "bottom": 10 + }, + "background": "#ff0000" } }, "editor": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index b9106c62f3..1a9408b17f 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -470,6 +470,19 @@ "color": "#202746", "size": 14, "background": "#000000aa" + }, + "notification": { + "margin": { + "top": 10 + } + }, + "notifications": { + "width": 256, + "margin": { + "right": 10, + "bottom": 10 + }, + "background": "#ff0000" } }, "editor": { diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index bb835c6640..460e01c527 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -69,7 +69,7 @@ impl ChatPanel { .with_style(move |cx| { let theme = &cx.global::().theme.chat_panel.channel_select; SelectStyle { - header: theme.header.container.clone(), + header: theme.header.container, menu: theme.menu.clone(), } }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 72db11c493..58cdc7fc54 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -45,6 +45,8 @@ pub struct Workspace { pub toolbar: Toolbar, pub disconnected_overlay: ContainedText, pub modal: ContainerStyle, + pub notification: ContainerStyle, + pub notifications: Notifications, } #[derive(Clone, Deserialize, Default)] @@ -109,6 +111,13 @@ pub struct Toolbar { pub item_spacing: f32, } +#[derive(Clone, Deserialize, Default)] +pub struct Notifications { + #[serde(flatten)] + pub container: ContainerStyle, + pub width: f32, +} + #[derive(Clone, Deserialize, Default)] pub struct Search { #[serde(flatten)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 688accfcb9..94b0a82f53 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -604,6 +604,24 @@ impl WeakItemHandle for WeakViewHandle { } } +pub trait Notification: View {} + +pub trait NotificationHandle { + fn to_any(&self) -> AnyViewHandle; +} + +impl NotificationHandle for ViewHandle { + fn to_any(&self) -> AnyViewHandle { + self.into() + } +} + +impl Into for &dyn NotificationHandle { + fn into(self) -> AnyViewHandle { + self.to_any() + } +} + #[derive(Clone)] pub struct WorkspaceParams { pub project: ModelHandle, @@ -683,6 +701,7 @@ pub struct Workspace { panes: Vec>, active_pane: ViewHandle, status_bar: ViewHandle, + notifications: Vec>, project: ModelHandle, leader_state: LeaderState, follower_states_by_leader: FollowerStatesByLeader, @@ -791,6 +810,7 @@ impl Workspace { panes: vec![pane.clone()], active_pane: pane.clone(), status_bar, + notifications: Default::default(), client: params.client.clone(), remote_entity_subscription: None, user_store: params.user_store.clone(), @@ -971,6 +991,15 @@ impl Workspace { } } + pub fn show_notification( + &mut self, + notification: ViewHandle, + cx: &mut ViewContext, + ) { + self.notifications.push(Box::new(notification)); + cx.notify(); + } + pub fn items<'a>( &'a self, cx: &'a AppContext, @@ -1703,6 +1732,34 @@ impl Workspace { } } + fn render_notifications( + &self, + theme: &theme::Workspace, + cx: &mut RenderContext, + ) -> Option { + if self.notifications.is_empty() { + None + } else { + Some( + Flex::column() + .with_children(self.notifications.iter().map(|notification| { + ChildView::new(notification.as_ref()) + .contained() + .with_style(theme.notification) + .boxed() + })) + .constrained() + .with_width(250.) + .contained() + .with_style(theme.notifications.container) + .aligned() + .bottom() + .right() + .boxed(), + ) + } + } + // RPC handlers async fn handle_follow( @@ -2037,6 +2094,7 @@ impl View for Workspace { .top() .boxed() })) + .with_children(self.render_notifications(&theme.workspace, cx)) .flex(1.0, true) .boxed(), ) diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index f74715ac0b..72547627fa 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -146,5 +146,13 @@ export default function workspace(theme: Theme) { ...text(theme, "sans", "active"), background: "#000000aa", }, + notification: { + margin: { top: 10 }, + }, + notifications: { + width: 256, + margin: { right: 10, bottom: 10 }, + background: "#ff0000", + } }; } From 3bca1c29e21a902b3b14045a6bbbb1ef34aebefb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 May 2022 18:33:39 -0600 Subject: [PATCH 03/12] Present a blank notification upon receipt of a contact request --- crates/client/src/user.rs | 30 ++++++++++------- crates/contacts_panel/src/contacts_panel.rs | 34 +++++++++++++++++-- crates/contacts_panel/src/notifications.rs | 36 +++++++++++++++++++++ crates/workspace/src/workspace.rs | 8 ++--- crates/zed/src/zed.rs | 3 +- 5 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 crates/contacts_panel/src/notifications.rs diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1874822774..3a2ea1a725 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -54,7 +54,9 @@ pub struct UserStore { _maintain_current_user: Task<()>, } -pub enum Event {} +pub enum Event { + NotifyIncomingRequest(Arc), +} impl Entity for UserStore { type Event = Event; @@ -182,12 +184,14 @@ impl UserStore { let mut incoming_requests = Vec::new(); for request in message.incoming_requests { - incoming_requests.push( - this.update(&mut cx, |this, cx| { - this.fetch_user(request.requester_id, cx) - }) - .await?, - ); + incoming_requests.push({ + let user = this + .update(&mut cx, |this, cx| { + this.fetch_user(request.requester_id, cx) + }) + .await?; + (user, request.should_notify) + }); } let mut outgoing_requests = Vec::new(); @@ -224,14 +228,18 @@ impl UserStore { this.incoming_contact_requests .retain(|user| !removed_incoming_requests.contains(&user.id)); // Update existing incoming requests and insert new ones - for request in incoming_requests { + for (user, should_notify) in incoming_requests { + if should_notify { + cx.emit(Event::NotifyIncomingRequest(user.clone())); + } + match this .incoming_contact_requests - .binary_search_by_key(&&request.github_login, |contact| { + .binary_search_by_key(&&user.github_login, |contact| { &contact.github_login }) { - Ok(ix) => this.incoming_contact_requests[ix] = request, - Err(ix) => this.incoming_contact_requests.insert(ix, request), + Ok(ix) => this.incoming_contact_requests[ix] = user, + Err(ix) => this.incoming_contact_requests.insert(ix, user), } } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 5d96a1b0c2..792aeb1e22 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,4 +1,5 @@ mod contact_finder; +mod notifications; use client::{Contact, User, UserStore}; use editor::{Cancel, Editor}; @@ -9,13 +10,14 @@ use gpui::{ impl_actions, platform::CursorStyle, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, - Subscription, View, ViewContext, ViewHandle, + Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; +use notifications::IncomingRequestNotification; use serde::Deserialize; use settings::Settings; use std::sync::Arc; use theme::IconButton; -use workspace::{AppState, JoinProject}; +use workspace::{AppState, JoinProject, Workspace}; impl_actions!( contacts_panel, @@ -60,7 +62,11 @@ pub fn init(cx: &mut MutableAppContext) { } impl ContactsPanel { - pub fn new(app_state: Arc, cx: &mut ViewContext) -> Self { + pub fn new( + app_state: Arc, + workspace: WeakViewHandle, + cx: &mut ViewContext, + ) -> Self { let user_query_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( Some(|theme| theme.contacts_panel.user_query_editor.clone()), @@ -77,6 +83,28 @@ impl ContactsPanel { }) .detach(); + cx.subscribe(&app_state.user_store, { + let user_store = app_state.user_store.clone(); + move |_, _, event, cx| match event { + client::Event::NotifyIncomingRequest(user) => { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_notification( + cx.add_view(|_| { + IncomingRequestNotification::new( + user.clone(), + user_store.clone(), + ) + }), + cx, + ) + }) + } + } + } + }) + .detach(); + let mut this = Self { list_state: ListState::new(0, Orientation::Top, 1000., { let this = cx.weak_handle(); diff --git a/crates/contacts_panel/src/notifications.rs b/crates/contacts_panel/src/notifications.rs new file mode 100644 index 0000000000..d2ef5176e3 --- /dev/null +++ b/crates/contacts_panel/src/notifications.rs @@ -0,0 +1,36 @@ +use client::{User, UserStore}; +use gpui::{color::Color, elements::*, Entity, ModelHandle, View}; +use std::sync::Arc; +use workspace::Notification; + +pub struct IncomingRequestNotification { + user: Arc, + user_store: ModelHandle, +} + +impl Entity for IncomingRequestNotification { + type Event = (); +} + +impl View for IncomingRequestNotification { + fn ui_name() -> &'static str { + "IncomingRequestNotification" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + Empty::new() + .constrained() + .with_height(200.) + .contained() + .with_background_color(Color::red()) + .boxed() + } +} + +impl Notification for IncomingRequestNotification {} + +impl IncomingRequestNotification { + pub fn new(user: Arc, user_store: ModelHandle) -> Self { + Self { user, user_store } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 94b0a82f53..f0e39126cc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1732,11 +1732,7 @@ impl Workspace { } } - fn render_notifications( - &self, - theme: &theme::Workspace, - cx: &mut RenderContext, - ) -> Option { + fn render_notifications(&self, theme: &theme::Workspace) -> Option { if self.notifications.is_empty() { None } else { @@ -2094,7 +2090,7 @@ impl View for Workspace { .top() .boxed() })) - .with_children(self.render_notifications(&theme.workspace, cx)) + .with_children(self.render_notifications(&theme.workspace)) .flex(1.0, true) .boxed(), ) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 77e400e02f..d4938501b8 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -172,7 +172,8 @@ pub fn build_workspace( }); let project_panel = ProjectPanel::new(project, cx); - let contact_panel = cx.add_view(|cx| ContactsPanel::new(app_state.clone(), cx)); + let contact_panel = + cx.add_view(|cx| ContactsPanel::new(app_state.clone(), workspace.weak_handle(), cx)); workspace.left_sidebar().update(cx, |sidebar, cx| { sidebar.add_item("icons/folder-tree-solid-14.svg", project_panel.into(), cx) From fe89de8b11465b94faa85371ee19d551cb98cbcb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 May 2022 18:50:18 -0600 Subject: [PATCH 04/12] Dismiss contact request notification if request is cancelled --- crates/client/src/user.rs | 15 +++-- .../src/contact_notifications.rs | 58 +++++++++++++++++++ crates/contacts_panel/src/contacts_panel.rs | 10 ++-- crates/contacts_panel/src/notifications.rs | 36 ------------ crates/workspace/src/workspace.rs | 26 ++++++++- 5 files changed, 100 insertions(+), 45 deletions(-) create mode 100644 crates/contacts_panel/src/contact_notifications.rs delete mode 100644 crates/contacts_panel/src/notifications.rs diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 3a2ea1a725..a0f0884294 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -55,7 +55,8 @@ pub struct UserStore { } pub enum Event { - NotifyIncomingRequest(Arc), + ContactRequested(Arc), + ContactRequestCancelled(Arc), } impl Entity for UserStore { @@ -225,12 +226,18 @@ impl UserStore { } // Remove incoming contact requests - this.incoming_contact_requests - .retain(|user| !removed_incoming_requests.contains(&user.id)); + this.incoming_contact_requests.retain(|user| { + if removed_incoming_requests.contains(&user.id) { + cx.emit(Event::ContactRequestCancelled(user.clone())); + false + } else { + true + } + }); // Update existing incoming requests and insert new ones for (user, should_notify) in incoming_requests { if should_notify { - cx.emit(Event::NotifyIncomingRequest(user.clone())); + cx.emit(Event::ContactRequested(user.clone())); } match this diff --git a/crates/contacts_panel/src/contact_notifications.rs b/crates/contacts_panel/src/contact_notifications.rs new file mode 100644 index 0000000000..894c1f4138 --- /dev/null +++ b/crates/contacts_panel/src/contact_notifications.rs @@ -0,0 +1,58 @@ +use client::{User, UserStore}; +use gpui::{color::Color, elements::*, Entity, ModelHandle, View, ViewContext}; +use std::sync::Arc; +use workspace::Notification; + +pub struct IncomingRequestNotification { + user: Arc, + user_store: ModelHandle, +} + +pub enum Event { + Dismiss, +} + +impl Entity for IncomingRequestNotification { + type Event = Event; +} + +impl View for IncomingRequestNotification { + fn ui_name() -> &'static str { + "IncomingRequestNotification" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + Empty::new() + .constrained() + .with_height(200.) + .contained() + .with_background_color(Color::red()) + .boxed() + } +} + +impl Notification for IncomingRequestNotification { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + matches!(event, Event::Dismiss) + } +} + +impl IncomingRequestNotification { + pub fn new( + user: Arc, + user_store: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + let user_id = user.id; + cx.subscribe(&user_store, move |_, _, event, cx| { + if let client::Event::ContactRequestCancelled(user) = event { + if user.id == user_id { + cx.emit(Event::Dismiss); + } + } + }) + .detach(); + + Self { user, user_store } + } +} diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 792aeb1e22..68fb8e1f26 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,7 +1,8 @@ mod contact_finder; -mod notifications; +mod contact_notifications; use client::{Contact, User, UserStore}; +use contact_notifications::IncomingRequestNotification; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -12,7 +13,6 @@ use gpui::{ Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; -use notifications::IncomingRequestNotification; use serde::Deserialize; use settings::Settings; use std::sync::Arc; @@ -86,14 +86,15 @@ impl ContactsPanel { cx.subscribe(&app_state.user_store, { let user_store = app_state.user_store.clone(); move |_, _, event, cx| match event { - client::Event::NotifyIncomingRequest(user) => { + client::Event::ContactRequested(user) => { if let Some(workspace) = workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { workspace.show_notification( - cx.add_view(|_| { + cx.add_view(|cx| { IncomingRequestNotification::new( user.clone(), user_store.clone(), + cx, ) }), cx, @@ -101,6 +102,7 @@ impl ContactsPanel { }) } } + _ => {} } }) .detach(); diff --git a/crates/contacts_panel/src/notifications.rs b/crates/contacts_panel/src/notifications.rs deleted file mode 100644 index d2ef5176e3..0000000000 --- a/crates/contacts_panel/src/notifications.rs +++ /dev/null @@ -1,36 +0,0 @@ -use client::{User, UserStore}; -use gpui::{color::Color, elements::*, Entity, ModelHandle, View}; -use std::sync::Arc; -use workspace::Notification; - -pub struct IncomingRequestNotification { - user: Arc, - user_store: ModelHandle, -} - -impl Entity for IncomingRequestNotification { - type Event = (); -} - -impl View for IncomingRequestNotification { - fn ui_name() -> &'static str { - "IncomingRequestNotification" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - Empty::new() - .constrained() - .with_height(200.) - .contained() - .with_background_color(Color::red()) - .boxed() - } -} - -impl Notification for IncomingRequestNotification {} - -impl IncomingRequestNotification { - pub fn new(user: Arc, user_store: ModelHandle) -> Self { - Self { user, user_store } - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f0e39126cc..b077b82518 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -604,13 +604,20 @@ impl WeakItemHandle for WeakViewHandle { } } -pub trait Notification: View {} +pub trait Notification: View { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool; +} pub trait NotificationHandle { + fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; } impl NotificationHandle for ViewHandle { + fn id(&self) -> usize { + self.id() + } + fn to_any(&self) -> AnyViewHandle { self.into() } @@ -996,10 +1003,27 @@ impl Workspace { notification: ViewHandle, cx: &mut ViewContext, ) { + cx.subscribe(¬ification, |this, handle, event, cx| { + if handle.read(cx).should_dismiss_notification_on_event(event) { + this.dismiss_notification(handle.id(), cx); + } + }) + .detach(); self.notifications.push(Box::new(notification)); cx.notify(); } + fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext) { + self.notifications.retain(|handle| { + if handle.id() == id { + cx.notify(); + false + } else { + true + } + }); + } + pub fn items<'a>( &'a self, cx: &'a AppContext, From 97d3616ed91a7b7f17cbc7c5dcf2e91b2eb9efa2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 11 May 2022 15:13:37 +0200 Subject: [PATCH 05/12] Show incoming request notification and implement dismissal --- assets/themes/cave-dark.json | 61 +++++++++- assets/themes/cave-light.json | 61 +++++++++- assets/themes/dark.json | 61 +++++++++- assets/themes/light.json | 61 +++++++++- assets/themes/solarized-dark.json | 61 +++++++++- assets/themes/solarized-light.json | 61 +++++++++- assets/themes/sulphurpool-dark.json | 61 +++++++++- assets/themes/sulphurpool-light.json | 61 +++++++++- crates/client/src/user.rs | 18 +++ crates/collab/src/rpc.rs | 61 +++++----- .../src/contact_notifications.rs | 113 +++++++++++++++++- crates/contacts_panel/src/contacts_panel.rs | 1 + crates/rpc/proto/zed.proto | 1 + crates/theme/src/theme.rs | 11 ++ styles/src/styleTree/app.ts | 4 +- .../styleTree/incomingRequestNotification.ts | 35 ++++++ styles/src/styleTree/workspace.ts | 8 +- 17 files changed, 688 insertions(+), 52 deletions(-) create mode 100644 styles/src/styleTree/incomingRequestNotification.ts diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 3ced053621..e8d03c60e0 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -474,6 +474,21 @@ "notification": { "margin": { "top": 10 + }, + "background": "#26232a", + "corner_radius": 6, + "padding": 12, + "border": { + "color": "#19171c", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] } }, "notifications": { @@ -481,8 +496,7 @@ "margin": { "right": 10, "bottom": 10 - }, - "background": "#ff0000" + } } }, "editor": { @@ -1659,5 +1673,48 @@ "padding": { "left": 6 } + }, + "incoming_request_notification": { + "header_avatar": { + "height": 12, + "width": 12, + "corner_radius": 6 + }, + "header_message": { + "family": "Zed Sans", + "color": "#e2dfe7", + "size": 12, + "margin": { + "left": 4 + } + }, + "header_height": 18, + "body_message": { + "family": "Zed Sans", + "color": "#8b8792", + "size": 12, + "margin": { + "top": 6, + "bottom": 6 + } + }, + "button": { + "family": "Zed Sans", + "color": "#e2dfe7", + "size": 12, + "background": "#19171c", + "padding": 4, + "corner_radius": 6, + "margin": { + "left": 6 + } + }, + "dismiss_button": { + "color": "#8b8792", + "icon_width": 8, + "icon_height": 8, + "button_width": 8, + "button_height": 8 + } } } \ No newline at end of file diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index a0a19149df..de7de76670 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -474,6 +474,21 @@ "notification": { "margin": { "top": 10 + }, + "background": "#e2dfe7", + "corner_radius": 6, + "padding": 12, + "border": { + "color": "#efecf4", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] } }, "notifications": { @@ -481,8 +496,7 @@ "margin": { "right": 10, "bottom": 10 - }, - "background": "#ff0000" + } } }, "editor": { @@ -1659,5 +1673,48 @@ "padding": { "left": 6 } + }, + "incoming_request_notification": { + "header_avatar": { + "height": 12, + "width": 12, + "corner_radius": 6 + }, + "header_message": { + "family": "Zed Sans", + "color": "#26232a", + "size": 12, + "margin": { + "left": 4 + } + }, + "header_height": 18, + "body_message": { + "family": "Zed Sans", + "color": "#585260", + "size": 12, + "margin": { + "top": 6, + "bottom": 6 + } + }, + "button": { + "family": "Zed Sans", + "color": "#26232a", + "size": 12, + "background": "#efecf4", + "padding": 4, + "corner_radius": 6, + "margin": { + "left": 6 + } + }, + "dismiss_button": { + "color": "#585260", + "icon_width": 8, + "icon_height": 8, + "button_width": 8, + "button_height": 8 + } } } \ No newline at end of file diff --git a/assets/themes/dark.json b/assets/themes/dark.json index cbff53933e..78fa14aa5d 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -474,6 +474,21 @@ "notification": { "margin": { "top": 10 + }, + "background": "#1c1c1c", + "corner_radius": 6, + "padding": 12, + "border": { + "color": "#070707", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#00000052", + "offset": [ + 0, + 2 + ] } }, "notifications": { @@ -481,8 +496,7 @@ "margin": { "right": 10, "bottom": 10 - }, - "background": "#ff0000" + } } }, "editor": { @@ -1659,5 +1673,48 @@ "padding": { "left": 6 } + }, + "incoming_request_notification": { + "header_avatar": { + "height": 12, + "width": 12, + "corner_radius": 6 + }, + "header_message": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 12, + "margin": { + "left": 4 + } + }, + "header_height": 18, + "body_message": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 12, + "margin": { + "top": 6, + "bottom": 6 + } + }, + "button": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 12, + "background": "#0e0e0e80", + "padding": 4, + "corner_radius": 6, + "margin": { + "left": 6 + } + }, + "dismiss_button": { + "color": "#9c9c9c", + "icon_width": 8, + "icon_height": 8, + "button_width": 8, + "button_height": 8 + } } } \ No newline at end of file diff --git a/assets/themes/light.json b/assets/themes/light.json index c7331a083b..61923bfec5 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -474,6 +474,21 @@ "notification": { "margin": { "top": 10 + }, + "background": "#f8f8f8", + "corner_radius": 6, + "padding": 12, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] } }, "notifications": { @@ -481,8 +496,7 @@ "margin": { "right": 10, "bottom": 10 - }, - "background": "#ff0000" + } } }, "editor": { @@ -1659,5 +1673,48 @@ "padding": { "left": 6 } + }, + "incoming_request_notification": { + "header_avatar": { + "height": 12, + "width": 12, + "corner_radius": 6 + }, + "header_message": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 12, + "margin": { + "left": 4 + } + }, + "header_height": 18, + "body_message": { + "family": "Zed Sans", + "color": "#474747", + "size": 12, + "margin": { + "top": 6, + "bottom": 6 + } + }, + "button": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 12, + "background": "#f1f1f1", + "padding": 4, + "corner_radius": 6, + "margin": { + "left": 6 + } + }, + "dismiss_button": { + "color": "#717171", + "icon_width": 8, + "icon_height": 8, + "button_width": 8, + "button_height": 8 + } } } \ No newline at end of file diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index d3367a3a3f..9865c12586 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -474,6 +474,21 @@ "notification": { "margin": { "top": 10 + }, + "background": "#073642", + "corner_radius": 6, + "padding": 12, + "border": { + "color": "#002b36", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] } }, "notifications": { @@ -481,8 +496,7 @@ "margin": { "right": 10, "bottom": 10 - }, - "background": "#ff0000" + } } }, "editor": { @@ -1659,5 +1673,48 @@ "padding": { "left": 6 } + }, + "incoming_request_notification": { + "header_avatar": { + "height": 12, + "width": 12, + "corner_radius": 6 + }, + "header_message": { + "family": "Zed Sans", + "color": "#eee8d5", + "size": 12, + "margin": { + "left": 4 + } + }, + "header_height": 18, + "body_message": { + "family": "Zed Sans", + "color": "#93a1a1", + "size": 12, + "margin": { + "top": 6, + "bottom": 6 + } + }, + "button": { + "family": "Zed Sans", + "color": "#eee8d5", + "size": 12, + "background": "#002b36", + "padding": 4, + "corner_radius": 6, + "margin": { + "left": 6 + } + }, + "dismiss_button": { + "color": "#93a1a1", + "icon_width": 8, + "icon_height": 8, + "button_width": 8, + "button_height": 8 + } } } \ No newline at end of file diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index d0c1f4e6e8..c61d354d35 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -474,6 +474,21 @@ "notification": { "margin": { "top": 10 + }, + "background": "#eee8d5", + "corner_radius": 6, + "padding": 12, + "border": { + "color": "#fdf6e3", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] } }, "notifications": { @@ -481,8 +496,7 @@ "margin": { "right": 10, "bottom": 10 - }, - "background": "#ff0000" + } } }, "editor": { @@ -1659,5 +1673,48 @@ "padding": { "left": 6 } + }, + "incoming_request_notification": { + "header_avatar": { + "height": 12, + "width": 12, + "corner_radius": 6 + }, + "header_message": { + "family": "Zed Sans", + "color": "#073642", + "size": 12, + "margin": { + "left": 4 + } + }, + "header_height": 18, + "body_message": { + "family": "Zed Sans", + "color": "#586e75", + "size": 12, + "margin": { + "top": 6, + "bottom": 6 + } + }, + "button": { + "family": "Zed Sans", + "color": "#073642", + "size": 12, + "background": "#fdf6e3", + "padding": 4, + "corner_radius": 6, + "margin": { + "left": 6 + } + }, + "dismiss_button": { + "color": "#586e75", + "icon_width": 8, + "icon_height": 8, + "button_width": 8, + "button_height": 8 + } } } \ No newline at end of file diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 05541f91f8..907ec58cc0 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -474,6 +474,21 @@ "notification": { "margin": { "top": 10 + }, + "background": "#293256", + "corner_radius": 6, + "padding": 12, + "border": { + "color": "#202746", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#0000003d", + "offset": [ + 0, + 2 + ] } }, "notifications": { @@ -481,8 +496,7 @@ "margin": { "right": 10, "bottom": 10 - }, - "background": "#ff0000" + } } }, "editor": { @@ -1659,5 +1673,48 @@ "padding": { "left": 6 } + }, + "incoming_request_notification": { + "header_avatar": { + "height": 12, + "width": 12, + "corner_radius": 6 + }, + "header_message": { + "family": "Zed Sans", + "color": "#dfe2f1", + "size": 12, + "margin": { + "left": 4 + } + }, + "header_height": 18, + "body_message": { + "family": "Zed Sans", + "color": "#979db4", + "size": 12, + "margin": { + "top": 6, + "bottom": 6 + } + }, + "button": { + "family": "Zed Sans", + "color": "#dfe2f1", + "size": 12, + "background": "#202746", + "padding": 4, + "corner_radius": 6, + "margin": { + "left": 6 + } + }, + "dismiss_button": { + "color": "#979db4", + "icon_width": 8, + "icon_height": 8, + "button_width": 8, + "button_height": 8 + } } } \ No newline at end of file diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 1a9408b17f..3ae43250f0 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -474,6 +474,21 @@ "notification": { "margin": { "top": 10 + }, + "background": "#dfe2f1", + "corner_radius": 6, + "padding": 12, + "border": { + "color": "#f5f7ff", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] } }, "notifications": { @@ -481,8 +496,7 @@ "margin": { "right": 10, "bottom": 10 - }, - "background": "#ff0000" + } } }, "editor": { @@ -1659,5 +1673,48 @@ "padding": { "left": 6 } + }, + "incoming_request_notification": { + "header_avatar": { + "height": 12, + "width": 12, + "corner_radius": 6 + }, + "header_message": { + "family": "Zed Sans", + "color": "#293256", + "size": 12, + "margin": { + "left": 4 + } + }, + "header_height": 18, + "body_message": { + "family": "Zed Sans", + "color": "#5e6687", + "size": 12, + "margin": { + "top": 6, + "bottom": 6 + } + }, + "button": { + "family": "Zed Sans", + "color": "#293256", + "size": 12, + "background": "#f5f7ff", + "padding": 4, + "corner_radius": 6, + "margin": { + "left": 6 + } + }, + "dismiss_button": { + "color": "#5e6687", + "icon_width": 8, + "icon_height": 8, + "button_width": 8, + "button_height": 8 + } } } \ No newline at end of file diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a0f0884294..4d5f44c320 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -356,6 +356,24 @@ impl UserStore { ) } + pub fn dismiss_contact_request( + &mut self, + requester_id: u64, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.upgrade(); + cx.spawn_weak(|_, _| async move { + client + .ok_or_else(|| anyhow!("can't upgrade client reference"))? + .request(proto::RespondToContactRequest { + requester_id, + response: proto::ContactRequestResponse::Dismiss as i32, + }) + .await?; + Ok(()) + }) + } + fn perform_contact_request( &mut self, user_id: u64, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 33d1d52677..0a34e75b3a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1023,35 +1023,42 @@ impl Server { .await .user_id_for_connection(request.sender_id)?; let requester_id = UserId::from_proto(request.payload.requester_id); - let accept = request.payload.response == proto::ContactRequestResponse::Accept as i32; - self.app_state - .db - .respond_to_contact_request(responder_id, requester_id, accept) - .await?; + if request.payload.response == proto::ContactRequestResponse::Dismiss as i32 { + self.app_state + .db + .dismiss_contact_request(responder_id, requester_id) + .await?; + } else { + let accept = request.payload.response == proto::ContactRequestResponse::Accept as i32; + self.app_state + .db + .respond_to_contact_request(responder_id, requester_id, accept) + .await?; - let store = self.store().await; - // Update responder with new contact - let mut update = proto::UpdateContacts::default(); - if accept { - update.contacts.push(store.contact_for_user(requester_id)); - } - update - .remove_incoming_requests - .push(requester_id.to_proto()); - for connection_id in store.connection_ids_for_user(responder_id) { - self.peer.send(connection_id, update.clone())?; - } + let store = self.store().await; + // Update responder with new contact + let mut update = proto::UpdateContacts::default(); + if accept { + update.contacts.push(store.contact_for_user(requester_id)); + } + update + .remove_incoming_requests + .push(requester_id.to_proto()); + for connection_id in store.connection_ids_for_user(responder_id) { + self.peer.send(connection_id, update.clone())?; + } - // Update requester with new contact - let mut update = proto::UpdateContacts::default(); - if accept { - update.contacts.push(store.contact_for_user(responder_id)); - } - update - .remove_outgoing_requests - .push(responder_id.to_proto()); - for connection_id in store.connection_ids_for_user(requester_id) { - self.peer.send(connection_id, update.clone())?; + // Update requester with new contact + let mut update = proto::UpdateContacts::default(); + if accept { + update.contacts.push(store.contact_for_user(responder_id)); + } + update + .remove_outgoing_requests + .push(responder_id.to_proto()); + for connection_id in store.connection_ids_for_user(requester_id) { + self.peer.send(connection_id, update.clone())?; + } } response.send(proto::Ack {})?; diff --git a/crates/contacts_panel/src/contact_notifications.rs b/crates/contacts_panel/src/contact_notifications.rs index 894c1f4138..3b31528b71 100644 --- a/crates/contacts_panel/src/contact_notifications.rs +++ b/crates/contacts_panel/src/contact_notifications.rs @@ -1,13 +1,26 @@ use client::{User, UserStore}; -use gpui::{color::Color, elements::*, Entity, ModelHandle, View, ViewContext}; +use gpui::{ + elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle, + MutableAppContext, RenderContext, View, ViewContext, +}; +use settings::Settings; use std::sync::Arc; use workspace::Notification; +impl_internal_actions!(contact_notifications, [Dismiss]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(IncomingRequestNotification::dismiss); +} + pub struct IncomingRequestNotification { user: Arc, user_store: ModelHandle, } +#[derive(Clone)] +struct Dismiss(u64); + pub enum Event { Dismiss, } @@ -21,12 +34,91 @@ impl View for IncomingRequestNotification { "IncomingRequestNotification" } - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { - Empty::new() - .constrained() - .with_height(200.) + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + enum Dismiss {} + enum Reject {} + enum Accept {} + + let theme = cx.global::().theme.clone(); + let theme = &theme.incoming_request_notification; + let user_id = self.user.id; + + Flex::column() + .with_child( + Flex::row() + .with_children(self.user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.header_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + format!("{} added you", self.user.github_login), + theme.header_message.text.clone(), + ) + .contained() + .with_style(theme.header_message.container) + .aligned() + .boxed(), + ) + .with_child( + MouseEventHandler::new::( + self.user.id as usize, + cx, + |_, _| { + Svg::new("icons/reject.svg") + .with_color(theme.dismiss_button.color) + .constrained() + .with_width(theme.dismiss_button.icon_width) + .aligned() + .contained() + .with_style(theme.dismiss_button.container) + .constrained() + .with_width(theme.dismiss_button.button_width) + .with_height(theme.dismiss_button.button_width) + .aligned() + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) + .flex_float() + .boxed(), + ) + .constrained() + .with_height(theme.header_height) + .boxed(), + ) + .with_child( + Label::new( + "They won't know if you decline.".to_string(), + theme.body_message.text.clone(), + ) + .contained() + .with_style(theme.body_message.container) + .boxed(), + ) + .with_child( + Flex::row() + .with_child( + Label::new("Decline".to_string(), theme.button.text.clone()) + .contained() + .with_style(theme.button.container) + .boxed(), + ) + .with_child( + Label::new("Accept".to_string(), theme.button.text.clone()) + .contained() + .with_style(theme.button.container) + .boxed(), + ) + .aligned() + .right() + .boxed(), + ) .contained() - .with_background_color(Color::red()) .boxed() } } @@ -55,4 +147,13 @@ impl IncomingRequestNotification { Self { user, user_store } } + + fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { + self.user_store.update(cx, |store, cx| { + store + .dismiss_contact_request(self.user.id, cx) + .detach_and_log_err(cx); + }); + cx.emit(Event::Dismiss); + } } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 68fb8e1f26..333de8c3d5 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -55,6 +55,7 @@ pub struct RespondToContactRequest { pub fn init(cx: &mut MutableAppContext) { contact_finder::init(cx); + contact_notifications::init(cx); cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8adba5fc80..c92b8c5c00 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -566,6 +566,7 @@ enum ContactRequestResponse { Accept = 0; Reject = 1; Block = 2; + Dismiss = 3; } message SendChannelMessage { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 58cdc7fc54..aeb656828e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -29,6 +29,7 @@ pub struct Theme { pub search: Search, pub project_diagnostics: ProjectDiagnostics, pub breadcrumbs: ContainedText, + pub incoming_request_notification: IncomingRequestNotification, } #[derive(Deserialize, Default)] @@ -354,6 +355,16 @@ pub struct ProjectDiagnostics { pub tab_summary_spacing: f32, } +#[derive(Deserialize, Default)] +pub struct IncomingRequestNotification { + pub header_avatar: ImageStyle, + pub header_message: ContainedText, + pub header_height: f32, + pub body_message: ContainedText, + pub button: ContainedText, + pub dismiss_button: IconButton, +} + #[derive(Clone, Deserialize, Default)] pub struct Editor { pub text_color: Color, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 0da6ada222..b4b9ffe383 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -10,6 +10,7 @@ import search from "./search"; import picker from "./picker"; import workspace from "./workspace"; import projectDiagnostics from "./projectDiagnostics"; +import incomingRequestNotification from "./incomingRequestNotification"; export const panel = { padding: { top: 12, left: 12, bottom: 12, right: 12 }, @@ -32,6 +33,7 @@ export default function app(theme: Theme): Object { padding: { left: 6, }, - } + }, + incomingRequestNotification: incomingRequestNotification(theme), }; } diff --git a/styles/src/styleTree/incomingRequestNotification.ts b/styles/src/styleTree/incomingRequestNotification.ts new file mode 100644 index 0000000000..17cfad80d6 --- /dev/null +++ b/styles/src/styleTree/incomingRequestNotification.ts @@ -0,0 +1,35 @@ +import Theme from "../themes/theme"; +import { backgroundColor, iconColor, text } from "./components"; + +export default function incomingRequestNotification(theme: Theme): Object { + return { + headerAvatar: { + height: 12, + width: 12, + cornerRadius: 6, + }, + headerMessage: { + ...text(theme, "sans", "primary", { size: "xs" }), + margin: { left: 4 } + }, + headerHeight: 18, + bodyMessage: { + ...text(theme, "sans", "secondary", { size: "xs" }), + margin: { top: 6, bottom: 6 }, + }, + button: { + ...text(theme, "sans", "primary", { size: "xs" }), + background: backgroundColor(theme, "on300"), + padding: 4, + cornerRadius: 6, + margin: { left: 6 }, + }, + dismissButton: { + color: iconColor(theme, "secondary"), + iconWidth: 8, + iconHeight: 8, + buttonWidth: 8, + buttonHeight: 8, + } + } +} \ No newline at end of file diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 72547627fa..1d4b78944f 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -1,5 +1,5 @@ import Theme from "../themes/theme"; -import { backgroundColor, border, iconColor, text } from "./components"; +import { backgroundColor, border, iconColor, shadow, text } from "./components"; import statusBar from "./statusBar"; export default function workspace(theme: Theme) { @@ -148,11 +148,15 @@ export default function workspace(theme: Theme) { }, notification: { margin: { top: 10 }, + background: backgroundColor(theme, 300), + cornerRadius: 6, + padding: 12, + border: border(theme, "primary"), + shadow: shadow(theme), }, notifications: { width: 256, margin: { right: 10, bottom: 10 }, - background: "#ff0000", } }; } From c71b26478624a3e187fef8687148da17a7c759ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 11 May 2022 15:25:33 +0200 Subject: [PATCH 06/12] Allow accepting/rejecting incoming requests via notification --- .../src/contact_notifications.rs | 65 ++++++++++++++++--- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/crates/contacts_panel/src/contact_notifications.rs b/crates/contacts_panel/src/contact_notifications.rs index 3b31528b71..e5fff481b0 100644 --- a/crates/contacts_panel/src/contact_notifications.rs +++ b/crates/contacts_panel/src/contact_notifications.rs @@ -7,10 +7,11 @@ use settings::Settings; use std::sync::Arc; use workspace::Notification; -impl_internal_actions!(contact_notifications, [Dismiss]); +impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(IncomingRequestNotification::dismiss); + cx.add_action(IncomingRequestNotification::respond_to_contact_request); } pub struct IncomingRequestNotification { @@ -21,6 +22,12 @@ pub struct IncomingRequestNotification { #[derive(Clone)] struct Dismiss(u64); +#[derive(Clone)] +pub struct RespondToContactRequest { + pub user_id: u64, + pub accept: bool, +} + pub enum Event { Dismiss, } @@ -103,16 +110,44 @@ impl View for IncomingRequestNotification { .with_child( Flex::row() .with_child( - Label::new("Decline".to_string(), theme.button.text.clone()) - .contained() - .with_style(theme.button.container) - .boxed(), + MouseEventHandler::new::( + self.user.id as usize, + cx, + |_, _| { + Label::new("Reject".to_string(), theme.button.text.clone()) + .contained() + .with_style(theme.button.container) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }); + }) + .boxed(), ) .with_child( - Label::new("Accept".to_string(), theme.button.text.clone()) - .contained() - .with_style(theme.button.container) - .boxed(), + MouseEventHandler::new::( + self.user.id as usize, + cx, + |_, _| { + Label::new("Accept".to_string(), theme.button.text.clone()) + .contained() + .with_style(theme.button.container) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }); + }) + .boxed(), ) .aligned() .right() @@ -156,4 +191,16 @@ impl IncomingRequestNotification { }); cx.emit(Event::Dismiss); } + + fn respond_to_contact_request( + &mut self, + action: &RespondToContactRequest, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(action.user_id, action.accept, cx) + }) + .detach(); + } } From 933a1f2cd6d2a72ff0743f8ae3f2d1381a5c52d9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 11 May 2022 17:39:03 +0200 Subject: [PATCH 07/12] Show badge when there are pending contact requests Co-Authored-By: Nathan Sobo --- assets/themes/cave-dark.json | 13 +++ assets/themes/cave-light.json | 13 +++ assets/themes/dark.json | 13 +++ assets/themes/light.json | 13 +++ assets/themes/solarized-dark.json | 13 +++ assets/themes/solarized-light.json | 13 +++ assets/themes/sulphurpool-dark.json | 13 +++ assets/themes/sulphurpool-light.json | 13 +++ crates/collab/src/rpc.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 16 ++- crates/gpui/src/elements/empty.rs | 15 ++- crates/project_panel/src/project_panel.rs | 6 + crates/theme/src/theme.rs | 1 + crates/workspace/src/sidebar.rs | 120 +++++++++++++++----- crates/workspace/src/workspace.rs | 4 +- styles/src/styleTree/statusBar.ts | 9 +- styles/src/styleTree/workspace.ts | 6 +- 17 files changed, 241 insertions(+), 42 deletions(-) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index e8d03c60e0..acb5315dda 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -341,6 +341,19 @@ "icon_color": "#efecf4", "background": "#5852605c" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#26232a" + }, + "background": "#576ddb" } } }, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index de7de76670..5d75efa22a 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -341,6 +341,19 @@ "icon_color": "#19171c", "background": "#8b87922e" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#e2dfe7" + }, + "background": "#576ddb" } } }, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 78fa14aa5d..393b5b20d8 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -341,6 +341,19 @@ "icon_color": "#ffffff", "background": "#2b2b2b" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#1c1c1c" + }, + "background": "#2472f2" } } }, diff --git a/assets/themes/light.json b/assets/themes/light.json index 61923bfec5..8518869825 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -341,6 +341,19 @@ "icon_color": "#000000", "background": "#e3e3e3" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#f8f8f8" + }, + "background": "#484bed" } } }, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 9865c12586..6ce85a9ee8 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -341,6 +341,19 @@ "icon_color": "#fdf6e3", "background": "#586e755c" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#073642" + }, + "background": "#268bd2" } } }, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index c61d354d35..a3bc6b8597 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -341,6 +341,19 @@ "icon_color": "#002b36", "background": "#93a1a12e" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#eee8d5" + }, + "background": "#268bd2" } } }, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 907ec58cc0..68657b31c2 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -341,6 +341,19 @@ "icon_color": "#f5f7ff", "background": "#5e66875c" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#293256" + }, + "background": "#3d8fd1" } } }, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 3ae43250f0..18e4b99363 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -341,6 +341,19 @@ "icon_color": "#202746", "background": "#979db42e" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#dfe2f1" + }, + "background": "#3d8fd1" } } }, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0a34e75b3a..8cd4b6387c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -7264,7 +7264,7 @@ mod tests { } fn render(&mut self, _: &mut gpui::RenderContext) -> gpui::ElementBox { - gpui::Element::boxed(gpui::elements::Empty) + gpui::Element::boxed(gpui::elements::Empty::new()) } } } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 333de8c3d5..003f3885b1 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -10,14 +10,14 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, impl_actions, platform::CursorStyle, - Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, - Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, + RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use serde::Deserialize; use settings::Settings; use std::sync::Arc; use theme::IconButton; -use workspace::{AppState, JoinProject, Workspace}; +use workspace::{sidebar::SidebarItem, AppState, JoinProject, Workspace}; impl_actions!( contacts_panel, @@ -599,6 +599,16 @@ impl ContactsPanel { } } +impl SidebarItem for ContactsPanel { + fn should_show_badge(&self, cx: &AppContext) -> bool { + !self + .user_store + .read(cx) + .incoming_contact_requests() + .is_empty() + } +} + fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { Svg::new(svg_path) .with_color(style.color) diff --git a/crates/gpui/src/elements/empty.rs b/crates/gpui/src/elements/empty.rs index 90b2123163..afe24127b5 100644 --- a/crates/gpui/src/elements/empty.rs +++ b/crates/gpui/src/elements/empty.rs @@ -8,11 +8,18 @@ use crate::{ }; use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; -pub struct Empty; +pub struct Empty { + collapsed: bool, +} impl Empty { pub fn new() -> Self { - Self + Self { collapsed: false } + } + + pub fn collapsed(mut self) -> Self { + self.collapsed = true; + self } } @@ -25,12 +32,12 @@ impl Element for Empty { constraint: SizeConstraint, _: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let x = if constraint.max.x().is_finite() { + let x = if constraint.max.x().is_finite() && !self.collapsed { constraint.max.x() } else { constraint.min.x() }; - let y = if constraint.max.y().is_finite() { + let y = if constraint.max.y().is_finite() && !self.collapsed { constraint.max.y() } else { constraint.min.y() diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 61c97f281d..639d7b44d9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -900,6 +900,12 @@ impl Entity for ProjectPanel { type Event = Event; } +impl workspace::sidebar::SidebarItem for ProjectPanel { + fn should_show_badge(&self, _: &AppContext) -> bool { + false + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index aeb656828e..5575dce9e7 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -162,6 +162,7 @@ pub struct StatusBarSidebarButtons { pub group_left: ContainerStyle, pub group_right: ContainerStyle, pub item: Interactive, + pub badge: ContainerStyle, } #[derive(Deserialize, Default)] diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index c9cbcbb4fb..366c74e43f 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -1,13 +1,40 @@ +use crate::StatusItemView; use gpui::{ - elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, Entity, RenderContext, View, - ViewContext, ViewHandle, + elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity, + RenderContext, Subscription, View, ViewContext, ViewHandle, }; use serde::Deserialize; use settings::Settings; use std::{cell::RefCell, rc::Rc}; use theme::Theme; -use crate::StatusItemView; +pub trait SidebarItem: View { + fn should_show_badge(&self, cx: &AppContext) -> bool; +} + +pub trait SidebarItemHandle { + fn should_show_badge(&self, cx: &AppContext) -> bool; + fn to_any(&self) -> AnyViewHandle; +} + +impl SidebarItemHandle for ViewHandle +where + T: SidebarItem, +{ + fn should_show_badge(&self, cx: &AppContext) -> bool { + self.read(cx).should_show_badge(cx) + } + + fn to_any(&self) -> AnyViewHandle { + self.into() + } +} + +impl Into for &dyn SidebarItemHandle { + fn into(self) -> AnyViewHandle { + self.to_any() + } +} pub struct Sidebar { side: Side, @@ -23,10 +50,10 @@ pub enum Side { Right, } -#[derive(Clone)] struct Item { icon_path: &'static str, - view: AnyViewHandle, + view: Rc, + _observation: Subscription, } pub struct SidebarButtons { @@ -58,13 +85,18 @@ impl Sidebar { } } - pub fn add_item( + pub fn add_item( &mut self, icon_path: &'static str, - view: AnyViewHandle, + view: ViewHandle, cx: &mut ViewContext, ) { - self.items.push(Item { icon_path, view }); + let subscription = cx.observe(&view, |_, _, cx| cx.notify()); + self.items.push(Item { + icon_path, + view: Rc::new(view), + _observation: subscription, + }); cx.notify() } @@ -82,10 +114,10 @@ impl Sidebar { cx.notify(); } - pub fn active_item(&self) -> Option<&AnyViewHandle> { + pub fn active_item(&self) -> Option<&dyn SidebarItemHandle> { self.active_item_ix .and_then(|ix| self.items.get(ix)) - .map(|item| &item.view) + .map(|item| item.view.as_ref()) } fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { @@ -185,34 +217,62 @@ impl View for SidebarButtons { .sidebar_buttons; let sidebar = self.sidebar.read(cx); let item_style = theme.item; + let badge_style = theme.badge; let active_ix = sidebar.active_item_ix; let side = sidebar.side; let group_style = match side { Side::Left => theme.group_left, Side::Right => theme.group_right, }; - let items = sidebar.items.clone(); + let items = sidebar + .items + .iter() + .map(|item| (item.icon_path, item.view.clone())) + .collect::>(); Flex::row() - .with_children(items.iter().enumerate().map(|(ix, item)| { - MouseEventHandler::new::(ix, cx, move |state, _| { - let style = item_style.style_for(state, Some(ix) == active_ix); - Svg::new(item.icon_path) - .with_color(style.icon_color) - .constrained() - .with_height(style.icon_size) - .contained() - .with_style(style.container) + .with_children( + items + .into_iter() + .enumerate() + .map(|(ix, (icon_path, item_view))| { + MouseEventHandler::new::(ix, cx, move |state, cx| { + let is_active = Some(ix) == active_ix; + let style = item_style.style_for(state, is_active); + Stack::new() + .with_child( + Svg::new(icon_path).with_color(style.icon_color).boxed(), + ) + .with_children(if !is_active && item_view.should_show_badge(cx) { + Some( + Empty::new() + .collapsed() + .contained() + .with_style(badge_style) + .aligned() + .bottom() + .right() + .boxed(), + ) + } else { + None + }) + .constrained() + .with_width(style.icon_size) + .with_height(style.icon_size) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(ToggleSidebarItem { + side, + item_index: ix, + }) + }) .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(ToggleSidebarItem { - side, - item_index: ix, - }) - }) - .boxed() - })) + }), + ) .contained() .with_style(group_style) .boxed() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b077b82518..68ecfa8903 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1102,7 +1102,7 @@ impl Workspace { }; let active_item = sidebar.update(cx, |sidebar, cx| { sidebar.toggle_item(action.item_index, cx); - sidebar.active_item().cloned() + sidebar.active_item().map(|item| item.to_any()) }); if let Some(active_item) = active_item { cx.focus(active_item); @@ -1123,7 +1123,7 @@ impl Workspace { }; let active_item = sidebar.update(cx, |sidebar, cx| { sidebar.activate_item(action.item_index, cx); - sidebar.active_item().cloned() + sidebar.active_item().map(|item| item.to_any()) }); if let Some(active_item) = active_item { if active_item.is_focused(cx) { diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index 621b77639e..c7b7c6a0a3 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -1,8 +1,8 @@ import Theme from "../themes/theme"; import { backgroundColor, border, iconColor, text } from "./components"; +import { workspaceBackground } from "./workspace"; export default function statusBar(theme: Theme) { - const statusContainer = { cornerRadius: 6, padding: { top: 3, bottom: 3, left: 6, right: 6 } @@ -100,6 +100,13 @@ export default function statusBar(theme: Theme) { iconColor: iconColor(theme, "active"), background: backgroundColor(theme, 300, "active"), } + }, + badge: { + cornerRadius: 3, + padding: 2, + margin: { bottom: -1, right: -1 }, + border: { width: 1, color: workspaceBackground(theme) }, + background: iconColor(theme, "feature"), } } } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 1d4b78944f..326b07b9ee 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -2,11 +2,15 @@ import Theme from "../themes/theme"; import { backgroundColor, border, iconColor, shadow, text } from "./components"; import statusBar from "./statusBar"; +export function workspaceBackground(theme: Theme) { + return backgroundColor(theme, 300) +} + export default function workspace(theme: Theme) { const tab = { height: 32, - background: backgroundColor(theme, 300), + background: workspaceBackground(theme), iconClose: iconColor(theme, "muted"), iconCloseActive: iconColor(theme, "active"), iconConflict: iconColor(theme, "warning"), From 50b44ebe85efff39890263d4d554b209605fe8f3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 11 May 2022 12:35:00 -0400 Subject: [PATCH 08/12] Add onMedia border token --- styles/dist/dark.json | 5 +++++ styles/dist/light.json | 5 +++++ styles/dist/solarized-dark.json | 4 ++++ styles/dist/solarized-light.json | 4 ++++ styles/dist/tokens.json | 18 ++++++++++++++++++ styles/src/themes/base16.ts | 1 + styles/src/themes/dark.ts | 1 + styles/src/themes/light.ts | 1 + styles/src/themes/theme.ts | 4 ++++ 9 files changed, 43 insertions(+) diff --git a/styles/dist/dark.json b/styles/dist/dark.json index c1be80296e..dac975e8f0 100644 --- a/styles/dist/dark.json +++ b/styles/dist/dark.json @@ -332,6 +332,11 @@ "description": "Step: 900", "type": "color" }, + "onMedia": { + "value": "#0707071a", + "description": "Step: 875", + "type": "color" + }, "ok": { "value": "#1b944726", "description": "Step: 600", diff --git a/styles/dist/light.json b/styles/dist/light.json index 166e00731e..806c50240e 100644 --- a/styles/dist/light.json +++ b/styles/dist/light.json @@ -332,6 +332,11 @@ "description": "Step: 250", "type": "color" }, + "onMedia": { + "value": "#b8b8b84d", + "description": "Step: 250", + "type": "color" + }, "ok": { "value": "#1b944726", "description": "Step: 600", diff --git a/styles/dist/solarized-dark.json b/styles/dist/solarized-dark.json index 862d5afbe8..c59a9dffad 100644 --- a/styles/dist/solarized-dark.json +++ b/styles/dist/solarized-dark.json @@ -271,6 +271,10 @@ "value": "#657b83", "type": "color" }, + "onMedia": { + "value": "#002b361a", + "type": "color" + }, "ok": { "value": "#85990026", "type": "color" diff --git a/styles/dist/solarized-light.json b/styles/dist/solarized-light.json index 993519c8f1..21e8e4ba6f 100644 --- a/styles/dist/solarized-light.json +++ b/styles/dist/solarized-light.json @@ -271,6 +271,10 @@ "value": "#839496", "type": "color" }, + "onMedia": { + "value": "#fdf6e31a", + "type": "color" + }, "ok": { "value": "#85990026", "type": "color" diff --git a/styles/dist/tokens.json b/styles/dist/tokens.json index 5a556b36a3..9ed6998725 100644 --- a/styles/dist/tokens.json +++ b/styles/dist/tokens.json @@ -1514,6 +1514,11 @@ "description": "Step: 900", "type": "color" }, + "onMedia": { + "value": "#0707071a", + "description": "Step: 875", + "type": "color" + }, "ok": { "value": "#1b944726", "description": "Step: 600", @@ -2207,6 +2212,11 @@ "description": "Step: 250", "type": "color" }, + "onMedia": { + "value": "#b8b8b84d", + "description": "Step: 250", + "type": "color" + }, "ok": { "value": "#1b944726", "description": "Step: 600", @@ -2839,6 +2849,10 @@ "value": "#657b83", "type": "color" }, + "onMedia": { + "value": "#002b361a", + "type": "color" + }, "ok": { "value": "#85990026", "type": "color" @@ -3406,6 +3420,10 @@ "value": "#839496", "type": "color" }, + "onMedia": { + "value": "#fdf6e31a", + "type": "color" + }, "ok": { "value": "#85990026", "type": "color" diff --git a/styles/src/themes/base16.ts b/styles/src/themes/base16.ts index eb67f5f8f2..98f1fb4878 100644 --- a/styles/src/themes/base16.ts +++ b/styles/src/themes/base16.ts @@ -82,6 +82,7 @@ export function createTheme(name: string, isLight: boolean, neutral: ColorToken[ muted: neutral[3], focused: neutral[3], active: neutral[3], + onMedia: withOpacity(neutral[0], 0.1), ok: withOpacity(accent.green, 0.15), error: withOpacity(accent.red, 0.15), warning: withOpacity(accent.yellow, 0.15), diff --git a/styles/src/themes/dark.ts b/styles/src/themes/dark.ts index 3dcf9ea691..77eb493591 100644 --- a/styles/src/themes/dark.ts +++ b/styles/src/themes/dark.ts @@ -65,6 +65,7 @@ const borderColor = { muted: colors.neutral[675], focused: colors.indigo[500], active: colors.neutral[900], + onMedia: withOpacity(colors.neutral[875], 0.1), ok: withOpacity(colors.green[600], 0.15), error: withOpacity(colors.red[500], 0.15), warning: withOpacity(colors.amber[400], 0.15), diff --git a/styles/src/themes/light.ts b/styles/src/themes/light.ts index c17aaf5807..a244267f46 100644 --- a/styles/src/themes/light.ts +++ b/styles/src/themes/light.ts @@ -65,6 +65,7 @@ const borderColor = { muted: colors.neutral[100], focused: colors.indigo[500], active: colors.neutral[250], + onMedia: withOpacity(colors.neutral[250], 0.3), ok: withOpacity(colors.green[600], 0.15), error: withOpacity(colors.red[500], 0.15), warning: withOpacity(colors.amber[400], 0.15), diff --git a/styles/src/themes/theme.ts b/styles/src/themes/theme.ts index aa422e0330..7113ee555c 100644 --- a/styles/src/themes/theme.ts +++ b/styles/src/themes/theme.ts @@ -87,6 +87,10 @@ export default interface Theme { muted: ColorToken; focused: ColorToken; active: ColorToken; + /** + * Used for rendering borders on top of media like avatars, images, video, etc. + */ + onMedia: ColorToken; ok: ColorToken; error: ColorToken; warning: ColorToken; From a5fd664b00ffa26a901dee0716f425d6e72982c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 11 May 2022 18:51:40 +0200 Subject: [PATCH 09/12] Add the ability to notify when a user accepts a contact request Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/collab/src/db.rs | 381 +++++++++++++++++++++++---------- crates/collab/src/rpc.rs | 48 +++-- crates/collab/src/rpc/store.rs | 37 ++-- 3 files changed, 316 insertions(+), 150 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 4bb61c3404..056f94ecfe 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -17,10 +17,11 @@ pub trait Db: Send + Sync { async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()>; async fn destroy_user(&self, id: UserId) -> Result<()>; - async fn get_contacts(&self, id: UserId) -> Result; + async fn get_contacts(&self, id: UserId) -> Result>; + async fn has_contact(&self, user_id_a: UserId, user_id_b: UserId) -> Result; async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()>; - async fn dismiss_contact_request( + async fn dismiss_contact_notification( &self, responder_id: UserId, requester_id: UserId, @@ -190,7 +191,7 @@ impl Db for PostgresDb { // contacts - async fn get_contacts(&self, user_id: UserId) -> Result { + async fn get_contacts(&self, user_id: UserId) -> Result> { let query = " SELECT user_id_a, user_id_b, a_to_b, accepted, should_notify FROM contacts @@ -201,46 +202,67 @@ impl Db for PostgresDb { .bind(user_id) .fetch(&self.pool); - let mut current = vec![user_id]; - let mut outgoing_requests = Vec::new(); - let mut incoming_requests = Vec::new(); + let mut contacts = vec![Contact::Accepted { + user_id, + should_notify: false, + }]; while let Some(row) = rows.next().await { let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?; if user_id_a == user_id { if accepted { - current.push(user_id_b); + contacts.push(Contact::Accepted { + user_id: user_id_b, + should_notify: should_notify && a_to_b, + }); } else if a_to_b { - outgoing_requests.push(user_id_b); + contacts.push(Contact::Outgoing { user_id: user_id_b }) } else { - incoming_requests.push(IncomingContactRequest { - requester_id: user_id_b, + contacts.push(Contact::Incoming { + user_id: user_id_b, should_notify, }); } } else { if accepted { - current.push(user_id_a); + contacts.push(Contact::Accepted { + user_id: user_id_a, + should_notify: should_notify && !a_to_b, + }); } else if a_to_b { - incoming_requests.push(IncomingContactRequest { - requester_id: user_id_a, + contacts.push(Contact::Incoming { + user_id: user_id_a, should_notify, }); } else { - outgoing_requests.push(user_id_a); + contacts.push(Contact::Outgoing { user_id: user_id_a }); } } } - current.sort_unstable(); - outgoing_requests.sort_unstable(); - incoming_requests.sort_unstable(); + contacts.sort_unstable_by_key(|contact| contact.user_id()); - Ok(Contacts { - current, - outgoing_requests, - incoming_requests, - }) + Ok(contacts) + } + + async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result { + let (id_a, id_b) = if user_id_1 < user_id_2 { + (user_id_1, user_id_2) + } else { + (user_id_2, user_id_1) + }; + + let query = " + SELECT 1 FROM contacts + WHERE user_id_a = $1 AND user_id_b = $2 AND accepted = 't' + LIMIT 1 + "; + Ok(sqlx::query_scalar::<_, i32>(query) + .bind(id_a.0) + .bind(id_b.0) + .fetch_optional(&self.pool) + .await? + .is_some()) } async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> { @@ -254,7 +276,8 @@ impl Db for PostgresDb { VALUES ($1, $2, $3, 'f', 't') ON CONFLICT (user_id_a, user_id_b) DO UPDATE SET - accepted = 't' + accepted = 't', + should_notify = 'f' WHERE NOT contacts.accepted AND ((contacts.a_to_b = excluded.a_to_b AND contacts.user_id_a = excluded.user_id_b) OR @@ -297,21 +320,26 @@ impl Db for PostgresDb { } } - async fn dismiss_contact_request( + async fn dismiss_contact_notification( &self, - responder_id: UserId, - requester_id: UserId, + user_id: UserId, + contact_user_id: UserId, ) -> Result<()> { - let (id_a, id_b, a_to_b) = if responder_id < requester_id { - (responder_id, requester_id, false) + let (id_a, id_b, a_to_b) = if user_id < contact_user_id { + (user_id, contact_user_id, true) } else { - (requester_id, responder_id, true) + (contact_user_id, user_id, false) }; let query = " UPDATE contacts SET should_notify = 'f' - WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; + WHERE + user_id_a = $1 AND user_id_b = $2 AND + ( + (a_to_b = $3 AND accepted) OR + (a_to_b != $3 AND NOT accepted) + ); "; let result = sqlx::query(query) @@ -342,7 +370,7 @@ impl Db for PostgresDb { let result = if accept { let query = " UPDATE contacts - SET accepted = 't', should_notify = 'f' + SET accepted = 't', should_notify = 't' WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3; "; sqlx::query(query) @@ -702,10 +730,28 @@ pub struct ChannelMessage { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Contacts { - pub current: Vec, - pub incoming_requests: Vec, - pub outgoing_requests: Vec, +pub enum Contact { + Accepted { + user_id: UserId, + should_notify: bool, + }, + Outgoing { + user_id: UserId, + }, + Incoming { + user_id: UserId, + should_notify: bool, + }, +} + +impl Contact { + pub fn user_id(&self) -> UserId { + match self { + Contact::Accepted { user_id, .. } => *user_id, + Contact::Outgoing { user_id } => *user_id, + Contact::Incoming { user_id, .. } => *user_id, + } + } } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -947,51 +993,60 @@ pub mod tests { // User starts with no contacts assert_eq!( db.get_contacts(user_1).await.unwrap(), - Contacts { - current: vec![user_1], - outgoing_requests: vec![], - incoming_requests: vec![], - }, + vec![Contact::Accepted { + user_id: user_1, + should_notify: false + }], ); // User requests a contact. Both users see the pending request. db.send_contact_request(user_1, user_2).await.unwrap(); + assert!(!db.has_contact(user_1, user_2).await.unwrap()); + assert!(!db.has_contact(user_2, user_1).await.unwrap()); assert_eq!( db.get_contacts(user_1).await.unwrap(), - Contacts { - current: vec![user_1], - outgoing_requests: vec![user_2], - incoming_requests: vec![], - }, + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false + }, + Contact::Outgoing { user_id: user_2 } + ], ); assert_eq!( db.get_contacts(user_2).await.unwrap(), - Contacts { - current: vec![user_2], - outgoing_requests: vec![], - incoming_requests: vec![IncomingContactRequest { - requester_id: user_1, + &[ + Contact::Incoming { + user_id: user_1, should_notify: true - }], - }, + }, + Contact::Accepted { + user_id: user_2, + should_notify: false + }, + ] ); // User 2 dismisses the contact request notification without accepting or rejecting. // We shouldn't notify them again. - db.dismiss_contact_request(user_1, user_2) + db.dismiss_contact_notification(user_1, user_2) .await .unwrap_err(); - db.dismiss_contact_request(user_2, user_1).await.unwrap(); + db.dismiss_contact_notification(user_2, user_1) + .await + .unwrap(); assert_eq!( db.get_contacts(user_2).await.unwrap(), - Contacts { - current: vec![user_2], - outgoing_requests: vec![], - incoming_requests: vec![IncomingContactRequest { - requester_id: user_1, + &[ + Contact::Incoming { + user_id: user_1, should_notify: false - }], - }, + }, + Contact::Accepted { + user_id: user_2, + should_notify: false + }, + ] ); // User can't accept their own contact request @@ -1005,44 +1060,106 @@ pub mod tests { .unwrap(); assert_eq!( db.get_contacts(user_1).await.unwrap(), - Contacts { - current: vec![user_1, user_2], - outgoing_requests: vec![], - incoming_requests: vec![], - }, + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false + }, + Contact::Accepted { + user_id: user_2, + should_notify: true + } + ], ); + assert!(db.has_contact(user_1, user_2).await.unwrap()); + assert!(db.has_contact(user_2, user_1).await.unwrap()); assert_eq!( db.get_contacts(user_2).await.unwrap(), - Contacts { - current: vec![user_1, user_2], - outgoing_requests: vec![], - incoming_requests: vec![], - }, + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false, + }, + Contact::Accepted { + user_id: user_2, + should_notify: false, + }, + ] ); // Users cannot re-request existing contacts. db.send_contact_request(user_1, user_2).await.unwrap_err(); db.send_contact_request(user_2, user_1).await.unwrap_err(); + // Users can't dismiss notifications of them accepting other users' requests. + db.dismiss_contact_notification(user_2, user_1) + .await + .unwrap_err(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false + }, + Contact::Accepted { + user_id: user_2, + should_notify: true, + }, + ] + ); + + // Users can dismiss notifications of other users accepting their requests. + db.dismiss_contact_notification(user_1, user_2) + .await + .unwrap(); + assert_eq!( + db.get_contacts(user_1).await.unwrap(), + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false + }, + Contact::Accepted { + user_id: user_2, + should_notify: false, + }, + ] + ); + // Users send each other concurrent contact requests and // see that they are immediately accepted. db.send_contact_request(user_1, user_3).await.unwrap(); db.send_contact_request(user_3, user_1).await.unwrap(); assert_eq!( db.get_contacts(user_1).await.unwrap(), - Contacts { - current: vec![user_1, user_2, user_3], - outgoing_requests: vec![], - incoming_requests: vec![], - }, + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false + }, + Contact::Accepted { + user_id: user_2, + should_notify: false, + }, + Contact::Accepted { + user_id: user_3, + should_notify: false + }, + ] ); assert_eq!( db.get_contacts(user_3).await.unwrap(), - Contacts { - current: vec![user_1, user_3], - outgoing_requests: vec![], - incoming_requests: vec![], - }, + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false + }, + Contact::Accepted { + user_id: user_3, + should_notify: false + } + ], ); // User declines a contact request. Both users see that it is gone. @@ -1050,21 +1167,33 @@ pub mod tests { db.respond_to_contact_request(user_3, user_2, false) .await .unwrap(); + assert!(!db.has_contact(user_2, user_3).await.unwrap()); + assert!(!db.has_contact(user_3, user_2).await.unwrap()); assert_eq!( db.get_contacts(user_2).await.unwrap(), - Contacts { - current: vec![user_1, user_2], - outgoing_requests: vec![], - incoming_requests: vec![], - }, + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false + }, + Contact::Accepted { + user_id: user_2, + should_notify: false + } + ] ); assert_eq!( db.get_contacts(user_3).await.unwrap(), - Contacts { - current: vec![user_1, user_3], - outgoing_requests: vec![], - incoming_requests: vec![], - }, + &[ + Contact::Accepted { + user_id: user_1, + should_notify: false + }, + Contact::Accepted { + user_id: user_3, + should_notify: false + } + ], ); } } @@ -1219,40 +1348,51 @@ pub mod tests { unimplemented!() } - async fn get_contacts(&self, id: UserId) -> Result { + async fn get_contacts(&self, id: UserId) -> Result> { self.background.simulate_random_delay().await; - let mut current = vec![id]; - let mut outgoing_requests = Vec::new(); - let mut incoming_requests = Vec::new(); + let mut contacts = vec![Contact::Accepted { + user_id: id, + should_notify: false, + }]; for contact in self.contacts.lock().iter() { if contact.requester_id == id { if contact.accepted { - current.push(contact.responder_id); + contacts.push(Contact::Accepted { + user_id: contact.responder_id, + should_notify: contact.should_notify, + }); } else { - outgoing_requests.push(contact.responder_id); + contacts.push(Contact::Outgoing { + user_id: contact.responder_id, + }); } } else if contact.responder_id == id { if contact.accepted { - current.push(contact.requester_id); + contacts.push(Contact::Accepted { + user_id: contact.requester_id, + should_notify: false, + }); } else { - incoming_requests.push(IncomingContactRequest { - requester_id: contact.requester_id, + contacts.push(Contact::Incoming { + user_id: contact.requester_id, should_notify: contact.should_notify, }); } } } - current.sort_unstable(); - outgoing_requests.sort_unstable(); - incoming_requests.sort_unstable(); + contacts.sort_unstable_by_key(|contact| contact.user_id()); + Ok(contacts) + } - Ok(Contacts { - current, - outgoing_requests, - incoming_requests, - }) + async fn has_contact(&self, user_id_a: UserId, user_id_b: UserId) -> Result { + self.background.simulate_random_delay().await; + Ok(self.contacts.lock().iter().any(|contact| { + contact.accepted + && ((contact.requester_id == user_id_a && contact.responder_id == user_id_b) + || (contact.requester_id == user_id_b && contact.responder_id == user_id_a)) + })) } async fn send_contact_request( @@ -1274,6 +1414,7 @@ pub mod tests { Err(anyhow!("contact already exists"))?; } else { contact.accepted = true; + contact.should_notify = false; return Ok(()); } } @@ -1294,22 +1435,29 @@ pub mod tests { Ok(()) } - async fn dismiss_contact_request( + async fn dismiss_contact_notification( &self, - responder_id: UserId, - requester_id: UserId, + user_id: UserId, + contact_user_id: UserId, ) -> Result<()> { let mut contacts = self.contacts.lock(); for contact in contacts.iter_mut() { - if contact.requester_id == requester_id && contact.responder_id == responder_id { - if contact.accepted { - return Err(anyhow!("contact already confirmed")); - } + if contact.requester_id == contact_user_id + && contact.responder_id == user_id + && !contact.accepted + { + contact.should_notify = false; + return Ok(()); + } + if contact.requester_id == user_id + && contact.responder_id == contact_user_id + && contact.accepted + { contact.should_notify = false; return Ok(()); } } - Err(anyhow!("no such contact request")) + Err(anyhow!("no such notification")) } async fn respond_to_contact_request( @@ -1326,6 +1474,7 @@ pub mod tests { } if accept { contact.accepted = true; + contact.should_notify = true; } else { contacts.remove(ix); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8cd4b6387c..4bf06fe7a3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,7 @@ mod store; use crate::{ auth, - db::{ChannelId, MessageId, UserId}, + db::{self, ChannelId, MessageId, UserId}, AppState, Result, }; use anyhow::anyhow; @@ -421,21 +421,27 @@ impl Server { let contacts = self.app_state.db.get_contacts(user_id).await?; let store = self.store().await; let updated_contact = store.contact_for_user(user_id); - for contact_user_id in contacts.current { - for contact_conn_id in store.connection_ids_for_user(contact_user_id) { - self.peer - .send( - contact_conn_id, - proto::UpdateContacts { - contacts: vec![updated_contact.clone()], - remove_contacts: Default::default(), - incoming_requests: Default::default(), - remove_incoming_requests: Default::default(), - outgoing_requests: Default::default(), - remove_outgoing_requests: Default::default(), - }, - ) - .trace_err(); + for contact in contacts { + if let db::Contact::Accepted { + user_id: contact_user_id, + .. + } = contact + { + for contact_conn_id in store.connection_ids_for_user(contact_user_id) { + self.peer + .send( + contact_conn_id, + proto::UpdateContacts { + contacts: vec![updated_contact.clone()], + remove_contacts: Default::default(), + incoming_requests: Default::default(), + remove_incoming_requests: Default::default(), + outgoing_requests: Default::default(), + remove_outgoing_requests: Default::default(), + }, + ) + .trace_err(); + } } } Ok(()) @@ -473,8 +479,12 @@ impl Server { guest_user_id = state.user_id_for_connection(request.sender_id)?; }; - let guest_contacts = self.app_state.db.get_contacts(guest_user_id).await?; - if !guest_contacts.current.contains(&host_user_id) { + let has_contact = self + .app_state + .db + .has_contact(guest_user_id, host_user_id) + .await?; + if !has_contact { return Err(anyhow!("no such project"))?; } @@ -1026,7 +1036,7 @@ impl Server { if request.payload.response == proto::ContactRequestResponse::Dismiss as i32 { self.app_state .db - .dismiss_contact_request(responder_id, requester_id) + .dismiss_contact_notification(responder_id, requester_id) .await?; } else { let accept = request.payload.response == proto::ContactRequestResponse::Accept as i32; diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 8ca2706228..9f56c95a47 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -217,23 +217,30 @@ impl Store { .is_empty() } - pub fn build_initial_contacts_update(&self, contacts: db::Contacts) -> proto::UpdateContacts { + pub fn build_initial_contacts_update( + &self, + contacts: Vec, + ) -> proto::UpdateContacts { let mut update = proto::UpdateContacts::default(); - for user_id in contacts.current { - update.contacts.push(self.contact_for_user(user_id)); - } - for request in contacts.incoming_requests { - update - .incoming_requests - .push(proto::IncomingContactRequest { - requester_id: request.requester_id.to_proto(), - should_notify: request.should_notify, - }) - } - - for requested_user_id in contacts.outgoing_requests { - update.outgoing_requests.push(requested_user_id.to_proto()) + for contact in contacts { + match contact { + db::Contact::Accepted { user_id, .. } => { + update.contacts.push(self.contact_for_user(user_id)); + } + db::Contact::Outgoing { user_id } => { + update.outgoing_requests.push(user_id.to_proto()) + } + db::Contact::Incoming { + user_id, + should_notify, + } => update + .incoming_requests + .push(proto::IncomingContactRequest { + requester_id: user_id.to_proto(), + should_notify, + }), + } } update From a33ef65f57033d6e758a30fa4a26a8b446025391 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 11 May 2022 10:18:40 -0700 Subject: [PATCH 10/12] Order returned ranges from marked_text_ranges by start index --- crates/util/src/test/marked_text.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs index 23aa2d5806..23ac35ce86 100644 --- a/crates/util/src/test/marked_text.rs +++ b/crates/util/src/test/marked_text.rs @@ -61,12 +61,12 @@ pub fn marked_text_ranges(full_marked_text: &str) -> (String, Vec>) let (range_marked_text, empty_offsets) = marked_text(full_marked_text); let (unmarked, range_lookup) = marked_text_ranges_by(&range_marked_text, vec![('[', ']'), ('(', ')'), ('<', '>')]); - ( - unmarked, - range_lookup - .into_values() - .flatten() - .chain(empty_offsets.into_iter().map(|offset| offset..offset)) - .collect(), - ) + let mut combined_ranges: Vec<_> = range_lookup + .into_values() + .flatten() + .chain(empty_offsets.into_iter().map(|offset| offset..offset)) + .collect(); + + combined_ranges.sort_by_key(|range| range.start); + (unmarked, combined_ranges) } From 3bc9b8ec856b383057fae40a150cb25409f060aa Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 May 2022 11:39:01 -0700 Subject: [PATCH 11/12] Add notifications for accepted contact requests Co-authored-by: Nathan Sobo --- assets/themes/cave-dark.json | 2 +- assets/themes/cave-light.json | 2 +- assets/themes/dark.json | 2 +- assets/themes/light.json | 2 +- assets/themes/solarized-dark.json | 2 +- assets/themes/solarized-light.json | 2 +- assets/themes/sulphurpool-dark.json | 2 +- assets/themes/sulphurpool-light.json | 2 +- crates/client/src/user.rs | 40 +++- crates/collab/src/rpc.rs | 10 +- crates/collab/src/rpc/store.rs | 12 +- .../src/contact_notification.rs | 224 ++++++++++++++++++ .../src/contact_notifications.rs | 206 ---------------- crates/contacts_panel/src/contacts_panel.rs | 35 ++- crates/rpc/proto/zed.proto | 1 + crates/theme/src/theme.rs | 4 +- styles/src/styleTree/app.ts | 4 +- ...Notification.ts => contactNotification.ts} | 2 +- 18 files changed, 301 insertions(+), 253 deletions(-) create mode 100644 crates/contacts_panel/src/contact_notification.rs delete mode 100644 crates/contacts_panel/src/contact_notifications.rs rename styles/src/styleTree/{incomingRequestNotification.ts => contactNotification.ts} (91%) diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index acb5315dda..ae8e32e945 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 5d75efa22a..bf444d4758 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 393b5b20d8..b4b86e2f49 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/light.json b/assets/themes/light.json index 8518869825..0ac7535acb 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 6ce85a9ee8..e9c50a1eae 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index a3bc6b8597..1ef6a68351 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 68657b31c2..37264cd203 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 18e4b99363..d5adf576dd 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 4d5f44c320..7de32e8077 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -54,13 +54,21 @@ pub struct UserStore { _maintain_current_user: Task<()>, } -pub enum Event { - ContactRequested(Arc), - ContactRequestCancelled(Arc), +#[derive(Clone)] +pub struct ContactEvent { + pub user: Arc, + pub kind: ContactEventKind, +} + +#[derive(Clone, Copy)] +pub enum ContactEventKind { + Requested, + Accepted, + Cancelled, } impl Entity for UserStore { - type Event = Event; + type Event = ContactEvent; } enum UpdateContacts { @@ -178,8 +186,10 @@ impl UserStore { // No need to paralellize here let mut updated_contacts = Vec::new(); for contact in message.contacts { - updated_contacts.push(Arc::new( - Contact::from_proto(contact, &this, &mut cx).await?, + let should_notify = contact.should_notify; + updated_contacts.push(( + Arc::new(Contact::from_proto(contact, &this, &mut cx).await?), + should_notify, )); } @@ -215,7 +225,13 @@ impl UserStore { this.contacts .retain(|contact| !removed_contacts.contains(&contact.user.id)); // Update existing contacts and insert new ones - for updated_contact in updated_contacts { + for (updated_contact, should_notify) in updated_contacts { + if should_notify { + cx.emit(ContactEvent { + user: updated_contact.user.clone(), + kind: ContactEventKind::Accepted, + }); + } match this.contacts.binary_search_by_key( &&updated_contact.user.github_login, |contact| &contact.user.github_login, @@ -228,7 +244,10 @@ impl UserStore { // Remove incoming contact requests this.incoming_contact_requests.retain(|user| { if removed_incoming_requests.contains(&user.id) { - cx.emit(Event::ContactRequestCancelled(user.clone())); + cx.emit(ContactEvent { + user: user.clone(), + kind: ContactEventKind::Cancelled, + }); false } else { true @@ -237,7 +256,10 @@ impl UserStore { // Update existing incoming requests and insert new ones for (user, should_notify) in incoming_requests { if should_notify { - cx.emit(Event::ContactRequested(user.clone())); + cx.emit(ContactEvent { + user: user.clone(), + kind: ContactEventKind::Requested, + }); } match this diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4bf06fe7a3..ecd3847945 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -420,7 +420,7 @@ impl Server { async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { let contacts = self.app_state.db.get_contacts(user_id).await?; let store = self.store().await; - let updated_contact = store.contact_for_user(user_id); + let updated_contact = store.contact_for_user(user_id, false); for contact in contacts { if let db::Contact::Accepted { user_id: contact_user_id, @@ -1049,7 +1049,9 @@ impl Server { // Update responder with new contact let mut update = proto::UpdateContacts::default(); if accept { - update.contacts.push(store.contact_for_user(requester_id)); + update + .contacts + .push(store.contact_for_user(requester_id, false)); } update .remove_incoming_requests @@ -1061,7 +1063,9 @@ impl Server { // Update requester with new contact let mut update = proto::UpdateContacts::default(); if accept { - update.contacts.push(store.contact_for_user(responder_id)); + update + .contacts + .push(store.contact_for_user(responder_id, true)); } update .remove_outgoing_requests diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 9f56c95a47..4ab6df0adc 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -225,8 +225,13 @@ impl Store { for contact in contacts { match contact { - db::Contact::Accepted { user_id, .. } => { - update.contacts.push(self.contact_for_user(user_id)); + db::Contact::Accepted { + user_id, + should_notify, + } => { + update + .contacts + .push(self.contact_for_user(user_id, should_notify)); } db::Contact::Outgoing { user_id } => { update.outgoing_requests.push(user_id.to_proto()) @@ -246,11 +251,12 @@ impl Store { update } - pub fn contact_for_user(&self, user_id: UserId) -> proto::Contact { + pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact { proto::Contact { user_id: user_id.to_proto(), projects: self.project_metadata_for_user(user_id), online: self.is_user_online(user_id), + should_notify, } } diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/contacts_panel/src/contact_notification.rs new file mode 100644 index 0000000000..cf3b9aa559 --- /dev/null +++ b/crates/contacts_panel/src/contact_notification.rs @@ -0,0 +1,224 @@ +use client::{ContactEvent, ContactEventKind, UserStore}; +use gpui::{ + elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle, + MutableAppContext, RenderContext, View, ViewContext, +}; +use settings::Settings; +use workspace::Notification; + +impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ContactNotification::dismiss); + cx.add_action(ContactNotification::respond_to_contact_request); +} + +pub struct ContactNotification { + user_store: ModelHandle, + event: ContactEvent, +} + +#[derive(Clone)] +struct Dismiss(u64); + +#[derive(Clone)] +pub struct RespondToContactRequest { + pub user_id: u64, + pub accept: bool, +} + +pub enum Event { + Dismiss, +} + +enum Reject {} +enum Accept {} + +impl Entity for ContactNotification { + type Event = Event; +} + +impl View for ContactNotification { + fn ui_name() -> &'static str { + "ContactNotification" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + match self.event.kind { + ContactEventKind::Requested => self.render_incoming_request(cx), + ContactEventKind::Accepted => self.render_acceptance(cx), + _ => unreachable!(), + } + } +} + +impl Notification for ContactNotification { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + matches!(event, Event::Dismiss) + } +} + +impl ContactNotification { + pub fn new( + event: ContactEvent, + user_store: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + cx.subscribe(&user_store, move |this, _, event, cx| { + if let client::ContactEvent { + kind: ContactEventKind::Cancelled, + user, + } = event + { + if user.id == this.event.user.id { + cx.emit(Event::Dismiss); + } + } + }) + .detach(); + + Self { event, user_store } + } + + fn render_incoming_request(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = cx.global::().theme.clone(); + let theme = &theme.contact_notification; + let user = &self.event.user; + let user_id = user.id; + + Flex::column() + .with_child(self.render_header("added you", theme, cx)) + .with_child( + Label::new( + "They won't know if you decline.".to_string(), + theme.body_message.text.clone(), + ) + .contained() + .with_style(theme.body_message.container) + .boxed(), + ) + .with_child( + Flex::row() + .with_child( + MouseEventHandler::new::( + self.event.user.id as usize, + cx, + |_, _| { + Label::new("Reject".to_string(), theme.button.text.clone()) + .contained() + .with_style(theme.button.container) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }); + }) + .boxed(), + ) + .with_child( + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Label::new("Accept".to_string(), theme.button.text.clone()) + .contained() + .with_style(theme.button.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }); + }) + .boxed(), + ) + .aligned() + .right() + .boxed(), + ) + .contained() + .boxed() + } + + fn render_acceptance(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = cx.global::().theme.clone(); + let theme = &theme.contact_notification; + + self.render_header("accepted your contact request", theme, cx) + } + + fn render_header( + &self, + message: &'static str, + theme: &theme::ContactNotification, + cx: &mut RenderContext, + ) -> ElementBox { + let user = &self.event.user; + let user_id = user.id; + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.header_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + format!("{} {}", user.github_login, message), + theme.header_message.text.clone(), + ) + .contained() + .with_style(theme.header_message.container) + .aligned() + .boxed(), + ) + .with_child( + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Svg::new("icons/reject.svg") + .with_color(theme.dismiss_button.color) + .constrained() + .with_width(theme.dismiss_button.icon_width) + .aligned() + .contained() + .with_style(theme.dismiss_button.container) + .constrained() + .with_width(theme.dismiss_button.button_width) + .with_height(theme.dismiss_button.button_width) + .aligned() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) + .flex_float() + .boxed(), + ) + .constrained() + .with_height(theme.header_height) + .boxed() + } + + fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { + self.user_store.update(cx, |store, cx| { + store + .dismiss_contact_request(self.event.user.id, cx) + .detach_and_log_err(cx); + }); + cx.emit(Event::Dismiss); + } + + fn respond_to_contact_request( + &mut self, + action: &RespondToContactRequest, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(action.user_id, action.accept, cx) + }) + .detach(); + } +} diff --git a/crates/contacts_panel/src/contact_notifications.rs b/crates/contacts_panel/src/contact_notifications.rs deleted file mode 100644 index e5fff481b0..0000000000 --- a/crates/contacts_panel/src/contact_notifications.rs +++ /dev/null @@ -1,206 +0,0 @@ -use client::{User, UserStore}; -use gpui::{ - elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle, - MutableAppContext, RenderContext, View, ViewContext, -}; -use settings::Settings; -use std::sync::Arc; -use workspace::Notification; - -impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(IncomingRequestNotification::dismiss); - cx.add_action(IncomingRequestNotification::respond_to_contact_request); -} - -pub struct IncomingRequestNotification { - user: Arc, - user_store: ModelHandle, -} - -#[derive(Clone)] -struct Dismiss(u64); - -#[derive(Clone)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, -} - -pub enum Event { - Dismiss, -} - -impl Entity for IncomingRequestNotification { - type Event = Event; -} - -impl View for IncomingRequestNotification { - fn ui_name() -> &'static str { - "IncomingRequestNotification" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - enum Dismiss {} - enum Reject {} - enum Accept {} - - let theme = cx.global::().theme.clone(); - let theme = &theme.incoming_request_notification; - let user_id = self.user.id; - - Flex::column() - .with_child( - Flex::row() - .with_children(self.user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.header_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - format!("{} added you", self.user.github_login), - theme.header_message.text.clone(), - ) - .contained() - .with_style(theme.header_message.container) - .aligned() - .boxed(), - ) - .with_child( - MouseEventHandler::new::( - self.user.id as usize, - cx, - |_, _| { - Svg::new("icons/reject.svg") - .with_color(theme.dismiss_button.color) - .constrained() - .with_width(theme.dismiss_button.icon_width) - .aligned() - .contained() - .with_style(theme.dismiss_button.container) - .constrained() - .with_width(theme.dismiss_button.button_width) - .with_height(theme.dismiss_button.button_width) - .aligned() - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) - .flex_float() - .boxed(), - ) - .constrained() - .with_height(theme.header_height) - .boxed(), - ) - .with_child( - Label::new( - "They won't know if you decline.".to_string(), - theme.body_message.text.clone(), - ) - .contained() - .with_style(theme.body_message.container) - .boxed(), - ) - .with_child( - Flex::row() - .with_child( - MouseEventHandler::new::( - self.user.id as usize, - cx, - |_, _| { - Label::new("Reject".to_string(), theme.button.text.clone()) - .contained() - .with_style(theme.button.container) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }); - }) - .boxed(), - ) - .with_child( - MouseEventHandler::new::( - self.user.id as usize, - cx, - |_, _| { - Label::new("Accept".to_string(), theme.button.text.clone()) - .contained() - .with_style(theme.button.container) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }); - }) - .boxed(), - ) - .aligned() - .right() - .boxed(), - ) - .contained() - .boxed() - } -} - -impl Notification for IncomingRequestNotification { - fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { - matches!(event, Event::Dismiss) - } -} - -impl IncomingRequestNotification { - pub fn new( - user: Arc, - user_store: ModelHandle, - cx: &mut ViewContext, - ) -> Self { - let user_id = user.id; - cx.subscribe(&user_store, move |_, _, event, cx| { - if let client::Event::ContactRequestCancelled(user) = event { - if user.id == user_id { - cx.emit(Event::Dismiss); - } - } - }) - .detach(); - - Self { user, user_store } - } - - fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { - self.user_store.update(cx, |store, cx| { - store - .dismiss_contact_request(self.user.id, cx) - .detach_and_log_err(cx); - }); - cx.emit(Event::Dismiss); - } - - fn respond_to_contact_request( - &mut self, - action: &RespondToContactRequest, - cx: &mut ViewContext, - ) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(action.user_id, action.accept, cx) - }) - .detach(); - } -} diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 003f3885b1..3a8a9605f3 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,8 +1,8 @@ mod contact_finder; -mod contact_notifications; +mod contact_notification; -use client::{Contact, User, UserStore}; -use contact_notifications::IncomingRequestNotification; +use client::{Contact, ContactEventKind, User, UserStore}; +use contact_notification::ContactNotification; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -55,7 +55,7 @@ pub struct RespondToContactRequest { pub fn init(cx: &mut MutableAppContext) { contact_finder::init(cx); - contact_notifications::init(cx); + contact_notification::init(cx); cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); @@ -85,25 +85,22 @@ impl ContactsPanel { .detach(); cx.subscribe(&app_state.user_store, { - let user_store = app_state.user_store.clone(); - move |_, _, event, cx| match event { - client::Event::ContactRequested(user) => { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.show_notification( + let user_store = app_state.user_store.downgrade(); + move |_, _, event, cx| { + if let Some((workspace, user_store)) = + workspace.upgrade(cx).zip(user_store.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| match event.kind { + ContactEventKind::Requested | ContactEventKind::Accepted => workspace + .show_notification( cx.add_view(|cx| { - IncomingRequestNotification::new( - user.clone(), - user_store.clone(), - cx, - ) + ContactNotification::new(event.clone(), user_store, cx) }), cx, - ) - }) - } + ), + _ => {} + }); } - _ => {} } }) .detach(); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c92b8c5c00..43467bb61a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -877,6 +877,7 @@ message Contact { uint64 user_id = 1; repeated ProjectMetadata projects = 2; bool online = 3; + bool should_notify = 4; } message ProjectMetadata { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5575dce9e7..1907bb1693 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -29,7 +29,7 @@ pub struct Theme { pub search: Search, pub project_diagnostics: ProjectDiagnostics, pub breadcrumbs: ContainedText, - pub incoming_request_notification: IncomingRequestNotification, + pub contact_notification: ContactNotification, } #[derive(Deserialize, Default)] @@ -357,7 +357,7 @@ pub struct ProjectDiagnostics { } #[derive(Deserialize, Default)] -pub struct IncomingRequestNotification { +pub struct ContactNotification { pub header_avatar: ImageStyle, pub header_message: ContainedText, pub header_height: f32, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index b4b9ffe383..8483597027 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -10,7 +10,7 @@ import search from "./search"; import picker from "./picker"; import workspace from "./workspace"; import projectDiagnostics from "./projectDiagnostics"; -import incomingRequestNotification from "./incomingRequestNotification"; +import contactNotification from "./contactNotification"; export const panel = { padding: { top: 12, left: 12, bottom: 12, right: 12 }, @@ -34,6 +34,6 @@ export default function app(theme: Theme): Object { left: 6, }, }, - incomingRequestNotification: incomingRequestNotification(theme), + contactNotification: contactNotification(theme), }; } diff --git a/styles/src/styleTree/incomingRequestNotification.ts b/styles/src/styleTree/contactNotification.ts similarity index 91% rename from styles/src/styleTree/incomingRequestNotification.ts rename to styles/src/styleTree/contactNotification.ts index 17cfad80d6..13e19df90b 100644 --- a/styles/src/styleTree/incomingRequestNotification.ts +++ b/styles/src/styleTree/contactNotification.ts @@ -1,7 +1,7 @@ import Theme from "../themes/theme"; import { backgroundColor, iconColor, text } from "./components"; -export default function incomingRequestNotification(theme: Theme): Object { +export default function contactNotification(theme: Theme): Object { return { headerAvatar: { height: 12, From 0ba656aa0ea69f25630ac8a11130633e71a7cc9b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 May 2022 14:20:05 -0700 Subject: [PATCH 12/12] Improve layout and styling of contact notifications Co-authored-by: Nathan Sobo --- assets/icons/{reject.svg => decline.svg} | 0 assets/themes/cave-dark.json | 14 +++- assets/themes/cave-light.json | 14 +++- assets/themes/dark.json | 14 +++- assets/themes/light.json | 14 +++- assets/themes/solarized-dark.json | 14 +++- assets/themes/solarized-light.json | 14 +++- assets/themes/sulphurpool-dark.json | 14 +++- assets/themes/sulphurpool-light.json | 14 +++- crates/client/src/user.rs | 2 +- crates/contacts_panel/src/contact_finder.rs | 2 +- .../src/contact_notification.rs | 67 +++++++++++-------- crates/contacts_panel/src/contacts_panel.rs | 8 +-- crates/rpc/proto/zed.proto | 2 +- crates/theme/src/theme.rs | 4 +- crates/workspace/src/workspace.rs | 2 +- styles/src/styleTree/contactNotification.ts | 17 +++-- styles/src/styleTree/workspace.ts | 2 +- 18 files changed, 152 insertions(+), 66 deletions(-) rename assets/icons/{reject.svg => decline.svg} (100%) diff --git a/assets/icons/reject.svg b/assets/icons/decline.svg similarity index 100% rename from assets/icons/reject.svg rename to assets/icons/decline.svg diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index ae8e32e945..bdc611b830 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -505,7 +505,7 @@ } }, "notifications": { - "width": 256, + "width": 380, "margin": { "right": 10, "bottom": 10 @@ -1698,7 +1698,8 @@ "color": "#e2dfe7", "size": 12, "margin": { - "left": 4 + "left": 8, + "right": 8 } }, "header_height": 18, @@ -1707,6 +1708,7 @@ "color": "#8b8792", "size": 12, "margin": { + "left": 20, "top": 6, "bottom": 6 } @@ -1720,6 +1722,9 @@ "corner_radius": 6, "margin": { "left": 6 + }, + "hover": { + "background": "#26232a3d" } }, "dismiss_button": { @@ -1727,7 +1732,10 @@ "icon_width": 8, "icon_height": 8, "button_width": 8, - "button_height": 8 + "button_height": 8, + "hover": { + "color": "#e2dfe7" + } } } } \ No newline at end of file diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index bf444d4758..b97b7f13fd 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -505,7 +505,7 @@ } }, "notifications": { - "width": 256, + "width": 380, "margin": { "right": 10, "bottom": 10 @@ -1698,7 +1698,8 @@ "color": "#26232a", "size": 12, "margin": { - "left": 4 + "left": 8, + "right": 8 } }, "header_height": 18, @@ -1707,6 +1708,7 @@ "color": "#585260", "size": 12, "margin": { + "left": 20, "top": 6, "bottom": 6 } @@ -1720,6 +1722,9 @@ "corner_radius": 6, "margin": { "left": 6 + }, + "hover": { + "background": "#e2dfe71f" } }, "dismiss_button": { @@ -1727,7 +1732,10 @@ "icon_width": 8, "icon_height": 8, "button_width": 8, - "button_height": 8 + "button_height": 8, + "hover": { + "color": "#26232a" + } } } } \ No newline at end of file diff --git a/assets/themes/dark.json b/assets/themes/dark.json index b4b86e2f49..37cb0a80bb 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -505,7 +505,7 @@ } }, "notifications": { - "width": 256, + "width": 380, "margin": { "right": 10, "bottom": 10 @@ -1698,7 +1698,8 @@ "color": "#f1f1f1", "size": 12, "margin": { - "left": 4 + "left": 8, + "right": 8 } }, "header_height": 18, @@ -1707,6 +1708,7 @@ "color": "#9c9c9c", "size": 12, "margin": { + "left": 20, "top": 6, "bottom": 6 } @@ -1720,6 +1722,9 @@ "corner_radius": 6, "margin": { "left": 6 + }, + "hover": { + "background": "#070707" } }, "dismiss_button": { @@ -1727,7 +1732,10 @@ "icon_width": 8, "icon_height": 8, "button_width": 8, - "button_height": 8 + "button_height": 8, + "hover": { + "color": "#c6c6c6" + } } } } \ No newline at end of file diff --git a/assets/themes/light.json b/assets/themes/light.json index 0ac7535acb..7491b039b9 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -505,7 +505,7 @@ } }, "notifications": { - "width": 256, + "width": 380, "margin": { "right": 10, "bottom": 10 @@ -1698,7 +1698,8 @@ "color": "#2b2b2b", "size": 12, "margin": { - "left": 4 + "left": 8, + "right": 8 } }, "header_height": 18, @@ -1707,6 +1708,7 @@ "color": "#474747", "size": 12, "margin": { + "left": 20, "top": 6, "bottom": 6 } @@ -1720,6 +1722,9 @@ "corner_radius": 6, "margin": { "left": 6 + }, + "hover": { + "background": "#e3e3e3" } }, "dismiss_button": { @@ -1727,7 +1732,10 @@ "icon_width": 8, "icon_height": 8, "button_width": 8, - "button_height": 8 + "button_height": 8, + "hover": { + "color": "#393939" + } } } } \ No newline at end of file diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index e9c50a1eae..b1fc074ff7 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -505,7 +505,7 @@ } }, "notifications": { - "width": 256, + "width": 380, "margin": { "right": 10, "bottom": 10 @@ -1698,7 +1698,8 @@ "color": "#eee8d5", "size": 12, "margin": { - "left": 4 + "left": 8, + "right": 8 } }, "header_height": 18, @@ -1707,6 +1708,7 @@ "color": "#93a1a1", "size": 12, "margin": { + "left": 20, "top": 6, "bottom": 6 } @@ -1720,6 +1722,9 @@ "corner_radius": 6, "margin": { "left": 6 + }, + "hover": { + "background": "#0736423d" } }, "dismiss_button": { @@ -1727,7 +1732,10 @@ "icon_width": 8, "icon_height": 8, "button_width": 8, - "button_height": 8 + "button_height": 8, + "hover": { + "color": "#eee8d5" + } } } } \ No newline at end of file diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 1ef6a68351..72e12bcb5b 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -505,7 +505,7 @@ } }, "notifications": { - "width": 256, + "width": 380, "margin": { "right": 10, "bottom": 10 @@ -1698,7 +1698,8 @@ "color": "#073642", "size": 12, "margin": { - "left": 4 + "left": 8, + "right": 8 } }, "header_height": 18, @@ -1707,6 +1708,7 @@ "color": "#586e75", "size": 12, "margin": { + "left": 20, "top": 6, "bottom": 6 } @@ -1720,6 +1722,9 @@ "corner_radius": 6, "margin": { "left": 6 + }, + "hover": { + "background": "#eee8d51f" } }, "dismiss_button": { @@ -1727,7 +1732,10 @@ "icon_width": 8, "icon_height": 8, "button_width": 8, - "button_height": 8 + "button_height": 8, + "hover": { + "color": "#073642" + } } } } \ No newline at end of file diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 37264cd203..26568e91b2 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -505,7 +505,7 @@ } }, "notifications": { - "width": 256, + "width": 380, "margin": { "right": 10, "bottom": 10 @@ -1698,7 +1698,8 @@ "color": "#dfe2f1", "size": 12, "margin": { - "left": 4 + "left": 8, + "right": 8 } }, "header_height": 18, @@ -1707,6 +1708,7 @@ "color": "#979db4", "size": 12, "margin": { + "left": 20, "top": 6, "bottom": 6 } @@ -1720,6 +1722,9 @@ "corner_radius": 6, "margin": { "left": 6 + }, + "hover": { + "background": "#2932563d" } }, "dismiss_button": { @@ -1727,7 +1732,10 @@ "icon_width": 8, "icon_height": 8, "button_width": 8, - "button_height": 8 + "button_height": 8, + "hover": { + "color": "#dfe2f1" + } } } } \ No newline at end of file diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index d5adf576dd..031d6652b9 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -505,7 +505,7 @@ } }, "notifications": { - "width": 256, + "width": 380, "margin": { "right": 10, "bottom": 10 @@ -1698,7 +1698,8 @@ "color": "#293256", "size": 12, "margin": { - "left": 4 + "left": 8, + "right": 8 } }, "header_height": 18, @@ -1707,6 +1708,7 @@ "color": "#5e6687", "size": 12, "margin": { + "left": 20, "top": 6, "bottom": 6 } @@ -1720,6 +1722,9 @@ "corner_radius": 6, "margin": { "left": 6 + }, + "hover": { + "background": "#dfe2f11f" } }, "dismiss_button": { @@ -1727,7 +1732,10 @@ "icon_width": 8, "icon_height": 8, "button_width": 8, - "button_height": 8 + "button_height": 8, + "hover": { + "color": "#293256" + } } } } \ No newline at end of file diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 7de32e8077..84254da73a 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -371,7 +371,7 @@ impl UserStore { response: if accept { proto::ContactRequestResponse::Accept } else { - proto::ContactRequestResponse::Reject + proto::ContactRequestResponse::Decline } as i32, }, cx, diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/contacts_panel/src/contact_finder.rs index 3b88eaf117..18e17a93d9 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/contacts_panel/src/contact_finder.rs @@ -118,7 +118,7 @@ impl PickerDelegate for ContactFinder { "icons/accept.svg" } ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => { - "icons/reject.svg" + "icons/decline.svg" } }; let button_style = if self.user_store.read(cx).is_contact_request_pending(&user) { diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/contacts_panel/src/contact_notification.rs index cf3b9aa559..6369f70ce0 100644 --- a/crates/contacts_panel/src/contact_notification.rs +++ b/crates/contacts_panel/src/contact_notification.rs @@ -6,6 +6,8 @@ use gpui::{ use settings::Settings; use workspace::Notification; +use crate::render_icon_button; + impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); pub fn init(cx: &mut MutableAppContext) { @@ -31,7 +33,7 @@ pub enum Event { Dismiss, } -enum Reject {} +enum Decline {} enum Accept {} impl Entity for ContactNotification { @@ -87,7 +89,7 @@ impl ContactNotification { let user_id = user.id; Flex::column() - .with_child(self.render_header("added you", theme, cx)) + .with_child(self.render_header("wants to add you as a contact.", theme, cx)) .with_child( Label::new( "They won't know if you decline.".to_string(), @@ -100,13 +102,14 @@ impl ContactNotification { .with_child( Flex::row() .with_child( - MouseEventHandler::new::( + MouseEventHandler::new::( self.event.user.id as usize, cx, - |_, _| { - Label::new("Reject".to_string(), theme.button.text.clone()) + |state, _| { + let button = theme.button.style_for(state, false); + Label::new("Decline".to_string(), button.text.clone()) .contained() - .with_style(theme.button.container) + .with_style(button.container) .boxed() }, ) @@ -120,10 +123,11 @@ impl ContactNotification { .boxed(), ) .with_child( - MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Label::new("Accept".to_string(), theme.button.text.clone()) + MouseEventHandler::new::(user.id as usize, cx, |state, _| { + let button = theme.button.style_for(state, false); + Label::new("Accept".to_string(), button.text.clone()) .contained() - .with_style(theme.button.container) + .with_style(button.container) .boxed() }) .with_cursor_style(CursorStyle::PointingHand) @@ -163,42 +167,51 @@ impl ContactNotification { Image::new(avatar) .with_style(theme.header_avatar) .aligned() - .left() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() .boxed() })) .with_child( - Label::new( + Text::new( format!("{} {}", user.github_login, message), theme.header_message.text.clone(), ) .contained() .with_style(theme.header_message.container) .aligned() + .top() + .left() + .flex(1., true) .boxed(), ) .with_child( - MouseEventHandler::new::(user.id as usize, cx, |_, _| { - Svg::new("icons/reject.svg") - .with_color(theme.dismiss_button.color) - .constrained() - .with_width(theme.dismiss_button.icon_width) - .aligned() - .contained() - .with_style(theme.dismiss_button.container) - .constrained() - .with_width(theme.dismiss_button.button_width) - .with_height(theme.dismiss_button.button_width) - .aligned() - .boxed() + MouseEventHandler::new::(user.id as usize, cx, |state, _| { + render_icon_button( + theme.dismiss_button.style_for(state, false), + "icons/decline.svg", + ) + .boxed() }) .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(5.)) .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() .flex_float() .boxed(), ) - .constrained() - .with_height(theme.header_height) - .boxed() + .named("contact notification header") } fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 3a8a9605f3..e26e64f9f6 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -344,7 +344,7 @@ impl ContactsPanel { is_incoming: bool, cx: &mut LayoutContext, ) -> ElementBox { - enum Reject {} + enum Decline {} enum Accept {} enum Cancel {} @@ -373,13 +373,13 @@ impl ContactsPanel { if is_incoming { row.add_children([ - MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_contact_button } else { &theme.contact_button.style_for(mouse_state, false) }; - render_icon_button(button_style, "icons/reject.svg") + render_icon_button(button_style, "icons/decline.svg") .aligned() .flex_float() .boxed() @@ -421,7 +421,7 @@ impl ContactsPanel { } else { &theme.contact_button.style_for(mouse_state, false) }; - render_icon_button(button_style, "icons/reject.svg") + render_icon_button(button_style, "icons/decline.svg") .aligned() .flex_float() .boxed() diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 43467bb61a..12ff05c757 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -564,7 +564,7 @@ message RespondToContactRequest { enum ContactRequestResponse { Accept = 0; - Reject = 1; + Decline = 1; Block = 2; Dismiss = 3; } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1907bb1693..9875f97498 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -362,8 +362,8 @@ pub struct ContactNotification { pub header_message: ContainedText, pub header_height: f32, pub body_message: ContainedText, - pub button: ContainedText, - pub dismiss_button: IconButton, + pub button: Interactive, + pub dismiss_button: Interactive, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 68ecfa8903..21d5581640 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1769,7 +1769,7 @@ impl Workspace { .boxed() })) .constrained() - .with_width(250.) + .with_width(theme.notifications.width) .contained() .with_style(theme.notifications.container) .aligned() diff --git a/styles/src/styleTree/contactNotification.ts b/styles/src/styleTree/contactNotification.ts index 13e19df90b..09360f2f91 100644 --- a/styles/src/styleTree/contactNotification.ts +++ b/styles/src/styleTree/contactNotification.ts @@ -1,21 +1,24 @@ import Theme from "../themes/theme"; import { backgroundColor, iconColor, text } from "./components"; +const avatarSize = 12; +const headerPadding = 8; + export default function contactNotification(theme: Theme): Object { return { headerAvatar: { - height: 12, - width: 12, + height: avatarSize, + width: avatarSize, cornerRadius: 6, }, headerMessage: { ...text(theme, "sans", "primary", { size: "xs" }), - margin: { left: 4 } + margin: { left: headerPadding, right: headerPadding } }, headerHeight: 18, bodyMessage: { ...text(theme, "sans", "secondary", { size: "xs" }), - margin: { top: 6, bottom: 6 }, + margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 }, }, button: { ...text(theme, "sans", "primary", { size: "xs" }), @@ -23,6 +26,9 @@ export default function contactNotification(theme: Theme): Object { padding: 4, cornerRadius: 6, margin: { left: 6 }, + hover: { + background: backgroundColor(theme, "on300", "hovered") + } }, dismissButton: { color: iconColor(theme, "secondary"), @@ -30,6 +36,9 @@ export default function contactNotification(theme: Theme): Object { iconHeight: 8, buttonWidth: 8, buttonHeight: 8, + hover: { + color: iconColor(theme, "primary") + } } } } \ No newline at end of file diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 326b07b9ee..65564f5cbc 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -159,7 +159,7 @@ export default function workspace(theme: Theme) { shadow: shadow(theme), }, notifications: { - width: 256, + width: 380, margin: { right: 10, bottom: 10 }, } };