Support very large channel membership lists (#11939)

Conrad Irwin created

Fixes the channel membership dialogue for the zed channel by not
downloading all 111k people in one go.

Release Notes:

- N/A

Change summary

crates/channel/src/channel_store.rs                |  39 ++--
crates/client/src/client.rs                        |   2 
crates/client/src/user.rs                          |  33 ++-
crates/collab/src/db.rs                            |   3 
crates/collab/src/db/queries/buffers.rs            |  21 --
crates/collab/src/db/queries/channels.rs           | 114 ++++++-------
crates/collab/src/db/queries/messages.rs           |   8 
crates/collab/src/db/tests/channel_tests.rs        |  50 +++---
crates/collab/src/rpc.rs                           |  99 ++++++-----
crates/collab/src/tests/channel_tests.rs           |   2 
crates/collab_ui/src/collab_panel.rs               |  30 ++-
crates/collab_ui/src/collab_panel/channel_modal.rs | 127 ++++++++-------
crates/rpc/proto/zed.proto                         |   3 
13 files changed, 270 insertions(+), 261 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs 🔗

@@ -123,6 +123,7 @@ impl Channel {
     }
 }
 
+#[derive(Debug)]
 pub struct ChannelMembership {
     pub user: Arc<User>,
     pub kind: proto::channel_member::Kind,
@@ -815,9 +816,11 @@ impl ChannelStore {
             Ok(())
         })
     }
-    pub fn get_channel_member_details(
+    pub fn fuzzy_search_members(
         &self,
         channel_id: ChannelId,
+        query: String,
+        limit: u16,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<ChannelMembership>>> {
         let client = self.client.clone();
@@ -826,26 +829,24 @@ impl ChannelStore {
             let response = client
                 .request(proto::GetChannelMembers {
                     channel_id: channel_id.0,
+                    query,
+                    limit: limit as u64,
                 })
                 .await?;
-
-            let user_ids = response.members.iter().map(|m| m.user_id).collect();
-            let user_store = user_store
-                .upgrade()
-                .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)
-                .map(|(user, member)| ChannelMembership {
-                    user,
-                    role: member.role(),
-                    kind: member.kind(),
-                })
-                .collect())
+            user_store.update(&mut cx, |user_store, _| {
+                user_store.insert(response.users);
+                response
+                    .members
+                    .into_iter()
+                    .filter_map(|member| {
+                        Some(ChannelMembership {
+                            user: user_store.get_cached_user(member.user_id)?,
+                            role: member.role(),
+                            kind: member.kind(),
+                        })
+                    })
+                    .collect()
+            })
         })
     }
 

crates/client/src/client.rs 🔗

@@ -85,7 +85,7 @@ lazy_static! {
 }
 
 pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
-pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
+pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
 
 actions!(client, [SignIn, SignOut, Reconnect]);
 

crates/client/src/user.rs 🔗

@@ -670,28 +670,31 @@ impl UserStore {
         cx.spawn(|this, mut cx| async move {
             if let Some(rpc) = client.upgrade() {
                 let response = rpc.request(request).await.context("error loading users")?;
-                let users = response
-                    .users
-                    .into_iter()
-                    .map(User::new)
-                    .collect::<Vec<_>>();
-
-                this.update(&mut cx, |this, _| {
-                    for user in &users {
-                        this.users.insert(user.id, user.clone());
-                        this.by_github_login
-                            .insert(user.github_login.clone(), user.id);
-                    }
-                })
-                .ok();
+                let users = response.users;
 
-                Ok(users)
+                this.update(&mut cx, |this, _| this.insert(users))
             } else {
                 Ok(Vec::new())
             }
         })
     }
 
