Start writing role to database (#3120)
Scaffolding for guest members in channels Release notes: - You can now set channels to "public" which will allow anyone to join and become a member. In a future release guests joining public channels will have reduced permissions.
This commit is contained in:
@@ -9,7 +9,7 @@ use db::RELEASE_CHANNEL;
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||
use rpc::{
|
||||
proto::{self, ChannelEdge, ChannelPermission},
|
||||
proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility},
|
||||
TypedEnvelope,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@@ -49,6 +49,7 @@ pub type ChannelData = (Channel, ChannelPath);
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: proto::ChannelVisibility,
|
||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||
pub unseen_message_id: Option<u64>,
|
||||
}
|
||||
@@ -79,7 +80,32 @@ pub struct ChannelPath(Arc<[ChannelId]>);
|
||||
pub struct ChannelMembership {
|
||||
pub user: Arc<User>,
|
||||
pub kind: proto::channel_member::Kind,
|
||||
pub admin: bool,
|
||||
pub role: proto::ChannelRole,
|
||||
}
|
||||
impl ChannelMembership {
|
||||
pub fn sort_key(&self) -> MembershipSortKey {
|
||||
MembershipSortKey {
|
||||
role_order: match self.role {
|
||||
proto::ChannelRole::Admin => 0,
|
||||
proto::ChannelRole::Member => 1,
|
||||
proto::ChannelRole::Banned => 2,
|
||||
proto::ChannelRole::Guest => 3,
|
||||
},
|
||||
kind_order: match self.kind {
|
||||
proto::channel_member::Kind::Member => 0,
|
||||
proto::channel_member::Kind::AncestorMember => 1,
|
||||
proto::channel_member::Kind::Invitee => 2,
|
||||
},
|
||||
username_order: self.user.github_login.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct MembershipSortKey<'a> {
|
||||
role_order: u8,
|
||||
kind_order: u8,
|
||||
username_order: &'a str,
|
||||
}
|
||||
|
||||
pub enum ChannelEvent {
|
||||
@@ -445,7 +471,7 @@ impl ChannelStore {
|
||||
insert_edge: parent_edge,
|
||||
channel_permissions: vec![ChannelPermission {
|
||||
channel_id,
|
||||
is_admin: true,
|
||||
role: ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
@@ -517,11 +543,30 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_channel_visibility(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|_, _| async move {
|
||||
let _ = client
|
||||
.request(proto::SetChannelVisibility {
|
||||
channel_id,
|
||||
visibility: visibility.into(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invite_member(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
@@ -535,7 +580,7 @@ impl ChannelStore {
|
||||
.request(proto::InviteChannelMember {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
role: role.into(),
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -579,11 +624,11 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_member_admin(
|
||||
pub fn set_member_role(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
@@ -594,10 +639,10 @@ impl ChannelStore {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::SetChannelMemberAdmin {
|
||||
.request(proto::SetChannelMemberRole {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
role: role.into(),
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -685,8 +730,8 @@ impl ChannelStore {
|
||||
.filter_map(|(user, member)| {
|
||||
Some(ChannelMembership {
|
||||
user,
|
||||
admin: member.admin,
|
||||
kind: proto::channel_member::Kind::from_i32(member.kind)?,
|
||||
role: member.role(),
|
||||
kind: member.kind(),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
@@ -881,6 +926,7 @@ impl ChannelStore {
|
||||
ix,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility(),
|
||||
name: channel.name,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
@@ -947,7 +993,7 @@ impl ChannelStore {
|
||||
}
|
||||
|
||||
for permission in payload.channel_permissions {
|
||||
if permission.is_admin {
|
||||
if permission.role() == proto::ChannelRole::Admin {
|
||||
self.channels_with_admin_privileges
|
||||
.insert(permission.channel_id);
|
||||
} else {
|
||||
|
||||
@@ -123,12 +123,15 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
|
||||
pub fn insert(&mut self, channel_proto: proto::Channel) {
|
||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||
Arc::make_mut(existing_channel).name = channel_proto.name;
|
||||
let existing_channel = Arc::make_mut(existing_channel);
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
existing_channel.name = channel_proto.name;
|
||||
} else {
|
||||
self.channels_by_id.insert(
|
||||
channel_proto.id,
|
||||
Arc::new(Channel {
|
||||
id: channel_proto.id,
|
||||
visibility: channel_proto.visibility(),
|
||||
name: channel_proto.name,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use gpui::{AppContext, ModelHandle, TestAppContext};
|
||||
use rpc::proto;
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
@@ -18,15 +18,17 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 1,
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
@@ -49,10 +51,12 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
proto::Channel {
|
||||
id: 3,
|
||||
name: "x".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
name: "y".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
insert_edge: vec![
|
||||
@@ -92,14 +96,17 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
proto::Channel {
|
||||
id: 0,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "c".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
insert_edge: vec![
|
||||
@@ -114,7 +121,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 0,
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
@@ -158,6 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
channels: vec![proto::Channel {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER REFERENCES rooms (id) NOT NULL,
|
||||
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
|
||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"host_connection_id" INTEGER,
|
||||
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
||||
@@ -192,7 +192,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
CREATE TABLE "channels" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now,
|
||||
"visibility" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
||||
@@ -226,6 +227,7 @@ CREATE TABLE "channel_members" (
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"admin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"role" VARCHAR,
|
||||
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE channel_members ADD COLUMN role TEXT;
|
||||
UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END;
|
||||
|
||||
ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'members';
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Add migration script here
|
||||
|
||||
ALTER TABLE projects
|
||||
DROP CONSTRAINT projects_room_id_fkey,
|
||||
ADD CONSTRAINT projects_room_id_fkey
|
||||
FOREIGN KEY (room_id)
|
||||
REFERENCES rooms (id)
|
||||
ON DELETE CASCADE;
|
||||
@@ -432,6 +432,7 @@ pub struct NewUserResult {
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::Result;
|
||||
use rpc::proto;
|
||||
use sea_orm::{entity::prelude::*, DbErr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -80,3 +81,101 @@ id_type!(SignupId);
|
||||
id_type!(UserId);
|
||||
id_type!(ChannelBufferCollaboratorId);
|
||||
id_type!(FlagId);
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||
pub enum ChannelRole {
|
||||
#[sea_orm(string_value = "admin")]
|
||||
Admin,
|
||||
#[sea_orm(string_value = "member")]
|
||||
#[default]
|
||||
Member,
|
||||
#[sea_orm(string_value = "guest")]
|
||||
Guest,
|
||||
#[sea_orm(string_value = "banned")]
|
||||
Banned,
|
||||
}
|
||||
|
||||
impl ChannelRole {
|
||||
pub fn should_override(&self, other: Self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin => matches!(other, Member | Banned | Guest),
|
||||
Member => matches!(other, Banned | Guest),
|
||||
Banned => matches!(other, Guest),
|
||||
Guest => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max(&self, other: Self) -> Self {
|
||||
if self.should_override(other) {
|
||||
*self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::ChannelRole> for ChannelRole {
|
||||
fn from(value: proto::ChannelRole) -> Self {
|
||||
match value {
|
||||
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||
proto::ChannelRole::Member => ChannelRole::Member,
|
||||
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<proto::ChannelRole> for ChannelRole {
|
||||
fn into(self) -> proto::ChannelRole {
|
||||
match self {
|
||||
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||
ChannelRole::Member => proto::ChannelRole::Member,
|
||||
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for ChannelRole {
|
||||
fn into(self) -> i32 {
|
||||
let proto: proto::ChannelRole = self.into();
|
||||
proto.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||
pub enum ChannelVisibility {
|
||||
#[sea_orm(string_value = "public")]
|
||||
Public,
|
||||
#[sea_orm(string_value = "members")]
|
||||
#[default]
|
||||
Members,
|
||||
}
|
||||
|
||||
impl From<proto::ChannelVisibility> for ChannelVisibility {
|
||||
fn from(value: proto::ChannelVisibility) -> Self {
|
||||
match value {
|
||||
proto::ChannelVisibility::Public => ChannelVisibility::Public,
|
||||
proto::ChannelVisibility::Members => ChannelVisibility::Members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<proto::ChannelVisibility> for ChannelVisibility {
|
||||
fn into(self) -> proto::ChannelVisibility {
|
||||
match self {
|
||||
ChannelVisibility::Public => proto::ChannelVisibility::Public,
|
||||
ChannelVisibility::Members => proto::ChannelVisibility::Members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for ChannelVisibility {
|
||||
fn into(self) -> i32 {
|
||||
let proto: proto::ChannelVisibility = self.into();
|
||||
proto.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +482,9 @@ impl Database {
|
||||
)
|
||||
.await?;
|
||||
|
||||
channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
||||
channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
let collaborators = self
|
||||
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -180,7 +180,9 @@ impl Database {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
||||
let mut channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
channel_members.retain(|member| !participant_user_ids.contains(member));
|
||||
|
||||
Ok((
|
||||
|
||||
@@ -53,7 +53,9 @@ impl Database {
|
||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let channel_members;
|
||||
if let Some(channel_id) = channel_id {
|
||||
channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
|
||||
channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?;
|
||||
} else {
|
||||
channel_members = Vec::new();
|
||||
|
||||
@@ -298,98 +300,139 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryParticipantIndices {
|
||||
ParticipantIndex,
|
||||
if channel_id.is_some() {
|
||||
Err(anyhow!("tried to join channel call directly"))?
|
||||
}
|
||||
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
.eq(room_id)
|
||||
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||
)
|
||||
.select_only()
|
||||
.column(room_participant::Column::ParticipantIndex)
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
|
||||
let participant_index = self
|
||||
.get_next_participant_index_internal(room_id, &*tx)
|
||||
.await?;
|
||||
|
||||
let mut participant_index = 0;
|
||||
while existing_participant_indices.contains(&participant_index) {
|
||||
participant_index += 1;
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
||||
.await?;
|
||||
|
||||
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
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))
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
calling_user_id: ActiveValue::set(user_id),
|
||||
calling_connection_id: ActiveValue::set(connection.id as i32),
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
..Default::default()
|
||||
}])
|
||||
.on_conflict(
|
||||
OnConflict::columns([room_participant::Column::UserId])
|
||||
.update_columns([
|
||||
room_participant::Column::AnsweringConnectionId,
|
||||
room_participant::Column::AnsweringConnectionServerId,
|
||||
room_participant::Column::AnsweringConnectionLost,
|
||||
room_participant::Column::ParticipantIndex,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
} else {
|
||||
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))
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("room does not exist or was already joined"))?;
|
||||
}
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("room does not exist or was already joined"))?;
|
||||
}
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Ok(JoinRoom {
|
||||
room,
|
||||
channel_id,
|
||||
channel_members,
|
||||
channel_id: None,
|
||||
channel_members: vec![],
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_next_participant_index_internal(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<i32> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryParticipantIndices {
|
||||
ParticipantIndex,
|
||||
}
|
||||
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
.eq(room_id)
|
||||
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||
)
|
||||
.select_only()
|
||||
.column(room_participant::Column::ParticipantIndex)
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut participant_index = 0;
|
||||
while existing_participant_indices.contains(&participant_index) {
|
||||
participant_index += 1;
|
||||
}
|
||||
|
||||
Ok(participant_index)
|
||||
}
|
||||
|
||||
pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result<Option<ChannelId>> {
|
||||
self.transaction(|tx| async move {
|
||||
let room: Option<room::Model> = room::Entity::find()
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(room.and_then(|room| room.channel_id))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn join_channel_room_internal(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<JoinRoom> {
|
||||
let participant_index = self
|
||||
.get_next_participant_index_internal(room_id, &*tx)
|
||||
.await?;
|
||||
|
||||
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
calling_user_id: ActiveValue::set(user_id),
|
||||
calling_connection_id: ActiveValue::set(connection.id as i32),
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
..Default::default()
|
||||
}])
|
||||
.on_conflict(
|
||||
OnConflict::columns([room_participant::Column::UserId])
|
||||
.update_columns([
|
||||
room_participant::Column::AnsweringConnectionId,
|
||||
room_participant::Column::AnsweringConnectionServerId,
|
||||
room_participant::Column::AnsweringConnectionLost,
|
||||
room_participant::Column::ParticipantIndex,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?;
|
||||
Ok(JoinRoom {
|
||||
room,
|
||||
channel_id: Some(channel_id),
|
||||
channel_members,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn rejoin_room(
|
||||
&self,
|
||||
rejoin_room: proto::RejoinRoom,
|
||||
@@ -681,7 +724,8 @@ impl Database {
|
||||
|
||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
self.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -839,7 +883,8 @@ impl Database {
|
||||
};
|
||||
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
self.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::db::ChannelId;
|
||||
use crate::db::{ChannelId, ChannelVisibility};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
@@ -7,6 +7,7 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId};
|
||||
use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "channel_members")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
@@ -9,7 +9,7 @@ pub struct Model {
|
||||
pub channel_id: ChannelId,
|
||||
pub user_id: UserId,
|
||||
pub accepted: bool,
|
||||
pub admin: bool,
|
||||
pub role: ChannelRole,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -159,6 +159,7 @@ fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)
|
||||
graph.channels.push(Channel {
|
||||
id: *id,
|
||||
name: name.to_string(),
|
||||
visibility: ChannelVisibility::Members,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -211,7 +211,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel, observer_id, user_id, false)
|
||||
db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.respond_to_channel_invite(channel, observer_id, true)
|
||||
|
||||
@@ -8,11 +8,14 @@ use crate::{
|
||||
db::{
|
||||
queries::channels::ChannelGraph,
|
||||
tests::{graph, TEST_RELEASE_CHANNEL},
|
||||
ChannelId, Database, NewUserParams,
|
||||
ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
|
||||
},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicI32, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
||||
|
||||
@@ -48,9 +51,9 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
// Make sure that people cannot read channels they haven't been invited to
|
||||
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(zed_id, b_id).await.is_err());
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -125,9 +128,13 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
);
|
||||
|
||||
// Update member permissions
|
||||
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
|
||||
let set_subchannel_admin = db
|
||||
.set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin)
|
||||
.await;
|
||||
assert!(set_subchannel_admin.is_err());
|
||||
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
|
||||
let set_channel_admin = db
|
||||
.set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin)
|
||||
.await;
|
||||
assert!(set_channel_admin.is_ok());
|
||||
|
||||
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||
@@ -150,7 +157,7 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
|
||||
// Remove a single channel
|
||||
db.delete_channel(crdb_id, a_id).await.unwrap();
|
||||
assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(crdb_id, a_id).await.is_err());
|
||||
|
||||
// Remove a channel tree
|
||||
let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
|
||||
@@ -158,9 +165,9 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
|
||||
assert_eq!(user_ids, &[a_id]);
|
||||
|
||||
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(rust_id, a_id).await.is_err());
|
||||
assert!(db.get_channel(cargo_id, a_id).await.is_err());
|
||||
assert!(db.get_channel(cargo_ra_id, a_id).await.is_err());
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
@@ -200,15 +207,11 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
.user_id;
|
||||
|
||||
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
let room_1 = db
|
||||
.get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// can join a room with membership to its channel
|
||||
let joined_room = db
|
||||
.join_room(
|
||||
room_1,
|
||||
let (joined_room, _) = db
|
||||
.join_channel(
|
||||
channel_1,
|
||||
user_1,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL,
|
||||
@@ -217,11 +220,12 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
assert_eq!(joined_room.room.participants.len(), 1);
|
||||
|
||||
let room_id = RoomId::from_proto(joined_room.room.id);
|
||||
drop(joined_room);
|
||||
// cannot join a room without membership to its channel
|
||||
assert!(db
|
||||
.join_room(
|
||||
room_1,
|
||||
room_id,
|
||||
user_2,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL
|
||||
@@ -239,58 +243,21 @@ test_both_dbs!(
|
||||
async fn test_channel_invites(db: &Arc<Database>) {
|
||||
db.create_server("test").await.unwrap();
|
||||
|
||||
let user_1 = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_2 = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user2".into(),
|
||||
github_user_id: 6,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let user_3 = db
|
||||
.create_user(
|
||||
"user3@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user3".into(),
|
||||
github_user_id: 7,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_1 = new_test_user(db, "user1@example.com").await;
|
||||
let user_2 = new_test_user(db, "user2@example.com").await;
|
||||
let user_3 = new_test_user(db, "user3@example.com").await;
|
||||
|
||||
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
|
||||
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, false)
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(channel_1_2, user_2, user_1, false)
|
||||
db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(channel_1_1, user_3, user_1, true)
|
||||
db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -314,27 +281,29 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
|
||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||
|
||||
let members = db
|
||||
.get_channel_member_details(channel_1_1, user_1)
|
||||
let mut members = db
|
||||
.get_channel_participant_details(channel_1_1, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
admin: false,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_3.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -349,7 +318,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let members = db
|
||||
.get_channel_member_details(channel_1_3, user_1)
|
||||
.get_channel_participant_details(channel_1_3, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -358,12 +327,12 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
admin: false,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -414,11 +383,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
||||
|
||||
let zed_archive_id = zed_id;
|
||||
|
||||
let (channel, _) = db
|
||||
.get_channel(zed_archive_id, user_1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let channel = db.get_channel(zed_archive_id, user_1).await.unwrap();
|
||||
assert_eq!(channel.name, "zed-archive");
|
||||
|
||||
let non_permissioned_rename = db
|
||||
@@ -846,6 +811,284 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_user_is_channel_participant,
|
||||
test_user_is_channel_participant_postgres,
|
||||
test_user_is_channel_participant_sqlite
|
||||
);
|
||||
|
||||
async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
let admin = new_test_user(db, "admin@example.com").await;
|
||||
let member = new_test_user(db, "member@example.com").await;
|
||||
let guest = new_test_user(db, "guest@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
let active_channel = db
|
||||
.create_channel("active", Some(zed_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
let vim_channel = db
|
||||
.create_channel("vim", Some(active_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(active_channel, member, admin, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.respond_to_channel_invite(active_channel, member, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, admin, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, member, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(vim_channel, guest, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_dag(channels, &[(vim_channel, None)]);
|
||||
let channels = db.get_channels_for_user(member).await.unwrap().channels;
|
||||
assert_dag(
|
||||
channels,
|
||||
&[(active_channel, None), (vim_channel, Some(active_channel))],
|
||||
);
|
||||
|
||||
db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Banned.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.remove_channel_member(vim_channel, guest, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// currently people invited to parent channels are not shown here
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(zed_channel, guest, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(zed_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(active_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.is_err(),);
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_dag(
|
||||
channels,
|
||||
&[(zed_channel, None), (vim_channel, Some(zed_channel))],
|
||||
)
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_user_joins_correct_channel,
|
||||
test_user_joins_correct_channel_postgres,
|
||||
test_user_joins_correct_channel_sqlite
|
||||
);
|
||||
|
||||
async fn test_user_joins_correct_channel(db: &Arc<Database>) {
|
||||
let admin = new_test_user(db, "admin@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
|
||||
let active_channel = db
|
||||
.create_channel("active", Some(zed_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim_channel = db
|
||||
.create_channel("vim", Some(active_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim2_channel = db
|
||||
.create_channel("vim2", Some(vim_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let most_public = db
|
||||
.transaction(
|
||||
|tx| async move { db.most_public_ancestor_for_channel(vim_channel, &*tx).await },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(most_public, Some(zed_channel))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
|
||||
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
|
||||
@@ -870,3 +1113,20 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)])
|
||||
|
||||
pretty_assertions::assert_eq!(actual_map, expected_map)
|
||||
}
|
||||
|
||||
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
|
||||
|
||||
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
|
||||
db.create_user(
|
||||
email,
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: email[0..email.find("@").unwrap()].to_string(),
|
||||
github_user_id: GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst),
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
db::{Database, MessageId, NewUserParams},
|
||||
db::{ChannelRole, Database, MessageId, NewUserParams},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -155,7 +155,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
|
||||
|
||||
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1, observer, user, false)
|
||||
db.invite_channel_member(channel_1, observer, user, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -163,7 +163,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_2, observer, user, false)
|
||||
db.invite_channel_member(channel_2, observer, user, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
+165
-87
@@ -3,8 +3,8 @@ mod connection_pool;
|
||||
use crate::{
|
||||
auth,
|
||||
db::{
|
||||
self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
|
||||
ServerId, User, UserId,
|
||||
self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, Database, MessageId,
|
||||
ProjectId, RoomId, ServerId, User, UserId,
|
||||
},
|
||||
executor::Executor,
|
||||
AppState, Result,
|
||||
@@ -255,7 +255,8 @@ impl Server {
|
||||
.add_request_handler(delete_channel)
|
||||
.add_request_handler(invite_channel_member)
|
||||
.add_request_handler(remove_channel_member)
|
||||
.add_request_handler(set_channel_member_admin)
|
||||
.add_request_handler(set_channel_member_role)
|
||||
.add_request_handler(set_channel_visibility)
|
||||
.add_request_handler(rename_channel)
|
||||
.add_request_handler(join_channel_buffer)
|
||||
.add_request_handler(leave_channel_buffer)
|
||||
@@ -977,6 +978,13 @@ async fn join_room(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let room_id = RoomId::from_proto(request.id);
|
||||
|
||||
let channel_id = session.db().await.channel_id_for_room(room_id).await?;
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
return join_channel_internal(channel_id, Box::new(response), session).await;
|
||||
}
|
||||
|
||||
let joined_room = {
|
||||
let room = session
|
||||
.db()
|
||||
@@ -992,16 +1000,6 @@ async fn join_room(
|
||||
room.into_inner()
|
||||
};
|
||||
|
||||
if let Some(channel_id) = joined_room.channel_id {
|
||||
channel_updated(
|
||||
channel_id,
|
||||
&joined_room.room,
|
||||
&joined_room.channel_members,
|
||||
&session.peer,
|
||||
&*session.connection_pool().await,
|
||||
)
|
||||
}
|
||||
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
.await
|
||||
@@ -1039,7 +1037,7 @@ async fn join_room(
|
||||
|
||||
response.send(proto::JoinRoomResponse {
|
||||
room: Some(joined_room.room),
|
||||
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
|
||||
channel_id: None,
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
@@ -2211,6 +2209,7 @@ async fn create_channel(
|
||||
let channel = proto::Channel {
|
||||
id: id.to_proto(),
|
||||
name: request.name,
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
};
|
||||
|
||||
response.send(proto::CreateChannelResponse {
|
||||
@@ -2283,17 +2282,20 @@ async fn invite_channel_member(
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let invitee_id = UserId::from_proto(request.user_id);
|
||||
db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
|
||||
.await?;
|
||||
db.invite_channel_member(
|
||||
channel_id,
|
||||
invitee_id,
|
||||
session.user_id,
|
||||
request.role().into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (channel, _) = db
|
||||
.get_channel(channel_id, session.user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
||||
let channel = db.get_channel(channel_id, session.user_id).await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
});
|
||||
for connection_id in session
|
||||
@@ -2335,27 +2337,63 @@ async fn remove_channel_member(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_channel_member_admin(
|
||||
request: proto::SetChannelMemberAdmin,
|
||||
response: Response<proto::SetChannelMemberAdmin>,
|
||||
async fn set_channel_visibility(
|
||||
request: proto::SetChannelVisibility,
|
||||
response: Response<proto::SetChannelVisibility>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let visibility = request.visibility().into();
|
||||
|
||||
let channel = db
|
||||
.set_channel_visibility(channel_id, visibility, session.user_id)
|
||||
.await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channels.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
visibility: channel.visibility.into(),
|
||||
});
|
||||
|
||||
let member_ids = db.get_channel_members(channel_id).await?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for member_id in member_ids {
|
||||
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_channel_member_role(
|
||||
request: proto::SetChannelMemberRole,
|
||||
response: Response<proto::SetChannelMemberRole>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let member_id = UserId::from_proto(request.user_id);
|
||||
db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
|
||||
let channel_member = db
|
||||
.set_channel_member_role(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
member_id,
|
||||
request.role().into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (channel, has_accepted) = db
|
||||
.get_channel(channel_id, member_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
||||
let channel = db.get_channel(channel_id, session.user_id).await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
if has_accepted {
|
||||
if channel_member.accepted {
|
||||
update.channel_permissions.push(proto::ChannelPermission {
|
||||
channel_id: channel.id.to_proto(),
|
||||
is_admin: request.admin,
|
||||
role: request.role,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2378,13 +2416,14 @@ async fn rename_channel(
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let new_name = db
|
||||
let channel = db
|
||||
.rename_channel(channel_id, session.user_id, &request.name)
|
||||
.await?;
|
||||
|
||||
let channel = proto::Channel {
|
||||
id: request.channel_id,
|
||||
name: new_name,
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
visibility: channel.visibility.into(),
|
||||
};
|
||||
response.send(proto::RenameChannelResponse {
|
||||
channel: Some(channel.clone()),
|
||||
@@ -2422,6 +2461,7 @@ async fn link_channel(
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
})
|
||||
.collect(),
|
||||
@@ -2513,6 +2553,7 @@ async fn move_channel(
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
})
|
||||
.collect(),
|
||||
@@ -2538,7 +2579,7 @@ async fn get_channel_members(
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let members = db
|
||||
.get_channel_member_details(channel_id, session.user_id)
|
||||
.get_channel_participant_details(channel_id, session.user_id)
|
||||
.await?;
|
||||
response.send(proto::GetChannelMembersResponse { members })?;
|
||||
Ok(())
|
||||
@@ -2554,53 +2595,68 @@ async fn respond_to_channel_invite(
|
||||
db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
|
||||
.await?;
|
||||
|
||||
if request.accept {
|
||||
channel_membership_updated(db, channel_id, &session).await?;
|
||||
} else {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channel_invitations
|
||||
.push(channel_id.to_proto());
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
}
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn channel_membership_updated(
|
||||
db: tokio::sync::MutexGuard<'_, DbHandle>,
|
||||
channel_id: ChannelId,
|
||||
session: &Session,
|
||||
) -> Result<(), crate::Error> {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channel_invitations
|
||||
.push(channel_id.to_proto());
|
||||
if request.accept {
|
||||
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
||||
update
|
||||
.channels
|
||||
.extend(
|
||||
result
|
||||
.channels
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
update.unseen_channel_messages = result.channel_messages;
|
||||
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
||||
update.insert_edge = result.channels.edges;
|
||||
update
|
||||
.channel_participants
|
||||
.extend(
|
||||
result
|
||||
.channel_participants
|
||||
.into_iter()
|
||||
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
||||
}),
|
||||
);
|
||||
update
|
||||
.channel_permissions
|
||||
.extend(
|
||||
result
|
||||
.channels_with_admin_privileges
|
||||
.into_iter()
|
||||
.map(|channel_id| proto::ChannelPermission {
|
||||
channel_id: channel_id.to_proto(),
|
||||
is_admin: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
||||
update.channels.extend(
|
||||
result
|
||||
.channels
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
update.unseen_channel_messages = result.channel_messages;
|
||||
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
||||
update.insert_edge = result.channels.edges;
|
||||
update
|
||||
.channel_participants
|
||||
.extend(
|
||||
result
|
||||
.channel_participants
|
||||
.into_iter()
|
||||
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
||||
}),
|
||||
);
|
||||
update
|
||||
.channel_permissions
|
||||
.extend(
|
||||
result
|
||||
.channels_with_admin_privileges
|
||||
.into_iter()
|
||||
.map(|channel_id| proto::ChannelPermission {
|
||||
channel_id: channel_id.to_proto(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}),
|
||||
);
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2610,19 +2666,35 @@ async fn join_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
join_channel_internal(channel_id, Box::new(response), session).await
|
||||
}
|
||||
|
||||
trait JoinChannelInternalResponse {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()>;
|
||||
}
|
||||
impl JoinChannelInternalResponse for Response<proto::JoinChannel> {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||
Response::<proto::JoinChannel>::send(self, result)
|
||||
}
|
||||
}
|
||||
impl JoinChannelInternalResponse for Response<proto::JoinRoom> {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||
Response::<proto::JoinRoom>::send(self, result)
|
||||
}
|
||||
}
|
||||
|
||||
async fn join_channel_internal(
|
||||
channel_id: ChannelId,
|
||||
response: Box<impl JoinChannelInternalResponse>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let joined_room = {
|
||||
leave_room_for_session(&session).await?;
|
||||
let db = session.db().await;
|
||||
|
||||
let room_id = db
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
|
||||
.await?;
|
||||
|
||||
let joined_room = db
|
||||
.join_room(
|
||||
room_id,
|
||||
let (joined_room, joined_channel) = db
|
||||
.join_channel(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
@@ -2649,9 +2721,13 @@ async fn join_channel(
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
if let Some(joined_channel) = joined_channel {
|
||||
channel_membership_updated(db, joined_channel, &session).await?
|
||||
}
|
||||
|
||||
room_updated(&joined_room.room, &session.peer);
|
||||
|
||||
joined_room.into_inner()
|
||||
joined_room
|
||||
};
|
||||
|
||||
channel_updated(
|
||||
@@ -2663,7 +2739,6 @@ async fn join_channel(
|
||||
);
|
||||
|
||||
update_user_contacts(session.user_id, &session).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3073,6 +3148,7 @@ fn build_initial_channels_update(
|
||||
update.channels.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
visibility: channel.visibility.into(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3097,7 +3173,7 @@ fn build_initial_channels_update(
|
||||
.into_iter()
|
||||
.map(|id| proto::ChannelPermission {
|
||||
channel_id: id.to_proto(),
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -3105,6 +3181,8 @@ fn build_initial_channels_update(
|
||||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
// TODO: Visibility
|
||||
visibility: ChannelVisibility::Public.into(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,10 @@ use collections::HashMap;
|
||||
use editor::{Anchor, Editor, ToOffset};
|
||||
use futures::future;
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
|
||||
use rpc::{
|
||||
proto::{self, PeerId},
|
||||
RECEIVE_TIMEOUT,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
@@ -445,6 +448,7 @@ fn channel(id: u64, name: &'static str) -> Channel {
|
||||
Channel {
|
||||
id,
|
||||
name: name.to_string(),
|
||||
visibility: proto::ChannelVisibility::Members,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ use call::ActiveCall;
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::User;
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||
use rpc::{proto, RECEIVE_TIMEOUT};
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -68,7 +71,12 @@ async fn test_core_channels(
|
||||
.update(cx_a, |store, cx| {
|
||||
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
|
||||
let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
|
||||
let invite = store.invite_member(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Make sure we're synchronously storing the pending invite
|
||||
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
@@ -103,12 +111,12 @@ async fn test_core_channels(
|
||||
&[
|
||||
(
|
||||
client_a.user_id().unwrap(),
|
||||
true,
|
||||
proto::ChannelRole::Admin,
|
||||
proto::channel_member::Kind::Member,
|
||||
),
|
||||
(
|
||||
client_b.user_id().unwrap(),
|
||||
false,
|
||||
proto::ChannelRole::Member,
|
||||
proto::channel_member::Kind::Invitee,
|
||||
),
|
||||
],
|
||||
@@ -183,7 +191,12 @@ async fn test_core_channels(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
|
||||
store.set_member_role(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -305,12 +318,12 @@ fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u
|
||||
#[track_caller]
|
||||
fn assert_members_eq(
|
||||
members: &[ChannelMembership],
|
||||
expected_members: &[(u64, bool, proto::channel_member::Kind)],
|
||||
expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)],
|
||||
) {
|
||||
assert_eq!(
|
||||
members
|
||||
.iter()
|
||||
.map(|member| (member.user.id, member.admin, member.kind))
|
||||
.map(|member| (member.user.id, member.role, member.kind))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_members
|
||||
);
|
||||
@@ -611,7 +624,12 @@ async fn test_permissions_update_while_invited(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
|
||||
channel_store.invite_member(
|
||||
rust_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -634,7 +652,12 @@ async fn test_permissions_update_while_invited(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
|
||||
channel_store.set_member_role(
|
||||
rust_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -803,7 +826,12 @@ async fn test_lost_channel_creation(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -884,6 +912,119 @@ async fn test_lost_channel_creation(
|
||||
],
|
||||
);
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_guest_access(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channels = server
|
||||
.make_channel_tree(&[("channel-a", None)], (&client_a, cx_a))
|
||||
.await;
|
||||
let channel_a_id = channels[0];
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert!(client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |channel_store, _| channel_store
|
||||
.channel_for_id(channel_a_id)
|
||||
.is_some()));
|
||||
|
||||
client_a.channel_store().update(cx_a, |channel_store, _| {
|
||||
let participants = channel_store.channel_participants(channel_a_id);
|
||||
assert_eq!(participants.len(), 1);
|
||||
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invite_access(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channels = server
|
||||
.make_channel_tree(
|
||||
&[("channel-a", None), ("channel-b", Some("channel-a"))],
|
||||
(&client_a, cx_a),
|
||||
)
|
||||
.await;
|
||||
let channel_a_id = channels[0];
|
||||
let channel_b_id = channels[0];
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||
assert!(channel_store.channel_for_id(channel_b_id).is_some());
|
||||
assert!(channel_store.channel_for_id(channel_a_id).is_some());
|
||||
});
|
||||
|
||||
client_a.channel_store().update(cx_a, |channel_store, _| {
|
||||
let participants = channel_store.channel_participants(channel_b_id);
|
||||
assert_eq!(participants.len(), 1);
|
||||
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_moving(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::db::ChannelRole;
|
||||
|
||||
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
@@ -50,7 +52,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
.await
|
||||
.unwrap();
|
||||
for user in &users[1..] {
|
||||
db.invite_channel_member(id, user.user_id, users[0].user_id, false)
|
||||
db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.respond_to_channel_invite(id, user.user_id, true)
|
||||
|
||||
@@ -18,7 +18,7 @@ use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
@@ -327,7 +327,7 @@ impl TestServer {
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
member_client.user_id().unwrap(),
|
||||
false,
|
||||
ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -616,7 +616,12 @@ impl TestClient {
|
||||
cx_self
|
||||
.read(ChannelStore::global)
|
||||
.update(cx_self, |channel_store, cx| {
|
||||
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
|
||||
channel_store.invite_member(
|
||||
channel,
|
||||
other_client.user_id().unwrap(),
|
||||
ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -11,7 +11,10 @@ use anyhow::Result;
|
||||
use call::ActiveCall;
|
||||
use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
|
||||
use channel_modal::ChannelModal;
|
||||
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, Contact, User, UserStore,
|
||||
};
|
||||
use contact_finder::ContactFinder;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
@@ -428,7 +431,7 @@ enum ListEntry {
|
||||
is_last: bool,
|
||||
},
|
||||
ParticipantScreen {
|
||||
peer_id: PeerId,
|
||||
peer_id: Option<PeerId>,
|
||||
is_last: bool,
|
||||
},
|
||||
IncomingRequest(Arc<User>),
|
||||
@@ -442,6 +445,9 @@ enum ListEntry {
|
||||
ChannelNotes {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
ChannelChat {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
ChannelEditor {
|
||||
depth: usize,
|
||||
},
|
||||
@@ -602,6 +608,13 @@ impl CollabPanel {
|
||||
ix,
|
||||
cx,
|
||||
),
|
||||
ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
|
||||
*channel_id,
|
||||
&theme.collab_panel,
|
||||
is_selected,
|
||||
ix,
|
||||
cx,
|
||||
),
|
||||
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
|
||||
channel.clone(),
|
||||
this.channel_store.clone(),
|
||||
@@ -804,7 +817,8 @@ impl CollabPanel {
|
||||
let room = room.read(cx);
|
||||
|
||||
if let Some(channel_id) = room.channel_id() {
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id })
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||
self.entries.push(ListEntry::ChannelChat { channel_id })
|
||||
}
|
||||
|
||||
// Populate the active user.
|
||||
@@ -836,7 +850,13 @@ impl CollabPanel {
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
host_user_id: user_id,
|
||||
is_last: projects.peek().is_none(),
|
||||
is_last: projects.peek().is_none() && !room.is_screen_sharing(),
|
||||
});
|
||||
}
|
||||
if room.is_screen_sharing() {
|
||||
self.entries.push(ListEntry::ParticipantScreen {
|
||||
peer_id: None,
|
||||
is_last: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -880,7 +900,7 @@ impl CollabPanel {
|
||||
}
|
||||
if !participant.video_tracks.is_empty() {
|
||||
self.entries.push(ListEntry::ParticipantScreen {
|
||||
peer_id: participant.peer_id,
|
||||
peer_id: Some(participant.peer_id),
|
||||
is_last: true,
|
||||
});
|
||||
}
|
||||
@@ -1225,14 +1245,18 @@ impl CollabPanel {
|
||||
) -> AnyElement<Self> {
|
||||
enum CallParticipant {}
|
||||
enum CallParticipantTooltip {}
|
||||
enum LeaveCallButton {}
|
||||
enum LeaveCallTooltip {}
|
||||
|
||||
let collab_theme = &theme.collab_panel;
|
||||
|
||||
let is_current_user =
|
||||
user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
|
||||
|
||||
let content =
|
||||
MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
|
||||
let content = MouseEventHandler::new::<CallParticipant, _>(
|
||||
user.id as usize,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let style = if is_current_user {
|
||||
*collab_theme
|
||||
.contact_row
|
||||
@@ -1268,14 +1292,32 @@ impl CollabPanel {
|
||||
Label::new("Calling", collab_theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(collab_theme.calling_indicator.container)
|
||||
.aligned(),
|
||||
.aligned()
|
||||
.into_any(),
|
||||
)
|
||||
} else if is_current_user {
|
||||
Some(
|
||||
Label::new("You", collab_theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(collab_theme.calling_indicator.container)
|
||||
.aligned(),
|
||||
MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme
|
||||
.collab_panel
|
||||
.leave_call_button
|
||||
.style_for(is_selected, state),
|
||||
"icons/exit.svg",
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
})
|
||||
.with_tooltip::<LeaveCallTooltip>(
|
||||
0,
|
||||
"Leave call",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -1284,7 +1326,8 @@ impl CollabPanel {
|
||||
.with_height(collab_theme.row_height)
|
||||
.contained()
|
||||
.with_style(style)
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if is_current_user || is_pending || peer_id.is_none() {
|
||||
return content.into_any();
|
||||
@@ -1406,7 +1449,7 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
fn render_participant_screen(
|
||||
peer_id: PeerId,
|
||||
peer_id: Option<PeerId>,
|
||||
is_last: bool,
|
||||
is_selected: bool,
|
||||
theme: &theme::CollabPanel,
|
||||
@@ -1421,8 +1464,8 @@ impl CollabPanel {
|
||||
.unwrap_or(0.);
|
||||
let tree_branch = theme.tree_branch;
|
||||
|
||||
MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||
peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||
@@ -1460,16 +1503,20 @@ impl CollabPanel {
|
||||
.contained()
|
||||
.with_style(row.container)
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(peer_id, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
);
|
||||
if peer_id.is_none() {
|
||||
return handler.into_any();
|
||||
}
|
||||
handler
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(peer_id.unwrap(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
@@ -1496,23 +1543,32 @@ impl CollabPanel {
|
||||
enum AddChannel {}
|
||||
|
||||
let tooltip_style = &theme.tooltip;
|
||||
let mut channel_link = None;
|
||||
let mut channel_tooltip_text = None;
|
||||
let mut channel_icon = None;
|
||||
|
||||
let text = match section {
|
||||
Section::ActiveCall => {
|
||||
let channel_name = iife!({
|
||||
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
|
||||
|
||||
let name = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_for_id(channel_id)?
|
||||
.name
|
||||
.as_str();
|
||||
let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
|
||||
|
||||
Some(name)
|
||||
channel_link = Some(channel.link());
|
||||
(channel_icon, channel_tooltip_text) = match channel.visibility {
|
||||
proto::ChannelVisibility::Public => {
|
||||
(Some("icons/public.svg"), Some("Copy public channel link."))
|
||||
}
|
||||
proto::ChannelVisibility::Members => {
|
||||
(Some("icons/hash.svg"), Some("Copy private channel link."))
|
||||
}
|
||||
};
|
||||
|
||||
Some(channel.name.as_str())
|
||||
});
|
||||
|
||||
if let Some(name) = channel_name {
|
||||
Cow::Owned(format!("#{}", name))
|
||||
Cow::Owned(format!("{}", name))
|
||||
} else {
|
||||
Cow::Borrowed("Current Call")
|
||||
}
|
||||
@@ -1527,28 +1583,30 @@ impl CollabPanel {
|
||||
|
||||
enum AddContact {}
|
||||
let button = match section {
|
||||
Section::ActiveCall => Some(
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link.clone();
|
||||
MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme
|
||||
.collab_panel
|
||||
.leave_call_button
|
||||
.style_for(is_selected, state),
|
||||
"icons/exit.svg",
|
||||
"icons/link.svg",
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
let item = ClipboardItem::new(channel_link_copy.clone());
|
||||
cx.write_to_clipboard(item)
|
||||
})
|
||||
.with_tooltip::<AddContact>(
|
||||
0,
|
||||
"Leave call",
|
||||
channel_tooltip_text.unwrap(),
|
||||
None,
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
@@ -1633,6 +1691,21 @@ impl CollabPanel {
|
||||
theme.collab_panel.contact_username.container.margin.left,
|
||||
),
|
||||
)
|
||||
} else if let Some(channel_icon) = channel_icon {
|
||||
Some(
|
||||
Svg::new(channel_icon)
|
||||
.with_color(header_style.text.color)
|
||||
.constrained()
|
||||
.with_max_width(icon_size)
|
||||
.with_max_height(icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(icon_size)
|
||||
.contained()
|
||||
.with_margin_right(
|
||||
theme.collab_panel.contact_username.container.margin.left,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
@@ -1908,6 +1981,12 @@ impl CollabPanel {
|
||||
let channel_id = channel.id;
|
||||
let collab_theme = &theme.collab_panel;
|
||||
let has_children = self.channel_store.read(cx).has_children(channel_id);
|
||||
let is_public = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_for_id(channel_id)
|
||||
.map(|channel| channel.visibility)
|
||||
== Some(proto::ChannelVisibility::Public);
|
||||
let other_selected =
|
||||
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
|
||||
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
|
||||
@@ -1965,12 +2044,16 @@ impl CollabPanel {
|
||||
|
||||
Flex::<Self>::row()
|
||||
.with_child(
|
||||
Svg::new("icons/hash.svg")
|
||||
.with_color(collab_theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
Svg::new(if is_public {
|
||||
"icons/public.svg"
|
||||
} else {
|
||||
"icons/hash.svg"
|
||||
})
|
||||
.with_color(collab_theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_child({
|
||||
let style = collab_theme.channel_name.inactive_state();
|
||||
@@ -2275,7 +2358,7 @@ impl CollabPanel {
|
||||
.with_child(render_tree_branch(
|
||||
tree_branch,
|
||||
&row.name.text,
|
||||
true,
|
||||
false,
|
||||
vec2f(host_avatar_width, theme.row_height),
|
||||
cx.font_cache(),
|
||||
))
|
||||
@@ -2308,6 +2391,62 @@ impl CollabPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_channel_chat(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
theme: &theme::CollabPanel,
|
||||
is_selected: bool,
|
||||
ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum ChannelChat {}
|
||||
let host_avatar_width = theme
|
||||
.contact_avatar
|
||||
.width
|
||||
.or(theme.contact_avatar.height)
|
||||
.unwrap_or(0.);
|
||||
|
||||
MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
|
||||
let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
|
||||
let row = theme.project_row.in_state(is_selected).style_for(state);
|
||||
|
||||
Flex::<Self>::row()
|
||||
.with_child(render_tree_branch(
|
||||
tree_branch,
|
||||
&row.name.text,
|
||||
true,
|
||||
vec2f(host_avatar_width, theme.row_height),
|
||||
cx.font_cache(),
|
||||
))
|
||||
.with_child(
|
||||
Svg::new("icons/conversations.svg")
|
||||
.with_color(theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new("chat", theme.channel_name.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.channel_name.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(*theme.channel_row.style_for(is_selected, state))
|
||||
.with_padding_left(theme.channel_row.default_style().padding.left)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_channel_invite(
|
||||
channel: Arc<Channel>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
@@ -2771,6 +2910,9 @@ impl CollabPanel {
|
||||
}
|
||||
}
|
||||
ListEntry::ParticipantScreen { peer_id, .. } => {
|
||||
let Some(peer_id) = peer_id else {
|
||||
return;
|
||||
};
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(*peer_id, cx)
|
||||
@@ -3498,6 +3640,14 @@ impl PartialEq for ListEntry {
|
||||
return channel_id == other_id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelChat { channel_id } => {
|
||||
if let ListEntry::ChannelChat {
|
||||
channel_id: other_id,
|
||||
} = other
|
||||
{
|
||||
return channel_id == other_id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelInvite(channel_1) => {
|
||||
if let ListEntry::ChannelInvite(channel_2) = other {
|
||||
return channel_1.id == channel_2.id;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::{proto, User, UserId, UserStore};
|
||||
use client::{
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
User, UserId, UserStore,
|
||||
};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
@@ -96,11 +100,14 @@ impl ChannelModal {
|
||||
let channel_id = self.channel_id;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if mode == Mode::ManageMembers {
|
||||
let members = channel_store
|
||||
let mut members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||
@@ -182,6 +189,81 @@ impl View for ChannelModal {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_visibility(
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
enum TogglePublic {}
|
||||
|
||||
if visibility == ChannelVisibility::Members {
|
||||
return Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.with_spacing(14.0)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
let style = theme.channel_link.style_for(state);
|
||||
Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(channel) =
|
||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
{
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
@@ -190,6 +272,7 @@ impl View for ChannelModal {
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
.with_child(Flex::row().with_children([
|
||||
render_mode_button::<InviteMembers>(
|
||||
Mode::InviteMembers,
|
||||
@@ -343,9 +426,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
|
||||
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
|
||||
Mode::ManageMembers => {
|
||||
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
|
||||
}
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_selected_member(cx);
|
||||
@@ -373,7 +458,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, admin) = self.user_at_index(ix).unwrap();
|
||||
let (user, role) = self.user_at_index(ix).unwrap();
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
@@ -409,15 +494,25 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_children(admin.and_then(|admin| {
|
||||
(in_manage && admin).then(|| {
|
||||
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
Some(
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
})
|
||||
}))
|
||||
.left(),
|
||||
)
|
||||
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
Some(
|
||||
Label::new("Guest", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
@@ -502,13 +597,13 @@ impl ChannelModalDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
||||
let channel_membership = self.members.get(*ix)?;
|
||||
Some((
|
||||
channel_membership.user.clone(),
|
||||
Some(channel_membership.admin),
|
||||
Some(channel_membership.role),
|
||||
))
|
||||
}),
|
||||
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
||||
@@ -516,17 +611,21 @@ impl ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, admin) = self.user_at_index(self.selected_index)?;
|
||||
let admin = !admin.unwrap_or(false);
|
||||
let (user, role) = self.user_at_index(self.selected_index)?;
|
||||
let new_role = if role == Some(ChannelRole::Admin) {
|
||||
ChannelRole::Member
|
||||
} else {
|
||||
ChannelRole::Admin
|
||||
};
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_admin(self.channel_id, user.id, admin, cx)
|
||||
store.set_member_role(self.channel_id, user.id, new_role, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.admin = admin;
|
||||
member.role = new_role;
|
||||
}
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
@@ -572,25 +671,30 @@ impl ChannelModalDelegate {
|
||||
|
||||
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||
store.invite_member(self.channel_id, user.id, false, cx)
|
||||
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
invite_member.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate_mut().members.push(ChannelMembership {
|
||||
let new_member = ChannelMembership {
|
||||
user,
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
admin: false,
|
||||
});
|
||||
role: ChannelRole::Member,
|
||||
};
|
||||
let members = &mut this.delegate_mut().members;
|
||||
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
@@ -598,7 +702,7 @@ impl ChannelModalDelegate {
|
||||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if user_is_admin {
|
||||
if role == ChannelRole::Admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
|
||||
@@ -19,6 +19,7 @@ settings = { path = "../settings" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
zed-actions = { path = "../zed-actions" }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
||||
@@ -6,8 +6,12 @@ use gpui::{
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::cmp::{self, Reverse};
|
||||
use util::ResultExt;
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(toggle_command_palette);
|
||||
@@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
)
|
||||
.await
|
||||
};
|
||||
let intercept_result = cx.read(|cx| {
|
||||
let mut intercept_result = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteInterceptor>() {
|
||||
cx.global::<CommandPaletteInterceptor>()(&query, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Dev {
|
||||
if parse_zed_link(&query).is_some() {
|
||||
intercept_result = Some(CommandInterceptResult {
|
||||
action: OpenZedURL { url: query.clone() }.boxed_clone(),
|
||||
string: query.clone(),
|
||||
positions: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
|
||||
@@ -146,7 +146,7 @@ message Envelope {
|
||||
DeleteChannel delete_channel = 120;
|
||||
GetChannelMembers get_channel_members = 121;
|
||||
GetChannelMembersResponse get_channel_members_response = 122;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 123;
|
||||
SetChannelMemberRole set_channel_member_role = 123;
|
||||
RenameChannel rename_channel = 124;
|
||||
RenameChannelResponse rename_channel_response = 125;
|
||||
|
||||
@@ -172,7 +172,8 @@ message Envelope {
|
||||
|
||||
LinkChannel link_channel = 142;
|
||||
UnlinkChannel unlink_channel = 143;
|
||||
MoveChannel move_channel = 144; // current max: 146
|
||||
MoveChannel move_channel = 144;
|
||||
SetChannelVisibility set_channel_visibility = 147; // current max: 147
|
||||
}
|
||||
}
|
||||
|
||||
@@ -992,7 +993,7 @@ message ChannelEdge {
|
||||
|
||||
message ChannelPermission {
|
||||
uint64 channel_id = 1;
|
||||
bool is_admin = 2;
|
||||
ChannelRole role = 3;
|
||||
}
|
||||
|
||||
message ChannelParticipants {
|
||||
@@ -1018,8 +1019,8 @@ message GetChannelMembersResponse {
|
||||
|
||||
message ChannelMember {
|
||||
uint64 user_id = 1;
|
||||
bool admin = 2;
|
||||
Kind kind = 3;
|
||||
ChannelRole role = 4;
|
||||
|
||||
enum Kind {
|
||||
Member = 0;
|
||||
@@ -1041,7 +1042,7 @@ message CreateChannelResponse {
|
||||
message InviteChannelMember {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
bool admin = 3;
|
||||
ChannelRole role = 4;
|
||||
}
|
||||
|
||||
message RemoveChannelMember {
|
||||
@@ -1049,10 +1050,22 @@ message RemoveChannelMember {
|
||||
uint64 user_id = 2;
|
||||
}
|
||||
|
||||
message SetChannelMemberAdmin {
|
||||
enum ChannelRole {
|
||||
Admin = 0;
|
||||
Member = 1;
|
||||
Guest = 2;
|
||||
Banned = 3;
|
||||
}
|
||||
|
||||
message SetChannelMemberRole {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
bool admin = 3;
|
||||
ChannelRole role = 3;
|
||||
}
|
||||
|
||||
message SetChannelVisibility {
|
||||
uint64 channel_id = 1;
|
||||
ChannelVisibility visibility = 2;
|
||||
}
|
||||
|
||||
message RenameChannel {
|
||||
@@ -1546,9 +1559,15 @@ message Nonce {
|
||||
uint64 lower_half = 2;
|
||||
}
|
||||
|
||||
enum ChannelVisibility {
|
||||
Public = 0;
|
||||
Members = 1;
|
||||
}
|
||||
|
||||
message Channel {
|
||||
uint64 id = 1;
|
||||
string name = 2;
|
||||
ChannelVisibility visibility = 3;
|
||||
}
|
||||
|
||||
message Contact {
|
||||
|
||||
@@ -232,7 +232,8 @@ messages!(
|
||||
(SaveBuffer, Foreground),
|
||||
(RenameChannel, Foreground),
|
||||
(RenameChannelResponse, Foreground),
|
||||
(SetChannelMemberAdmin, Foreground),
|
||||
(SetChannelMemberRole, Foreground),
|
||||
(SetChannelVisibility, Foreground),
|
||||
(SearchProject, Background),
|
||||
(SearchProjectResponse, Background),
|
||||
(ShareProject, Foreground),
|
||||
@@ -332,7 +333,8 @@ request_messages!(
|
||||
(RemoveContact, Ack),
|
||||
(RespondToContactRequest, Ack),
|
||||
(RespondToChannelInvite, Ack),
|
||||
(SetChannelMemberAdmin, Ack),
|
||||
(SetChannelMemberRole, Ack),
|
||||
(SetChannelVisibility, Ack),
|
||||
(SendChannelMessage, SendChannelMessageResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannelMembers, GetChannelMembersResponse),
|
||||
|
||||
@@ -286,6 +286,8 @@ pub struct TabbedModal {
|
||||
pub header: ContainerStyle,
|
||||
pub body: ContainerStyle,
|
||||
pub title: ContainedText,
|
||||
pub visibility_toggle: Interactive<ContainedText>,
|
||||
pub channel_link: Interactive<ContainedText>,
|
||||
pub picker: Picker,
|
||||
pub max_height: f32,
|
||||
pub max_width: f32,
|
||||
|
||||
@@ -289,6 +289,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
cx.add_global_action(restart);
|
||||
cx.add_async_action(Workspace::save_all);
|
||||
cx.add_action(Workspace::add_folder_to_project);
|
||||
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
|
||||
@@ -8,3 +8,4 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use gpui::actions;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{actions, impl_actions};
|
||||
use serde::Deserialize;
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
@@ -26,3 +29,13 @@ actions!(
|
||||
ResetDatabase,
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Deserialize, Clone, PartialEq)]
|
||||
pub struct OpenBrowser {
|
||||
pub url: Arc<str>,
|
||||
}
|
||||
#[derive(Deserialize, Clone, PartialEq)]
|
||||
pub struct OpenZedURL {
|
||||
pub url: String,
|
||||
}
|
||||
impl_actions!(zed, [OpenBrowser, OpenZedURL]);
|
||||
|
||||
+6
-200
@@ -3,22 +3,16 @@
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use backtrace::Backtrace;
|
||||
use cli::{
|
||||
ipc::{self, IpcSender},
|
||||
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
|
||||
};
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
use client::{
|
||||
self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
||||
};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use editor::Editor;
|
||||
use futures::StreamExt;
|
||||
use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
|
||||
use isahc::{config::Configurable, Request};
|
||||
use language::{LanguageRegistry, Point};
|
||||
use language::LanguageRegistry;
|
||||
use log::LevelFilter;
|
||||
use node_runtime::RealNodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
@@ -28,7 +22,6 @@ use settings::{default_settings, handle_settings_file_changes, watch_config_file
|
||||
use simplelog::ConfigBuilder;
|
||||
use smol::process::Command;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs::OpenOptions,
|
||||
@@ -42,11 +35,9 @@ use std::{
|
||||
thread,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel},
|
||||
http::{self, HttpClient},
|
||||
paths::PathLikeWithPosition,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||
@@ -58,12 +49,9 @@ use zed::{
|
||||
assets::Assets,
|
||||
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
||||
only_instance::{ensure_only_instance, IsOnlyInstance},
|
||||
open_listener::{handle_cli_connection, OpenListener, OpenRequest},
|
||||
};
|
||||
|
||||
use crate::open_listener::{OpenListener, OpenRequest};
|
||||
|
||||
mod open_listener;
|
||||
|
||||
fn main() {
|
||||
let http = http::client();
|
||||
init_paths();
|
||||
@@ -113,6 +101,7 @@ fn main() {
|
||||
|
||||
app.run(move |cx| {
|
||||
cx.set_global(*RELEASE_CHANNEL);
|
||||
cx.set_global(listener.clone());
|
||||
|
||||
let mut store = SettingsStore::default();
|
||||
store
|
||||
@@ -735,189 +724,6 @@ async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()>
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
|
||||
|
||||
fn connect_to_cli(
|
||||
server_name: &str,
|
||||
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
||||
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
|
||||
.context("error connecting to cli")?;
|
||||
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
|
||||
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
|
||||
|
||||
handshake_tx
|
||||
.send(IpcHandshake {
|
||||
requests: request_tx,
|
||||
responses: response_rx,
|
||||
})
|
||||
.context("error sending ipc handshake")?;
|
||||
|
||||
let (mut async_request_tx, async_request_rx) =
|
||||
futures::channel::mpsc::channel::<CliRequest>(16);
|
||||
thread::spawn(move || {
|
||||
while let Ok(cli_request) = request_rx.recv() {
|
||||
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
|
||||
Ok((async_request_rx, response_tx))
|
||||
}
|
||||
|
||||
async fn handle_cli_connection(
|
||||
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
|
||||
app_state: Arc<AppState>,
|
||||
mut cx: AsyncAppContext,
|
||||
) {
|
||||
if let Some(request) = requests.next().await {
|
||||
match request {
|
||||
CliRequest::Open { paths, wait } => {
|
||||
let mut caret_positions = HashMap::new();
|
||||
|
||||
let paths = if paths.is_empty() {
|
||||
workspace::last_opened_workspace_paths()
|
||||
.await
|
||||
.map(|location| location.paths().to_vec())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
paths
|
||||
.into_iter()
|
||||
.filter_map(|path_with_position_string| {
|
||||
let path_with_position = PathLikeWithPosition::parse_str(
|
||||
&path_with_position_string,
|
||||
|path_str| {
|
||||
Ok::<_, std::convert::Infallible>(
|
||||
Path::new(path_str).to_path_buf(),
|
||||
)
|
||||
},
|
||||
)
|
||||
.expect("Infallible");
|
||||
let path = path_with_position.path_like;
|
||||
if let Some(row) = path_with_position.row {
|
||||
if path.is_file() {
|
||||
let row = row.saturating_sub(1);
|
||||
let col =
|
||||
path_with_position.column.unwrap_or(0).saturating_sub(1);
|
||||
caret_positions.insert(path.clone(), Point::new(row, col));
|
||||
}
|
||||
}
|
||||
Some(path)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut errored = false;
|
||||
match cx
|
||||
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.await
|
||||
{
|
||||
Ok((workspace, items)) => {
|
||||
let mut item_release_futures = Vec::new();
|
||||
|
||||
for (item, path) in items.into_iter().zip(&paths) {
|
||||
match item {
|
||||
Some(Ok(item)) => {
|
||||
if let Some(point) = caret_positions.remove(path) {
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let snapshot =
|
||||
editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(point, Bias::Left);
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::center()),
|
||||
cx,
|
||||
|s| s.select_ranges([point..point]),
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
let released = oneshot::channel();
|
||||
cx.update(|cx| {
|
||||
item.on_release(
|
||||
cx,
|
||||
Box::new(move |_| {
|
||||
let _ = released.0.send(());
|
||||
}),
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
item_release_futures.push(released.1);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {:?}: {}", path, err),
|
||||
})
|
||||
.log_err();
|
||||
errored = true;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if wait {
|
||||
let background = cx.background();
|
||||
let wait = async move {
|
||||
if paths.is_empty() {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
if let Some(workspace) = workspace.upgrade(&cx) {
|
||||
let _subscription = cx.update(|cx| {
|
||||
cx.observe_release(&workspace, move |_, _| {
|
||||
let _ = done_tx.send(());
|
||||
})
|
||||
});
|
||||
drop(workspace);
|
||||
let _ = done_rx.await;
|
||||
}
|
||||
} else {
|
||||
let _ =
|
||||
futures::future::try_join_all(item_release_futures).await;
|
||||
};
|
||||
}
|
||||
.fuse();
|
||||
futures::pin_mut!(wait);
|
||||
|
||||
loop {
|
||||
// Repeatedly check if CLI is still open to avoid wasting resources
|
||||
// waiting for files or workspaces to close.
|
||||
let mut timer = background.timer(Duration::from_secs(1)).fuse();
|
||||
futures::select_biased! {
|
||||
_ = wait => break,
|
||||
_ = timer => {
|
||||
if responses.send(CliResponse::Ping).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
errored = true;
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {:?}: {}", paths, error),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
responses
|
||||
.send(CliResponse::Exit {
|
||||
status: i32::from(errored),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
||||
&[
|
||||
("Go to file", &file_finder::Toggle),
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
use anyhow::anyhow;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use cli::{ipc, IpcHandshake};
|
||||
use cli::{ipc::IpcSender, CliRequest, CliResponse};
|
||||
use futures::channel::mpsc;
|
||||
use editor::scroll::autoscroll::Autoscroll;
|
||||
use editor::Editor;
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use gpui::AsyncAppContext;
|
||||
use language::{Bias, Point};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::prelude::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{path::PathBuf, sync::atomic::AtomicBool};
|
||||
use util::channel::parse_zed_link;
|
||||
use util::paths::PathLikeWithPosition;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::connect_to_cli;
|
||||
use workspace::AppState;
|
||||
|
||||
pub enum OpenRequest {
|
||||
Paths {
|
||||
@@ -96,3 +107,186 @@ impl OpenListener {
|
||||
Some(OpenRequest::Paths { paths })
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_to_cli(
|
||||
server_name: &str,
|
||||
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
||||
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
|
||||
.context("error connecting to cli")?;
|
||||
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
|
||||
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
|
||||
|
||||
handshake_tx
|
||||
.send(IpcHandshake {
|
||||
requests: request_tx,
|
||||
responses: response_rx,
|
||||
})
|
||||
.context("error sending ipc handshake")?;
|
||||
|
||||
let (mut async_request_tx, async_request_rx) =
|
||||
futures::channel::mpsc::channel::<CliRequest>(16);
|
||||
thread::spawn(move || {
|
||||
while let Ok(cli_request) = request_rx.recv() {
|
||||
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
|
||||
Ok((async_request_rx, response_tx))
|
||||
}
|
||||
|
||||
pub async fn handle_cli_connection(
|
||||
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
|
||||
app_state: Arc<AppState>,
|
||||
mut cx: AsyncAppContext,
|
||||
) {
|
||||
if let Some(request) = requests.next().await {
|
||||
match request {
|
||||
CliRequest::Open { paths, wait } => {
|
||||
let mut caret_positions = HashMap::new();
|
||||
|
||||
let paths = if paths.is_empty() {
|
||||
workspace::last_opened_workspace_paths()
|
||||
.await
|
||||
.map(|location| location.paths().to_vec())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
paths
|
||||
.into_iter()
|
||||
.filter_map(|path_with_position_string| {
|
||||
let path_with_position = PathLikeWithPosition::parse_str(
|
||||
&path_with_position_string,
|
||||
|path_str| {
|
||||
Ok::<_, std::convert::Infallible>(
|
||||
Path::new(path_str).to_path_buf(),
|
||||
)
|
||||
},
|
||||
)
|
||||
.expect("Infallible");
|
||||
let path = path_with_position.path_like;
|
||||
if let Some(row) = path_with_position.row {
|
||||
if path.is_file() {
|
||||
let row = row.saturating_sub(1);
|
||||
let col =
|
||||
path_with_position.column.unwrap_or(0).saturating_sub(1);
|
||||
caret_positions.insert(path.clone(), Point::new(row, col));
|
||||
}
|
||||
}
|
||||
Some(path)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut errored = false;
|
||||
match cx
|
||||
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.await
|
||||
{
|
||||
Ok((workspace, items)) => {
|
||||
let mut item_release_futures = Vec::new();
|
||||
|
||||
for (item, path) in items.into_iter().zip(&paths) {
|
||||
match item {
|
||||
Some(Ok(item)) => {
|
||||
if let Some(point) = caret_positions.remove(path) {
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let snapshot =
|
||||
editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(point, Bias::Left);
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::center()),
|
||||
cx,
|
||||
|s| s.select_ranges([point..point]),
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
let released = oneshot::channel();
|
||||
cx.update(|cx| {
|
||||
item.on_release(
|
||||
cx,
|
||||
Box::new(move |_| {
|
||||
let _ = released.0.send(());
|
||||
}),
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
item_release_futures.push(released.1);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {:?}: {}", path, err),
|
||||
})
|
||||
.log_err();
|
||||
errored = true;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if wait {
|
||||
let background = cx.background();
|
||||
let wait = async move {
|
||||
if paths.is_empty() {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
if let Some(workspace) = workspace.upgrade(&cx) {
|
||||
let _subscription = cx.update(|cx| {
|
||||
cx.observe_release(&workspace, move |_, _| {
|
||||
let _ = done_tx.send(());
|
||||
})
|
||||
});
|
||||
drop(workspace);
|
||||
let _ = done_rx.await;
|
||||
}
|
||||
} else {
|
||||
let _ =
|
||||
futures::future::try_join_all(item_release_futures).await;
|
||||
};
|
||||
}
|
||||
.fuse();
|
||||
futures::pin_mut!(wait);
|
||||
|
||||
loop {
|
||||
// Repeatedly check if CLI is still open to avoid wasting resources
|
||||
// waiting for files or workspaces to close.
|
||||
let mut timer = background.timer(Duration::from_secs(1)).fuse();
|
||||
futures::select_biased! {
|
||||
_ = wait => break,
|
||||
_ = timer => {
|
||||
if responses.send(CliResponse::Ping).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
errored = true;
|
||||
responses
|
||||
.send(CliResponse::Stderr {
|
||||
message: format!("error opening {:?}: {}", paths, error),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
responses
|
||||
.send(CliResponse::Exit {
|
||||
status: i32::from(errored),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod assets;
|
||||
pub mod languages;
|
||||
pub mod menus;
|
||||
pub mod only_instance;
|
||||
pub mod open_listener;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
@@ -28,6 +29,7 @@ use gpui::{
|
||||
AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
|
||||
};
|
||||
pub use lsp;
|
||||
use open_listener::OpenListener;
|
||||
pub use project;
|
||||
use project_panel::ProjectPanel;
|
||||
use quick_action_bar::QuickActionBar;
|
||||
@@ -87,6 +89,10 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||
},
|
||||
);
|
||||
cx.add_global_action(quit);
|
||||
cx.add_global_action(move |action: &OpenZedURL, cx| {
|
||||
cx.global::<Arc<OpenListener>>()
|
||||
.open_urls(vec![action.url.clone()])
|
||||
});
|
||||
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
||||
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
||||
theme::adjust_font_size(cx, |size| *size += 1.0)
|
||||
|
||||
Reference in New Issue
Block a user