Merge branch 'main' into contact-panel-keyboard-nav

This commit is contained in:
Max Brunsfeld 2022-05-11 17:41:17 -07:00
commit f54d74eda9
44 changed files with 1808 additions and 269 deletions

View file

@ -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) {
@ -159,7 +159,7 @@ impl PickerDelegate for ContactFinder {
impl ContactFinder {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
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

View file

@ -0,0 +1,237 @@
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;
use crate::render_icon_button;
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 Decline {}
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("wants to add you as a contact.", 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::<Decline, _, _>(
self.event.user.id as usize,
cx,
|state, _| {
let button = theme.button.style_for(state, false);
Label::new("Decline".to_string(), button.text.clone())
.contained()
.with_style(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, |state, _| {
let button = theme.button.style_for(state, false);
Label::new("Accept".to_string(), button.text.clone())
.contained()
.with_style(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()
.constrained()
.with_height(
cx.font_cache()
.line_height(theme.header_message.text.font_size),
)
.aligned()
.top()
.boxed()
}))
.with_child(
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::<Dismiss, _, _>(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(),
)
.named("contact notification header")
}
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,6 +1,8 @@
mod contact_finder;
mod contact_notification;
use client::{Contact, User, UserStore};
use client::{Contact, ContactEventKind, User, UserStore};
use contact_notification::ContactNotification;
use editor::{Cancel, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
@ -8,15 +10,18 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f},
impl_actions,
platform::CursorStyle,
Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext,
Subscription, View, ViewContext, ViewHandle,
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::menu::{Confirm, SelectNext, SelectPrev};
use workspace::{AppState, JoinProject};
use workspace::{
menu::{Confirm, SelectNext, SelectPrev},
sidebar::SidebarItem,
AppState, JoinProject, Workspace,
};
impl_actions!(
contacts_panel,
@ -65,6 +70,7 @@ pub struct RespondToContactRequest {
pub fn init(cx: &mut MutableAppContext) {
contact_finder::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);
@ -75,7 +81,11 @@ pub fn init(cx: &mut MutableAppContext) {
}
impl ContactsPanel {
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(
app_state: Arc<AppState>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let user_query_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
@ -93,6 +103,27 @@ impl ContactsPanel {
})
.detach();
cx.subscribe(&app_state.user_store, {
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| {
ContactNotification::new(event.clone(), user_store, cx)
}),
cx,
),
_ => {}
});
}
}
})
.detach();
let mut this = Self {
list_state: ListState::new(0, Orientation::Top, 1000., {
let this = cx.weak_handle();
@ -382,7 +413,7 @@ impl ContactsPanel {
is_selected: bool,
cx: &mut LayoutContext,
) -> ElementBox {
enum Reject {}
enum Decline {}
enum Accept {}
enum Cancel {}
@ -413,13 +444,13 @@ impl ContactsPanel {
if is_incoming {
row.add_children([
MouseEventHandler::new::<Reject, _, _>(user.id as usize, cx, |mouse_state, _| {
MouseEventHandler::new::<Decline, _, _>(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()
@ -463,7 +494,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()
@ -707,6 +738,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)
@ -824,11 +865,16 @@ mod tests {
use gpui::TestAppContext;
use language::LanguageRegistry;
use theme::ThemeRegistry;
use workspace::WorkspaceParams;
#[gpui::test]
async fn test_contact_panel(cx: &mut TestAppContext) {
let (app_state, server) = init(cx).await;
let panel = cx.add_view(0, |cx| ContactsPanel::new(app_state.clone(), cx));
let workspace_params = cx.update(WorkspaceParams::test);
let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
let panel = cx.add_view(0, |cx| {
ContactsPanel::new(app_state.clone(), workspace.downgrade(), cx)
});
let get_users_request = server.receive::<proto::GetUsers>().await.unwrap();
server
@ -865,6 +911,7 @@ mod tests {
proto::Contact {
user_id: 3,
online: true,
should_notify: false,
projects: vec![proto::ProjectMetadata {
id: 101,
worktree_root_names: vec!["dir1".to_string()],
@ -875,6 +922,7 @@ mod tests {
proto::Contact {
user_id: 4,
online: true,
should_notify: false,
projects: vec![proto::ProjectMetadata {
id: 102,
worktree_root_names: vec!["dir2".to_string()],
@ -885,6 +933,7 @@ mod tests {
proto::Contact {
user_id: 5,
online: false,
should_notify: false,
projects: vec![],
},
],