talkers (#8158)
Release Notes: - Added an "Unmute" action for guests in calls. This lets them use the mic, but not edit projects.
This commit is contained in:
parent
fa2c92d190
commit
c6826a61a0
14 changed files with 225 additions and 67 deletions
|
@ -156,7 +156,7 @@ impl Room {
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
connect.await?;
|
connect.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
if !this.read_only() {
|
if this.can_use_microphone() {
|
||||||
if let Some(live_kit) = &this.live_kit {
|
if let Some(live_kit) = &this.live_kit {
|
||||||
if !live_kit.muted_by_user && !live_kit.deafened {
|
if !live_kit.muted_by_user && !live_kit.deafened {
|
||||||
return this.share_microphone(cx);
|
return this.share_microphone(cx);
|
||||||
|
@ -1322,11 +1322,6 @@ impl Room {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_only(&self) -> bool {
|
|
||||||
!(self.local_participant().role == proto::ChannelRole::Member
|
|
||||||
|| self.local_participant().role == proto::ChannelRole::Admin)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_speaking(&self) -> bool {
|
pub fn is_speaking(&self) -> bool {
|
||||||
self.live_kit
|
self.live_kit
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -1337,6 +1332,22 @@ impl Room {
|
||||||
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
|
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_use_microphone(&self) -> bool {
|
||||||
|
use proto::ChannelRole::*;
|
||||||
|
match self.local_participant.role {
|
||||||
|
Admin | Member | Talker => true,
|
||||||
|
Guest | Banned => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_share_projects(&self) -> bool {
|
||||||
|
use proto::ChannelRole::*;
|
||||||
|
match self.local_participant.role {
|
||||||
|
Admin | Member => true,
|
||||||
|
Guest | Banned | Talker => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
if self.status.is_offline() {
|
if self.status.is_offline() {
|
||||||
|
|
|
@ -120,7 +120,8 @@ impl ChannelMembership {
|
||||||
proto::ChannelRole::Admin => 0,
|
proto::ChannelRole::Admin => 0,
|
||||||
proto::ChannelRole::Member => 1,
|
proto::ChannelRole::Member => 1,
|
||||||
proto::ChannelRole::Banned => 2,
|
proto::ChannelRole::Banned => 2,
|
||||||
proto::ChannelRole::Guest => 3,
|
proto::ChannelRole::Talker => 3,
|
||||||
|
proto::ChannelRole::Guest => 4,
|
||||||
},
|
},
|
||||||
kind_order: match self.kind {
|
kind_order: match self.kind {
|
||||||
proto::channel_member::Kind::Member => 0,
|
proto::channel_member::Kind::Member => 0,
|
||||||
|
|
|
@ -100,8 +100,12 @@ pub enum ChannelRole {
|
||||||
#[sea_orm(string_value = "member")]
|
#[sea_orm(string_value = "member")]
|
||||||
#[default]
|
#[default]
|
||||||
Member,
|
Member,
|
||||||
|
/// Talker can read, but not write.
|
||||||
|
/// They can use microphones and the channel chat
|
||||||
|
#[sea_orm(string_value = "talker")]
|
||||||
|
Talker,
|
||||||
/// Guest can read, but not write.
|
/// Guest can read, but not write.
|
||||||
/// (thought they can use the channel chat)
|
/// They can not use microphones but can use the chat.
|
||||||
#[sea_orm(string_value = "guest")]
|
#[sea_orm(string_value = "guest")]
|
||||||
Guest,
|
Guest,
|
||||||
/// Banned may not read.
|
/// Banned may not read.
|
||||||
|
@ -114,8 +118,9 @@ impl ChannelRole {
|
||||||
pub fn should_override(&self, other: Self) -> bool {
|
pub fn should_override(&self, other: Self) -> bool {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Admin => matches!(other, Member | Banned | Guest),
|
Admin => matches!(other, Member | Banned | Talker | Guest),
|
||||||
Member => matches!(other, Banned | Guest),
|
Member => matches!(other, Banned | Talker | Guest),
|
||||||
|
Talker => matches!(other, Guest),
|
||||||
Banned => matches!(other, Guest),
|
Banned => matches!(other, Guest),
|
||||||
Guest => false,
|
Guest => false,
|
||||||
}
|
}
|
||||||
|
@ -134,7 +139,7 @@ impl ChannelRole {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Admin | Member => true,
|
Admin | Member => true,
|
||||||
Guest => visibility == ChannelVisibility::Public,
|
Guest | Talker => visibility == ChannelVisibility::Public,
|
||||||
Banned => false,
|
Banned => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,7 +149,7 @@ impl ChannelRole {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Admin | Member => true,
|
Admin | Member => true,
|
||||||
Guest | Banned => false,
|
Guest | Talker | Banned => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,16 +157,16 @@ impl ChannelRole {
|
||||||
pub fn can_only_see_public_descendants(&self) -> bool {
|
pub fn can_only_see_public_descendants(&self) -> bool {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Guest => true,
|
Guest | Talker => true,
|
||||||
Admin | Member | Banned => false,
|
Admin | Member | Banned => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if the role can share screen/microphone/projects into rooms.
|
/// True if the role can share screen/microphone/projects into rooms.
|
||||||
pub fn can_publish_to_rooms(&self) -> bool {
|
pub fn can_use_microphone(&self) -> bool {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Admin | Member => true,
|
Admin | Member | Talker => true,
|
||||||
Guest | Banned => false,
|
Guest | Banned => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,7 +176,7 @@ impl ChannelRole {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Admin | Member => true,
|
Admin | Member => true,
|
||||||
Guest | Banned => false,
|
Talker | Guest | Banned => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +184,7 @@ impl ChannelRole {
|
||||||
pub fn can_read_projects(&self) -> bool {
|
pub fn can_read_projects(&self) -> bool {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Admin | Member | Guest => true,
|
Admin | Member | Guest | Talker => true,
|
||||||
Banned => false,
|
Banned => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,7 +193,7 @@ impl ChannelRole {
|
||||||
use ChannelRole::*;
|
use ChannelRole::*;
|
||||||
match self {
|
match self {
|
||||||
Admin | Member => true,
|
Admin | Member => true,
|
||||||
Banned | Guest => false,
|
Banned | Guest | Talker => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,6 +203,7 @@ impl From<proto::ChannelRole> for ChannelRole {
|
||||||
match value {
|
match value {
|
||||||
proto::ChannelRole::Admin => ChannelRole::Admin,
|
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||||
proto::ChannelRole::Member => ChannelRole::Member,
|
proto::ChannelRole::Member => ChannelRole::Member,
|
||||||
|
proto::ChannelRole::Talker => ChannelRole::Talker,
|
||||||
proto::ChannelRole::Guest => ChannelRole::Guest,
|
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||||
proto::ChannelRole::Banned => ChannelRole::Banned,
|
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||||
}
|
}
|
||||||
|
@ -209,6 +215,7 @@ impl Into<proto::ChannelRole> for ChannelRole {
|
||||||
match self {
|
match self {
|
||||||
ChannelRole::Admin => proto::ChannelRole::Admin,
|
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||||
ChannelRole::Member => proto::ChannelRole::Member,
|
ChannelRole::Member => proto::ChannelRole::Member,
|
||||||
|
ChannelRole::Talker => proto::ChannelRole::Talker,
|
||||||
ChannelRole::Guest => proto::ChannelRole::Guest,
|
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||||
ChannelRole::Banned => proto::ChannelRole::Banned,
|
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||||
}
|
}
|
||||||
|
|
|
@ -795,6 +795,7 @@ impl Database {
|
||||||
match role {
|
match role {
|
||||||
Some(ChannelRole::Admin) => Ok(role.unwrap()),
|
Some(ChannelRole::Admin) => Ok(role.unwrap()),
|
||||||
Some(ChannelRole::Member)
|
Some(ChannelRole::Member)
|
||||||
|
| Some(ChannelRole::Talker)
|
||||||
| Some(ChannelRole::Banned)
|
| Some(ChannelRole::Banned)
|
||||||
| Some(ChannelRole::Guest)
|
| Some(ChannelRole::Guest)
|
||||||
| None => Err(anyhow!(
|
| None => Err(anyhow!(
|
||||||
|
@ -813,7 +814,10 @@ impl Database {
|
||||||
let channel_role = self.channel_role_for_user(channel, user_id, tx).await?;
|
let channel_role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||||
match channel_role {
|
match channel_role {
|
||||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
|
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
|
||||||
Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
|
Some(ChannelRole::Banned)
|
||||||
|
| Some(ChannelRole::Guest)
|
||||||
|
| Some(ChannelRole::Talker)
|
||||||
|
| None => Err(anyhow!(
|
||||||
"user is not a channel member or channel does not exist"
|
"user is not a channel member or channel does not exist"
|
||||||
))?,
|
))?,
|
||||||
}
|
}
|
||||||
|
@ -828,9 +832,10 @@ impl Database {
|
||||||
) -> Result<ChannelRole> {
|
) -> Result<ChannelRole> {
|
||||||
let role = self.channel_role_for_user(channel, user_id, tx).await?;
|
let role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||||
match role {
|
match role {
|
||||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
|
Some(ChannelRole::Admin)
|
||||||
Ok(role.unwrap())
|
| Some(ChannelRole::Member)
|
||||||
}
|
| Some(ChannelRole::Guest)
|
||||||
|
| Some(ChannelRole::Talker) => Ok(role.unwrap()),
|
||||||
Some(ChannelRole::Banned) | None => Err(anyhow!(
|
Some(ChannelRole::Banned) | None => Err(anyhow!(
|
||||||
"user is not a channel participant or channel does not exist"
|
"user is not a channel participant or channel does not exist"
|
||||||
))?,
|
))?,
|
||||||
|
|
|
@ -51,7 +51,7 @@ impl Database {
|
||||||
if !participant
|
if !participant
|
||||||
.role
|
.role
|
||||||
.unwrap_or(ChannelRole::Member)
|
.unwrap_or(ChannelRole::Member)
|
||||||
.can_publish_to_rooms()
|
.can_edit_projects()
|
||||||
{
|
{
|
||||||
return Err(anyhow!("guests cannot share projects"))?;
|
return Err(anyhow!("guests cannot share projects"))?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,7 +169,7 @@ impl Database {
|
||||||
|
|
||||||
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
|
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
|
||||||
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
|
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
|
||||||
ChannelRole::Guest => ChannelRole::Guest,
|
ChannelRole::Guest | ChannelRole::Talker => ChannelRole::Guest,
|
||||||
ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()),
|
ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ use axum::{
|
||||||
Extension, Router, TypedHeader,
|
Extension, Router, TypedHeader,
|
||||||
};
|
};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
pub use connection_pool::ConnectionPool;
|
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::oneshot,
|
channel::oneshot,
|
||||||
future::{self, BoxFuture},
|
future::{self, BoxFuture},
|
||||||
|
@ -558,6 +558,7 @@ impl Server {
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
address: String,
|
address: String,
|
||||||
user: User,
|
user: User,
|
||||||
|
zed_version: ZedVersion,
|
||||||
impersonator: Option<User>,
|
impersonator: Option<User>,
|
||||||
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||||
executor: Executor,
|
executor: Executor,
|
||||||
|
@ -599,7 +600,7 @@ impl Server {
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut pool = this.connection_pool.lock();
|
let mut pool = this.connection_pool.lock();
|
||||||
pool.add_connection(connection_id, user_id, user.admin);
|
pool.add_connection(connection_id, user_id, user.admin, zed_version);
|
||||||
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
||||||
this.peer.send(connection_id, build_update_user_channels(&channels_for_user))?;
|
this.peer.send(connection_id, build_update_user_channels(&channels_for_user))?;
|
||||||
this.peer.send(connection_id, build_channels_update(
|
this.peer.send(connection_id, build_channels_update(
|
||||||
|
@ -879,18 +880,21 @@ pub async fn handle_websocket_request(
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
// the first version of zed that sent this header was 0.121.x
|
let Some(version) = app_version_header.map(|header| ZedVersion(header.0 .0)) else {
|
||||||
if let Some(version) = app_version_header.map(|header| header.0 .0) {
|
return (
|
||||||
// 0.123.0 was a nightly version with incompatible collab changes
|
StatusCode::UPGRADE_REQUIRED,
|
||||||
// that were reverted.
|
"no version header found".to_string(),
|
||||||
if version == "0.123.0".parse().unwrap() {
|
)
|
||||||
|
.into_response();
|
||||||
|
};
|
||||||
|
|
||||||
|
if !version.is_supported() {
|
||||||
return (
|
return (
|
||||||
StatusCode::UPGRADE_REQUIRED,
|
StatusCode::UPGRADE_REQUIRED,
|
||||||
"client must be upgraded".to_string(),
|
"client must be upgraded".to_string(),
|
||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let socket_address = socket_address.to_string();
|
let socket_address = socket_address.to_string();
|
||||||
ws.on_upgrade(move |socket| {
|
ws.on_upgrade(move |socket| {
|
||||||
|
@ -906,6 +910,7 @@ pub async fn handle_websocket_request(
|
||||||
connection,
|
connection,
|
||||||
socket_address,
|
socket_address,
|
||||||
user,
|
user,
|
||||||
|
version,
|
||||||
impersonator.0,
|
impersonator.0,
|
||||||
None,
|
None,
|
||||||
Executor::Production,
|
Executor::Production,
|
||||||
|
@ -1311,6 +1316,22 @@ async fn set_room_participant_role(
|
||||||
response: Response<proto::SetRoomParticipantRole>,
|
response: Response<proto::SetRoomParticipantRole>,
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let user_id = UserId::from_proto(request.user_id);
|
||||||
|
let role = ChannelRole::from(request.role());
|
||||||
|
|
||||||
|
if role == ChannelRole::Talker {
|
||||||
|
let pool = session.connection_pool().await;
|
||||||
|
|
||||||
|
for connection in pool.user_connections(user_id) {
|
||||||
|
if !connection.zed_version.supports_talker_role() {
|
||||||
|
Err(anyhow!(
|
||||||
|
"This user is on zed {} which does not support unmute",
|
||||||
|
connection.zed_version
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (live_kit_room, can_publish) = {
|
let (live_kit_room, can_publish) = {
|
||||||
let room = session
|
let room = session
|
||||||
.db()
|
.db()
|
||||||
|
@ -1318,13 +1339,13 @@ async fn set_room_participant_role(
|
||||||
.set_room_participant_role(
|
.set_room_participant_role(
|
||||||
session.user_id,
|
session.user_id,
|
||||||
RoomId::from_proto(request.room_id),
|
RoomId::from_proto(request.room_id),
|
||||||
UserId::from_proto(request.user_id),
|
user_id,
|
||||||
ChannelRole::from(request.role()),
|
role,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let live_kit_room = room.live_kit_room.clone();
|
let live_kit_room = room.live_kit_room.clone();
|
||||||
let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms();
|
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
|
||||||
room_updated(&room, &session.peer);
|
room_updated(&room, &session.peer);
|
||||||
(live_kit_room, can_publish)
|
(live_kit_room, can_publish)
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ use collections::{BTreeMap, HashSet};
|
||||||
use rpc::ConnectionId;
|
use rpc::ConnectionId;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use util::SemanticVersion;
|
||||||
|
|
||||||
#[derive(Default, Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
pub struct ConnectionPool {
|
pub struct ConnectionPool {
|
||||||
|
@ -16,10 +17,30 @@ struct ConnectedUser {
|
||||||
connection_ids: HashSet<ConnectionId>,
|
connection_ids: HashSet<ConnectionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ZedVersion(pub SemanticVersion);
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
impl fmt::Display for ZedVersion {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZedVersion {
|
||||||
|
pub fn is_supported(&self) -> bool {
|
||||||
|
self.0 != SemanticVersion::new(0, 123, 0)
|
||||||
|
}
|
||||||
|
pub fn supports_talker_role(&self) -> bool {
|
||||||
|
self.0 >= SemanticVersion::new(0, 125, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Connection {
|
pub struct Connection {
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
|
pub zed_version: ZedVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectionPool {
|
impl ConnectionPool {
|
||||||
|
@ -29,9 +50,21 @@ impl ConnectionPool {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
|
pub fn add_connection(
|
||||||
self.connections
|
&mut self,
|
||||||
.insert(connection_id, Connection { user_id, admin });
|
connection_id: ConnectionId,
|
||||||
|
user_id: UserId,
|
||||||
|
admin: bool,
|
||||||
|
zed_version: ZedVersion,
|
||||||
|
) {
|
||||||
|
self.connections.insert(
|
||||||
|
connection_id,
|
||||||
|
Connection {
|
||||||
|
user_id,
|
||||||
|
admin,
|
||||||
|
zed_version,
|
||||||
|
},
|
||||||
|
);
|
||||||
let connected_user = self.connected_users.entry(user_id).or_default();
|
let connected_user = self.connected_users.entry(user_id).or_default();
|
||||||
connected_user.connection_ids.insert(connection_id);
|
connected_user.connection_ids.insert(connection_id);
|
||||||
}
|
}
|
||||||
|
@ -57,6 +90,19 @@ impl ConnectionPool {
|
||||||
self.connections.values()
|
self.connections.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn user_connections(&self, user_id: UserId) -> impl Iterator<Item = &Connection> + '_ {
|
||||||
|
self.connected_users
|
||||||
|
.get(&user_id)
|
||||||
|
.into_iter()
|
||||||
|
.map(|state| {
|
||||||
|
state
|
||||||
|
.connection_ids
|
||||||
|
.iter()
|
||||||
|
.flat_map(|cid| self.connections.get(cid))
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
|
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
|
||||||
self.connected_users
|
self.connected_users
|
||||||
.get(&user_id)
|
.get(&user_id)
|
||||||
|
|
|
@ -104,7 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||||
});
|
});
|
||||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
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, |e, cx| e.read_only(cx)));
|
||||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||||
assert!(room_b
|
assert!(room_b
|
||||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||||
.await
|
.await
|
||||||
|
@ -130,7 +130,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||||
|
|
||||||
// B sees themselves as muted, and can unmute.
|
// B sees themselves as muted, and can unmute.
|
||||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||||
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
|
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
|
||||||
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
|
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
|
||||||
cx_a.run_until_parked();
|
cx_a.run_until_parked();
|
||||||
|
@ -223,7 +223,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||||
let room_b = cx_b
|
let room_b = cx_b
|
||||||
.read(ActiveCall::global)
|
.read(ActiveCall::global)
|
||||||
.update(cx_b, |call, _| call.room().unwrap().clone());
|
.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||||
|
|
||||||
// A tries to grant write access to B, but cannot because B has not
|
// A tries to grant write access to B, but cannot because B has not
|
||||||
// yet signed the zed CLA.
|
// yet signed the zed CLA.
|
||||||
|
@ -240,7 +240,26 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
cx_a.run_until_parked();
|
cx_a.run_until_parked();
|
||||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||||
|
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||||
|
|
||||||
|
// A tries to grant write access to B, but cannot because B has not
|
||||||
|
// yet signed the zed CLA.
|
||||||
|
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::Talker,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||||
|
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||||
|
|
||||||
// User B signs the zed CLA.
|
// User B signs the zed CLA.
|
||||||
server
|
server
|
||||||
|
@ -264,5 +283,6 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx_a.run_until_parked();
|
cx_a.run_until_parked();
|
||||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
|
||||||
|
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{tests::TestDb, NewUserParams, UserId},
|
db::{tests::TestDb, NewUserParams, UserId},
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
rpc::{Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||||
AppState, Config,
|
AppState, Config,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
@ -37,7 +37,7 @@ use std::{
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use util::http::FakeHttpClient;
|
use util::{http::FakeHttpClient, SemanticVersion};
|
||||||
use workspace::{Workspace, WorkspaceStore};
|
use workspace::{Workspace, WorkspaceStore};
|
||||||
|
|
||||||
pub struct TestServer {
|
pub struct TestServer {
|
||||||
|
@ -231,6 +231,7 @@ impl TestServer {
|
||||||
server_conn,
|
server_conn,
|
||||||
client_name,
|
client_name,
|
||||||
user,
|
user,
|
||||||
|
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||||
None,
|
None,
|
||||||
Some(connection_id_tx),
|
Some(connection_id_tx),
|
||||||
Executor::Deterministic(cx.background_executor().clone()),
|
Executor::Deterministic(cx.background_executor().clone()),
|
||||||
|
|
|
@ -854,6 +854,10 @@ impl CollabPanel {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else if role == proto::ChannelRole::Guest {
|
} else if role == proto::ChannelRole::Guest {
|
||||||
Label::new("Guest").color(Color::Muted).into_any_element()
|
Label::new("Guest").color(Color::Muted).into_any_element()
|
||||||
|
} else if role == proto::ChannelRole::Talker {
|
||||||
|
Label::new("Mic only")
|
||||||
|
.color(Color::Muted)
|
||||||
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
div().into_any_element()
|
div().into_any_element()
|
||||||
})
|
})
|
||||||
|
@ -1013,13 +1017,38 @@ impl CollabPanel {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let this = cx.view().clone();
|
let this = cx.view().clone();
|
||||||
if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) {
|
if !(role == proto::ChannelRole::Guest
|
||||||
|
|| role == proto::ChannelRole::Talker
|
||||||
|
|| role == proto::ChannelRole::Member)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
|
let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
|
||||||
if role == proto::ChannelRole::Guest {
|
if role == proto::ChannelRole::Guest {
|
||||||
context_menu.entry(
|
context_menu = context_menu.entry(
|
||||||
|
"Grant Mic 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::Talker,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_prompt_err("Failed to grant mic access", cx, |_, _| None)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
|
||||||
|
context_menu = context_menu.entry(
|
||||||
"Grant Write Access",
|
"Grant Write Access",
|
||||||
None,
|
None,
|
||||||
cx.handler_for(&this, move |_, cx| {
|
cx.handler_for(&this, move |_, cx| {
|
||||||
|
@ -1043,10 +1072,16 @@ impl CollabPanel {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
} else if role == proto::ChannelRole::Member {
|
}
|
||||||
context_menu.entry(
|
if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
|
||||||
"Revoke Write Access",
|
let label = if role == proto::ChannelRole::Talker {
|
||||||
|
"Mute"
|
||||||
|
} else {
|
||||||
|
"Revoke Access"
|
||||||
|
};
|
||||||
|
context_menu = context_menu.entry(
|
||||||
|
label,
|
||||||
None,
|
None,
|
||||||
cx.handler_for(&this, move |_, cx| {
|
cx.handler_for(&this, move |_, cx| {
|
||||||
ActiveCall::global(cx)
|
ActiveCall::global(cx)
|
||||||
|
@ -1062,12 +1097,12 @@ impl CollabPanel {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
|
.detach_and_prompt_err("Failed to revoke access", cx, |_, _| None)
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context_menu
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.focus_view(&context_menu);
|
cx.focus_view(&context_menu);
|
||||||
|
|
|
@ -187,9 +187,10 @@ impl Render for CollabTitlebarItem {
|
||||||
let is_muted = room.is_muted();
|
let is_muted = room.is_muted();
|
||||||
let is_deafened = room.is_deafened().unwrap_or(false);
|
let is_deafened = room.is_deafened().unwrap_or(false);
|
||||||
let is_screen_sharing = room.is_screen_sharing();
|
let is_screen_sharing = room.is_screen_sharing();
|
||||||
let read_only = room.read_only();
|
let can_use_microphone = room.can_use_microphone();
|
||||||
|
let can_share_projects = room.can_share_projects();
|
||||||
|
|
||||||
this.when(is_local && !read_only, |this| {
|
this.when(is_local && can_share_projects, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new(
|
Button::new(
|
||||||
"toggle_sharing",
|
"toggle_sharing",
|
||||||
|
@ -235,7 +236,7 @@ impl Render for CollabTitlebarItem {
|
||||||
)
|
)
|
||||||
.pr_2(),
|
.pr_2(),
|
||||||
)
|
)
|
||||||
.when(!read_only, |this| {
|
.when(can_use_microphone, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
IconButton::new(
|
IconButton::new(
|
||||||
"mute-microphone",
|
"mute-microphone",
|
||||||
|
@ -276,7 +277,7 @@ impl Render for CollabTitlebarItem {
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.selected(is_deafened)
|
.selected(is_deafened)
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
if !read_only {
|
if can_use_microphone {
|
||||||
Tooltip::with_meta(
|
Tooltip::with_meta(
|
||||||
"Deafen Audio",
|
"Deafen Audio",
|
||||||
None,
|
None,
|
||||||
|
@ -289,7 +290,7 @@ impl Render for CollabTitlebarItem {
|
||||||
})
|
})
|
||||||
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
|
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
|
||||||
)
|
)
|
||||||
.when(!read_only, |this| {
|
.when(can_share_projects, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
IconButton::new("screen-share", ui::IconName::Screen)
|
IconButton::new("screen-share", ui::IconName::Screen)
|
||||||
.style(ButtonStyle::Subtle)
|
.style(ButtonStyle::Subtle)
|
||||||
|
|
|
@ -1107,6 +1107,7 @@ enum ChannelRole {
|
||||||
Member = 1;
|
Member = 1;
|
||||||
Guest = 2;
|
Guest = 2;
|
||||||
Banned = 3;
|
Banned = 3;
|
||||||
|
Talker = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetChannelMemberRole {
|
message SetChannelMemberRole {
|
||||||
|
|
|
@ -14,9 +14,18 @@ pub struct SemanticVersion {
|
||||||
pub patch: usize,
|
pub patch: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SemanticVersion {
|
||||||
|
pub fn new(major: usize, minor: usize, patch: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
major,
|
||||||
|
minor,
|
||||||
|
patch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for SemanticVersion {
|
impl FromStr for SemanticVersion {
|
||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
let mut components = s.trim().split('.');
|
let mut components = s.trim().split('.');
|
||||||
let major = components
|
let major = components
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue