Add buffer integration test
Rearrange channel crate structure Get channel buffer from database co-authored-by: Max <max@zed.dev>
This commit is contained in:
parent
ff5035ea37
commit
a7a4e2e369
33 changed files with 403 additions and 39 deletions
51
crates/channel/Cargo.toml
Normal file
51
crates/channel/Cargo.toml
Normal file
|
@ -0,0 +1,51 @@
|
|||
[package]
|
||||
name = "channel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/channel.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
rpc = { path = "../rpc" }
|
||||
text = { path = "../text" }
|
||||
language = { path = "../language" }
|
||||
settings = { path = "../settings" }
|
||||
staff_mode = { path = "../staff_mode" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
image = "0.23"
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
smol.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
url = "2.2"
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
7
crates/channel/src/channel.rs
Normal file
7
crates/channel/src/channel.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod channel_store;
|
||||
|
||||
pub mod channel_buffer;
|
||||
pub use channel_store::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod channel_store_tests;
|
80
crates/channel/src/channel_buffer.rs
Normal file
80
crates/channel/src/channel_buffer.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use crate::ChannelId;
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
use gpui::{Entity, ModelContext, ModelHandle, Task};
|
||||
use rpc::proto::GetChannelBuffer;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Open the channel document
|
||||
// ChannelDocumentView { ChannelDocument, Editor } -> On clone, clones internal ChannelDocument handle, instantiates new editor
|
||||
// Produces a view which is: (ChannelDocument, Editor), ChannelDocument manages subscriptions
|
||||
// ChannelDocuments -> Buffers -> Editor with that buffer
|
||||
|
||||
// ChannelDocuments {
|
||||
// ChannleBuffers: HashMap<bufferId, ModelHandle<language::Buffer>>
|
||||
// }
|
||||
|
||||
pub struct ChannelBuffer {
|
||||
channel_id: ChannelId,
|
||||
buffer: Option<ModelHandle<language::Buffer>>,
|
||||
client: Arc<Client>,
|
||||
}
|
||||
|
||||
impl Entity for ChannelBuffer {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl ChannelBuffer {
|
||||
pub fn for_channel(
|
||||
channel_id: ChannelId,
|
||||
client: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
channel_id,
|
||||
client,
|
||||
buffer: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_update(
|
||||
&mut self,
|
||||
buffer: ModelHandle<language::Buffer>,
|
||||
event: &language::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
pub fn buffer(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<language::Buffer>>> {
|
||||
if let Some(buffer) = &self.buffer {
|
||||
Task::ready(Ok(buffer.clone()))
|
||||
} else {
|
||||
let channel_id = self.channel_id;
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = client.request(GetChannelBuffer { channel_id }).await?;
|
||||
|
||||
let base_text = response.base_text;
|
||||
let operations = response
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
|
||||
|
||||
cx.subscribe(&buffer, Self::on_buffer_update).detach();
|
||||
|
||||
this.buffer = Some(buffer.clone());
|
||||
anyhow::Ok(buffer)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
560
crates/channel/src/channel_store.rs
Normal file
560
crates/channel/src/channel_store.rs
Normal file
|
@ -0,0 +1,560 @@
|
|||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use client::Status;
|
||||
use client::UserId;
|
||||
use client::{Client, Subscription, User, UserStore};
|
||||
use collections::HashMap;
|
||||
use collections::HashSet;
|
||||
use futures::channel::mpsc;
|
||||
use futures::Future;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use rpc::{proto, TypedEnvelope};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub type ChannelId = u64;
|
||||
|
||||
pub struct ChannelStore {
|
||||
channels_by_id: HashMap<ChannelId, Arc<Channel>>,
|
||||
channel_paths: Vec<Vec<ChannelId>>,
|
||||
channel_invitations: Vec<Arc<Channel>>,
|
||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||
channels_with_admin_privileges: HashSet<ChannelId>,
|
||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
_rpc_subscription: Subscription,
|
||||
_watch_connection_status: Task<()>,
|
||||
_update_channels: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct ChannelMembership {
|
||||
pub user: Arc<User>,
|
||||
pub kind: proto::channel_member::Kind,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
pub enum ChannelEvent {
|
||||
ChannelCreated(ChannelId),
|
||||
ChannelRenamed(ChannelId),
|
||||
}
|
||||
|
||||
impl Entity for ChannelStore {
|
||||
type Event = ChannelEvent;
|
||||
}
|
||||
|
||||
pub enum ChannelMemberStatus {
|
||||
Invited,
|
||||
Member,
|
||||
NotMember,
|
||||
}
|
||||
|
||||
impl ChannelStore {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let rpc_subscription =
|
||||
client.add_message_handler(cx.handle(), Self::handle_update_channels);
|
||||
|
||||
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
|
||||
let mut connection_status = client.status();
|
||||
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(status) = connection_status.next().await {
|
||||
if matches!(status, Status::ConnectionLost | Status::SignedOut) {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.channels_by_id.clear();
|
||||
this.channel_invitations.clear();
|
||||
this.channel_participants.clear();
|
||||
this.channels_with_admin_privileges.clear();
|
||||
this.channel_paths.clear();
|
||||
this.outgoing_invites.clear();
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
channels_by_id: HashMap::default(),
|
||||
channel_invitations: Vec::default(),
|
||||
channel_paths: Vec::default(),
|
||||
channel_participants: Default::default(),
|
||||
channels_with_admin_privileges: Default::default(),
|
||||
outgoing_invites: Default::default(),
|
||||
update_channels_tx,
|
||||
client,
|
||||
user_store,
|
||||
_rpc_subscription: rpc_subscription,
|
||||
_watch_connection_status: watch_connection_status,
|
||||
_update_channels: cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(update_channels) = update_channels_rx.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
let update_task = this.update(&mut cx, |this, cx| {
|
||||
this.update_channels(update_channels, cx)
|
||||
});
|
||||
if let Some(update_task) = update_task {
|
||||
update_task.await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_children(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_paths.iter().any(|path| {
|
||||
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||
path.len() > ix + 1
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn channel_count(&self) -> usize {
|
||||
self.channel_paths.len()
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
|
||||
self.channel_paths.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)?;
|
||||
let id = path.last().unwrap();
|
||||
let channel = self.channel_for_id(*id).unwrap();
|
||||
Some((path.len() - 1, channel))
|
||||
}
|
||||
|
||||
pub fn channel_invitations(&self) -> &[Arc<Channel>] {
|
||||
&self.channel_invitations
|
||||
}
|
||||
|
||||
pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc<Channel>> {
|
||||
self.channels_by_id.get(&channel_id)
|
||||
}
|
||||
|
||||
pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_paths.iter().any(|path| {
|
||||
if let Some(ix) = path.iter().position(|id| *id == channel_id) {
|
||||
path[..=ix]
|
||||
.iter()
|
||||
.any(|id| self.channels_with_admin_privileges.contains(id))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
|
||||
self.channel_participants
|
||||
.get(&channel_id)
|
||||
.map_or(&[], |v| v.as_slice())
|
||||
}
|
||||
|
||||
pub fn create_channel(
|
||||
&self,
|
||||
name: &str,
|
||||
parent_id: Option<ChannelId>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ChannelId>> {
|
||||
let client = self.client.clone();
|
||||
let name = name.trim_start_matches("#").to_owned();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let channel = client
|
||||
.request(proto::CreateChannel { name, parent_id })
|
||||
.await?
|
||||
.channel
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
|
||||
let channel_id = channel.id;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert!(task.is_none());
|
||||
|
||||
// This event is emitted because the collab panel wants to clear the pending edit state
|
||||
// before this frame is rendered. But we can't guarantee that the collab panel's future
|
||||
// will resolve before this flush_effects finishes. Synchronously emitting this event
|
||||
// ensures that the collab panel will observe this creation before the frame completes
|
||||
cx.emit(ChannelEvent::ChannelCreated(channel_id));
|
||||
});
|
||||
|
||||
Ok(channel_id)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invite_member(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("invite request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::InviteChannelMember {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
result?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_member(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("invite request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::RemoveChannelMember {
|
||||
channel_id,
|
||||
user_id,
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
});
|
||||
result?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_member_admin(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("member request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::SetChannelMemberAdmin {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
result?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rename(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
new_name: &str,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let name = new_name.to_string();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let channel = client
|
||||
.request(proto::RenameChannel { channel_id, name })
|
||||
.await?
|
||||
.channel
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert!(task.is_none());
|
||||
|
||||
// This event is emitted because the collab panel wants to clear the pending edit state
|
||||
// before this frame is rendered. But we can't guarantee that the collab panel's future
|
||||
// will resolve before this flush_effects finishes. Synchronously emitting this event
|
||||
// ensures that the collab panel will observe this creation before the frame complete
|
||||
cx.emit(ChannelEvent::ChannelRenamed(channel_id))
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn respond_to_channel_invite(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
accept: bool,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
let client = self.client.clone();
|
||||
async move {
|
||||
client
|
||||
.request(proto::RespondToChannelInvite { channel_id, accept })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_channel_member_details(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<ChannelMembership>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.downgrade();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let response = client
|
||||
.request(proto::GetChannelMembers { channel_id })
|
||||
.await?;
|
||||
|
||||
let user_ids = response.members.iter().map(|m| m.user_id).collect();
|
||||
let user_store = user_store
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("user store dropped"))?;
|
||||
let users = user_store
|
||||
.update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))
|
||||
.await?;
|
||||
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.zip(response.members)
|
||||
.filter_map(|(user, member)| {
|
||||
Some(ChannelMembership {
|
||||
user,
|
||||
admin: member.admin,
|
||||
kind: proto::channel_member::Kind::from_i32(member.kind)?,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
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?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_pending_channel_invite_response(&self, _: &Arc<Channel>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn has_pending_channel_invite(&self, channel_id: ChannelId, user_id: UserId) -> bool {
|
||||
self.outgoing_invites.contains(&(channel_id, user_id))
|
||||
}
|
||||
|
||||
async fn handle_update_channels(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::UpdateChannels>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update_channels_tx
|
||||
.unbounded_send(message.payload)
|
||||
.unwrap();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn update_channels(
|
||||
&mut self,
|
||||
payload: proto::UpdateChannels,
|
||||
cx: &mut ModelContext<ChannelStore>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if !payload.remove_channel_invitations.is_empty() {
|
||||
self.channel_invitations
|
||||
.retain(|channel| !payload.remove_channel_invitations.contains(&channel.id));
|
||||
}
|
||||
for channel in payload.channel_invitations {
|
||||
match self
|
||||
.channel_invitations
|
||||
.binary_search_by_key(&channel.id, |c| c.id)
|
||||
{
|
||||
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
|
||||
Err(ix) => self.channel_invitations.insert(
|
||||
ix,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
|
||||
if channels_changed {
|
||||
if !payload.remove_channels.is_empty() {
|
||||
self.channels_by_id
|
||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
||||
self.channel_participants
|
||||
.retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
|
||||
self.channels_with_admin_privileges
|
||||
.retain(|channel_id| !payload.remove_channels.contains(channel_id));
|
||||
}
|
||||
|
||||
for channel in payload.channels {
|
||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) {
|
||||
// FIXME: We may be missing a path for this existing channel in certain cases
|
||||
let existing_channel = Arc::make_mut(existing_channel);
|
||||
existing_channel.name = channel.name;
|
||||
continue;
|
||||
}
|
||||
|
||||
self.channels_by_id.insert(
|
||||
channel.id,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(parent_id) = channel.parent_id {
|
||||
let mut ix = 0;
|
||||
while ix < self.channel_paths.len() {
|
||||
let path = &self.channel_paths[ix];
|
||||
if path.ends_with(&[parent_id]) {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(channel.id);
|
||||
self.channel_paths.insert(ix + 1, new_path);
|
||||
ix += 1;
|
||||
}
|
||||
ix += 1;
|
||||
}
|
||||
} else {
|
||||
self.channel_paths.push(vec![channel.id]);
|
||||
}
|
||||
}
|
||||
|
||||
self.channel_paths.sort_by(|a, b| {
|
||||
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 permission in payload.channel_permissions {
|
||||
if permission.is_admin {
|
||||
self.channels_with_admin_privileges
|
||||
.insert(permission.channel_id);
|
||||
} else {
|
||||
self.channels_with_admin_privileges
|
||||
.remove(&permission.channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
if payload.channel_participants.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut all_user_ids = Vec::new();
|
||||
let channel_participants = payload.channel_participants;
|
||||
for entry in &channel_participants {
|
||||
for user_id in entry.participant_user_ids.iter() {
|
||||
if let Err(ix) = all_user_ids.binary_search(user_id) {
|
||||
all_user_ids.insert(ix, *user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let users = self
|
||||
.user_store
|
||||
.update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx));
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
let users = users.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for entry in &channel_participants {
|
||||
let mut participants: Vec<_> = entry
|
||||
.participant_user_ids
|
||||
.iter()
|
||||
.filter_map(|user_id| {
|
||||
users
|
||||
.binary_search_by_key(&user_id, |user| &user.id)
|
||||
.ok()
|
||||
.map(|ix| users[ix].clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
participants.sort_by_key(|u| u.id);
|
||||
|
||||
this.channel_participants
|
||||
.insert(entry.channel_id, participants);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
anyhow::Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn channel_path_sorting_key<'a>(
|
||||
path: &'a [ChannelId],
|
||||
channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>,
|
||||
) -> impl 'a + Iterator<Item = Option<&'a str>> {
|
||||
path.iter()
|
||||
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
||||
}
|
||||
}
|
168
crates/channel/src/channel_store_tests.rs
Normal file
168
crates/channel/src/channel_store_tests.rs
Normal file
|
@ -0,0 +1,168 @@
|
|||
use super::*;
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{AppContext, ModelHandle};
|
||||
use rpc::proto;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_update_channels(cx: &mut AppContext) {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
|
||||
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
channels: vec![
|
||||
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 {
|
||||
channel_id: 1,
|
||||
is_admin: true,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string(), false),
|
||||
(0, "b".to_string(), true),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
channels: vec![
|
||||
proto::Channel {
|
||||
id: 3,
|
||||
name: "x".to_string(),
|
||||
parent_id: Some(1),
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
name: "y".to_string(),
|
||||
parent_id: Some(2),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
(0, "a".to_string(), false),
|
||||
(1, "y".to_string(), false),
|
||||
(0, "b".to_string(), true),
|
||||
(1, "x".to_string(), true),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
|
||||
let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
channels: vec![
|
||||
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),
|
||||
},
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 0,
|
||||
is_admin: true,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
// Sanity check
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string(), true),
|
||||
(1, "b".to_string(), true),
|
||||
(2, "c".to_string(), true),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
update_channels(
|
||||
&channel_store,
|
||||
proto::UpdateChannels {
|
||||
remove_channels: vec![1, 2],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
// Make sure that the 1/2/3 path is gone
|
||||
assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx);
|
||||
}
|
||||
|
||||
fn update_channels(
|
||||
channel_store: &ModelHandle<ChannelStore>,
|
||||
message: proto::UpdateChannels,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
|
||||
assert!(task.is_none());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_channels(
|
||||
channel_store: &ModelHandle<ChannelStore>,
|
||||
expected_channels: &[(usize, String, bool)],
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let actual = channel_store.read_with(cx, |store, _| {
|
||||
store
|
||||
.channels()
|
||||
.map(|(depth, channel)| {
|
||||
(
|
||||
depth,
|
||||
channel.name.to_string(),
|
||||
store.is_user_admin(channel.id),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(actual, expected_channels);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue