Detailed changes
@@ -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)
+ }
+
+ /// Returns an iterator over all unique channels
+ pub fn channels(&self) -> impl '_ + Iterator<Item = &Arc<Channel>> {
+ self.channel_index.by_id().values()
}
- pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
- self.channel_paths.iter().map(move |path| {
+ /// 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();
+ let channels_changed = !payload.channels.is_empty()
+ || !payload.delete_channels.is_empty()
+ || !payload.insert_edge.is_empty()
+ || !payload.delete_edge.is_empty();
+
if channels_changed {
- if !payload.remove_channels.is_empty() {
- self.channels_by_id
- .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+ if !payload.delete_channels.is_empty() {
+ self.channel_index.delete_channels(&payload.delete_channels);
self.channel_participants
- .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+ .retain(|channel_id, _| !payload.delete_channels.contains(channel_id));
self.channels_with_admin_privileges
- .retain(|channel_id| !payload.remove_channels.contains(channel_id));
+ .retain(|channel_id| !payload.delete_channels.contains(channel_id));
- for channel_id in &payload.remove_channels {
+ for channel_id in &payload.delete_channels {
let channel_id = *channel_id;
if let Some(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(())
}))
}
+}
+
+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)
+ }
+}
- 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 Default for ChannelPath {
+ fn default() -> Self {
+ ChannelPath(Arc::from([]))
}
}
@@ -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,61 +391,80 @@ impl Database {
.all(&*tx)
.await?;
- let parents_by_child_id = self
- .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
+ 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?;
- let channels_with_admin_privileges = channel_memberships
- .iter()
- .filter_map(|membership| membership.admin.then_some(membership.channel_id))
- .collect();
+ self.get_user_channels(channel_membership, &tx).await
+ })
+ .await
+ }
- 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(),
- });
- }
- }
+ 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?;
- #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
- enum QueryUserIdsAndChannelIds {
- ChannelId,
- UserId,
- }
+ let channels_with_admin_privileges = channel_memberships
+ .iter()
+ .filter_map(|membership| membership.admin.then_some(membership.channel_id))
+ .collect();
- 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)
- }
+ 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,
- channel_participants,
- channels_with_admin_privileges,
- })
+ Ok(ChannelsForUser {
+ channels: graph,
+ channel_participants,
+ channels_with_admin_privileges,
})
- .await
}
pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
@@ -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
+}
@@ -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 Some(parent_id) = parent_id else {
+ return Ok(());
+ };
- let user_ids_to_notify = if let Some(parent_id) = parent_id {
- db.get_channel_members(parent_id).await?
- } else {
- vec![session.user_id]
+ let update = proto::UpdateChannels {
+ channels: vec![channel],
+ insert_edge: vec![ChannelEdge {
+ parent_id: parent_id.to_proto(),
+ channel_id: id.to_proto(),
+ }],
+ ..Default::default()
};
+ let user_ids_to_notify = db.get_channel_members(parent_id).await?;
+
let connection_pool = session.connection_pool().await;
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()
@@ -5,16 +5,17 @@ use crate::{
channel_view::{self, ChannelView},
chat_panel::ChatPanel,
face_pile::FacePile,
- CollaborationPanelSettings,
+ panel_settings, CollaborationPanelSettings,
};
use anyhow::Result;
use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
use channel_modal::ChannelModal;
use client::{proto::PeerId, Client, Contact, User, UserStore};
use contact_finder::ContactFinder;
use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
+use drag_and_drop::{DragAndDrop, Draggable};
use editor::{Cancel, Editor};
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use futures::StreamExt;
@@ -22,9 +23,9 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::{
- Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
- MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
- Stack, Svg,
+ Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
+ ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
+ SafeStylable, Stack, Svg,
},
fonts::TextStyle,
geometry::{
@@ -40,8 +41,8 @@ use menu::{Confirm, SelectNext, SelectPrev};
use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize};
use settings::SettingsStore;
-use std::{borrow::Cow, mem, sync::Arc};
-use theme::{components::ComponentExt, IconButton};
+use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
+use theme::{components::ComponentExt, IconButton, Interactive};
use util::{iife, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},
@@ -50,33 +51,38 @@ use workspace::{
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RemoveChannel {
- channel_id: u64,
+struct ToggleCollapse {
+ location: ChannelPath,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleCollapse {
- channel_id: u64,
+struct NewChannel {
+ location: ChannelPath,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct NewChannel {
- channel_id: u64,
+struct RenameChannel {
+ location: ChannelPath,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct InviteMembers {
- channel_id: u64,
+struct ToggleSelectedIx {
+ ix: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ManageMembers {
- channel_id: u64,
+struct RemoveChannel {
+ channel_id: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RenameChannel {
- channel_id: u64,
+struct InviteMembers {
+ channel_id: ChannelId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct ManageMembers {
+ channel_id: ChannelId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -89,6 +95,41 @@ pub struct JoinChannelCall {
pub channel_id: u64,
}
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct OpenChannelBuffer {
+ channel_id: ChannelId,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct StartMoveChannelFor {
+ channel_id: ChannelId,
+ parent_id: Option<ChannelId>,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct StartLinkChannelFor {
+ channel_id: ChannelId,
+ parent_id: Option<ChannelId>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct LinkChannel {
+ to: ChannelId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct MoveChannel {
+ to: ChannelId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct UnlinkChannel {
+ channel_id: ChannelId,
+ parent_id: ChannelId,
+}
+
+type DraggedChannel = (Channel, Option<ChannelId>);
+
actions!(
collab_panel,
[
@@ -96,7 +137,10 @@ actions!(
Remove,
Secondary,
CollapseSelectedChannel,
- ExpandSelectedChannel
+ ExpandSelectedChannel,
+ StartMoveChannel,
+ StartLinkChannel,
+ MoveOrLinkToSelected,
]
);
@@ -111,12 +155,33 @@ impl_actions!(
ToggleCollapse,
OpenChannelNotes,
JoinChannelCall,
+ OpenChannelBuffer,
+ LinkChannel,
+ StartMoveChannelFor,
+ StartLinkChannelFor,
+ MoveChannel,
+ UnlinkChannel,
+ ToggleSelectedIx
]
);
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+struct ChannelMoveClipboard {
+ channel_id: ChannelId,
+ parent_id: Option<ChannelId>,
+ intent: ClipboardIntent,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+enum ClipboardIntent {
+ Move,
+ Link,
+}
+
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
pub fn init(cx: &mut AppContext) {
+ settings::register::<panel_settings::CollaborationPanelSettings>(cx);
contact_finder::init(cx);
channel_modal::init(cx);
channel_view::init(cx);
@@ -133,20 +198,148 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(CollabPanel::manage_members);
cx.add_action(CollabPanel::rename_selected_channel);
cx.add_action(CollabPanel::rename_channel);
- cx.add_action(CollabPanel::toggle_channel_collapsed);
+ cx.add_action(CollabPanel::toggle_channel_collapsed_action);
cx.add_action(CollabPanel::collapse_selected_channel);
cx.add_action(CollabPanel::expand_selected_channel);
cx.add_action(CollabPanel::open_channel_notes);
+
+ cx.add_action(
+ |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
+ if panel.selection.take() != Some(action.ix) {
+ panel.selection = Some(action.ix)
+ }
+
+ cx.notify();
+ },
+ );
+
+ cx.add_action(
+ |panel: &mut CollabPanel,
+ action: &StartMoveChannelFor,
+ _: &mut ViewContext<CollabPanel>| {
+ panel.channel_clipboard = Some(ChannelMoveClipboard {
+ channel_id: action.channel_id,
+ parent_id: action.parent_id,
+ intent: ClipboardIntent::Move,
+ });
+ },
+ );
+
+ cx.add_action(
+ |panel: &mut CollabPanel,
+ action: &StartLinkChannelFor,
+ _: &mut ViewContext<CollabPanel>| {
+ panel.channel_clipboard = Some(ChannelMoveClipboard {
+ channel_id: action.channel_id,
+ parent_id: action.parent_id,
+ intent: ClipboardIntent::Link,
+ })
+ },
+ );
+
+ cx.add_action(
+ |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
+ if let Some((_, path)) = panel.selected_channel() {
+ panel.channel_clipboard = Some(ChannelMoveClipboard {
+ channel_id: path.channel_id(),
+ parent_id: path.parent_id(),
+ intent: ClipboardIntent::Move,
+ })
+ }
+ },
+ );
+
+ cx.add_action(
+ |panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext<CollabPanel>| {
+ if let Some((_, path)) = panel.selected_channel() {
+ panel.channel_clipboard = Some(ChannelMoveClipboard {
+ channel_id: path.channel_id(),
+ parent_id: path.parent_id(),
+ intent: ClipboardIntent::Link,
+ })
+ }
+ },
+ );
+
+ cx.add_action(
+ |panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext<CollabPanel>| {
+ let clipboard = panel.channel_clipboard.take();
+ if let Some(((selected_channel, _), clipboard)) =
+ panel.selected_channel().zip(clipboard)
+ {
+ match clipboard.intent {
+ ClipboardIntent::Move if clipboard.parent_id.is_some() => {
+ let parent_id = clipboard.parent_id.unwrap();
+ panel.channel_store.update(cx, |channel_store, cx| {
+ channel_store
+ .move_channel(
+ clipboard.channel_id,
+ parent_id,
+ selected_channel.id,
+ cx,
+ )
+ .detach_and_log_err(cx)
+ })
+ }
+ _ => panel.channel_store.update(cx, |channel_store, cx| {
+ channel_store
+ .link_channel(clipboard.channel_id, selected_channel.id, cx)
+ .detach_and_log_err(cx)
+ }),
+ }
+ }
+ },
+ );
+
+ cx.add_action(
+ |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext<CollabPanel>| {
+ if let Some(clipboard) = panel.channel_clipboard.take() {
+ panel.channel_store.update(cx, |channel_store, cx| {
+ channel_store
+ .link_channel(clipboard.channel_id, action.to, cx)
+ .detach_and_log_err(cx)
+ })
+ }
+ },
+ );
+
+ cx.add_action(
+ |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
+ if let Some(clipboard) = panel.channel_clipboard.take() {
+ panel.channel_store.update(cx, |channel_store, cx| {
+ if let Some(parent) = clipboard.parent_id {
+ channel_store
+ .move_channel(clipboard.channel_id, parent, action.to, cx)
+ .detach_and_log_err(cx)
+ } else {
+ channel_store
+ .link_channel(clipboard.channel_id, action.to, cx)
+ .detach_and_log_err(cx)
+ }
+ })
+ }
+ },
+ );
+
+ cx.add_action(
+ |panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext<CollabPanel>| {
+ panel.channel_store.update(cx, |channel_store, cx| {
+ channel_store
+ .unlink_channel(action.channel_id, action.parent_id, cx)
+ .detach_and_log_err(cx)
+ })
+ },
+ );
}
#[derive(Debug)]
pub enum ChannelEditingState {
Create {
- parent_id: Option<u64>,
+ location: Option<ChannelPath>,
pending_name: Option<String>,
},
Rename {
- channel_id: u64,
+ location: ChannelPath,
pending_name: Option<String>,
},
}
@@ -164,6 +357,7 @@ pub struct CollabPanel {
width: Option<f32>,
fs: Arc<dyn Fs>,
has_focus: bool,
+ channel_clipboard: Option<ChannelMoveClipboard>,
pending_serialization: Task<Option<()>>,
context_menu: ViewHandle<ContextMenu>,
filter_editor: ViewHandle<Editor>,
@@ -179,7 +373,8 @@ pub struct CollabPanel {
list_state: ListState<Self>,
subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>,
- collapsed_channels: Vec<ChannelId>,
+ collapsed_channels: Vec<ChannelPath>,
+ drag_target_channel: Option<ChannelData>,
workspace: WeakViewHandle<Workspace>,
context_menu_on_selected: bool,
}
@@ -187,7 +382,7 @@ pub struct CollabPanel {
#[derive(Serialize, Deserialize)]
struct SerializedCollabPanel {
width: Option<f32>,
- collapsed_channels: Option<Vec<ChannelId>>,
+ collapsed_channels: Option<Vec<ChannelPath>>,
}
#[derive(Debug)]
@@ -231,6 +426,7 @@ enum ListEntry {
Channel {
channel: Arc<Channel>,
depth: usize,
+ path: ChannelPath,
},
ChannelNotes {
channel_id: ChannelId,
@@ -348,12 +544,18 @@ impl CollabPanel {
cx,
)
}
- ListEntry::Channel { channel, depth } => {
+ ListEntry::Channel {
+ channel,
+ depth,
+ path,
+ } => {
let channel_row = this.render_channel(
&*channel,
*depth,
+ path.to_owned(),
&theme.collab_panel,
is_selected,
+ ix,
cx,
);
@@ -420,6 +622,7 @@ impl CollabPanel {
let mut this = Self {
width: None,
has_focus: false,
+ channel_clipboard: None,
fs: workspace.app_state().fs.clone(),
pending_serialization: Task::ready(None),
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
@@ -438,6 +641,7 @@ impl CollabPanel {
workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(),
context_menu_on_selected: true,
+ drag_target_channel: None,
list_state,
};
@@ -507,7 +711,13 @@ impl CollabPanel {
.log_err()
.flatten()
{
- Some(serde_json::from_str::<SerializedCollabPanel>(&panel)?)
+ match serde_json::from_str::<SerializedCollabPanel>(&panel) {
+ Ok(panel) => Some(panel),
+ Err(err) => {
+ log::error!("Failed to deserialize collaboration panel: {}", err);
+ None
+ }
+ }
} else {
None
};
@@ -678,16 +888,13 @@ impl CollabPanel {
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
self.match_candidates.clear();
self.match_candidates
- .extend(
- channel_store
- .channels()
- .enumerate()
- .map(|(ix, (_, channel))| StringMatchCandidate {
- id: ix,
- string: channel.name.clone(),
- char_bag: channel.name.chars().collect(),
- }),
- );
+ .extend(channel_store.channel_dag_entries().enumerate().map(
+ |(ix, (_, channel))| StringMatchCandidate {
+ id: ix,
+ string: channel.name.clone(),
+ char_bag: channel.name.chars().collect(),
+ },
+ ));
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -697,28 +904,24 @@ impl CollabPanel {
executor.clone(),
));
if let Some(state) = &self.channel_editing_state {
- if matches!(
- state,
- ChannelEditingState::Create {
- parent_id: None,
- ..
- }
- ) {
+ if matches!(state, ChannelEditingState::Create { location: None, .. }) {
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
}
}
let mut collapse_depth = None;
for mat in matches {
- let (depth, channel) =
- channel_store.channel_at_index(mat.candidate_id).unwrap();
+ let (channel, path) = channel_store
+ .channel_dag_entry_at(mat.candidate_id)
+ .unwrap();
+ let depth = path.len() - 1;
- if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+ if collapse_depth.is_none() && self.is_channel_collapsed(path) {
collapse_depth = Some(depth);
} else if let Some(collapsed_depth) = collapse_depth {
if depth > collapsed_depth {
continue;
}
- if self.is_channel_collapsed(channel.id) {
+ if self.is_channel_collapsed(path) {
collapse_depth = Some(depth);
} else {
collapse_depth = None;
@@ -726,25 +929,29 @@ impl CollabPanel {
}
match &self.channel_editing_state {
- Some(ChannelEditingState::Create { parent_id, .. })
- if *parent_id == Some(channel.id) =>
- {
+ Some(ChannelEditingState::Create {
+ location: parent_path,
+ ..
+ }) if parent_path.as_ref() == Some(path) => {
self.entries.push(ListEntry::Channel {
channel: channel.clone(),
depth,
+ path: path.clone(),
});
self.entries
.push(ListEntry::ChannelEditor { depth: depth + 1 });
}
- Some(ChannelEditingState::Rename { channel_id, .. })
- if *channel_id == channel.id =>
- {
+ Some(ChannelEditingState::Rename {
+ location: parent_path,
+ ..
+ }) if parent_path == path => {
self.entries.push(ListEntry::ChannelEditor { depth });
}
_ => {
self.entries.push(ListEntry::Channel {
channel: channel.clone(),
depth,
+ path: path.clone(),
});
}
}
@@ -1531,7 +1738,7 @@ impl CollabPanel {
.constrained()
.with_height(theme.collab_panel.row_height)
.contained()
- .with_style(gpui::elements::ContainerStyle {
+ .with_style(ContainerStyle {
background_color: Some(theme.editor.background),
..*theme.collab_panel.contact_row.default_style()
})
@@ -1546,14 +1753,17 @@ impl CollabPanel {
&self,
channel: &Channel,
depth: usize,
+ path: ChannelPath,
theme: &theme::CollabPanel,
is_selected: bool,
+ ix: usize,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let channel_id = channel.id;
let has_children = self.channel_store.read(cx).has_children(channel_id);
- let disclosed =
- has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
+ let other_selected =
+ self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
+ let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
let is_active = iife!({
let call_channel = ActiveCall::global(cx)
@@ -1569,9 +1779,37 @@ impl CollabPanel {
enum ChannelCall {}
- MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
+ let mut is_dragged_over = false;
+ if cx
+ .global::<DragAndDrop<Workspace>>()
+ .currently_dragged::<DraggedChannel>(cx.window())
+ .is_some()
+ && self
+ .drag_target_channel
+ .as_ref()
+ .filter(|(_, dragged_path)| path.starts_with(dragged_path))
+ .is_some()
+ {
+ is_dragged_over = true;
+ }
+
+ MouseEventHandler::new::<Channel, _>(path.unique_id() as usize, cx, |state, cx| {
let row_hovered = state.hovered();
+ let mut select_state = |interactive: &Interactive<ContainerStyle>| {
+ if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
+ interactive.clicked.as_ref().unwrap().clone()
+ } else if state.hovered() || other_selected {
+ interactive
+ .hovered
+ .as_ref()
+ .unwrap_or(&interactive.default)
+ .clone()
+ } else {
+ interactive.default.clone()
+ }
+ };
+
Flex::<Self>::row()
.with_child(
Svg::new("icons/hash.svg")
@@ -1637,25 +1875,135 @@ impl CollabPanel {
)
.align_children_center()
.styleable_component()
- .disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
- .with_id(channel_id as usize)
+ .disclosable(
+ disclosed,
+ Box::new(ToggleCollapse {
+ location: path.clone(),
+ }),
+ )
+ .with_id(path.unique_id() as usize)
.with_style(theme.disclosure.clone())
.element()
.constrained()
.with_height(theme.row_height)
.contained()
- .with_style(*theme.channel_row.style_for(is_selected || is_active, state))
+ .with_style(select_state(
+ theme
+ .channel_row
+ .in_state(is_selected || is_active || is_dragged_over),
+ ))
.with_padding_left(
theme.channel_row.default_style().padding.left
+ theme.channel_indent * depth as f32,
)
})
.on_click(MouseButton::Left, move |_, this, cx| {
- this.join_channel_chat(channel_id, cx);
+ if this.drag_target_channel.take().is_none() {
+ this.join_channel_chat(channel_id, cx);
+ }
})
- .on_click(MouseButton::Right, move |e, this, cx| {
- this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
+ .on_click(MouseButton::Right, {
+ let path = path.clone();
+ move |e, this, cx| {
+ this.deploy_channel_context_menu(Some(e.position), &path, ix, cx);
+ }
})
+ .on_up(MouseButton::Left, move |e, this, cx| {
+ if let Some((_, dragged_channel)) = cx
+ .global::<DragAndDrop<Workspace>>()
+ .currently_dragged::<DraggedChannel>(cx.window())
+ {
+ if e.modifiers.alt {
+ this.channel_store.update(cx, |channel_store, cx| {
+ channel_store
+ .link_channel(dragged_channel.0.id, channel_id, cx)
+ .detach_and_log_err(cx)
+ })
+ } else {
+ this.channel_store.update(cx, |channel_store, cx| {
+ match dragged_channel.1 {
+ Some(parent_id) => channel_store.move_channel(
+ dragged_channel.0.id,
+ parent_id,
+ channel_id,
+ cx,
+ ),
+ None => {
+ channel_store.link_channel(dragged_channel.0.id, channel_id, cx)
+ }
+ }
+ .detach_and_log_err(cx)
+ })
+ }
+ }
+ })
+ .on_move({
+ let channel = channel.clone();
+ let path = path.clone();
+ move |_, this, cx| {
+ if let Some((_, _dragged_channel)) =
+ cx.global::<DragAndDrop<Workspace>>()
+ .currently_dragged::<DraggedChannel>(cx.window())
+ {
+ match &this.drag_target_channel {
+ Some(current_target)
+ if current_target.0 == channel && current_target.1 == path =>
+ {
+ return
+ }
+ _ => {
+ this.drag_target_channel = Some((channel.clone(), path.clone()));
+ cx.notify();
+ }
+ }
+ }
+ }
+ })
+ .as_draggable(
+ (channel.clone(), path.parent_id()),
+ move |e, (channel, _), cx: &mut ViewContext<Workspace>| {
+ let theme = &theme::current(cx).collab_panel;
+
+ Flex::<Workspace>::row()
+ .with_children(e.alt.then(|| {
+ Svg::new("icons/plus.svg")
+ .with_color(theme.channel_hash.color)
+ .constrained()
+ .with_width(theme.channel_hash.width)
+ .aligned()
+ .left()
+ }))
+ .with_child(
+ Svg::new("icons/hash.svg")
+ .with_color(theme.channel_hash.color)
+ .constrained()
+ .with_width(theme.channel_hash.width)
+ .aligned()
+ .left(),
+ )
+ .with_child(
+ Label::new(channel.name.clone(), theme.channel_name.text.clone())
+ .contained()
+ .with_style(theme.channel_name.container)
+ .aligned()
+ .left(),
+ )
+ .align_children_center()
+ .contained()
+ .with_background_color(
+ theme
+ .container
+ .background_color
+ .unwrap_or(gpui::color::Color::transparent_black()),
+ )
+ .contained()
+ .with_padding_left(
+ theme.channel_row.default_style().padding.left
+ + theme.channel_indent * depth as f32,
+ )
+ .into_any()
+ },
+ )
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
}
@@ -1898,14 +2246,42 @@ impl CollabPanel {
.into_any()
}
+ fn has_subchannels(&self, ix: usize) -> bool {
+ self.entries
+ .get(ix)
+ .zip(self.entries.get(ix + 1))
+ .map(|entries| match entries {
+ (
+ ListEntry::Channel {
+ path: this_path, ..
+ },
+ ListEntry::Channel {
+ path: next_path, ..
+ },
+ ) => next_path.starts_with(this_path),
+ _ => false,
+ })
+ .unwrap_or(false)
+ }
+
fn deploy_channel_context_menu(
&mut self,
position: Option<Vector2F>,
- channel_id: u64,
+ path: &ChannelPath,
+ ix: usize,
cx: &mut ViewContext<Self>,
) {
self.context_menu_on_selected = position.is_none();
+ let channel_name = self.channel_clipboard.as_ref().and_then(|channel| {
+ let channel_name = self
+ .channel_store
+ .read(cx)
+ .channel_for_id(channel.channel_id)
+ .map(|channel| channel.name.clone())?;
+ Some(channel_name)
+ });
+
self.context_menu.update(cx, |context_menu, cx| {
context_menu.set_position_mode(if self.context_menu_on_selected {
OverlayPositionMode::Local
@@ -1913,27 +2289,124 @@ impl CollabPanel {
OverlayPositionMode::Window
});
- let expand_action_name = if self.is_channel_collapsed(channel_id) {
- "Expand Subchannels"
+ let mut items = Vec::new();
+
+ let select_action_name = if self.selection == Some(ix) {
+ "Unselect"
} else {
- "Collapse Subchannels"
+ "Select"
};
- let mut items = vec![
- ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
- ContextMenuItem::action("Open Notes", OpenChannelNotes { channel_id }),
- ];
+ items.push(ContextMenuItem::action(
+ select_action_name,
+ ToggleSelectedIx { ix },
+ ));
+
+ if self.has_subchannels(ix) {
+ let expand_action_name = if self.is_channel_collapsed(&path) {
+ "Expand Subchannels"
+ } else {
+ "Collapse Subchannels"
+ };
+ items.push(ContextMenuItem::action(
+ expand_action_name,
+ ToggleCollapse {
+ location: path.clone(),
+ },
+ ));
+ }
+
+ items.push(ContextMenuItem::action(
+ "Open Notes",
+ OpenChannelBuffer {
+ channel_id: path.channel_id(),
+ },
+ ));
+
+ if self.channel_store.read(cx).is_user_admin(path.channel_id()) {
+ let parent_id = path.parent_id();
- if self.channel_store.read(cx).is_user_admin(channel_id) {
items.extend([
ContextMenuItem::Separator,
- ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
- ContextMenuItem::action("Rename", RenameChannel { channel_id }),
+ ContextMenuItem::action(
+ "New Subchannel",
+ NewChannel {
+ location: path.clone(),
+ },
+ ),
+ ContextMenuItem::action(
+ "Rename",
+ RenameChannel {
+ location: path.clone(),
+ },
+ ),
+ ContextMenuItem::Separator,
+ ]);
+
+ if let Some(parent_id) = parent_id {
+ items.push(ContextMenuItem::action(
+ "Unlink from parent",
+ UnlinkChannel {
+ channel_id: path.channel_id(),
+ parent_id,
+ },
+ ));
+ }
+
+ items.extend([
+ ContextMenuItem::action(
+ "Move this channel",
+ StartMoveChannelFor {
+ channel_id: path.channel_id(),
+ parent_id,
+ },
+ ),
+ ContextMenuItem::action(
+ "Link this channel",
+ StartLinkChannelFor {
+ channel_id: path.channel_id(),
+ parent_id,
+ },
+ ),
+ ]);
+
+ if let Some(channel_name) = channel_name {
+ items.push(ContextMenuItem::Separator);
+ items.push(ContextMenuItem::action(
+ format!("Move '#{}' here", channel_name),
+ MoveChannel {
+ to: path.channel_id(),
+ },
+ ));
+ items.push(ContextMenuItem::action(
+ format!("Link '#{}' here", channel_name),
+ LinkChannel {
+ to: path.channel_id(),
+ },
+ ));
+ }
+
+ items.extend([
ContextMenuItem::Separator,
- ContextMenuItem::action("Invite Members", InviteMembers { channel_id }),
- ContextMenuItem::action("Manage Members", ManageMembers { channel_id }),
+ ContextMenuItem::action(
+ "Invite Members",
+ InviteMembers {
+ channel_id: path.channel_id(),
+ },
+ ),
+ ContextMenuItem::action(
+ "Manage Members",
+ ManageMembers {
+ channel_id: path.channel_id(),
+ },
+ ),
ContextMenuItem::Separator,
- ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
+ ContextMenuItem::action(
+ "Delete",
+ RemoveChannel {
+ channel_id: path.channel_id(),
+ },
+ ),
]);
}
@@ -2059,7 +2532,7 @@ impl CollabPanel {
if let Some(editing_state) = &mut self.channel_editing_state {
match editing_state {
ChannelEditingState::Create {
- parent_id,
+ location,
pending_name,
..
} => {
@@ -2072,13 +2545,17 @@ impl CollabPanel {
self.channel_store
.update(cx, |channel_store, cx| {
- channel_store.create_channel(&channel_name, *parent_id, cx)
+ channel_store.create_channel(
+ &channel_name,
+ location.as_ref().map(|location| location.channel_id()),
+ cx,
+ )
})
.detach();
cx.notify();
}
ChannelEditingState::Rename {
- channel_id,
+ location,
pending_name,
} => {
if pending_name.is_some() {
@@ -2089,7 +2566,7 @@ impl CollabPanel {
self.channel_store
.update(cx, |channel_store, cx| {
- channel_store.rename(*channel_id, &channel_name, cx)
+ channel_store.rename(location.channel_id(), &channel_name, cx)
})
.detach();
cx.notify();
@@ -2116,38 +2593,55 @@ impl CollabPanel {
_: &CollapseSelectedChannel,
cx: &mut ViewContext<Self>,
) {
- let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+ let Some((_, path)) = self
+ .selected_channel()
+ .map(|(channel, parent)| (channel.id, parent))
+ else {
return;
};
- if self.is_channel_collapsed(channel_id) {
+ if self.is_channel_collapsed(&path) {
return;
}
- self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+ self.toggle_channel_collapsed(&path.clone(), cx);
}
fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
- let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+ let Some((_, path)) = self
+ .selected_channel()
+ .map(|(channel, parent)| (channel.id, parent))
+ else {
return;
};
- if !self.is_channel_collapsed(channel_id) {
+ if !self.is_channel_collapsed(&path) {
return;
}
- self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+ self.toggle_channel_collapsed(path.to_owned(), cx)
}
- fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
- let channel_id = action.channel_id;
+ fn toggle_channel_collapsed_action(
+ &mut self,
+ action: &ToggleCollapse,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.toggle_channel_collapsed(&action.location, cx);
+ }
- match self.collapsed_channels.binary_search(&channel_id) {
+ fn toggle_channel_collapsed<'a>(
+ &mut self,
+ path: impl Into<Cow<'a, ChannelPath>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let path = path.into();
+ match self.collapsed_channels.binary_search(&path) {
Ok(ix) => {
self.collapsed_channels.remove(ix);
}
Err(ix) => {
- self.collapsed_channels.insert(ix, channel_id);
+ self.collapsed_channels.insert(ix, path.into_owned());
}
};
self.serialize(cx);
@@ -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 {