Add UI/UX for moving channels (#2976)
TODO: - [x] Add drag and drop - [x] Polish up in-flight decisions. - [x] Fix chat panel panic - [x] Add nice hover effect highlighting the matching ones - [x] Fix and test keyboard Release Notes: - N/A
This commit is contained in:
commit
e37373a636
28 changed files with 2964 additions and 808 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1214,6 +1214,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"settings",
|
"settings",
|
||||||
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
@ -1495,6 +1496,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"sha-1 0.9.8",
|
"sha-1 0.9.8",
|
||||||
|
"smallvec",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
|
@ -1525,6 +1527,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
"db",
|
"db",
|
||||||
|
"drag_and_drop",
|
||||||
"editor",
|
"editor",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
"feedback",
|
"feedback",
|
||||||
|
|
|
@ -585,6 +585,14 @@
|
||||||
"space": "menu::Confirm"
|
"space": "menu::Confirm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "CollabPanel > Editor",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-c": "collab_panel::StartLinkChannel",
|
||||||
|
"cmd-x": "collab_panel::StartMoveChannel",
|
||||||
|
"cmd-v": "collab_panel::MoveOrLinkToSelected"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "ChannelModal",
|
"context": "ChannelModal",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -28,6 +28,7 @@ anyhow.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
image = "0.23"
|
image = "0.23"
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
|
|
|
@ -4,7 +4,9 @@ mod channel_store;
|
||||||
|
|
||||||
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent};
|
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent};
|
||||||
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
|
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
|
||||||
pub use channel_store::{Channel, ChannelEvent, ChannelId, ChannelMembership, ChannelStore};
|
pub use channel_store::{
|
||||||
|
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
|
||||||
|
};
|
||||||
|
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
@ -1,20 +1,37 @@
|
||||||
|
mod channel_index;
|
||||||
|
|
||||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
|
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use client::{Client, Subscription, User, UserId, UserStore};
|
use client::{Client, Subscription, User, UserId, UserStore};
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{
|
||||||
|
hash_map::{self, DefaultHasher},
|
||||||
|
HashMap, HashSet,
|
||||||
|
};
|
||||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||||
use rpc::{proto, TypedEnvelope};
|
use rpc::{
|
||||||
use std::{mem, sync::Arc, time::Duration};
|
proto::{self, ChannelEdge, ChannelPermission},
|
||||||
|
TypedEnvelope,
|
||||||
|
};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
mem,
|
||||||
|
ops::Deref,
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use self::channel_index::ChannelIndex;
|
||||||
|
|
||||||
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>,
|
||||||
|
@ -30,12 +47,17 @@ pub struct ChannelStore {
|
||||||
_update_channels: Task<()>,
|
_update_channels: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type ChannelData = (Channel, ChannelPath);
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub id: ChannelId,
|
pub id: ChannelId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ChannelPath(Arc<[ChannelId]>);
|
||||||
|
|
||||||
pub struct ChannelMembership {
|
pub struct ChannelMembership {
|
||||||
pub user: Arc<User>,
|
pub user: Arc<User>,
|
||||||
pub kind: proto::channel_member::Kind,
|
pub kind: proto::channel_member::Kind,
|
||||||
|
@ -82,9 +104,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 +137,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 {
|
||||||
|
@ -125,29 +146,43 @@ impl ChannelStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the number of unique channels in the store
|
||||||
pub fn channel_count(&self) -> usize {
|
pub fn channel_count(&self) -> usize {
|
||||||
self.channel_paths.len()
|
self.channel_index.by_id().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the index of a channel ID in the list of unique channels
|
||||||
pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> {
|
pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> {
|
||||||
self.channel_paths
|
self.channel_index
|
||||||
.iter()
|
.by_id()
|
||||||
.position(|path| path.ends_with(&[channel_id]))
|
.keys()
|
||||||
|
.position(|id| *id == channel_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
|
/// Returns an iterator over all unique channels
|
||||||
self.channel_paths.iter().map(move |path| {
|
pub fn channels(&self) -> impl '_ + Iterator<Item = &Arc<Channel>> {
|
||||||
|
self.channel_index.by_id().values()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all entries in the channel DAG
|
||||||
|
pub fn channel_dag_entries(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
|
||||||
|
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_dag_entry_at(&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_at(&self, ix: usize) -> Option<&Arc<Channel>> {
|
||||||
|
self.channel_index.by_id().values().nth(ix)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn channel_invitations(&self) -> &[Arc<Channel>] {
|
pub fn channel_invitations(&self) -> &[Arc<Channel>] {
|
||||||
|
@ -155,7 +190,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 {
|
||||||
|
@ -268,7 +303,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()
|
||||||
|
@ -294,18 +329,33 @@ impl ChannelStore {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let name = name.trim_start_matches("#").to_owned();
|
let name = name.trim_start_matches("#").to_owned();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let channel = client
|
let response = client
|
||||||
.request(proto::CreateChannel { name, parent_id })
|
.request(proto::CreateChannel { name, parent_id })
|
||||||
.await?
|
.await?;
|
||||||
|
|
||||||
|
let channel = response
|
||||||
.channel
|
.channel
|
||||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||||
|
|
||||||
let channel_id = channel.id;
|
let channel_id = channel.id;
|
||||||
|
|
||||||
|
let parent_edge = if let Some(parent_id) = parent_id {
|
||||||
|
vec![ChannelEdge {
|
||||||
|
channel_id: channel.id,
|
||||||
|
parent_id,
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
let task = this.update_channels(
|
let task = this.update_channels(
|
||||||
proto::UpdateChannels {
|
proto::UpdateChannels {
|
||||||
channels: vec![channel],
|
channels: vec![channel],
|
||||||
|
insert_edge: parent_edge,
|
||||||
|
channel_permissions: vec![ChannelPermission {
|
||||||
|
channel_id,
|
||||||
|
is_admin: true,
|
||||||
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
|
@ -323,6 +373,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: 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: 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,
|
||||||
|
@ -502,7 +605,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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -639,11 +742,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();
|
||||||
|
|
||||||
|
@ -690,17 +793,20 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
|
let channels_changed = !payload.channels.is_empty()
|
||||||
if channels_changed {
|
|| !payload.delete_channels.is_empty()
|
||||||
if !payload.remove_channels.is_empty() {
|
|| !payload.insert_edge.is_empty()
|
||||||
self.channels_by_id
|
|| !payload.delete_edge.is_empty();
|
||||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
|
||||||
self.channel_participants
|
|
||||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
|
||||||
self.channels_with_admin_privileges
|
|
||||||
.retain(|channel_id| !payload.remove_channels.contains(channel_id));
|
|
||||||
|
|
||||||
for channel_id in &payload.remove_channels {
|
if channels_changed {
|
||||||
|
if !payload.delete_channels.is_empty() {
|
||||||
|
self.channel_index.delete_channels(&payload.delete_channels);
|
||||||
|
self.channel_participants
|
||||||
|
.retain(|channel_id, _| !payload.delete_channels.contains(channel_id));
|
||||||
|
self.channels_with_admin_privileges
|
||||||
|
.retain(|channel_id| !payload.delete_channels.contains(channel_id));
|
||||||
|
|
||||||
|
for channel_id in &payload.delete_channels {
|
||||||
let channel_id = *channel_id;
|
let channel_id = *channel_id;
|
||||||
if let Some(OpenedModelHandle::Open(buffer)) =
|
if let Some(OpenedModelHandle::Open(buffer)) =
|
||||||
self.opened_buffers.remove(&channel_id)
|
self.opened_buffers.remove(&channel_id)
|
||||||
|
@ -712,44 +818,18 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for channel_proto in payload.channels {
|
let mut index = self.channel_index.bulk_insert();
|
||||||
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;
|
index.insert(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.insert_edge {
|
||||||
let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
|
index.insert_edge(edge.channel_id, edge.parent_id);
|
||||||
let b = Self::channel_path_sorting_key(b, &self.channels_by_id);
|
}
|
||||||
a.cmp(b)
|
|
||||||
});
|
for edge in payload.delete_edge {
|
||||||
self.channel_paths.dedup();
|
index.delete_edge(edge.parent_id, edge.channel_id);
|
||||||
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 {
|
||||||
|
@ -807,12 +887,51 @@ impl ChannelStore {
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn channel_path_sorting_key<'a>(
|
impl Deref for ChannelPath {
|
||||||
path: &'a [ChannelId],
|
type Target = [ChannelId];
|
||||||
channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>,
|
|
||||||
) -> impl 'a + Iterator<Item = Option<&'a str>> {
|
fn deref(&self) -> &Self::Target {
|
||||||
path.iter()
|
&self.0
|
||||||
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelPath {
|
||||||
|
pub fn new(path: Arc<[ChannelId]>) -> Self {
|
||||||
|
debug_assert!(path.len() >= 1);
|
||||||
|
Self(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent_id(&self) -> Option<ChannelId> {
|
||||||
|
self.0.len().checked_sub(2).map(|i| self.0[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channel_id(&self) -> ChannelId {
|
||||||
|
self.0[self.0.len() - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unique_id(&self) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
self.0.deref().hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ChannelPath> for Cow<'static, ChannelPath> {
|
||||||
|
fn from(value: ChannelPath) -> Self {
|
||||||
|
Cow::Owned(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ChannelPath> for Cow<'a, ChannelPath> {
|
||||||
|
fn from(value: &'a ChannelPath) -> Self {
|
||||||
|
Cow::Borrowed(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ChannelPath {
|
||||||
|
fn default() -> Self {
|
||||||
|
ChannelPath(Arc::from([]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
162
crates/channel/src/channel_store/channel_index.rs
Normal file
162
crates/channel/src/channel_store/channel_index.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{Channel, ChannelId};
|
||||||
|
use collections::BTreeMap;
|
||||||
|
use rpc::proto;
|
||||||
|
|
||||||
|
use super::ChannelPath;
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct ChannelIndex {
|
||||||
|
paths: Vec<ChannelPath>,
|
||||||
|
channels_by_id: BTreeMap<ChannelId, Arc<Channel>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelIndex {
|
||||||
|
pub fn by_id(&self) -> &BTreeMap<ChannelId, Arc<Channel>> {
|
||||||
|
&self.channels_by_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.paths.clear();
|
||||||
|
self.channels_by_id.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(|path| {
|
||||||
|
path.iter()
|
||||||
|
.all(|channel_id| self.channels_by_id.contains_key(channel_id))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
|
||||||
|
ChannelPathsInsertGuard {
|
||||||
|
paths: &mut self.paths,
|
||||||
|
channels_by_id: &mut self.channels_by_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for ChannelIndex {
|
||||||
|
type Target = [ChannelPath];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.paths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A guard for ensuring that the paths index maintains its sort and uniqueness
|
||||||
|
/// invariants after a series of insertions
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChannelPathsInsertGuard<'a> {
|
||||||
|
paths: &'a mut Vec<ChannelPath>,
|
||||||
|
channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ChannelPathsInsertGuard<'a> {
|
||||||
|
/// Remove the given edge from this index. This will not remove the channel.
|
||||||
|
/// If this operation would result in a dangling edge, re-insert it.
|
||||||
|
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])
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure that there is at least one channel path in the index
|
||||||
|
if !self
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.any(|path| path.iter().any(|id| id == &channel_id))
|
||||||
|
{
|
||||||
|
self.insert_root(channel_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, channel_proto: proto::Channel) {
|
||||||
|
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||||
|
Arc::make_mut(existing_channel).name = channel_proto.name;
|
||||||
|
} else {
|
||||||
|
self.channels_by_id.insert(
|
||||||
|
channel_proto.id,
|
||||||
|
Arc::new(Channel {
|
||||||
|
id: channel_proto.id,
|
||||||
|
name: channel_proto.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
self.insert_root(channel_proto.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_edge(&mut self, channel_id: ChannelId, parent_id: ChannelId) {
|
||||||
|
let mut parents = Vec::new();
|
||||||
|
let mut descendants = Vec::new();
|
||||||
|
let mut ixs_to_remove = Vec::new();
|
||||||
|
|
||||||
|
for (ix, path) in self.paths.iter().enumerate() {
|
||||||
|
if path
|
||||||
|
.windows(2)
|
||||||
|
.any(|window| window[0] == parent_id && window[1] == channel_id)
|
||||||
|
{
|
||||||
|
// We already have this edge in the index
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if path.ends_with(&[parent_id]) {
|
||||||
|
parents.push(path);
|
||||||
|
} else if let Some(position) = path.iter().position(|id| id == &channel_id) {
|
||||||
|
if position == 0 {
|
||||||
|
ixs_to_remove.push(ix);
|
||||||
|
}
|
||||||
|
descendants.push(path.split_at(position).1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_paths = Vec::new();
|
||||||
|
for parent in parents.iter() {
|
||||||
|
if descendants.is_empty() {
|
||||||
|
let mut new_path = Vec::with_capacity(parent.len() + 1);
|
||||||
|
new_path.extend_from_slice(parent);
|
||||||
|
new_path.push(channel_id);
|
||||||
|
new_paths.push(ChannelPath::new(new_path.into()));
|
||||||
|
} else {
|
||||||
|
for descendant in descendants.iter() {
|
||||||
|
let mut new_path = Vec::with_capacity(parent.len() + descendant.len());
|
||||||
|
new_path.extend_from_slice(parent);
|
||||||
|
new_path.extend_from_slice(descendant);
|
||||||
|
new_paths.push(ChannelPath::new(new_path.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ix in ixs_to_remove.into_iter().rev() {
|
||||||
|
self.paths.swap_remove(ix);
|
||||||
|
}
|
||||||
|
self.paths.extend(new_paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_root(&mut self, channel_id: ChannelId) {
|
||||||
|
self.paths.push(ChannelPath::new(Arc::from([channel_id])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Drop for ChannelPathsInsertGuard<'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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn channel_path_sorting_key<'a>(
|
||||||
|
path: &'a [ChannelId],
|
||||||
|
channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>,
|
||||||
|
) -> impl 'a + Iterator<Item = Option<&'a str>> {
|
||||||
|
path.iter()
|
||||||
|
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
||||||
|
}
|
|
@ -18,12 +18,10 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
parent_id: None,
|
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "a".to_string(),
|
name: "a".to_string(),
|
||||||
parent_id: None,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
channel_permissions: vec![proto::ChannelPermission {
|
channel_permissions: vec![proto::ChannelPermission {
|
||||||
|
@ -51,12 +49,20 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 3,
|
id: 3,
|
||||||
name: "x".to_string(),
|
name: "x".to_string(),
|
||||||
parent_id: Some(1),
|
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 4,
|
id: 4,
|
||||||
name: "y".to_string(),
|
name: "y".to_string(),
|
||||||
parent_id: Some(2),
|
},
|
||||||
|
],
|
||||||
|
insert_edge: vec![
|
||||||
|
proto::ChannelEdge {
|
||||||
|
parent_id: 1,
|
||||||
|
channel_id: 3,
|
||||||
|
},
|
||||||
|
proto::ChannelEdge {
|
||||||
|
parent_id: 2,
|
||||||
|
channel_id: 4,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -86,17 +92,24 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: "a".to_string(),
|
name: "a".to_string(),
|
||||||
parent_id: None,
|
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
parent_id: Some(0),
|
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "c".to_string(),
|
name: "c".to_string(),
|
||||||
parent_id: Some(1),
|
},
|
||||||
|
],
|
||||||
|
insert_edge: vec![
|
||||||
|
proto::ChannelEdge {
|
||||||
|
parent_id: 0,
|
||||||
|
channel_id: 1,
|
||||||
|
},
|
||||||
|
proto::ChannelEdge {
|
||||||
|
parent_id: 1,
|
||||||
|
channel_id: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
channel_permissions: vec![proto::ChannelPermission {
|
channel_permissions: vec![proto::ChannelPermission {
|
||||||
|
@ -122,7 +135,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,
|
||||||
|
@ -145,7 +158,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
channels: vec![proto::Channel {
|
channels: vec![proto::Channel {
|
||||||
id: channel_id,
|
id: channel_id,
|
||||||
name: "the-channel".to_string(),
|
name: "the-channel".to_string(),
|
||||||
parent_id: None,
|
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
@ -169,7 +181,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
// Join a channel and populate its existing messages.
|
// Join a channel and populate its existing messages.
|
||||||
let channel = channel_store.update(cx, |store, cx| {
|
let channel = channel_store.update(cx, |store, cx| {
|
||||||
let channel_id = store.channels().next().unwrap().1.id;
|
let channel_id = store.channel_dag_entries().next().unwrap().1.id;
|
||||||
store.open_channel_chat(channel_id, cx)
|
store.open_channel_chat(channel_id, cx)
|
||||||
});
|
});
|
||||||
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
|
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
|
||||||
|
@ -351,7 +363,7 @@ fn assert_channels(
|
||||||
) {
|
) {
|
||||||
let actual = channel_store.read_with(cx, |store, _| {
|
let actual = channel_store.read_with(cx, |store, _| {
|
||||||
store
|
store
|
||||||
.channels()
|
.channel_dag_entries()
|
||||||
.map(|(depth, channel)| {
|
.map(|(depth, channel)| {
|
||||||
(
|
(
|
||||||
depth,
|
depth,
|
||||||
|
|
|
@ -41,6 +41,7 @@ prost.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||||
scrypt = "0.7"
|
scrypt = "0.7"
|
||||||
|
smallvec.workspace = true
|
||||||
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
|
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
|
||||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
|
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
|
||||||
sea-query = "0.27"
|
sea-query = "0.27"
|
||||||
|
@ -72,7 +73,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 +81,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
|
||||||
|
|
|
@ -14,7 +14,10 @@ use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use rand::{prelude::StdRng, Rng, SeedableRng};
|
use rand::{prelude::StdRng, Rng, SeedableRng};
|
||||||
use rpc::{proto, ConnectionId};
|
use rpc::{
|
||||||
|
proto::{self},
|
||||||
|
ConnectionId,
|
||||||
|
};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection,
|
entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection,
|
||||||
DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType,
|
DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType,
|
||||||
|
@ -43,6 +46,8 @@ pub use ids::*;
|
||||||
pub use sea_orm::ConnectOptions;
|
pub use sea_orm::ConnectOptions;
|
||||||
pub use tables::user::Model as User;
|
pub use tables::user::Model as User;
|
||||||
|
|
||||||
|
use self::queries::channels::ChannelGraph;
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
options: ConnectOptions,
|
options: ConnectOptions,
|
||||||
pool: DatabaseConnection,
|
pool: DatabaseConnection,
|
||||||
|
@ -421,16 +426,15 @@ pub struct NewUserResult {
|
||||||
pub signup_device_id: Option<String>,
|
pub signup_device_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromQueryResult, Debug, PartialEq)]
|
#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub id: ChannelId,
|
pub id: ChannelId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub parent_id: Option<ChannelId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct ChannelsForUser {
|
pub struct ChannelsForUser {
|
||||||
pub channels: Vec<Channel>,
|
pub channels: ChannelGraph,
|
||||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||||
pub channels_with_admin_privileges: HashSet<ChannelId>,
|
pub channels_with_admin_privileges: HashSet<ChannelId>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
use rpc::proto::ChannelEdge;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
type ChannelDescendants = HashMap<ChannelId, SmallSet<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)>> {
|
||||||
|
@ -46,7 +51,6 @@ impl Database {
|
||||||
.insert(&*tx)
|
.insert(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let channel_paths_stmt;
|
|
||||||
if let Some(parent) = parent {
|
if let Some(parent) = parent {
|
||||||
let sql = r#"
|
let sql = r#"
|
||||||
INSERT INTO channel_paths
|
INSERT INTO channel_paths
|
||||||
|
@ -58,7 +62,7 @@ impl Database {
|
||||||
WHERE
|
WHERE
|
||||||
channel_id = $3
|
channel_id = $3
|
||||||
"#;
|
"#;
|
||||||
channel_paths_stmt = Statement::from_sql_and_values(
|
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||||
self.pool.get_database_backend(),
|
self.pool.get_database_backend(),
|
||||||
sql,
|
sql,
|
||||||
[
|
[
|
||||||
|
@ -100,7 +104,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 +153,19 @@ impl Database {
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Delete any other paths that include 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
|
||||||
|
@ -310,7 +327,6 @@ impl Database {
|
||||||
.map(|channel| Channel {
|
.map(|channel| Channel {
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
parent_id: None,
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -319,6 +335,49 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_channel_graph(
|
||||||
|
&self,
|
||||||
|
parents_by_child_id: ChannelDescendants,
|
||||||
|
trim_dangling_parents: bool,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<ChannelGraph> {
|
||||||
|
let mut channels = Vec::with_capacity(parents_by_child_id.len());
|
||||||
|
{
|
||||||
|
let mut rows = channel::Entity::find()
|
||||||
|
.filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(row) = rows.next().await {
|
||||||
|
let row = row?;
|
||||||
|
channels.push(Channel {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut edges = Vec::with_capacity(parents_by_child_id.len());
|
||||||
|
for (channel, parents) in parents_by_child_id.iter() {
|
||||||
|
for parent in parents.into_iter() {
|
||||||
|
if trim_dangling_parents {
|
||||||
|
if parents_by_child_id.contains_key(parent) {
|
||||||
|
edges.push(ChannelEdge {
|
||||||
|
channel_id: channel.to_proto(),
|
||||||
|
parent_id: parent.to_proto(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edges.push(ChannelEdge {
|
||||||
|
channel_id: channel.to_proto(),
|
||||||
|
parent_id: parent.to_proto(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ChannelGraph { channels, edges })
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<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;
|
||||||
|
@ -332,63 +391,82 @@ impl Database {
|
||||||
.all(&*tx)
|
.all(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let parents_by_child_id = self
|
self.get_user_channels(channel_memberships, &tx).await
|
||||||
.get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let channels_with_admin_privileges = channel_memberships
|
|
||||||
.iter()
|
|
||||||
.filter_map(|membership| membership.admin.then_some(membership.channel_id))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut channels = Vec::with_capacity(parents_by_child_id.len());
|
|
||||||
{
|
|
||||||
let mut rows = channel::Entity::find()
|
|
||||||
.filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let row = row?;
|
|
||||||
channels.push(Channel {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
parent_id: parents_by_child_id.get(&row.id).copied().flatten(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
|
||||||
enum QueryUserIdsAndChannelIds {
|
|
||||||
ChannelId,
|
|
||||||
UserId,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut channel_participants: HashMap<ChannelId, Vec<UserId>> = HashMap::default();
|
|
||||||
{
|
|
||||||
let mut rows = room_participant::Entity::find()
|
|
||||||
.inner_join(room::Entity)
|
|
||||||
.filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id)))
|
|
||||||
.select_only()
|
|
||||||
.column(room::Column::ChannelId)
|
|
||||||
.column(room_participant::Column::UserId)
|
|
||||||
.into_values::<_, QueryUserIdsAndChannelIds>()
|
|
||||||
.stream(&*tx)
|
|
||||||
.await?;
|
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let row: (ChannelId, UserId) = row?;
|
|
||||||
channel_participants.entry(row.0).or_default().push(row.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ChannelsForUser {
|
|
||||||
channels,
|
|
||||||
channel_participants,
|
|
||||||
channels_with_admin_privileges,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_channel_for_user(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> Result<ChannelsForUser> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let tx = tx;
|
||||||
|
|
||||||
|
let channel_membership = channel_member::Entity::find()
|
||||||
|
.filter(
|
||||||
|
channel_member::Column::UserId
|
||||||
|
.eq(user_id)
|
||||||
|
.and(channel_member::Column::ChannelId.eq(channel_id))
|
||||||
|
.and(channel_member::Column::Accepted.eq(true)),
|
||||||
|
)
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.get_user_channels(channel_membership, &tx).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_channels(
|
||||||
|
&self,
|
||||||
|
channel_memberships: Vec<channel_member::Model>,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<ChannelsForUser> {
|
||||||
|
let parents_by_child_id = self
|
||||||
|
.get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let channels_with_admin_privileges = channel_memberships
|
||||||
|
.iter()
|
||||||
|
.filter_map(|membership| membership.admin.then_some(membership.channel_id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let graph = self
|
||||||
|
.get_channel_graph(parents_by_child_id, true, &tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
|
enum QueryUserIdsAndChannelIds {
|
||||||
|
ChannelId,
|
||||||
|
UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut channel_participants: HashMap<ChannelId, Vec<UserId>> = HashMap::default();
|
||||||
|
{
|
||||||
|
let mut rows = room_participant::Entity::find()
|
||||||
|
.inner_join(room::Entity)
|
||||||
|
.filter(room::Column::ChannelId.is_in(graph.channels.iter().map(|c| c.id)))
|
||||||
|
.select_only()
|
||||||
|
.column(room::Column::ChannelId)
|
||||||
|
.column(room_participant::Column::UserId)
|
||||||
|
.into_values::<_, QueryUserIdsAndChannelIds>()
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
while let Some(row) = rows.next().await {
|
||||||
|
let row: (ChannelId, UserId) = row?;
|
||||||
|
channel_participants.entry(row.0).or_default().push(row.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ChannelsForUser {
|
||||||
|
channels: graph,
|
||||||
|
channel_participants,
|
||||||
|
channels_with_admin_privileges,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
|
pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
|
||||||
self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await })
|
self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await })
|
||||||
.await
|
.await
|
||||||
|
@ -559,6 +637,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 +645,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 +662,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 +707,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 +726,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)
|
||||||
|
@ -677,7 +774,6 @@ impl Database {
|
||||||
Channel {
|
Channel {
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
parent_id: None,
|
|
||||||
},
|
},
|
||||||
is_accepted,
|
is_accepted,
|
||||||
)))
|
)))
|
||||||
|
@ -703,9 +799,276 @@ 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<ChannelGraph> {
|
||||||
|
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<ChannelGraph> {
|
||||||
|
self.check_user_is_channel_admin(to, user, &*tx).await?;
|
||||||
|
|
||||||
|
let to_ancestors = self.get_channel_ancestors(to, &*tx).await?;
|
||||||
|
let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?;
|
||||||
|
for ancestor in to_ancestors {
|
||||||
|
if channel_descendants.contains_key(&ancestor) {
|
||||||
|
return Err(anyhow!("Cannot create a channel cycle").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now insert all of the new paths
|
||||||
|
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 (descdenant_id, descendant_parent_ids) in
|
||||||
|
channel_descendants.iter().filter(|(id, _)| id != &&channel)
|
||||||
|
{
|
||||||
|
for descendant_parent_id in descendant_parent_ids.iter() {
|
||||||
|
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||||
|
self.pool.get_database_backend(),
|
||||||
|
sql,
|
||||||
|
[
|
||||||
|
descdenant_id.to_proto().into(),
|
||||||
|
descdenant_id.to_proto().into(),
|
||||||
|
descendant_parent_id.to_proto().into(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
tx.execute(channel_paths_stmt).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're linking a channel, remove any root edges for the channel
|
||||||
|
{
|
||||||
|
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?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(channel) = channel_descendants.get_mut(&channel) {
|
||||||
|
// Remove the other parents
|
||||||
|
channel.clear();
|
||||||
|
channel.insert(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
let channels = self
|
||||||
|
.get_channel_graph(channel_descendants, false, &*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: 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: ChannelId,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<()> {
|
||||||
|
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?;
|
||||||
|
|
||||||
|
// 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: ChannelId,
|
||||||
|
to: ChannelId,
|
||||||
|
) -> Result<ChannelGraph> {
|
||||||
|
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)]
|
||||||
enum QueryUserIds {
|
enum QueryUserIds {
|
||||||
UserId,
|
UserId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChannelGraph {
|
||||||
|
pub channels: Vec<Channel>,
|
||||||
|
pub edges: Vec<ChannelEdge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelGraph {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.channels.is_empty() && self.edges.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl PartialEq for ChannelGraph {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
// Order independent comparison for tests
|
||||||
|
let channels_set = self.channels.iter().collect::<HashSet<_>>();
|
||||||
|
let other_channels_set = other.channels.iter().collect::<HashSet<_>>();
|
||||||
|
let edges_set = self
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.map(|edge| (edge.channel_id, edge.parent_id))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let other_edges_set = other
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.map(|edge| (edge.channel_id, edge.parent_id))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
channels_set == other_channels_set && edges_set == other_edges_set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
impl PartialEq for ChannelGraph {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.channels == other.channels && self.edges == other.edges
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SmallSet<T>(SmallVec<[T; 1]>);
|
||||||
|
|
||||||
|
impl<T> Deref for SmallSet<T> {
|
||||||
|
type Target = [T];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for SmallSet<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(SmallVec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SmallSet<T> {
|
||||||
|
fn insert(&mut self, value: T) -> bool
|
||||||
|
where
|
||||||
|
T: Ord,
|
||||||
|
{
|
||||||
|
match self.binary_search(&value) {
|
||||||
|
Ok(_) => false,
|
||||||
|
Err(ix) => {
|
||||||
|
self.0.insert(ix, value);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.0.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
mod message_tests;
|
mod message_tests;
|
||||||
|
@ -6,6 +7,7 @@ mod message_tests;
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::executor::Background;
|
use gpui::executor::Background;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use rpc::proto::ChannelEdge;
|
||||||
use sea_orm::ConnectionTrait;
|
use sea_orm::ConnectionTrait;
|
||||||
use sqlx::migrate::MigrateDatabase;
|
use sqlx::migrate::MigrateDatabase;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -143,3 +145,27 @@ impl Drop for TestDb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The second tuples are (channel_id, parent)
|
||||||
|
fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)]) -> ChannelGraph {
|
||||||
|
let mut graph = ChannelGraph {
|
||||||
|
channels: vec![],
|
||||||
|
edges: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (id, name) in channels {
|
||||||
|
graph.channels.push(Channel {
|
||||||
|
id: *id,
|
||||||
|
name: name.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (channel, parent) in edges {
|
||||||
|
graph.edges.push(ChannelEdge {
|
||||||
|
channel_id: channel.to_proto(),
|
||||||
|
parent_id: parent.to_proto(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
817
crates/collab/src/db/tests/channel_tests.rs
Normal file
817
crates/collab/src/db/tests/channel_tests.rs
Normal file
|
@ -0,0 +1,817 @@
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use rpc::{
|
||||||
|
proto::{self},
|
||||||
|
ConnectionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{queries::channels::ChannelGraph, tests::graph, 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,
|
||||||
|
graph(
|
||||||
|
&[
|
||||||
|
(zed_id, "zed"),
|
||||||
|
(crdb_id, "crdb"),
|
||||||
|
(livestreaming_id, "livestreaming"),
|
||||||
|
(replace_id, "replace"),
|
||||||
|
(rust_id, "rust"),
|
||||||
|
(cargo_id, "cargo"),
|
||||||
|
(cargo_ra_id, "cargo-ra")
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
(crdb_id, zed_id),
|
||||||
|
(livestreaming_id, zed_id),
|
||||||
|
(replace_id, zed_id),
|
||||||
|
(cargo_id, rust_id),
|
||||||
|
(cargo_ra_id, cargo_id),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result.channels,
|
||||||
|
graph(
|
||||||
|
&[
|
||||||
|
(zed_id, "zed"),
|
||||||
|
(crdb_id, "crdb"),
|
||||||
|
(livestreaming_id, "livestreaming"),
|
||||||
|
(replace_id, "replace")
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
(crdb_id, zed_id),
|
||||||
|
(livestreaming_id, zed_id),
|
||||||
|
(replace_id, 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,
|
||||||
|
graph(
|
||||||
|
&[
|
||||||
|
(zed_id, "zed"),
|
||||||
|
(crdb_id, "crdb"),
|
||||||
|
(livestreaming_id, "livestreaming"),
|
||||||
|
(replace_id, "replace")
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
(crdb_id, zed_id),
|
||||||
|
(livestreaming_id, zed_id),
|
||||||
|
(replace_id, 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_db_channel_moving,
|
||||||
|
test_channels_moving_postgres,
|
||||||
|
test_channels_moving_sqlite
|
||||||
|
);
|
||||||
|
|
||||||
|
async fn test_db_channel_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,
|
||||||
|
graph(
|
||||||
|
&[(livestreaming_dag_sub_id, "livestreaming_dag_sub")],
|
||||||
|
&[(livestreaming_dag_sub_id, 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,
|
||||||
|
graph(
|
||||||
|
&[
|
||||||
|
(livestreaming_id, "livestreaming"),
|
||||||
|
(livestreaming_dag_id, "livestreaming_dag"),
|
||||||
|
(livestreaming_dag_sub_id, "livestreaming_dag_sub"),
|
||||||
|
],
|
||||||
|
&[
|
||||||
|
(livestreaming_id, gpui2_id),
|
||||||
|
(livestreaming_dag_id, livestreaming_id),
|
||||||
|
(livestreaming_dag_sub_id, livestreaming_id),
|
||||||
|
(livestreaming_dag_sub_id, 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(zed_id)),
|
||||||
|
(livestreaming_id, Some(crdb_id)),
|
||||||
|
(livestreaming_id, Some(gpui2_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, 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, 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, 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, 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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// You should be able to move a root channel into a non-root channel
|
||||||
|
db.link_channel(a_id, crdb_id, 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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Prep for DAG deletion test
|
||||||
|
db.link_channel(a_id, livestreaming_id, 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 the 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: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
|
||||||
|
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
|
||||||
|
for channel in actual.channels {
|
||||||
|
actual_map.insert(channel.id, HashSet::default());
|
||||||
|
}
|
||||||
|
for edge in actual.edges {
|
||||||
|
actual_map
|
||||||
|
.get_mut(&ChannelId::from_proto(edge.channel_id))
|
||||||
|
.unwrap()
|
||||||
|
.insert(ChannelId::from_proto(edge.parent_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut expected_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
|
||||||
|
|
||||||
|
for (child, parent) in expected {
|
||||||
|
let entry = expected_map.entry(*child).or_default();
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
entry.insert(*parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pretty_assertions::assert_eq!(actual_map, expected_map)
|
||||||
|
}
|
|
@ -575,458 +575,6 @@ async fn test_fuzzy_search_users() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_background_executor() -> Arc<Background> {
|
fn build_background_executor() -> Arc<Background> {
|
||||||
Deterministic::new(0).build_background()
|
Deterministic::new(0).build_background()
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ use lazy_static::lazy_static;
|
||||||
use prometheus::{register_int_gauge, IntGauge};
|
use prometheus::{register_int_gauge, IntGauge};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{
|
proto::{
|
||||||
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage,
|
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage,
|
||||||
LiveKitConnectionInfo, RequestMessage,
|
EnvelopedMessage, LiveKitConnectionInfo, RequestMessage,
|
||||||
},
|
},
|
||||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||||
};
|
};
|
||||||
|
@ -250,7 +250,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)
|
||||||
|
@ -267,6 +267,9 @@ impl Server {
|
||||||
.add_request_handler(send_channel_message)
|
.add_request_handler(send_channel_message)
|
||||||
.add_request_handler(remove_channel_message)
|
.add_request_handler(remove_channel_message)
|
||||||
.add_request_handler(get_channel_messages)
|
.add_request_handler(get_channel_messages)
|
||||||
|
.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)
|
||||||
|
@ -2197,56 +2200,58 @@ async fn create_channel(
|
||||||
let channel = proto::Channel {
|
let channel = proto::Channel {
|
||||||
id: id.to_proto(),
|
id: id.to_proto(),
|
||||||
name: request.name,
|
name: request.name,
|
||||||
parent_id: request.parent_id,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
response.send(proto::ChannelResponse {
|
response.send(proto::CreateChannelResponse {
|
||||||
channel: Some(channel.clone()),
|
channel: Some(channel.clone()),
|
||||||
|
parent_id: request.parent_id,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut update = proto::UpdateChannels::default();
|
let Some(parent_id) = parent_id else {
|
||||||
update.channels.push(channel);
|
return Ok(());
|
||||||
|
|
||||||
let user_ids_to_notify = if let Some(parent_id) = parent_id {
|
|
||||||
db.get_channel_members(parent_id).await?
|
|
||||||
} else {
|
|
||||||
vec![session.user_id]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let update = proto::UpdateChannels {
|
||||||
|
channels: vec![channel],
|
||||||
|
insert_edge: vec![ChannelEdge {
|
||||||
|
parent_id: parent_id.to_proto(),
|
||||||
|
channel_id: id.to_proto(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_ids_to_notify = db.get_channel_members(parent_id).await?;
|
||||||
|
|
||||||
let connection_pool = session.connection_pool().await;
|
let connection_pool = session.connection_pool().await;
|
||||||
for user_id in user_ids_to_notify {
|
for user_id in user_ids_to_notify {
|
||||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||||
let mut update = update.clone();
|
|
||||||
if user_id == session.user_id {
|
if user_id == session.user_id {
|
||||||
update.channel_permissions.push(proto::ChannelPermission {
|
continue;
|
||||||
channel_id: id.to_proto(),
|
|
||||||
is_admin: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
session.peer.send(connection_id, update)?;
|
session.peer.send(connection_id, update.clone())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
@ -2279,7 +2284,6 @@ async fn invite_channel_member(
|
||||||
update.channel_invitations.push(proto::Channel {
|
update.channel_invitations.push(proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
parent_id: None,
|
|
||||||
});
|
});
|
||||||
for connection_id in session
|
for connection_id in session
|
||||||
.connection_pool()
|
.connection_pool()
|
||||||
|
@ -2306,7 +2310,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()
|
||||||
|
@ -2370,9 +2374,8 @@ async fn rename_channel(
|
||||||
let channel = proto::Channel {
|
let channel = proto::Channel {
|
||||||
id: request.channel_id,
|
id: request.channel_id,
|
||||||
name: new_name,
|
name: new_name,
|
||||||
parent_id: None,
|
|
||||||
};
|
};
|
||||||
response.send(proto::ChannelResponse {
|
response.send(proto::RenameChannelResponse {
|
||||||
channel: Some(channel.clone()),
|
channel: Some(channel.clone()),
|
||||||
})?;
|
})?;
|
||||||
let mut update = proto::UpdateChannels::default();
|
let mut update = proto::UpdateChannels::default();
|
||||||
|
@ -2390,6 +2393,127 @@ 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
|
||||||
|
.channels
|
||||||
|
.into_iter()
|
||||||
|
.map(|channel| proto::Channel {
|
||||||
|
id: channel.id.to_proto(),
|
||||||
|
name: channel.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
insert_edge: channels_to_send.edges,
|
||||||
|
..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 = ChannelId::from_proto(request.from);
|
||||||
|
|
||||||
|
db.unlink_channel(session.user_id, channel_id, from).await?;
|
||||||
|
|
||||||
|
let members = db.get_channel_members(from).await?;
|
||||||
|
|
||||||
|
let update = proto::UpdateChannels {
|
||||||
|
delete_edge: vec![proto::ChannelEdge {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
parent_id: from.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 = ChannelId::from_proto(request.from);
|
||||||
|
let to = ChannelId::from_proto(request.to);
|
||||||
|
|
||||||
|
let channels_to_send = db
|
||||||
|
.move_channel(session.user_id, channel_id, from_parent, to)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let members_from = db.get_channel_members(from_parent).await?;
|
||||||
|
let members_to = db.get_channel_members(to).await?;
|
||||||
|
|
||||||
|
let update = proto::UpdateChannels {
|
||||||
|
delete_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_from {
|
||||||
|
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||||
|
session.peer.send(connection_id, update.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let update = proto::UpdateChannels {
|
||||||
|
channels: channels_to_send
|
||||||
|
.channels
|
||||||
|
.into_iter()
|
||||||
|
.map(|channel| proto::Channel {
|
||||||
|
id: channel.id.to_proto(),
|
||||||
|
name: channel.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
insert_edge: channels_to_send.edges,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for member_id in members_to {
|
||||||
|
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>,
|
||||||
|
@ -2419,14 +2543,20 @@ async fn respond_to_channel_invite(
|
||||||
.remove_channel_invitations
|
.remove_channel_invitations
|
||||||
.push(channel_id.to_proto());
|
.push(channel_id.to_proto());
|
||||||
if request.accept {
|
if request.accept {
|
||||||
let result = db.get_channels_for_user(session.user_id).await?;
|
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
||||||
update
|
update
|
||||||
.channels
|
.channels
|
||||||
.extend(result.channels.into_iter().map(|channel| proto::Channel {
|
.extend(
|
||||||
id: channel.id.to_proto(),
|
result
|
||||||
name: channel.name,
|
.channels
|
||||||
parent_id: channel.parent_id.map(ChannelId::to_proto),
|
.channels
|
||||||
}));
|
.into_iter()
|
||||||
|
.map(|channel| proto::Channel {
|
||||||
|
id: channel.id.to_proto(),
|
||||||
|
name: channel.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
update.insert_edge = result.channels.edges;
|
||||||
update
|
update
|
||||||
.channel_participants
|
.channel_participants
|
||||||
.extend(
|
.extend(
|
||||||
|
@ -2844,14 +2974,15 @@ fn build_initial_channels_update(
|
||||||
) -> proto::UpdateChannels {
|
) -> proto::UpdateChannels {
|
||||||
let mut update = proto::UpdateChannels::default();
|
let mut update = proto::UpdateChannels::default();
|
||||||
|
|
||||||
for channel in channels.channels {
|
for channel in channels.channels.channels {
|
||||||
update.channels.push(proto::Channel {
|
update.channels.push(proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
parent_id: channel.parent_id.map(|id| id.to_proto()),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update.insert_edge = channels.channels.edges;
|
||||||
|
|
||||||
for (channel_id, participants) in channels.channel_participants {
|
for (channel_id, participants) in channels.channel_participants {
|
||||||
update
|
update
|
||||||
.channel_participants
|
.channel_participants
|
||||||
|
@ -2877,7 +3008,6 @@ fn build_initial_channels_update(
|
||||||
update.channel_invitations.push(proto::Channel {
|
update.channel_invitations.push(proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
parent_id: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ async fn test_core_channel_buffers(
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
.make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Client A joins the channel buffer
|
// Client A joins the channel buffer
|
||||||
|
@ -135,6 +135,7 @@ async fn test_channel_buffer_replica_ids(
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel(
|
.make_channel(
|
||||||
"the-channel",
|
"the-channel",
|
||||||
|
None,
|
||||||
(&client_a, cx_a),
|
(&client_a, cx_a),
|
||||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||||
)
|
)
|
||||||
|
@ -279,7 +280,7 @@ async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mu
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel("the-channel", (&client_a, cx_a), &mut [])
|
.make_channel("the-channel", None, (&client_a, cx_a), &mut [])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let channel_buffer_1 = client_a
|
let channel_buffer_1 = client_a
|
||||||
|
@ -341,7 +342,12 @@ async fn test_channel_buffer_disconnect(
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
.make_channel(
|
||||||
|
"the-channel",
|
||||||
|
None,
|
||||||
|
(&client_a, cx_a),
|
||||||
|
&mut [(&client_b, cx_b)],
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let channel_buffer_a = client_a
|
let channel_buffer_a = client_a
|
||||||
|
@ -411,7 +417,12 @@ async fn test_rejoin_channel_buffer(
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
.make_channel(
|
||||||
|
"the-channel",
|
||||||
|
None,
|
||||||
|
(&client_a, cx_a),
|
||||||
|
&mut [(&client_b, cx_b)],
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let channel_buffer_a = client_a
|
let channel_buffer_a = client_a
|
||||||
|
@ -491,6 +502,7 @@ async fn test_channel_buffers_and_server_restarts(
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel(
|
.make_channel(
|
||||||
"the-channel",
|
"the-channel",
|
||||||
|
None,
|
||||||
(&client_a, cx_a),
|
(&client_a, cx_a),
|
||||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,12 @@ async fn test_basic_channel_messages(
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
.make_channel(
|
||||||
|
"the-channel",
|
||||||
|
None,
|
||||||
|
(&client_a, cx_a),
|
||||||
|
&mut [(&client_b, cx_b)],
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let channel_chat_a = client_a
|
let channel_chat_a = client_a
|
||||||
|
@ -68,7 +73,12 @@ async fn test_rejoin_channel_chat(
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
.make_channel(
|
||||||
|
"the-channel",
|
||||||
|
None,
|
||||||
|
(&client_a, cx_a),
|
||||||
|
&mut [(&client_b, cx_b)],
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let channel_chat_a = client_a
|
let channel_chat_a = client_a
|
||||||
|
@ -139,6 +149,7 @@ async fn test_remove_channel_message(
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel(
|
.make_channel(
|
||||||
"the-channel",
|
"the-channel",
|
||||||
|
None,
|
||||||
(&client_a, cx_a),
|
(&client_a, cx_a),
|
||||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||||
)
|
)
|
||||||
|
|
|
@ -56,7 +56,10 @@ async fn test_core_channels(
|
||||||
);
|
);
|
||||||
|
|
||||||
client_b.channel_store().read_with(cx_b, |channels, _| {
|
client_b.channel_store().read_with(cx_b, |channels, _| {
|
||||||
assert!(channels.channels().collect::<Vec<_>>().is_empty())
|
assert!(channels
|
||||||
|
.channel_dag_entries()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.is_empty())
|
||||||
});
|
});
|
||||||
|
|
||||||
// Invite client B to channel A as client A.
|
// Invite client B to channel A as client A.
|
||||||
|
@ -142,6 +145,8 @@ async fn test_core_channels(
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
println!("STARTING CREATE CHANNEL C");
|
||||||
|
|
||||||
let channel_c_id = client_a
|
let channel_c_id = client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |channel_store, cx| {
|
.update(cx_a, |channel_store, cx| {
|
||||||
|
@ -326,7 +331,7 @@ async fn test_joining_channel_ancestor_member(
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let parent_id = server
|
let parent_id = server
|
||||||
.make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
.make_channel("parent", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let sub_id = client_a
|
let sub_id = client_a
|
||||||
|
@ -361,6 +366,7 @@ async fn test_channel_room(
|
||||||
let zed_id = server
|
let zed_id = server
|
||||||
.make_channel(
|
.make_channel(
|
||||||
"zed",
|
"zed",
|
||||||
|
None,
|
||||||
(&client_a, cx_a),
|
(&client_a, cx_a),
|
||||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||||
)
|
)
|
||||||
|
@ -544,9 +550,11 @@ async fn test_channel_jumping(deterministic: Arc<Deterministic>, cx_a: &mut Test
|
||||||
let mut server = TestServer::start(&deterministic).await;
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
|
||||||
let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
|
let zed_id = server
|
||||||
|
.make_channel("zed", None, (&client_a, cx_a), &mut [])
|
||||||
|
.await;
|
||||||
let rust_id = server
|
let rust_id = server
|
||||||
.make_channel("rust", (&client_a, cx_a), &mut [])
|
.make_channel("rust", None, (&client_a, cx_a), &mut [])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let active_call_a = cx_a.read(ActiveCall::global);
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
@ -597,7 +605,7 @@ async fn test_permissions_update_while_invited(
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let rust_id = server
|
let rust_id = server
|
||||||
.make_channel("rust", (&client_a, cx_a), &mut [])
|
.make_channel("rust", None, (&client_a, cx_a), &mut [])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
client_a
|
client_a
|
||||||
|
@ -658,7 +666,7 @@ async fn test_channel_rename(
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
let rust_id = server
|
let rust_id = server
|
||||||
.make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
.make_channel("rust", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Rename the channel
|
// Rename the channel
|
||||||
|
@ -716,6 +724,7 @@ async fn test_call_from_channel(
|
||||||
let channel_id = server
|
let channel_id = server
|
||||||
.make_channel(
|
.make_channel(
|
||||||
"x",
|
"x",
|
||||||
|
None,
|
||||||
(&client_a, cx_a),
|
(&client_a, cx_a),
|
||||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||||
)
|
)
|
||||||
|
@ -786,7 +795,9 @@ async fn test_lost_channel_creation(
|
||||||
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await;
|
let channel_id = server
|
||||||
|
.make_channel("x", None, (&client_a, cx_a), &mut [])
|
||||||
|
.await;
|
||||||
|
|
||||||
// Invite a member
|
// Invite a member
|
||||||
client_a
|
client_a
|
||||||
|
@ -874,6 +885,257 @@ async fn test_lost_channel_creation(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_channel_moving(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
cx_c: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
let client_c = server.create_client(cx_c, "user_c").await;
|
||||||
|
|
||||||
|
let channels = server
|
||||||
|
.make_channel_tree(
|
||||||
|
&[
|
||||||
|
("channel-a", None),
|
||||||
|
("channel-b", Some("channel-a")),
|
||||||
|
("channel-c", Some("channel-b")),
|
||||||
|
("channel-d", Some("channel-c")),
|
||||||
|
],
|
||||||
|
(&client_a, cx_a),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let channel_a_id = channels[0];
|
||||||
|
let channel_b_id = channels[1];
|
||||||
|
let channel_c_id = channels[2];
|
||||||
|
let channel_d_id = channels[3];
|
||||||
|
|
||||||
|
// Current shape:
|
||||||
|
// a - b - c - d
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_a.channel_store(),
|
||||||
|
cx_a,
|
||||||
|
&[
|
||||||
|
(channel_a_id, 0),
|
||||||
|
(channel_b_id, 1),
|
||||||
|
(channel_c_id, 2),
|
||||||
|
(channel_d_id, 3),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel_store, cx| {
|
||||||
|
channel_store.move_channel(channel_d_id, channel_c_id, channel_b_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Current shape:
|
||||||
|
// /- d
|
||||||
|
// a - b -- c
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_a.channel_store(),
|
||||||
|
cx_a,
|
||||||
|
&[
|
||||||
|
(channel_a_id, 0),
|
||||||
|
(channel_b_id, 1),
|
||||||
|
(channel_c_id, 2),
|
||||||
|
(channel_d_id, 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel_store, cx| {
|
||||||
|
channel_store.link_channel(channel_d_id, channel_c_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Current shape for A:
|
||||||
|
// /------\
|
||||||
|
// a - b -- c -- d
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_a.channel_store(),
|
||||||
|
cx_a,
|
||||||
|
&[
|
||||||
|
(channel_a_id, 0),
|
||||||
|
(channel_b_id, 1),
|
||||||
|
(channel_c_id, 2),
|
||||||
|
(channel_d_id, 3),
|
||||||
|
(channel_d_id, 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let b_channels = server
|
||||||
|
.make_channel_tree(
|
||||||
|
&[
|
||||||
|
("channel-mu", None),
|
||||||
|
("channel-gamma", Some("channel-mu")),
|
||||||
|
("channel-epsilon", Some("channel-mu")),
|
||||||
|
],
|
||||||
|
(&client_b, cx_b),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let channel_mu_id = b_channels[0];
|
||||||
|
let channel_ga_id = b_channels[1];
|
||||||
|
let channel_ep_id = b_channels[2];
|
||||||
|
|
||||||
|
// Current shape for B:
|
||||||
|
// /- ep
|
||||||
|
// mu -- ga
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_b.channel_store(),
|
||||||
|
cx_b,
|
||||||
|
&[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)],
|
||||||
|
);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Current shape for B:
|
||||||
|
// /- ep
|
||||||
|
// mu -- ga
|
||||||
|
// /---------\
|
||||||
|
// b -- c -- d
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_b.channel_store(),
|
||||||
|
cx_b,
|
||||||
|
&[
|
||||||
|
// New channels from a
|
||||||
|
(channel_b_id, 0),
|
||||||
|
(channel_c_id, 1),
|
||||||
|
(channel_d_id, 2),
|
||||||
|
(channel_d_id, 1),
|
||||||
|
// B's old channels
|
||||||
|
(channel_mu_id, 0),
|
||||||
|
(channel_ep_id, 1),
|
||||||
|
(channel_ga_id, 1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
client_b
|
||||||
|
.add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Current shape for C:
|
||||||
|
// - ep
|
||||||
|
assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]);
|
||||||
|
|
||||||
|
println!("*******************************************");
|
||||||
|
println!("********** STARTING LINK CHANNEL **********");
|
||||||
|
println!("*******************************************");
|
||||||
|
dbg!(client_b.user_id());
|
||||||
|
client_b
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_b, |channel_store, cx| {
|
||||||
|
channel_store.link_channel(channel_b_id, channel_ep_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Current shape for B:
|
||||||
|
// /---------\
|
||||||
|
// /- ep -- b -- c -- d
|
||||||
|
// mu -- ga
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_b.channel_store(),
|
||||||
|
cx_b,
|
||||||
|
&[
|
||||||
|
(channel_mu_id, 0),
|
||||||
|
(channel_ep_id, 1),
|
||||||
|
(channel_b_id, 2),
|
||||||
|
(channel_c_id, 3),
|
||||||
|
(channel_d_id, 4),
|
||||||
|
(channel_d_id, 3),
|
||||||
|
(channel_ga_id, 1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Current shape for C:
|
||||||
|
// /---------\
|
||||||
|
// ep -- b -- c -- d
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_c.channel_store(),
|
||||||
|
cx_c,
|
||||||
|
&[
|
||||||
|
(channel_ep_id, 0),
|
||||||
|
(channel_b_id, 1),
|
||||||
|
(channel_c_id, 2),
|
||||||
|
(channel_d_id, 3),
|
||||||
|
(channel_d_id, 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
client_b
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_b, |channel_store, cx| {
|
||||||
|
channel_store.link_channel(channel_ga_id, channel_b_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Current shape for B:
|
||||||
|
// /---------\
|
||||||
|
// /- ep -- b -- c -- d
|
||||||
|
// / \
|
||||||
|
// mu ---------- ga
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_b.channel_store(),
|
||||||
|
cx_b,
|
||||||
|
&[
|
||||||
|
(channel_mu_id, 0),
|
||||||
|
(channel_ep_id, 1),
|
||||||
|
(channel_b_id, 2),
|
||||||
|
(channel_c_id, 3),
|
||||||
|
(channel_d_id, 4),
|
||||||
|
(channel_d_id, 3),
|
||||||
|
(channel_ga_id, 3),
|
||||||
|
(channel_ga_id, 1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Current shape for A:
|
||||||
|
// /------\
|
||||||
|
// a - b -- c -- d
|
||||||
|
// \-- ga
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_a.channel_store(),
|
||||||
|
cx_a,
|
||||||
|
&[
|
||||||
|
(channel_a_id, 0),
|
||||||
|
(channel_b_id, 1),
|
||||||
|
(channel_c_id, 2),
|
||||||
|
(channel_d_id, 3),
|
||||||
|
(channel_d_id, 2),
|
||||||
|
(channel_ga_id, 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Current shape for C:
|
||||||
|
// /-------\
|
||||||
|
// ep -- b -- c -- d
|
||||||
|
// \-- ga
|
||||||
|
assert_channels_list_shape(
|
||||||
|
client_c.channel_store(),
|
||||||
|
cx_c,
|
||||||
|
&[
|
||||||
|
(channel_ep_id, 0),
|
||||||
|
(channel_b_id, 1),
|
||||||
|
(channel_c_id, 2),
|
||||||
|
(channel_d_id, 3),
|
||||||
|
(channel_d_id, 2),
|
||||||
|
(channel_ga_id, 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
struct ExpectedChannel {
|
struct ExpectedChannel {
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
@ -911,7 +1173,7 @@ fn assert_channels(
|
||||||
) {
|
) {
|
||||||
let actual = channel_store.read_with(cx, |store, _| {
|
let actual = channel_store.read_with(cx, |store, _| {
|
||||||
store
|
store
|
||||||
.channels()
|
.channel_dag_entries()
|
||||||
.map(|(depth, channel)| ExpectedChannel {
|
.map(|(depth, channel)| ExpectedChannel {
|
||||||
depth,
|
depth,
|
||||||
name: channel.name.clone(),
|
name: channel.name.clone(),
|
||||||
|
@ -920,5 +1182,22 @@ fn assert_channels(
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
assert_eq!(actual, expected_channels);
|
pretty_assertions::assert_eq!(actual, expected_channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_channels_list_shape(
|
||||||
|
channel_store: &ModelHandle<ChannelStore>,
|
||||||
|
cx: &TestAppContext,
|
||||||
|
expected_channels: &[(u64, usize)],
|
||||||
|
) {
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
let actual = channel_store.read_with(cx, |store, _| {
|
||||||
|
store
|
||||||
|
.channel_dag_entries()
|
||||||
|
.map(|(depth, channel)| (channel.id, depth))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
pretty_assertions::assert_eq!(dbg!(actual), expected_channels);
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||||
match rng.gen_range(0..100_u32) {
|
match rng.gen_range(0..100_u32) {
|
||||||
0..=29 => {
|
0..=29 => {
|
||||||
let channel_name = client.channel_store().read_with(cx, |store, cx| {
|
let channel_name = client.channel_store().read_with(cx, |store, cx| {
|
||||||
store.channels().find_map(|(_, channel)| {
|
store.channel_dag_entries().find_map(|(_, channel)| {
|
||||||
if store.has_open_channel_buffer(channel.id, cx) {
|
if store.has_open_channel_buffer(channel.id, cx) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||||
ChannelBufferOperation::JoinChannelNotes { channel_name } => {
|
ChannelBufferOperation::JoinChannelNotes { channel_name } => {
|
||||||
let buffer = client.channel_store().update(cx, |store, cx| {
|
let buffer = client.channel_store().update(cx, |store, cx| {
|
||||||
let channel_id = store
|
let channel_id = store
|
||||||
.channels()
|
.channel_dag_entries()
|
||||||
.find(|(_, c)| c.name == channel_name)
|
.find(|(_, c)| c.name == channel_name)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.1
|
.1
|
||||||
|
|
|
@ -288,6 +288,7 @@ impl TestServer {
|
||||||
pub async fn make_channel(
|
pub async fn make_channel(
|
||||||
&self,
|
&self,
|
||||||
channel: &str,
|
channel: &str,
|
||||||
|
parent: Option<u64>,
|
||||||
admin: (&TestClient, &mut TestAppContext),
|
admin: (&TestClient, &mut TestAppContext),
|
||||||
members: &mut [(&TestClient, &mut TestAppContext)],
|
members: &mut [(&TestClient, &mut TestAppContext)],
|
||||||
) -> u64 {
|
) -> u64 {
|
||||||
|
@ -296,7 +297,7 @@ impl TestServer {
|
||||||
.app_state
|
.app_state
|
||||||
.channel_store
|
.channel_store
|
||||||
.update(admin_cx, |channel_store, cx| {
|
.update(admin_cx, |channel_store, cx| {
|
||||||
channel_store.create_channel(channel, None, cx)
|
channel_store.create_channel(channel, parent, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -331,6 +332,39 @@ impl TestServer {
|
||||||
channel_id
|
channel_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn make_channel_tree(
|
||||||
|
&self,
|
||||||
|
channels: &[(&str, Option<&str>)],
|
||||||
|
creator: (&TestClient, &mut TestAppContext),
|
||||||
|
) -> Vec<u64> {
|
||||||
|
let mut observed_channels = HashMap::default();
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for (channel, parent) in channels {
|
||||||
|
let id;
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
if let Some(parent_id) = observed_channels.get(parent) {
|
||||||
|
id = self
|
||||||
|
.make_channel(channel, Some(*parent_id), (creator.0, creator.1), &mut [])
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
panic!(
|
||||||
|
"Edge {}->{} referenced before {} was created",
|
||||||
|
parent, channel, parent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = self
|
||||||
|
.make_channel(channel, None, (creator.0, creator.1), &mut [])
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
observed_channels.insert(channel, id);
|
||||||
|
result.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
|
pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
|
||||||
self.make_contacts(clients).await;
|
self.make_contacts(clients).await;
|
||||||
|
|
||||||
|
@ -549,6 +583,34 @@ impl TestClient {
|
||||||
) -> WindowHandle<Workspace> {
|
) -> WindowHandle<Workspace> {
|
||||||
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_admin_to_channel(
|
||||||
|
&self,
|
||||||
|
user: (&TestClient, &mut TestAppContext),
|
||||||
|
channel: u64,
|
||||||
|
cx_self: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let (other_client, other_cx) = user;
|
||||||
|
|
||||||
|
self.app_state
|
||||||
|
.channel_store
|
||||||
|
.update(cx_self, |channel_store, cx| {
|
||||||
|
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx_self.foreground().run_until_parked();
|
||||||
|
|
||||||
|
other_client
|
||||||
|
.app_state
|
||||||
|
.channel_store
|
||||||
|
.update(other_cx, |channels, _| {
|
||||||
|
channels.respond_to_channel_invite(channel, true)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TestClient {
|
impl Drop for TestClient {
|
||||||
|
|
|
@ -30,6 +30,7 @@ channel = { path = "../channel" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
|
drag_and_drop = { path = "../drag_and_drop" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
feedback = { path = "../feedback" }
|
feedback = { path = "../feedback" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
|
|
@ -166,8 +166,8 @@ impl ChatPanel {
|
||||||
let selected_channel_id = this
|
let selected_channel_id = this
|
||||||
.channel_store
|
.channel_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.channel_at_index(selected_ix)
|
.channel_at(selected_ix)
|
||||||
.map(|e| e.1.id);
|
.map(|e| e.id);
|
||||||
if let Some(selected_channel_id) = selected_channel_id {
|
if let Some(selected_channel_id) = selected_channel_id {
|
||||||
this.select_channel(selected_channel_id, cx)
|
this.select_channel(selected_channel_id, cx)
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
@ -391,7 +391,7 @@ impl ChatPanel {
|
||||||
(ItemType::Unselected, true) => &theme.channel_select.hovered_item,
|
(ItemType::Unselected, true) => &theme.channel_select.hovered_item,
|
||||||
};
|
};
|
||||||
|
|
||||||
let channel = &channel_store.read(cx).channel_at_index(ix).unwrap().1;
|
let channel = &channel_store.read(cx).channel_at(ix).unwrap();
|
||||||
let channel_id = channel.id;
|
let channel_id = channel.id;
|
||||||
|
|
||||||
let mut row = Flex::row()
|
let mut row = Flex::row()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ use collections::HashSet;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{Empty, MouseEventHandler, Overlay},
|
elements::{Empty, MouseEventHandler, Overlay},
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, Modifiers, MouseButton},
|
||||||
scene::{MouseDown, MouseDrag},
|
scene::{MouseDown, MouseDrag},
|
||||||
AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext,
|
AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext,
|
||||||
};
|
};
|
||||||
|
@ -21,12 +21,13 @@ enum State<V> {
|
||||||
region: RectF,
|
region: RectF,
|
||||||
},
|
},
|
||||||
Dragging {
|
Dragging {
|
||||||
|
modifiers: Modifiers,
|
||||||
window: AnyWindowHandle,
|
window: AnyWindowHandle,
|
||||||
position: Vector2F,
|
position: Vector2F,
|
||||||
region_offset: Vector2F,
|
region_offset: Vector2F,
|
||||||
region: RectF,
|
region: RectF,
|
||||||
payload: Rc<dyn Any + 'static>,
|
payload: Rc<dyn Any + 'static>,
|
||||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
|
render: Rc<dyn Fn(&Modifiers, Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
|
||||||
},
|
},
|
||||||
Canceled,
|
Canceled,
|
||||||
}
|
}
|
||||||
|
@ -49,6 +50,7 @@ impl<V> Clone for State<V> {
|
||||||
region,
|
region,
|
||||||
},
|
},
|
||||||
State::Dragging {
|
State::Dragging {
|
||||||
|
modifiers,
|
||||||
window,
|
window,
|
||||||
position,
|
position,
|
||||||
region_offset,
|
region_offset,
|
||||||
|
@ -62,6 +64,7 @@ impl<V> Clone for State<V> {
|
||||||
region: region.clone(),
|
region: region.clone(),
|
||||||
payload: payload.clone(),
|
payload: payload.clone(),
|
||||||
render: render.clone(),
|
render: render.clone(),
|
||||||
|
modifiers: modifiers.clone(),
|
||||||
},
|
},
|
||||||
State::Canceled => State::Canceled,
|
State::Canceled => State::Canceled,
|
||||||
}
|
}
|
||||||
|
@ -111,6 +114,27 @@ impl<V: 'static> DragAndDrop<V> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn any_currently_dragged(&self, window: AnyWindowHandle) -> bool {
|
||||||
|
self.currently_dragged
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| {
|
||||||
|
if let State::Dragging {
|
||||||
|
window: window_dragged_from,
|
||||||
|
..
|
||||||
|
} = state
|
||||||
|
{
|
||||||
|
if &window != window_dragged_from {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn drag_started(event: MouseDown, cx: &mut WindowContext) {
|
pub fn drag_started(event: MouseDown, cx: &mut WindowContext) {
|
||||||
cx.update_global(|this: &mut Self, _| {
|
cx.update_global(|this: &mut Self, _| {
|
||||||
this.currently_dragged = Some(State::Down {
|
this.currently_dragged = Some(State::Down {
|
||||||
|
@ -124,7 +148,7 @@ impl<V: 'static> DragAndDrop<V> {
|
||||||
event: MouseDrag,
|
event: MouseDrag,
|
||||||
payload: Rc<T>,
|
payload: Rc<T>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
|
render: Rc<impl 'static + Fn(&Modifiers, &T, &mut ViewContext<V>) -> AnyElement<V>>,
|
||||||
) {
|
) {
|
||||||
let window = cx.window();
|
let window = cx.window();
|
||||||
cx.update_global(|this: &mut Self, cx| {
|
cx.update_global(|this: &mut Self, cx| {
|
||||||
|
@ -141,13 +165,14 @@ impl<V: 'static> DragAndDrop<V> {
|
||||||
}) => {
|
}) => {
|
||||||
if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
|
if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
|
||||||
this.currently_dragged = Some(State::Dragging {
|
this.currently_dragged = Some(State::Dragging {
|
||||||
|
modifiers: event.modifiers,
|
||||||
window,
|
window,
|
||||||
region_offset,
|
region_offset,
|
||||||
region,
|
region,
|
||||||
position: event.position,
|
position: event.position,
|
||||||
payload,
|
payload,
|
||||||
render: Rc::new(move |payload, cx| {
|
render: Rc::new(move |modifiers, payload, cx| {
|
||||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
render(modifiers, payload.downcast_ref::<T>().unwrap(), cx)
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -160,16 +185,18 @@ impl<V: 'static> DragAndDrop<V> {
|
||||||
Some(&State::Dragging {
|
Some(&State::Dragging {
|
||||||
region_offset,
|
region_offset,
|
||||||
region,
|
region,
|
||||||
|
modifiers,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
this.currently_dragged = Some(State::Dragging {
|
this.currently_dragged = Some(State::Dragging {
|
||||||
|
modifiers,
|
||||||
window,
|
window,
|
||||||
region_offset,
|
region_offset,
|
||||||
region,
|
region,
|
||||||
position: event.position,
|
position: event.position,
|
||||||
payload,
|
payload,
|
||||||
render: Rc::new(move |payload, cx| {
|
render: Rc::new(move |modifiers, payload, cx| {
|
||||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
render(modifiers, payload.downcast_ref::<T>().unwrap(), cx)
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -178,6 +205,25 @@ impl<V: 'static> DragAndDrop<V> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_modifiers(new_modifiers: Modifiers, cx: &mut ViewContext<V>) -> bool {
|
||||||
|
let result = cx.update_global(|this: &mut Self, _| match &mut this.currently_dragged {
|
||||||
|
Some(state) => match state {
|
||||||
|
State::Dragging { modifiers, .. } => {
|
||||||
|
*modifiers = new_modifiers;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
None => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if result {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render(cx: &mut ViewContext<V>) -> Option<AnyElement<V>> {
|
pub fn render(cx: &mut ViewContext<V>) -> Option<AnyElement<V>> {
|
||||||
enum DraggedElementHandler {}
|
enum DraggedElementHandler {}
|
||||||
cx.global::<Self>()
|
cx.global::<Self>()
|
||||||
|
@ -188,6 +234,7 @@ impl<V: 'static> DragAndDrop<V> {
|
||||||
State::Down { .. } => None,
|
State::Down { .. } => None,
|
||||||
State::DeadZone { .. } => None,
|
State::DeadZone { .. } => None,
|
||||||
State::Dragging {
|
State::Dragging {
|
||||||
|
modifiers,
|
||||||
window,
|
window,
|
||||||
region_offset,
|
region_offset,
|
||||||
position,
|
position,
|
||||||
|
@ -205,7 +252,7 @@ impl<V: 'static> DragAndDrop<V> {
|
||||||
MouseEventHandler::new::<DraggedElementHandler, _>(
|
MouseEventHandler::new::<DraggedElementHandler, _>(
|
||||||
0,
|
0,
|
||||||
cx,
|
cx,
|
||||||
|_, cx| render(payload, cx),
|
|_, cx| render(&modifiers, payload, cx),
|
||||||
)
|
)
|
||||||
.with_cursor_style(CursorStyle::Arrow)
|
.with_cursor_style(CursorStyle::Arrow)
|
||||||
.on_up(MouseButton::Left, |_, _, cx| {
|
.on_up(MouseButton::Left, |_, _, cx| {
|
||||||
|
@ -295,7 +342,7 @@ pub trait Draggable<V> {
|
||||||
fn as_draggable<D: View, P: Any>(
|
fn as_draggable<D: View, P: Any>(
|
||||||
self,
|
self,
|
||||||
payload: P,
|
payload: P,
|
||||||
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
|
render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext<D>) -> AnyElement<D>,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
@ -305,7 +352,7 @@ impl<V: 'static> Draggable<V> for MouseEventHandler<V> {
|
||||||
fn as_draggable<D: View, P: Any>(
|
fn as_draggable<D: View, P: Any>(
|
||||||
self,
|
self,
|
||||||
payload: P,
|
payload: P,
|
||||||
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
|
render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext<D>) -> AnyElement<D>,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
|
|
|
@ -1513,7 +1513,7 @@ impl ProjectPanel {
|
||||||
.as_draggable(entry_id, {
|
.as_draggable(entry_id, {
|
||||||
let row_container_style = theme.dragged_entry.container;
|
let row_container_style = theme.dragged_entry.container;
|
||||||
|
|
||||||
move |_, cx: &mut ViewContext<Workspace>| {
|
move |_, _, cx: &mut ViewContext<Workspace>| {
|
||||||
let theme = theme::current(cx).clone();
|
let theme = theme::current(cx).clone();
|
||||||
Self::render_entry_visual_element(
|
Self::render_entry_visual_element(
|
||||||
&details,
|
&details,
|
||||||
|
|
|
@ -135,17 +135,18 @@ message Envelope {
|
||||||
RefreshInlayHints refresh_inlay_hints = 118;
|
RefreshInlayHints refresh_inlay_hints = 118;
|
||||||
|
|
||||||
CreateChannel create_channel = 119;
|
CreateChannel create_channel = 119;
|
||||||
ChannelResponse channel_response = 120;
|
CreateChannelResponse create_channel_response = 120;
|
||||||
InviteChannelMember invite_channel_member = 121;
|
InviteChannelMember invite_channel_member = 121;
|
||||||
RemoveChannelMember remove_channel_member = 122;
|
RemoveChannelMember remove_channel_member = 122;
|
||||||
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;
|
||||||
RenameChannel rename_channel = 130;
|
RenameChannel rename_channel = 130;
|
||||||
|
RenameChannelResponse rename_channel_response = 154;
|
||||||
|
|
||||||
JoinChannelBuffer join_channel_buffer = 131;
|
JoinChannelBuffer join_channel_buffer = 131;
|
||||||
JoinChannelBufferResponse join_channel_buffer_response = 132;
|
JoinChannelBufferResponse join_channel_buffer_response = 132;
|
||||||
|
@ -165,7 +166,11 @@ message Envelope {
|
||||||
ChannelMessageSent channel_message_sent = 147;
|
ChannelMessageSent channel_message_sent = 147;
|
||||||
GetChannelMessages get_channel_messages = 148;
|
GetChannelMessages get_channel_messages = 148;
|
||||||
GetChannelMessagesResponse get_channel_messages_response = 149;
|
GetChannelMessagesResponse get_channel_messages_response = 149;
|
||||||
RemoveChannelMessage remove_channel_message = 150; // Current max
|
RemoveChannelMessage remove_channel_message = 150;
|
||||||
|
|
||||||
|
LinkChannel link_channel = 151;
|
||||||
|
UnlinkChannel unlink_channel = 152;
|
||||||
|
MoveChannel move_channel = 153; // Current max: 154
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -955,11 +960,18 @@ message LspDiskBasedDiagnosticsUpdated {}
|
||||||
|
|
||||||
message UpdateChannels {
|
message UpdateChannels {
|
||||||
repeated Channel channels = 1;
|
repeated Channel channels = 1;
|
||||||
repeated uint64 remove_channels = 2;
|
repeated ChannelEdge insert_edge = 2;
|
||||||
repeated Channel channel_invitations = 3;
|
repeated ChannelEdge delete_edge = 3;
|
||||||
repeated uint64 remove_channel_invitations = 4;
|
repeated uint64 delete_channels = 4;
|
||||||
repeated ChannelParticipants channel_participants = 5;
|
repeated Channel channel_invitations = 5;
|
||||||
repeated ChannelPermission channel_permissions = 6;
|
repeated uint64 remove_channel_invitations = 6;
|
||||||
|
repeated ChannelParticipants channel_participants = 7;
|
||||||
|
repeated ChannelPermission channel_permissions = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChannelEdge {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
uint64 parent_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChannelPermission {
|
message ChannelPermission {
|
||||||
|
@ -976,7 +988,7 @@ message JoinChannel {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoveChannel {
|
message DeleteChannel {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1005,8 +1017,9 @@ message CreateChannel {
|
||||||
optional uint64 parent_id = 2;
|
optional uint64 parent_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChannelResponse {
|
message CreateChannelResponse {
|
||||||
Channel channel = 1;
|
Channel channel = 1;
|
||||||
|
optional uint64 parent_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InviteChannelMember {
|
message InviteChannelMember {
|
||||||
|
@ -1031,6 +1044,10 @@ message RenameChannel {
|
||||||
string name = 2;
|
string name = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RenameChannelResponse {
|
||||||
|
Channel channel = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message JoinChannelChat {
|
message JoinChannelChat {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
}
|
}
|
||||||
|
@ -1074,6 +1091,22 @@ message GetChannelMessagesResponse {
|
||||||
bool done = 2;
|
bool done = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LinkChannel {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
uint64 to = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnlinkChannel {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
uint64 from = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MoveChannel {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
uint64 from = 2;
|
||||||
|
uint64 to = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message JoinChannelBuffer {
|
message JoinChannelBuffer {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
}
|
}
|
||||||
|
@ -1486,7 +1519,6 @@ message Nonce {
|
||||||
message Channel {
|
message Channel {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
optional uint64 parent_id = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Contact {
|
message Contact {
|
||||||
|
|
|
@ -146,7 +146,7 @@ messages!(
|
||||||
(CopyProjectEntry, Foreground),
|
(CopyProjectEntry, Foreground),
|
||||||
(CreateBufferForPeer, Foreground),
|
(CreateBufferForPeer, Foreground),
|
||||||
(CreateChannel, Foreground),
|
(CreateChannel, Foreground),
|
||||||
(ChannelResponse, Foreground),
|
(CreateChannelResponse, Foreground),
|
||||||
(ChannelMessageSent, Foreground),
|
(ChannelMessageSent, Foreground),
|
||||||
(CreateProjectEntry, Foreground),
|
(CreateProjectEntry, Foreground),
|
||||||
(CreateRoom, Foreground),
|
(CreateRoom, Foreground),
|
||||||
|
@ -229,6 +229,7 @@ messages!(
|
||||||
(RoomUpdated, Foreground),
|
(RoomUpdated, Foreground),
|
||||||
(SaveBuffer, Foreground),
|
(SaveBuffer, Foreground),
|
||||||
(RenameChannel, Foreground),
|
(RenameChannel, Foreground),
|
||||||
|
(RenameChannelResponse, Foreground),
|
||||||
(SetChannelMemberAdmin, Foreground),
|
(SetChannelMemberAdmin, Foreground),
|
||||||
(SearchProject, Background),
|
(SearchProject, Background),
|
||||||
(SearchProjectResponse, Background),
|
(SearchProjectResponse, Background),
|
||||||
|
@ -246,7 +247,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),
|
||||||
|
@ -282,7 +286,7 @@ request_messages!(
|
||||||
(CopyProjectEntry, ProjectEntryResponse),
|
(CopyProjectEntry, ProjectEntryResponse),
|
||||||
(CreateProjectEntry, ProjectEntryResponse),
|
(CreateProjectEntry, ProjectEntryResponse),
|
||||||
(CreateRoom, CreateRoomResponse),
|
(CreateRoom, CreateRoomResponse),
|
||||||
(CreateChannel, ChannelResponse),
|
(CreateChannel, CreateChannelResponse),
|
||||||
(DeclineCall, Ack),
|
(DeclineCall, Ack),
|
||||||
(DeleteProjectEntry, ProjectEntryResponse),
|
(DeleteProjectEntry, ProjectEntryResponse),
|
||||||
(ExpandProjectEntry, ExpandProjectEntryResponse),
|
(ExpandProjectEntry, ExpandProjectEntryResponse),
|
||||||
|
@ -327,10 +331,13 @@ request_messages!(
|
||||||
(GetChannelMessages, GetChannelMessagesResponse),
|
(GetChannelMessages, GetChannelMessagesResponse),
|
||||||
(GetChannelMembers, GetChannelMembersResponse),
|
(GetChannelMembers, GetChannelMembersResponse),
|
||||||
(JoinChannel, JoinRoomResponse),
|
(JoinChannel, JoinRoomResponse),
|
||||||
(RemoveChannel, Ack),
|
|
||||||
(RemoveChannelMessage, Ack),
|
(RemoveChannelMessage, Ack),
|
||||||
|
(DeleteChannel, Ack),
|
||||||
(RenameProjectEntry, ProjectEntryResponse),
|
(RenameProjectEntry, ProjectEntryResponse),
|
||||||
(RenameChannel, ChannelResponse),
|
(RenameChannel, RenameChannelResponse),
|
||||||
|
(LinkChannel, Ack),
|
||||||
|
(UnlinkChannel, Ack),
|
||||||
|
(MoveChannel, Ack),
|
||||||
(SaveBuffer, BufferSaved),
|
(SaveBuffer, BufferSaved),
|
||||||
(SearchProject, SearchProjectResponse),
|
(SearchProject, SearchProjectResponse),
|
||||||
(ShareProject, ShareProjectResponse),
|
(ShareProject, ShareProjectResponse),
|
||||||
|
|
|
@ -1383,7 +1383,7 @@ impl Pane {
|
||||||
let theme = theme::current(cx).clone();
|
let theme = theme::current(cx).clone();
|
||||||
|
|
||||||
let detail = detail.clone();
|
let detail = detail.clone();
|
||||||
move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
|
move |_, dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
|
||||||
let tab_style = &theme.workspace.tab_bar.dragged_tab;
|
let tab_style = &theme.workspace.tab_bar.dragged_tab;
|
||||||
Self::render_dragged_tab(
|
Self::render_dragged_tab(
|
||||||
&dragged_item.handle,
|
&dragged_item.handle,
|
||||||
|
|
|
@ -33,8 +33,8 @@ use gpui::{
|
||||||
},
|
},
|
||||||
impl_actions,
|
impl_actions,
|
||||||
platform::{
|
platform::{
|
||||||
CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
|
CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
|
||||||
WindowOptions,
|
WindowBounds, WindowOptions,
|
||||||
},
|
},
|
||||||
AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
|
AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
|
||||||
ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
|
ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
|
||||||
|
@ -3815,6 +3815,10 @@ impl View for Workspace {
|
||||||
cx.focus(&self.active_pane);
|
cx.focus(&self.active_pane);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
DragAndDrop::<Workspace>::update_modifiers(e.modifiers, cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewId {
|
impl ViewId {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue