From 9c68c3e8a9f5bb0c61dbf58d14f91556b9851800 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 May 2022 16:46:53 -0600 Subject: [PATCH 01/10] 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 b6058c01e5ce2b453308acf2e0e0c4700b0f8d98..f724cc19a611866e6252af79c9e67ac1d61dff6c 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 5a480911d4a5ae3f6f2855da23bffdba29bf7216..3b88eaf11797c5b6eaa8b56ec391cf50067e2500 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 a63ff7b0bd18c6270963bbaf759940b1042cbdde..e85147d7e2a9b5a20e86ebd20fcce87f07be073d 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 3f8aa933ba483e3a3d989ef753b3f8cabd42ce10..9e2c79c5dca5596ac372c4cf6fabad4d839a85f2 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 5658cf201197ad0db637f6a11d956e6b9854671f..f5057ba39d37232a0b39758d6d25ce2531850aeb 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 157ea8ef7380795d318430bffbd2dc989a3b6596..5322a8924aa209969cc4d7ec611dc765ac4e072e 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 1904ed89d90292c6ed176ed512748e2a4f72bd7e..718268788c39f17f32ca993ca5e9ce2f2b9fb4ce 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 d5b0bf2ed54f8c999fa669e72a44dc8d1c7595eb..688accfcb9dc15f29f020aaad5c4f1ee00db108d 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/10] 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 cb1208a1db764bbb540dc73540eb723bfded2d91..3ced0536218e713a2d46ff5b12baa1ad3f29e077 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 f0b3f5bd438a53178d9f56f88e8fbf1dbd41472d..a0a19149dfddb6f516c9c19895565e6c41353963 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 9cc3badc8104dbee88d2862ad7feeedba22383e8..cbff53933e5a1077208a9284cbe96522b5eef07b 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 e2563fadad64d74d0b7add301cbfb2fc0969be6d..c7331a083b416365288861915c27fadd57a75b55 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 6e8c405b6c212bf77fc99c99ea3c2a6dcf5a2f07..d3367a3a3f570b3f1c37e70113145e69dd27c6b7 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 3f5b26ee56a24a883fc21c45a9de60bec20b9a5d..d0c1f4e6e8e52b2d9e6edc6b9db9714e1152edc7 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 0f2a868f24d028be4c960db27b39b36b540a3e5d..05541f91f855d98bf1bfe365f9ff3a799f32606d 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 b9106c62f3d273a537616f0cbd38090b8c854411..1a9408b17f4cab439dab99ed760a03a902cd0c4a 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 bb835c66401d595607546fbca5453d85461c2164..460e01c527bddca2145f5f0fbf284403fdeb952a 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 72db11c4931436ad6bf90650561d3baa5fe1eef1..58cdc7fc54e596a6af5ac3177ee000f18f61f119 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 688accfcb9dc15f29f020aaad5c4f1ee00db108d..94b0a82f539dab79d8ca4386e436513d702072b2 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 f74715ac0b928d3162a526e3b3ee18617e7efd37..72547627fabf4e3dd7d0d97e0304e7ab194b7ec1 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/10] 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 1874822774a36ef2c7c777bd5e7d91f2d5e72b71..3a2ea1a725ef3bd203e5cd22445bfab47125f77d 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 5d96a1b0c20f351c659e83256eb30610cde9ba10..792aeb1e22d346eaf404357f527af6e03a860fed 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 0000000000000000000000000000000000000000..d2ef5176e3a52b47926daf7190050cf2fbe8e596 --- /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 94b0a82f539dab79d8ca4386e436513d702072b2..f0e39126cc50827eb451048c1164cabc6516d2f2 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 77e400e02f96d276002018c63048a1fd133d5b2d..d4938501b81c04ee329765056857a9266cecedd8 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/10] 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 3a2ea1a725ef3bd203e5cd22445bfab47125f77d..a0f088429448350f3af4aa69c255f0c52c38eca3 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 0000000000000000000000000000000000000000..894c1f4138e3d3a3549fe6bcd3dd8aad027439f6 --- /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 792aeb1e22d346eaf404357f527af6e03a860fed..68fb8e1f26306ad17aa88e91b70e10b28e4a2c26 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 d2ef5176e3a52b47926daf7190050cf2fbe8e596..0000000000000000000000000000000000000000 --- 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 f0e39126cc50827eb451048c1164cabc6516d2f2..b077b82518225e0b81173fbca74bd37c0ef309f7 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/10] 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 | 63 +++++----- .../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, 689 insertions(+), 53 deletions(-) create mode 100644 styles/src/styleTree/incomingRequestNotification.ts diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 3ced0536218e713a2d46ff5b12baa1ad3f29e077..e8d03c60e0f158e22ef75586dfb4dc3f4fd1c1a2 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 a0a19149dfddb6f516c9c19895565e6c41353963..de7de76670211674b16084e2014c4f95eb7f62a4 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 cbff53933e5a1077208a9284cbe96522b5eef07b..78fa14aa5de5eec18ad5c12a98fdddcce7ee90a4 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 c7331a083b416365288861915c27fadd57a75b55..61923bfec5bbc9038c3b1b0af1ca8d0ae284f731 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 d3367a3a3f570b3f1c37e70113145e69dd27c6b7..9865c125865fe791111c336c3a70c200e993094d 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 d0c1f4e6e8e52b2d9e6edc6b9db9714e1152edc7..c61d354d35f8e7f3f29b3ee0f359f20b4aae6745 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 05541f91f855d98bf1bfe365f9ff3a799f32606d..907ec58cc0cf2000e9cb69fac9bff22bc010523e 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 1a9408b17f4cab439dab99ed760a03a902cd0c4a..3ae43250f0328ffbad67f7d632f4fa795c5e375e 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 a0f088429448350f3af4aa69c255f0c52c38eca3..4d5f44c320801514a28a02bda5085a37ccfa296d 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 33d1d526775f841e98e24e154d1ae29dcf54292e..0a34e75b3a7dc6a31fe5841dfa788bac56dd0aa0 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?; - - 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())?; - } + 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())?; + } - // 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 894c1f4138e3d3a3549fe6bcd3dd8aad027439f6..3b31528b71dfd4a21c56a5172c48bf44a8e332a2 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 68fb8e1f26306ad17aa88e91b70e10b28e4a2c26..333de8c3d5de35ccd73c4c4a3d1c4ab8843f8127 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 8adba5fc80c10dbac3413e803750e086a9ba8563..c92b8c5c00f4905feb362767bcf741ea78bddd21 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 58cdc7fc54e596a6af5ac3177ee000f18f61f119..aeb656828ec228ff502431e4f5b7f0535e258feb 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 0da6ada222d77ba1ec2cdbe7b1446ef76535cf05..b4b9ffe3838a9fe908b9a4f2d1e0be6461fbae12 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 0000000000000000000000000000000000000000..17cfad80d6e6415c27630d60f13c00619da3756f --- /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 72547627fabf4e3dd7d0d97e0304e7ab194b7ec1..1d4b78944fbb1945eb84baab1108162c290fe1f8 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/10] 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 3b31528b71dfd4a21c56a5172c48bf44a8e332a2..e5fff481b0e8ac8a0b1975401d56ae9640030b09 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/10] 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 e8d03c60e0f158e22ef75586dfb4dc3f4fd1c1a2..acb5315ddaac226dee41a1ff86103c2acd284440 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 de7de76670211674b16084e2014c4f95eb7f62a4..5d75efa22a70967326832825ff8fc4730e518dab 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 78fa14aa5de5eec18ad5c12a98fdddcce7ee90a4..393b5b20d84547a12cc9671082bc45643f419117 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 61923bfec5bbc9038c3b1b0af1ca8d0ae284f731..851886982514a8930cc75eab3d9891bebb241638 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 9865c125865fe791111c336c3a70c200e993094d..6ce85a9ee88dacbc5ee9ece35c635e0c4e69dcb7 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 c61d354d35f8e7f3f29b3ee0f359f20b4aae6745..a3bc6b8597743483c73fc2379613f836669510b9 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 907ec58cc0cf2000e9cb69fac9bff22bc010523e..68657b31c2a35fccfdf65b7f314f6cda8c19d9cf 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 3ae43250f0328ffbad67f7d632f4fa795c5e375e..18e4b99363633f4980c97140ff30f1d30184767d 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 0a34e75b3a7dc6a31fe5841dfa788bac56dd0aa0..8cd4b6387c640291f47a9e7ac826bbc0b767e585 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 333de8c3d5de35ccd73c4c4a3d1c4ab8843f8127..003f3885b192ffece610e9f17a368e89cd2b446b 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 90b21231639665aec813967e9595d2673437d777..afe24127b58ef6eadc4acf20801d95011ca1036b 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 61c97f281d327f01f1cad86bb81dac7bca92bc0f..639d7b44d9c7f1d89d6af6486842a258765ab3d1 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 aeb656828ec228ff502431e4f5b7f0535e258feb..5575dce9e7af0b73ac1d566d7a061bdd2af05188 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 c9cbcbb4fb073367c04ca63b9d951310a0ecaed4..366c74e43f7a3bcd31dd44cf6ee0465b07f66558 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 b077b82518225e0b81173fbca74bd37c0ef309f7..68ecfa8903638eb26f180c6f10c1536aeaad9977 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 621b77639ea5f7a89f78d3756811fff7d41b6101..c7b7c6a0a35b3138e77d648b042344f98c507ff2 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 1d4b78944fbb1945eb84baab1108162c290fe1f8..326b07b9eef4eed9ba98b116d56eaeb422ff3f54 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 a5fd664b00ffa26a901dee0716f425d6e72982c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 11 May 2022 18:51:40 +0200 Subject: [PATCH 08/10] 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 4bb61c34046df885acb4a97960f0392da275f093..056f94ecfe7d099b6d07f42fb8ded114435be397 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 8cd4b6387c640291f47a9e7ac826bbc0b767e585..4bf06fe7a3ffe243412af2283fb957160cb7e40c 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 8ca270622832b4fc23151508d3df4a4fa9f013c8..9f56c95a47ca8958a2a51d0e04bcad685647a31c 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 3bc9b8ec856b383057fae40a150cb25409f060aa Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 May 2022 11:39:01 -0700 Subject: [PATCH 09/10] 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 acb5315ddaac226dee41a1ff86103c2acd284440..ae8e32e945d377cdc07fbd8f26f86b0c80d331f4 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 5d75efa22a70967326832825ff8fc4730e518dab..bf444d4758fbc81e2c9b2b774bd703f0883357d8 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 393b5b20d84547a12cc9671082bc45643f419117..b4b86e2f4921be27385070701bc77d755cdefc6f 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 851886982514a8930cc75eab3d9891bebb241638..0ac7535acb88180f0d6df69f51babf3ea784a988 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 6ce85a9ee88dacbc5ee9ece35c635e0c4e69dcb7..e9c50a1eaebc4e2c96549cbe1b8b00a9168cf442 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 a3bc6b8597743483c73fc2379613f836669510b9..1ef6a6835198babdc2a5ff273bde5fef4dc06bc3 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 68657b31c2a35fccfdf65b7f314f6cda8c19d9cf..37264cd20398aa866f10797eabd59411fd8f8e7b 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 18e4b99363633f4980c97140ff30f1d30184767d..d5adf576ddc00a9ed8e273b02a6c7173bd42df06 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 4d5f44c320801514a28a02bda5085a37ccfa296d..7de32e80774ee64dae3e37f1739ed3f427d52bb7 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 4bf06fe7a3ffe243412af2283fb957160cb7e40c..ecd384794551e46f0d42f34072b79f2b32962d2b 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 9f56c95a47ca8958a2a51d0e04bcad685647a31c..4ab6df0adc9fd4bbdc62a2bd187abf5f029a12eb 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 0000000000000000000000000000000000000000..cf3b9aa5590f9ee97e818ab1368339ef1fd01c4d --- /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 e5fff481b0e8ac8a0b1975401d56ae9640030b09..0000000000000000000000000000000000000000 --- 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 003f3885b192ffece610e9f17a368e89cd2b446b..3a8a9605f31620f233f8b0b2d9071754b04f87b6 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 c92b8c5c00f4905feb362767bcf741ea78bddd21..43467bb61ae8f3e0faac373c014eba1315d65dc1 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 5575dce9e7af0b73ac1d566d7a061bdd2af05188..1907bb16934cf31d66bc3f71735cc57d3f1a4f53 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 b4b9ffe3838a9fe908b9a4f2d1e0be6461fbae12..84835970279e89de515547d6232bcf3dd647ffbf 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 17cfad80d6e6415c27630d60f13c00619da3756f..13e19df90bc07cdb28d79153f9dcf5d056856741 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 10/10] 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 ae8e32e945d377cdc07fbd8f26f86b0c80d331f4..bdc611b830065dfee72f0e0104eb08373579ee80 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 bf444d4758fbc81e2c9b2b774bd703f0883357d8..b97b7f13fd9a45238c0bee8a9408c775ec6dfd1b 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 b4b86e2f4921be27385070701bc77d755cdefc6f..37cb0a80bbde9b35a5e725c674380adad86a02d7 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 0ac7535acb88180f0d6df69f51babf3ea784a988..7491b039b91eb91b780f4a815a018d2eafde5549 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 e9c50a1eaebc4e2c96549cbe1b8b00a9168cf442..b1fc074ff73131023d55aeb5d78678164d488db4 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 1ef6a6835198babdc2a5ff273bde5fef4dc06bc3..72e12bcb5b5b00653056c79ca2d33caaebdf4bef 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 37264cd20398aa866f10797eabd59411fd8f8e7b..26568e91b2be123ca2005cc5c165736fab776f36 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 d5adf576ddc00a9ed8e273b02a6c7173bd42df06..031d6652b9f9acc8aa1c123d993b0dd7db231a77 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 7de32e80774ee64dae3e37f1739ed3f427d52bb7..84254da73a5bb0815acf3b66bf0cef112088f030 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 3b88eaf11797c5b6eaa8b56ec391cf50067e2500..18e17a93d9ba448f533d83a0a09701b5d3ae84cc 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 cf3b9aa5590f9ee97e818ab1368339ef1fd01c4d..6369f70ce0244fdd0a1a8540d4a91a0d86c0acbc 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 3a8a9605f31620f233f8b0b2d9071754b04f87b6..e26e64f9f6ee18353dd00163fe1008d52b384b92 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 43467bb61ae8f3e0faac373c014eba1315d65dc1..12ff05c7575ec53b1102ab253d4a9db08790b69a 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 1907bb16934cf31d66bc3f71735cc57d3f1a4f53..9875f974983706204b0287913cc2eb15402123ab 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 68ecfa8903638eb26f180c6f10c1536aeaad9977..21d5581640d8654f7192bb3d7b07d771fcf8ff2f 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 13e19df90bc07cdb28d79153f9dcf5d056856741..09360f2f9118876070a0700392f6fc2756b9e5c4 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 326b07b9eef4eed9ba98b116d56eaeb422ff3f54..65564f5cbc35299fd49e39656e97370798312084 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 }, } };