Compare commits

...
Sign in to create a new pull request.

11 commits

Author SHA1 Message Date
Mikayla
3a1f13645a
Improve database and RPC API for moving and linking channels, improve test legibility 2023-09-09 18:20:14 -07:00
Mikayla
bf296ebbd7
Add move, link, and unlink operations 2023-09-09 13:24:04 -07:00
Mikayla
114961fc69
remove extraneous depth field 2023-09-09 12:10:18 -07:00
Mikayla
80f5e66efc
Render the DAG 2023-09-09 12:06:17 -07:00
Mikayla
b7172d5e0d
Finish integration tests for channel moving
Refactor channel store to combine the channels_by_id and channel_paths into a 'ChannelIndex'
2023-09-09 12:02:40 -07:00
Mikayla
1ab2007fcd
WIP: Add channel DAG related RPC messages, change update message 2023-09-09 12:01:15 -07:00
Mikayla
441848d195
Improve channel deletion to be DAG aware 2023-09-09 12:00:36 -07:00
Mikayla
273fa9dd22
Add removing of previous channel channel, allowing for channel moving operations 2023-09-09 12:00:36 -07:00
Mikayla
e0602da8df
Expand DAG tests to include more complex tree operations and removal behavior 2023-09-09 12:00:36 -07:00
Mikayla
65b795c213
Add channel linking operation 2023-09-09 12:00:34 -07:00
Mikayla
fe10ecebb6
Add channel moving test 2023-09-09 12:00:16 -07:00
13 changed files with 2052 additions and 637 deletions

View file

