Cargo.lock 🔗
@@ -1078,6 +1078,7 @@ dependencies = [
"menu",
"postage",
"project",
+ "room",
"serde",
"settings",
"theme",
@@ -4461,6 +4462,7 @@ dependencies = [
"futures",
"gpui",
"project",
+ "util",
]
[[package]]
Nathan Sobo and Antonio Scandurra created
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
Cargo.lock | 2
crates/collab/src/integration_tests.rs | 20
crates/collab_titlebar_item/Cargo.toml | 3
crates/collab_titlebar_item/src/collab_titlebar_item.rs | 3
crates/collab_titlebar_item/src/contacts_popover.rs | 163 +++++++++-
crates/gpui/src/app.rs | 11
crates/room/Cargo.toml | 3
crates/room/src/room.rs | 74 ++++
crates/zed/src/zed.rs | 3
9 files changed, 229 insertions(+), 53 deletions(-)
@@ -1078,6 +1078,7 @@ dependencies = [
"menu",
"postage",
"project",
+ "room",
"serde",
"settings",
"theme",
@@ -4461,6 +4462,7 @@ dependencies = [
"futures",
"gpui",
"project",
+ "util",
]
[[package]]
@@ -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
@@ -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"] }
@@ -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 {
@@ -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
@@ -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,
@@ -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"] }
@@ -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(())
}
@@ -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};