Compare commits
11 commits
main
...
channel-da
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3a1f13645a | ||
![]() |
bf296ebbd7 | ||
![]() |
114961fc69 | ||
![]() |
80f5e66efc | ||
![]() |
b7172d5e0d | ||
![]() |
1ab2007fcd | ||
![]() |
441848d195 | ||
![]() |
273fa9dd22 | ||
![]() |
e0602da8df | ||
![]() |
65b795c213 | ||
![]() |
fe10ecebb6 |
13 changed files with 2052 additions and 637 deletions
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
161
crates/channel/src/channel_store/channel_index.rs
Normal file
161
crates/channel/src/channel_store/channel_index.rs
Normal 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()))
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
mod buffer_tests;
|
||||
mod channel_tests;
|
||||
mod db_tests;
|
||||
mod feature_flag_tests;
|
||||
|
||||
|
|
844
crates/collab/src/db/tests/channel_tests.rs
Normal file
844
crates/collab/src/db/tests/channel_tests.rs
Normal 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)
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 Fowler–Noll–Vo 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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue