656 lines
23 KiB
Rust
656 lines
23 KiB
Rust
use crate::channel_buffer::ChannelBuffer;
|
|
use anyhow::{anyhow, Result};
|
|
use client::{Client, Status, Subscription, User, UserId, UserStore};
|
|
use collections::{hash_map, HashMap, HashSet};
|
|
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
|
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
|
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>,
|
|
opened_buffers: HashMap<ChannelId, OpenedChannelBuffer>,
|
|
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,
|
|
}
|
|
|
|
enum OpenedChannelBuffer {
|
|
Open(WeakModelHandle<ChannelBuffer>),
|
|
Loading(Shared<Task<Result<ModelHandle<ChannelBuffer>, Arc<anyhow::Error>>>>),
|
|
}
|
|
|
|
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 !status.is_connected() {
|
|
if let Some(this) = this.upgrade(&cx) {
|
|
this.update(&mut cx, |this, cx| {
|
|
if matches!(status, Status::ConnectionLost | Status::SignedOut) {
|
|
this.handle_disconnect(cx);
|
|
} else {
|
|
this.disconnect_buffers(cx);
|
|
}
|
|
});
|
|
} 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(),
|
|
opened_buffers: 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 open_channel_buffer(
|
|
&mut self,
|
|
channel_id: ChannelId,
|
|
cx: &mut ModelContext<Self>,
|
|
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
|
|
// Make sure that a given channel buffer is only opened once per
|
|
// app instance, even if this method is called multiple times
|
|
// with the same channel id while the first task is still running.
|
|
let task = loop {
|
|
match self.opened_buffers.entry(channel_id) {
|
|
hash_map::Entry::Occupied(e) => match e.get() {
|
|
OpenedChannelBuffer::Open(buffer) => {
|
|
if let Some(buffer) = buffer.upgrade(cx) {
|
|
break Task::ready(Ok(buffer)).shared();
|
|
} else {
|
|
self.opened_buffers.remove(&channel_id);
|
|
continue;
|
|
}
|
|
}
|
|
OpenedChannelBuffer::Loading(task) => break task.clone(),
|
|
},
|
|
hash_map::Entry::Vacant(e) => {
|
|
let client = self.client.clone();
|
|
let task = cx
|
|
.spawn(|this, cx| async move {
|
|
let channel = this.read_with(&cx, |this, _| {
|
|
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
|
|
Arc::new(anyhow!("no channel for id: {}", channel_id))
|
|
})
|
|
})?;
|
|
|
|
ChannelBuffer::new(channel, client, cx)
|
|
.await
|
|
.map_err(Arc::new)
|
|
})
|
|
.shared();
|
|
e.insert(OpenedChannelBuffer::Loading(task.clone()));
|
|
cx.spawn({
|
|
let task = task.clone();
|
|
|this, mut cx| async move {
|
|
let result = task.await;
|
|
this.update(&mut cx, |this, cx| match result {
|
|
Ok(buffer) => {
|
|
cx.observe_release(&buffer, move |this, _, _| {
|
|
this.opened_buffers.remove(&channel_id);
|
|
})
|
|
.detach();
|
|
this.opened_buffers.insert(
|
|
channel_id,
|
|
OpenedChannelBuffer::Open(buffer.downgrade()),
|
|
);
|
|
}
|
|
Err(error) => {
|
|
log::error!("failed to open channel buffer {error:?}");
|
|
this.opened_buffers.remove(&channel_id);
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.detach();
|
|
break task;
|
|
}
|
|
}
|
|
};
|
|
cx.foreground()
|
|
.spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
|
|
}
|
|
|
|
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(())
|
|
}
|
|
|
|
fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) {
|
|
self.disconnect_buffers(cx);
|
|
self.channels_by_id.clear();
|
|
self.channel_invitations.clear();
|
|
self.channel_participants.clear();
|
|
self.channels_with_admin_privileges.clear();
|
|
self.channel_paths.clear();
|
|
self.outgoing_invites.clear();
|
|
cx.notify();
|
|
}
|
|
|
|
fn disconnect_buffers(&mut self, cx: &mut ModelContext<ChannelStore>) {
|
|
for (_, buffer) in self.opened_buffers.drain() {
|
|
if let OpenedChannelBuffer::Open(buffer) = buffer {
|
|
if let Some(buffer) = buffer.upgrade(cx) {
|
|
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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_id in &payload.remove_channels {
|
|
let channel_id = *channel_id;
|
|
if let Some(OpenedChannelBuffer::Open(buffer)) =
|
|
self.opened_buffers.remove(&channel_id)
|
|
{
|
|
if let Some(buffer) = buffer.upgrade(cx) {
|
|
buffer.update(cx, ChannelBuffer::disconnect);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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]);
|
|
}
|
|
}
|
|
}
|
|
|
|
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()))
|
|
}
|
|
}
|