diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index acb5315dda..ae8e32e945 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 5d75efa22a..bf444d4758 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 393b5b20d8..b4b86e2f49 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/light.json b/assets/themes/light.json index 8518869825..0ac7535acb 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 6ce85a9ee8..e9c50a1eae 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index a3bc6b8597..1ef6a68351 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 68657b31c2..37264cd203 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 18e4b99363..d5adf576dd 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -1687,7 +1687,7 @@ "left": 6 } }, - "incoming_request_notification": { + "contact_notification": { "header_avatar": { "height": 12, "width": 12, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 4d5f44c320..7de32e8077 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -54,13 +54,21 @@ pub struct UserStore { _maintain_current_user: Task<()>, } -pub enum Event { - ContactRequested(Arc), - ContactRequestCancelled(Arc), +#[derive(Clone)] +pub struct ContactEvent { + pub user: Arc, + pub kind: ContactEventKind, +} + +#[derive(Clone, Copy)] +pub enum ContactEventKind { + Requested, + Accepted, + Cancelled, } impl Entity for UserStore { - type Event = Event; + type Event = ContactEvent; } enum UpdateContacts { @@ -178,8 +186,10 @@ impl UserStore { // No need to paralellize here let mut updated_contacts = Vec::new(); for contact in message.contacts { - updated_contacts.push(Arc::new( - Contact::from_proto(contact, &this, &mut cx).await?, + let should_notify = contact.should_notify; + updated_contacts.push(( + Arc::new(Contact::from_proto(contact, &this, &mut cx).await?), + should_notify, )); } @@ -215,7 +225,13 @@ impl UserStore { this.contacts .retain(|contact| !removed_contacts.contains(&contact.user.id)); // Update existing contacts and insert new ones - for updated_contact in updated_contacts { + for (updated_contact, should_notify) in updated_contacts { + if should_notify { + cx.emit(ContactEvent { + user: updated_contact.user.clone(), + kind: ContactEventKind::Accepted, + }); + } match this.contacts.binary_search_by_key( &&updated_contact.user.github_login, |contact| &contact.user.github_login, @@ -228,7 +244,10 @@ impl UserStore { // Remove incoming contact requests this.incoming_contact_requests.retain(|user| { if removed_incoming_requests.contains(&user.id) { - cx.emit(Event::ContactRequestCancelled(user.clone())); + cx.emit(ContactEvent { + user: user.clone(), + kind: ContactEventKind::Cancelled, + }); false } else { true @@ -237,7 +256,10 @@ impl UserStore { // Update existing incoming requests and insert new ones for (user, should_notify) in incoming_requests { if should_notify { - cx.emit(Event::ContactRequested(user.clone())); + cx.emit(ContactEvent { + user: user.clone(), + kind: ContactEventKind::Requested, + }); } match this diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4bf06fe7a3..ecd3847945 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -420,7 +420,7 @@ impl Server { async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { let contacts = self.app_state.db.get_contacts(user_id).await?; let store = self.store().await; - let updated_contact = store.contact_for_user(user_id); + let updated_contact = store.contact_for_user(user_id, false); for contact in contacts { if let db::Contact::Accepted { user_id: contact_user_id, @@ -1049,7 +1049,9 @@ impl Server { // Update responder with new contact let mut update = proto::UpdateContacts::default(); if accept { - update.contacts.push(store.contact_for_user(requester_id)); + update + .contacts + .push(store.contact_for_user(requester_id, false)); } update .remove_incoming_requests @@ -1061,7 +1063,9 @@ impl Server { // Update requester with new contact let mut update = proto::UpdateContacts::default(); if accept { - update.contacts.push(store.contact_for_user(responder_id)); + update + .contacts + .push(store.contact_for_user(responder_id, true)); } update .remove_outgoing_requests diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 9f56c95a47..4ab6df0adc 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -225,8 +225,13 @@ impl Store { for contact in contacts { match contact { - db::Contact::Accepted { user_id, .. } => { - update.contacts.push(self.contact_for_user(user_id)); + db::Contact::Accepted { + user_id, + should_notify, + } => { + update + .contacts + .push(self.contact_for_user(user_id, should_notify)); } db::Contact::Outgoing { user_id } => { update.outgoing_requests.push(user_id.to_proto()) @@ -246,11 +251,12 @@ impl Store { update } - pub fn contact_for_user(&self, user_id: UserId) -> proto::Contact { + pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact { proto::Contact { user_id: user_id.to_proto(), projects: self.project_metadata_for_user(user_id), online: self.is_user_online(user_id), + should_notify, } } diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/contacts_panel/src/contact_notification.rs new file mode 100644 index 0000000000..cf3b9aa559 --- /dev/null +++ b/crates/contacts_panel/src/contact_notification.rs @@ -0,0 +1,224 @@ +use client::{ContactEvent, ContactEventKind, UserStore}; +use gpui::{ + elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle, + MutableAppContext, RenderContext, View, ViewContext, +}; +use settings::Settings; +use workspace::Notification; + +impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ContactNotification::dismiss); + cx.add_action(ContactNotification::respond_to_contact_request); +} + +pub struct ContactNotification { + user_store: ModelHandle, + event: ContactEvent, +} + +#[derive(Clone)] +struct Dismiss(u64); + +#[derive(Clone)] +pub struct RespondToContactRequest { + pub user_id: u64, + pub accept: bool, +} + +pub enum Event { + Dismiss, +} + +enum Reject {} +enum Accept {} + +impl Entity for ContactNotification { + type Event = Event; +} + +impl View for ContactNotification { + fn ui_name() -> &'static str { + "ContactNotification" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + match self.event.kind { + ContactEventKind::Requested => self.render_incoming_request(cx), + ContactEventKind::Accepted => self.render_acceptance(cx), + _ => unreachable!(), + } + } +} + +impl Notification for ContactNotification { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + matches!(event, Event::Dismiss) + } +} + +impl ContactNotification { + pub fn new( + event: ContactEvent, + user_store: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + cx.subscribe(&user_store, move |this, _, event, cx| { + if let client::ContactEvent { + kind: ContactEventKind::Cancelled, + user, + } = event + { + if user.id == this.event.user.id { + cx.emit(Event::Dismiss); + } + } + }) + .detach(); + + Self { event, user_store } + } + + fn render_incoming_request(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = cx.global::().theme.clone(); + let theme = &theme.contact_notification; + let user = &self.event.user; + let user_id = user.id; + + Flex::column() + .with_child(self.render_header("added you", theme, cx)) + .with_child( + Label::new( + "They won't know if you decline.".to_string(), + theme.body_message.text.clone(), + ) + .contained() + .with_style(theme.body_message.container) + .boxed(), + ) + .with_child( + Flex::row() + .with_child( + MouseEventHandler::new::( + self.event.user.id as usize, + cx, + |_, _| { + Label::new("Reject".to_string(), theme.button.text.clone()) + .contained() + .with_style(theme.button.container) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }); + }) + .boxed(), + ) + .with_child( + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Label::new("Accept".to_string(), theme.button.text.clone()) + .contained() + .with_style(theme.button.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }); + }) + .boxed(), + ) + .aligned() + .right() + .boxed(), + ) + .contained() + .boxed() + } + + fn render_acceptance(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = cx.global::().theme.clone(); + let theme = &theme.contact_notification; + + self.render_header("accepted your contact request", theme, cx) + } + + fn render_header( + &self, + message: &'static str, + theme: &theme::ContactNotification, + cx: &mut RenderContext, + ) -> ElementBox { + let user = &self.event.user; + let user_id = user.id; + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.header_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + format!("{} {}", user.github_login, message), + theme.header_message.text.clone(), + ) + .contained() + .with_style(theme.header_message.container) + .aligned() + .boxed(), + ) + .with_child( + MouseEventHandler::new::(user.id as usize, cx, |_, _| { + Svg::new("icons/reject.svg") + .with_color(theme.dismiss_button.color) + .constrained() + .with_width(theme.dismiss_button.icon_width) + .aligned() + .contained() + .with_style(theme.dismiss_button.container) + .constrained() + .with_width(theme.dismiss_button.button_width) + .with_height(theme.dismiss_button.button_width) + .aligned() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) + .flex_float() + .boxed(), + ) + .constrained() + .with_height(theme.header_height) + .boxed() + } + + fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { + self.user_store.update(cx, |store, cx| { + store + .dismiss_contact_request(self.event.user.id, cx) + .detach_and_log_err(cx); + }); + cx.emit(Event::Dismiss); + } + + fn respond_to_contact_request( + &mut self, + action: &RespondToContactRequest, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(action.user_id, action.accept, cx) + }) + .detach(); + } +} diff --git a/crates/contacts_panel/src/contact_notifications.rs b/crates/contacts_panel/src/contact_notifications.rs deleted file mode 100644 index e5fff481b0..0000000000 --- a/crates/contacts_panel/src/contact_notifications.rs +++ /dev/null @@ -1,206 +0,0 @@ -use client::{User, UserStore}; -use gpui::{ - elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle, - MutableAppContext, RenderContext, View, ViewContext, -}; -use settings::Settings; -use std::sync::Arc; -use workspace::Notification; - -impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(IncomingRequestNotification::dismiss); - cx.add_action(IncomingRequestNotification::respond_to_contact_request); -} - -pub struct IncomingRequestNotification { - user: Arc, - user_store: ModelHandle, -} - -#[derive(Clone)] -struct Dismiss(u64); - -#[derive(Clone)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, -} - -pub enum Event { - Dismiss, -} - -impl Entity for IncomingRequestNotification { - type Event = Event; -} - -impl View for IncomingRequestNotification { - fn ui_name() -> &'static str { - "IncomingRequestNotification" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - enum Dismiss {} - enum Reject {} - enum Accept {} - - let theme = cx.global::().theme.clone(); - let theme = &theme.incoming_request_notification; - let user_id = self.user.id; - - Flex::column() - .with_child( - Flex::row() - .with_children(self.user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.header_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - format!("{} added you", self.user.github_login), - theme.header_message.text.clone(), - ) - .contained() - .with_style(theme.header_message.container) - .aligned() - .boxed(), - ) - .with_child( - MouseEventHandler::new::( - self.user.id as usize, - cx, - |_, _| { - Svg::new("icons/reject.svg") - .with_color(theme.dismiss_button.color) - .constrained() - .with_width(theme.dismiss_button.icon_width) - .aligned() - .contained() - .with_style(theme.dismiss_button.container) - .constrained() - .with_width(theme.dismiss_button.button_width) - .with_height(theme.dismiss_button.button_width) - .aligned() - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) - .flex_float() - .boxed(), - ) - .constrained() - .with_height(theme.header_height) - .boxed(), - ) - .with_child( - Label::new( - "They won't know if you decline.".to_string(), - theme.body_message.text.clone(), - ) - .contained() - .with_style(theme.body_message.container) - .boxed(), - ) - .with_child( - Flex::row() - .with_child( - MouseEventHandler::new::( - self.user.id as usize, - cx, - |_, _| { - Label::new("Reject".to_string(), theme.button.text.clone()) - .contained() - .with_style(theme.button.container) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }); - }) - .boxed(), - ) - .with_child( - MouseEventHandler::new::( - self.user.id as usize, - cx, - |_, _| { - Label::new("Accept".to_string(), theme.button.text.clone()) - .contained() - .with_style(theme.button.container) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }); - }) - .boxed(), - ) - .aligned() - .right() - .boxed(), - ) - .contained() - .boxed() - } -} - -impl Notification for IncomingRequestNotification { - fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { - matches!(event, Event::Dismiss) - } -} - -impl IncomingRequestNotification { - pub fn new( - user: Arc, - user_store: ModelHandle, - cx: &mut ViewContext, - ) -> Self { - let user_id = user.id; - cx.subscribe(&user_store, move |_, _, event, cx| { - if let client::Event::ContactRequestCancelled(user) = event { - if user.id == user_id { - cx.emit(Event::Dismiss); - } - } - }) - .detach(); - - Self { user, user_store } - } - - fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { - self.user_store.update(cx, |store, cx| { - store - .dismiss_contact_request(self.user.id, cx) - .detach_and_log_err(cx); - }); - cx.emit(Event::Dismiss); - } - - fn respond_to_contact_request( - &mut self, - action: &RespondToContactRequest, - cx: &mut ViewContext, - ) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(action.user_id, action.accept, cx) - }) - .detach(); - } -} diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 003f3885b1..3a8a9605f3 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,8 +1,8 @@ mod contact_finder; -mod contact_notifications; +mod contact_notification; -use client::{Contact, User, UserStore}; -use contact_notifications::IncomingRequestNotification; +use client::{Contact, ContactEventKind, User, UserStore}; +use contact_notification::ContactNotification; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -55,7 +55,7 @@ pub struct RespondToContactRequest { pub fn init(cx: &mut MutableAppContext) { contact_finder::init(cx); - contact_notifications::init(cx); + contact_notification::init(cx); cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); @@ -85,25 +85,22 @@ impl ContactsPanel { .detach(); cx.subscribe(&app_state.user_store, { - let user_store = app_state.user_store.clone(); - move |_, _, event, cx| match event { - client::Event::ContactRequested(user) => { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.show_notification( + let user_store = app_state.user_store.downgrade(); + move |_, _, event, cx| { + if let Some((workspace, user_store)) = + workspace.upgrade(cx).zip(user_store.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| match event.kind { + ContactEventKind::Requested | ContactEventKind::Accepted => workspace + .show_notification( cx.add_view(|cx| { - IncomingRequestNotification::new( - user.clone(), - user_store.clone(), - cx, - ) + ContactNotification::new(event.clone(), user_store, cx) }), cx, - ) - }) - } + ), + _ => {} + }); } - _ => {} } }) .detach(); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c92b8c5c00..43467bb61a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -877,6 +877,7 @@ message Contact { uint64 user_id = 1; repeated ProjectMetadata projects = 2; bool online = 3; + bool should_notify = 4; } message ProjectMetadata { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5575dce9e7..1907bb1693 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -29,7 +29,7 @@ pub struct Theme { pub search: Search, pub project_diagnostics: ProjectDiagnostics, pub breadcrumbs: ContainedText, - pub incoming_request_notification: IncomingRequestNotification, + pub contact_notification: ContactNotification, } #[derive(Deserialize, Default)] @@ -357,7 +357,7 @@ pub struct ProjectDiagnostics { } #[derive(Deserialize, Default)] -pub struct IncomingRequestNotification { +pub struct ContactNotification { pub header_avatar: ImageStyle, pub header_message: ContainedText, pub header_height: f32, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index b4b9ffe383..8483597027 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -10,7 +10,7 @@ import search from "./search"; import picker from "./picker"; import workspace from "./workspace"; import projectDiagnostics from "./projectDiagnostics"; -import incomingRequestNotification from "./incomingRequestNotification"; +import contactNotification from "./contactNotification"; export const panel = { padding: { top: 12, left: 12, bottom: 12, right: 12 }, @@ -34,6 +34,6 @@ export default function app(theme: Theme): Object { left: 6, }, }, - incomingRequestNotification: incomingRequestNotification(theme), + contactNotification: contactNotification(theme), }; } diff --git a/styles/src/styleTree/incomingRequestNotification.ts b/styles/src/styleTree/contactNotification.ts similarity index 91% rename from styles/src/styleTree/incomingRequestNotification.ts rename to styles/src/styleTree/contactNotification.ts index 17cfad80d6..13e19df90b 100644 --- a/styles/src/styleTree/incomingRequestNotification.ts +++ b/styles/src/styleTree/contactNotification.ts @@ -1,7 +1,7 @@ import Theme from "../themes/theme"; import { backgroundColor, iconColor, text } from "./components"; -export default function incomingRequestNotification(theme: Theme): Object { +export default function contactNotification(theme: Theme): Object { return { headerAvatar: { height: 12,