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 crate::channel_buffer::ChannelBuffer;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use client::{Client, Subscription, User, UserId, UserStore};
|
use client::{Client, Subscription, User, UserId, UserStore};
|
||||||
|
@ -8,13 +10,15 @@ use rpc::{proto, TypedEnvelope};
|
||||||
use std::{mem, sync::Arc, time::Duration};
|
use std::{mem, sync::Arc, time::Duration};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use self::channel_index::ChannelIndex;
|
||||||
|
pub use self::channel_index::ChannelPath;
|
||||||
|
|
||||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
pub type ChannelId = u64;
|
pub type ChannelId = u64;
|
||||||
|
|
||||||
pub struct ChannelStore {
|
pub struct ChannelStore {
|
||||||
channels_by_id: HashMap<ChannelId, Arc<Channel>>,
|
channel_index: ChannelIndex,
|
||||||
channel_paths: Vec<Vec<ChannelId>>,
|
|
||||||
channel_invitations: Vec<Arc<Channel>>,
|
channel_invitations: Vec<Arc<Channel>>,
|
||||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||||
channels_with_admin_privileges: HashSet<ChannelId>,
|
channels_with_admin_privileges: HashSet<ChannelId>,
|
||||||
|
@ -87,9 +91,8 @@ impl ChannelStore {
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
channels_by_id: HashMap::default(),
|
|
||||||
channel_invitations: Vec::default(),
|
channel_invitations: Vec::default(),
|
||||||
channel_paths: Vec::default(),
|
channel_index: ChannelIndex::default(),
|
||||||
channel_participants: Default::default(),
|
channel_participants: Default::default(),
|
||||||
channels_with_admin_privileges: Default::default(),
|
channels_with_admin_privileges: Default::default(),
|
||||||
outgoing_invites: Default::default(),
|
outgoing_invites: Default::default(),
|
||||||
|
@ -116,7 +119,7 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_children(&self, channel_id: ChannelId) -> bool {
|
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) {
|
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||||
path.len() > ix + 1
|
path.len() > ix + 1
|
||||||
} else {
|
} else {
|
||||||
|
@ -126,22 +129,23 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channel_count(&self) -> usize {
|
pub fn channel_count(&self) -> usize {
|
||||||
self.channel_paths.len()
|
self.channel_index.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
|
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 id = path.last().unwrap();
|
||||||
let channel = self.channel_for_id(*id).unwrap();
|
let channel = self.channel_for_id(*id).unwrap();
|
||||||
(path.len() - 1, channel)
|
(path.len() - 1, channel)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc<Channel>)> {
|
pub fn channel_at_index(&self, ix: usize) -> Option<(&Arc<Channel>, &ChannelPath)> {
|
||||||
let path = self.channel_paths.get(ix)?;
|
let path = self.channel_index.get(ix)?;
|
||||||
let id = path.last().unwrap();
|
let id = path.last().unwrap();
|
||||||
let channel = self.channel_for_id(*id).unwrap();
|
let channel = self.channel_for_id(*id).unwrap();
|
||||||
Some((path.len() - 1, channel))
|
|
||||||
|
Some((channel, path))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channel_invitations(&self) -> &[Arc<Channel>] {
|
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>> {
|
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 {
|
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 {
|
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) {
|
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||||
path[..=ix]
|
path[..=ix]
|
||||||
.iter()
|
.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(
|
pub fn invite_member(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
@ -464,7 +521,7 @@ impl ChannelStore {
|
||||||
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
|
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
async move {
|
async move {
|
||||||
client.request(proto::RemoveChannel { channel_id }).await?;
|
client.request(proto::DeleteChannel { channel_id }).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -591,11 +648,11 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
self.channels_by_id.clear();
|
self.channel_index.clear();
|
||||||
self.channel_invitations.clear();
|
self.channel_invitations.clear();
|
||||||
self.channel_participants.clear();
|
self.channel_participants.clear();
|
||||||
self.channels_with_admin_privileges.clear();
|
self.channels_with_admin_privileges.clear();
|
||||||
self.channel_paths.clear();
|
self.channel_index.clear();
|
||||||
self.outgoing_invites.clear();
|
self.outgoing_invites.clear();
|
||||||
cx.notify();
|
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 channels_changed {
|
||||||
if !payload.remove_channels.is_empty() {
|
if !payload.delete_channels.is_empty() {
|
||||||
self.channels_by_id
|
self.channel_index.delete_channels(&payload.delete_channels);
|
||||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
|
||||||
self.channel_participants
|
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
|
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;
|
let channel_id = *channel_id;
|
||||||
if let Some(OpenedChannelBuffer::Open(buffer)) =
|
if let Some(OpenedChannelBuffer::Open(buffer)) =
|
||||||
self.opened_buffers.remove(&channel_id)
|
self.opened_buffers.remove(&channel_id)
|
||||||
|
@ -664,44 +720,15 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for channel_proto in payload.channels {
|
let mut channel_index = self.channel_index.start_upsert();
|
||||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
for channel in payload.channels {
|
||||||
Arc::make_mut(existing_channel).name = channel_proto.name;
|
channel_index.upsert(channel)
|
||||||
} 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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.channel_paths.sort_by(|a, b| {
|
for edge in payload.delete_channel_edge {
|
||||||
let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
|
self.channel_index
|
||||||
let b = Self::channel_path_sorting_key(b, &self.channels_by_id);
|
.delete_edge(edge.parent_id, edge.channel_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 permission in payload.channel_permissions {
|
for permission in payload.channel_permissions {
|
||||||
|
@ -759,12 +786,4 @@ impl ChannelStore {
|
||||||
anyhow::Ok(())
|
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(
|
update_channels(
|
||||||
&channel_store,
|
&channel_store,
|
||||||
proto::UpdateChannels {
|
proto::UpdateChannels {
|
||||||
remove_channels: vec![1, 2],
|
delete_channels: vec![1, 2],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -72,7 +72,6 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||||
git = { path = "../git", features = ["test-support"] }
|
git = { path = "../git", features = ["test-support"] }
|
||||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
pretty_assertions.workspace = true
|
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
settings = { path = "../settings", 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"] }
|
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||||
|
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
pretty_assertions.workspace = true
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
type ChannelDescendants = HashMap<ChannelId, HashSet<ChannelId>>;
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub async fn all_channels(&self) -> Result<Vec<(ChannelId, String)>> {
|
pub async fn all_channels(&self) -> Result<Vec<(ChannelId, String)>> {
|
||||||
|
@ -100,7 +102,7 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_channel(
|
pub async fn delete_channel(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
|
@ -149,6 +151,19 @@ impl Database {
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.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))
|
Ok((channels_to_remove.into_keys().collect(), members_to_notify))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
@ -319,6 +334,43 @@ impl Database {
|
||||||
.await
|
.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> {
|
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let tx = tx;
|
let tx = tx;
|
||||||
|
@ -341,21 +393,7 @@ impl Database {
|
||||||
.filter_map(|membership| membership.admin.then_some(membership.channel_id))
|
.filter_map(|membership| membership.admin.then_some(membership.channel_id))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut channels = Vec::with_capacity(parents_by_child_id.len());
|
let channels = self.get_all_channels(parents_by_child_id, &tx).await?;
|
||||||
{
|
|
||||||
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(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
enum QueryUserIdsAndChannelIds {
|
enum QueryUserIdsAndChannelIds {
|
||||||
|
@ -559,6 +597,7 @@ impl Database {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the channel ancestors, deepest first
|
||||||
pub async fn get_channel_ancestors(
|
pub async fn get_channel_ancestors(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
@ -566,6 +605,7 @@ impl Database {
|
||||||
) -> Result<Vec<ChannelId>> {
|
) -> Result<Vec<ChannelId>> {
|
||||||
let paths = channel_path::Entity::find()
|
let paths = channel_path::Entity::find()
|
||||||
.filter(channel_path::Column::ChannelId.eq(channel_id))
|
.filter(channel_path::Column::ChannelId.eq(channel_id))
|
||||||
|
.order_by(channel_path::Column::IdPath, sea_query::Order::Desc)
|
||||||
.all(tx)
|
.all(tx)
|
||||||
.await?;
|
.await?;
|
||||||
let mut channel_ids = Vec::new();
|
let mut channel_ids = Vec::new();
|
||||||
|
@ -582,11 +622,25 @@ impl Database {
|
||||||
Ok(channel_ids)
|
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(
|
async fn get_channel_descendants(
|
||||||
&self,
|
&self,
|
||||||
channel_ids: impl IntoIterator<Item = ChannelId>,
|
channel_ids: impl IntoIterator<Item = ChannelId>,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<HashMap<ChannelId, Option<ChannelId>>> {
|
) -> Result<ChannelDescendants> {
|
||||||
let mut values = String::new();
|
let mut values = String::new();
|
||||||
for id in channel_ids {
|
for id in channel_ids {
|
||||||
if !values.is_empty() {
|
if !values.is_empty() {
|
||||||
|
@ -613,7 +667,7 @@ impl Database {
|
||||||
|
|
||||||
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
|
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()
|
let mut paths = channel_path::Entity::find()
|
||||||
.from_raw_sql(stmt)
|
.from_raw_sql(stmt)
|
||||||
.stream(tx)
|
.stream(tx)
|
||||||
|
@ -632,7 +686,10 @@ impl Database {
|
||||||
parent_id = Some(id);
|
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)
|
Ok(parents_by_child_id)
|
||||||
|
@ -703,6 +760,191 @@ impl Database {
|
||||||
})
|
})
|
||||||
.await
|
.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)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
mod buffer_tests;
|
mod buffer_tests;
|
||||||
|
mod channel_tests;
|
||||||
mod db_tests;
|
mod db_tests;
|
||||||
mod feature_flag_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());
|
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]
|
#[gpui::test]
|
||||||
async fn test_multiple_signup_overwrite() {
|
async fn test_multiple_signup_overwrite() {
|
||||||
let test_db = TestDb::postgres(build_background_executor());
|
let test_db = TestDb::postgres(build_background_executor());
|
||||||
|
|
|
@ -2,7 +2,10 @@ mod connection_pool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth,
|
auth,
|
||||||
db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId},
|
db::{
|
||||||
|
self, Channel, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User,
|
||||||
|
UserId,
|
||||||
|
},
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
AppState, Result,
|
AppState, Result,
|
||||||
};
|
};
|
||||||
|
@ -243,7 +246,7 @@ impl Server {
|
||||||
.add_request_handler(remove_contact)
|
.add_request_handler(remove_contact)
|
||||||
.add_request_handler(respond_to_contact_request)
|
.add_request_handler(respond_to_contact_request)
|
||||||
.add_request_handler(create_channel)
|
.add_request_handler(create_channel)
|
||||||
.add_request_handler(remove_channel)
|
.add_request_handler(delete_channel)
|
||||||
.add_request_handler(invite_channel_member)
|
.add_request_handler(invite_channel_member)
|
||||||
.add_request_handler(remove_channel_member)
|
.add_request_handler(remove_channel_member)
|
||||||
.add_request_handler(set_channel_member_admin)
|
.add_request_handler(set_channel_member_admin)
|
||||||
|
@ -255,6 +258,9 @@ impl Server {
|
||||||
.add_request_handler(get_channel_members)
|
.add_request_handler(get_channel_members)
|
||||||
.add_request_handler(respond_to_channel_invite)
|
.add_request_handler(respond_to_channel_invite)
|
||||||
.add_request_handler(join_channel)
|
.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_request_handler(follow)
|
||||||
.add_message_handler(unfollow)
|
.add_message_handler(unfollow)
|
||||||
.add_message_handler(update_followers)
|
.add_message_handler(update_followers)
|
||||||
|
@ -2227,23 +2233,23 @@ async fn create_channel(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_channel(
|
async fn delete_channel(
|
||||||
request: proto::RemoveChannel,
|
request: proto::DeleteChannel,
|
||||||
response: Response<proto::RemoveChannel>,
|
response: Response<proto::DeleteChannel>,
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let db = session.db().await;
|
let db = session.db().await;
|
||||||
|
|
||||||
let channel_id = request.channel_id;
|
let channel_id = request.channel_id;
|
||||||
let (removed_channels, member_ids) = db
|
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?;
|
.await?;
|
||||||
response.send(proto::Ack {})?;
|
response.send(proto::Ack {})?;
|
||||||
|
|
||||||
// Notify members of removed channels
|
// Notify members of removed channels
|
||||||
let mut update = proto::UpdateChannels::default();
|
let mut update = proto::UpdateChannels::default();
|
||||||
update
|
update
|
||||||
.remove_channels
|
.delete_channels
|
||||||
.extend(removed_channels.into_iter().map(|id| id.to_proto()));
|
.extend(removed_channels.into_iter().map(|id| id.to_proto()));
|
||||||
|
|
||||||
let connection_pool = session.connection_pool().await;
|
let connection_pool = session.connection_pool().await;
|
||||||
|
@ -2303,7 +2309,7 @@ async fn remove_channel_member(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut update = proto::UpdateChannels::default();
|
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
|
for connection_id in session
|
||||||
.connection_pool()
|
.connection_pool()
|
||||||
|
@ -2387,6 +2393,126 @@ async fn rename_channel(
|
||||||
Ok(())
|
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(
|
async fn get_channel_members(
|
||||||
request: proto::GetChannelMembers,
|
request: proto::GetChannelMembers,
|
||||||
response: Response<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)]
|
#[derive(Debug, PartialEq)]
|
||||||
struct ExpectedChannel {
|
struct ExpectedChannel {
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
@ -920,5 +1057,5 @@ fn assert_channels(
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.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 anyhow::Result;
|
||||||
use call::ActiveCall;
|
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 client::{proto::PeerId, Client, Contact, User, UserStore};
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
|
@ -35,7 +35,7 @@ use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings}
|
||||||
use project::{Fs, Project};
|
use project::{Fs, Project};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use settings::SettingsStore;
|
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 theme::{components::ComponentExt, IconButton};
|
||||||
use util::{iife, ResultExt, TryFutureExt};
|
use util::{iife, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -54,37 +54,59 @@ use self::contact_finder::ContactFinder;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct RemoveChannel {
|
struct RemoveChannel {
|
||||||
channel_id: u64,
|
channel_id: ChannelId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct ToggleCollapse {
|
struct ToggleCollapse {
|
||||||
channel_id: u64,
|
location: ChannelLocation<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct NewChannel {
|
struct NewChannel {
|
||||||
channel_id: u64,
|
location: ChannelLocation<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct InviteMembers {
|
struct InviteMembers {
|
||||||
channel_id: u64,
|
channel_id: ChannelId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct ManageMembers {
|
struct ManageMembers {
|
||||||
channel_id: u64,
|
channel_id: ChannelId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct RenameChannel {
|
struct RenameChannel {
|
||||||
channel_id: u64,
|
location: ChannelLocation<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct OpenChannelBuffer {
|
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!(
|
actions!(
|
||||||
|
@ -107,12 +129,40 @@ impl_actions!(
|
||||||
ManageMembers,
|
ManageMembers,
|
||||||
RenameChannel,
|
RenameChannel,
|
||||||
ToggleCollapse,
|
ToggleCollapse,
|
||||||
OpenChannelBuffer
|
OpenChannelBuffer,
|
||||||
|
LinkChannel,
|
||||||
|
MoveChannel,
|
||||||
|
PutChannel,
|
||||||
|
UnlinkChannel
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
|
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) {
|
pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
|
||||||
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
|
settings::register::<panel_settings::CollaborationPanelSettings>(cx);
|
||||||
contact_finder::init(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::collapse_selected_channel);
|
||||||
cx.add_action(CollabPanel::expand_selected_channel);
|
cx.add_action(CollabPanel::expand_selected_channel);
|
||||||
cx.add_action(CollabPanel::open_channel_buffer);
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum ChannelEditingState {
|
pub enum ChannelEditingState {
|
||||||
Create {
|
Create {
|
||||||
parent_id: Option<u64>,
|
location: Option<ChannelLocation<'static>>,
|
||||||
pending_name: Option<String>,
|
pending_name: Option<String>,
|
||||||
},
|
},
|
||||||
Rename {
|
Rename {
|
||||||
channel_id: u64,
|
location: ChannelLocation<'static>,
|
||||||
pending_name: Option<String>,
|
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 {
|
pub struct CollabPanel {
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
has_focus: bool,
|
has_focus: bool,
|
||||||
|
link_or_move: Option<ChannelCopy>,
|
||||||
pending_serialization: Task<Option<()>>,
|
pending_serialization: Task<Option<()>>,
|
||||||
context_menu: ViewHandle<ContextMenu>,
|
context_menu: ViewHandle<ContextMenu>,
|
||||||
filter_editor: ViewHandle<Editor>,
|
filter_editor: ViewHandle<Editor>,
|
||||||
|
@ -177,7 +302,7 @@ pub struct CollabPanel {
|
||||||
list_state: ListState<Self>,
|
list_state: ListState<Self>,
|
||||||
subscriptions: Vec<Subscription>,
|
subscriptions: Vec<Subscription>,
|
||||||
collapsed_sections: Vec<Section>,
|
collapsed_sections: Vec<Section>,
|
||||||
collapsed_channels: Vec<ChannelId>,
|
collapsed_channels: Vec<ChannelLocation<'static>>,
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
context_menu_on_selected: bool,
|
context_menu_on_selected: bool,
|
||||||
}
|
}
|
||||||
|
@ -185,7 +310,7 @@ pub struct CollabPanel {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct SerializedCollabPanel {
|
struct SerializedCollabPanel {
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
collapsed_channels: Option<Vec<ChannelId>>,
|
collapsed_channels: Option<Vec<ChannelLocation<'static>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -229,6 +354,7 @@ enum ListEntry {
|
||||||
Channel {
|
Channel {
|
||||||
channel: Arc<Channel>,
|
channel: Arc<Channel>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
path: ChannelPath,
|
||||||
},
|
},
|
||||||
ChannelNotes {
|
ChannelNotes {
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
|
@ -353,10 +479,15 @@ impl CollabPanel {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ListEntry::Channel { channel, depth } => {
|
ListEntry::Channel {
|
||||||
|
channel,
|
||||||
|
depth,
|
||||||
|
path,
|
||||||
|
} => {
|
||||||
let channel_row = this.render_channel(
|
let channel_row = this.render_channel(
|
||||||
&*channel,
|
&*channel,
|
||||||
*depth,
|
*depth,
|
||||||
|
path.to_owned(),
|
||||||
&theme.collab_panel,
|
&theme.collab_panel,
|
||||||
is_selected,
|
is_selected,
|
||||||
cx,
|
cx,
|
||||||
|
@ -425,6 +556,7 @@ impl CollabPanel {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
width: None,
|
width: None,
|
||||||
has_focus: false,
|
has_focus: false,
|
||||||
|
link_or_move: None,
|
||||||
fs: workspace.app_state().fs.clone(),
|
fs: workspace.app_state().fs.clone(),
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
||||||
|
@ -512,7 +644,13 @@ impl CollabPanel {
|
||||||
.log_err()
|
.log_err()
|
||||||
.flatten()
|
.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 {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -702,28 +840,24 @@ impl CollabPanel {
|
||||||
executor.clone(),
|
executor.clone(),
|
||||||
));
|
));
|
||||||
if let Some(state) = &self.channel_editing_state {
|
if let Some(state) = &self.channel_editing_state {
|
||||||
if matches!(
|
if matches!(state, ChannelEditingState::Create { location: None, .. }) {
|
||||||
state,
|
|
||||||
ChannelEditingState::Create {
|
|
||||||
parent_id: None,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut collapse_depth = None;
|
let mut collapse_depth = None;
|
||||||
for mat in matches {
|
for mat in matches {
|
||||||
let (depth, channel) =
|
let (channel, path) = channel_store.channel_at_index(mat.candidate_id).unwrap();
|
||||||
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);
|
collapse_depth = Some(depth);
|
||||||
} else if let Some(collapsed_depth) = collapse_depth {
|
} else if let Some(collapsed_depth) = collapse_depth {
|
||||||
if depth > collapsed_depth {
|
if depth > collapsed_depth {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if self.is_channel_collapsed(channel.id) {
|
if self.is_channel_collapsed(&location) {
|
||||||
collapse_depth = Some(depth);
|
collapse_depth = Some(depth);
|
||||||
} else {
|
} else {
|
||||||
collapse_depth = None;
|
collapse_depth = None;
|
||||||
|
@ -731,18 +865,21 @@ impl CollabPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
match &self.channel_editing_state {
|
match &self.channel_editing_state {
|
||||||
Some(ChannelEditingState::Create { parent_id, .. })
|
Some(ChannelEditingState::Create {
|
||||||
if *parent_id == Some(channel.id) =>
|
location: parent_id,
|
||||||
{
|
..
|
||||||
|
}) if *parent_id == Some(location) => {
|
||||||
self.entries.push(ListEntry::Channel {
|
self.entries.push(ListEntry::Channel {
|
||||||
channel: channel.clone(),
|
channel: channel.clone(),
|
||||||
depth,
|
depth,
|
||||||
|
path: path.clone(),
|
||||||
});
|
});
|
||||||
self.entries
|
self.entries
|
||||||
.push(ListEntry::ChannelEditor { depth: depth + 1 });
|
.push(ListEntry::ChannelEditor { depth: depth + 1 });
|
||||||
}
|
}
|
||||||
Some(ChannelEditingState::Rename { channel_id, .. })
|
Some(ChannelEditingState::Rename { location, .. })
|
||||||
if *channel_id == channel.id =>
|
if location.channel == channel.id
|
||||||
|
&& location.path == Cow::Borrowed(path) =>
|
||||||
{
|
{
|
||||||
self.entries.push(ListEntry::ChannelEditor { depth });
|
self.entries.push(ListEntry::ChannelEditor { depth });
|
||||||
}
|
}
|
||||||
|
@ -750,6 +887,7 @@ impl CollabPanel {
|
||||||
self.entries.push(ListEntry::Channel {
|
self.entries.push(ListEntry::Channel {
|
||||||
channel: channel.clone(),
|
channel: channel.clone(),
|
||||||
depth,
|
depth,
|
||||||
|
path: path.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1546,14 +1684,21 @@ impl CollabPanel {
|
||||||
&self,
|
&self,
|
||||||
channel: &Channel,
|
channel: &Channel,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
path: ChannelPath,
|
||||||
theme: &theme::CollabPanel,
|
theme: &theme::CollabPanel,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
let channel_id = channel.id;
|
let channel_id = channel.id;
|
||||||
let has_children = self.channel_store.read(cx).has_children(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 is_active = iife!({
|
||||||
let call_channel = ActiveCall::global(cx)
|
let call_channel = ActiveCall::global(cx)
|
||||||
|
@ -1567,7 +1712,7 @@ impl CollabPanel {
|
||||||
|
|
||||||
const FACEPILE_LIMIT: usize = 3;
|
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()
|
Flex::<Self>::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Svg::new("icons/hash.svg")
|
Svg::new("icons/hash.svg")
|
||||||
|
@ -1618,8 +1763,13 @@ impl CollabPanel {
|
||||||
})
|
})
|
||||||
.align_children_center()
|
.align_children_center()
|
||||||
.styleable_component()
|
.styleable_component()
|
||||||
.disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
|
.disclosable(
|
||||||
.with_id(channel_id as usize)
|
disclosed,
|
||||||
|
Box::new(ToggleCollapse {
|
||||||
|
location: (channel_id, path.clone()).into(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_id(id(&path) as usize)
|
||||||
.with_style(theme.disclosure.clone())
|
.with_style(theme.disclosure.clone())
|
||||||
.element()
|
.element()
|
||||||
.constrained()
|
.constrained()
|
||||||
|
@ -1635,7 +1785,11 @@ impl CollabPanel {
|
||||||
this.join_channel(channel_id, cx);
|
this.join_channel(channel_id, cx);
|
||||||
})
|
})
|
||||||
.on_click(MouseButton::Right, move |e, this, 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)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.into_any()
|
.into_any()
|
||||||
|
@ -1882,11 +2036,20 @@ impl CollabPanel {
|
||||||
fn deploy_channel_context_menu(
|
fn deploy_channel_context_menu(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Option<Vector2F>,
|
position: Option<Vector2F>,
|
||||||
channel_id: u64,
|
location: &ChannelLocation<'static>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.context_menu_on_selected = position.is_none();
|
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| {
|
self.context_menu.update(cx, |context_menu, cx| {
|
||||||
context_menu.set_position_mode(if self.context_menu_on_selected {
|
context_menu.set_position_mode(if self.context_menu_on_selected {
|
||||||
OverlayPositionMode::Local
|
OverlayPositionMode::Local
|
||||||
|
@ -1894,27 +2057,112 @@ impl CollabPanel {
|
||||||
OverlayPositionMode::Window
|
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"
|
"Expand Subchannels"
|
||||||
} else {
|
} else {
|
||||||
"Collapse Subchannels"
|
"Collapse Subchannels"
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut items = vec![
|
items.extend([
|
||||||
ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
|
ContextMenuItem::action(
|
||||||
ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
|
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([
|
items.extend([
|
||||||
ContextMenuItem::Separator,
|
ContextMenuItem::Separator,
|
||||||
ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
|
ContextMenuItem::action(
|
||||||
ContextMenuItem::action("Rename", RenameChannel { channel_id }),
|
"New Subchannel",
|
||||||
|
NewChannel {
|
||||||
|
location: location.clone(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ContextMenuItem::action(
|
||||||
|
"Rename",
|
||||||
|
RenameChannel {
|
||||||
|
location: location.clone(),
|
||||||
|
},
|
||||||
|
),
|
||||||
ContextMenuItem::Separator,
|
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::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 {
|
if let Some(editing_state) = &mut self.channel_editing_state {
|
||||||
match editing_state {
|
match editing_state {
|
||||||
ChannelEditingState::Create {
|
ChannelEditingState::Create {
|
||||||
parent_id,
|
location,
|
||||||
pending_name,
|
pending_name,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
@ -2053,13 +2301,17 @@ impl CollabPanel {
|
||||||
|
|
||||||
self.channel_store
|
self.channel_store
|
||||||
.update(cx, |channel_store, cx| {
|
.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();
|
.detach();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
ChannelEditingState::Rename {
|
ChannelEditingState::Rename {
|
||||||
channel_id,
|
location,
|
||||||
pending_name,
|
pending_name,
|
||||||
} => {
|
} => {
|
||||||
if pending_name.is_some() {
|
if pending_name.is_some() {
|
||||||
|
@ -2070,7 +2322,7 @@ impl CollabPanel {
|
||||||
|
|
||||||
self.channel_store
|
self.channel_store
|
||||||
.update(cx, |channel_store, cx| {
|
.update(cx, |channel_store, cx| {
|
||||||
channel_store.rename(*channel_id, &channel_name, cx)
|
channel_store.rename(location.channel, &channel_name, cx)
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -2097,38 +2349,58 @@ impl CollabPanel {
|
||||||
_: &CollapseSelectedChannel,
|
_: &CollapseSelectedChannel,
|
||||||
cx: &mut ViewContext<Self>,
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.is_channel_collapsed(channel_id) {
|
let path = path.to_owned();
|
||||||
|
|
||||||
|
if self.is_channel_collapsed(&(channel_id, path.clone()).into()) {
|
||||||
return;
|
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>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if !self.is_channel_collapsed(channel_id) {
|
let path = path.to_owned();
|
||||||
|
|
||||||
|
if !self.is_channel_collapsed(&(channel_id, path.clone()).into()) {
|
||||||
return;
|
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>) {
|
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) => {
|
Ok(ix) => {
|
||||||
self.collapsed_channels.remove(ix);
|
self.collapsed_channels.remove(ix);
|
||||||
}
|
}
|
||||||
Err(ix) => {
|
Err(ix) => {
|
||||||
self.collapsed_channels.insert(ix, channel_id);
|
self.collapsed_channels.insert(ix, location);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.serialize(cx);
|
self.serialize(cx);
|
||||||
|
@ -2137,8 +2409,8 @@ impl CollabPanel {
|
||||||
cx.focus_self();
|
cx.focus_self();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
|
fn is_channel_collapsed(&self, location: &ChannelLocation) -> bool {
|
||||||
self.collapsed_channels.binary_search(&channel).is_ok()
|
self.collapsed_channels.binary_search(location).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leave_call(cx: &mut ViewContext<Self>) {
|
fn leave_call(cx: &mut ViewContext<Self>) {
|
||||||
|
@ -2163,7 +2435,7 @@ impl CollabPanel {
|
||||||
|
|
||||||
fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
|
fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.channel_editing_state = Some(ChannelEditingState::Create {
|
self.channel_editing_state = Some(ChannelEditingState::Create {
|
||||||
parent_id: None,
|
location: None,
|
||||||
pending_name: None,
|
pending_name: None,
|
||||||
});
|
});
|
||||||
self.update_entries(false, cx);
|
self.update_entries(false, cx);
|
||||||
|
@ -2181,9 +2453,9 @@ impl CollabPanel {
|
||||||
|
|
||||||
fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
|
fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
|
||||||
self.collapsed_channels
|
self.collapsed_channels
|
||||||
.retain(|&channel| channel != action.channel_id);
|
.retain(|channel| *channel != action.location);
|
||||||
self.channel_editing_state = Some(ChannelEditingState::Create {
|
self.channel_editing_state = Some(ChannelEditingState::Create {
|
||||||
parent_id: Some(action.channel_id),
|
location: Some(action.location.to_owned()),
|
||||||
pending_name: None,
|
pending_name: None,
|
||||||
});
|
});
|
||||||
self.update_entries(false, cx);
|
self.update_entries(false, cx);
|
||||||
|
@ -2201,16 +2473,16 @@ impl CollabPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
|
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)
|
self.remove_channel(channel.id, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
|
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(
|
self.rename_channel(
|
||||||
&RenameChannel {
|
&RenameChannel {
|
||||||
channel_id: channel.id,
|
location: (channel.id, parent.to_owned()).into(),
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -2219,12 +2491,15 @@ impl CollabPanel {
|
||||||
|
|
||||||
fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
|
fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
|
||||||
let channel_store = self.channel_store.read(cx);
|
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;
|
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 {
|
self.channel_editing_state = Some(ChannelEditingState::Rename {
|
||||||
channel_id: action.channel_id,
|
location: action.location.to_owned(),
|
||||||
pending_name: None,
|
pending_name: None,
|
||||||
});
|
});
|
||||||
self.channel_name_editor.update(cx, |editor, cx| {
|
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>) {
|
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;
|
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
|
self.selection
|
||||||
.and_then(|ix| self.entries.get(ix))
|
.and_then(|ix| self.entries.get(ix))
|
||||||
.and_then(|entry| match entry {
|
.and_then(|entry| match entry {
|
||||||
ListEntry::Channel { channel, .. } => Some(channel),
|
ListEntry::Channel {
|
||||||
|
channel,
|
||||||
|
path: parent,
|
||||||
|
..
|
||||||
|
} => Some((channel, parent)),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2657,13 +2936,17 @@ impl PartialEq for ListEntry {
|
||||||
ListEntry::Channel {
|
ListEntry::Channel {
|
||||||
channel: channel_1,
|
channel: channel_1,
|
||||||
depth: depth_1,
|
depth: depth_1,
|
||||||
|
path: parent_1,
|
||||||
} => {
|
} => {
|
||||||
if let ListEntry::Channel {
|
if let ListEntry::Channel {
|
||||||
channel: channel_2,
|
channel: channel_2,
|
||||||
depth: depth_2,
|
depth: depth_2,
|
||||||
|
path: parent_2,
|
||||||
} = other
|
} = 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 } => {
|
ListEntry::ChannelNotes { channel_id } => {
|
||||||
|
@ -2726,3 +3009,26 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(style.container)
|
.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;
|
RespondToChannelInvite respond_to_channel_invite = 123;
|
||||||
UpdateChannels update_channels = 124;
|
UpdateChannels update_channels = 124;
|
||||||
JoinChannel join_channel = 125;
|
JoinChannel join_channel = 125;
|
||||||
RemoveChannel remove_channel = 126;
|
DeleteChannel delete_channel = 126;
|
||||||
GetChannelMembers get_channel_members = 127;
|
GetChannelMembers get_channel_members = 127;
|
||||||
GetChannelMembersResponse get_channel_members_response = 128;
|
GetChannelMembersResponse get_channel_members_response = 128;
|
||||||
SetChannelMemberAdmin set_channel_member_admin = 129;
|
SetChannelMemberAdmin set_channel_member_admin = 129;
|
||||||
|
@ -155,7 +155,10 @@ message Envelope {
|
||||||
RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
|
RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
|
||||||
UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
|
UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
|
||||||
RejoinChannelBuffers rejoin_channel_buffers = 140;
|
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 {
|
message UpdateChannels {
|
||||||
repeated Channel channels = 1;
|
repeated Channel channels = 1;
|
||||||
repeated uint64 remove_channels = 2;
|
repeated ChannelEdge delete_channel_edge = 2;
|
||||||
repeated Channel channel_invitations = 3;
|
repeated uint64 delete_channels = 3;
|
||||||
repeated uint64 remove_channel_invitations = 4;
|
repeated Channel channel_invitations = 4;
|
||||||
repeated ChannelParticipants channel_participants = 5;
|
repeated uint64 remove_channel_invitations = 5;
|
||||||
repeated ChannelPermission channel_permissions = 6;
|
repeated ChannelParticipants channel_participants = 6;
|
||||||
|
repeated ChannelPermission channel_permissions = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChannelEdge {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
uint64 parent_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChannelPermission {
|
message ChannelPermission {
|
||||||
|
@ -966,7 +975,7 @@ message JoinChannel {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoveChannel {
|
message DeleteChannel {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1021,6 +1030,22 @@ message RenameChannel {
|
||||||
string name = 2;
|
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 {
|
message JoinChannelBuffer {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,7 +237,10 @@ messages!(
|
||||||
(UpdateBuffer, Foreground),
|
(UpdateBuffer, Foreground),
|
||||||
(UpdateBufferFile, Foreground),
|
(UpdateBufferFile, Foreground),
|
||||||
(UpdateContacts, Foreground),
|
(UpdateContacts, Foreground),
|
||||||
(RemoveChannel, Foreground),
|
(DeleteChannel, Foreground),
|
||||||
|
(MoveChannel, Foreground),
|
||||||
|
(LinkChannel, Foreground),
|
||||||
|
(UnlinkChannel, Foreground),
|
||||||
(UpdateChannels, Foreground),
|
(UpdateChannels, Foreground),
|
||||||
(UpdateDiagnosticSummary, Foreground),
|
(UpdateDiagnosticSummary, Foreground),
|
||||||
(UpdateFollowers, Foreground),
|
(UpdateFollowers, Foreground),
|
||||||
|
@ -315,9 +318,12 @@ request_messages!(
|
||||||
(SetChannelMemberAdmin, Ack),
|
(SetChannelMemberAdmin, Ack),
|
||||||
(GetChannelMembers, GetChannelMembersResponse),
|
(GetChannelMembers, GetChannelMembersResponse),
|
||||||
(JoinChannel, JoinRoomResponse),
|
(JoinChannel, JoinRoomResponse),
|
||||||
(RemoveChannel, Ack),
|
(DeleteChannel, Ack),
|
||||||
(RenameProjectEntry, ProjectEntryResponse),
|
(RenameProjectEntry, ProjectEntryResponse),
|
||||||
(RenameChannel, ChannelResponse),
|
(RenameChannel, ChannelResponse),
|
||||||
|
(LinkChannel, Ack),
|
||||||
|
(UnlinkChannel, Ack),
|
||||||
|
(MoveChannel, Ack),
|
||||||
(SaveBuffer, BufferSaved),
|
(SaveBuffer, BufferSaved),
|
||||||
(SearchProject, SearchProjectResponse),
|
(SearchProject, SearchProjectResponse),
|
||||||
(ShareProject, ShareProjectResponse),
|
(ShareProject, ShareProjectResponse),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue