guest promotion (#3969)
Release Notes: - Adds the ability to promote read-only guests to read-write participants in calls
This commit is contained in:
commit
5d3f5611e5
24 changed files with 579 additions and 163 deletions
|
@ -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,
|
||||
|
|
|
@ -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"))?;
|
||||
}
|
||||
|
|
|
@ -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<RoomGuard<proto::Room>> {
|
||||
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?;
|
||||
|
|
|
@ -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,50 @@ async fn leave_room(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_room_participant_role(
|
||||
request: proto::SetRoomParticipantRole,
|
||||
response: Response<proto::SetRoomParticipantRole>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
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();
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn call(
|
||||
request: proto::Call,
|
||||
response: Response<proto::Call>,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::tests::TestServer;
|
||||
use call::ActiveCall;
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use editor::Editor;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
use rpc::proto;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_guests(
|
||||
|
@ -13,37 +13,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,38 +38,122 @@ async fn test_channel_guests(
|
|||
|
||||
// b should be following a in the shared project.
|
||||
// B is a guest,
|
||||
cx_a.executor().run_until_parked();
|
||||
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::<Workspace>()
|
||||
.unwrap()
|
||||
.root_view(cx_b)
|
||||
.unwrap();
|
||||
let project_b = workspace_b.update(cx_b, |workspace, _| workspace.project().clone());
|
||||
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),
|
||||
);
|
||||
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();
|
||||
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]
|
||||
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 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
|
||||
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();
|
||||
|
||||
// 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 (project_b, editor_b) = workspace_b.update(cx_b, |workspace, cx| {
|
||||
(
|
||||
workspace.project().clone(),
|
||||
workspace.active_item_as::<Editor>(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| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.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, |editor, cx| !editor.read_only(cx)));
|
||||
room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
.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());
|
||||
}
|
||||
|
|
|
@ -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, &[]);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Project> {
|
||||
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<Project> {
|
||||
cx.update(|cx| {
|
||||
Project::local(
|
||||
|
@ -619,6 +666,18 @@ impl TestClient {
|
|||
) -> (View<Workspace>, &'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<Workspace>, &'a mut VisualTestContext) {
|
||||
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue