Merge branch 'main' into contact-panel-keyboard-nav
This commit is contained in:
commit
f54d74eda9
44 changed files with 1808 additions and 269 deletions
|
@ -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
|
||||
|
|
237
crates/contacts_panel/src/contact_notification.rs
Normal file
237
crates/contacts_panel/src/contact_notification.rs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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![],
|
||||
},
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue