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",
"postage",
"project",
"room",
"serde",
"settings",
"theme",
@ -4461,6 +4462,7 @@ dependencies = [
"futures",
"gpui",
"project",
"util",
]
[[package]]

View file

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

View file

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

View file

@ -71,8 +71,9 @@ impl CollabTitlebarItem {
Some(_) => {}
None => {
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 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.subscribe(&view, |this, _, event, cx| {
match event {

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use client::{Contact, User, UserStore};
use client::{Client, Contact, User, UserStore};
use editor::{Cancel, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
@ -9,10 +9,11 @@ use gpui::{
ViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use room::Room;
use settings::Settings;
use theme::IconButton;
impl_internal_actions!(contacts_panel, [ToggleExpanded]);
impl_internal_actions!(contacts_panel, [ToggleExpanded, Call]);
pub fn init(cx: &mut MutableAppContext) {
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::confirm);
cx.add_action(ContactsPopover::toggle_expanded);
cx.add_action(ContactsPopover::call);
}
#[derive(Clone, PartialEq)]
struct ToggleExpanded(Section);
#[derive(Clone, PartialEq)]
struct Call {
recipient_user_id: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
enum Section {
Requests,
@ -73,18 +80,24 @@ pub enum Event {
}
pub struct ContactsPopover {
room: Option<(ModelHandle<Room>, Subscription)>,
entries: Vec<ContactEntry>,
match_candidates: Vec<StringMatchCandidate>,
list_state: ListState,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
filter_editor: ViewHandle<Editor>,
collapsed_sections: Vec<Section>,
selection: Option<usize>,
_maintain_contacts: Subscription,
_subscriptions: Vec<Subscription>,
}
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 mut editor = Editor::single_line(
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
@ -143,25 +156,52 @@ impl ContactsPopover {
cx,
),
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 {
room: None,
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)),
_subscriptions: subscriptions,
client,
user_store,
};
this.update_entries(cx);
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>) {
let did_clear = self.filter_editor.update(cx, |editor, cx| {
if editor.buffer().read(cx).len(cx) > 0 {
@ -357,6 +397,43 @@ impl ContactsPopover {
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(
section: Section,
theme: &theme::ContactsPanel,
@ -412,32 +489,46 @@ impl ContactsPopover {
.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)
fn render_contact(
contact: &Contact,
theme: &theme::ContactsPanel,
is_selected: bool,
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()
.left()
.boxed()
}))
.with_child(
Label::new(
user.github_login.clone(),
theme.contact_username.text.clone(),
.flex(1., true)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.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()
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
.boxed()
})
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(Call {
recipient_user_id: user_id,
})
})
.boxed()
}
fn render_contact_request(
@ -553,6 +644,21 @@ impl ContactsPopover {
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
.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 {
@ -606,6 +712,7 @@ impl View for ContactsPopover {
.with_height(theme.contacts_panel.user_query_editor_height)
.boxed(),
)
.with_children(self.render_active_call(cx))
.with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
.with_children(
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
where
E: Entity,

View file

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

View file

@ -1,13 +1,14 @@
mod participant;
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 futures::StreamExt;
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant};
use project::Project;
use std::sync::Arc;
use util::ResultExt;
pub enum Event {
PeerChangedActiveProject,
@ -18,9 +19,11 @@ pub struct Room {
status: RoomStatus,
local_participant: LocalParticipant,
remote_participants: HashMap<PeerId, RemoteParticipant>,
pending_user_ids: Vec<u64>,
pending_users: Vec<Arc<User>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_subscriptions: Vec<client::Subscription>,
_load_pending_users: Option<Task<()>>,
}
impl Entity for Room {
@ -28,7 +31,44 @@ impl Entity for 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();
cx.spawn_weak(|this, mut cx| async move {
let is_connected = client_status
@ -51,32 +91,36 @@ impl Room {
projects: 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)],
_load_pending_users: None,
client,
user_store,
}
}
pub fn create(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|mut cx| async move {
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(
call: &Call,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
let room_id = call.room_id;
cx.spawn(|mut cx| async move {
let response = client.request(proto::JoinRoom { id: room_id }).await?;
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))?;
Ok(room)
})
@ -98,8 +142,8 @@ impl Room {
&self.remote_participants
}
pub fn pending_user_ids(&self) -> &[u64] {
&self.pending_user_ids
pub fn pending_users(&self) -> &[Arc<User>] {
&self.pending_users
}
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();
Ok(())
}

View file

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