Detailed changes
@@ -1091,6 +1091,7 @@ dependencies = [
"gpui",
"log",
"menu",
+ "picker",
"postage",
"project",
"serde",
@@ -1141,30 +1142,6 @@ dependencies = [
"cache-padded",
]
-[[package]]
-name = "contacts_panel"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "collections",
- "editor",
- "futures",
- "fuzzy",
- "gpui",
- "language",
- "log",
- "menu",
- "picker",
- "postage",
- "project",
- "serde",
- "settings",
- "theme",
- "util",
- "workspace",
-]
-
[[package]]
name = "context_menu"
version = "0.1.0"
@@ -7165,7 +7142,6 @@ dependencies = [
"collab_ui",
"collections",
"command_palette",
- "contacts_panel",
"context_menu",
"ctor",
"diagnostics",
@@ -395,7 +395,6 @@
"context": "Workspace",
"bindings": {
"shift-escape": "dock::FocusDock",
- "cmd-shift-c": "contacts_panel::ToggleFocus",
"cmd-shift-b": "workspace::ToggleRightSidebar"
}
},
@@ -29,6 +29,7 @@ editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
+picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
@@ -1,6 +1,6 @@
-use crate::contacts_popover;
+use crate::{contact_notification::ContactNotification, contacts_popover};
use call::{ActiveCall, ParticipantLocation};
-use client::{Authenticate, PeerId};
+use client::{Authenticate, ContactEventKind, PeerId, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use gpui::{
@@ -9,8 +9,8 @@ use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
json::{self, ToJson},
- Border, CursorStyle, Entity, ImageData, MouseButton, MutableAppContext, RenderContext,
- Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
+ Border, CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext,
+ RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
use std::{ops::Range, sync::Arc};
@@ -29,6 +29,7 @@ pub fn init(cx: &mut MutableAppContext) {
pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>,
+ user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
_subscriptions: Vec<Subscription>,
}
@@ -71,7 +72,11 @@ impl View for CollabTitlebarItem {
}
impl CollabTitlebarItem {
- pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+ pub fn new(
+ workspace: &ViewHandle<Workspace>,
+ user_store: &ModelHandle<UserStore>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
@@ -79,9 +84,33 @@ impl CollabTitlebarItem {
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
this.window_activation_changed(active, cx)
}));
+ subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
+ subscriptions.push(
+ cx.subscribe(user_store, move |this, user_store, event, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ workspace.update(cx, |workspace, cx| {
+ if let client::Event::Contact { user, kind } = event {
+ if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
+ workspace.show_notification(user.id as usize, cx, |cx| {
+ cx.add_view(|cx| {
+ ContactNotification::new(
+ user.clone(),
+ *kind,
+ user_store,
+ cx,
+ )
+ })
+ })
+ }
+ }
+ });
+ }
+ }),
+ );
Self {
workspace: workspace.downgrade(),
+ user_store: user_store.clone(),
contacts_popover: None,
_subscriptions: subscriptions,
}
@@ -160,6 +189,26 @@ impl CollabTitlebarItem {
cx: &mut RenderContext<Self>,
) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
+ let badge = if self
+ .user_store
+ .read(cx)
+ .incoming_contact_requests()
+ .is_empty()
+ {
+ None
+ } else {
+ Some(
+ Empty::new()
+ .collapsed()
+ .contained()
+ .with_style(titlebar.toggle_contacts_badge)
+ .contained()
+ .with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
+ .with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
+ .aligned()
+ .boxed(),
+ )
+ };
Stack::new()
.with_child(
MouseEventHandler::<ToggleContactsPopover>::new(0, cx, |state, _| {
@@ -185,6 +234,7 @@ impl CollabTitlebarItem {
.aligned()
.boxed(),
)
+ .with_children(badge)
.with_children(self.contacts_popover.as_ref().map(|popover| {
Overlay::new(
ChildView::new(popover)
@@ -1,6 +1,9 @@
mod collab_titlebar_item;
+mod contact_finder;
+mod contact_notification;
mod contacts_popover;
mod incoming_call_notification;
+mod notifications;
mod project_shared_notification;
use call::ActiveCall;
@@ -11,6 +14,8 @@ use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
+ contact_notification::init(cx);
+ contact_finder::init(cx);
contacts_popover::init(cx);
collab_titlebar_item::init(cx);
incoming_call_notification::init(cx);
@@ -9,8 +9,6 @@ use std::sync::Arc;
use util::TryFutureExt;
use workspace::Workspace;
-use crate::render_icon_button;
-
actions!(contact_finder, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
@@ -117,11 +115,10 @@ impl PickerDelegate for ContactFinder {
let icon_path = match request_status {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
- "icons/check_8.svg"
- }
- ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
- "icons/x_mark_8.svg"
+ Some("icons/check_8.svg")
}
+ ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
+ ContactRequestStatus::RequestAccepted => None,
};
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.contact_finder.disabled_contact_button
@@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder {
.left()
.boxed(),
)
- .with_child(
- render_icon_button(button_style, icon_path)
+ .with_children(icon_path.map(|icon_path| {
+ Svg::new(icon_path)
+ .with_color(button_style.color)
+ .constrained()
+ .with_width(button_style.icon_width)
+ .aligned()
+ .contained()
+ .with_style(button_style.container)
+ .constrained()
+ .with_width(button_style.button_width)
+ .with_height(button_style.button_width)
.aligned()
.flex_float()
- .boxed(),
- )
+ .boxed()
+ }))
.contained()
.with_style(style.container)
.constrained()
@@ -49,10 +49,7 @@ impl View for ContactNotification {
self.user.clone(),
"wants to add you as a contact",
Some("They won't know if you decline."),
- RespondToContactRequest {
- user_id: self.user.id,
- accept: false,
- },
+ Dismiss(self.user.id),
vec![
(
"Decline",
@@ -1,22 +1,27 @@
use std::sync::Arc;
+use crate::contact_finder;
use call::ActiveCall;
use client::{Contact, User, UserStore};
use editor::{Cancel, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- elements::*, impl_internal_actions, keymap, AppContext, ClipboardItem, CursorStyle, Entity,
- ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext,
- ViewHandle,
+ elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem,
+ CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
+ View, ViewContext, ViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
+use serde::Deserialize;
use settings::Settings;
use theme::IconButton;
-impl_internal_actions!(contacts_panel, [ToggleExpanded, Call]);
+impl_actions!(contacts_popover, [RemoveContact, RespondToContactRequest]);
+impl_internal_actions!(contacts_popover, [ToggleExpanded, Call]);
pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(ContactsPopover::remove_contact);
+ cx.add_action(ContactsPopover::respond_to_contact_request);
cx.add_action(ContactsPopover::clear_filter);
cx.add_action(ContactsPopover::select_next);
cx.add_action(ContactsPopover::select_prev);
@@ -77,6 +82,18 @@ impl PartialEq for ContactEntry {
}
}
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct RequestContact(pub u64);
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct RemoveContact(pub u64);
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct RespondToContactRequest {
+ pub user_id: u64,
+ pub accept: bool,
+}
+
pub enum Event {
Dismissed,
}
@@ -186,6 +203,24 @@ impl ContactsPopover {
this
}
+ fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
+ self.user_store
+ .update(cx, |store, cx| store.remove_contact(request.0, cx))
+ .detach();
+ }
+
+ 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();
+ }
+
fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
let did_clear = self.filter_editor.update(cx, |editor, cx| {
if editor.buffer().read(cx).len(cx) > 0 {
@@ -574,18 +609,15 @@ impl ContactsPopover {
};
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
- // .flex_float()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
- todo!();
- // cx.dispatch_action(RespondToContactRequest {
- // user_id,
- // accept: false,
- // })
+ cx.dispatch_action(RespondToContactRequest {
+ user_id,
+ accept: false,
+ })
})
- // .flex_float()
.contained()
.with_margin_right(button_spacing)
.boxed(),
@@ -602,11 +634,10 @@ impl ContactsPopover {
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
- todo!()
- // cx.dispatch_action(RespondToContactRequest {
- // user_id,
- // accept: true,
- // })
+ cx.dispatch_action(RespondToContactRequest {
+ user_id,
+ accept: true,
+ })
})
.boxed(),
]);
@@ -626,8 +657,7 @@ impl ContactsPopover {
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
- todo!()
- // cx.dispatch_action(RemoveContact(user_id))
+ cx.dispatch_action(RemoveContact(user_id))
})
.flex_float()
.boxed(),
@@ -692,8 +722,7 @@ impl View for ContactsPopover {
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
- todo!()
- // cx.dispatch_action(contact_finder::Toggle)
+ cx.dispatch_action(contact_finder::Toggle)
})
.boxed(),
)
@@ -1,9 +1,7 @@
-use crate::render_icon_button;
use client::User;
use gpui::{
- elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text},
- platform::CursorStyle,
- Action, Element, ElementBox, MouseButton, RenderContext, View,
+ elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
+ View,
};
use settings::Settings;
use std::sync::Arc;
@@ -53,11 +51,18 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
)
.with_child(
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
- render_icon_button(
- theme.dismiss_button.style_for(state, false),
- "icons/x_mark_thin_8.svg",
- )
- .boxed()
+ let style = theme.dismiss_button.style_for(state, false);
+ Svg::new("icons/x_mark_thin_8.svg")
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_width(style.button_width)
+ .with_height(style.button_width)
+ .boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(5.))
@@ -1,32 +0,0 @@
-[package]
-name = "contacts_panel"
-version = "0.1.0"
-edition = "2021"
-
-[lib]
-path = "src/contacts_panel.rs"
-doctest = false
-
-[dependencies]
-client = { path = "../client" }
-collections = { path = "../collections" }
-editor = { path = "../editor" }
-fuzzy = { path = "../fuzzy" }
-gpui = { path = "../gpui" }
-menu = { path = "../menu" }
-picker = { path = "../picker" }
-project = { path = "../project" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-util = { path = "../util" }
-workspace = { path = "../workspace" }
-anyhow = "1.0"
-futures = "0.3"
-log = "0.4"
-postage = { version = "0.4.1", features = ["futures-traits"] }
-serde = { version = "1.0", features = ["derive", "rc"] }
-
-[dev-dependencies]
-language = { path = "../language", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
@@ -1,1000 +0,0 @@
-mod contact_finder;
-mod contact_notification;
-mod notifications;
-
-use client::{Contact, ContactEventKind, User, UserStore};
-use contact_notification::ContactNotification;
-use editor::{Cancel, Editor};
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
- actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle,
- AnyViewHandle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
- MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
- WeakViewHandle,
-};
-use menu::{Confirm, SelectNext, SelectPrev};
-use serde::Deserialize;
-use settings::Settings;
-use std::sync::Arc;
-use theme::IconButton;
-use workspace::{sidebar::SidebarItem, Workspace};
-
-actions!(contacts_panel, [ToggleFocus]);
-
-impl_actions!(
- contacts_panel,
- [RequestContact, RemoveContact, RespondToContactRequest]
-);
-
-impl_internal_actions!(contacts_panel, [ToggleExpanded]);
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-enum Section {
- Requests,
- Online,
- Offline,
-}
-
-#[derive(Clone)]
-enum ContactEntry {
- Header(Section),
- IncomingRequest(Arc<User>),
- OutgoingRequest(Arc<User>),
- Contact(Arc<Contact>),
-}
-
-#[derive(Clone, PartialEq)]
-struct ToggleExpanded(Section);
-
-pub struct ContactsPanel {
- entries: Vec<ContactEntry>,
- match_candidates: Vec<StringMatchCandidate>,
- list_state: ListState,
- user_store: ModelHandle<UserStore>,
- filter_editor: ViewHandle<Editor>,
- collapsed_sections: Vec<Section>,
- selection: Option<usize>,
- _maintain_contacts: Subscription,
-}
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct RequestContact(pub u64);
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct RemoveContact(pub u64);
-
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct RespondToContactRequest {
- pub user_id: u64,
- pub accept: bool,
-}
-
-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);
- cx.add_action(ContactsPanel::clear_filter);
- cx.add_action(ContactsPanel::select_next);
- cx.add_action(ContactsPanel::select_prev);
- cx.add_action(ContactsPanel::confirm);
- cx.add_action(ContactsPanel::toggle_expanded);
-}
-
-impl ContactsPanel {
- pub fn new(
- user_store: ModelHandle<UserStore>,
- workspace: WeakViewHandle<Workspace>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let filter_editor = cx.add_view(|cx| {
- let mut editor = Editor::single_line(
- Some(|theme| theme.contacts_panel.user_query_editor.clone()),
- cx,
- );
- editor.set_placeholder_text("Filter contacts", cx);
- editor
- });
-
- cx.subscribe(&filter_editor, |this, _, event, cx| {
- if let editor::Event::BufferEdited = event {
- let query = this.filter_editor.read(cx).text(cx);
- if !query.is_empty() {
- this.selection.take();
- }
- this.update_entries(cx);
- if !query.is_empty() {
- this.selection = this
- .entries
- .iter()
- .position(|entry| !matches!(entry, ContactEntry::Header(_)));
- }
- }
- })
- .detach();
-
- cx.subscribe(&user_store, move |_, user_store, event, cx| {
- if let Some(workspace) = workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- if let client::Event::Contact { user, kind } = event {
- if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
- workspace.show_notification(user.id as usize, cx, |cx| {
- cx.add_view(|cx| {
- ContactNotification::new(user.clone(), *kind, user_store, cx)
- })
- })
- }
- }
- });
- }
-
- if let client::Event::ShowContacts = event {
- cx.emit(Event::Activate);
- }
- })
- .detach();
-
- let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
- let theme = cx.global::<Settings>().theme.clone();
- let is_selected = this.selection == Some(ix);
-
- match &this.entries[ix] {
- ContactEntry::Header(section) => {
- let is_collapsed = this.collapsed_sections.contains(section);
- Self::render_header(
- *section,
- &theme.contacts_panel,
- is_selected,
- is_collapsed,
- cx,
- )
- }
- ContactEntry::IncomingRequest(user) => Self::render_contact_request(
- user.clone(),
- this.user_store.clone(),
- &theme.contacts_panel,
- true,
- is_selected,
- cx,
- ),
- ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
- user.clone(),
- this.user_store.clone(),
- &theme.contacts_panel,
- false,
- is_selected,
- cx,
- ),
- ContactEntry::Contact(contact) => {
- Self::render_contact(&contact.user, &theme.contacts_panel, is_selected)
- }
- }
- });
-
- let mut this = Self {
- list_state,
- selection: None,
- collapsed_sections: Default::default(),
- entries: Default::default(),
- match_candidates: Default::default(),
- filter_editor,
- _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)),
- user_store,
- };
- this.update_entries(cx);
- this
- }
-
- fn render_header(
- section: Section,
- theme: &theme::ContactsPanel,
- is_selected: bool,
- is_collapsed: bool,
- cx: &mut RenderContext<Self>,
- ) -> ElementBox {
- enum Header {}
-
- let header_style = theme.header_row.style_for(Default::default(), is_selected);
- let text = match section {
- Section::Requests => "Requests",
- Section::Online => "Online",
- Section::Offline => "Offline",
- };
- let icon_size = theme.section_icon_size;
- MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
- Flex::row()
- .with_child(
- Svg::new(if is_collapsed {
- "icons/chevron_right_8.svg"
- } else {
- "icons/chevron_down_8.svg"
- })
- .with_color(header_style.text.color)
- .constrained()
- .with_max_width(icon_size)
- .with_max_height(icon_size)
- .aligned()
- .constrained()
- .with_width(icon_size)
- .boxed(),
- )
- .with_child(
- Label::new(text.to_string(), header_style.text.clone())
- .aligned()
- .left()
- .contained()
- .with_margin_left(theme.contact_username.container.margin.left)
- .flex(1., true)
- .boxed(),
- )
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(header_style.container)
- .boxed()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, cx| {
- cx.dispatch_action(ToggleExpanded(section))
- })
- .boxed()
- }
-
- fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox {
- Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::new(avatar)
- .with_style(theme.contact_avatar)
- .aligned()
- .left()
- .boxed()
- }))
- .with_child(
- Label::new(
- user.github_login.clone(),
- theme.contact_username.text.clone(),
- )
- .contained()
- .with_style(theme.contact_username.container)
- .aligned()
- .left()
- .flex(1., true)
- .boxed(),
- )
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
- .boxed()
- }
-
- fn render_contact_request(
- user: Arc<User>,
- user_store: ModelHandle<UserStore>,
- theme: &theme::ContactsPanel,
- is_incoming: bool,
- is_selected: bool,
- cx: &mut RenderContext<ContactsPanel>,
- ) -> ElementBox {
- enum Decline {}
- enum Accept {}
- enum Cancel {}
-
- let mut row = Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::new(avatar)
- .with_style(theme.contact_avatar)
- .aligned()
- .left()
- .boxed()
- }))
- .with_child(
- Label::new(
- user.github_login.clone(),
- theme.contact_username.text.clone(),
- )
- .contained()
- .with_style(theme.contact_username.container)
- .aligned()
- .left()
- .flex(1., true)
- .boxed(),
- );
-
- let user_id = user.id;
- let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
- let button_spacing = theme.contact_button_spacing;
-
- if is_incoming {
- row.add_children([
- MouseEventHandler::<Decline>::new(user.id as usize, cx, |mouse_state, _| {
- let button_style = if is_contact_request_pending {
- &theme.disabled_button
- } else {
- theme.contact_button.style_for(mouse_state, false)
- };
- render_icon_button(button_style, "icons/x_mark_8.svg")
- .aligned()
- // .flex_float()
- .boxed()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, cx| {
- cx.dispatch_action(RespondToContactRequest {
- user_id,
- accept: false,
- })
- })
- // .flex_float()
- .contained()
- .with_margin_right(button_spacing)
- .boxed(),
- MouseEventHandler::<Accept>::new(user.id as usize, cx, |mouse_state, _| {
- let button_style = if is_contact_request_pending {
- &theme.disabled_button
- } else {
- theme.contact_button.style_for(mouse_state, false)
- };
- render_icon_button(button_style, "icons/check_8.svg")
- .aligned()
- .flex_float()
- .boxed()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, cx| {
- cx.dispatch_action(RespondToContactRequest {
- user_id,
- accept: true,
- })
- })
- .boxed(),
- ]);
- } else {
- row.add_child(
- MouseEventHandler::<Cancel>::new(user.id as usize, cx, |mouse_state, _| {
- let button_style = if is_contact_request_pending {
- &theme.disabled_button
- } else {
- theme.contact_button.style_for(mouse_state, false)
- };
- render_icon_button(button_style, "icons/x_mark_8.svg")
- .aligned()
- .flex_float()
- .boxed()
- })
- .with_padding(Padding::uniform(2.))
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, cx| {
- cx.dispatch_action(RemoveContact(user_id))
- })
- .flex_float()
- .boxed(),
- );
- }
-
- row.constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
- .boxed()
- }
-
- fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
- let user_store = self.user_store.read(cx);
- let query = self.filter_editor.read(cx).text(cx);
- let executor = cx.background().clone();
-
- let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
- self.entries.clear();
-
- let mut request_entries = Vec::new();
- let incoming = user_store.incoming_contact_requests();
- if !incoming.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(
- incoming
- .iter()
- .enumerate()
- .map(|(ix, user)| StringMatchCandidate {
- id: ix,
- string: user.github_login.clone(),
- char_bag: user.github_login.chars().collect(),
- }),
- );
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(
- matches
- .iter()
- .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
- );
- }
-
- let outgoing = user_store.outgoing_contact_requests();
- if !outgoing.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(
- outgoing
- .iter()
- .enumerate()
- .map(|(ix, user)| StringMatchCandidate {
- id: ix,
- string: user.github_login.clone(),
- char_bag: user.github_login.chars().collect(),
- }),
- );
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(
- matches
- .iter()
- .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
- );
- }
-
- if !request_entries.is_empty() {
- self.entries.push(ContactEntry::Header(Section::Requests));
- if !self.collapsed_sections.contains(&Section::Requests) {
- self.entries.append(&mut request_entries);
- }
- }
-
- let current_user = user_store.current_user();
-
- let contacts = user_store.contacts();
- if !contacts.is_empty() {
- // Always put the current user first.
- self.match_candidates.clear();
- self.match_candidates.reserve(contacts.len());
- self.match_candidates.push(StringMatchCandidate {
- id: 0,
- string: Default::default(),
- char_bag: Default::default(),
- });
- for (ix, contact) in contacts.iter().enumerate() {
- let candidate = StringMatchCandidate {
- id: ix,
- string: contact.user.github_login.clone(),
- char_bag: contact.user.github_login.chars().collect(),
- };
- if current_user
- .as_ref()
- .map_or(false, |current_user| current_user.id == contact.user.id)
- {
- self.match_candidates[0] = candidate;
- } else {
- self.match_candidates.push(candidate);
- }
- }
- if self.match_candidates[0].string.is_empty() {
- self.match_candidates.remove(0);
- }
-
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
-
- let (online_contacts, offline_contacts) = matches
- .iter()
- .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-
- for (matches, section) in [
- (online_contacts, Section::Online),
- (offline_contacts, Section::Offline),
- ] {
- if !matches.is_empty() {
- self.entries.push(ContactEntry::Header(section));
- if !self.collapsed_sections.contains(§ion) {
- for mat in matches {
- let contact = &contacts[mat.candidate_id];
- self.entries.push(ContactEntry::Contact(contact.clone()));
- }
- }
- }
- }
- }
-
- if let Some(prev_selected_entry) = prev_selected_entry {
- self.selection.take();
- for (ix, entry) in self.entries.iter().enumerate() {
- if *entry == prev_selected_entry {
- self.selection = Some(ix);
- break;
- }
- }
- }
-
- self.list_state.reset(self.entries.len());
- cx.notify();
- }
-
- fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext<Self>) {
- self.user_store
- .update(cx, |store, cx| store.request_contact(request.0, cx))
- .detach();
- }
-
- fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
- self.user_store
- .update(cx, |store, cx| store.remove_contact(request.0, cx))
- .detach();
- }
-
- 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();
- }
-
- fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
- let did_clear = self.filter_editor.update(cx, |editor, cx| {
- if editor.buffer().read(cx).len(cx) > 0 {
- editor.set_text("", cx);
- true
- } else {
- false
- }
- });
- if !did_clear {
- cx.propagate_action();
- }
- }
-
- fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- if let Some(ix) = self.selection {
- if self.entries.len() > ix + 1 {
- self.selection = Some(ix + 1);
- }
- } else if !self.entries.is_empty() {
- self.selection = Some(0);
- }
- cx.notify();
- self.list_state.reset(self.entries.len());
- }
-
- fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- if let Some(ix) = self.selection {
- if ix > 0 {
- self.selection = Some(ix - 1);
- } else {
- self.selection = None;
- }
- }
- cx.notify();
- self.list_state.reset(self.entries.len());
- }
-
- fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
- if let Some(selection) = self.selection {
- if let Some(entry) = self.entries.get(selection) {
- match entry {
- ContactEntry::Header(section) => {
- let section = *section;
- self.toggle_expanded(&ToggleExpanded(section), cx);
- }
- _ => {}
- }
- }
- }
- }
-
- fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
- let section = action.0;
- if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
- self.collapsed_sections.remove(ix);
- } else {
- self.collapsed_sections.push(section);
- }
- self.update_entries(cx);
- }
-}
-
-impl SidebarItem for ContactsPanel {
- fn should_show_badge(&self, cx: &AppContext) -> bool {
- !self
- .user_store
- .read(cx)
- .incoming_contact_requests()
- .is_empty()
- }
-
- fn contains_focused_view(&self, cx: &AppContext) -> bool {
- self.filter_editor.is_focused(cx)
- }
-
- fn should_activate_item_on_event(&self, event: &Event, _: &AppContext) -> bool {
- matches!(event, Event::Activate)
- }
-}
-
-fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
- Svg::new(svg_path)
- .with_color(style.color)
- .constrained()
- .with_width(style.icon_width)
- .aligned()
- .contained()
- .with_style(style.container)
- .constrained()
- .with_width(style.button_width)
- .with_height(style.button_width)
-}
-
-pub enum Event {
- Activate,
-}
-
-impl Entity for ContactsPanel {
- type Event = Event;
-}
-
-impl View for ContactsPanel {
- fn ui_name() -> &'static str {
- "ContactsPanel"
- }
-
- fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- enum AddContact {}
-
- let theme = cx.global::<Settings>().theme.clone();
- let theme = &theme.contacts_panel;
- Container::new(
- Flex::column()
- .with_child(
- Flex::row()
- .with_child(
- ChildView::new(self.filter_editor.clone())
- .contained()
- .with_style(theme.user_query_editor.container)
- .flex(1., true)
- .boxed(),
- )
- .with_child(
- MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
- Svg::new("icons/user_plus_16.svg")
- .with_color(theme.add_contact_button.color)
- .constrained()
- .with_height(16.)
- .contained()
- .with_style(theme.add_contact_button.container)
- .aligned()
- .boxed()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, cx| {
- cx.dispatch_action(contact_finder::Toggle)
- })
- .boxed(),
- )
- .constrained()
- .with_height(theme.user_query_editor_height)
- .boxed(),
- )
- .with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
- .with_children(
- self.user_store
- .read(cx)
- .invite_info()
- .cloned()
- .and_then(|info| {
- enum InviteLink {}
-
- if info.count > 0 {
- Some(
- MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
- let style =
- theme.invite_row.style_for(state, false).clone();
-
- let copied =
- cx.read_from_clipboard().map_or(false, |item| {
- item.text().as_str() == info.url.as_ref()
- });
-
- Label::new(
- format!(
- "{} invite link ({} left)",
- if copied { "Copied" } else { "Copy" },
- info.count
- ),
- style.label.clone(),
- )
- .aligned()
- .left()
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(style.container)
- .boxed()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, cx| {
- cx.write_to_clipboard(ClipboardItem::new(
- info.url.to_string(),
- ));
- cx.notify();
- })
- .boxed(),
- )
- } else {
- None
- }
- }),
- )
- .boxed(),
- )
- .with_style(theme.container)
- .boxed()
- }
-
- fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- cx.focus(&self.filter_editor);
- }
-
- fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
- let mut cx = Self::default_keymap_context();
- cx.set.insert("menu".into());
- cx
- }
-}
-
-impl PartialEq for ContactEntry {
- fn eq(&self, other: &Self) -> bool {
- match self {
- ContactEntry::Header(section_1) => {
- if let ContactEntry::Header(section_2) = other {
- return section_1 == section_2;
- }
- }
- ContactEntry::IncomingRequest(user_1) => {
- if let ContactEntry::IncomingRequest(user_2) = other {
- return user_1.id == user_2.id;
- }
- }
- ContactEntry::OutgoingRequest(user_1) => {
- if let ContactEntry::OutgoingRequest(user_2) = other {
- return user_1.id == user_2.id;
- }
- }
- ContactEntry::Contact(contact_1) => {
- if let ContactEntry::Contact(contact_2) = other {
- return contact_1.user.id == contact_2.user.id;
- }
- }
- }
- false
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use client::{
- proto,
- test::{FakeHttpClient, FakeServer},
- Client,
- };
- use collections::HashSet;
- use gpui::TestAppContext;
- use language::LanguageRegistry;
- use project::{FakeFs, Project, ProjectStore};
-
- #[gpui::test]
- async fn test_contact_panel(cx: &mut TestAppContext) {
- Settings::test_async(cx);
- let current_user_id = 100;
-
- let languages = Arc::new(LanguageRegistry::test());
- let http_client = FakeHttpClient::with_404_response();
- let client = Client::new(http_client.clone());
- let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
- let project_store = cx.add_model(|_| ProjectStore::new());
- let server = FakeServer::for_client(current_user_id, &client, cx).await;
- let fs = FakeFs::new(cx.background());
- let project = cx.update(|cx| {
- Project::local(
- client.clone(),
- user_store.clone(),
- project_store.clone(),
- languages,
- fs,
- cx,
- )
- });
-
- let (_, workspace) =
- cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
- let panel = cx.add_view(&workspace, |cx| {
- ContactsPanel::new(user_store.clone(), workspace.downgrade(), cx)
- });
-
- workspace.update(cx, |_, cx| {
- cx.observe(&panel, |_, panel, cx| {
- let entries = render_to_strings(&panel, cx);
- assert!(
- entries.iter().collect::<HashSet<_>>().len() == entries.len(),
- "Duplicate contact panel entries {:?}",
- entries
- )
- })
- .detach();
- });
-
- let get_users_request = server.receive::<proto::GetUsers>().await.unwrap();
- server
- .respond(
- get_users_request.receipt(),
- proto::UsersResponse {
- users: [
- "user_zero",
- "user_one",
- "user_two",
- "user_three",
- "user_four",
- "user_five",
- ]
- .into_iter()
- .enumerate()
- .map(|(id, name)| proto::User {
- id: id as u64,
- github_login: name.to_string(),
- ..Default::default()
- })
- .chain([proto::User {
- id: current_user_id,
- github_login: "the_current_user".to_string(),
- ..Default::default()
- }])
- .collect(),
- },
- )
- .await;
-
- server.send(proto::UpdateContacts {
- incoming_requests: vec![proto::IncomingContactRequest {
- requester_id: 1,
- should_notify: false,
- }],
- outgoing_requests: vec![2],
- contacts: vec![
- proto::Contact {
- user_id: 3,
- online: true,
- should_notify: false,
- },
- proto::Contact {
- user_id: 4,
- online: true,
- should_notify: false,
- },
- proto::Contact {
- user_id: 5,
- online: false,
- should_notify: false,
- },
- proto::Contact {
- user_id: current_user_id,
- online: true,
- should_notify: false,
- },
- ],
- ..Default::default()
- });
-
- cx.foreground().run_until_parked();
- assert_eq!(
- cx.read(|cx| render_to_strings(&panel, cx)),
- &[
- "v Requests",
- " incoming user_one",
- " outgoing user_two",
- "v Online",
- " the_current_user",
- " user_four",
- " user_three",
- "v Offline",
- " user_five",
- ]
- );
-
- panel.update(cx, |panel, cx| {
- panel
- .filter_editor
- .update(cx, |editor, cx| editor.set_text("f", cx))
- });
- cx.foreground().run_until_parked();
- assert_eq!(
- cx.read(|cx| render_to_strings(&panel, cx)),
- &[
- "v Online",
- " user_four <=== selected",
- "v Offline",
- " user_five",
- ]
- );
-
- panel.update(cx, |panel, cx| {
- panel.select_next(&Default::default(), cx);
- });
- assert_eq!(
- cx.read(|cx| render_to_strings(&panel, cx)),
- &[
- "v Online",
- " user_four",
- "v Offline <=== selected",
- " user_five",
- ]
- );
-
- panel.update(cx, |panel, cx| {
- panel.select_next(&Default::default(), cx);
- });
- assert_eq!(
- cx.read(|cx| render_to_strings(&panel, cx)),
- &[
- "v Online",
- " user_four",
- "v Offline",
- " user_five <=== selected",
- ]
- );
- }
-
- fn render_to_strings(panel: &ViewHandle<ContactsPanel>, cx: &AppContext) -> Vec<String> {
- let panel = panel.read(cx);
- let mut entries = Vec::new();
- entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| {
- let mut string = match entry {
- ContactEntry::Header(name) => {
- let icon = if panel.collapsed_sections.contains(name) {
- ">"
- } else {
- "v"
- };
- format!("{} {:?}", icon, name)
- }
- ContactEntry::IncomingRequest(user) => {
- format!(" incoming {}", user.github_login)
- }
- ContactEntry::OutgoingRequest(user) => {
- format!(" outgoing {}", user.github_login)
- }
- ContactEntry::Contact(contact) => {
- format!(" {}", contact.user.github_login)
- }
- };
-
- if panel.selection == Some(ix) {
- string.push_str(" <=== selected");
- }
-
- string
- }));
- entries
- }
-}
@@ -78,6 +78,7 @@ pub struct Titlebar {
pub outdated_warning: ContainedText,
pub share_button: Interactive<ContainedText>,
pub toggle_contacts_button: Interactive<IconButton>,
+ pub toggle_contacts_badge: ContainerStyle,
pub contacts_popover: AddParticipantPopover,
}
@@ -28,7 +28,6 @@ command_palette = { path = "../command_palette" }
context_menu = { path = "../context_menu" }
client = { path = "../client" }
clock = { path = "../clock" }
-contacts_panel = { path = "../contacts_panel" }
diagnostics = { path = "../diagnostics" }
editor = { path = "../editor" }
file_finder = { path = "../file_finder" }
@@ -112,7 +112,6 @@ fn main() {
go_to_line::init(cx);
file_finder::init(cx);
chat_panel::init(cx);
- contacts_panel::init(cx);
outline::init(cx);
project_symbols::init(cx);
project_panel::init(cx);
@@ -244,10 +244,6 @@ pub fn menus() -> Vec<Menu<'static>> {
name: "Project Panel",
action: Box::new(project_panel::ToggleFocus),
},
- MenuItem::Action {
- name: "Contacts Panel",
- action: Box::new(contacts_panel::ToggleFocus),
- },
MenuItem::Action {
name: "Command Palette",
action: Box::new(command_palette::Toggle),
@@ -12,8 +12,6 @@ use breadcrumbs::Breadcrumbs;
pub use client;
use collab_ui::CollabTitlebarItem;
use collections::VecDeque;
-pub use contacts_panel;
-use contacts_panel::ContactsPanel;
pub use editor;
use editor::{Editor, MultiBuffer};
use gpui::{
@@ -208,13 +206,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
},
);
- cx.add_action(
- |workspace: &mut Workspace,
- _: &contacts_panel::ToggleFocus,
- cx: &mut ViewContext<Workspace>| {
- workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx);
- },
- );
activity_indicator::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
@@ -281,14 +272,11 @@ pub fn initialize_workspace(
}));
});
- let collab_titlebar_item = cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, cx));
+ let collab_titlebar_item =
+ cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
workspace.set_titlebar_item(collab_titlebar_item, cx);
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
- let contact_panel = cx.add_view(|cx| {
- ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx)
- });
-
workspace.left_sidebar().update(cx, |sidebar, cx| {
sidebar.add_item(
"icons/folder_tree_16.svg",
@@ -297,14 +285,6 @@ pub fn initialize_workspace(
cx,
)
});
- workspace.right_sidebar().update(cx, |sidebar, cx| {
- sidebar.add_item(
- "icons/user_group_16.svg",
- "Contacts Panel".to_string(),
- contact_panel,
- cx,
- )
- });
let diagnostic_summary =
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
@@ -144,6 +144,13 @@ export default function workspace(theme: Theme) {
color: iconColor(theme, "active"),
},
},
+ toggleContactsBadge: {
+ cornerRadius: 3,
+ padding: 2,
+ margin: { top: 3, left: 3 },
+ border: { width: 1, color: workspaceBackground(theme) },
+ background: iconColor(theme, "feature"),
+ },
shareButton: {
...titlebarButton
},