assets/themes/cave-dark.json 🔗
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
Max Brunsfeld and Nathan Sobo created
Co-authored-by: Nathan Sobo <nathan@zed.dev>
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
crates/contacts_panel/src/contact_notification.rs | 224 ++++++++++++++++
crates/contacts_panel/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
styles/src/styleTree/contactNotification.ts | 2
18 files changed, 301 insertions(+), 253 deletions(-)
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
@@ -1687,7 +1687,7 @@
"left": 6
}
},
- "incoming_request_notification": {
+ "contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,
@@ -54,13 +54,21 @@ pub struct UserStore {
_maintain_current_user: Task<()>,
}
-pub enum Event {
- ContactRequested(Arc<User>),
- ContactRequestCancelled(Arc<User>),
+#[derive(Clone)]
+pub struct ContactEvent {
+ pub user: Arc<User>,
+ 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
@@ -420,7 +420,7 @@ impl Server {
async fn update_user_contacts(self: &Arc<Server>, 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
@@ -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,
}
}
@@ -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<UserStore>,
+ 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<Self>) -> 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: &<Self as Entity>::Event) -> bool {
+ matches!(event, Event::Dismiss)
+ }
+}
+
+impl ContactNotification {
+ pub fn new(
+ event: ContactEvent,
+ user_store: ModelHandle<UserStore>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>) -> ElementBox {
+ let theme = cx.global::<Settings>().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::<Reject, _, _>(
+ 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::<Accept, _, _>(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<Self>) -> ElementBox {
+ let theme = cx.global::<Settings>().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<Self>,
+ ) -> 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::<Dismiss, _, _>(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>) {
+ 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>,
+ ) {
+ self.user_store
+ .update(cx, |store, cx| {
+ store.respond_to_contact_request(action.user_id, action.accept, cx)
+ })
+ .detach();
+ }
+}
@@ -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>,
- user_store: ModelHandle<UserStore>,
-}
-
-#[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<Self>) -> ElementBox {
- enum Dismiss {}
- enum Reject {}
- enum Accept {}
-
- let theme = cx.global::<Settings>().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::<Dismiss, _, _>(
- 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::<Reject, _, _>(
- 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::<Accept, _, _>(
- 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: &<Self as Entity>::Event) -> bool {
- matches!(event, Event::Dismiss)
- }
-}
-
-impl IncomingRequestNotification {
- pub fn new(
- user: Arc<User>,
- user_store: ModelHandle<UserStore>,
- cx: &mut ViewContext<Self>,
- ) -> 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>) {
- 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>,
- ) {
- self.user_store
- .update(cx, |store, cx| {
- store.respond_to_contact_request(action.user_id, action.accept, cx)
- })
- .detach();
- }
-}
@@ -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();
@@ -877,6 +877,7 @@ message Contact {
uint64 user_id = 1;
repeated ProjectMetadata projects = 2;
bool online = 3;
+ bool should_notify = 4;
}
message ProjectMetadata {
@@ -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,
@@ -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),
};
}
@@ -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,