Start a call when clicking on a contact in the contacts popover

Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
This commit is contained in:
Nathan Sobo 2022-09-28 11:02:26 -06:00
parent 815cf44647
commit 8ff4f044b7
9 changed files with 229 additions and 53 deletions

2
Cargo.lock generated
View file

@ -1078,6 +1078,7 @@ dependencies = [
"menu", "menu",
"postage", "postage",
"project", "project",
"room",
"serde", "serde",
"settings", "settings",
"theme", "theme",
@ -4461,6 +4462,7 @@ dependencies = [
"futures", "futures",
"gpui", "gpui",
"project", "project",
"util",
] ]
[[package]] [[package]]

View file

@ -83,7 +83,7 @@ async fn test_basic_calls(
.await; .await;
let room_a = cx_a let room_a = cx_a
.update(|cx| Room::create(client_a.clone(), cx)) .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx))
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@ -125,7 +125,7 @@ async fn test_basic_calls(
// User B joins the room using the first client. // User B joins the room using the first client.
let room_b = cx_b let room_b = cx_b
.update(|cx| Room::join(&call_b, client_b.clone(), cx)) .update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx))
.await .await
.unwrap(); .unwrap();
assert!(incoming_call_b.next().await.unwrap().is_none()); assert!(incoming_call_b.next().await.unwrap().is_none());
@ -229,7 +229,7 @@ async fn test_leaving_room_on_disconnection(
.await; .await;
let room_a = cx_a let room_a = cx_a
.update(|cx| Room::create(client_a.clone(), cx)) .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx))
.await .await
.unwrap(); .unwrap();
@ -245,7 +245,7 @@ async fn test_leaving_room_on_disconnection(
// User B receives the call and joins the room. // User B receives the call and joins the room.
let call_b = incoming_call_b.next().await.unwrap().unwrap(); let call_b = incoming_call_b.next().await.unwrap().unwrap();
let room_b = cx_b let room_b = cx_b
.update(|cx| Room::join(&call_b, client_b.clone(), cx)) .update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx))
.await .await
.unwrap(); .unwrap();
deterministic.run_until_parked(); deterministic.run_until_parked();
@ -6284,17 +6284,9 @@ async fn room_participants(
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
let remote_users = futures::future::try_join_all(remote_users).await.unwrap(); let remote_users = futures::future::try_join_all(remote_users).await.unwrap();
let pending_users = room.update(cx, |room, cx| { let pending_users = room.read_with(cx, |room, _| {
room.pending_user_ids() room.pending_users().iter().cloned().collect::<Vec<_>>()
.iter()
.map(|user_id| {
client
.user_store
.update(cx, |users, cx| users.get_user(*user_id, cx))
})
.collect::<Vec<_>>()
}); });
let pending_users = futures::future::try_join_all(pending_users).await.unwrap();
RoomParticipants { RoomParticipants {
remote: remote_users remote: remote_users

View file

@ -14,6 +14,7 @@ test-support = [
"editor/test-support", "editor/test-support",
"gpui/test-support", "gpui/test-support",
"project/test-support", "project/test-support",
"room/test-support",
"settings/test-support", "settings/test-support",
"util/test-support", "util/test-support",
"workspace/test-support", "workspace/test-support",
@ -28,6 +29,7 @@ fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" } menu = { path = "../menu" }
project = { path = "../project" } project = { path = "../project" }
room = { path = "../room" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
@ -44,6 +46,7 @@ collections = { path = "../collections", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
room = { path = "../room", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -71,8 +71,9 @@ impl CollabTitlebarItem {
Some(_) => {} Some(_) => {}
None => { None => {
if let Some(workspace) = self.workspace.upgrade(cx) { if let Some(workspace) = self.workspace.upgrade(cx) {
let client = workspace.read(cx).client().clone();
let user_store = workspace.read(cx).user_store().clone(); let user_store = workspace.read(cx).user_store().clone();
let view = cx.add_view(|cx| ContactsPopover::new(user_store, cx)); let view = cx.add_view(|cx| ContactsPopover::new(client, user_store, cx));
cx.focus(&view); cx.focus(&view);
cx.subscribe(&view, |this, _, event, cx| { cx.subscribe(&view, |this, _, event, cx| {
match event { match event {

View file

@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use client::{Contact, User, UserStore}; use client::{Client, Contact, User, UserStore};
use editor::{Cancel, Editor}; use editor::{Cancel, Editor};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
@ -9,10 +9,11 @@ use gpui::{
ViewHandle, ViewHandle,
}; };
use menu::{Confirm, SelectNext, SelectPrev}; use menu::{Confirm, SelectNext, SelectPrev};
use room::Room;
use settings::Settings; use settings::Settings;
use theme::IconButton; use theme::IconButton;
impl_internal_actions!(contacts_panel, [ToggleExpanded]); impl_internal_actions!(contacts_panel, [ToggleExpanded, Call]);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPopover::clear_filter); cx.add_action(ContactsPopover::clear_filter);
@ -20,11 +21,17 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPopover::select_prev); cx.add_action(ContactsPopover::select_prev);
cx.add_action(ContactsPopover::confirm); cx.add_action(ContactsPopover::confirm);
cx.add_action(ContactsPopover::toggle_expanded); cx.add_action(ContactsPopover::toggle_expanded);
cx.add_action(ContactsPopover::call);
} }
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
struct ToggleExpanded(Section); struct ToggleExpanded(Section);
#[derive(Clone, PartialEq)]
struct Call {
recipient_user_id: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
enum Section { enum Section {
Requests, Requests,
@ -73,18 +80,24 @@ pub enum Event {
} }
pub struct ContactsPopover { pub struct ContactsPopover {
room: Option<(ModelHandle<Room>, Subscription)>,
entries: Vec<ContactEntry>, entries: Vec<ContactEntry>,
match_candidates: Vec<StringMatchCandidate>, match_candidates: Vec<StringMatchCandidate>,
list_state: ListState, list_state: ListState,
client: Arc<Client>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
filter_editor: ViewHandle<Editor>, filter_editor: ViewHandle<Editor>,
collapsed_sections: Vec<Section>, collapsed_sections: Vec<Section>,
selection: Option<usize>, selection: Option<usize>,
_maintain_contacts: Subscription, _subscriptions: Vec<Subscription>,
} }
impl ContactsPopover { impl ContactsPopover {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self { pub fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let filter_editor = cx.add_view(|cx| { let filter_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line( let mut editor = Editor::single_line(
Some(|theme| theme.contacts_panel.user_query_editor.clone()), Some(|theme| theme.contacts_panel.user_query_editor.clone()),
@ -143,25 +156,52 @@ impl ContactsPopover {
cx, cx,
), ),
ContactEntry::Contact(contact) => { ContactEntry::Contact(contact) => {
Self::render_contact(&contact.user, &theme.contacts_panel, is_selected) Self::render_contact(contact, &theme.contacts_panel, is_selected, cx)
} }
} }
}); });
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
let weak_self = cx.weak_handle();
subscriptions.push(Room::observe(cx, move |room, cx| {
if let Some(this) = weak_self.upgrade(cx) {
this.update(cx, |this, cx| this.set_room(room, cx));
}
}));
let mut this = Self { let mut this = Self {
room: None,
list_state, list_state,
selection: None, selection: None,
collapsed_sections: Default::default(), collapsed_sections: Default::default(),
entries: Default::default(), entries: Default::default(),
match_candidates: Default::default(), match_candidates: Default::default(),
filter_editor, filter_editor,
_maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)), _subscriptions: subscriptions,
client,
user_store, user_store,
}; };
this.update_entries(cx); this.update_entries(cx);
this this
} }
fn set_room(&mut self, room: Option<ModelHandle<Room>>, cx: &mut ViewContext<Self>) {
if let Some(room) = room {
let observation = cx.observe(&room, |this, room, cx| this.room_updated(room, cx));
self.room = Some((room, observation));
} else {
self.room = None;
}
cx.notify();
}
fn room_updated(&mut self, room: ModelHandle<Room>, cx: &mut ViewContext<Self>) {
cx.notify();
}
fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
let did_clear = self.filter_editor.update(cx, |editor, cx| { let did_clear = self.filter_editor.update(cx, |editor, cx| {
if editor.buffer().read(cx).len(cx) > 0 { if editor.buffer().read(cx).len(cx) > 0 {
@ -357,6 +397,43 @@ impl ContactsPopover {
cx.notify(); cx.notify();
} }
fn render_active_call(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
let (room, _) = self.room.as_ref()?;
let theme = &cx.global::<Settings>().theme.contacts_panel;
Some(
Flex::column()
.with_children(room.read(cx).pending_users().iter().map(|user| {
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.default)
.boxed()
}))
.boxed(),
)
}
fn render_header( fn render_header(
section: Section, section: Section,
theme: &theme::ContactsPanel, theme: &theme::ContactsPanel,
@ -412,32 +489,46 @@ impl ContactsPopover {
.boxed() .boxed()
} }
fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox { fn render_contact(
Flex::row() contact: &Contact,
.with_children(user.avatar.clone().map(|avatar| { theme: &theme::ContactsPanel,
Image::new(avatar) is_selected: bool,
.with_style(theme.contact_avatar) cx: &mut RenderContext<Self>,
) -> ElementBox {
let user_id = contact.user.id;
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, _| {
Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(
contact.user.github_login.clone(),
theme.contact_username.text.clone(),
)
.contained()
.with_style(theme.contact_username.container)
.aligned() .aligned()
.left() .left()
.boxed() .flex(1., true)
})) .boxed(),
.with_child(
Label::new(
user.github_login.clone(),
theme.contact_username.text.clone(),
) )
.constrained()
.with_height(theme.row_height)
.contained() .contained()
.with_style(theme.contact_username.container) .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
.aligned() .boxed()
.left() })
.flex(1., true) .on_click(MouseButton::Left, move |_, cx| {
.boxed(), cx.dispatch_action(Call {
) recipient_user_id: user_id,
.constrained() })
.with_height(theme.row_height) })
.contained() .boxed()
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
.boxed()
} }
fn render_contact_request( fn render_contact_request(
@ -553,6 +644,21 @@ impl ContactsPopover {
.with_style(*theme.contact_row.style_for(Default::default(), is_selected)) .with_style(*theme.contact_row.style_for(Default::default(), is_selected))
.boxed() .boxed()
} }
fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
let client = self.client.clone();
let user_store = self.user_store.clone();
let recipient_user_id = action.recipient_user_id;
cx.spawn_weak(|_, mut cx| async move {
let room = cx
.update(|cx| Room::get_or_create(&client, &user_store, cx))
.await?;
room.update(&mut cx, |room, cx| room.call(recipient_user_id, cx))
.await?;
anyhow::Ok(())
})
.detach();
}
} }
impl Entity for ContactsPopover { impl Entity for ContactsPopover {
@ -606,6 +712,7 @@ impl View for ContactsPopover {
.with_height(theme.contacts_panel.user_query_editor_height) .with_height(theme.contacts_panel.user_query_editor_height)
.boxed(), .boxed(),
) )
.with_children(self.render_active_call(cx))
.with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) .with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
.with_children( .with_children(
self.user_store self.user_store

View file

@ -1519,6 +1519,17 @@ impl MutableAppContext {
} }
} }
pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
where
G: Any + Default,
F: 'static + FnMut(&mut MutableAppContext),
{
if !self.has_global::<G>() {
self.set_global(G::default());
}
self.observe_global::<G, F>(observe)
}
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
where where
E: Entity, E: Entity,

View file

@ -13,6 +13,7 @@ test-support = [
"collections/test-support", "collections/test-support",
"gpui/test-support", "gpui/test-support",
"project/test-support", "project/test-support",
"util/test-support"
] ]
[dependencies] [dependencies]
@ -20,6 +21,7 @@ client = { path = "../client" }
collections = { path = "../collections" } collections = { path = "../collections" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
util = { path = "../util" }
anyhow = "1.0.38" anyhow = "1.0.38"
futures = "0.3" futures = "0.3"
@ -29,3 +31,4 @@ client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View file

@ -1,13 +1,14 @@
mod participant; mod participant;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use client::{call::Call, proto, Client, PeerId, TypedEnvelope}; use client::{call::Call, proto, Client, PeerId, TypedEnvelope, User, UserStore};
use collections::HashMap; use collections::HashMap;
use futures::StreamExt; use futures::StreamExt;
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant};
use project::Project; use project::Project;
use std::sync::Arc; use std::sync::Arc;
use util::ResultExt;
pub enum Event { pub enum Event {
PeerChangedActiveProject, PeerChangedActiveProject,
@ -18,9 +19,11 @@ pub struct Room {
status: RoomStatus, status: RoomStatus,
local_participant: LocalParticipant, local_participant: LocalParticipant,
remote_participants: HashMap<PeerId, RemoteParticipant>, remote_participants: HashMap<PeerId, RemoteParticipant>,
pending_user_ids: Vec<u64>, pending_users: Vec<Arc<User>>,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_subscriptions: Vec<client::Subscription>, _subscriptions: Vec<client::Subscription>,
_load_pending_users: Option<Task<()>>,
} }
impl Entity for Room { impl Entity for Room {
@ -28,7 +31,44 @@ impl Entity for Room {
} }
impl Room { impl Room {
fn new(id: u64, client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self { pub fn observe<F>(cx: &mut MutableAppContext, mut callback: F) -> gpui::Subscription
where
F: 'static + FnMut(Option<ModelHandle<Self>>, &mut MutableAppContext),
{
cx.observe_default_global::<Option<ModelHandle<Self>>, _>(move |cx| {
let room = cx.global::<Option<ModelHandle<Self>>>().clone();
callback(room, cx);
})
}
pub fn get_or_create(
client: &Arc<Client>,
user_store: &ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
if let Some(room) = cx.global::<Option<ModelHandle<Self>>>() {
Task::ready(Ok(room.clone()))
} else {
let client = client.clone();
let user_store = user_store.clone();
cx.spawn(|mut cx| async move {
let room = cx.update(|cx| Room::create(client, user_store, cx)).await?;
cx.update(|cx| cx.set_global(Some(room.clone())));
Ok(room)
})
}
}
pub fn clear(cx: &mut MutableAppContext) {
cx.set_global::<Option<ModelHandle<Self>>>(None);
}
fn new(
id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let mut client_status = client.status(); let mut client_status = client.status();
cx.spawn_weak(|this, mut cx| async move { cx.spawn_weak(|this, mut cx| async move {
let is_connected = client_status let is_connected = client_status
@ -51,32 +91,36 @@ impl Room {
projects: Default::default(), projects: Default::default(),
}, },
remote_participants: Default::default(), remote_participants: Default::default(),
pending_user_ids: Default::default(), pending_users: Default::default(),
_subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)], _subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
_load_pending_users: None,
client, client,
user_store,
} }
} }
pub fn create( pub fn create(
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> { ) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let room = client.request(proto::CreateRoom {}).await?; let room = client.request(proto::CreateRoom {}).await?;
Ok(cx.add_model(|cx| Self::new(room.id, client, cx))) Ok(cx.add_model(|cx| Self::new(room.id, client, user_store, cx)))
}) })
} }
pub fn join( pub fn join(
call: &Call, call: &Call,
client: Arc<Client>, client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext, cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> { ) -> Task<Result<ModelHandle<Self>>> {
let room_id = call.room_id; let room_id = call.room_id;
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let response = client.request(proto::JoinRoom { id: room_id }).await?; let response = client.request(proto::JoinRoom { id: room_id }).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.add_model(|cx| Self::new(room_id, client, cx)); let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
room.update(&mut cx, |room, cx| room.apply_room_update(room_proto, cx))?; room.update(&mut cx, |room, cx| room.apply_room_update(room_proto, cx))?;
Ok(room) Ok(room)
}) })
@ -98,8 +142,8 @@ impl Room {
&self.remote_participants &self.remote_participants
} }
pub fn pending_user_ids(&self) -> &[u64] { pub fn pending_users(&self) -> &[Arc<User>] {
&self.pending_user_ids &self.pending_users
} }
async fn handle_room_updated( async fn handle_room_updated(
@ -131,7 +175,19 @@ impl Room {
); );
} }
} }
self.pending_user_ids = room.pending_user_ids;
let pending_users = self.user_store.update(cx, move |user_store, cx| {
user_store.get_users(room.pending_user_ids, cx)
});
self._load_pending_users = Some(cx.spawn(|this, mut cx| async move {
if let Some(pending_users) = pending_users.await.log_err() {
this.update(&mut cx, |this, cx| {
this.pending_users = pending_users;
cx.notify();
});
}
}));
cx.notify(); cx.notify();
Ok(()) Ok(())
} }

View file

@ -21,10 +21,11 @@ use gpui::{
geometry::vector::vec2f, geometry::vector::vec2f,
impl_actions, impl_actions,
platform::{WindowBounds, WindowOptions}, platform::{WindowBounds, WindowOptions},
AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind, AssetSource, AsyncAppContext, ModelHandle, TitlebarOptions, ViewContext, WindowKind,
}; };
use language::Rope; use language::Rope;
pub use lsp; pub use lsp;
use postage::watch;
pub use project::{self, fs}; pub use project::{self, fs};
use project_panel::ProjectPanel; use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar}; use search::{BufferSearchBar, ProjectSearchBar};