+    pub fn insert(&mut self, users: Vec<proto::User>) -> Vec<Arc<User>> {
+        let mut ret = Vec::with_capacity(users.len());
+        for user in users {
+            let user = User::new(user);
+            if let Some(old) = self.users.insert(user.id, user.clone()) {
+                if old.github_login != user.github_login {
+                    self.by_github_login.remove(&old.github_login);
+                }
+            }
+            self.by_github_login
+                .insert(user.github_login.clone(), user.id);
+            ret.push(user)
+        }
+        ret
+    }
+
     pub fn set_participant_indices(
         &mut self,
         participant_indices: HashMap<u64, ParticipantIndex>,

crates/collab/src/db.rs 🔗

@@ -509,8 +509,7 @@ pub type NotificationBatch = Vec<(UserId, proto::Notification)>;
 
 pub struct CreatedChannelMessage {
     pub message_id: MessageId,
-    pub participant_connection_ids: Vec<ConnectionId>,
-    pub channel_members: Vec<UserId>,
+    pub participant_connection_ids: HashSet<ConnectionId>,
     pub notifications: NotificationBatch,
 }
 

crates/collab/src/db/queries/buffers.rs 🔗

@@ -440,12 +440,7 @@ impl Database {
         channel_id: ChannelId,
         user: UserId,
         operations: &[proto::Operation],
-    ) -> Result<(
-        Vec<ConnectionId>,
-        Vec<UserId>,
-        i32,
-        Vec<proto::VectorClockEntry>,
-    )> {
+    ) -> Result<(HashSet<ConnectionId>, i32, Vec<proto::VectorClockEntry>)> {
         self.transaction(move |tx| async move {
             let channel = self.get_channel_internal(channel_id, &tx).await?;
 
@@ -479,7 +474,6 @@ impl Database {
                 .filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
                 .collect::<Vec<_>>();
 
-            let mut channel_members;
             let max_version;
 
             if !operations.is_empty() {
@@ -504,12 +498,6 @@ impl Database {
                 )
                 .await?;
 
-                channel_members = self.get_channel_participants(&channel, &tx).await?;
-                let collaborators = self
-                    .get_channel_buffer_collaborators_internal(channel_id, &tx)
-                    .await?;
-                channel_members.retain(|member| !collaborators.contains(member));
-
                 buffer_operation::Entity::insert_many(operations)
                     .on_conflict(
                         OnConflict::columns([
@@ -524,11 +512,10 @@ impl Database {
                     .exec(&*tx)
                     .await?;
             } else {
-                channel_members = Vec::new();
                 max_version = Vec::new();
             }
 
-            let mut connections = Vec::new();
+            let mut connections = HashSet::default();
             let mut rows = channel_buffer_collaborator::Entity::find()
                 .filter(
                     Condition::all()
@@ -538,13 +525,13 @@ impl Database {
                 .await?;
             while let Some(row) = rows.next().await {
                 let row = row?;
-                connections.push(ConnectionId {
+                connections.insert(ConnectionId {
                     id: row.connection_id as u32,
                     owner_id: row.connection_server_id.0 as u32,
                 });
             }
 
-            Ok((connections, channel_members, buffer.epoch, max_version))
+            Ok((connections, buffer.epoch, max_version))
         })
         .await
     }

crates/collab/src/db/queries/channels.rs 🔗

@@ -3,7 +3,7 @@ use rpc::{
     proto::{channel_member::Kind, ChannelBufferVersion, VectorClockEntry},
     ErrorCode, ErrorCodeExt,
 };
-use sea_orm::TryGetableMany;
+use sea_orm::{DbBackend, TryGetableMany};
 
 impl Database {
     #[cfg(test)]
@@ -700,77 +700,73 @@ impl Database {
     pub async fn get_channel_participant_details(
         &self,
         channel_id: ChannelId,
+        filter: &str,
+        limit: u64,
         user_id: UserId,
-    ) -> Result<Vec<proto::ChannelMember>> {
-        let (role, members) = self
+    ) -> Result<(Vec<proto::ChannelMember>, Vec<proto::User>)> {
+        let members = self
             .transaction(move |tx| async move {
                 let channel = self.get_channel_internal(channel_id, &tx).await?;
-                let role = self
-                    .check_user_is_channel_participant(&channel, user_id, &tx)
+                self.check_user_is_channel_participant(&channel, user_id, &tx)
                     .await?;
-                Ok((
-                    role,
-                    self.get_channel_participant_details_internal(&channel, &tx)
-                        .await?,
-                ))
+                let mut query = channel_member::Entity::find()
+                    .find_also_related(user::Entity)
+                    .filter(channel_member::Column::ChannelId.eq(channel.root_id()));
+
+                if cfg!(any(test, sqlite)) && self.pool.get_database_backend() == DbBackend::Sqlite {
+                    query = query.filter(Expr::cust_with_values(
+                        "UPPER(github_login) LIKE ?",
+                        [Self::fuzzy_like_string(&filter.to_uppercase())],
+                    ))
+                } else {
+                    query = query.filter(Expr::cust_with_values(
+                        "github_login ILIKE $1",
+                        [Self::fuzzy_like_string(filter)],
+                    ))
+                }
+                let members = query.order_by(
+                        Expr::cust(
+                            "not role = 'admin', not role = 'member', not role = 'guest', not accepted, github_login",
+                        ),
+                        sea_orm::Order::Asc,
+                    )
+                    .limit(limit)
+                    .all(&*tx)
+                    .await?;
+
+                Ok(members)
             })
             .await?;
 
-        if role == ChannelRole::Admin {
-            Ok(members
-                .into_iter()
-                .map(|channel_member| proto::ChannelMember {
-                    role: channel_member.role.into(),
-                    user_id: channel_member.user_id.to_proto(),
-                    kind: if channel_member.accepted {
+        let mut users: Vec<proto::User> = Vec::with_capacity(members.len());
+
+        let members = members
+            .into_iter()
+            .map(|(member, user)| {
+                if let Some(user) = user {
+                    users.push(proto::User {
+                        id: user.id.to_proto(),
+                        avatar_url: format!(
+                            "https://github.com/{}.png?size=128",
+                            user.github_login
+                        ),
+                        github_login: user.github_login,
+                    })
+                }
+                proto::ChannelMember {
+                    role: member.role.into(),
+                    user_id: member.user_id.to_proto(),
+                    kind: if member.accepted {
                         Kind::Member
                     } else {
                         Kind::Invitee
                     }
                     .into(),
-                })
-                .collect())
-        } else {
-            return Ok(members
-                .into_iter()
-                .filter_map(|member| {
-                    if !member.accepted {
-                        return None;
-                    }
-                    Some(proto::ChannelMember {
-                        role: member.role.into(),
-                        user_id: member.user_id.to_proto(),
-                        kind: Kind::Member.into(),
-                    })
-                })
-                .collect());
-        }
-    }
-
-    async fn get_channel_participant_details_internal(
-        &self,
-        channel: &channel::Model,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<channel_member::Model>> {
-        Ok(channel_member::Entity::find()
-            .filter(channel_member::Column::ChannelId.eq(channel.root_id()))
-            .all(tx)
-            .await?)
-    }
+                }
+            })
+            .collect();
 
-    /// Returns the participants in the given channel.
-    pub async fn get_channel_participants(
-        &self,
-        channel: &channel::Model,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<UserId>> {
-        let participants = self
-            .get_channel_participant_details_internal(channel, tx)
-            .await?;
-        Ok(participants
-            .into_iter()
-            .map(|member| member.user_id)
-            .collect())
+        Ok((members, users))
     }
 
     /// Returns whether the given user is an admin in the specified channel.

crates/collab/src/db/queries/messages.rs 🔗

@@ -251,7 +251,7 @@ impl Database {
                 .await?;
 
             let mut is_participant = false;
-            let mut participant_connection_ids = Vec::new();
+            let mut participant_connection_ids = HashSet::default();
             let mut participant_user_ids = Vec::new();
             while let Some(row) = rows.next().await {
                 let row = row?;
@@ -259,7 +259,7 @@ impl Database {
                     is_participant = true;
                 }
                 participant_user_ids.push(row.user_id);
-                participant_connection_ids.push(row.connection());
+                participant_connection_ids.insert(row.connection());
             }
             drop(rows);
 
@@ -336,13 +336,9 @@ impl Database {
                 }
             }
 
-            let mut channel_members = self.get_channel_participants(&channel, &tx).await?;
-            channel_members.retain(|member| !participant_user_ids.contains(member));
-
             Ok(CreatedChannelMessage {
                 message_id,
                 participant_connection_ids,
-                channel_members,
                 notifications,
             })
         })

crates/collab/src/db/tests/channel_tests.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     db::{
         tests::{channel_tree, new_test_connection, new_test_user},
-        Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId,
+        Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
     },
     test_both_dbs,
 };
@@ -40,15 +40,15 @@ async fn test_channels(db: &Arc<Database>) {
         .await
         .unwrap();
 
-    let mut members = db
-        .transaction(|tx| async move {
-            let channel = db.get_channel_internal(replace_id, &tx).await?;
-            db.get_channel_participants(&channel, &tx).await
-        })
+    let (members, _) = db
+        .get_channel_participant_details(replace_id, "", 10, a_id)
         .await
         .unwrap();
-    members.sort();
-    assert_eq!(members, &[a_id, b_id]);
+    let ids = members
+        .into_iter()
+        .map(|m| UserId::from_proto(m.user_id))
+        .collect::<Vec<_>>();
+    assert_eq!(ids, &[a_id, b_id]);
 
     let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
     let cargo_id = db.create_sub_channel("cargo", rust_id, a_id).await.unwrap();
@@ -195,8 +195,8 @@ async fn test_channel_invites(db: &Arc<Database>) {
 
     assert_eq!(user_3_invites, &[channel_1_1]);
 
-    let mut members = db
-        .get_channel_participant_details(channel_1_1, user_1)
+    let (mut members, _) = db
+        .get_channel_participant_details(channel_1_1, "", 100, user_1)
         .await
         .unwrap();
 
@@ -231,8 +231,8 @@ async fn test_channel_invites(db: &Arc<Database>) {
         .await
         .unwrap();
 
-    let members = db
-        .get_channel_participant_details(channel_1_3, user_1)
+    let (members, _) = db
+        .get_channel_participant_details(channel_1_3, "", 100, user_1)
         .await
         .unwrap();
     assert_eq!(
@@ -243,16 +243,16 @@ async fn test_channel_invites(db: &Arc<Database>) {
                 kind: proto::channel_member::Kind::Member.into(),
                 role: proto::ChannelRole::Admin.into(),
             },
-            proto::ChannelMember {
-                user_id: user_2.to_proto(),
-                kind: proto::channel_member::Kind::Member.into(),
-                role: proto::ChannelRole::Member.into(),
-            },
             proto::ChannelMember {
                 user_id: user_3.to_proto(),
                 kind: proto::channel_member::Kind::Invitee.into(),
                 role: proto::ChannelRole::Admin.into(),
             },
+            proto::ChannelMember {
+                user_id: user_2.to_proto(),
+                kind: proto::channel_member::Kind::Member.into(),
+                role: proto::ChannelRole::Member.into(),
+            },
         ]
     );
 }
@@ -482,8 +482,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
     .await
     .unwrap();
 
-    let mut members = db
-        .get_channel_participant_details(public_channel_id, admin)
+    let (mut members, _) = db
+        .get_channel_participant_details(public_channel_id, "", 100, admin)
         .await
         .unwrap();
 
@@ -557,8 +557,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
         .await
         .is_err());
 
-    let mut members = db
-        .get_channel_participant_details(public_channel_id, admin)
+    let (mut members, _) = db
+        .get_channel_participant_details(public_channel_id, "", 100, admin)
         .await
         .unwrap();
 
@@ -594,8 +594,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
         .unwrap();
 
     // currently people invited to parent channels are not shown here
-    let mut members = db
-        .get_channel_participant_details(public_channel_id, admin)
+    let (mut members, _) = db
+        .get_channel_participant_details(public_channel_id, "", 100, admin)
         .await
         .unwrap();
 
@@ -663,8 +663,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
     .await
     .unwrap();
 
-    let mut members = db
-        .get_channel_participant_details(public_channel_id, admin)
+    let (mut members, _) = db
+        .get_channel_participant_details(public_channel_id, "", 100, admin)
         .await
         .unwrap();
 

crates/collab/src/rpc.rs 🔗

@@ -3683,10 +3683,15 @@ async fn get_channel_members(
 ) -> Result<()> {
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
-    let members = db
-        .get_channel_participant_details(channel_id, session.user_id())
+    let limit = if request.limit == 0 {
+        u16::MAX as u64
+    } else {
+        request.limit
+    };
+    let (members, users) = db
+        .get_channel_participant_details(channel_id, &request.query, limit, session.user_id())
         .await?;
-    response.send(proto::GetChannelMembersResponse { members })?;
+    response.send(proto::GetChannelMembersResponse { members, users })?;
     Ok(())
 }
 
@@ -3886,13 +3891,13 @@ async fn update_channel_buffer(
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
 
-    let (collaborators, non_collaborators, epoch, version) = db
+    let (collaborators, epoch, version) = db
         .update_channel_buffer(channel_id, session.user_id(), &request.operations)
         .await?;
 
     channel_buffer_updated(
         session.connection_id,
-        collaborators,
+        collaborators.clone(),
         &proto::UpdateChannelBuffer {
             channel_id: channel_id.to_proto(),
             operations: request.operations,
@@ -3902,25 +3907,29 @@ async fn update_channel_buffer(
 
     let pool = &*session.connection_pool().await;
 
-    broadcast(
-        None,
-        non_collaborators
-            .iter()
-            .flat_map(|user_id| pool.user_connection_ids(*user_id)),
-        |peer_id| {
-            session.peer.send(
-                peer_id,
-                proto::UpdateChannels {
-                    latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
-                        channel_id: channel_id.to_proto(),
-                        epoch: epoch as u64,
-                        version: version.clone(),
-                    }],
-                    ..Default::default()
-                },
-            )
-        },
-    );
+    let non_collaborators =
+        pool.channel_connection_ids(channel_id)
+            .filter_map(|(connection_id, _)| {
+                if collaborators.contains(&connection_id) {
+                    None
+                } else {
+                    Some(connection_id)
+                }
+            });
+
+    broadcast(None, non_collaborators, |peer_id| {
+        session.peer.send(
+            peer_id,
+            proto::UpdateChannels {
+                latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
+                    channel_id: channel_id.to_proto(),
+                    epoch: epoch as u64,
+                    version: version.clone(),
+                }],
+                ..Default::default()
+            },
+        )
+    });
 
     Ok(())
 }
@@ -4048,7 +4057,6 @@ async fn send_channel_message(
     let CreatedChannelMessage {
         message_id,
         participant_connection_ids,
-        channel_members,
         notifications,
     } = session
         .db()
@@ -4079,7 +4087,7 @@ async fn send_channel_message(
     };
     broadcast(
         Some(session.connection_id),
-        participant_connection_ids,
+        participant_connection_ids.clone(),
         |connection| {
             session.peer.send(
                 connection,
@@ -4095,24 +4103,27 @@ async fn send_channel_message(
     })?;
 
     let pool = &*session.connection_pool().await;
-    broadcast(
-        None,
-        channel_members
-            .iter()
-            .flat_map(|user_id| pool.user_connection_ids(*user_id)),
-        |peer_id| {
-            session.peer.send(
-                peer_id,
-                proto::UpdateChannels {
-                    latest_channel_message_ids: vec![proto::ChannelMessageId {
-                        channel_id: channel_id.to_proto(),
-                        message_id: message_id.to_proto(),
-                    }],
-                    ..Default::default()
-                },
-            )
-        },
-    );
+    let non_participants =
+        pool.channel_connection_ids(channel_id)
+            .filter_map(|(connection_id, _)| {
+                if participant_connection_ids.contains(&connection_id) {
+                    None
+                } else {
+                    Some(connection_id)
+                }
+            });
+    broadcast(None, non_participants, |peer_id| {
+        session.peer.send(
+            peer_id,
+            proto::UpdateChannels {
+                latest_channel_message_ids: vec![proto::ChannelMessageId {
+                    channel_id: channel_id.to_proto(),
+                    message_id: message_id.to_proto(),
+                }],
+                ..Default::default()
+            },
+        )
+    });
     send_notifications(pool, &session.peer, notifications);
 
     Ok(())

crates/collab/src/tests/channel_tests.rs 🔗

@@ -99,7 +99,7 @@ async fn test_core_channels(
         .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)
+            store.fuzzy_search_members(channel_a_id, "".to_string(), 10, cx)
         })
         .await
         .unwrap();

crates/collab_ui/src/collab_panel.rs 🔗

@@ -1569,11 +1569,28 @@ impl CollabPanel {
 
                     *pending_name = Some(channel_name.clone());
 
-                    self.channel_store
-                        .update(cx, |channel_store, cx| {
-                            channel_store.create_channel(&channel_name, *location, cx)
+                    let create = self.channel_store.update(cx, |channel_store, cx| {
+                        channel_store.create_channel(&channel_name, *location, cx)
+                    });
+                    if location.is_none() {
+                        cx.spawn(|this, mut cx| async move {
+                            let channel_id = create.await?;
+                            this.update(&mut cx, |this, cx| {
+                                this.show_channel_modal(
+                                    channel_id,
+                                    channel_modal::Mode::InviteMembers,
+                                    cx,
+                                )
+                            })
                         })
-                        .detach();
+                        .detach_and_prompt_err(
+                            "Failed to create channel",
+                            cx,
+                            |_, _| None,
+                        );
+                    } else {
+                        create.detach_and_prompt_err("Failed to create channel", cx, |_, _| None);
+                    }
                     cx.notify();
                 }
                 ChannelEditingState::Rename {
@@ -1859,12 +1876,8 @@ impl CollabPanel {
         let workspace = self.workspace.clone();
         let user_store = self.user_store.clone();
         let channel_store = self.channel_store.clone();
-        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 {
-            let members = members.await?;
             workspace.update(&mut cx, |workspace, cx| {
                 workspace.toggle_modal(cx, |cx| {
                     ChannelModal::new(
@@ -1872,7 +1885,6 @@ impl CollabPanel {
                         channel_store.clone(),
                         channel_id,
                         mode,
-                        members,
                         cx,
                     )
                 });

crates/collab_ui/src/collab_panel/channel_modal.rs 🔗

@@ -37,7 +37,6 @@ impl ChannelModal {
         channel_store: Model<ChannelStore>,
         channel_id: ChannelId,
         mode: Mode,
-        members: Vec<ChannelMembership>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
@@ -54,7 +53,8 @@ impl ChannelModal {
                     channel_id,
                     match_candidates: Vec::new(),
                     context_menu: None,
-                    members,
+                    members: Vec::new(),
+                    has_all_members: false,
                     mode,
                 },
                 cx,
@@ -78,37 +78,15 @@ impl ChannelModal {
     }
 
     fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.clone();
-        let channel_id = self.channel_id;
-        cx.spawn(|this, mut cx| async move {
-            if mode == Mode::ManageMembers {
-                let mut members = channel_store
-                    .update(&mut cx, |channel_store, cx| {
-                        channel_store.get_channel_member_details(channel_id, cx)
-                    })?
-                    .await?;
-
-                members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
-
-                this.update(&mut cx, |this, cx| {
-                    this.picker
-                        .update(cx, |picker, _| picker.delegate.members = members);
-                })?;
-            }
-
-            this.update(&mut cx, |this, cx| {
-                this.picker.update(cx, |picker, cx| {
-                    let delegate = &mut picker.delegate;
-                    delegate.mode = mode;
-                    delegate.selected_index = 0;
-                    picker.set_query("", cx);
-                    picker.update_matches(picker.query(cx), cx);
-                    cx.notify()
-                });
-                cx.notify()
-            })
-        })
-        .detach();
+        self.picker.update(cx, |picker, cx| {
+            let delegate = &mut picker.delegate;
+            delegate.mode = mode;
+            delegate.selected_index = 0;
+            picker.set_query("", cx);
+            picker.update_matches(picker.query(cx), cx);
+            cx.notify()
+        });
+        cx.notify()
     }
 
     fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
@@ -260,6 +238,7 @@ pub struct ChannelModalDelegate {
     mode: Mode,
     match_candidates: Vec<StringMatchCandidate>,
     members: Vec<ChannelMembership>,
+    has_all_members: bool,
     context_menu: Option<(View<ContextMenu>, Subscription)>,
 }
 
@@ -288,37 +267,59 @@ impl PickerDelegate for ChannelModalDelegate {
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
         match self.mode {
             Mode::ManageMembers => {
-                self.match_candidates.clear();
-                self.match_candidates
-                    .extend(self.members.iter().enumerate().map(|(id, member)| {
-                        StringMatchCandidate {
-                            id,
-                            string: member.user.github_login.clone(),
-                            char_bag: member.user.github_login.chars().collect(),
+                if self.has_all_members {
+                    self.match_candidates.clear();
+                    self.match_candidates
+                        .extend(self.members.iter().enumerate().map(|(id, member)| {
+                            StringMatchCandidate {
+                                id,
+                                string: member.user.github_login.clone(),
+                                char_bag: member.user.github_login.chars().collect(),
+                            }
+                        }));
+
+                    let matches = cx.background_executor().block(match_strings(
+                        &self.match_candidates,
+                        &query,
+                        true,
+                        usize::MAX,
+                        &Default::default(),
+                        cx.background_executor().clone(),
+                    ));
+
+                    cx.spawn(|picker, mut cx| async move {
+                        picker
+                            .update(&mut cx, |picker, cx| {
+                                let delegate = &mut picker.delegate;
+                                delegate.matching_member_indices.clear();
+                                delegate
+                                    .matching_member_indices
+                                    .extend(matches.into_iter().map(|m| m.candidate_id));
+                                cx.notify();
+                            })
+                            .ok();
+                    })
+                } else {
+                    let search_members = self.channel_store.update(cx, |store, cx| {
+                        store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx)
+                    });
+                    cx.spawn(|picker, mut cx| async move {
+                        async {
+                            let members = search_members.await?;
+                            picker.update(&mut cx, |picker, cx| {
+                                picker.delegate.has_all_members =
+                                    query == "" && members.len() < 100;
+                                picker.delegate.matching_member_indices =
+                                    (0..members.len()).collect();
+                                picker.delegate.members = members;
+                                cx.notify();
+                            })?;
+                            anyhow::Ok(())
                         }
-                    }));
-
-                let matches = cx.background_executor().block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    cx.background_executor().clone(),
-                ));
-
-                cx.spawn(|picker, mut cx| async move {
-                    picker
-                        .update(&mut cx, |picker, cx| {
-                            let delegate = &mut picker.delegate;
-                            delegate.matching_member_indices.clear();
-                            delegate
-                                .matching_member_indices
-                                .extend(matches.into_iter().map(|m| m.candidate_id));
-                            cx.notify();
-                        })
-                        .ok();
-                })
+                        .log_err()
+                        .await;
+                    })
+                }
             }
             Mode::InviteMembers => {
                 let search_users = self

crates/rpc/proto/zed.proto 🔗

@@ -1268,10 +1268,13 @@ message DeleteChannel {
 
 message GetChannelMembers {
     uint64 channel_id = 1;
+    string query = 2;
+    uint64 limit = 3;
 }
 
 message GetChannelMembersResponse {
     repeated ChannelMember members = 1;
+    repeated User users = 2;
 }
 
 message ChannelMember {