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_derive",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
|
@ -1495,6 +1496,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"settings",
|
||||
"sha-1 0.9.8",
|
||||
"smallvec",
|
||||
"sqlx",
|
||||
"text",
|
||||
"theme",
|
||||
|
@ -1525,6 +1527,7 @@ dependencies = [
|
|||
"collections",
|
||||
"context_menu",
|
||||
"db",
|
||||
"drag_and_drop",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"feedback",
|
||||
|
|
|
@ -585,6 +585,14 @@
|
|||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel > Editor",
|
||||
"bindings": {
|
||||
"cmd-c": "collab_panel::StartLinkChannel",
|
||||
"cmd-x": "collab_panel::StartMoveChannel",
|
||||
"cmd-v": "collab_panel::MoveOrLinkToSelected"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal",
|
||||
"bindings": {
|
||||
|
|
|
@ -28,6 +28,7 @@ anyhow.workspace = true
|
|||
futures.workspace = true
|
||||
image = "0.23"
|
||||
lazy_static.workspace = true
|
||||
smallvec.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
|
|
|
@ -4,7 +4,9 @@ mod channel_store;
|
|||
|
||||
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent};
|
||||
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 std::sync::Arc;
|
||||
|
|
|
@ -1,20 +1,37 @@
|
|||
mod channel_index;
|
||||
|
||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
|
||||
use anyhow::{anyhow, Result};
|
||||
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 gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||
use rpc::{proto, TypedEnvelope};
|
||||
use std::{mem, sync::Arc, time::Duration};
|
||||
use rpc::{
|
||||
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 self::channel_index::ChannelIndex;
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub type ChannelId = u64;
|
||||
|
||||
pub struct ChannelStore {
|
||||
channels_by_id: HashMap<ChannelId, Arc<Channel>>,
|
||||
channel_paths: Vec<Vec<ChannelId>>,
|
||||
channel_index: ChannelIndex,
|
||||
channel_invitations: Vec<Arc<Channel>>,
|
||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||
channels_with_admin_privileges: HashSet<ChannelId>,
|
||||
|
@ -30,12 +47,17 @@ pub struct ChannelStore {
|
|||
_update_channels: Task<()>,
|
||||
}
|
||||
|
||||
pub type ChannelData = (Channel, ChannelPath);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct ChannelPath(Arc<[ChannelId]>);
|
||||
|
||||
pub struct ChannelMembership {
|
||||
pub user: Arc<User>,
|
||||
pub kind: proto::channel_member::Kind,
|
||||
|
@ -82,9 +104,8 @@ impl ChannelStore {
|
|||
});
|
||||
|
||||
Self {
|
||||
channels_by_id: HashMap::default(),
|
||||
channel_invitations: Vec::default(),
|
||||
channel_paths: Vec::default(),
|
||||
channel_index: ChannelIndex::default(),
|
||||
channel_participants: Default::default(),
|
||||
channels_with_admin_privileges: Default::default(),
|
||||
outgoing_invites: Default::default(),
|
||||
|
@ -116,7 +137,7 @@ impl ChannelStore {
|
|||
}
|
||||
|
||||
pub fn has_children(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_paths.iter().any(|path| {
|
||||
self.channel_index.iter().any(|path| {
|
||||
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||
path.len() > ix + 1
|
||||
} else {
|
||||
|
@ -125,29 +146,43 @@ impl ChannelStore {
|
|||
})
|
||||
}
|
||||
|
||||
/// Returns the number of unique channels in the store
|
||||
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> {
|
||||
self.channel_paths
|
||||
.iter()
|
||||
.position(|path| path.ends_with(&[channel_id]))
|
||||
self.channel_index
|
||||
.by_id()
|
||||
.keys()
|
||||
.position(|id| *id == channel_id)
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
|
||||
self.channel_paths.iter().map(move |path| {
|
||||
/// Returns an iterator over all unique channels
|
||||
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 channel = self.channel_for_id(*id).unwrap();
|
||||
(path.len() - 1, channel)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc<Channel>)> {
|
||||
let path = self.channel_paths.get(ix)?;
|
||||
pub fn channel_dag_entry_at(&self, ix: usize) -> Option<(&Arc<Channel>, &ChannelPath)> {
|
||||
let path = self.channel_index.get(ix)?;
|
||||
let id = path.last().unwrap();
|
||||
let channel = self.channel_for_id(*id).unwrap();
|
||||
Some((path.len() - 1, channel))
|
||||
|
||||
Some((channel, path))
|
||||
}
|
||||
|
||||
pub fn channel_at(&self, ix: usize) -> Option<&Arc<Channel>> {
|
||||
self.channel_index.by_id().values().nth(ix)
|
||||
}
|
||||
|
||||
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>> {
|
||||
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 {
|
||||
|
@ -268,7 +303,7 @@ impl ChannelStore {
|
|||
}
|
||||
|
||||
pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_paths.iter().any(|path| {
|
||||
self.channel_index.iter().any(|path| {
|
||||
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||
path[..=ix]
|
||||
.iter()
|
||||
|
@ -294,18 +329,33 @@ impl ChannelStore {
|
|||
let client = self.client.clone();
|
||||
let name = name.trim_start_matches("#").to_owned();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let channel = client
|
||||
let response = client
|
||||
.request(proto::CreateChannel { name, parent_id })
|
||||
.await?
|
||||
.await?;
|
||||
|
||||
let channel = response
|
||||
.channel
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
|
||||
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| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
insert_edge: parent_edge,
|
||||
channel_permissions: vec![ChannelPermission {
|
||||
channel_id,
|
||||
is_admin: true,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
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(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -502,7 +605,7 @@ impl ChannelStore {
|
|||
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
client.request(proto::RemoveChannel { channel_id }).await?;
|
||||
client.request(proto::DeleteChannel { channel_id }).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -639,11 +742,11 @@ impl ChannelStore {
|
|||
}
|
||||
|
||||
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.channels_by_id.clear();
|
||||
self.channel_index.clear();
|
||||
self.channel_invitations.clear();
|
||||
self.channel_participants.clear();
|
||||
self.channels_with_admin_privileges.clear();
|
||||
self.channel_paths.clear();
|
||||
self.channel_index.clear();
|
||||
self.outgoing_invites.clear();
|
||||
cx.notify();
|
||||
|
||||
|
@ -690,17 +793,20 @@ impl ChannelStore {
|
|||
}
|
||||
}
|
||||
|
||||
let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
|
||||
if channels_changed {
|
||||
if !payload.remove_channels.is_empty() {
|
||||
self.channels_by_id
|
||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
||||
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));
|
||||
let channels_changed = !payload.channels.is_empty()
|
||||
|| !payload.delete_channels.is_empty()
|
||||
|| !payload.insert_edge.is_empty()
|
||||
|| !payload.delete_edge.is_empty();
|
||||
|
||||
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;
|
||||
if let Some(OpenedModelHandle::Open(buffer)) =
|
||||
self.opened_buffers.remove(&channel_id)
|
||||
|
@ -712,44 +818,18 @@ impl ChannelStore {
|
|||
}
|
||||
}
|
||||
|
||||
for channel_proto in payload.channels {
|
||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||
Arc::make_mut(existing_channel).name = channel_proto.name;
|
||||
} else {
|
||||
let channel = Arc::new(Channel {
|
||||
id: channel_proto.id,
|
||||
name: channel_proto.name,
|
||||
});
|
||||
self.channels_by_id.insert(channel.id, channel.clone());
|
||||
|
||||
if let Some(parent_id) = channel_proto.parent_id {
|
||||
let mut ix = 0;
|
||||
while ix < self.channel_paths.len() {
|
||||
let path = &self.channel_paths[ix];
|
||||
if path.ends_with(&[parent_id]) {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(channel.id);
|
||||
self.channel_paths.insert(ix + 1, new_path);
|
||||
ix += 1;
|
||||
}
|
||||
ix += 1;
|
||||
}
|
||||
} else {
|
||||
self.channel_paths.push(vec![channel.id]);
|
||||
}
|
||||
}
|
||||
let mut index = self.channel_index.bulk_insert();
|
||||
for channel in payload.channels {
|
||||
index.insert(channel)
|
||||
}
|
||||
|
||||
self.channel_paths.sort_by(|a, b| {
|
||||
let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
|
||||
let b = Self::channel_path_sorting_key(b, &self.channels_by_id);
|
||||
a.cmp(b)
|
||||
});
|
||||
self.channel_paths.dedup();
|
||||
self.channel_paths.retain(|path| {
|
||||
path.iter()
|
||||
.all(|channel_id| self.channels_by_id.contains_key(channel_id))
|
||||
});
|
||||
for edge in payload.insert_edge {
|
||||
index.insert_edge(edge.channel_id, edge.parent_id);
|
||||
}
|
||||
|
||||
for edge in payload.delete_edge {
|
||||
index.delete_edge(edge.parent_id, edge.channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
for permission in payload.channel_permissions {
|
||||
|
@ -807,12 +887,51 @@ impl ChannelStore {
|
|||
anyhow::Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_path_sorting_key<'a>(
|
||||
path: &'a [ChannelId],
|
||||
channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>,
|
||||
) -> impl 'a + Iterator<Item = Option<&'a str>> {
|
||||
path.iter()
|
||||
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
||||
impl Deref for ChannelPath {
|
||||
type Target = [ChannelId];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "a".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
|
@ -51,12 +49,20 @@ fn test_update_channels(cx: &mut AppContext) {
|
|||
proto::Channel {
|
||||
id: 3,
|
||||
name: "x".to_string(),
|
||||
parent_id: Some(1),
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
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()
|
||||
|
@ -86,17 +92,24 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
|||
proto::Channel {
|
||||
id: 0,
|
||||
name: "a".to_string(),
|
||||
parent_id: None,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
parent_id: Some(0),
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
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 {
|
||||
|
@ -122,7 +135,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
|||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
remove_channels: vec![1, 2],
|
||||
delete_channels: vec![1, 2],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
|
@ -145,7 +158,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
|||
channels: vec![proto::Channel {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string(),
|
||||
parent_id: None,
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
@ -169,7 +181,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
|||
|
||||
// Join a channel and populate its existing messages.
|
||||
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)
|
||||
});
|
||||
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
|
||||
|
@ -351,7 +363,7 @@ fn assert_channels(
|
|||
) {
|
||||
let actual = channel_store.read_with(cx, |store, _| {
|
||||
store
|
||||
.channels()
|
||||
.channel_dag_entries()
|
||||
.map(|(depth, channel)| {
|
||||
(
|
||||
depth,
|
||||
|
|
|
@ -41,6 +41,7 @@ prost.workspace = true
|
|||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||
scrypt = "0.7"
|
||||
smallvec.workspace = true
|
||||
# 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-query = "0.27"
|
||||
|
@ -72,7 +73,6 @@ fs = { path = "../fs", features = ["test-support"] }
|
|||
git = { path = "../git", features = ["test-support"] }
|
||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
|
@ -81,6 +81,7 @@ workspace = { path = "../workspace", features = ["test-support"] }
|
|||
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||
|
||||
async-trait.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
indoc.workspace = true
|
||||
|
|
|
@ -14,7 +14,10 @@ use collections::{BTreeMap, HashMap, HashSet};
|
|||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use rand::{prelude::StdRng, Rng, SeedableRng};
|
||||
use rpc::{proto, ConnectionId};
|
||||
use rpc::{
|
||||
proto::{self},
|
||||
ConnectionId,
|
||||
};
|
||||
use sea_orm::{
|
||||
entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection,
|
||||
DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType,
|
||||
|
@ -43,6 +46,8 @@ pub use ids::*;
|
|||
pub use sea_orm::ConnectOptions;
|
||||
pub use tables::user::Model as User;
|
||||
|
||||
use self::queries::channels::ChannelGraph;
|
||||
|
||||
pub struct Database {
|
||||
options: ConnectOptions,
|
||||
pool: DatabaseConnection,
|
||||
|
@ -421,16 +426,15 @@ pub struct NewUserResult {
|
|||
pub signup_device_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromQueryResult, Debug, PartialEq)]
|
||||
#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub parent_id: Option<ChannelId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ChannelsForUser {
|
||||
pub channels: Vec<Channel>,
|
||||
pub channels: ChannelGraph,
|
||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||
pub channels_with_admin_privileges: HashSet<ChannelId>,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
use rpc::proto::ChannelEdge;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::*;
|
||||
|
||||
type ChannelDescendants = HashMap<ChannelId, SmallSet<ChannelId>>;
|
||||
|
||||
impl Database {
|
||||
#[cfg(test)]
|
||||
pub async fn all_channels(&self) -> Result<Vec<(ChannelId, String)>> {
|
||||
|
@ -46,7 +51,6 @@ impl Database {
|
|||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let channel_paths_stmt;
|
||||
if let Some(parent) = parent {
|
||||
let sql = r#"
|
||||
INSERT INTO channel_paths
|
||||
|
@ -58,7 +62,7 @@ impl Database {
|
|||
WHERE
|
||||
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(),
|
||||
sql,
|
||||
[
|
||||
|
@ -100,7 +104,7 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_channel(
|
||||
pub async fn delete_channel(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
|
@ -149,6 +153,19 @@ impl Database {
|
|||
.exec(&*tx)
|
||||
.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))
|
||||
})
|
||||
.await
|
||||
|
@ -310,7 +327,6 @@ impl Database {
|
|||
.map(|channel| Channel {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
parent_id: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -319,6 +335,49 @@ impl Database {
|
|||
.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> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = tx;
|
||||
|
@ -332,63 +391,82 @@ impl Database {
|
|||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
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 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,
|
||||
})
|
||||
self.get_user_channels(channel_memberships, &tx).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>> {
|
||||
self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await })
|
||||
.await
|
||||
|
@ -559,6 +637,7 @@ impl Database {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the channel ancestors, deepest first
|
||||
pub async fn get_channel_ancestors(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -566,6 +645,7 @@ impl Database {
|
|||
) -> Result<Vec<ChannelId>> {
|
||||
let paths = channel_path::Entity::find()
|
||||
.filter(channel_path::Column::ChannelId.eq(channel_id))
|
||||
.order_by(channel_path::Column::IdPath, sea_query::Order::Desc)
|
||||
.all(tx)
|
||||
.await?;
|
||||
let mut channel_ids = Vec::new();
|
||||
|
@ -582,11 +662,25 @@ impl Database {
|
|||
Ok(channel_ids)
|
||||
}
|
||||
|
||||
/// Returns the channel descendants,
|
||||
/// Structured as a map from child ids to their parent ids
|
||||
/// For example, the descendants of 'a' in this DAG:
|
||||
///
|
||||
/// /- b -\
|
||||
/// a -- c -- d
|
||||
///
|
||||
/// would be:
|
||||
/// {
|
||||
/// a: [],
|
||||
/// b: [a],
|
||||
/// c: [a],
|
||||
/// d: [a, c],
|
||||
/// }
|
||||
async fn get_channel_descendants(
|
||||
&self,
|
||||
channel_ids: impl IntoIterator<Item = ChannelId>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<HashMap<ChannelId, Option<ChannelId>>> {
|
||||
) -> Result<ChannelDescendants> {
|
||||
let mut values = String::new();
|
||||
for id in channel_ids {
|
||||
if !values.is_empty() {
|
||||
|
@ -613,7 +707,7 @@ impl Database {
|
|||
|
||||
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
|
||||
|
||||
let mut parents_by_child_id = HashMap::default();
|
||||
let mut parents_by_child_id: ChannelDescendants = HashMap::default();
|
||||
let mut paths = channel_path::Entity::find()
|
||||
.from_raw_sql(stmt)
|
||||
.stream(tx)
|
||||
|
@ -632,7 +726,10 @@ impl Database {
|
|||
parent_id = Some(id);
|
||||
}
|
||||
}
|
||||
parents_by_child_id.insert(path.channel_id, parent_id);
|
||||
let entry = parents_by_child_id.entry(path.channel_id).or_default();
|
||||
if let Some(parent_id) = parent_id {
|
||||
entry.insert(parent_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(parents_by_child_id)
|
||||
|
@ -677,7 +774,6 @@ impl Database {
|
|||
Channel {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
parent_id: None,
|
||||
},
|
||||
is_accepted,
|
||||
)))
|
||||
|
@ -703,9 +799,276 @@ impl Database {
|
|||
})
|
||||
.await
|
||||
}
|
||||
|
||||
// Insert an edge from the given channel to the given other channel.
|
||||
pub async fn link_channel(
|
||||
&self,
|
||||
user: UserId,
|
||||
channel: ChannelId,
|
||||
to: ChannelId,
|
||||
) -> Result<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)]
|
||||
enum QueryUserIds {
|
||||
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 channel_tests;
|
||||
mod db_tests;
|
||||
mod feature_flag_tests;
|
||||
mod message_tests;
|
||||
|
@ -6,6 +7,7 @@ mod message_tests;
|
|||
use super::*;
|
||||
use gpui::executor::Background;
|
||||
use parking_lot::Mutex;
|
||||
use rpc::proto::ChannelEdge;
|
||||
use sea_orm::ConnectionTrait;
|
||||
use sqlx::migrate::MigrateDatabase;
|
||||
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> {
|
||||
Deterministic::new(0).build_background()
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ use lazy_static::lazy_static;
|
|||
use prometheus::{register_int_gauge, IntGauge};
|
||||
use rpc::{
|
||||
proto::{
|
||||
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage,
|
||||
LiveKitConnectionInfo, RequestMessage,
|
||||
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage,
|
||||
EnvelopedMessage, LiveKitConnectionInfo, RequestMessage,
|
||||
},
|
||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||
};
|
||||
|
@ -250,7 +250,7 @@ impl Server {
|
|||
.add_request_handler(remove_contact)
|
||||
.add_request_handler(respond_to_contact_request)
|
||||
.add_request_handler(create_channel)
|
||||
.add_request_handler(remove_channel)
|
||||
.add_request_handler(delete_channel)
|
||||
.add_request_handler(invite_channel_member)
|
||||
.add_request_handler(remove_channel_member)
|
||||
.add_request_handler(set_channel_member_admin)
|
||||
|
@ -267,6 +267,9 @@ impl Server {
|
|||
.add_request_handler(send_channel_message)
|
||||
.add_request_handler(remove_channel_message)
|
||||
.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_message_handler(unfollow)
|
||||
.add_message_handler(update_followers)
|
||||
|
@ -2197,56 +2200,58 @@ async fn create_channel(
|
|||
let channel = proto::Channel {
|
||||
id: id.to_proto(),
|
||||
name: request.name,
|
||||
parent_id: request.parent_id,
|
||||
};
|
||||
|
||||
response.send(proto::ChannelResponse {
|
||||
response.send(proto::CreateChannelResponse {
|
||||
channel: Some(channel.clone()),
|
||||
parent_id: request.parent_id,
|
||||
})?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channels.push(channel);
|
||||
|
||||
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 Some(parent_id) = parent_id else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
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;
|
||||
for user_id in user_ids_to_notify {
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
let mut update = update.clone();
|
||||
if user_id == session.user_id {
|
||||
update.channel_permissions.push(proto::ChannelPermission {
|
||||
channel_id: id.to_proto(),
|
||||
is_admin: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
session.peer.send(connection_id, update)?;
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_channel(
|
||||
request: proto::RemoveChannel,
|
||||
response: Response<proto::RemoveChannel>,
|
||||
async fn delete_channel(
|
||||
request: proto::DeleteChannel,
|
||||
response: Response<proto::DeleteChannel>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
|
||||
let channel_id = request.channel_id;
|
||||
let (removed_channels, member_ids) = db
|
||||
.remove_channel(ChannelId::from_proto(channel_id), session.user_id)
|
||||
.delete_channel(ChannelId::from_proto(channel_id), session.user_id)
|
||||
.await?;
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
// Notify members of removed channels
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channels
|
||||
.delete_channels
|
||||
.extend(removed_channels.into_iter().map(|id| id.to_proto()));
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
|
@ -2279,7 +2284,6 @@ async fn invite_channel_member(
|
|||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
parent_id: None,
|
||||
});
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
|
@ -2306,7 +2310,7 @@ async fn remove_channel_member(
|
|||
.await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.remove_channels.push(channel_id.to_proto());
|
||||
update.delete_channels.push(channel_id.to_proto());
|
||||
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
|
@ -2370,9 +2374,8 @@ async fn rename_channel(
|
|||
let channel = proto::Channel {
|
||||
id: request.channel_id,
|
||||
name: new_name,
|
||||
parent_id: None,
|
||||
};
|
||||
response.send(proto::ChannelResponse {
|
||||
response.send(proto::RenameChannelResponse {
|
||||
channel: Some(channel.clone()),
|
||||
})?;
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
|
@ -2390,6 +2393,127 @@ async fn rename_channel(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn link_channel(
|
||||
request: proto::LinkChannel,
|
||||
response: Response<proto::LinkChannel>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let to = ChannelId::from_proto(request.to);
|
||||
let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?;
|
||||
|
||||
let members = db.get_channel_members(to).await?;
|
||||
let connection_pool = session.connection_pool().await;
|
||||
let update = proto::UpdateChannels {
|
||||
channels: channels_to_send
|
||||
.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(
|
||||
request: proto::GetChannelMembers,
|
||||
response: Response<proto::GetChannelMembers>,
|
||||
|
@ -2419,14 +2543,20 @@ async fn respond_to_channel_invite(
|
|||
.remove_channel_invitations
|
||||
.push(channel_id.to_proto());
|
||||
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
|
||||
.channels
|
||||
.extend(result.channels.into_iter().map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
parent_id: channel.parent_id.map(ChannelId::to_proto),
|
||||
}));
|
||||
.extend(
|
||||
result
|
||||
.channels
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
update.insert_edge = result.channels.edges;
|
||||
update
|
||||
.channel_participants
|
||||
.extend(
|
||||
|
@ -2844,14 +2974,15 @@ fn build_initial_channels_update(
|
|||
) -> proto::UpdateChannels {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
|
||||
for channel in channels.channels {
|
||||
for channel in channels.channels.channels {
|
||||
update.channels.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
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 {
|
||||
update
|
||||
.channel_participants
|
||||
|
@ -2877,7 +3008,6 @@ fn build_initial_channels_update(
|
|||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
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 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;
|
||||
|
||||
// Client A joins the channel buffer
|
||||
|
@ -135,6 +135,7 @@ async fn test_channel_buffer_replica_ids(
|
|||
let channel_id = server
|
||||
.make_channel(
|
||||
"the-channel",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&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 channel_id = server
|
||||
.make_channel("the-channel", (&client_a, cx_a), &mut [])
|
||||
.make_channel("the-channel", None, (&client_a, cx_a), &mut [])
|
||||
.await;
|
||||
|
||||
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 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;
|
||||
|
||||
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 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;
|
||||
|
||||
let channel_buffer_a = client_a
|
||||
|
@ -491,6 +502,7 @@ async fn test_channel_buffers_and_server_restarts(
|
|||
let channel_id = server
|
||||
.make_channel(
|
||||
"the-channel",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&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 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;
|
||||
|
||||
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 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;
|
||||
|
||||
let channel_chat_a = client_a
|
||||
|
@ -139,6 +149,7 @@ async fn test_remove_channel_message(
|
|||
let channel_id = server
|
||||
.make_channel(
|
||||
"the-channel",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&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, _| {
|
||||
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.
|
||||
|
@ -142,6 +145,8 @@ async fn test_core_channels(
|
|||
],
|
||||
);
|
||||
|
||||
println!("STARTING CREATE CHANNEL C");
|
||||
|
||||
let channel_c_id = client_a
|
||||
.channel_store()
|
||||
.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 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;
|
||||
|
||||
let sub_id = client_a
|
||||
|
@ -361,6 +366,7 @@ async fn test_channel_room(
|
|||
let zed_id = server
|
||||
.make_channel(
|
||||
"zed",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&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 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
|
||||
.make_channel("rust", (&client_a, cx_a), &mut [])
|
||||
.make_channel("rust", None, (&client_a, cx_a), &mut [])
|
||||
.await;
|
||||
|
||||
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 rust_id = server
|
||||
.make_channel("rust", (&client_a, cx_a), &mut [])
|
||||
.make_channel("rust", None, (&client_a, cx_a), &mut [])
|
||||
.await;
|
||||
|
||||
client_a
|
||||
|
@ -658,7 +666,7 @@ async fn test_channel_rename(
|
|||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
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;
|
||||
|
||||
// Rename the channel
|
||||
|
@ -716,6 +724,7 @@ async fn test_call_from_channel(
|
|||
let channel_id = server
|
||||
.make_channel(
|
||||
"x",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&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)])
|
||||
.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
|
||||
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)]
|
||||
struct ExpectedChannel {
|
||||
depth: usize,
|
||||
|
@ -911,7 +1173,7 @@ fn assert_channels(
|
|||
) {
|
||||
let actual = channel_store.read_with(cx, |store, _| {
|
||||
store
|
||||
.channels()
|
||||
.channel_dag_entries()
|
||||
.map(|(depth, channel)| ExpectedChannel {
|
||||
depth,
|
||||
name: channel.name.clone(),
|
||||
|
@ -920,5 +1182,22 @@ fn assert_channels(
|
|||
})
|
||||
.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) {
|
||||
0..=29 => {
|
||||
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) {
|
||||
None
|
||||
} else {
|
||||
|
@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
|||
ChannelBufferOperation::JoinChannelNotes { channel_name } => {
|
||||
let buffer = client.channel_store().update(cx, |store, cx| {
|
||||
let channel_id = store
|
||||
.channels()
|
||||
.channel_dag_entries()
|
||||
.find(|(_, c)| c.name == channel_name)
|
||||
.unwrap()
|
||||
.1
|
||||
|
|
|
@ -288,6 +288,7 @@ impl TestServer {
|
|||
pub async fn make_channel(
|
||||
&self,
|
||||
channel: &str,
|
||||
parent: Option<u64>,
|
||||
admin: (&TestClient, &mut TestAppContext),
|
||||
members: &mut [(&TestClient, &mut TestAppContext)],
|
||||
) -> u64 {
|
||||
|
@ -296,7 +297,7 @@ impl TestServer {
|
|||
.app_state
|
||||
.channel_store
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.create_channel(channel, None, cx)
|
||||
channel_store.create_channel(channel, parent, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -331,6 +332,39 @@ impl TestServer {
|
|||
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)]) {
|
||||
self.make_contacts(clients).await;
|
||||
|
||||
|
@ -549,6 +583,34 @@ impl TestClient {
|
|||
) -> WindowHandle<Workspace> {
|
||||
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 {
|
||||
|
|
|
@ -30,6 +30,7 @@ channel = { path = "../channel" }
|
|||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { path = "../editor" }
|
||||
feedback = { path = "../feedback" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
|
|
|
@ -166,8 +166,8 @@ impl ChatPanel {
|
|||
let selected_channel_id = this
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_at_index(selected_ix)
|
||||
.map(|e| e.1.id);
|
||||
.channel_at(selected_ix)
|
||||
.map(|e| e.id);
|
||||
if let Some(selected_channel_id) = selected_channel_id {
|
||||
this.select_channel(selected_channel_id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
|
@ -391,7 +391,7 @@ impl ChatPanel {
|
|||
(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 mut row = Flex::row()
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ use collections::HashSet;
|
|||
use gpui::{
|
||||
elements::{Empty, MouseEventHandler, Overlay},
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
platform::{CursorStyle, Modifiers, MouseButton},
|
||||
scene::{MouseDown, MouseDrag},
|
||||
AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext,
|
||||
};
|
||||
|
@ -21,12 +21,13 @@ enum State<V> {
|
|||
region: RectF,
|
||||
},
|
||||
Dragging {
|
||||
modifiers: Modifiers,
|
||||
window: AnyWindowHandle,
|
||||
position: Vector2F,
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
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,
|
||||
}
|
||||
|
@ -49,6 +50,7 @@ impl<V> Clone for State<V> {
|
|||
region,
|
||||
},
|
||||
State::Dragging {
|
||||
modifiers,
|
||||
window,
|
||||
position,
|
||||
region_offset,
|
||||
|
@ -62,6 +64,7 @@ impl<V> Clone for State<V> {
|
|||
region: region.clone(),
|
||||
payload: payload.clone(),
|
||||
render: render.clone(),
|
||||
modifiers: modifiers.clone(),
|
||||
},
|
||||
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) {
|
||||
cx.update_global(|this: &mut Self, _| {
|
||||
this.currently_dragged = Some(State::Down {
|
||||
|
@ -124,7 +148,7 @@ impl<V: 'static> DragAndDrop<V> {
|
|||
event: MouseDrag,
|
||||
payload: Rc<T>,
|
||||
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();
|
||||
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 {
|
||||
this.currently_dragged = Some(State::Dragging {
|
||||
modifiers: event.modifiers,
|
||||
window,
|
||||
region_offset,
|
||||
region,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
render: Rc::new(move |modifiers, payload, cx| {
|
||||
render(modifiers, payload.downcast_ref::<T>().unwrap(), cx)
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
|
@ -160,16 +185,18 @@ impl<V: 'static> DragAndDrop<V> {
|
|||
Some(&State::Dragging {
|
||||
region_offset,
|
||||
region,
|
||||
modifiers,
|
||||
..
|
||||
}) => {
|
||||
this.currently_dragged = Some(State::Dragging {
|
||||
modifiers,
|
||||
window,
|
||||
region_offset,
|
||||
region,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
render: Rc::new(move |modifiers, payload, 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>> {
|
||||
enum DraggedElementHandler {}
|
||||
cx.global::<Self>()
|
||||
|
@ -188,6 +234,7 @@ impl<V: 'static> DragAndDrop<V> {
|
|||
State::Down { .. } => None,
|
||||
State::DeadZone { .. } => None,
|
||||
State::Dragging {
|
||||
modifiers,
|
||||
window,
|
||||
region_offset,
|
||||
position,
|
||||
|
@ -205,7 +252,7 @@ impl<V: 'static> DragAndDrop<V> {
|
|||
MouseEventHandler::new::<DraggedElementHandler, _>(
|
||||
0,
|
||||
cx,
|
||||
|_, cx| render(payload, cx),
|
||||
|_, cx| render(&modifiers, payload, cx),
|
||||
)
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, _, cx| {
|
||||
|
@ -295,7 +342,7 @@ pub trait Draggable<V> {
|
|||
fn as_draggable<D: View, P: Any>(
|
||||
self,
|
||||
payload: P,
|
||||
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
|
||||
render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext<D>) -> AnyElement<D>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
@ -305,7 +352,7 @@ impl<V: 'static> Draggable<V> for MouseEventHandler<V> {
|
|||
fn as_draggable<D: View, P: Any>(
|
||||
self,
|
||||
payload: P,
|
||||
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
|
||||
render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext<D>) -> AnyElement<D>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
|
|
|
@ -1513,7 +1513,7 @@ impl ProjectPanel {
|
|||
.as_draggable(entry_id, {
|
||||
let row_container_style = theme.dragged_entry.container;
|
||||
|
||||
move |_, cx: &mut ViewContext<Workspace>| {
|
||||
move |_, _, cx: &mut ViewContext<Workspace>| {
|
||||
let theme = theme::current(cx).clone();
|
||||
Self::render_entry_visual_element(
|
||||
&details,
|
||||
|
|
|
@ -135,17 +135,18 @@ message Envelope {
|
|||
RefreshInlayHints refresh_inlay_hints = 118;
|
||||
|
||||
CreateChannel create_channel = 119;
|
||||
ChannelResponse channel_response = 120;
|
||||
CreateChannelResponse create_channel_response = 120;
|
||||
InviteChannelMember invite_channel_member = 121;
|
||||
RemoveChannelMember remove_channel_member = 122;
|
||||
RespondToChannelInvite respond_to_channel_invite = 123;
|
||||
UpdateChannels update_channels = 124;
|
||||
JoinChannel join_channel = 125;
|
||||
RemoveChannel remove_channel = 126;
|
||||
DeleteChannel delete_channel = 126;
|
||||
GetChannelMembers get_channel_members = 127;
|
||||
GetChannelMembersResponse get_channel_members_response = 128;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 129;
|
||||
RenameChannel rename_channel = 130;
|
||||
RenameChannelResponse rename_channel_response = 154;
|
||||
|
||||
JoinChannelBuffer join_channel_buffer = 131;
|
||||
JoinChannelBufferResponse join_channel_buffer_response = 132;
|
||||
|
@ -165,7 +166,11 @@ message Envelope {
|
|||
ChannelMessageSent channel_message_sent = 147;
|
||||
GetChannelMessages get_channel_messages = 148;
|
||||
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 {
|
||||
repeated Channel channels = 1;
|
||||
repeated uint64 remove_channels = 2;
|
||||
repeated Channel channel_invitations = 3;
|
||||
repeated uint64 remove_channel_invitations = 4;
|
||||
repeated ChannelParticipants channel_participants = 5;
|
||||
repeated ChannelPermission channel_permissions = 6;
|
||||
repeated ChannelEdge insert_edge = 2;
|
||||
repeated ChannelEdge delete_edge = 3;
|
||||
repeated uint64 delete_channels = 4;
|
||||
repeated Channel channel_invitations = 5;
|
||||
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 {
|
||||
|
@ -976,7 +988,7 @@ message JoinChannel {
|
|||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
message RemoveChannel {
|
||||
message DeleteChannel {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
|
@ -1005,8 +1017,9 @@ message CreateChannel {
|
|||
optional uint64 parent_id = 2;
|
||||
}
|
||||
|
||||
message ChannelResponse {
|
||||
message CreateChannelResponse {
|
||||
Channel channel = 1;
|
||||
optional uint64 parent_id = 2;
|
||||
}
|
||||
|
||||
message InviteChannelMember {
|
||||
|
@ -1031,6 +1044,10 @@ message RenameChannel {
|
|||
string name = 2;
|
||||
}
|
||||
|
||||
message RenameChannelResponse {
|
||||
Channel channel = 1;
|
||||
}
|
||||
|
||||
message JoinChannelChat {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
@ -1074,6 +1091,22 @@ message GetChannelMessagesResponse {
|
|||
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 {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
@ -1486,7 +1519,6 @@ message Nonce {
|
|||
message Channel {
|
||||
uint64 id = 1;
|
||||
string name = 2;
|
||||
optional uint64 parent_id = 3;
|
||||
}
|
||||
|
||||
message Contact {
|
||||
|
|
|
@ -146,7 +146,7 @@ messages!(
|
|||
(CopyProjectEntry, Foreground),
|
||||
(CreateBufferForPeer, Foreground),
|
||||
(CreateChannel, Foreground),
|
||||
(ChannelResponse, Foreground),
|
||||
(CreateChannelResponse, Foreground),
|
||||
(ChannelMessageSent, Foreground),
|
||||
(CreateProjectEntry, Foreground),
|
||||
(CreateRoom, Foreground),
|
||||
|
@ -229,6 +229,7 @@ messages!(
|
|||
(RoomUpdated, Foreground),
|
||||
(SaveBuffer, Foreground),
|
||||
(RenameChannel, Foreground),
|
||||
(RenameChannelResponse, Foreground),
|
||||
(SetChannelMemberAdmin, Foreground),
|
||||
(SearchProject, Background),
|
||||
(SearchProjectResponse, Background),
|
||||
|
@ -246,7 +247,10 @@ messages!(
|
|||
(UpdateBuffer, Foreground),
|
||||
(UpdateBufferFile, Foreground),
|
||||
(UpdateContacts, Foreground),
|
||||
(RemoveChannel, Foreground),
|
||||
(DeleteChannel, Foreground),
|
||||
(MoveChannel, Foreground),
|
||||
(LinkChannel, Foreground),
|
||||
(UnlinkChannel, Foreground),
|
||||
(UpdateChannels, Foreground),
|
||||
(UpdateDiagnosticSummary, Foreground),
|
||||
(UpdateFollowers, Foreground),
|
||||
|
@ -282,7 +286,7 @@ request_messages!(
|
|||
(CopyProjectEntry, ProjectEntryResponse),
|
||||
(CreateProjectEntry, ProjectEntryResponse),
|
||||
(CreateRoom, CreateRoomResponse),
|
||||
(CreateChannel, ChannelResponse),
|
||||
(CreateChannel, CreateChannelResponse),
|
||||
(DeclineCall, Ack),
|
||||
(DeleteProjectEntry, ProjectEntryResponse),
|
||||
(ExpandProjectEntry, ExpandProjectEntryResponse),
|
||||
|
@ -327,10 +331,13 @@ request_messages!(
|
|||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannelMembers, GetChannelMembersResponse),
|
||||
(JoinChannel, JoinRoomResponse),
|
||||
(RemoveChannel, Ack),
|
||||
(RemoveChannelMessage, Ack),
|
||||
(DeleteChannel, Ack),
|
||||
(RenameProjectEntry, ProjectEntryResponse),
|
||||
(RenameChannel, ChannelResponse),
|
||||
(RenameChannel, RenameChannelResponse),
|
||||
(LinkChannel, Ack),
|
||||
(UnlinkChannel, Ack),
|
||||
(MoveChannel, Ack),
|
||||
(SaveBuffer, BufferSaved),
|
||||
(SearchProject, SearchProjectResponse),
|
||||
(ShareProject, ShareProjectResponse),
|
||||
|
|
|
@ -1383,7 +1383,7 @@ impl Pane {
|
|||
let theme = theme::current(cx).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;
|
||||
Self::render_dragged_tab(
|
||||
&dragged_item.handle,
|
||||
|
|
|
@ -33,8 +33,8 @@ use gpui::{
|
|||
},
|
||||
impl_actions,
|
||||
platform::{
|
||||
CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
|
||||
WindowOptions,
|
||||
CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
|
||||
WindowBounds, WindowOptions,
|
||||
},
|
||||
AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
|
||||
ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
|
@ -3815,6 +3815,10 @@ impl View for Workspace {
|
|||
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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue