Add notifications for accepted contact requests

Co-authored-by: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2022-05-11 11:39:01 -07:00
parent a5fd664b00
commit 3bc9b8ec85
18 changed files with 301 additions and 253 deletions

View File

@ -1687,7 +1687,7 @@
"left": 6
}
},
"incoming_request_notification": {
"contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,

View File

@ -1687,7 +1687,7 @@
"left": 6
}
},
"incoming_request_notification": {
"contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,

View File

@ -1687,7 +1687,7 @@
"left": 6
}
},
"incoming_request_notification": {
"contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,

View File

@ -1687,7 +1687,7 @@
"left": 6
}
},
"incoming_request_notification": {
"contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,

View File

@ -1687,7 +1687,7 @@
"left": 6
}
},
"incoming_request_notification": {
"contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,

View File

@ -1687,7 +1687,7 @@
"left": 6
}
},
"incoming_request_notification": {
"contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,

View File

@ -1687,7 +1687,7 @@
"left": 6
}
},
"incoming_request_notification": {
"contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,

View File

@ -1687,7 +1687,7 @@
"left": 6
}
},
"incoming_request_notification": {
"contact_notification": {
"header_avatar": {
"height": 12,
"width": 12,

View File

@ -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

View File

@ -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

View File

@ -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,
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();

View File

@ -877,6 +877,7 @@ message Contact {
uint64 user_id = 1;
repeated ProjectMetadata projects = 2;
bool online = 3;
bool should_notify = 4;
}
message ProjectMetadata {

View File

@ -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,

View File

@ -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),
};
}

View File

@ -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,