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:
Conrad Irwin 2024-02-22 10:07:36 -07:00 committed by GitHub
parent fa2c92d190
commit c6826a61a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 225 additions and 67 deletions

View file

@ -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() {

View file

@ -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,

View file

@ -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,
} }

View file

@ -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"
))?, ))?,

View file

@ -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"))?;
} }

View file

@ -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()),
}; };

View file

@ -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,17 +880,20 @@ 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() { )
return ( .into_response();
StatusCode::UPGRADE_REQUIRED, };
"client must be upgraded".to_string(),
) if !version.is_supported() {
.into_response(); return (
} StatusCode::UPGRADE_REQUIRED,
"client must be upgraded".to_string(),
)
.into_response();
} }
let socket_address = socket_address.to_string(); let socket_address = socket_address.to_string();
@ -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)
}; };

View file

@ -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)

View file

@ -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()));
} }

View file

@ -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()),

View file

@ -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);

View file

@ -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)

View file

@ -1107,6 +1107,7 @@ enum ChannelRole {
Member = 1; Member = 1;
Guest = 2; Guest = 2;
Banned = 3; Banned = 3;
Talker = 4;
} }
message SetChannelMemberRole { message SetChannelMemberRole {

View file

@ -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