@ -1,3 +1,5 @@
mod channel_index;
use crate::channel_buffer::ChannelBuffer;
use anyhow::{anyhow, Result};
use client::{Client, Subscription, User, UserId, UserStore};
@ -8,13 +10,15 @@ use rpc::{proto, TypedEnvelope};
use std::{mem, sync::Arc, time::Duration};
use util::ResultExt;
use self::channel_index::ChannelIndex;
pub use self::channel_index::ChannelPath;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub type ChannelId = u64;
pub struct ChannelStore {
channels_by_id: HashMap<ChannelId, Arc<Channel>>,
channel_paths: Vec<Vec<ChannelId>>,
channel_index: ChannelIndex,
channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channels_with_admin_privileges: HashSet<ChannelId>,
@ -87,9 +91,8 @@ impl ChannelStore {
});
Self {
channels_by_id: HashMap::default(),
channel_invitations: Vec::default(),
channel_paths: Vec::default(),
channel_index: ChannelIndex::default(),
channel_participants: Default::default(),
channels_with_admin_privileges: Default::default(),
outgoing_invites: Default::default(),
@ -116,7 +119,7 @@ impl ChannelStore {
}
pub fn has_children(&self, channel_id: ChannelId) -> bool {
self.channel_paths.iter().any(|path| {
self.channel_index.iter().any(|path| {
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
path.len() > ix + 1
} else {
@ -126,22 +129,23 @@ impl ChannelStore {
}
pub fn channel_count(&self) -> usize {
self.channel_paths.len()
self.channel_index.len()
}
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
self.channel_paths.iter().map(move |path| {
self.channel_index.iter().map(move |path| {
let id = path.last().unwrap();
let channel = self.channel_for_id(*id).unwrap();
(path.len() - 1, channel)
})
}
pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc<Channel>)> {
let path = self.channel_paths.get(ix)?;
pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc<Channel>, &ChannelPath)> {
let path = self.channel_index.get(ix)?;
let id = path.last().unwrap();
let channel = self.channel_for_id(*id).unwrap();
Some((path.len() - 1, channel))
Some((channel, path))
}
pub fn channel_invitations(&self) -> &[Arc<Channel>] {
@ -149,7 +153,7 @@ impl ChannelStore {
}
pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc<Channel>> {
self.channels_by_id.get(&channel_id)
self.channel_index.by_id().get(&channel_id)
}
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool {
@ -230,7 +234,7 @@ impl ChannelStore {
}
pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
self.channel_paths.iter().any(|path| {
self.channel_index.iter().any(|path| {
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
path[..=ix]
.iter()
@ -285,6 +289,59 @@ impl ChannelStore {
})
}
pub fn link_channel(
&mut self,
channel_id: ChannelId,
to: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(|_, _| async move {
let _ = client
.request(proto::LinkChannel { channel_id, to })
.await?;
Ok(())
})
}
pub fn unlink_channel(
&mut self,
channel_id: ChannelId,
from: Option<ChannelId>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(|_, _| async move {
let _ = client
.request(proto::UnlinkChannel { channel_id, from })
.await?;
Ok(())
})
}
pub fn move_channel(
&mut self,
channel_id: ChannelId,
from: Option<ChannelId>,
to: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(|_, _| async move {
let _ = client
.request(proto::MoveChannel {
channel_id,
from,
to,
})
.await?;
Ok(())
})
}
pub fn invite_member(
&mut self,
channel_id: ChannelId,
@ -464,7 +521,7 @@ impl ChannelStore {
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
let client = self.client.clone();
async move {
client.request(proto::RemoveChannel { channel_id }).await?;
client.request(proto::DeleteChannel { channel_id }).await?;
Ok(())
}
}
@ -591,11 +648,11 @@ impl ChannelStore {
}
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
self.channels_by_id.clear();
self.channel_index.clear();
self.channel_invitations.clear();
self.channel_participants.clear();
self.channels_with_admin_privileges.clear();
self.channel_paths.clear();
self.channel_index.clear();
self.outgoing_invites.clear();
cx.notify();
@ -642,17 +699,16 @@ impl ChannelStore {
}
}
let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty();
if channels_changed {
if !payload.remove_channels.is_empty() {
self.channels_by_id
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
if !payload.delete_channels.is_empty() {
self.channel_index.delete_channels(&payload.delete_channels);
self.channel_participants
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
.retain(|channel_id, _| !payload.delete_channels.contains(channel_id));
self.channels_with_admin_privileges
.retain(|channel_id| !payload.remove_channels.contains(channel_id));
.retain(|channel_id| !payload.delete_channels.contains(channel_id));
for channel_id in &payload.remove_channels {
for channel_id in &payload.delete_channels {
let channel_id = *channel_id;
if let Some(OpenedChannelBuffer::Open(buffer)) =
self.opened_buffers.remove(&channel_id)
@ -664,44 +720,15 @@ impl ChannelStore {
}
}
for channel_proto in payload.channels {
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
Arc::make_mut(existing_channel).name = channel_proto.name;
} else {
let channel = Arc::new(Channel {
id: channel_proto.id,
name: channel_proto.name,
});
self.channels_by_id.insert(channel.id, channel.clone());
if let Some(parent_id) = channel_proto.parent_id {
let mut ix = 0;
while ix < self.channel_paths.len() {
let path = &self.channel_paths[ix];
if path.ends_with(&[parent_id]) {
let mut new_path = path.clone();
new_path.push(channel.id);
self.channel_paths.insert(ix + 1, new_path);
ix += 1;
}
ix += 1;
}
} else {
self.channel_paths.push(vec![channel.id]);
}
}
let mut channel_index = self.channel_index.start_upsert();
for channel in payload.channels {
channel_index.upsert(channel)
}
}
self.channel_paths.sort_by(|a, b| {
let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
let b = Self::channel_path_sorting_key(b, &self.channels_by_id);
a.cmp(b)
});
self.channel_paths.dedup();
self.channel_paths.retain(|path| {
path.iter()
.all(|channel_id| self.channels_by_id.contains_key(channel_id))
});
for edge in payload.delete_channel_edge {
self.channel_index
.delete_edge(edge.parent_id, edge.channel_id);
}
for permission in payload.channel_permissions {
@ -759,12 +786,4 @@ impl ChannelStore {
anyhow::Ok(())
}))
}
fn channel_path_sorting_key<'a>(
path: &'a [ChannelId],
channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>,
) -> impl 'a + Iterator<Item = Option<&'a str>> {
path.iter()
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
}
}

View file

@ -0,0 +1,161 @@
use std::{sync::Arc, ops::Deref};
use collections::HashMap;
use rpc::proto;
use serde_derive::{Serialize, Deserialize};
use crate::{ChannelId, Channel};
pub type ChannelsById = HashMap<ChannelId, Arc<Channel>>;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
pub struct ChannelPath(Arc<[ChannelId]>);
impl Deref for ChannelPath {
type Target = [ChannelId];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ChannelPath {
pub fn parent_id(&self) -> Option<ChannelId> {
self.0.len().checked_sub(2).map(|i| {
self.0[i]
})
}
}
impl Default for ChannelPath {
fn default() -> Self {
ChannelPath(Arc::from([]))
}
}
#[derive(Default, Debug)]
pub struct ChannelIndex {
paths: Vec<ChannelPath>,
channels_by_id: ChannelsById,
}
impl ChannelIndex {
pub fn by_id(&self) -> &ChannelsById {
&self.channels_by_id
}
pub fn clear(&mut self) {
self.paths.clear();
self.channels_by_id.clear();
}
pub fn len(&self) -> usize {
self.paths.len()
}
pub fn get(&self, idx: usize) -> Option<&ChannelPath> {
self.paths.get(idx)
}
pub fn iter(&self) -> impl Iterator<Item = &ChannelPath> {
self.paths.iter()
}
/// Remove the given edge from this index. This will not remove the channel
/// and may result in dangling channels.
pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) {
self.paths.retain(|path| {
!path
.windows(2)
.any(|window| window == [parent_id, channel_id])
});
}
/// Delete the given channels from this index.
pub fn delete_channels(&mut self, channels: &[ChannelId]) {
self.channels_by_id.retain(|channel_id, _| !channels.contains(channel_id));
self.paths.retain(|channel_path| !channel_path.iter().any(|channel_id| {channels.contains(channel_id)}))
}
/// Upsert one or more channels into this index.
pub fn start_upsert(& mut self) -> ChannelPathsUpsertGuard {
ChannelPathsUpsertGuard {
paths: &mut self.paths,
channels_by_id: &mut self.channels_by_id,
}
}
}
/// A guard for ensuring that the paths index maintains its sort and uniqueness
/// invariants after a series of insertions
pub struct ChannelPathsUpsertGuard<'a> {
paths: &'a mut Vec<ChannelPath>,
channels_by_id: &'a mut ChannelsById,
}
impl<'a> ChannelPathsUpsertGuard<'a> {
pub fn upsert(&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;
if let Some(parent_id) = channel_proto.parent_id {
self.insert_edge(parent_id, channel_proto.id)
}
} else {
let channel = Arc::new(Channel {
id: channel_proto.id,
name: channel_proto.name,
});
self.channels_by_id.insert(channel.id, channel.clone());
if let Some(parent_id) = channel_proto.parent_id {
self.insert_edge(parent_id, channel.id);
} else {
self.insert_root(channel.id);
}
}
}
fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) {
let mut ix = 0;
while ix < self.paths.len() {
let path = &self.paths[ix];
if path.ends_with(&[parent_id]) {
let mut new_path = path.to_vec();
new_path.push(channel_id);
self.paths.insert(ix + 1, ChannelPath(new_path.into()));
ix += 1;
}
ix += 1;
}
}
fn insert_root(&mut self, channel_id: ChannelId) {
self.paths.push(ChannelPath(Arc::from([channel_id])));
}
}
impl<'a> Drop for ChannelPathsUpsertGuard<'a> {
fn drop(&mut self) {
self.paths.sort_by(|a, b| {
let a = channel_path_sorting_key(a, &self.channels_by_id);
let b = channel_path_sorting_key(b, &self.channels_by_id);
a.cmp(b)
});
self.paths.dedup();
self.paths.retain(|path| {
path.iter()
.all(|channel_id| self.channels_by_id.contains_key(channel_id))
});
}
}
fn channel_path_sorting_key<'a>(
path: &'a [ChannelId],
channels_by_id: &'a ChannelsById,
) -> impl 'a + Iterator<Item = Option<&'a str>> {
path.iter()
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
}

View file

@ -127,7 +127,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
update_channels(
&channel_store,
proto::UpdateChannels {
remove_channels: vec![1, 2],
delete_channels: vec![1, 2],
..Default::default()
},
cx,

View file

@ -72,7 +72,6 @@ fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
pretty_assertions.workspace = true
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
@ -81,6 +80,7 @@ workspace = { path = "../workspace", features = ["test-support"] }
collab_ui = { path = "../collab_ui", features = ["test-support"] }
async-trait.workspace = true
pretty_assertions.workspace = true
ctor.workspace = true
env_logger.workspace = true
indoc.workspace = true

View file

@ -1,5 +1,7 @@
use super::*;
type ChannelDescendants = HashMap<ChannelId, HashSet<ChannelId>>;
impl Database {
#[cfg(test)]
pub async fn all_channels(&self) -> Result<Vec<(ChannelId, String)>> {
@ -100,7 +102,7 @@ impl Database {
.await
}
pub async fn remove_channel(
pub async fn delete_channel(
&self,
channel_id: ChannelId,
user_id: UserId,
@ -149,6 +151,19 @@ impl Database {
.exec(&*tx)
.await?;
// Delete any other paths that incldue this channel
let sql = r#"
DELETE FROM channel_paths
WHERE
id_path LIKE '%' || $1 || '%'
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[channel_id.to_proto().into()],
);
tx.execute(channel_paths_stmt).await?;
Ok((channels_to_remove.into_keys().collect(), members_to_notify))
})
.await
@ -319,6 +334,43 @@ impl Database {
.await
}
async fn get_all_channels(
&self,
parents_by_child_id: ChannelDescendants,
tx: &DatabaseTransaction,
) -> Result<Vec<Channel>> {
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?;
// As these rows are pulled from the map's keys, this unwrap is safe.
let parents = parents_by_child_id.get(&row.id).unwrap();
if parents.len() > 0 {
for parent in parents {
channels.push(Channel {
id: row.id,
name: row.name.clone(),
parent_id: Some(*parent),
});
}
} else {
channels.push(Channel {
id: row.id,
name: row.name,
parent_id: None,
});
}
}
}
Ok(channels)
}
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
self.transaction(|tx| async move {
let tx = tx;
@ -341,21 +393,7 @@ impl Database {
.filter_map(|membership| membership.admin.then_some(membership.channel_id))
.collect();
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,
parent_id: parents_by_child_id.get(&row.id).copied().flatten(),
});
}
}
let channels = self.get_all_channels(parents_by_child_id, &tx).await?;
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryUserIdsAndChannelIds {
@ -559,6 +597,7 @@ impl Database {
Ok(())
}
/// Returns the channel ancestors, deepest first
pub async fn get_channel_ancestors(
&self,
channel_id: ChannelId,
@ -566,6 +605,7 @@ impl Database {
) -> Result<Vec<ChannelId>> {
let paths = channel_path::Entity::find()
.filter(channel_path::Column::ChannelId.eq(channel_id))
.order_by(channel_path::Column::IdPath, sea_query::Order::Desc)
.all(tx)
.await?;
let mut channel_ids = Vec::new();
@ -582,11 +622,25 @@ impl Database {
Ok(channel_ids)
}
/// Returns the channel descendants,
/// Structured as a map from child ids to their parent ids
/// For example, the descendants of 'a' in this DAG:
///
/// /- b -\
/// a -- c -- d
///
/// would be:
/// {
/// a: [],
/// b: [a],
/// c: [a],
/// d: [a, c],
/// }
async fn get_channel_descendants(
&self,
channel_ids: impl IntoIterator<Item = ChannelId>,
tx: &DatabaseTransaction,
) -> Result<HashMap<ChannelId, Option<ChannelId>>> {
) -> Result<ChannelDescendants> {
let mut values = String::new();
for id in channel_ids {
if !values.is_empty() {
@ -613,7 +667,7 @@ impl Database {
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
let mut parents_by_child_id = HashMap::default();
let mut parents_by_child_id: ChannelDescendants = HashMap::default();
let mut paths = channel_path::Entity::find()
.from_raw_sql(stmt)
.stream(tx)
@ -632,7 +686,10 @@ impl Database {
parent_id = Some(id);
}
}
parents_by_child_id.insert(path.channel_id, parent_id);
let entry = parents_by_child_id.entry(path.channel_id).or_default();
if let Some(parent_id) = parent_id {
entry.insert(parent_id);
}
}
Ok(parents_by_child_id)
@ -703,6 +760,191 @@ impl Database {
})
.await
}
// Insert an edge from the given channel to the given other channel.
pub async fn link_channel(
&self,
user: UserId,
channel: ChannelId,
to: ChannelId,
) -> Result<Vec<Channel>> {
self.transaction(|tx| async move {
// Note that even with these maxed permissions, this linking operation
// is still insecure because you can't remove someone's permissions to a
// channel if they've linked the channel to one where they're an admin.
self.check_user_is_channel_admin(channel, user, &*tx)
.await?;
self.link_channel_internal(user, channel, to, &*tx).await
})
.await
}
pub async fn link_channel_internal(
&self,
user: UserId,
channel: ChannelId,
to: ChannelId,
tx: &DatabaseTransaction,
) -> Result<Vec<Channel>> {
self.check_user_is_channel_admin(to, user, &*tx).await?;
let to_ancestors = self.get_channel_ancestors(to, &*tx).await?;
let mut from_descendants = self.get_channel_descendants([channel], &*tx).await?;
for ancestor in to_ancestors {
if from_descendants.contains_key(&ancestor) {
return Err(anyhow!("Cannot create a channel cycle").into());
}
}
let sql = r#"
INSERT INTO channel_paths
(id_path, channel_id)
SELECT
id_path || $1 || '/', $2
FROM
channel_paths
WHERE
channel_id = $3
ON CONFLICT (id_path) DO NOTHING;
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[
channel.to_proto().into(),
channel.to_proto().into(),
to.to_proto().into(),
],
);
tx.execute(channel_paths_stmt).await?;
for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id != &&channel) {
for to_id in to_ids {
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[
from_id.to_proto().into(),
from_id.to_proto().into(),
to_id.to_proto().into(),
],
);
tx.execute(channel_paths_stmt).await?;
}
}
if let Some(channel) = from_descendants.get_mut(&channel) {
// Remove the other parents
channel.clear();
channel.insert(to);
}
let channels = self.get_all_channels(from_descendants, &*tx).await?;
Ok(channels)
}
/// Unlink a channel from a given parent. This will add in a root edge if
/// the channel has no other parents after this operation.
pub async fn unlink_channel(
&self,
user: UserId,
channel: ChannelId,
from: Option<ChannelId>,
) -> Result<()> {
self.transaction(|tx| async move {
// Note that even with these maxed permissions, this linking operation
// is still insecure because you can't remove someone's permissions to a
// channel if they've linked the channel to one where they're an admin.
self.check_user_is_channel_admin(channel, user, &*tx)
.await?;
self.unlink_channel_internal(user, channel, from, &*tx)
.await?;
Ok(())
})
.await
}
pub async fn unlink_channel_internal(
&self,
user: UserId,
channel: ChannelId,
from: Option<ChannelId>,
tx: &DatabaseTransaction,
) -> Result<()> {
if let Some(from) = from {
self.check_user_is_channel_admin(from, user, &*tx).await?;
let sql = r#"
DELETE FROM channel_paths
WHERE
id_path LIKE '%' || $1 || '/' || $2 || '%'
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[from.to_proto().into(), channel.to_proto().into()],
);
tx.execute(channel_paths_stmt).await?;
} else {
let sql = r#"
DELETE FROM channel_paths
WHERE
id_path = '/' || $1 || '/'
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[channel.to_proto().into()],
);
tx.execute(channel_paths_stmt).await?;
}
// Make sure that there is always at least one path to the channel
let sql = r#"
INSERT INTO channel_paths
(id_path, channel_id)
SELECT
'/' || $1 || '/', $2
WHERE NOT EXISTS
(SELECT *
FROM channel_paths
WHERE channel_id = $2)
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[channel.to_proto().into(), channel.to_proto().into()],
);
tx.execute(channel_paths_stmt).await?;
Ok(())
}
/// Move a channel from one parent to another, returns the
/// Channels that were moved for notifying clients
pub async fn move_channel(
&self,
user: UserId,
channel: ChannelId,
from: Option<ChannelId>,
to: ChannelId,
) -> Result<Vec<Channel>> {
self.transaction(|tx| async move {
self.check_user_is_channel_admin(channel, user, &*tx)
.await?;
let moved_channels = self.link_channel_internal(user, channel, to, &*tx).await?;
self.unlink_channel_internal(user, channel, from, &*tx)
.await?;
Ok(moved_channels)
})
.await
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]

View file

@ -1,4 +1,5 @@
mod buffer_tests;
mod channel_tests;
mod db_tests;
mod feature_flag_tests;

View file

@ -0,0 +1,844 @@
use rpc::{proto, ConnectionId};
use crate::{
db::{Channel, ChannelId, Database, NewUserParams},
test_both_dbs,
};
use std::sync::Arc;
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
async fn test_channels(db: &Arc<Database>) {
let a_id = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let b_id = db
.create_user(
"user2@example.com",
false,
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", 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());
db.invite_channel_member(zed_id, b_id, a_id, false)
.await
.unwrap();
db.respond_to_channel_invite(zed_id, b_id, true)
.await
.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(zed_id), "3", a_id)
.await
.unwrap();
let replace_id = db
.create_channel("replace", Some(zed_id), "4", a_id)
.await
.unwrap();
let mut members = db.get_channel_members(replace_id).await.unwrap();
members.sort();
assert_eq!(members, &[a_id, b_id]);
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
let cargo_id = db
.create_channel("cargo", Some(rust_id), "6", a_id)
.await
.unwrap();
let cargo_ra_id = db
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
.await
.unwrap();
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: rust_id,
name: "rust".to_string(),
parent_id: None,
},
Channel {
id: cargo_id,
name: "cargo".to_string(),
parent_id: Some(rust_id),
},
Channel {
id: cargo_ra_id,
name: "cargo-ra".to_string(),
parent_id: Some(cargo_id),
}
]
);
let result = db.get_channels_for_user(b_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
]
);
// Update member permissions
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
assert!(set_subchannel_admin.is_err());
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
assert!(set_channel_admin.is_ok());
let result = db.get_channels_for_user(b_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
]
);
// 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());
// Remove a channel tree
let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
channel_ids.sort();
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());
}
test_both_dbs!(
test_joining_channels,
test_joining_channels_postgres,
test_joining_channels_sqlite
);
async fn test_joining_channels(db: &Arc<Database>) {
let owner_id = db.create_server("test").await.unwrap().0 as u32;
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 channel_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
// can join a room with membership to its channel
let joined_room = db
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(joined_room.room.participants.len(), 1);
drop(joined_room);
// cannot join a room without membership to its channel
assert!(db
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
.await
.is_err());
}
test_both_dbs!(
test_channel_invites,
test_channel_invites_postgres,
test_channel_invites_sqlite
);
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 channel_1_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let channel_1_2 = db
.create_root_channel("channel_2", "2", user_1)
.await
.unwrap();
db.invite_channel_member(channel_1_1, user_2, user_1, false)
.await
.unwrap();
db.invite_channel_member(channel_1_2, user_2, user_1, false)
.await
.unwrap();
db.invite_channel_member(channel_1_1, user_3, user_1, true)
.await
.unwrap();
let user_2_invites = db
.get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
.await
.unwrap()
.into_iter()
.map(|channel| channel.id)
.collect::<Vec<_>>();
assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
let user_3_invites = db
.get_channel_invites_for_user(user_3) // -> [channel_1_1]
.await
.unwrap()
.into_iter()
.map(|channel| channel.id)
.collect::<Vec<_>>();
assert_eq!(user_3_invites, &[channel_1_1]);
let members = db
.get_channel_member_details(channel_1_1, user_1)
.await
.unwrap();
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
admin: true,
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
admin: false,
},
proto::ChannelMember {
user_id: user_3.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
admin: true,
},
]
);
db.respond_to_channel_invite(channel_1_1, user_2, true)
.await
.unwrap();
let channel_1_3 = db
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
.await
.unwrap();
let members = db
.get_channel_member_details(channel_1_3, user_1)
.await
.unwrap();
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
admin: true,
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
admin: false,
},
]
);
}
test_both_dbs!(
test_channel_renames,
test_channel_renames_postgres,
test_channel_renames_sqlite
);
async fn test_channel_renames(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 zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
db.rename_channel(zed_id, user_1, "#zed-archive")
.await
.unwrap();
let zed_archive_id = zed_id;
let (channel, _) = db
.get_channel(zed_archive_id, user_1)
.await
.unwrap()
.unwrap();
assert_eq!(channel.name, "zed-archive");
let non_permissioned_rename = db
.rename_channel(zed_archive_id, user_2, "hacked-lol")
.await;
assert!(non_permissioned_rename.is_err());
let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
assert!(bad_name_rename.is_err())
}
test_both_dbs!(
test_channels_moving,
test_channels_moving_postgres,
test_channels_moving_sqlite
);
async fn test_channels_moving(db: &Arc<Database>) {
let a_id = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let gpui2_id = db
.create_channel("gpui2", Some(zed_id), "3", a_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(crdb_id), "4", a_id)
.await
.unwrap();
let livestreaming_dag_id = db
.create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id)
.await
.unwrap();
// ========================================================================
// sanity check
// Initial DAG:
// /- gpui2
// zed -- crdb - livestreaming - livestreaming_dag
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(
result.channels,
&[
(zed_id, None),
(crdb_id, Some(zed_id)),
(gpui2_id, Some(zed_id)),
(livestreaming_id, Some(crdb_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
],
);
// Attempt to make a cycle
assert!(db
.link_channel(a_id, zed_id, livestreaming_id)
.await
.is_err());
// ========================================================================
// Make a link
db.link_channel(a_id, livestreaming_id, zed_id)
.await
.unwrap();
// DAG is now:
// /- gpui2
// zed -- crdb - livestreaming - livestreaming_dag
// \---------/
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(gpui2_id, Some(zed_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_id, Some(crdb_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
]);
// ========================================================================
// Create a new channel below a channel with multiple parents
let livestreaming_dag_sub_id = db
.create_channel(
"livestreaming_dag_sub",
Some(livestreaming_dag_id),
"6",
a_id,
)
.await
.unwrap();
// DAG is now:
// /- gpui2
// zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
// \---------/
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(gpui2_id, Some(zed_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_id, Some(crdb_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Test a complex DAG by making another link
let returned_channels = db
.link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id)
.await
.unwrap();
// DAG is now:
// /- gpui2 /---------------------\
// zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
// \--------/
// make sure we're getting just the new link
// Not using the assert_dag helper because we want to make sure we're returning the full data
pretty_assertions::assert_eq!(
returned_channels,
vec![Channel {
id: livestreaming_dag_sub_id,
name: "livestreaming_dag_sub".to_string(),
parent_id: Some(livestreaming_id),
}]
);
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(gpui2_id, Some(zed_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_id, Some(crdb_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Test a complex DAG by making another link
let returned_channels = db
.link_channel(a_id, livestreaming_id, gpui2_id)
.await
.unwrap();
// DAG is now:
// /- gpui2 -\ /---------------------\
// zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id
// \---------/
// Make sure that we're correctly getting the full sub-dag
pretty_assertions::assert_eq!(
returned_channels,
vec![
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(gpui2_id),
},
Channel {
id: livestreaming_dag_id,
name: "livestreaming_dag".to_string(),
parent_id: Some(livestreaming_id),
},
Channel {
id: livestreaming_dag_sub_id,
name: "livestreaming_dag_sub".to_string(),
parent_id: Some(livestreaming_id),
},
Channel {
id: livestreaming_dag_sub_id,
name: "livestreaming_dag_sub".to_string(),
parent_id: Some(livestreaming_dag_id),
}
]
);
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(gpui2_id, Some(zed_id)),
(livestreaming_id, Some(gpui2_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_id, Some(crdb_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Test unlinking in a complex DAG by removing the inner link
db
.unlink_channel(
a_id,
livestreaming_dag_sub_id,
Some(livestreaming_id),
)
.await
.unwrap();
// DAG is now:
// /- gpui2 -\
// zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
// \---------/
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(gpui2_id, Some(zed_id)),
(livestreaming_id, Some(gpui2_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_id, Some(crdb_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Test unlinking in a complex DAG by removing the inner link
db.unlink_channel(a_id, livestreaming_id, Some(gpui2_id))
.await
.unwrap();
// DAG is now:
// /- gpui2
// zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
// \---------/
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(gpui2_id, Some(zed_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_id, Some(crdb_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Test moving DAG nodes by moving livestreaming to be below gpui2
db.move_channel(a_id, livestreaming_id, Some(crdb_id), gpui2_id)
.await
.unwrap();
// DAG is now:
// /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub
// zed - crdb /
// \---------/
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(gpui2_id, Some(zed_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_id, Some(gpui2_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Deleting a channel should not delete children that still have other parents
db.delete_channel(gpui2_id, a_id).await.unwrap();
// DAG is now:
// zed - crdb
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Unlinking a channel from it's parent should automatically promote it to a root channel
db.unlink_channel(a_id, crdb_id, Some(zed_id))
.await
.unwrap();
// DAG is now:
// crdb
// zed
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, None),
(livestreaming_id, Some(zed_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Unlinking a root channel should not have any effect
db.unlink_channel(a_id, crdb_id, None)
.await
.unwrap();
// DAG is now:
// crdb
// zed
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
//
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, None),
(livestreaming_id, Some(zed_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// You should be able to move a root channel into a non-root channel
db.move_channel(a_id, crdb_id, None, zed_id)
.await
.unwrap();
// DAG is now:
// zed - crdb
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Moving a non-root channel without a parent id should be the equivalent of a link operation
db.move_channel(a_id, livestreaming_id, None, crdb_id)
.await
.unwrap();
// DAG is now:
// zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub
// \--------/
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_dag(result.channels, &[
(zed_id, None),
(crdb_id, Some(zed_id)),
(livestreaming_id, Some(zed_id)),
(livestreaming_id, Some(crdb_id)),
(livestreaming_dag_id, Some(livestreaming_id)),
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
]);
// ========================================================================
// Deleting a parent of a DAG should delete the whole DAG:
db.delete_channel(zed_id, a_id).await.unwrap();
let result = db.get_channels_for_user(a_id).await.unwrap();
assert!(
result.channels.is_empty()
)
}
#[track_caller]
fn assert_dag(actual: Vec<Channel>, expected: &[(ChannelId, Option<ChannelId>)]) {
let actual = actual
.iter()
.map(|channel| (channel.id, channel.parent_id))
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(actual, expected)
}

View file

@ -877,458 +877,6 @@ async fn test_invite_codes() {
assert!(db.has_contact(user5, user1).await.unwrap());
}
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
async fn test_channels(db: &Arc<Database>) {
let a_id = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let b_id = db
.create_user(
"user2@example.com",
false,
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", 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());
db.invite_channel_member(zed_id, b_id, a_id, false)
.await
.unwrap();
db.respond_to_channel_invite(zed_id, b_id, true)
.await
.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(zed_id), "3", a_id)
.await
.unwrap();
let replace_id = db
.create_channel("replace", Some(zed_id), "4", a_id)
.await
.unwrap();
let mut members = db.get_channel_members(replace_id).await.unwrap();
members.sort();
assert_eq!(members, &[a_id, b_id]);
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
let cargo_id = db
.create_channel("cargo", Some(rust_id), "6", a_id)
.await
.unwrap();
let cargo_ra_id = db
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
.await
.unwrap();
let result = db.get_channels_for_user(a_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: rust_id,
name: "rust".to_string(),
parent_id: None,
},
Channel {
id: cargo_id,
name: "cargo".to_string(),
parent_id: Some(rust_id),
},
Channel {
id: cargo_ra_id,
name: "cargo-ra".to_string(),
parent_id: Some(cargo_id),
}
]
);
let result = db.get_channels_for_user(b_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
]
);
// Update member permissions
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
assert!(set_subchannel_admin.is_err());
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
assert!(set_channel_admin.is_ok());
let result = db.get_channels_for_user(b_id).await.unwrap();
assert_eq!(
result.channels,
vec![
Channel {
id: zed_id,
name: "zed".to_string(),
parent_id: None,
},
Channel {
id: crdb_id,
name: "crdb".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: livestreaming_id,
name: "livestreaming".to_string(),
parent_id: Some(zed_id),
},
Channel {
id: replace_id,
name: "replace".to_string(),
parent_id: Some(zed_id),
},
]
);
// Remove a single channel
db.remove_channel(crdb_id, a_id).await.unwrap();
assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
// Remove a channel tree
let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap();
channel_ids.sort();
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());
}
test_both_dbs!(
test_joining_channels,
test_joining_channels_postgres,
test_joining_channels_sqlite
);
async fn test_joining_channels(db: &Arc<Database>) {
let owner_id = db.create_server("test").await.unwrap().0 as u32;
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 channel_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
// can join a room with membership to its channel
let joined_room = db
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(joined_room.room.participants.len(), 1);
drop(joined_room);
// cannot join a room without membership to its channel
assert!(db
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
.await
.is_err());
}
test_both_dbs!(
test_channel_invites,
test_channel_invites_postgres,
test_channel_invites_sqlite
);
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 channel_1_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let channel_1_2 = db
.create_root_channel("channel_2", "2", user_1)
.await
.unwrap();
db.invite_channel_member(channel_1_1, user_2, user_1, false)
.await
.unwrap();
db.invite_channel_member(channel_1_2, user_2, user_1, false)
.await
.unwrap();
db.invite_channel_member(channel_1_1, user_3, user_1, true)
.await
.unwrap();
let user_2_invites = db
.get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
.await
.unwrap()
.into_iter()
.map(|channel| channel.id)
.collect::<Vec<_>>();
assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
let user_3_invites = db
.get_channel_invites_for_user(user_3) // -> [channel_1_1]
.await
.unwrap()
.into_iter()
.map(|channel| channel.id)
.collect::<Vec<_>>();
assert_eq!(user_3_invites, &[channel_1_1]);
let members = db
.get_channel_member_details(channel_1_1, user_1)
.await
.unwrap();
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
admin: true,
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
admin: false,
},
proto::ChannelMember {
user_id: user_3.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
admin: true,
},
]
);
db.respond_to_channel_invite(channel_1_1, user_2, true)
.await
.unwrap();
let channel_1_3 = db
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
.await
.unwrap();
let members = db
.get_channel_member_details(channel_1_3, user_1)
.await
.unwrap();
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
admin: true,
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
admin: false,
},
]
);
}
test_both_dbs!(
test_channel_renames,
test_channel_renames_postgres,
test_channel_renames_sqlite
);
async fn test_channel_renames(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 zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
db.rename_channel(zed_id, user_1, "#zed-archive")
.await
.unwrap();
let zed_archive_id = zed_id;
let (channel, _) = db
.get_channel(zed_archive_id, user_1)
.await
.unwrap()
.unwrap();
assert_eq!(channel.name, "zed-archive");
let non_permissioned_rename = db
.rename_channel(zed_archive_id, user_2, "hacked-lol")
.await;
assert!(non_permissioned_rename.is_err());
let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
assert!(bad_name_rename.is_err())
}
#[gpui::test]
async fn test_multiple_signup_overwrite() {
let test_db = TestDb::postgres(build_background_executor());

View file

@ -2,7 +2,10 @@ mod connection_pool;
use crate::{
auth,
db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId},
db::{
self, Channel, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User,
UserId,
},
executor::Executor,
AppState, Result,
};
@ -243,7 +246,7 @@ impl Server {
.add_request_handler(remove_contact)
.add_request_handler(respond_to_contact_request)
.add_request_handler(create_channel)
.add_request_handler(remove_channel)
.add_request_handler(delete_channel)
.add_request_handler(invite_channel_member)
.add_request_handler(remove_channel_member)
.add_request_handler(set_channel_member_admin)
@ -255,6 +258,9 @@ impl Server {
.add_request_handler(get_channel_members)
.add_request_handler(respond_to_channel_invite)
.add_request_handler(join_channel)
.add_request_handler(link_channel)
.add_request_handler(unlink_channel)
.add_request_handler(move_channel)
.add_request_handler(follow)
.add_message_handler(unfollow)
.add_message_handler(update_followers)
@ -2227,23 +2233,23 @@ async fn create_channel(
Ok(())
}
async fn remove_channel(
request: proto::RemoveChannel,
response: Response<proto::RemoveChannel>,
async fn delete_channel(
request: proto::DeleteChannel,
response: Response<proto::DeleteChannel>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = request.channel_id;
let (removed_channels, member_ids) = db
.remove_channel(ChannelId::from_proto(channel_id), session.user_id)
.delete_channel(ChannelId::from_proto(channel_id), session.user_id)
.await?;
response.send(proto::Ack {})?;
// Notify members of removed channels
let mut update = proto::UpdateChannels::default();
update
.remove_channels
.delete_channels
.extend(removed_channels.into_iter().map(|id| id.to_proto()));
let connection_pool = session.connection_pool().await;
@ -2303,7 +2309,7 @@ async fn remove_channel_member(
.await?;
let mut update = proto::UpdateChannels::default();
update.remove_channels.push(channel_id.to_proto());
update.delete_channels.push(channel_id.to_proto());
for connection_id in session
.connection_pool()
@ -2387,6 +2393,126 @@ async fn rename_channel(
Ok(())
}
async fn link_channel(
request: proto::LinkChannel,
response: Response<proto::LinkChannel>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let to = ChannelId::from_proto(request.to);
let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?;
let members = db.get_channel_members(to).await?;
let connection_pool = session.connection_pool().await;
let update = proto::UpdateChannels {
channels: channels_to_send
.into_iter()
.map(|channel| proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
parent_id: channel.parent_id.map(ChannelId::to_proto),
})
.collect(),
..Default::default()
};
for member_id in members {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
response.send(Ack {})?;
Ok(())
}
async fn unlink_channel(
request: proto::UnlinkChannel,
response: Response<proto::UnlinkChannel>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let from = request.from.map(ChannelId::from_proto);
db.unlink_channel(session.user_id, channel_id, from).await?;
if let Some(from_parent) = from {
let members = db.get_channel_members(from_parent).await?;
let update = proto::UpdateChannels {
delete_channel_edge: vec![proto::ChannelEdge {
channel_id: channel_id.to_proto(),
parent_id: from_parent.to_proto(),
}],
..Default::default()
};
let connection_pool = session.connection_pool().await;
for member_id in members {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
}
response.send(Ack {})?;
Ok(())
}
async fn move_channel(
request: proto::MoveChannel,
response: Response<proto::MoveChannel>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let from_parent = request.from.map(ChannelId::from_proto);
let to = ChannelId::from_proto(request.to);
let channels_to_send: Vec<Channel> = db
.move_channel(session.user_id, channel_id, from_parent, to)
.await?;
if let Some(from_parent) = from_parent {
let members = db.get_channel_members(from_parent).await?;
let update = proto::UpdateChannels {
delete_channel_edge: vec![proto::ChannelEdge {
channel_id: channel_id.to_proto(),
parent_id: from_parent.to_proto(),
}],
..Default::default()
};
let connection_pool = session.connection_pool().await;
for member_id in members {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
}
let members = db.get_channel_members(to).await?;
let connection_pool = session.connection_pool().await;
let update = proto::UpdateChannels {
channels: channels_to_send
.into_iter()
.map(|channel| proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
parent_id: channel.parent_id.map(ChannelId::to_proto),
})
.collect(),
..Default::default()
};
for member_id in members {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
response.send(Ack {})?;
Ok(())
}
async fn get_channel_members(
request: proto::GetChannelMembers,
response: Response<proto::GetChannelMembers>,

View file

@ -874,6 +874,143 @@ async fn test_lost_channel_creation(
);
}
#[gpui::test]
async fn test_channel_moving(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let channel_a_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-a", None, cx)
})
.await
.unwrap();
let channel_b_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-b", Some(channel_a_id), cx)
})
.await
.unwrap();
let channel_c_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("channel-c", Some(channel_b_id), cx)
})
.await
.unwrap();
// Current shape:
// a - b - c
deterministic.run_until_parked();
assert_channels(
client_a.channel_store(),
cx_a,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
depth: 1,
user_is_admin: true,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".to_string(),
depth: 2,
user_is_admin: true,
},
],
);
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.move_channel(channel_c_id, Some(channel_b_id), channel_a_id, cx)
})
.await
.unwrap();
// Current shape:
// /- c
// a -- b
deterministic.run_until_parked();
assert_channels(
client_a.channel_store(),
cx_a,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
depth: 1,
user_is_admin: true,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".to_string(),
depth: 1,
user_is_admin: true,
},
],
);
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.link_channel(channel_c_id, channel_b_id, cx)
})
.await
.unwrap();
// Current shape:
// /------\
// a -- b -- c
deterministic.run_until_parked();
assert_channels(
client_a.channel_store(),
cx_a,
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
depth: 0,
user_is_admin: true,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
depth: 1,
user_is_admin: true,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".to_string(),
depth: 2,
user_is_admin: true,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".to_string(),
depth: 1,
user_is_admin: true,
},
],
);
}
#[derive(Debug, PartialEq)]
struct ExpectedChannel {
depth: usize,
@ -920,5 +1057,5 @@ fn assert_channels(
})
.collect::<Vec<_>>()
});
assert_eq!(actual, expected_channels);
pretty_assertions::assert_eq!(actual, expected_channels);
}

