diff --git a/Cargo.lock b/Cargo.lock
index cd9dee0bda..ecbe076711 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1625,6 +1625,7 @@ dependencies = [
"theme",
"util",
"workspace",
+ "zed-actions",
]
[[package]]
@@ -10174,6 +10175,7 @@ name = "zed-actions"
version = "0.1.0"
dependencies = [
"gpui",
+ "serde",
]
[[package]]
diff --git a/Procfile b/Procfile
index 2eb7de20fb..3f42c3a967 100644
--- a/Procfile
+++ b/Procfile
@@ -1,4 +1,4 @@
web: cd ../zed.dev && PORT=3000 npm run dev
-collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
+collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
livekit: livekit-server --dev
postgrest: postgrest crates/collab/admin_api.conf
diff --git a/assets/icons/link.svg b/assets/icons/link.svg
new file mode 100644
index 0000000000..4925bd8e00
--- /dev/null
+++ b/assets/icons/link.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/public.svg b/assets/icons/public.svg
new file mode 100644
index 0000000000..38278cdaba
--- /dev/null
+++ b/assets/icons/public.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs
index bceb2c094d..9c80dcc2b7 100644
--- a/crates/channel/src/channel_store.rs
+++ b/crates/channel/src/channel_store.rs
@@ -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,
}
@@ -79,7 +80,32 @@ pub struct ChannelPath(Arc<[ChannelId]>);
pub struct ChannelMembership {
pub user: Arc,
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,
+ ) -> Task> {
+ 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,
) -> Task> {
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,
) -> Task> {
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 {
diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs
index bf0de1b644..36379a3942 100644
--- a/crates/channel/src/channel_store/channel_index.rs
+++ b/crates/channel/src/channel_store/channel_index.rs
@@ -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,
diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs
index 9303a52092..23f2e11a03 100644
--- a/crates/channel/src/channel_store_tests.rs
+++ b/crates/channel/src/channel_store_tests.rs
@@ -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()
});
diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
index 5a84bfd796..8eb6b52fd8 100644
--- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
+++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
@@ -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
);
diff --git a/crates/collab/migrations/20231011214412_add_guest_role.sql b/crates/collab/migrations/20231011214412_add_guest_role.sql
new file mode 100644
index 0000000000..1713547158
--- /dev/null
+++ b/crates/collab/migrations/20231011214412_add_guest_role.sql
@@ -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';
diff --git a/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql b/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql
new file mode 100644
index 0000000000..be535ff7fa
--- /dev/null
+++ b/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql
@@ -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;
diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs
index e60b7cc33d..08f78c685d 100644
--- a/crates/collab/src/db.rs
+++ b/crates/collab/src/db.rs
@@ -432,6 +432,7 @@ pub struct NewUserResult {
pub struct Channel {
pub id: ChannelId,
pub name: String,
+ pub visibility: ChannelVisibility,
}
#[derive(Debug, PartialEq)]
diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs
index 23bb9e53bf..f0de4c255e 100644
--- a/crates/collab/src/db/ids.rs
+++ b/crates/collab/src/db/ids.rs
@@ -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 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 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 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 for ChannelVisibility {
+ fn from(value: proto::ChannelVisibility) -> Self {
+ match value {
+ proto::ChannelVisibility::Public => ChannelVisibility::Public,
+ proto::ChannelVisibility::Members => ChannelVisibility::Members,
+ }
+ }
+}
+
+impl Into for ChannelVisibility {
+ fn into(self) -> proto::ChannelVisibility {
+ match self {
+ ChannelVisibility::Public => proto::ChannelVisibility::Public,
+ ChannelVisibility::Members => proto::ChannelVisibility::Members,
+ }
+ }
+}
+
+impl Into for ChannelVisibility {
+ fn into(self) -> i32 {
+ let proto: proto::ChannelVisibility = self.into();
+ proto.into()
+ }
+}
diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs
index c85432f2bb..69f100e6b8 100644
--- a/crates/collab/src/db/queries/buffers.rs
+++ b/crates/collab/src/db/queries/buffers.rs
@@ -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?;
diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs
index c576d2406b..ee989b2ea0 100644
--- a/crates/collab/src/db/queries/channels.rs
+++ b/crates/collab/src/db/queries/channels.rs
@@ -1,8 +1,5 @@
use super::*;
-use rpc::proto::ChannelEdge;
-use smallvec::SmallVec;
-
-type ChannelDescendants = HashMap>;
+use rpc::proto::{channel_member::Kind, ChannelEdge};
impl Database {
#[cfg(test)]
@@ -37,8 +34,9 @@ impl Database {
}
let channel = channel::ActiveModel {
+ id: ActiveValue::NotSet,
name: ActiveValue::Set(name.to_string()),
- ..Default::default()
+ visibility: ActiveValue::Set(ChannelVisibility::Members),
}
.insert(&*tx)
.await?;
@@ -74,11 +72,11 @@ impl Database {
}
channel_member::ActiveModel {
+ id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel.id),
user_id: ActiveValue::Set(creator_id),
accepted: ActiveValue::Set(true),
- admin: ActiveValue::Set(true),
- ..Default::default()
+ role: ActiveValue::Set(ChannelRole::Admin),
}
.insert(&*tx)
.await?;
@@ -88,6 +86,116 @@ impl Database {
.await
}
+ pub async fn join_channel(
+ &self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ connection: ConnectionId,
+ environment: &str,
+ ) -> Result<(JoinRoom, Option)> {
+ self.transaction(move |tx| async move {
+ let mut joined_channel_id = None;
+
+ let channel = channel::Entity::find()
+ .filter(channel::Column::Id.eq(channel_id))
+ .one(&*tx)
+ .await?;
+
+ let mut role = self
+ .channel_role_for_user(channel_id, user_id, &*tx)
+ .await?;
+
+ if role.is_none() && channel.is_some() {
+ if let Some(invitation) = self
+ .pending_invite_for_channel(channel_id, user_id, &*tx)
+ .await?
+ {
+ // note, this may be a parent channel
+ joined_channel_id = Some(invitation.channel_id);
+ role = Some(invitation.role);
+
+ channel_member::Entity::update(channel_member::ActiveModel {
+ accepted: ActiveValue::Set(true),
+ ..invitation.into_active_model()
+ })
+ .exec(&*tx)
+ .await?;
+
+ debug_assert!(
+ self.channel_role_for_user(channel_id, user_id, &*tx)
+ .await?
+ == role
+ );
+ }
+ }
+ if role.is_none()
+ && channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public)
+ {
+ let channel_id_to_join = self
+ .most_public_ancestor_for_channel(channel_id, &*tx)
+ .await?
+ .unwrap_or(channel_id);
+ // TODO: change this back to Guest.
+ role = Some(ChannelRole::Member);
+ joined_channel_id = Some(channel_id_to_join);
+
+ channel_member::Entity::insert(channel_member::ActiveModel {
+ id: ActiveValue::NotSet,
+ channel_id: ActiveValue::Set(channel_id_to_join),
+ user_id: ActiveValue::Set(user_id),
+ accepted: ActiveValue::Set(true),
+ // TODO: change this back to Guest.
+ role: ActiveValue::Set(ChannelRole::Member),
+ })
+ .exec(&*tx)
+ .await?;
+
+ debug_assert!(
+ self.channel_role_for_user(channel_id, user_id, &*tx)
+ .await?
+ == role
+ );
+ }
+
+ if channel.is_none() || role.is_none() || role == Some(ChannelRole::Banned) {
+ Err(anyhow!("no such channel, or not allowed"))?
+ }
+
+ let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
+ let room_id = self
+ .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
+ .await?;
+
+ self.join_channel_room_internal(channel_id, room_id, user_id, connection, &*tx)
+ .await
+ .map(|jr| (jr, joined_channel_id))
+ })
+ .await
+ }
+
+ pub async fn set_channel_visibility(
+ &self,
+ channel_id: ChannelId,
+ visibility: ChannelVisibility,
+ user_id: UserId,
+ ) -> Result {
+ self.transaction(move |tx| async move {
+ self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+ .await?;
+
+ let channel = channel::ActiveModel {
+ id: ActiveValue::Unchanged(channel_id),
+ visibility: ActiveValue::Set(visibility),
+ ..Default::default()
+ }
+ .update(&*tx)
+ .await?;
+
+ Ok(channel)
+ })
+ .await
+ }
+
pub async fn delete_channel(
&self,
channel_id: ChannelId,
@@ -98,17 +206,19 @@ impl Database {
.await?;
// Don't remove descendant channels that have additional parents.
- let mut channels_to_remove = self.get_channel_descendants([channel_id], &*tx).await?;
+ let mut channels_to_remove: HashSet = HashSet::default();
+ channels_to_remove.insert(channel_id);
+
+ let graph = self.get_channel_descendants([channel_id], &*tx).await?;
+ for edge in graph.iter() {
+ channels_to_remove.insert(ChannelId::from_proto(edge.channel_id));
+ }
+
{
let mut channels_to_keep = channel_path::Entity::find()
.filter(
channel_path::Column::ChannelId
- .is_in(
- channels_to_remove
- .keys()
- .copied()
- .filter(|&id| id != channel_id),
- )
+ .is_in(channels_to_remove.iter().copied())
.and(
channel_path::Column::IdPath
.not_like(&format!("%/{}/%", channel_id)),
@@ -133,7 +243,7 @@ impl Database {
.await?;
channel::Entity::delete_many()
- .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied()))
+ .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied()))
.exec(&*tx)
.await?;
@@ -150,7 +260,7 @@ impl Database {
);
tx.execute(channel_paths_stmt).await?;
- Ok((channels_to_remove.into_keys().collect(), members_to_notify))
+ Ok((channels_to_remove.into_iter().collect(), members_to_notify))
})
.await
}
@@ -159,19 +269,19 @@ impl Database {
&self,
channel_id: ChannelId,
invitee_id: UserId,
- inviter_id: UserId,
- is_admin: bool,
+ admin_id: UserId,
+ role: ChannelRole,
) -> Result<()> {
self.transaction(move |tx| async move {
- self.check_user_is_channel_admin(channel_id, inviter_id, &*tx)
+ self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
.await?;
channel_member::ActiveModel {
+ id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
user_id: ActiveValue::Set(invitee_id),
accepted: ActiveValue::Set(false),
- admin: ActiveValue::Set(is_admin),
- ..Default::default()
+ role: ActiveValue::Set(role),
}
.insert(&*tx)
.await?;
@@ -194,14 +304,14 @@ impl Database {
channel_id: ChannelId,
user_id: UserId,
new_name: &str,
- ) -> Result {
+ ) -> Result {
self.transaction(move |tx| async move {
let new_name = Self::sanitize_channel_name(new_name)?.to_string();
self.check_user_is_channel_admin(channel_id, user_id, &*tx)
.await?;
- channel::ActiveModel {
+ let channel = channel::ActiveModel {
id: ActiveValue::Unchanged(channel_id),
name: ActiveValue::Set(new_name.clone()),
..Default::default()
@@ -209,7 +319,11 @@ impl Database {
.update(&*tx)
.await?;
- Ok(new_name)
+ Ok(Channel {
+ id: channel.id,
+ name: channel.name,
+ visibility: channel.visibility,
+ })
})
.await
}
@@ -260,10 +374,10 @@ impl Database {
&self,
channel_id: ChannelId,
member_id: UserId,
- remover_id: UserId,
+ admin_id: UserId,
) -> Result<()> {
self.transaction(|tx| async move {
- self.check_user_is_channel_admin(channel_id, remover_id, &*tx)
+ self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
.await?;
let result = channel_member::Entity::delete_many()
@@ -311,6 +425,7 @@ impl Database {
.map(|channel| Channel {
id: channel.id,
name: channel.name,
+ visibility: channel.visibility,
})
.collect();
@@ -319,49 +434,6 @@ impl Database {
.await
}
- async fn get_channel_graph(
- &self,
- parents_by_child_id: ChannelDescendants,
- trim_dangling_parents: bool,
- tx: &DatabaseTransaction,
- ) -> Result {
- let mut channels = Vec::with_capacity(parents_by_child_id.len());
- {
- let mut rows = channel::Entity::find()
- .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
- .stream(&*tx)
- .await?;
- while let Some(row) = rows.next().await {
- let row = row?;
- channels.push(Channel {
- id: row.id,
- name: row.name,
- })
- }
- }
-
- let mut edges = Vec::with_capacity(parents_by_child_id.len());
- for (channel, parents) in parents_by_child_id.iter() {
- for parent in parents.into_iter() {
- if trim_dangling_parents {
- if parents_by_child_id.contains_key(parent) {
- edges.push(ChannelEdge {
- channel_id: channel.to_proto(),
- parent_id: parent.to_proto(),
- });
- }
- } else {
- edges.push(ChannelEdge {
- channel_id: channel.to_proto(),
- parent_id: parent.to_proto(),
- });
- }
- }
- }
-
- Ok(ChannelGraph { channels, edges })
- }
-
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result {
self.transaction(|tx| async move {
let tx = tx;
@@ -411,19 +483,108 @@ impl Database {
channel_memberships: Vec,
tx: &DatabaseTransaction,
) -> Result {
- let parents_by_child_id = self
+ let mut edges = self
.get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
.await?;
- let channels_with_admin_privileges = channel_memberships
- .iter()
- .filter_map(|membership| membership.admin.then_some(membership.channel_id))
- .collect();
+ let mut role_for_channel: HashMap = HashMap::default();
- let graph = self
- .get_channel_graph(parents_by_child_id, true, &tx)
+ for membership in channel_memberships.iter() {
+ role_for_channel.insert(membership.channel_id, membership.role);
+ }
+
+ for ChannelEdge {
+ parent_id,
+ channel_id,
+ } in edges.iter()
+ {
+ let parent_id = ChannelId::from_proto(*parent_id);
+ let channel_id = ChannelId::from_proto(*channel_id);
+ debug_assert!(role_for_channel.get(&parent_id).is_some());
+ let parent_role = role_for_channel[&parent_id];
+ if let Some(existing_role) = role_for_channel.get(&channel_id) {
+ if existing_role.should_override(parent_role) {
+ continue;
+ }
+ }
+ role_for_channel.insert(channel_id, parent_role);
+ }
+
+ let mut channels: Vec = Vec::new();
+ let mut channels_with_admin_privileges: HashSet = HashSet::default();
+ let mut channels_to_remove: HashSet = HashSet::default();
+
+ let mut rows = channel::Entity::find()
+ .filter(channel::Column::Id.is_in(role_for_channel.keys().copied()))
+ .stream(&*tx)
.await?;
+ while let Some(row) = rows.next().await {
+ let channel = row?;
+ let role = role_for_channel[&channel.id];
+
+ if role == ChannelRole::Banned
+ || role == ChannelRole::Guest && channel.visibility != ChannelVisibility::Public
+ {
+ channels_to_remove.insert(channel.id.0 as u64);
+ continue;
+ }
+
+ channels.push(Channel {
+ id: channel.id,
+ name: channel.name,
+ visibility: channel.visibility,
+ });
+
+ if role == ChannelRole::Admin {
+ channels_with_admin_privileges.insert(channel.id);
+ }
+ }
+ drop(rows);
+
+ if !channels_to_remove.is_empty() {
+ // Note: this code assumes each channel has one parent.
+ // If there are multiple valid public paths to a channel,
+ // e.g.
+ // If both of these paths are present (* indicating public):
+ // - zed* -> projects -> vim*
+ // - zed* -> conrad -> public-projects* -> vim*
+ // Users would only see one of them (based on edge sort order)
+ let mut replacement_parent: HashMap = HashMap::default();
+ for ChannelEdge {
+ parent_id,
+ channel_id,
+ } in edges.iter()
+ {
+ if channels_to_remove.contains(channel_id) {
+ replacement_parent.insert(*channel_id, *parent_id);
+ }
+ }
+
+ let mut new_edges: Vec = Vec::new();
+ 'outer: for ChannelEdge {
+ mut parent_id,
+ channel_id,
+ } in edges.iter()
+ {
+ if channels_to_remove.contains(channel_id) {
+ continue;
+ }
+ while channels_to_remove.contains(&parent_id) {
+ if let Some(new_parent_id) = replacement_parent.get(&parent_id) {
+ parent_id = *new_parent_id;
+ } else {
+ continue 'outer;
+ }
+ }
+ new_edges.push(ChannelEdge {
+ parent_id,
+ channel_id: *channel_id,
+ })
+ }
+ edges = new_edges;
+ }
+
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryUserIdsAndChannelIds {
ChannelId,
@@ -434,7 +595,7 @@ impl Database {
{
let mut rows = room_participant::Entity::find()
.inner_join(room::Entity)
- .filter(room::Column::ChannelId.is_in(graph.channels.iter().map(|c| c.id)))
+ .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id)))
.select_only()
.column(room::Column::ChannelId)
.column(room_participant::Column::UserId)
@@ -447,7 +608,7 @@ impl Database {
}
}
- let channel_ids = graph.channels.iter().map(|c| c.id).collect::>();
+ let channel_ids = channels.iter().map(|c| c.id).collect::>();
let channel_buffer_changes = self
.unseen_channel_buffer_changes(user_id, &channel_ids, &*tx)
.await?;
@@ -457,7 +618,7 @@ impl Database {
.await?;
Ok(ChannelsForUser {
- channels: graph,
+ channels: ChannelGraph { channels, edges },
channel_participants,
channels_with_admin_privileges,
unseen_buffer_changes: channel_buffer_changes,
@@ -466,116 +627,143 @@ impl Database {
}
pub async fn get_channel_members(&self, id: ChannelId) -> Result> {
- self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await })
+ self.transaction(|tx| async move { self.get_channel_participants_internal(id, &*tx).await })
.await
}
- pub async fn set_channel_member_admin(
+ pub async fn set_channel_member_role(
&self,
channel_id: ChannelId,
- from: UserId,
+ admin_id: UserId,
for_user: UserId,
- admin: bool,
- ) -> Result<()> {
+ role: ChannelRole,
+ ) -> Result {
self.transaction(|tx| async move {
- self.check_user_is_channel_admin(channel_id, from, &*tx)
+ self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
.await?;
- let result = channel_member::Entity::update_many()
+ let membership = channel_member::Entity::find()
.filter(
channel_member::Column::ChannelId
.eq(channel_id)
.and(channel_member::Column::UserId.eq(for_user)),
)
- .set(channel_member::ActiveModel {
- admin: ActiveValue::set(admin),
- ..Default::default()
- })
- .exec(&*tx)
+ .one(&*tx)
.await?;
- if result.rows_affected == 0 {
- Err(anyhow!("no such member"))?;
- }
+ let Some(membership) = membership else {
+ Err(anyhow!("no such member"))?
+ };
- Ok(())
+ let mut update = membership.into_active_model();
+ update.role = ActiveValue::Set(role);
+ let updated = channel_member::Entity::update(update).exec(&*tx).await?;
+
+ Ok(updated)
})
.await
}
- pub async fn get_channel_member_details(
+ pub async fn get_channel_participant_details(
&self,
channel_id: ChannelId,
- user_id: UserId,
+ admin_id: UserId,
) -> Result> {
self.transaction(|tx| async move {
- self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+ self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
.await?;
+ let channel_visibility = channel::Entity::find()
+ .filter(channel::Column::Id.eq(channel_id))
+ .one(&*tx)
+ .await?
+ .map(|channel| channel.visibility)
+ .unwrap_or(ChannelVisibility::Members);
+
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryMemberDetails {
UserId,
- Admin,
+ Role,
IsDirectMember,
Accepted,
+ Visibility,
}
let tx = tx;
let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?;
let mut stream = channel_member::Entity::find()
- .distinct()
+ .left_join(channel::Entity)
.filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied()))
.select_only()
.column(channel_member::Column::UserId)
- .column(channel_member::Column::Admin)
+ .column(channel_member::Column::Role)
.column_as(
channel_member::Column::ChannelId.eq(channel_id),
QueryMemberDetails::IsDirectMember,
)
.column(channel_member::Column::Accepted)
- .order_by_asc(channel_member::Column::UserId)
+ .column(channel::Column::Visibility)
.into_values::<_, QueryMemberDetails>()
.stream(&*tx)
.await?;
- let mut rows = Vec::::new();
- while let Some(row) = stream.next().await {
- let (user_id, is_admin, is_direct_member, is_invite_accepted): (
+ struct UserDetail {
+ kind: Kind,
+ channel_role: ChannelRole,
+ }
+ let mut user_details: HashMap = HashMap::default();
+
+ while let Some(user_membership) = stream.next().await {
+ let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): (
UserId,
+ ChannelRole,
bool,
bool,
- bool,
- ) = row?;
+ ChannelVisibility,
+ ) = user_membership?;
let kind = match (is_direct_member, is_invite_accepted) {
(true, true) => proto::channel_member::Kind::Member,
(true, false) => proto::channel_member::Kind::Invitee,
(false, true) => proto::channel_member::Kind::AncestorMember,
(false, false) => continue,
};
- let user_id = user_id.to_proto();
- let kind = kind.into();
- if let Some(last_row) = rows.last_mut() {
- if last_row.user_id == user_id {
- if is_direct_member {
- last_row.kind = kind;
- last_row.admin = is_admin;
- }
- continue;
- }
+
+ if channel_role == ChannelRole::Guest
+ && visibility != ChannelVisibility::Public
+ && channel_visibility != ChannelVisibility::Public
+ {
+ continue;
+ }
+
+ if let Some(details_mut) = user_details.get_mut(&user_id) {
+ if channel_role.should_override(details_mut.channel_role) {
+ details_mut.channel_role = channel_role;
+ }
+ if kind == Kind::Member {
+ details_mut.kind = kind;
+ // the UI is going to be a bit confusing if you already have permissions
+ // that are greater than or equal to the ones you're being invited to.
+ } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember {
+ details_mut.kind = kind;
+ }
+ } else {
+ user_details.insert(user_id, UserDetail { kind, channel_role });
}
- rows.push(proto::ChannelMember {
- user_id,
- kind,
- admin: is_admin,
- });
}
- Ok(rows)
+ Ok(user_details
+ .into_iter()
+ .map(|(user_id, details)| proto::ChannelMember {
+ user_id: user_id.to_proto(),
+ kind: details.kind.into(),
+ role: details.channel_role.into(),
+ })
+ .collect())
})
.await
}
- pub async fn get_channel_members_internal(
+ pub async fn get_channel_participants_internal(
&self,
id: ChannelId,
tx: &DatabaseTransaction,
@@ -596,46 +784,197 @@ impl Database {
Ok(user_ids)
}
- pub async fn check_user_is_channel_member(
- &self,
- channel_id: ChannelId,
- user_id: UserId,
- tx: &DatabaseTransaction,
- ) -> Result<()> {
- let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
- channel_member::Entity::find()
- .filter(
- channel_member::Column::ChannelId
- .is_in(channel_ids)
- .and(channel_member::Column::UserId.eq(user_id)),
- )
- .one(&*tx)
- .await?
- .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?;
- Ok(())
- }
-
pub async fn check_user_is_channel_admin(
&self,
channel_id: ChannelId,
user_id: UserId,
tx: &DatabaseTransaction,
) -> Result<()> {
+ match self.channel_role_for_user(channel_id, user_id, tx).await? {
+ Some(ChannelRole::Admin) => Ok(()),
+ Some(ChannelRole::Member)
+ | Some(ChannelRole::Banned)
+ | Some(ChannelRole::Guest)
+ | None => Err(anyhow!(
+ "user is not a channel admin or channel does not exist"
+ ))?,
+ }
+ }
+
+ pub async fn check_user_is_channel_member(
+ &self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ tx: &DatabaseTransaction,
+ ) -> Result<()> {
+ match self.channel_role_for_user(channel_id, user_id, tx).await? {
+ Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(()),
+ Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
+ "user is not a channel member or channel does not exist"
+ ))?,
+ }
+ }
+
+ pub async fn check_user_is_channel_participant(
+ &self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ tx: &DatabaseTransaction,
+ ) -> Result<()> {
+ match self.channel_role_for_user(channel_id, user_id, tx).await? {
+ Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
+ Ok(())
+ }
+ Some(ChannelRole::Banned) | None => Err(anyhow!(
+ "user is not a channel participant or channel does not exist"
+ ))?,
+ }
+ }
+
+ pub async fn pending_invite_for_channel(
+ &self,
+ channel_id: ChannelId,
+ user_id: UserId,
+ tx: &DatabaseTransaction,
+ ) -> Result