Implement basic channel member management UI
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
129f2890c5
commit
a7e883d956
9 changed files with 368 additions and 100 deletions
|
@ -1,6 +1,8 @@
|
||||||
use crate::{Client, Subscription, User, UserStore};
|
use crate::{Client, Subscription, User, UserStore};
|
||||||
|
use anyhow::anyhow;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
use collections::HashSet;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
||||||
use rpc::{proto, TypedEnvelope};
|
use rpc::{proto, TypedEnvelope};
|
||||||
|
@ -13,6 +15,7 @@ pub struct ChannelStore {
|
||||||
channels: Vec<Arc<Channel>>,
|
channels: Vec<Arc<Channel>>,
|
||||||
channel_invitations: Vec<Arc<Channel>>,
|
channel_invitations: Vec<Arc<Channel>>,
|
||||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||||
|
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
_rpc_subscription: Subscription,
|
_rpc_subscription: Subscription,
|
||||||
|
@ -33,6 +36,7 @@ impl Entity for ChannelStore {
|
||||||
pub enum ChannelMemberStatus {
|
pub enum ChannelMemberStatus {
|
||||||
Invited,
|
Invited,
|
||||||
Member,
|
Member,
|
||||||
|
NotMember,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelStore {
|
impl ChannelStore {
|
||||||
|
@ -48,6 +52,7 @@ impl ChannelStore {
|
||||||
channels: vec![],
|
channels: vec![],
|
||||||
channel_invitations: vec![],
|
channel_invitations: vec![],
|
||||||
channel_participants: Default::default(),
|
channel_participants: Default::default(),
|
||||||
|
outgoing_invites: Default::default(),
|
||||||
client,
|
client,
|
||||||
user_store,
|
user_store,
|
||||||
_rpc_subscription: rpc_subscription,
|
_rpc_subscription: rpc_subscription,
|
||||||
|
@ -88,13 +93,19 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn invite_member(
|
pub fn invite_member(
|
||||||
&self,
|
&mut self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
admin: bool,
|
admin: bool,
|
||||||
) -> impl Future<Output = Result<()>> {
|
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();
|
let client = self.client.clone();
|
||||||
async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
client
|
client
|
||||||
.request(proto::InviteChannelMember {
|
.request(proto::InviteChannelMember {
|
||||||
channel_id,
|
channel_id,
|
||||||
|
@ -102,8 +113,12 @@ impl ChannelStore {
|
||||||
admin,
|
admin,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn respond_to_channel_invite(
|
pub fn respond_to_channel_invite(
|
||||||
|
@ -120,24 +135,34 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_channel_members(
|
pub fn get_channel_member_details(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
) -> impl 'static + Future<Output = Result<HashMap<UserId, ChannelMemberStatus>>> {
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Vec<(Arc<User>, proto::channel_member::Kind)>>> {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
async move {
|
let user_store = self.user_store.downgrade();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
let response = client
|
let response = client
|
||||||
.request(proto::GetChannelMembers { channel_id })
|
.request(proto::GetChannelMembers { channel_id })
|
||||||
.await?;
|
.await?;
|
||||||
let mut result = HashMap::default();
|
|
||||||
for member_id in response.members {
|
let user_ids = response.members.iter().map(|m| m.user_id).collect();
|
||||||
result.insert(member_id, ChannelMemberStatus::Member);
|
let user_store = user_store
|
||||||
}
|
.upgrade(&cx)
|
||||||
for invitee_id in response.invited_members {
|
.ok_or_else(|| anyhow!("user store dropped"))?;
|
||||||
result.insert(invitee_id, ChannelMemberStatus::Invited);
|
let users = user_store
|
||||||
}
|
.update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))
|
||||||
Ok(result)
|
.await?;
|
||||||
}
|
|
||||||
|
Ok(users
|
||||||
|
.into_iter()
|
||||||
|
.zip(response.members)
|
||||||
|
.filter_map(|(user, member)| {
|
||||||
|
Some((user, proto::channel_member::Kind::from_i32(member.kind)?))
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
|
pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
|
||||||
|
@ -148,25 +173,22 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_channel_invite_pending(&self, _: &Arc<Channel>) -> bool {
|
pub fn has_pending_channel_invite_response(&self, _: &Arc<Channel>) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_pending_channel_invite(&self, channel_id: ChannelId, user_id: UserId) -> bool {
|
||||||
|
self.outgoing_invites.contains(&(channel_id, user_id))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_member(
|
pub fn remove_member(
|
||||||
&self,
|
&self,
|
||||||
channel_id: ChannelId,
|
_channel_id: ChannelId,
|
||||||
user_id: u64,
|
_user_id: u64,
|
||||||
cx: &mut ModelContext<Self>,
|
_cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
todo!()
|
dbg!("TODO");
|
||||||
}
|
Task::Ready(Some(Ok(())))
|
||||||
|
|
||||||
pub fn channel_members(
|
|
||||||
&self,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Task<Result<Vec<Arc<User>>>> {
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_update_channels(
|
async fn handle_update_channels(
|
||||||
|
|
|
@ -213,20 +213,21 @@ impl Database {
|
||||||
);
|
);
|
||||||
|
|
||||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||||
let channel_members = if let Some(channel_id) = channel_id {
|
let channel_members;
|
||||||
self.get_channel_members_internal(channel_id, &tx).await?
|
if let Some(channel_id) = channel_id {
|
||||||
|
channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
channel_members = Vec::new();
|
||||||
};
|
|
||||||
|
|
||||||
// Delete the room if it becomes empty.
|
// Delete the room if it becomes empty.
|
||||||
if room.participants.is_empty() {
|
if room.participants.is_empty() {
|
||||||
project::Entity::delete_many()
|
project::Entity::delete_many()
|
||||||
.filter(project::Column::RoomId.eq(room_id))
|
.filter(project::Column::RoomId.eq(room_id))
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(RefreshedRoom {
|
Ok(RefreshedRoom {
|
||||||
room,
|
room,
|
||||||
|
@ -3475,10 +3476,61 @@ impl Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
|
pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
|
||||||
|
self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await })
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add a chekc whether this user is allowed to read this channel
|
||||||
|
pub async fn get_channel_member_details(
|
||||||
|
&self,
|
||||||
|
id: ChannelId,
|
||||||
|
) -> Result<Vec<proto::ChannelMember>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
|
enum QueryMemberDetails {
|
||||||
|
UserId,
|
||||||
|
IsDirectMember,
|
||||||
|
Accepted,
|
||||||
|
}
|
||||||
|
|
||||||
let tx = tx;
|
let tx = tx;
|
||||||
let user_ids = self.get_channel_members_internal(id, &*tx).await?;
|
let ancestor_ids = self.get_channel_ancestors(id, &*tx).await?;
|
||||||
Ok(user_ids)
|
let mut stream = channel_member::Entity::find()
|
||||||
|
.distinct()
|
||||||
|
.filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied()))
|
||||||
|
.select_only()
|
||||||
|
.column(channel_member::Column::UserId)
|
||||||
|
.column_as(
|
||||||
|
channel_member::Column::ChannelId.eq(id),
|
||||||
|
QueryMemberDetails::IsDirectMember,
|
||||||
|
)
|
||||||
|
.column(channel_member::Column::Accepted)
|
||||||
|
.order_by_asc(channel_member::Column::UserId)
|
||||||
|
.into_values::<_, QueryMemberDetails>()
|
||||||
|
.stream(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut rows = Vec::<proto::ChannelMember>::new();
|
||||||
|
while let Some(row) = stream.next().await {
|
||||||
|
let (user_id, is_direct_member, is_invite_accepted): (UserId, bool, bool) = row?;
|
||||||
|
let kind = match (is_direct_member, is_invite_accepted) {
|
||||||
|
(true, true) => proto::channel_member::Kind::Member,
|
||||||
|
(true, false) => proto::channel_member::Kind::Invitee,
|
||||||
|
(false, true) => proto::channel_member::Kind::AncestorMember,
|
||||||
|
(false, false) => continue,
|
||||||
|
};
|
||||||
|
let user_id = user_id.to_proto();
|
||||||
|
let kind = kind.into();
|
||||||
|
if let Some(last_row) = rows.last_mut() {
|
||||||
|
if last_row.user_id == user_id {
|
||||||
|
last_row.kind = last_row.kind.min(kind);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.push(proto::ChannelMember { user_id, kind });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -1161,7 +1161,50 @@ test_both_dbs!(
|
||||||
.map(|channel| channel.id)
|
.map(|channel| channel.id)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(user_3_invites, &[channel_1_1])
|
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||||
|
|
||||||
|
let members = db.get_channel_member_details(channel_1_1).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
members,
|
||||||
|
&[
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: user_1.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: user_2.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Invitee.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: user_3.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Invitee.into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
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).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
members,
|
||||||
|
&[
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: user_1.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: user_2.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -246,6 +246,7 @@ impl Server {
|
||||||
.add_request_handler(remove_channel)
|
.add_request_handler(remove_channel)
|
||||||
.add_request_handler(invite_channel_member)
|
.add_request_handler(invite_channel_member)
|
||||||
.add_request_handler(remove_channel_member)
|
.add_request_handler(remove_channel_member)
|
||||||
|
.add_request_handler(get_channel_members)
|
||||||
.add_request_handler(respond_to_channel_invite)
|
.add_request_handler(respond_to_channel_invite)
|
||||||
.add_request_handler(join_channel)
|
.add_request_handler(join_channel)
|
||||||
.add_request_handler(follow)
|
.add_request_handler(follow)
|
||||||
|
@ -2236,6 +2237,18 @@ async fn remove_channel_member(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_channel_members(
|
||||||
|
request: proto::GetChannelMembers,
|
||||||
|
response: Response<proto::GetChannelMembers>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = session.db().await;
|
||||||
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
|
let members = db.get_channel_member_details(channel_id).await?;
|
||||||
|
response.send(proto::GetChannelMembersResponse { members })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn respond_to_channel_invite(
|
async fn respond_to_channel_invite(
|
||||||
request: proto::RespondToChannelInvite,
|
request: proto::RespondToChannelInvite,
|
||||||
response: Response<proto::RespondToChannelInvite>,
|
response: Response<proto::RespondToChannelInvite>,
|
||||||
|
|
|
@ -291,8 +291,13 @@ impl TestServer {
|
||||||
admin_client
|
admin_client
|
||||||
.app_state
|
.app_state
|
||||||
.channel_store
|
.channel_store
|
||||||
.update(admin_cx, |channel_store, _| {
|
.update(admin_cx, |channel_store, cx| {
|
||||||
channel_store.invite_member(channel_id, member_client.user_id().unwrap(), false)
|
channel_store.invite_member(
|
||||||
|
channel_id,
|
||||||
|
member_client.user_id().unwrap(),
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use client::{Channel, User};
|
use client::{Channel, User};
|
||||||
use gpui::{executor::Deterministic, TestAppContext};
|
use gpui::{executor::Deterministic, TestAppContext};
|
||||||
|
use rpc::proto;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::tests::{room_participants, RoomParticipants};
|
use crate::tests::{room_participants, RoomParticipants};
|
||||||
|
@ -46,8 +47,14 @@ async fn test_basic_channels(
|
||||||
// Invite client B to channel A as client A.
|
// Invite client B to channel A as client A.
|
||||||
client_a
|
client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |channel_store, _| {
|
.update(cx_a, |store, cx| {
|
||||||
channel_store.invite_member(channel_a_id, client_b.user_id().unwrap(), false)
|
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||||
|
|
||||||
|
let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
|
||||||
|
|
||||||
|
// Make sure we're synchronously storing the pending invite
|
||||||
|
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||||
|
invite
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -66,6 +73,27 @@ async fn test_basic_channels(
|
||||||
})]
|
})]
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
let members = client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |store, cx| {
|
||||||
|
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||||
|
store.get_channel_member_details(channel_a_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_members_eq(
|
||||||
|
&members,
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
client_a.user_id().unwrap(),
|
||||||
|
proto::channel_member::Kind::Member,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::channel_member::Kind::Invitee,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Client B now sees that they are a member channel A.
|
// Client B now sees that they are a member channel A.
|
||||||
client_b
|
client_b
|
||||||
|
@ -113,6 +141,19 @@ fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_members_eq(
|
||||||
|
members: &[(Arc<User>, proto::channel_member::Kind)],
|
||||||
|
expected_members: &[(u64, proto::channel_member::Kind)],
|
||||||
|
) {
|
||||||
|
assert_eq!(
|
||||||
|
members
|
||||||
|
.iter()
|
||||||
|
.map(|(user, status)| (user.id, *status))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
expected_members
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_channel_room(
|
async fn test_channel_room(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
|
|
|
@ -1333,7 +1333,9 @@ impl CollabPanel {
|
||||||
enum Accept {}
|
enum Accept {}
|
||||||
|
|
||||||
let channel_id = channel.id;
|
let channel_id = channel.id;
|
||||||
let is_invite_pending = channel_store.read(cx).is_channel_invite_pending(&channel);
|
let is_invite_pending = channel_store
|
||||||
|
.read(cx)
|
||||||
|
.has_pending_channel_invite_response(&channel);
|
||||||
let button_spacing = theme.contact_button_spacing;
|
let button_spacing = theme.contact_button_spacing;
|
||||||
|
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
@ -1682,7 +1684,10 @@ impl CollabPanel {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
let user_store = self.user_store.clone();
|
let user_store = self.user_store.clone();
|
||||||
let channel_store = self.channel_store.clone();
|
let channel_store = self.channel_store.clone();
|
||||||
let members = self.channel_store.read(cx).get_channel_members(channel_id);
|
let members = self.channel_store.update(cx, |channel_store, cx| {
|
||||||
|
channel_store.get_channel_member_details(channel_id, cx)
|
||||||
|
});
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
let members = members.await?;
|
let members = members.await?;
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
@ -1692,6 +1697,7 @@ impl CollabPanel {
|
||||||
user_store.clone(),
|
user_store.clone(),
|
||||||
channel_store.clone(),
|
channel_store.clone(),
|
||||||
channel_id,
|
channel_id,
|
||||||
|
channel_modal::Mode::InviteMembers,
|
||||||
members,
|
members,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
use client::{
|
use client::{proto, ChannelId, ChannelStore, User, UserId, UserStore};
|
||||||
ChannelId, ChannelMemberStatus, ChannelStore, ContactRequestStatus, User, UserId, UserStore,
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
};
|
|
||||||
use collections::HashMap;
|
|
||||||
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
|
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
|
||||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -17,30 +15,48 @@ pub fn build_channel_modal(
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
members: HashMap<UserId, ChannelMemberStatus>,
|
mode: Mode,
|
||||||
|
members: Vec<(Arc<User>, proto::channel_member::Kind)>,
|
||||||
cx: &mut ViewContext<ChannelModal>,
|
cx: &mut ViewContext<ChannelModal>,
|
||||||
) -> ChannelModal {
|
) -> ChannelModal {
|
||||||
Picker::new(
|
Picker::new(
|
||||||
ChannelModalDelegate {
|
ChannelModalDelegate {
|
||||||
potential_contacts: Arc::from([]),
|
matches: Vec::new(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
user_store,
|
user_store,
|
||||||
channel_store,
|
channel_store,
|
||||||
channel_id: channel,
|
channel_id: channel,
|
||||||
member_statuses: members,
|
match_candidates: members
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, member)| StringMatchCandidate {
|
||||||
|
id,
|
||||||
|
string: member.0.github_login.clone(),
|
||||||
|
char_bag: member.0.github_login.chars().collect(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
members,
|
||||||
|
mode,
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.with_theme(|theme| theme.picker.clone())
|
.with_theme(|theme| theme.picker.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Mode {
|
||||||
|
ManageMembers,
|
||||||
|
InviteMembers,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ChannelModalDelegate {
|
pub struct ChannelModalDelegate {
|
||||||
potential_contacts: Arc<[Arc<User>]>,
|
matches: Vec<(Arc<User>, Option<proto::channel_member::Kind>)>,
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
member_statuses: HashMap<UserId, ChannelMemberStatus>,
|
mode: Mode,
|
||||||
|
match_candidates: Arc<[StringMatchCandidate]>,
|
||||||
|
members: Vec<(Arc<User>, proto::channel_member::Kind)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickerDelegate for ChannelModalDelegate {
|
impl PickerDelegate for ChannelModalDelegate {
|
||||||
|
@ -49,7 +65,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
fn match_count(&self) -> usize {
|
||||||
self.potential_contacts.len()
|
self.matches.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
fn selected_index(&self) -> usize {
|
||||||
|
@ -61,39 +77,80 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||||
let search_users = self
|
match self.mode {
|
||||||
.user_store
|
Mode::ManageMembers => {
|
||||||
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
let match_candidates = self.match_candidates.clone();
|
||||||
|
cx.spawn(|picker, mut cx| async move {
|
||||||
cx.spawn(|picker, mut cx| async move {
|
async move {
|
||||||
async {
|
let matches = match_strings(
|
||||||
let potential_contacts = search_users.await?;
|
&match_candidates,
|
||||||
picker.update(&mut cx, |picker, cx| {
|
&query,
|
||||||
picker.delegate_mut().potential_contacts = potential_contacts.into();
|
true,
|
||||||
cx.notify();
|
usize::MAX,
|
||||||
})?;
|
&Default::default(),
|
||||||
anyhow::Ok(())
|
cx.background().clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
picker.update(&mut cx, |picker, cx| {
|
||||||
|
let delegate = picker.delegate_mut();
|
||||||
|
delegate.matches.clear();
|
||||||
|
delegate.matches.extend(matches.into_iter().map(|m| {
|
||||||
|
let member = &delegate.members[m.candidate_id];
|
||||||
|
(member.0.clone(), Some(member.1))
|
||||||
|
}));
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
.await;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.log_err()
|
Mode::InviteMembers => {
|
||||||
.await;
|
let search_users = self
|
||||||
})
|
.user_store
|
||||||
|
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||||
|
cx.spawn(|picker, mut cx| async move {
|
||||||
|
async {
|
||||||
|
let users = search_users.await?;
|
||||||
|
picker.update(&mut cx, |picker, cx| {
|
||||||
|
let delegate = picker.delegate_mut();
|
||||||
|
delegate.matches.clear();
|
||||||
|
delegate
|
||||||
|
.matches
|
||||||
|
.extend(users.into_iter().map(|user| (user, None)));
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
if let Some(user) = self.potential_contacts.get(self.selected_index) {
|
if let Some((user, _)) = self.matches.get(self.selected_index) {
|
||||||
let user_store = self.user_store.read(cx);
|
match self.mode {
|
||||||
match user_store.contact_request_status(user) {
|
Mode::ManageMembers => {
|
||||||
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
//
|
||||||
self.user_store
|
|
||||||
.update(cx, |store, cx| store.request_contact(user.id, cx))
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
ContactRequestStatus::RequestSent => {
|
Mode::InviteMembers => match self.member_status(user.id, cx) {
|
||||||
self.user_store
|
Some(proto::channel_member::Kind::Member) => {}
|
||||||
.update(cx, |store, cx| store.remove_contact(user.id, cx))
|
Some(proto::channel_member::Kind::Invitee) => self
|
||||||
.detach();
|
.channel_store
|
||||||
}
|
.update(cx, |store, cx| {
|
||||||
_ => {}
|
store.remove_member(self.channel_id, user.id, cx)
|
||||||
|
})
|
||||||
|
.detach(),
|
||||||
|
Some(proto::channel_member::Kind::AncestorMember) | None => self
|
||||||
|
.channel_store
|
||||||
|
.update(cx, |store, cx| {
|
||||||
|
store.invite_member(self.channel_id, user.id, false, cx)
|
||||||
|
})
|
||||||
|
.detach(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,12 +165,16 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
) -> Option<AnyElement<Picker<Self>>> {
|
) -> Option<AnyElement<Picker<Self>>> {
|
||||||
let theme = &theme::current(cx).collab_panel.channel_modal;
|
let theme = &theme::current(cx).collab_panel.channel_modal;
|
||||||
|
|
||||||
|
let operation = match self.mode {
|
||||||
|
Mode::ManageMembers => "Manage",
|
||||||
|
Mode::InviteMembers => "Add",
|
||||||
|
};
|
||||||
self.channel_store
|
self.channel_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.channel_for_id(self.channel_id)
|
.channel_for_id(self.channel_id)
|
||||||
.map(|channel| {
|
.map(|channel| {
|
||||||
Label::new(
|
Label::new(
|
||||||
format!("Add members for #{}", channel.name),
|
format!("{} members for #{}", operation, channel.name),
|
||||||
theme.picker.item.default_style().label.clone(),
|
theme.picker.item.default_style().label.clone(),
|
||||||
)
|
)
|
||||||
.into_any()
|
.into_any()
|
||||||
|
@ -128,19 +189,17 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
cx: &gpui::AppContext,
|
cx: &gpui::AppContext,
|
||||||
) -> AnyElement<Picker<Self>> {
|
) -> AnyElement<Picker<Self>> {
|
||||||
let theme = &theme::current(cx).collab_panel.channel_modal;
|
let theme = &theme::current(cx).collab_panel.channel_modal;
|
||||||
let user = &self.potential_contacts[ix];
|
let (user, _) = &self.matches[ix];
|
||||||
let request_status = self.member_statuses.get(&user.id);
|
let request_status = self.member_status(user.id, cx);
|
||||||
|
|
||||||
let icon_path = match request_status {
|
let icon_path = match request_status {
|
||||||
Some(ChannelMemberStatus::Member) => Some("icons/check_8.svg"),
|
Some(proto::channel_member::Kind::AncestorMember) => Some("icons/check_8.svg"),
|
||||||
Some(ChannelMemberStatus::Invited) => Some("icons/x_mark_8.svg"),
|
Some(proto::channel_member::Kind::Member) => Some("icons/check_8.svg"),
|
||||||
|
Some(proto::channel_member::Kind::Invitee) => Some("icons/x_mark_8.svg"),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
let button_style = &theme.contact_button;
|
||||||
&theme.disabled_contact_button
|
|
||||||
} else {
|
|
||||||
&theme.contact_button
|
|
||||||
};
|
|
||||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(user.avatar.clone().map(|avatar| {
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
|
@ -177,3 +236,20 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ChannelModalDelegate {
|
||||||
|
fn member_status(
|
||||||
|
&self,
|
||||||
|
user_id: UserId,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<proto::channel_member::Kind> {
|
||||||
|
self.members
|
||||||
|
.iter()
|
||||||
|
.find_map(|(user, status)| (user.id == user_id).then_some(*status))
|
||||||
|
.or(self
|
||||||
|
.channel_store
|
||||||
|
.read(cx)
|
||||||
|
.has_pending_channel_invite(self.channel_id, user_id)
|
||||||
|
.then_some(proto::channel_member::Kind::Invitee))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -893,8 +893,18 @@ message GetChannelMembers {
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetChannelMembersResponse {
|
message GetChannelMembersResponse {
|
||||||
repeated uint64 members = 1;
|
repeated ChannelMember members = 1;
|
||||||
repeated uint64 invited_members = 2;
|
}
|
||||||
|
|
||||||
|
message ChannelMember {
|
||||||
|
uint64 user_id = 1;
|
||||||
|
Kind kind = 2;
|
||||||
|
|
||||||
|
enum Kind {
|
||||||
|
Member = 0;
|
||||||
|
Invitee = 1;
|
||||||
|
AncestorMember = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateChannel {
|
message CreateChannel {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue