From 844d161c400e5f4e41f56743d010791314d16c0f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 8 Jan 2024 16:52:20 -0700 Subject: [PATCH 1/4] Allow adding write access to guests --- Cargo.lock | 2 + crates/call/src/room.rs | 23 ++- crates/collab/Cargo.toml | 2 + crates/collab/src/db/queries/rooms.rs | 40 +++++ crates/collab/src/rpc.rs | 22 +++ .../collab/src/tests/channel_guest_tests.rs | 112 +++++++----- crates/collab/src/tests/test_server.rs | 63 ++++++- crates/collab_ui/src/collab_panel.rs | 166 ++++++++---------- crates/gpui/src/app/test_context.rs | 4 + crates/language/src/buffer.rs | 6 + crates/multi_buffer/src/multi_buffer.rs | 7 +- crates/project/src/project.rs | 18 +- crates/rpc/proto/zed.proto | 9 +- crates/rpc/src/proto.rs | 2 + crates/vim/src/test/vim_test_context.rs | 1 - crates/workspace/src/notifications.rs | 17 +- 16 files changed, 349 insertions(+), 145 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00c13d10ff..4249503e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1473,6 +1473,7 @@ dependencies = [ "editor", "env_logger", "envy", + "file_finder", "fs", "futures 0.3.28", "git", @@ -1486,6 +1487,7 @@ dependencies = [ "live_kit_server", "log", "lsp", + "menu", "nanoid", "node_runtime", "notifications", diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index e2c9bf5886..6f8f8e1717 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -620,6 +620,27 @@ impl Room { self.local_participant.role == proto::ChannelRole::Admin } + pub fn set_participant_role( + &mut self, + user_id: u64, + role: proto::ChannelRole, + cx: &ModelContext, + ) -> Task> { + let client = self.client.clone(); + let room_id = self.id; + let role = role.into(); + cx.spawn(|_, _| async move { + client + .request(proto::SetRoomParticipantRole { + room_id, + user_id, + role, + }) + .await + .map(|_| ()) + }) + } + pub fn pending_participants(&self) -> &[Arc] { &self.pending_participants } @@ -731,7 +752,7 @@ impl Room { this.joined_projects.retain(|project| { if let Some(project) = project.upgrade() { - project.update(cx, |project, _| project.set_role(role)); + project.update(cx, |project, cx| project.set_role(role, cx)); true } else { false diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index baf279d634..13bbd0cf25 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -74,6 +74,8 @@ live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } node_runtime = { path = "../node_runtime" } notifications = { path = "../notifications", features = ["test-support"] } +file_finder = { path = "../file_finder"} +menu = { path = "../menu"} project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 9a87f91b81..c3e71880fd 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -1004,6 +1004,46 @@ impl Database { .await } + pub async fn set_room_participant_role( + &self, + admin_id: UserId, + room_id: RoomId, + user_id: UserId, + role: ChannelRole, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + room_participant::Entity::find() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::UserId.eq(admin_id)) + .add(room_participant::Column::Role.eq(ChannelRole::Admin)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("only admins can set participant role"))?; + + let result = room_participant::Entity::update_many() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::UserId.eq(user_id)), + ) + .set(room_participant::ActiveModel { + role: ActiveValue::set(Some(ChannelRole::from(role))), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if result.rows_affected != 1 { + Err(anyhow!("could not update room participant role"))?; + } + Ok(self.get_room(room_id, &tx).await?) + }) + .await + } + pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { self.transaction(|tx| async move { self.room_connection_lost(connection, &*tx).await?; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5301ca9a23..766cf4bcc0 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -202,6 +202,7 @@ impl Server { .add_request_handler(join_room) .add_request_handler(rejoin_room) .add_request_handler(leave_room) + .add_request_handler(set_room_participant_role) .add_request_handler(call) .add_request_handler(cancel_call) .add_message_handler(decline_call) @@ -1258,6 +1259,27 @@ async fn leave_room( Ok(()) } +async fn set_room_participant_role( + request: proto::SetRoomParticipantRole, + response: Response, + session: Session, +) -> Result<()> { + let room = session + .db() + .await + .set_room_participant_role( + session.user_id, + RoomId::from_proto(request.room_id), + UserId::from_proto(request.user_id), + ChannelRole::from(request.role()), + ) + .await?; + + room_updated(&room, &session.peer); + response.send(proto::Ack {})?; + Ok(()) +} + async fn call( request: proto::Call, response: Response, diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index 32cc074ec9..45e9e5a1d7 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -1,5 +1,6 @@ use crate::tests::TestServer; use call::ActiveCall; +use editor::Editor; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use rpc::proto; use workspace::Workspace; @@ -13,37 +14,18 @@ async fn test_channel_guests( let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - - let channel_id = server - .make_channel("the-channel", None, (&client_a, cx_a), &mut []) - .await; - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx) - }) - .await - .unwrap(); - - client_a - .fs() - .insert_tree( - "/a", - serde_json::json!({ - "a.txt": "a-contents", - }), - ) - .await; - let active_call_a = cx_a.read(ActiveCall::global); + let channel_id = server + .make_public_channel("the-channel", &client_a, cx_a) + .await; + // Client A shares a project in the channel + let project_a = client_a.build_test_project(cx_a).await; active_call_a .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) .await .unwrap(); - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await @@ -57,33 +39,16 @@ async fn test_channel_guests( // b should be following a in the shared project. // B is a guest, - cx_a.executor().run_until_parked(); - - // todo!() the test window does not call activation handlers - // correctly yet, so this API does not work. - // let project_b = active_call_b.read_with(cx_b, |call, _| { - // call.location() - // .unwrap() - // .upgrade() - // .expect("should not be weak") - // }); - - let window_b = cx_b.update(|cx| cx.active_window().unwrap()); - let cx_b = &mut VisualTestContext::from_window(window_b, cx_b); - - let workspace_b = window_b - .downcast::() - .unwrap() - .root_view(cx_b) - .unwrap(); - let project_b = workspace_b.update(cx_b, |workspace, _| workspace.project().clone()); + executor.run_until_parked(); + let active_call_b = cx_b.read(ActiveCall::global); + let project_b = + active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap()); assert_eq!( project_b.read_with(cx_b, |project, _| project.remote_id()), Some(project_id), ); assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); - assert!(project_b .update(cx_b, |project, cx| { let worktree_id = project.worktrees().next().unwrap().read(cx).id(); @@ -92,3 +57,60 @@ async fn test_channel_guests( .await .is_err()) } + +#[gpui::test] +async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let channel_id = server + .make_public_channel("the-channel", &client_a, cx_a) + .await; + + let project_a = client_a.build_test_project(cx_a).await; + cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx)) + .await + .unwrap(); + + // Client A shares a project in the channel + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + cx_a.run_until_parked(); + + // Client B joins channel A as a guest + cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) + .await + .unwrap(); + cx_a.run_until_parked(); + + let project_b = + active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap()); + + // client B opens 1.txt + let (workspace, cx_b) = client_b.active_workspace(cx_b); + cx_b.simulate_keystrokes("cmd-p 1 enter"); + let editor_b = cx_b.update(|cx| workspace.read(cx).active_item_as::(cx).unwrap()); + + active_call_a + .update(cx_a, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_participant_role( + client_b.user_id().unwrap(), + proto::ChannelRole::Member, + cx, + ) + }) + }) + .await + .unwrap(); + + cx_a.run_until_parked(); + + assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only())); + assert!(!editor_b.update(cx_b, |e, cx| e.read_only(cx))); +} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 034a85961f..6d8bb05d67 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -20,7 +20,11 @@ use node_runtime::FakeNodeRuntime; use notifications::NotificationStore; use parking_lot::Mutex; use project::{Project, WorktreeId}; -use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT}; +use rpc::{ + proto::{self, ChannelRole}, + RECEIVE_TIMEOUT, +}; +use serde_json::json; use settings::SettingsStore; use std::{ cell::{Ref, RefCell, RefMut}, @@ -228,12 +232,16 @@ impl TestServer { Project::init(&client, cx); client::init(&client, cx); language::init(cx); - editor::init_settings(cx); + editor::init(cx); workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); channel::init(&client, user_store.clone(), cx); notifications::init(client.clone(), user_store, cx); + collab_ui::init(&app_state, cx); + file_finder::init(cx); + menu::init(); + settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap(); }); client @@ -351,6 +359,31 @@ impl TestServer { channel_id } + pub async fn make_public_channel( + &self, + channel: &str, + client: &TestClient, + cx: &mut TestAppContext, + ) -> u64 { + let channel_id = self + .make_channel(channel, None, (client, cx), &mut []) + .await; + + client + .channel_store() + .update(cx, |channel_store, cx| { + channel_store.set_channel_visibility( + channel_id, + proto::ChannelVisibility::Public, + cx, + ) + }) + .await + .unwrap(); + + channel_id + } + pub async fn make_channel_tree( &self, channels: &[(&str, Option<&str>)], @@ -580,6 +613,20 @@ impl TestClient { (project, worktree.read_with(cx, |tree, _| tree.id())) } + pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Model { + self.fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + self.build_local_project("/a", cx).await.0 + } + pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model { cx.update(|cx| { Project::local( @@ -619,6 +666,18 @@ impl TestClient { ) -> (View, &'a mut VisualTestContext) { cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } + + pub fn active_workspace<'a>( + &'a self, + cx: &'a mut TestAppContext, + ) -> (View, &'a mut VisualTestContext) { + let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); + + let view = window.root_view(cx).unwrap(); + let cx = Box::new(VisualTestContext::from_window(*window.deref(), cx)); + // it might be nice to try and cleanup these at the end of each test. + (view, Box::leak(cx)) + } } impl Drop for TestClient { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 86d0131d70..e25ad0d225 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -37,7 +37,7 @@ use ui::{ use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - notifications::NotifyResultExt, + notifications::{NotifyResultExt, NotifyTaskExt}, Workspace, }; @@ -140,6 +140,7 @@ enum ListEntry { user: Arc, peer_id: Option, is_pending: bool, + role: proto::ChannelRole, }, ParticipantProject { project_id: u64, @@ -151,10 +152,6 @@ enum ListEntry { peer_id: Option, is_last: bool, }, - GuestCount { - count: usize, - has_visible_participants: bool, - }, IncomingRequest(Arc), OutgoingRequest(Arc), ChannelInvite(Arc), @@ -384,14 +381,10 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); - let mut guest_count_ix = 0; - let mut guest_count = if room.read_only() { 1 } else { 0 }; - let mut non_guest_count = if room.read_only() { 0 } else { 1 }; if let Some(channel_id) = room.channel_id() { self.entries.push(ListEntry::ChannelNotes { channel_id }); self.entries.push(ListEntry::ChannelChat { channel_id }); - guest_count_ix = self.entries.len(); } // Populate the active user. @@ -410,12 +403,13 @@ impl CollabPanel { &Default::default(), executor.clone(), )); - if !matches.is_empty() && !room.read_only() { + if !matches.is_empty() { let user_id = user.id; self.entries.push(ListEntry::CallParticipant { user, peer_id: None, is_pending: false, + role: room.local_participant().role, }); let mut projects = room.local_participant().projects.iter().peekable(); while let Some(project) = projects.next() { @@ -442,12 +436,6 @@ impl CollabPanel { room.remote_participants() .iter() .filter_map(|(_, participant)| { - if participant.role == proto::ChannelRole::Guest { - guest_count += 1; - return None; - } else { - non_guest_count += 1; - } Some(StringMatchCandidate { id: participant.user.id as usize, string: participant.user.github_login.clone(), @@ -455,7 +443,7 @@ impl CollabPanel { }) }), ); - let matches = executor.block(match_strings( + let mut matches = executor.block(match_strings( &self.match_candidates, &query, true, @@ -463,6 +451,15 @@ impl CollabPanel { &Default::default(), executor.clone(), )); + matches.sort_by(|a, b| { + let a_is_guest = room.role_for_user(a.candidate_id as u64) + == Some(proto::ChannelRole::Guest); + let b_is_guest = room.role_for_user(b.candidate_id as u64) + == Some(proto::ChannelRole::Guest); + a_is_guest + .cmp(&b_is_guest) + .then_with(|| a.string.cmp(&b.string)) + }); for mat in matches { let user_id = mat.candidate_id as u64; let participant = &room.remote_participants()[&user_id]; @@ -470,6 +467,7 @@ impl CollabPanel { user: participant.user.clone(), peer_id: Some(participant.peer_id), is_pending: false, + role: participant.role, }); let mut projects = participant.projects.iter().peekable(); while let Some(project) = projects.next() { @@ -488,15 +486,6 @@ impl CollabPanel { }); } } - if guest_count > 0 { - self.entries.insert( - guest_count_ix, - ListEntry::GuestCount { - count: guest_count, - has_visible_participants: non_guest_count > 0, - }, - ); - } // Populate pending participants. self.match_candidates.clear(); @@ -521,6 +510,7 @@ impl CollabPanel { user: room.pending_participants()[mat.candidate_id].clone(), peer_id: None, is_pending: true, + role: proto::ChannelRole::Member, })); } } @@ -834,13 +824,19 @@ impl CollabPanel { user: &Arc, peer_id: Option, is_pending: bool, + role: proto::ChannelRole, is_selected: bool, cx: &mut ViewContext, ) -> ListItem { + let user_id = user.id; let is_current_user = - self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); + self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id); let tooltip = format!("Follow {}", user.github_login); + let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| { + room.read(cx).local_participant().role == proto::ChannelRole::Admin + }); + ListItem::new(SharedString::from(user.github_login.clone())) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) @@ -853,17 +849,27 @@ impl CollabPanel { .on_click(move |_, cx| Self::leave_call(cx)) .tooltip(|cx| Tooltip::text("Leave Call", cx)) .into_any_element() + } else if role == proto::ChannelRole::Guest { + Label::new("Guest").color(Color::Muted).into_any_element() } else { div().into_any_element() }) - .when_some(peer_id, |this, peer_id| { - this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) + .when_some(peer_id, |el, peer_id| { + if role == proto::ChannelRole::Guest { + return el; + } + el.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) .on_click(cx.listener(move |this, _, cx| { this.workspace .update(cx, |workspace, cx| workspace.follow(peer_id, cx)) .ok(); })) }) + .when(is_call_admin && role == proto::ChannelRole::Guest, |el| { + el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { + this.deploy_participant_context_menu(event.position, user_id, cx) + })) + }) } fn render_participant_project( @@ -986,41 +992,6 @@ impl CollabPanel { .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } - fn render_guest_count( - &self, - count: usize, - has_visible_participants: bool, - is_selected: bool, - cx: &mut ViewContext, - ) -> impl IntoElement { - let manageable_channel_id = ActiveCall::global(cx).read(cx).room().and_then(|room| { - let room = room.read(cx); - if room.local_participant_is_admin() { - room.channel_id() - } else { - None - } - }); - - ListItem::new("guest_count") - .selected(is_selected) - .start_slot( - h_stack() - .gap_1() - .child(render_tree_branch(!has_visible_participants, false, cx)) - .child(""), - ) - .child(Label::new(if count == 1 { - format!("{} guest", count) - } else { - format!("{} guests", count) - })) - .when_some(manageable_channel_id, |el, channel_id| { - el.tooltip(move |cx| Tooltip::text("Manage Members", cx)) - .on_click(cx.listener(move |this, _, cx| this.manage_members(channel_id, cx))) - }) - } - fn has_subchannels(&self, ix: usize) -> bool { self.entries.get(ix).map_or(false, |entry| { if let ListEntry::Channel { has_children, .. } = entry { @@ -1031,6 +1002,47 @@ impl CollabPanel { }) } + fn deploy_participant_context_menu( + &mut self, + position: Point, + user_id: u64, + cx: &mut ViewContext, + ) { + let this = cx.view().clone(); + + let context_menu = ContextMenu::build(cx, |context_menu, cx| { + context_menu.entry( + "Allow Write Access", + None, + cx.handler_for(&this, move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| { + let Some(room) = call.room() else { + return Task::ready(Ok(())); + }; + room.update(cx, |room, cx| { + room.set_participant_role(user_id, proto::ChannelRole::Member, cx) + }) + }) + .detach_and_notify_err(cx) + }), + ) + }); + + cx.focus_view(&context_menu); + let subscription = + cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + if this.context_menu.as_ref().is_some_and(|context_menu| { + context_menu.0.focus_handle(cx).contains_focused(cx) + }) { + cx.focus_self(); + } + this.context_menu.take(); + cx.notify(); + }); + self.context_menu = Some((context_menu, position, subscription)); + } + fn deploy_channel_context_menu( &mut self, position: Point, @@ -1242,18 +1254,6 @@ impl CollabPanel { }); } } - ListEntry::GuestCount { .. } => { - let Some(room) = ActiveCall::global(cx).read(cx).room() else { - return; - }; - let room = room.read(cx); - let Some(channel_id) = room.channel_id() else { - return; - }; - if room.local_participant_is_admin() { - self.manage_members(channel_id, cx) - } - } ListEntry::Channel { channel, .. } => { let is_active = maybe!({ let call_channel = ActiveCall::global(cx) @@ -1788,8 +1788,9 @@ impl CollabPanel { user, peer_id, is_pending, + role, } => self - .render_call_participant(user, *peer_id, *is_pending, is_selected, cx) + .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx) .into_any_element(), ListEntry::ParticipantProject { project_id, @@ -1809,12 +1810,6 @@ impl CollabPanel { ListEntry::ParticipantScreen { peer_id, is_last } => self .render_participant_screen(*peer_id, *is_last, is_selected, cx) .into_any_element(), - ListEntry::GuestCount { - count, - has_visible_participants, - } => self - .render_guest_count(*count, *has_visible_participants, is_selected, cx) - .into_any_element(), ListEntry::ChannelNotes { channel_id } => self .render_channel_notes(*channel_id, is_selected, cx) .into_any_element(), @@ -2621,11 +2616,6 @@ impl PartialEq for ListEntry { return true; } } - ListEntry::GuestCount { .. } => { - if let ListEntry::GuestCount { .. } = other { - return true; - } - } } false } diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 0f71ea61a9..6ec37afb0f 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -238,6 +238,10 @@ impl TestAppContext { } } + pub fn run_until_parked(&mut self) { + self.background_executor.run_until_parked() + } + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) where A: Action, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d9472e8a77..08210875b8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -254,6 +254,7 @@ pub enum Event { LanguageChanged, Reparsed, DiagnosticsUpdated, + CapabilityChanged, Closed, } @@ -631,6 +632,11 @@ impl Buffer { .set_language_registry(language_registry); } + pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext) { + self.capability = capability; + cx.emit(Event::CapabilityChanged) + } + pub fn did_save( &mut self, version: clock::Global, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 946e6af5ab..f3ecd2d25f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -80,6 +80,7 @@ pub enum Event { Reloaded, DiffBaseChanged, LanguageChanged, + CapabilityChanged, Reparsed, Saved, FileHandleChanged, @@ -1404,7 +1405,7 @@ impl MultiBuffer { fn on_buffer_event( &mut self, - _: Model, + buffer: Model, event: &language::Event, cx: &mut ModelContext, ) { @@ -1421,6 +1422,10 @@ impl MultiBuffer { language::Event::Reparsed => Event::Reparsed, language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, language::Event::Closed => Event::Closed, + language::Event::CapabilityChanged => { + self.capability = buffer.read(cx).capability(); + Event::CapabilityChanged + } // language::Event::Operation(_) => return, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 044b750ad9..c412fad0e1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -799,7 +799,7 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), }; - this.set_role(role); + this.set_role(role, cx); for worktree in worktrees { let _ = this.add_worktree(&worktree, cx); } @@ -1622,14 +1622,22 @@ impl Project { cx.notify(); } - pub fn set_role(&mut self, role: proto::ChannelRole) { - if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state { - *capability = if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin - { + pub fn set_role(&mut self, role: proto::ChannelRole, cx: &mut ModelContext) { + let new_capability = + if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin { Capability::ReadWrite } else { Capability::ReadOnly }; + if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state { + if *capability == new_capability { + return; + } + + *capability = new_capability; + } + for buffer in self.opened_buffers() { + buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx)); } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index e423441a30..dd5d77440e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -180,7 +180,8 @@ message Envelope { DeleteNotification delete_notification = 152; MarkNotificationRead mark_notification_read = 153; LspExtExpandMacro lsp_ext_expand_macro = 154; - LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; // Current max + LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; + SetRoomParticipantRole set_room_participant_role = 156; // Current max } } @@ -1633,3 +1634,9 @@ message LspExtExpandMacroResponse { string name = 1; string expansion = 2; } + +message SetRoomParticipantRole { + uint64 room_id = 1; + uint64 user_id = 2; + ChannelRole role = 3; +} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 25b8b00dae..5d0a994154 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -283,6 +283,7 @@ messages!( (UsersResponse, Foreground), (LspExtExpandMacro, Background), (LspExtExpandMacroResponse, Background), + (SetRoomParticipantRole, Foreground), ); request_messages!( @@ -367,6 +368,7 @@ request_messages!( (UpdateProject, Ack), (UpdateWorktree, Ack), (LspExtExpandMacro, LspExtExpandMacroResponse), + (SetRoomParticipantRole, Ack), ); entity_messages!( diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 5ed5296bff..965ac63d38 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -17,7 +17,6 @@ pub struct VimTestContext { impl VimTestContext { pub fn init(cx: &mut gpui::TestAppContext) { if cx.has_global::() { - dbg!("OOPS"); return; } cx.update(|cx| { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 394772b9c4..ac091744c6 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -2,7 +2,7 @@ use crate::{Toast, Workspace}; use collections::HashMap; use gpui::{ AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render, - View, ViewContext, VisualContext, + Task, View, ViewContext, VisualContext, WindowContext, }; use std::{any::TypeId, ops::DerefMut}; @@ -393,3 +393,18 @@ where } } } + +pub trait NotifyTaskExt { + fn detach_and_notify_err(self, cx: &mut WindowContext); +} + +impl NotifyTaskExt for Task> +where + E: std::fmt::Debug + 'static, + R: 'static, +{ + fn detach_and_notify_err(self, cx: &mut WindowContext) { + cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) }) + .detach(); + } +} From 8669b08161a8718c3765ad5d217e17f0fdaeed6d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 8 Jan 2024 22:23:34 -0700 Subject: [PATCH 2/4] Failing test for unmuting microphone --- crates/call/src/room.rs | 6 ++- .../collab/src/tests/channel_guest_tests.rs | 44 ++++++++++++++----- crates/live_kit_client/src/test.rs | 8 ++++ 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 6f8f8e1717..0a599e85cf 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -173,7 +173,11 @@ impl Room { cx.spawn(|this, mut cx| async move { connect.await?; - if !cx.update(|cx| Self::mute_on_join(cx))? { + let is_read_only = this + .update(&mut cx, |room, _| room.read_only()) + .unwrap_or(true); + + if !cx.update(|cx| Self::mute_on_join(cx))? && !is_read_only { this.update(&mut cx, |this, cx| this.share_microphone(cx))? .await?; } diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index 45e9e5a1d7..8d21db678b 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -3,7 +3,6 @@ use call::ActiveCall; use editor::Editor; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use rpc::proto; -use workspace::Workspace; #[gpui::test] async fn test_channel_guests( @@ -44,6 +43,8 @@ async fn test_channel_guests( let active_call_b = cx_b.read(ActiveCall::global); let project_b = active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap()); + let room_b = active_call_b.update(cx_b, |call, _| call.room().unwrap().clone()); + assert_eq!( project_b.read_with(cx_b, |project, _| project.remote_id()), Some(project_id), @@ -55,7 +56,8 @@ async fn test_channel_guests( project.create_entry((worktree_id, "b.txt"), false, cx) }) .await - .is_err()) + .is_err()); + assert!(room_b.read_with(cx_b, |room, _| !room.is_sharing_mic())); } #[gpui::test] @@ -64,7 +66,6 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); let channel_id = server .make_public_channel("the-channel", &client_a, cx_a) @@ -76,7 +77,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test .unwrap(); // Client A shares a project in the channel - let project_id = active_call_a + active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); @@ -88,14 +89,29 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test .unwrap(); cx_a.run_until_parked(); - let project_b = - active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap()); - - // client B opens 1.txt - let (workspace, cx_b) = client_b.active_workspace(cx_b); + // client B opens 1.txt as a guest + let (workspace_b, cx_b) = client_b.active_workspace(cx_b); + let room_b = cx_b + .read(ActiveCall::global) + .update(cx_b, |call, _| call.room().unwrap().clone()); cx_b.simulate_keystrokes("cmd-p 1 enter"); - let editor_b = cx_b.update(|cx| workspace.read(cx).active_item_as::(cx).unwrap()); + let (project_b, editor_b) = workspace_b.update(cx_b, |workspace, cx| { + ( + workspace.project().clone(), + workspace.active_item_as::(cx).unwrap(), + ) + }); + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx))); + assert!(dbg!( + room_b + .update(cx_b, |room, cx| room.share_microphone(cx)) + .await + ) + .is_err()); + + // B is promoted active_call_a .update(cx_a, |call, cx| { call.room().unwrap().update(cx, |room, cx| { @@ -108,9 +124,13 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test }) .await .unwrap(); - cx_a.run_until_parked(); + // project and buffers are now editable assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only())); - assert!(!editor_b.update(cx_b, |e, cx| e.read_only(cx))); + assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx))); + room_b + .update(cx_b, |room, cx| room.share_microphone(cx)) + .await + .unwrap() } diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 1106e66f31..07a12a4ab6 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -167,6 +167,10 @@ impl TestServer { let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); + if claims.video.can_publish == Some(false) { + return Err(anyhow!("user is not allowed to publish")); + } + let mut server_rooms = self.rooms.lock(); let room = server_rooms .get_mut(&*room_name) @@ -205,6 +209,10 @@ impl TestServer { let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); + if claims.video.can_publish == Some(false) { + return Err(anyhow!("user is not allowed to publish")); + } + let mut server_rooms = self.rooms.lock(); let room = server_rooms .get_mut(&*room_name) From 4da9d61a4264604dd22427fe0e1a40050b85558b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 9 Jan 2024 16:10:12 -0700 Subject: [PATCH 3/4] Implement live kit promotion/demotion --- crates/call/src/room.rs | 30 +++++++++ crates/collab/src/db/ids.rs | 2 +- crates/collab/src/db/queries/projects.rs | 2 +- crates/collab/src/rpc.rs | 45 ++++++++++--- .../collab/src/tests/channel_guest_tests.rs | 27 +++++++- crates/collab_ui/src/collab_panel.rs | 67 ++++++++++++++----- crates/live_kit_client/src/test.rs | 61 ++++++++++++++--- crates/live_kit_server/src/api.rs | 29 ++++++++ crates/live_kit_server/src/live_kit_server.rs | 2 +- 9 files changed, 223 insertions(+), 42 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 0a599e85cf..3d1f1e70c7 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -754,6 +754,18 @@ impl Room { if this.local_participant.role != role { this.local_participant.role = role; + if role == proto::ChannelRole::Guest { + for project in mem::take(&mut this.shared_projects) { + if let Some(project) = project.upgrade() { + this.unshare_project(project, cx).log_err(); + } + } + this.local_participant.projects.clear(); + if let Some(live_kit_room) = &mut this.live_kit { + live_kit_room.stop_publishing(cx); + } + } + this.joined_projects.retain(|project| { if let Some(project) = project.upgrade() { project.update(cx, |project, cx| project.set_role(role, cx)); @@ -1632,6 +1644,24 @@ impl LiveKitRoom { Ok((result, old_muted)) } + + fn stop_publishing(&mut self, cx: &mut ModelContext) { + if let LocalTrack::Published { + track_publication, .. + } = mem::replace(&mut self.microphone_track, LocalTrack::None) + { + self.room.unpublish_track(track_publication); + cx.notify(); + } + + if let LocalTrack::Published { + track_publication, .. + } = mem::replace(&mut self.screen_track, LocalTrack::None) + { + self.room.unpublish_track(track_publication); + cx.notify(); + } + } } enum LocalTrack { diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 9f77225fb7..9dbbfaff8f 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -133,7 +133,7 @@ impl ChannelRole { } } - pub fn can_share_projects(&self) -> bool { + pub fn can_publish_to_rooms(&self) -> bool { use ChannelRole::*; match self { Admin | Member => true, diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 6e1bf16309..a55345dc16 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -49,7 +49,7 @@ impl Database { if !participant .role .unwrap_or(ChannelRole::Member) - .can_share_projects() + .can_publish_to_rooms() { return Err(anyhow!("guests cannot share projects"))?; } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 766cf4bcc0..b933ce7c9a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1264,18 +1264,41 @@ async fn set_room_participant_role( response: Response, session: Session, ) -> Result<()> { - let room = session - .db() - .await - .set_room_participant_role( - session.user_id, - RoomId::from_proto(request.room_id), - UserId::from_proto(request.user_id), - ChannelRole::from(request.role()), - ) - .await?; + let (live_kit_room, can_publish) = { + let room = session + .db() + .await + .set_room_participant_role( + session.user_id, + RoomId::from_proto(request.room_id), + UserId::from_proto(request.user_id), + ChannelRole::from(request.role()), + ) + .await?; + + let live_kit_room = room.live_kit_room.clone(); + let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms(); + room_updated(&room, &session.peer); + (live_kit_room, can_publish) + }; + + if let Some(live_kit) = session.live_kit_client.as_ref() { + live_kit + .update_participant( + live_kit_room.clone(), + request.user_id.to_string(), + live_kit_server::proto::ParticipantPermission { + can_subscribe: true, + can_publish, + can_publish_data: can_publish, + hidden: false, + recorder: false, + }, + ) + .await + .trace_err(); + } - room_updated(&room, &session.peer); response.send(proto::Ack {})?; Ok(()) } diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index 8d21db678b..9b68ce3922 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -1,7 +1,7 @@ use crate::tests::TestServer; use call::ActiveCall; use editor::Editor; -use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use gpui::{BackgroundExecutor, TestAppContext}; use rpc::proto; #[gpui::test] @@ -132,5 +132,28 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test room_b .update(cx_b, |room, cx| room.share_microphone(cx)) .await - .unwrap() + .unwrap(); + + // B is demoted + active_call_a + .update(cx_a, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_participant_role( + client_b.user_id().unwrap(), + proto::ChannelRole::Guest, + cx, + ) + }) + }) + .await + .unwrap(); + cx_a.run_until_parked(); + + // project and buffers are no longer editable + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx))); + assert!(room_b + .update(cx_b, |room, cx| room.share_microphone(cx)) + .await + .is_err()); } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index e25ad0d225..687c9e45b6 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -865,9 +865,9 @@ impl CollabPanel { .ok(); })) }) - .when(is_call_admin && role == proto::ChannelRole::Guest, |el| { + .when(is_call_admin, |el| { el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { - this.deploy_participant_context_menu(event.position, user_id, cx) + this.deploy_participant_context_menu(event.position, user_id, role, cx) })) }) } @@ -1006,27 +1006,60 @@ impl CollabPanel { &mut self, position: Point, user_id: u64, + role: proto::ChannelRole, cx: &mut ViewContext, ) { let this = cx.view().clone(); + if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) { + return; + } let context_menu = ContextMenu::build(cx, |context_menu, cx| { - context_menu.entry( - "Allow Write Access", - None, - cx.handler_for(&this, move |_, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| { - let Some(room) = call.room() else { - return Task::ready(Ok(())); - }; - room.update(cx, |room, cx| { - room.set_participant_role(user_id, proto::ChannelRole::Member, cx) + if role == proto::ChannelRole::Guest { + context_menu.entry( + "Grant Write Access", + None, + cx.handler_for(&this, move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| { + let Some(room) = call.room() else { + return Task::ready(Ok(())); + }; + room.update(cx, |room, cx| { + room.set_participant_role( + user_id, + proto::ChannelRole::Member, + cx, + ) + }) }) - }) - .detach_and_notify_err(cx) - }), - ) + .detach_and_notify_err(cx) + }), + ) + } else if role == proto::ChannelRole::Member { + context_menu.entry( + "Revoke Write Access", + None, + cx.handler_for(&this, move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| { + let Some(room) = call.room() else { + return Task::ready(Ok(())); + }; + room.update(cx, |room, cx| { + room.set_participant_role( + user_id, + proto::ChannelRole::Guest, + cx, + ) + }) + }) + .detach_and_notify_err(cx) + }), + ) + } else { + unreachable!() + } }); cx.focus_view(&context_menu); diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 07a12a4ab6..4575fdd2c1 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use collections::{BTreeMap, HashMap}; use futures::Stream; use gpui::BackgroundExecutor; -use live_kit_server::token; +use live_kit_server::{proto, token}; use media::core_video::CVImageBuffer; use parking_lot::Mutex; use postage::watch; @@ -151,6 +151,21 @@ impl TestServer { Ok(()) } + async fn update_participant( + &self, + room_name: String, + identity: String, + permission: proto::ParticipantPermission, + ) -> Result<()> { + self.executor.simulate_random_delay().await; + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + room.participant_permissions.insert(identity, permission); + Ok(()) + } + pub async fn disconnect_client(&self, client_identity: String) { self.executor.simulate_random_delay().await; let mut server_rooms = self.rooms.lock(); @@ -167,15 +182,22 @@ impl TestServer { let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); - if claims.video.can_publish == Some(false) { - return Err(anyhow!("user is not allowed to publish")); - } - let mut server_rooms = self.rooms.lock(); let room = server_rooms .get_mut(&*room_name) .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + let can_publish = room + .participant_permissions + .get(&identity) + .map(|permission| permission.can_publish) + .or(claims.video.can_publish) + .unwrap_or(true); + + if !can_publish { + return Err(anyhow!("user is not allowed to publish")); + } + let track = Arc::new(RemoteVideoTrack { sid: nanoid::nanoid!(17), publisher_id: identity.clone(), @@ -209,15 +231,22 @@ impl TestServer { let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); - if claims.video.can_publish == Some(false) { - return Err(anyhow!("user is not allowed to publish")); - } - let mut server_rooms = self.rooms.lock(); let room = server_rooms .get_mut(&*room_name) .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + let can_publish = room + .participant_permissions + .get(&identity) + .map(|permission| permission.can_publish) + .or(claims.video.can_publish) + .unwrap_or(true); + + if !can_publish { + return Err(anyhow!("user is not allowed to publish")); + } + let track = Arc::new(RemoteAudioTrack { sid: nanoid::nanoid!(17), publisher_id: identity.clone(), @@ -273,6 +302,7 @@ struct TestServerRoom { client_rooms: HashMap>, video_tracks: Vec>, audio_tracks: Vec>, + participant_permissions: HashMap, } impl TestServerRoom {} @@ -305,6 +335,19 @@ impl live_kit_server::api::Client for TestApiClient { Ok(()) } + async fn update_participant( + &self, + room: String, + identity: String, + permission: live_kit_server::proto::ParticipantPermission, + ) -> Result<()> { + let server = TestServer::get(&self.url)?; + server + .update_participant(room, identity, permission) + .await?; + Ok(()) + } + fn room_token(&self, room: &str, identity: &str) -> Result { let server = TestServer::get(&self.url)?; token::create( diff --git a/crates/live_kit_server/src/api.rs b/crates/live_kit_server/src/api.rs index 2c1e174fb4..e7e933c9c3 100644 --- a/crates/live_kit_server/src/api.rs +++ b/crates/live_kit_server/src/api.rs @@ -11,10 +11,18 @@ pub trait Client: Send + Sync { async fn create_room(&self, name: String) -> Result<()>; async fn delete_room(&self, name: String) -> Result<()>; async fn remove_participant(&self, room: String, identity: String) -> Result<()>; + async fn update_participant( + &self, + room: String, + identity: String, + permission: proto::ParticipantPermission, + ) -> Result<()>; fn room_token(&self, room: &str, identity: &str) -> Result; fn guest_token(&self, room: &str, identity: &str) -> Result; } +pub struct LiveKitParticipantUpdate {} + #[derive(Clone)] pub struct LiveKitClient { http: reqwest::Client, @@ -131,6 +139,27 @@ impl Client for LiveKitClient { Ok(()) } + async fn update_participant( + &self, + room: String, + identity: String, + permission: proto::ParticipantPermission, + ) -> Result<()> { + let _: proto::ParticipantInfo = self + .request( + "twirp/livekit.RoomService/UpdateParticipant", + token::VideoGrant::to_admin(&room), + proto::UpdateParticipantRequest { + room: room.clone(), + identity, + metadata: "".to_string(), + permission: Some(permission), + }, + ) + .await?; + Ok(()) + } + fn room_token(&self, room: &str, identity: &str) -> Result { token::create( &self.key, diff --git a/crates/live_kit_server/src/live_kit_server.rs b/crates/live_kit_server/src/live_kit_server.rs index 7471a96ec4..aa7c1f2fd0 100644 --- a/crates/live_kit_server/src/live_kit_server.rs +++ b/crates/live_kit_server/src/live_kit_server.rs @@ -1,3 +1,3 @@ pub mod api; -mod proto; +pub mod proto; pub mod token; From 2ca462722cea521dc66935e88f87f1b3fffc78ca Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 9 Jan 2024 22:11:09 -0700 Subject: [PATCH 4/4] Fix some tests (mostly more run_until_parked's...) --- crates/collab/src/tests/channel_tests.rs | 1 + crates/collab/src/tests/following_tests.rs | 15 +++++---------- crates/collab/src/tests/integration_tests.rs | 1 + crates/gpui/src/app/test_context.rs | 3 ++- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 49e7060301..2a88bc4c57 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1337,6 +1337,7 @@ async fn test_guest_access( }) .await .unwrap(); + executor.run_until_parked(); assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 0486e29461..46524319fb 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -234,14 +234,14 @@ async fn test_basic_following( workspace_c.update(cx_c, |workspace, cx| { workspace.close_window(&Default::default(), cx); }); - cx_c.update(|_| { - drop(workspace_c); - }); - cx_b.executor().run_until_parked(); + executor.run_until_parked(); // are you sure you want to leave the call? cx_c.simulate_prompt_answer(0); - cx_b.executor().run_until_parked(); + cx_c.cx.update(|_| { + drop(workspace_c); + }); executor.run_until_parked(); + cx_c.cx.update(|_| {}); weak_workspace_c.assert_dropped(); weak_project_c.assert_dropped(); @@ -1363,8 +1363,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - cx_a.update(editor::init); - cx_b.update(editor::init); client_a .fs() @@ -1400,9 +1398,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); - cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); - active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index a21235b6f3..cedc841527 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3065,6 +3065,7 @@ async fn test_local_settings( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); + executor.run_until_parked(); // As client B, join that project and observe the local settings. let project_b = client_b.build_remote_project(project_id, cx_b).await; diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 2b6c361a78..d207434835 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -545,7 +545,8 @@ use derive_more::{Deref, DerefMut}; pub struct VisualTestContext { #[deref] #[deref_mut] - cx: TestAppContext, + /// cx is the original TestAppContext (you can more easily access this using Deref) + pub cx: TestAppContext, window: AnyWindowHandle, }