View file

@ -4,7 +4,7 @@ mod panel_settings;
use anyhow::Result;
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
use channel::{Channel, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
use client::{proto::PeerId, Client, Contact, User, UserStore};
use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
@ -35,7 +35,7 @@ use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings}
use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize};
use settings::SettingsStore;
use std::{borrow::Cow, mem, sync::Arc};
use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
use theme::{components::ComponentExt, IconButton};
use util::{iife, ResultExt, TryFutureExt};
use workspace::{
@ -54,37 +54,59 @@ use self::contact_finder::ContactFinder;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct RemoveChannel {
channel_id: u64,
channel_id: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct ToggleCollapse {
channel_id: u64,
location: ChannelLocation<'static>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct NewChannel {
channel_id: u64,
location: ChannelLocation<'static>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct InviteMembers {
channel_id: u64,
channel_id: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct ManageMembers {
channel_id: u64,
channel_id: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct RenameChannel {
channel_id: u64,
location: ChannelLocation<'static>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct OpenChannelBuffer {
channel_id: u64,
channel_id: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct LinkChannel {
channel_id: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct MoveChannel {
channel_id: ChannelId,
parent_id: Option<ChannelId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct PutChannel {
to: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct UnlinkChannel {
channel_id: ChannelId,
parent_id: Option<ChannelId>,
}
actions!(
@ -107,12 +129,40 @@ impl_actions!(
ManageMembers,
RenameChannel,
ToggleCollapse,
OpenChannelBuffer
OpenChannelBuffer,
LinkChannel,
MoveChannel,
PutChannel,
UnlinkChannel
]
);
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ChannelLocation<'a> {
channel: ChannelId,
path: Cow<'a, ChannelPath>,
}
impl From<(ChannelId, ChannelPath)> for ChannelLocation<'static> {
fn from(value: (ChannelId, ChannelPath)) -> Self {
ChannelLocation {
channel: value.0,
path: Cow::Owned(value.1),
}
}
}
impl<'a> From<(ChannelId, &'a ChannelPath)> for ChannelLocation<'a> {
fn from(value: (ChannelId, &'a ChannelPath)) -> Self {
ChannelLocation {
channel: value.0,
path: Cow::Borrowed(value.1),
}
}
}
pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
contact_finder::init(cx);
@ -135,16 +185,65 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
cx.add_action(CollabPanel::collapse_selected_channel);
cx.add_action(CollabPanel::expand_selected_channel);
cx.add_action(CollabPanel::open_channel_buffer);
cx.add_action(
|panel: &mut CollabPanel, action: &LinkChannel, _: &mut ViewContext<CollabPanel>| {
panel.link_or_move = Some(ChannelCopy::Link(action.channel_id));
},
);
cx.add_action(
|panel: &mut CollabPanel, action: &MoveChannel, _: &mut ViewContext<CollabPanel>| {
panel.link_or_move = Some(ChannelCopy::Move {
channel_id: action.channel_id,
parent_id: action.parent_id,
});
},
);
cx.add_action(
|panel: &mut CollabPanel, action: &PutChannel, cx: &mut ViewContext<CollabPanel>| {
if let Some(copy) = panel.link_or_move.take() {
match copy {
ChannelCopy::Move {
channel_id,
parent_id,
} => panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.move_channel(channel_id, parent_id, action.to, cx)
.detach_and_log_err(cx)
}),
ChannelCopy::Link(channel) => {
panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.link_channel(channel, action.to, cx)
.detach_and_log_err(cx)
})
}
}
}
},
);
cx.add_action(
|panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext<CollabPanel>| {
panel.channel_store.update(cx, |channel_store, cx| {
channel_store
.unlink_channel(action.channel_id, action.parent_id, cx)
.detach_and_log_err(cx)
})
},
);
}
#[derive(Debug)]
pub enum ChannelEditingState {
Create {
parent_id: Option<u64>,
location: Option<ChannelLocation<'static>>,
pending_name: Option<String>,
},
Rename {
channel_id: u64,
location: ChannelLocation<'static>,
pending_name: Option<String>,
},
}
@ -158,10 +257,36 @@ impl ChannelEditingState {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ChannelCopy {
Move {
channel_id: u64,
parent_id: Option<u64>,
},
Link(u64),
}
impl ChannelCopy {
fn channel_id(&self) -> u64 {
match self {
ChannelCopy::Move { channel_id, .. } => *channel_id,
ChannelCopy::Link(channel_id) => *channel_id,
}
}
fn is_move(&self) -> bool {
match self {
ChannelCopy::Move { .. } => true,
ChannelCopy::Link(_) => false,
}
}
}
pub struct CollabPanel {
width: Option<f32>,
fs: Arc<dyn Fs>,
has_focus: bool,
link_or_move: Option<ChannelCopy>,
pending_serialization: Task<Option<()>>,
context_menu: ViewHandle<ContextMenu>,
filter_editor: ViewHandle<Editor>,
@ -177,7 +302,7 @@ pub struct CollabPanel {
list_state: ListState<Self>,
subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>,
collapsed_channels: Vec<ChannelId>,
collapsed_channels: Vec<ChannelLocation<'static>>,
workspace: WeakViewHandle<Workspace>,
context_menu_on_selected: bool,
}
@ -185,7 +310,7 @@ pub struct CollabPanel {
#[derive(Serialize, Deserialize)]
struct SerializedCollabPanel {
width: Option<f32>,
collapsed_channels: Option<Vec<ChannelId>>,
collapsed_channels: Option<Vec<ChannelLocation<'static>>>,
}
#[derive(Debug)]
@ -229,6 +354,7 @@ enum ListEntry {
Channel {
channel: Arc<Channel>,
depth: usize,
path: ChannelPath,
},
ChannelNotes {
channel_id: ChannelId,
@ -353,10 +479,15 @@ impl CollabPanel {
cx,
)
}
ListEntry::Channel { channel, depth } => {
ListEntry::Channel {
channel,
depth,
path,
} => {
let channel_row = this.render_channel(
&*channel,
*depth,
path.to_owned(),
&theme.collab_panel,
is_selected,
cx,
@ -425,6 +556,7 @@ impl CollabPanel {
let mut this = Self {
width: None,
has_focus: false,
link_or_move: None,
fs: workspace.app_state().fs.clone(),
pending_serialization: Task::ready(None),
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
@ -512,7 +644,13 @@ impl CollabPanel {
.log_err()
.flatten()
{
Some(serde_json::from_str::<SerializedCollabPanel>(&panel)?)
match serde_json::from_str::<SerializedCollabPanel>(&panel) {
Ok(panel) => Some(panel),
Err(err) => {
log::error!("Failed to deserialize collaboration panel: {}", err);
None
}
}
} else {
None
};
@ -702,28 +840,24 @@ impl CollabPanel {
executor.clone(),
));
if let Some(state) = &self.channel_editing_state {
if matches!(
state,
ChannelEditingState::Create {
parent_id: None,
..
}
) {
if matches!(state, ChannelEditingState::Create { location: None, .. }) {
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
}
}
let mut collapse_depth = None;
for mat in matches {
let (depth, channel) =
channel_store.channel_at_index(mat.candidate_id).unwrap();
let (channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap();
let depth = path.len() - 1;
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
let location: ChannelLocation<'_> = (channel.id, path).into();
if collapse_depth.is_none() && self.is_channel_collapsed(&location) {
collapse_depth = Some(depth);
} else if let Some(collapsed_depth) = collapse_depth {
if depth > collapsed_depth {
continue;
}
if self.is_channel_collapsed(channel.id) {
if self.is_channel_collapsed(&location) {
collapse_depth = Some(depth);
} else {
collapse_depth = None;
@ -731,18 +865,21 @@ impl CollabPanel {
}
match &self.channel_editing_state {
Some(ChannelEditingState::Create { parent_id, .. })
if *parent_id == Some(channel.id) =>
{
Some(ChannelEditingState::Create {
location: parent_id,
..
}) if *parent_id == Some(location) => {
self.entries.push(ListEntry::Channel {
channel: channel.clone(),
depth,
path: path.clone(),
});
self.entries
.push(ListEntry::ChannelEditor { depth: depth + 1 });
}
Some(ChannelEditingState::Rename { channel_id, .. })
if *channel_id == channel.id =>
Some(ChannelEditingState::Rename { location, .. })
if location.channel == channel.id
&& location.path == Cow::Borrowed(path) =>
{
self.entries.push(ListEntry::ChannelEditor { depth });
}
@ -750,6 +887,7 @@ impl CollabPanel {
self.entries.push(ListEntry::Channel {
channel: channel.clone(),
depth,
path: path.clone(),
});
}
}
@ -1546,14 +1684,21 @@ impl CollabPanel {
&self,
channel: &Channel,
depth: usize,
path: ChannelPath,
theme: &theme::CollabPanel,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let channel_id = channel.id;
let has_children = self.channel_store.read(cx).has_children(channel_id);
let disclosed =
has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
let disclosed = {
let location = ChannelLocation {
channel: channel_id,
path: Cow::Borrowed(&path),
};
has_children.then(|| !self.collapsed_channels.binary_search(&location).is_ok())
};
let is_active = iife!({
let call_channel = ActiveCall::global(cx)
@ -1567,7 +1712,7 @@ impl CollabPanel {
const FACEPILE_LIMIT: usize = 3;
MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
MouseEventHandler::new::<Channel, _>(id(&path) as usize, cx, |state, cx| {
Flex::<Self>::row()
.with_child(
Svg::new("icons/hash.svg")
@ -1618,8 +1763,13 @@ impl CollabPanel {
})
.align_children_center()
.styleable_component()
.disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
.with_id(channel_id as usize)
.disclosable(
disclosed,
Box::new(ToggleCollapse {
location: (channel_id, path.clone()).into(),
}),
)
.with_id(id(&path) as usize)
.with_style(theme.disclosure.clone())
.element()
.constrained()
@ -1635,7 +1785,11 @@ impl CollabPanel {
this.join_channel(channel_id, cx);
})
.on_click(MouseButton::Right, move |e, this, cx| {
this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
this.deploy_channel_context_menu(
Some(e.position),
&(channel_id, path.clone()).into(),
cx,
);
})
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
@ -1882,11 +2036,20 @@ impl CollabPanel {
fn deploy_channel_context_menu(
&mut self,
position: Option<Vector2F>,
channel_id: u64,
location: &ChannelLocation<'static>,
cx: &mut ViewContext<Self>,
) {
self.context_menu_on_selected = position.is_none();
let operation_details = self.link_or_move.as_ref().and_then(|link_or_move| {
let channel_name = self
.channel_store
.read(cx)
.channel_for_id(link_or_move.channel_id())
.map(|channel| channel.name.clone())?;
Some((channel_name, link_or_move.is_move()))
});
self.context_menu.update(cx, |context_menu, cx| {
context_menu.set_position_mode(if self.context_menu_on_selected {
OverlayPositionMode::Local
@ -1894,27 +2057,112 @@ impl CollabPanel {
OverlayPositionMode::Window
});
let expand_action_name = if self.is_channel_collapsed(channel_id) {
let mut items = Vec::new();
if let Some((channel_name, is_move)) = operation_details {
items.push(ContextMenuItem::action(
format!(
"{} '#{}' here",
if is_move { "Move" } else { "Link" },
channel_name
),
PutChannel {
to: location.channel,
},
));
items.push(ContextMenuItem::Separator)
}
let expand_action_name = if self.is_channel_collapsed(&location) {
"Expand Subchannels"
} else {
"Collapse Subchannels"
};
let mut items = vec![
ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
];
items.extend([
ContextMenuItem::action(
expand_action_name,
ToggleCollapse {
location: location.clone(),
},
),
ContextMenuItem::action(
"Open Notes",
OpenChannelBuffer {
channel_id: location.channel,
},
),
]);
if self.channel_store.read(cx).is_user_admin(location.channel) {
let parent_id = location.path.parent_id();
if self.channel_store.read(cx).is_user_admin(channel_id) {
items.extend([
ContextMenuItem::Separator,
ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
ContextMenuItem::action("Rename", RenameChannel { channel_id }),
ContextMenuItem::action(
"New Subchannel",
NewChannel {
location: location.clone(),
},
),
ContextMenuItem::action(
"Rename",
RenameChannel {
location: location.clone(),
},
),
ContextMenuItem::Separator,
ContextMenuItem::action("Invite Members", InviteMembers { channel_id }),
ContextMenuItem::action("Manage Members", ManageMembers { channel_id }),
]);
items.push(ContextMenuItem::action(
if parent_id.is_some() {
"Unlink from parent"
} else {
"Unlink from root"
},
UnlinkChannel {
channel_id: location.channel,
parent_id,
},
));
items.extend([
ContextMenuItem::action(
"Link this channel",
LinkChannel {
channel_id: location.channel,
},
),
ContextMenuItem::action(
"Move this channel",
MoveChannel {
channel_id: location.channel,
parent_id,
},
),
]);
items.extend([
ContextMenuItem::Separator,
ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
ContextMenuItem::action(
"Invite Members",
InviteMembers {
channel_id: location.channel,
},
),
ContextMenuItem::action(
"Manage Members",
ManageMembers {
channel_id: location.channel,
},
),
ContextMenuItem::Separator,
ContextMenuItem::action(
"Delete",
RemoveChannel {
channel_id: location.channel,
},
),
]);
}
@ -2040,7 +2288,7 @@ impl CollabPanel {
if let Some(editing_state) = &mut self.channel_editing_state {
match editing_state {
ChannelEditingState::Create {
parent_id,
location,
pending_name,
..
} => {
@ -2053,13 +2301,17 @@ impl CollabPanel {
self.channel_store
.update(cx, |channel_store, cx| {
channel_store.create_channel(&channel_name, *parent_id, cx)
channel_store.create_channel(
&channel_name,
location.as_ref().map(|location| location.channel),
cx,
)
})
.detach();
cx.notify();
}
ChannelEditingState::Rename {
channel_id,
location,
pending_name,
} => {
if pending_name.is_some() {
@ -2070,7 +2322,7 @@ impl CollabPanel {
self.channel_store
.update(cx, |channel_store, cx| {
channel_store.rename(*channel_id, &channel_name, cx)
channel_store.rename(location.channel, &channel_name, cx)
})
.detach();
cx.notify();
@ -2097,38 +2349,58 @@ impl CollabPanel {
_: &CollapseSelectedChannel,
cx: &mut ViewContext<Self>,
) {
let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
let Some((channel_id, path)) = self
.selected_channel()
.map(|(channel, parent)| (channel.id, parent))
else {
return;
};
if self.is_channel_collapsed(channel_id) {
let path = path.to_owned();
if self.is_channel_collapsed(&(channel_id, path.clone()).into()) {
return;
}
self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
self.toggle_channel_collapsed(
&ToggleCollapse {
location: (channel_id, path).into(),
},
cx,
)
}
fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
let Some((channel_id, path)) = self
.selected_channel()
.map(|(channel, parent)| (channel.id, parent))
else {
return;
};
if !self.is_channel_collapsed(channel_id) {
let path = path.to_owned();
if !self.is_channel_collapsed(&(channel_id, path.clone()).into()) {
return;
}
self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
self.toggle_channel_collapsed(
&ToggleCollapse {
location: (channel_id, path).into(),
},
cx,
)
}
fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
let channel_id = action.channel_id;
let location = action.location.clone();
match self.collapsed_channels.binary_search(&channel_id) {
match self.collapsed_channels.binary_search(&location) {
Ok(ix) => {
self.collapsed_channels.remove(ix);
}
Err(ix) => {
self.collapsed_channels.insert(ix, channel_id);
self.collapsed_channels.insert(ix, location);
}
};
self.serialize(cx);
@ -2137,8 +2409,8 @@ impl CollabPanel {
cx.focus_self();
}
fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
self.collapsed_channels.binary_search(&channel).is_ok()
fn is_channel_collapsed(&self, location: &ChannelLocation) -> bool {
self.collapsed_channels.binary_search(location).is_ok()
}
fn leave_call(cx: &mut ViewContext<Self>) {
@ -2163,7 +2435,7 @@ impl CollabPanel {
fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
self.channel_editing_state = Some(ChannelEditingState::Create {
parent_id: None,
location: None,
pending_name: None,
});
self.update_entries(false, cx);
@ -2181,9 +2453,9 @@ impl CollabPanel {
fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
self.collapsed_channels
.retain(|&channel| channel != action.channel_id);
.retain(|channel| *channel != action.location);
self.channel_editing_state = Some(ChannelEditingState::Create {
parent_id: Some(action.channel_id),
location: Some(action.location.to_owned()),
pending_name: None,
});
self.update_entries(false, cx);
@ -2201,16 +2473,16 @@ impl CollabPanel {
}
fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
if let Some(channel) = self.selected_channel() {
if let Some((channel, _)) = self.selected_channel() {
self.remove_channel(channel.id, cx)
}
}
fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
if let Some(channel) = self.selected_channel() {
if let Some((channel, parent)) = self.selected_channel() {
self.rename_channel(
&RenameChannel {
channel_id: channel.id,
location: (channel.id, parent.to_owned()).into(),
},
cx,
);
@ -2219,12 +2491,15 @@ impl CollabPanel {
fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.read(cx);
if !channel_store.is_user_admin(action.channel_id) {
if !channel_store.is_user_admin(action.location.channel) {
return;
}
if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
if let Some(channel) = channel_store
.channel_for_id(action.location.channel)
.cloned()
{
self.channel_editing_state = Some(ChannelEditingState::Rename {
channel_id: action.channel_id,
location: action.location.to_owned(),
pending_name: None,
});
self.channel_name_editor.update(cx, |editor, cx| {
@ -2266,18 +2541,22 @@ impl CollabPanel {
}
fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
let Some(channel) = self.selected_channel() else {
let Some((channel, path)) = self.selected_channel() else {
return;
};
self.deploy_channel_context_menu(None, channel.id, cx);
self.deploy_channel_context_menu(None, &(channel.id, path.to_owned()).into(), cx);
}
fn selected_channel(&self) -> Option<&Arc<Channel>> {
fn selected_channel(&self) -> Option<(&Arc<Channel>, &ChannelPath)> {
self.selection
.and_then(|ix| self.entries.get(ix))
.and_then(|entry| match entry {
ListEntry::Channel { channel, .. } => Some(channel),
ListEntry::Channel {
channel,
path: parent,
..
} => Some((channel, parent)),
_ => None,
})
}
@ -2657,13 +2936,17 @@ impl PartialEq for ListEntry {
ListEntry::Channel {
channel: channel_1,
depth: depth_1,
path: parent_1,
} => {
if let ListEntry::Channel {
channel: channel_2,
depth: depth_2,
path: parent_2,
} = other
{
return channel_1.id == channel_2.id && depth_1 == depth_2;
return channel_1.id == channel_2.id
&& depth_1 == depth_2
&& parent_1 == parent_2;
}
}
ListEntry::ChannelNotes { channel_id } => {
@ -2726,3 +3009,26 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen
.contained()
.with_style(style.container)
}
/// Hash a channel path to a u64, for use as a mouse id
/// Based on the FowlerNollVo hash:
/// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
fn id(path: &[ChannelId]) -> u64 {
// I probably should have done this, but I didn't
// let hasher = DefaultHasher::new();
// let path = path.hash(&mut hasher);
// let x = hasher.finish();
const OFFSET: u64 = 14695981039346656037;
const PRIME: u64 = 1099511628211;
let mut hash = OFFSET;
for id in path.iter() {
for id in id.to_ne_bytes() {
hash = hash ^ (id as u64);
hash = (hash as u128 * PRIME as u128) as u64;
}
}
hash
}

View file

@ -141,7 +141,7 @@ message Envelope {
RespondToChannelInvite respond_to_channel_invite = 123;
UpdateChannels update_channels = 124;
JoinChannel join_channel = 125;
RemoveChannel remove_channel = 126;
DeleteChannel delete_channel = 126;
GetChannelMembers get_channel_members = 127;
GetChannelMembersResponse get_channel_members_response = 128;
SetChannelMemberAdmin set_channel_member_admin = 129;
@ -155,7 +155,10 @@ message Envelope {
RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
RejoinChannelBuffers rejoin_channel_buffers = 140;
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; // Current max
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141;
LinkChannel link_channel = 142;
UnlinkChannel unlink_channel = 143;
MoveChannel move_channel = 144; // Current max
}
}
@ -945,11 +948,17 @@ message LspDiskBasedDiagnosticsUpdated {}
message UpdateChannels {
repeated Channel channels = 1;
repeated uint64 remove_channels = 2;
repeated Channel channel_invitations = 3;
repeated uint64 remove_channel_invitations = 4;
repeated ChannelParticipants channel_participants = 5;
repeated ChannelPermission channel_permissions = 6;
repeated ChannelEdge delete_channel_edge = 2;
repeated uint64 delete_channels = 3;
repeated Channel channel_invitations = 4;
repeated uint64 remove_channel_invitations = 5;
repeated ChannelParticipants channel_participants = 6;
repeated ChannelPermission channel_permissions = 7;
}
message ChannelEdge {
uint64 channel_id = 1;
uint64 parent_id = 2;
}
message ChannelPermission {
@ -966,7 +975,7 @@ message JoinChannel {
uint64 channel_id = 1;
}
message RemoveChannel {
message DeleteChannel {
uint64 channel_id = 1;
}
@ -1021,6 +1030,22 @@ message RenameChannel {
string name = 2;
}
message LinkChannel {
uint64 channel_id = 1;
uint64 to = 2;
}
message UnlinkChannel {
uint64 channel_id = 1;
optional uint64 from = 2;
}
message MoveChannel {
uint64 channel_id = 1;
optional uint64 from = 2;
uint64 to = 3;
}
message JoinChannelBuffer {
uint64 channel_id = 1;
}

View file

@ -237,7 +237,10 @@ messages!(
(UpdateBuffer, Foreground),
(UpdateBufferFile, Foreground),
(UpdateContacts, Foreground),
(RemoveChannel, Foreground),
(DeleteChannel, Foreground),
(MoveChannel, Foreground),
(LinkChannel, Foreground),
(UnlinkChannel, Foreground),
(UpdateChannels, Foreground),
(UpdateDiagnosticSummary, Foreground),
(UpdateFollowers, Foreground),
@ -315,9 +318,12 @@ request_messages!(
(SetChannelMemberAdmin, Ack),
(GetChannelMembers, GetChannelMembersResponse),
(JoinChannel, JoinRoomResponse),
(RemoveChannel, Ack),
(DeleteChannel, Ack),
(RenameProjectEntry, ProjectEntryResponse),
(RenameChannel, ChannelResponse),
(LinkChannel, Ack),
(UnlinkChannel, Ack),
(MoveChannel, Ack),
(SaveBuffer, BufferSaved),
(SearchProject, SearchProjectResponse),
(ShareProject, ShareProjectResponse),