diff --git a/Cargo.lock b/Cargo.lock index c06a3ee93c..64202ed1c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1078,6 +1078,7 @@ dependencies = [ "menu", "postage", "project", + "room", "serde", "settings", "theme", @@ -4461,6 +4462,7 @@ dependencies = [ "futures", "gpui", "project", + "util", ] [[package]] diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index e04bd80c79..bd60893a57 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -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::>() }); 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::>() + let pending_users = room.read_with(cx, |room, _| { + room.pending_users().iter().cloned().collect::>() }); - let pending_users = futures::future::try_join_all(pending_users).await.unwrap(); RoomParticipants { remote: remote_users diff --git a/crates/collab_titlebar_item/Cargo.toml b/crates/collab_titlebar_item/Cargo.toml index 4f85ddd8d9..fbdfb34386 100644 --- a/crates/collab_titlebar_item/Cargo.toml +++ b/crates/collab_titlebar_item/Cargo.toml @@ -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"] } diff --git a/crates/collab_titlebar_item/src/collab_titlebar_item.rs b/crates/collab_titlebar_item/src/collab_titlebar_item.rs index c27a2b3452..f9da9e5b7a 100644 --- a/crates/collab_titlebar_item/src/collab_titlebar_item.rs +++ b/crates/collab_titlebar_item/src/collab_titlebar_item.rs @@ -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 { diff --git a/crates/collab_titlebar_item/src/contacts_popover.rs b/crates/collab_titlebar_item/src/contacts_popover.rs index e44f6a2331..26c1194d74 100644 --- a/crates/collab_titlebar_item/src/contacts_popover.rs +++ b/crates/collab_titlebar_item/src/contacts_popover.rs @@ -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, Subscription)>, entries: Vec, match_candidates: Vec, list_state: ListState, + client: Arc, user_store: ModelHandle, filter_editor: ViewHandle, collapsed_sections: Vec
, selection: Option, - _maintain_contacts: Subscription, + _subscriptions: Vec, } impl ContactsPopover { - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { + pub fn new( + client: Arc, + user_store: ModelHandle, + cx: &mut ViewContext, + ) -> 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>, cx: &mut ViewContext) { + 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, cx: &mut ViewContext) { + cx.notify(); + } + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { 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) -> Option { + let (room, _) = self.room.as_ref()?; + let theme = &cx.global::().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, + ) -> ElementBox { + let user_id = contact.user.id; + MouseEventHandler::::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) { + 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 diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 308ea6c831..18a2f8a4d0 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1519,6 +1519,17 @@ impl MutableAppContext { } } + pub fn observe_default_global(&mut self, observe: F) -> Subscription + where + G: Any + Default, + F: 'static + FnMut(&mut MutableAppContext), + { + if !self.has_global::() { + self.set_global(G::default()); + } + self.observe_global::(observe) + } + pub fn observe_release(&mut self, handle: &H, callback: F) -> Subscription where E: Entity, diff --git a/crates/room/Cargo.toml b/crates/room/Cargo.toml index 33b6620b27..169f04d352 100644 --- a/crates/room/Cargo.toml +++ b/crates/room/Cargo.toml @@ -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"] } diff --git a/crates/room/src/room.rs b/crates/room/src/room.rs index 82363d4b19..f7d5a58fa6 100644 --- a/crates/room/src/room.rs +++ b/crates/room/src/room.rs @@ -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, - pending_user_ids: Vec, + pending_users: Vec>, client: Arc, + user_store: ModelHandle, _subscriptions: Vec, + _load_pending_users: Option>, } impl Entity for Room { @@ -28,7 +31,44 @@ impl Entity for Room { } impl Room { - fn new(id: u64, client: Arc, cx: &mut ModelContext) -> Self { + pub fn observe(cx: &mut MutableAppContext, mut callback: F) -> gpui::Subscription + where + F: 'static + FnMut(Option>, &mut MutableAppContext), + { + cx.observe_default_global::>, _>(move |cx| { + let room = cx.global::>>().clone(); + callback(room, cx); + }) + } + + pub fn get_or_create( + client: &Arc, + user_store: &ModelHandle, + cx: &mut MutableAppContext, + ) -> Task>> { + if let Some(room) = cx.global::>>() { + 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::>>(None); + } + + fn new( + id: u64, + client: Arc, + user_store: ModelHandle, + cx: &mut ModelContext, + ) -> 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, + user_store: ModelHandle, cx: &mut MutableAppContext, ) -> Task>> { 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, + user_store: ModelHandle, cx: &mut MutableAppContext, ) -> Task>> { 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] { + &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(()) } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 26888dc0d7..bd6f28a402 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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};