Add logged out collab panel (#3412)

Conrad Irwin created

Release Notes:

- N/A

Change summary

Cargo.lock                                           |    1 
crates/call2/src/call2.rs                            |    7 
crates/call2/src/room.rs                             |   13 
crates/collab_ui2/src/collab_panel.rs                | 1114 +++++++------
crates/collab_ui2/src/collab_panel/contact_finder.rs |  266 +-
crates/collab_ui2/src/collab_titlebar_item.rs        |   12 
crates/ui2/src/components/avatar.rs                  |    6 
crates/ui2/src/components/list.rs                    |   33 
crates/ui2/src/components/slot.rs                    |    4 
crates/zed2/Cargo.toml                               |    2 
crates/zed2/src/main.rs                              |    4 
11 files changed, 718 insertions(+), 744 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -11661,6 +11661,7 @@ dependencies = [
  "auto_update2",
  "backtrace",
  "call2",
+ "channel2",
  "chrono",
  "cli",
  "client2",

crates/call2/src/call2.rs 🔗

@@ -660,9 +660,12 @@ impl CallHandler for Call {
         self.active_call.as_ref().map(|call| {
             call.0.update(cx, |this, cx| {
                 this.room().map(|room| {
-                    room.update(cx, |this, cx| {
-                        this.toggle_mute(cx).log_err();
+                    let room = room.clone();
+                    cx.spawn(|_, mut cx| async move {
+                        room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
+                            .await
                     })
+                    .detach_and_log_err(cx);
                 })
             })
         });

crates/call2/src/room.rs 🔗

@@ -1,4 +1,7 @@
-use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant};
+use crate::{
+    call_settings::CallSettings,
+    participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
+};
 use anyhow::{anyhow, Result};
 use audio::{Audio, Sound};
 use client::{
@@ -18,6 +21,7 @@ use live_kit_client::{
 };
 use postage::{sink::Sink, stream::Stream, watch};
 use project::Project;
+use settings::Settings as _;
 use std::{future::Future, mem, sync::Arc, time::Duration};
 use util::{post_inc, ResultExt, TryFutureExt};
 
@@ -328,10 +332,8 @@ impl Room {
         }
     }
 
-    pub fn mute_on_join(_cx: &AppContext) -> bool {
-        // todo!() po: This should be uncommented, though then unmuting does not work
-        false
-        //CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
+    pub fn mute_on_join(cx: &AppContext) -> bool {
+        CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
     }
 
     fn from_join_response(
@@ -1265,7 +1267,6 @@ impl Room {
                     .ok_or_else(|| anyhow!("live-kit was not initialized"))?
                     .await
             };
-
             let publication = publish_track.await;
             this.upgrade()
                 .ok_or_else(|| anyhow!("room was dropped"))?

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1,5 +1,6 @@
+#![allow(unused)]
 // mod channel_modal;
-// mod contact_finder;
+mod contact_finder;
 
 // use crate::{
 //     channel_view::{self, ChannelView},
@@ -15,7 +16,7 @@
 //     proto::{self, PeerId},
 //     Client, Contact, User, UserStore,
 // };
-// use contact_finder::ContactFinder;
+use contact_finder::ContactFinder;
 // use context_menu::{ContextMenu, ContextMenuItem};
 // use db::kvp::KEY_VALUE_STORE;
 // use drag_and_drop::{DragAndDrop, Draggable};
@@ -155,22 +156,30 @@ actions!(
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
-use std::sync::Arc;
+use std::{iter::once, mem, sync::Arc};
 
-use client::{Client, Contact, UserStore};
+use call::ActiveCall;
+use channel::{Channel, ChannelId, ChannelStore};
+use client::{Client, Contact, User, UserStore};
 use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
+use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
-    Focusable, FocusableView, InteractiveElement, Model, ParentElement, Render, Styled, View,
-    ViewContext, VisualContext, WeakView,
+    actions, div, img, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
+    Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render,
+    RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
 };
 use project::Fs;
 use serde_derive::{Deserialize, Serialize};
 use settings::Settings;
-use ui::{h_stack, Avatar, Label};
-use util::ResultExt;
+use ui::{
+    h_stack, v_stack, Avatar, Button, Icon, IconButton, Label, List, ListHeader, ListItem, Tooltip,
+};
+use util::{maybe, ResultExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
+    notifications::NotifyResultExt,
     Workspace,
 };
 
@@ -268,26 +277,26 @@ pub fn init(cx: &mut AppContext) {
     //     );
 }
 
-// #[derive(Debug)]
-// pub enum ChannelEditingState {
-//     Create {
-//         location: Option<ChannelId>,
-//         pending_name: Option<String>,
-//     },
-//     Rename {
-//         location: ChannelId,
-//         pending_name: Option<String>,
-//     },
-// }
+#[derive(Debug)]
+pub enum ChannelEditingState {
+    Create {
+        location: Option<ChannelId>,
+        pending_name: Option<String>,
+    },
+    Rename {
+        location: ChannelId,
+        pending_name: Option<String>,
+    },
+}
 
-// impl ChannelEditingState {
-//     fn pending_name(&self) -> Option<&str> {
-//         match self {
-//             ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
-//             ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
-//         }
-//     }
-// }
+impl ChannelEditingState {
+    fn pending_name(&self) -> Option<&str> {
+        match self {
+            ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
+            ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+        }
+    }
+}
 
 pub struct CollabPanel {
     width: Option<f32>,
@@ -296,22 +305,22 @@ pub struct CollabPanel {
     // channel_clipboard: Option<ChannelMoveClipboard>,
     // pending_serialization: Task<Option<()>>,
     // context_menu: ViewHandle<ContextMenu>,
-    // filter_editor: ViewHandle<Editor>,
+    filter_editor: View<Editor>,
     // channel_name_editor: ViewHandle<Editor>,
-    // channel_editing_state: Option<ChannelEditingState>,
-    // entries: Vec<ListEntry>,
+    channel_editing_state: Option<ChannelEditingState>,
+    entries: Vec<ListEntry>,
     // selection: Option<usize>,
+    channel_store: Model<ChannelStore>,
     user_store: Model<UserStore>,
-    _client: Arc<Client>,
-    // channel_store: ModelHandle<ChannelStore>,
+    client: Arc<Client>,
     // project: ModelHandle<Project>,
-    // match_candidates: Vec<StringMatchCandidate>,
+    match_candidates: Vec<StringMatchCandidate>,
     // list_state: ListState<Self>,
-    // subscriptions: Vec<Subscription>,
-    // collapsed_sections: Vec<Section>,
-    // collapsed_channels: Vec<ChannelId>,
+    subscriptions: Vec<Subscription>,
+    collapsed_sections: Vec<Section>,
+    collapsed_channels: Vec<ChannelId>,
     // drag_target_channel: ChannelDragTarget,
-    _workspace: WeakView<Workspace>,
+    workspace: WeakView<Workspace>,
     // context_menu_on_selected: bool,
 }
 
@@ -335,58 +344,58 @@ struct SerializedCollabPanel {
 //     Dismissed,
 // }
 
-// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-// enum Section {
-//     ActiveCall,
-//     Channels,
-//     ChannelInvites,
-//     ContactRequests,
-//     Contacts,
-//     Online,
-//     Offline,
-// }
+#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+enum Section {
+    ActiveCall,
+    Channels,
+    ChannelInvites,
+    ContactRequests,
+    Contacts,
+    Online,
+    Offline,
+}
 
-// #[derive(Clone, Debug)]
-// enum ListEntry {
-//     Header(Section),
-//     CallParticipant {
-//         user: Arc<User>,
-//         peer_id: Option<PeerId>,
-//         is_pending: bool,
-//     },
-//     ParticipantProject {
-//         project_id: u64,
-//         worktree_root_names: Vec<String>,
-//         host_user_id: u64,
-//         is_last: bool,
-//     },
-//     ParticipantScreen {
-//         peer_id: Option<PeerId>,
-//         is_last: bool,
-//     },
-//     IncomingRequest(Arc<User>),
-//     OutgoingRequest(Arc<User>),
-//     ChannelInvite(Arc<Channel>),
-//     Channel {
-//         channel: Arc<Channel>,
-//         depth: usize,
-//         has_children: bool,
-//     },
-//     ChannelNotes {
-//         channel_id: ChannelId,
-//     },
-//     ChannelChat {
-//         channel_id: ChannelId,
-//     },
-//     ChannelEditor {
-//         depth: usize,
-//     },
-//     Contact {
-//         contact: Arc<Contact>,
-//         calling: bool,
-//     },
-//     ContactPlaceholder,
-// }
+#[derive(Clone, Debug)]
+enum ListEntry {
+    Header(Section),
+    //     CallParticipant {
+    //         user: Arc<User>,
+    //         peer_id: Option<PeerId>,
+    //         is_pending: bool,
+    //     },
+    //     ParticipantProject {
+    //         project_id: u64,
+    //         worktree_root_names: Vec<String>,
+    //         host_user_id: u64,
+    //         is_last: bool,
+    //     },
+    //     ParticipantScreen {
+    //         peer_id: Option<PeerId>,
+    //         is_last: bool,
+    //     },
+    IncomingRequest(Arc<User>),
+    OutgoingRequest(Arc<User>),
+    //     ChannelInvite(Arc<Channel>),
+    Channel {
+        channel: Arc<Channel>,
+        depth: usize,
+        has_children: bool,
+    },
+    //     ChannelNotes {
+    //         channel_id: ChannelId,
+    //     },
+    //     ChannelChat {
+    //         channel_id: ChannelId,
+    //     },
+    ChannelEditor {
+        depth: usize,
+    },
+    Contact {
+        contact: Arc<Contact>,
+        calling: bool,
+    },
+    ContactPlaceholder,
+}
 
 // impl Entity for CollabPanel {
 //     type Event = Event;
@@ -397,16 +406,11 @@ impl CollabPanel {
         cx.build_view(|cx| {
             //             let view_id = cx.view_id();
 
-            //             let filter_editor = cx.add_view(|cx| {
-            //                 let mut editor = Editor::single_line(
-            //                     Some(Arc::new(|theme| {
-            //                         theme.collab_panel.user_query_editor.clone()
-            //                     })),
-            //                     cx,
-            //                 );
-            //                 editor.set_placeholder_text("Filter channels, contacts", cx);
-            //                 editor
-            //             });
+            let filter_editor = cx.build_view(|cx| {
+                let mut editor = Editor::single_line(cx);
+                editor.set_placeholder_text("Filter channels, contacts", cx);
+                editor
+            });
 
             //             cx.subscribe(&filter_editor, |this, _, event, cx| {
             //                 if let editor::Event::BufferEdited = event {
@@ -585,7 +589,7 @@ impl CollabPanel {
             //                     }
             //                 });
 
-            let this = Self {
+            let mut this = Self {
                 width: None,
                 focus_handle: cx.focus_handle(),
                 //                 channel_clipboard: None,
@@ -593,25 +597,25 @@ impl CollabPanel {
                 //                 pending_serialization: Task::ready(None),
                 //                 context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
                 //                 channel_name_editor,
-                //                 filter_editor,
-                //                 entries: Vec::default(),
-                //                 channel_editing_state: None,
+                filter_editor,
+                entries: Vec::default(),
+                channel_editing_state: None,
                 //                 selection: None,
+                channel_store: ChannelStore::global(cx),
                 user_store: workspace.user_store().clone(),
-                //                 channel_store: ChannelStore::global(cx),
                 //                 project: workspace.project().clone(),
-                //                 subscriptions: Vec::default(),
-                //                 match_candidates: Vec::default(),
-                //                 collapsed_sections: vec![Section::Offline],
-                //                 collapsed_channels: Vec::default(),
-                _workspace: workspace.weak_handle(),
-                _client: workspace.app_state().client.clone(),
+                subscriptions: Vec::default(),
+                match_candidates: Vec::default(),
+                collapsed_sections: vec![Section::Offline],
+                collapsed_channels: Vec::default(),
+                workspace: workspace.weak_handle(),
+                client: workspace.app_state().client.clone(),
                 //                 context_menu_on_selected: true,
                 //                 drag_target_channel: ChannelDragTarget::None,
                 //                 list_state,
             };
 
-            //             this.update_entries(false, cx);
+            this.update_entries(false, cx);
 
             //             // Update the dock position when the setting changes.
             //             let mut old_dock_position = this.position(cx);
@@ -628,10 +632,10 @@ impl CollabPanel {
             //                 );
 
             //             let active_call = ActiveCall::global(cx);
-            //             this.subscriptions
-            //                 .push(cx.observe(&this.user_store, |this, _, cx| {
-            //                     this.update_entries(true, cx)
-            //                 }));
+            this.subscriptions
+                .push(cx.observe(&this.user_store, |this, _, cx| {
+                    this.update_entries(true, cx)
+                }));
             //             this.subscriptions
             //                 .push(cx.observe(&this.channel_store, |this, _, cx| {
             //                     this.update_entries(true, cx)
@@ -720,449 +724,449 @@ impl CollabPanel {
     //         );
     //     }
 
-    //     fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
-    //         let channel_store = self.channel_store.read(cx);
-    //         let user_store = self.user_store.read(cx);
-    //         let query = self.filter_editor.read(cx).text(cx);
-    //         let executor = cx.background().clone();
-
-    //         let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
-    //         let old_entries = mem::take(&mut self.entries);
-    //         let mut scroll_to_top = false;
-
-    //         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
-    //             self.entries.push(ListEntry::Header(Section::ActiveCall));
-    //             if !old_entries
-    //                 .iter()
-    //                 .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
-    //             {
-    //                 scroll_to_top = true;
-    //             }
-
-    //             if !self.collapsed_sections.contains(&Section::ActiveCall) {
-    //                 let room = room.read(cx);
-
-    //                 if let Some(channel_id) = room.channel_id() {
-    //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
-    //                     self.entries.push(ListEntry::ChannelChat { channel_id })
-    //                 }
-
-    //                 // Populate the active user.
-    //                 if let Some(user) = user_store.current_user() {
-    //                     self.match_candidates.clear();
-    //                     self.match_candidates.push(StringMatchCandidate {
-    //                         id: 0,
-    //                         string: user.github_login.clone(),
-    //                         char_bag: user.github_login.chars().collect(),
-    //                     });
-    //                     let matches = executor.block(match_strings(
-    //                         &self.match_candidates,
-    //                         &query,
-    //                         true,
-    //                         usize::MAX,
-    //                         &Default::default(),
-    //                         executor.clone(),
-    //                     ));
-    //                     if !matches.is_empty() {
-    //                         let user_id = user.id;
-    //                         self.entries.push(ListEntry::CallParticipant {
-    //                             user,
-    //                             peer_id: None,
-    //                             is_pending: false,
-    //                         });
-    //                         let mut projects = room.local_participant().projects.iter().peekable();
-    //                         while let Some(project) = projects.next() {
-    //                             self.entries.push(ListEntry::ParticipantProject {
-    //                                 project_id: project.id,
-    //                                 worktree_root_names: project.worktree_root_names.clone(),
-    //                                 host_user_id: user_id,
-    //                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
-    //                             });
-    //                         }
-    //                         if room.is_screen_sharing() {
-    //                             self.entries.push(ListEntry::ParticipantScreen {
-    //                                 peer_id: None,
-    //                                 is_last: true,
-    //                             });
-    //                         }
-    //                     }
-    //                 }
-
-    //                 // Populate remote participants.
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(room.remote_participants().iter().map(|(_, participant)| {
-    //                         StringMatchCandidate {
-    //                             id: participant.user.id as usize,
-    //                             string: participant.user.github_login.clone(),
-    //                             char_bag: participant.user.github_login.chars().collect(),
-    //                         }
-    //                     }));
-    //                 let matches = executor.block(match_strings(
-    //                     &self.match_candidates,
-    //                     &query,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 for mat in matches {
-    //                     let user_id = mat.candidate_id as u64;
-    //                     let participant = &room.remote_participants()[&user_id];
-    //                     self.entries.push(ListEntry::CallParticipant {
-    //                         user: participant.user.clone(),
-    //                         peer_id: Some(participant.peer_id),
-    //                         is_pending: false,
-    //                     });
-    //                     let mut projects = participant.projects.iter().peekable();
-    //                     while let Some(project) = projects.next() {
-    //                         self.entries.push(ListEntry::ParticipantProject {
-    //                             project_id: project.id,
-    //                             worktree_root_names: project.worktree_root_names.clone(),
-    //                             host_user_id: participant.user.id,
-    //                             is_last: projects.peek().is_none()
-    //                                 && participant.video_tracks.is_empty(),
-    //                         });
-    //                     }
-    //                     if !participant.video_tracks.is_empty() {
-    //                         self.entries.push(ListEntry::ParticipantScreen {
-    //                             peer_id: Some(participant.peer_id),
-    //                             is_last: true,
-    //                         });
-    //                     }
-    //                 }
-
-    //                 // Populate pending participants.
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(room.pending_participants().iter().enumerate().map(
-    //                         |(id, participant)| StringMatchCandidate {
-    //                             id,
-    //                             string: participant.github_login.clone(),
-    //                             char_bag: participant.github_login.chars().collect(),
-    //                         },
-    //                     ));
-    //                 let matches = executor.block(match_strings(
-    //                     &self.match_candidates,
-    //                     &query,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 self.entries
-    //                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
-    //                         user: room.pending_participants()[mat.candidate_id].clone(),
-    //                         peer_id: None,
-    //                         is_pending: true,
-    //                     }));
-    //             }
-    //         }
-
-    //         let mut request_entries = Vec::new();
-
-    //         if cx.has_flag::<ChannelsAlpha>() {
-    //             self.entries.push(ListEntry::Header(Section::Channels));
-
-    //             if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(channel_store.ordered_channels().enumerate().map(
-    //                         |(ix, (_, channel))| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: channel.name.clone(),
-    //                             char_bag: channel.name.chars().collect(),
-    //                         },
-    //                     ));
-    //                 let matches = executor.block(match_strings(
-    //                     &self.match_candidates,
-    //                     &query,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 if let Some(state) = &self.channel_editing_state {
-    //                     if matches!(state, ChannelEditingState::Create { location: None, .. }) {
-    //                         self.entries.push(ListEntry::ChannelEditor { depth: 0 });
-    //                     }
-    //                 }
-    //                 let mut collapse_depth = None;
-    //                 for mat in matches {
-    //                     let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
-    //                     let depth = channel.parent_path.len();
-
-    //                     if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
-    //                         collapse_depth = Some(depth);
-    //                     } else if let Some(collapsed_depth) = collapse_depth {
-    //                         if depth > collapsed_depth {
-    //                             continue;
-    //                         }
-    //                         if self.is_channel_collapsed(channel.id) {
-    //                             collapse_depth = Some(depth);
-    //                         } else {
-    //                             collapse_depth = None;
-    //                         }
-    //                     }
-
-    //                     let has_children = channel_store
-    //                         .channel_at_index(mat.candidate_id + 1)
-    //                         .map_or(false, |next_channel| {
-    //                             next_channel.parent_path.ends_with(&[channel.id])
-    //                         });
-
-    //                     match &self.channel_editing_state {
-    //                         Some(ChannelEditingState::Create {
-    //                             location: parent_id,
-    //                             ..
-    //                         }) if *parent_id == Some(channel.id) => {
-    //                             self.entries.push(ListEntry::Channel {
-    //                                 channel: channel.clone(),
-    //                                 depth,
-    //                                 has_children: false,
-    //                             });
-    //                             self.entries
-    //                                 .push(ListEntry::ChannelEditor { depth: depth + 1 });
-    //                         }
-    //                         Some(ChannelEditingState::Rename {
-    //                             location: parent_id,
-    //                             ..
-    //                         }) if parent_id == &channel.id => {
-    //                             self.entries.push(ListEntry::ChannelEditor { depth });
-    //                         }
-    //                         _ => {
-    //                             self.entries.push(ListEntry::Channel {
-    //                                 channel: channel.clone(),
-    //                                 depth,
-    //                                 has_children,
-    //                             });
-    //                         }
-    //                     }
-    //                 }
-    //             }
-
-    //             let channel_invites = channel_store.channel_invitations();
-    //             if !channel_invites.is_empty() {
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
-    //                         StringMatchCandidate {
-    //                             id: ix,
-    //                             string: channel.name.clone(),
-    //                             char_bag: channel.name.chars().collect(),
-    //                         }
-    //                     }));
-    //                 let matches = executor.block(match_strings(
-    //                     &self.match_candidates,
-    //                     &query,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 request_entries.extend(matches.iter().map(|mat| {
-    //                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
-    //                 }));
-
-    //                 if !request_entries.is_empty() {
-    //                     self.entries
-    //                         .push(ListEntry::Header(Section::ChannelInvites));
-    //                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
-    //                         self.entries.append(&mut request_entries);
-    //                     }
-    //                 }
-    //             }
-    //         }
-
-    //         self.entries.push(ListEntry::Header(Section::Contacts));
-
-    //         request_entries.clear();
-    //         let incoming = user_store.incoming_contact_requests();
-    //         if !incoming.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     incoming
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, user)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: user.github_login.clone(),
-    //                             char_bag: user.github_login.chars().collect(),
-    //                         }),
-    //                 );
-    //             let matches = executor.block(match_strings(
-    //                 &self.match_candidates,
-    //                 &query,
-    //                 true,
-    //                 usize::MAX,
-    //                 &Default::default(),
-    //                 executor.clone(),
-    //             ));
-    //             request_entries.extend(
-    //                 matches
-    //                     .iter()
-    //                     .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
-    //             );
-    //         }
-
-    //         let outgoing = user_store.outgoing_contact_requests();
-    //         if !outgoing.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     outgoing
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, user)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: user.github_login.clone(),
-    //                             char_bag: user.github_login.chars().collect(),
-    //                         }),
-    //                 );
-    //             let matches = executor.block(match_strings(
-    //                 &self.match_candidates,
-    //                 &query,
-    //                 true,
-    //                 usize::MAX,
-    //                 &Default::default(),
-    //                 executor.clone(),
-    //             ));
-    //             request_entries.extend(
-    //                 matches
-    //                     .iter()
-    //                     .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
-    //             );
-    //         }
-
-    //         if !request_entries.is_empty() {
-    //             self.entries
-    //                 .push(ListEntry::Header(Section::ContactRequests));
-    //             if !self.collapsed_sections.contains(&Section::ContactRequests) {
-    //                 self.entries.append(&mut request_entries);
-    //             }
-    //         }
-
-    //         let contacts = user_store.contacts();
-    //         if !contacts.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     contacts
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, contact)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: contact.user.github_login.clone(),
-    //                             char_bag: contact.user.github_login.chars().collect(),
-    //                         }),
-    //                 );
-
-    //             let matches = executor.block(match_strings(
-    //                 &self.match_candidates,
-    //                 &query,
-    //                 true,
-    //                 usize::MAX,
-    //                 &Default::default(),
-    //                 executor.clone(),
-    //             ));
-
-    //             let (online_contacts, offline_contacts) = matches
-    //                 .iter()
-    //                 .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-
-    //             for (matches, section) in [
-    //                 (online_contacts, Section::Online),
-    //                 (offline_contacts, Section::Offline),
-    //             ] {
-    //                 if !matches.is_empty() {
-    //                     self.entries.push(ListEntry::Header(section));
-    //                     if !self.collapsed_sections.contains(&section) {
-    //                         let active_call = &ActiveCall::global(cx).read(cx);
-    //                         for mat in matches {
-    //                             let contact = &contacts[mat.candidate_id];
-    //                             self.entries.push(ListEntry::Contact {
-    //                                 contact: contact.clone(),
-    //                                 calling: active_call.pending_invites().contains(&contact.user.id),
-    //                             });
-    //                         }
-    //                     }
-    //                 }
-    //             }
-    //         }
-
-    //         if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
-    //             self.entries.push(ListEntry::ContactPlaceholder);
-    //         }
-
-    //         if select_same_item {
-    //             if let Some(prev_selected_entry) = prev_selected_entry {
-    //                 self.selection.take();
-    //                 for (ix, entry) in self.entries.iter().enumerate() {
-    //                     if *entry == prev_selected_entry {
-    //                         self.selection = Some(ix);
-    //                         break;
-    //                     }
-    //                 }
-    //             }
-    //         } else {
-    //             self.selection = self.selection.and_then(|prev_selection| {
-    //                 if self.entries.is_empty() {
-    //                     None
-    //                 } else {
-    //                     Some(prev_selection.min(self.entries.len() - 1))
-    //                 }
-    //             });
-    //         }
-
-    //         let old_scroll_top = self.list_state.logical_scroll_top();
-
-    //         self.list_state.reset(self.entries.len());
+    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.read(cx);
+        let user_store = self.user_store.read(cx);
+        let query = self.filter_editor.read(cx).text(cx);
+        let executor = cx.background_executor().clone();
+
+        // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
+        let _old_entries = mem::take(&mut self.entries);
+        //         let mut scroll_to_top = false;
+
+        //         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+        //             self.entries.push(ListEntry::Header(Section::ActiveCall));
+        //             if !old_entries
+        //                 .iter()
+        //                 .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+        //             {
+        //                 scroll_to_top = true;
+        //             }
+
+        //             if !self.collapsed_sections.contains(&Section::ActiveCall) {
+        //                 let room = room.read(cx);
+
+        //                 if let Some(channel_id) = room.channel_id() {
+        //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
+        //                     self.entries.push(ListEntry::ChannelChat { channel_id })
+        //                 }
+
+        //                 // Populate the active user.
+        //                 if let Some(user) = user_store.current_user() {
+        //                     self.match_candidates.clear();
+        //                     self.match_candidates.push(StringMatchCandidate {
+        //                         id: 0,
+        //                         string: user.github_login.clone(),
+        //                         char_bag: user.github_login.chars().collect(),
+        //                     });
+        //                     let matches = executor.block(match_strings(
+        //                         &self.match_candidates,
+        //                         &query,
+        //                         true,
+        //                         usize::MAX,
+        //                         &Default::default(),
+        //                         executor.clone(),
+        //                     ));
+        //                     if !matches.is_empty() {
+        //                         let user_id = user.id;
+        //                         self.entries.push(ListEntry::CallParticipant {
+        //                             user,
+        //                             peer_id: None,
+        //                             is_pending: false,
+        //                         });
+        //                         let mut projects = room.local_participant().projects.iter().peekable();
+        //                         while let Some(project) = projects.next() {
+        //                             self.entries.push(ListEntry::ParticipantProject {
+        //                                 project_id: project.id,
+        //                                 worktree_root_names: project.worktree_root_names.clone(),
+        //                                 host_user_id: user_id,
+        //                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+        //                             });
+        //                         }
+        //                         if room.is_screen_sharing() {
+        //                             self.entries.push(ListEntry::ParticipantScreen {
+        //                                 peer_id: None,
+        //                                 is_last: true,
+        //                             });
+        //                         }
+        //                     }
+        //                 }
+
+        //                 // Populate remote participants.
+        //                 self.match_candidates.clear();
+        //                 self.match_candidates
+        //                     .extend(room.remote_participants().iter().map(|(_, participant)| {
+        //                         StringMatchCandidate {
+        //                             id: participant.user.id as usize,
+        //                             string: participant.user.github_login.clone(),
+        //                             char_bag: participant.user.github_login.chars().collect(),
+        //                         }
+        //                     }));
+        //                 let matches = executor.block(match_strings(
+        //                     &self.match_candidates,
+        //                     &query,
+        //                     true,
+        //                     usize::MAX,
+        //                     &Default::default(),
+        //                     executor.clone(),
+        //                 ));
+        //                 for mat in matches {
+        //                     let user_id = mat.candidate_id as u64;
+        //                     let participant = &room.remote_participants()[&user_id];
+        //                     self.entries.push(ListEntry::CallParticipant {
+        //                         user: participant.user.clone(),
+        //                         peer_id: Some(participant.peer_id),
+        //                         is_pending: false,
+        //                     });
+        //                     let mut projects = participant.projects.iter().peekable();
+        //                     while let Some(project) = projects.next() {
+        //                         self.entries.push(ListEntry::ParticipantProject {
+        //                             project_id: project.id,
+        //                             worktree_root_names: project.worktree_root_names.clone(),
+        //                             host_user_id: participant.user.id,
+        //                             is_last: projects.peek().is_none()
+        //                                 && participant.video_tracks.is_empty(),
+        //                         });
+        //                     }
+        //                     if !participant.video_tracks.is_empty() {
+        //                         self.entries.push(ListEntry::ParticipantScreen {
+        //                             peer_id: Some(participant.peer_id),
+        //                             is_last: true,
+        //                         });
+        //                     }
+        //                 }
+
+        //                 // Populate pending participants.
+        //                 self.match_candidates.clear();
+        //                 self.match_candidates
+        //                     .extend(room.pending_participants().iter().enumerate().map(
+        //                         |(id, participant)| StringMatchCandidate {
+        //                             id,
+        //                             string: participant.github_login.clone(),
+        //                             char_bag: participant.github_login.chars().collect(),
+        //                         },
+        //                     ));
+        //                 let matches = executor.block(match_strings(
+        //                     &self.match_candidates,
+        //                     &query,
+        //                     true,
+        //                     usize::MAX,
+        //                     &Default::default(),
+        //                     executor.clone(),
+        //                 ));
+        //                 self.entries
+        //                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+        //                         user: room.pending_participants()[mat.candidate_id].clone(),
+        //                         peer_id: None,
+        //                         is_pending: true,
+        //                     }));
+        //             }
+        //         }
+
+        let mut request_entries = Vec::new();
+
+        if cx.has_flag::<ChannelsAlpha>() {
+            self.entries.push(ListEntry::Header(Section::Channels));
+
+            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(channel_store.ordered_channels().enumerate().map(
+                        |(ix, (_, channel))| StringMatchCandidate {
+                            id: ix,
+                            string: channel.name.clone(),
+                            char_bag: channel.name.chars().collect(),
+                        },
+                    ));
+                let matches = executor.block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                if let Some(state) = &self.channel_editing_state {
+                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
+                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
+                    }
+                }
+                let mut collapse_depth = None;
+                for mat in matches {
+                    let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+                    let depth = channel.parent_path.len();
+
+                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+                        collapse_depth = Some(depth);
+                    } else if let Some(collapsed_depth) = collapse_depth {
+                        if depth > collapsed_depth {
+                            continue;
+                        }
+                        if self.is_channel_collapsed(channel.id) {
+                            collapse_depth = Some(depth);
+                        } else {
+                            collapse_depth = None;
+                        }
+                    }
+
+                    let has_children = channel_store
+                        .channel_at_index(mat.candidate_id + 1)
+                        .map_or(false, |next_channel| {
+                            next_channel.parent_path.ends_with(&[channel.id])
+                        });
+
+                    match &self.channel_editing_state {
+                        Some(ChannelEditingState::Create {
+                            location: parent_id,
+                            ..
+                        }) if *parent_id == Some(channel.id) => {
+                            self.entries.push(ListEntry::Channel {
+                                channel: channel.clone(),
+                                depth,
+                                has_children: false,
+                            });
+                            self.entries
+                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
+                        }
+                        Some(ChannelEditingState::Rename {
+                            location: parent_id,
+                            ..
+                        }) if parent_id == &channel.id => {
+                            self.entries.push(ListEntry::ChannelEditor { depth });
+                        }
+                        _ => {
+                            self.entries.push(ListEntry::Channel {
+                                channel: channel.clone(),
+                                depth,
+                                has_children,
+                            });
+                        }
+                    }
+                }
+            }
 
-    //         if scroll_to_top {
-    //             self.list_state.scroll_to(ListOffset::default());
-    //         } else {
-    //             // Attempt to maintain the same scroll position.
-    //             if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
-    //                 let new_scroll_top = self
-    //                     .entries
-    //                     .iter()
-    //                     .position(|entry| entry == old_top_entry)
-    //                     .map(|item_ix| ListOffset {
-    //                         item_ix,
-    //                         offset_in_item: old_scroll_top.offset_in_item,
-    //                     })
-    //                     .or_else(|| {
-    //                         let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
-    //                         let item_ix = self
-    //                             .entries
-    //                             .iter()
-    //                             .position(|entry| entry == entry_after_old_top)?;
-    //                         Some(ListOffset {
-    //                             item_ix,
-    //                             offset_in_item: 0.,
-    //                         })
-    //                     })
-    //                     .or_else(|| {
-    //                         let entry_before_old_top =
-    //                             old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
-    //                         let item_ix = self
-    //                             .entries
-    //                             .iter()
-    //                             .position(|entry| entry == entry_before_old_top)?;
-    //                         Some(ListOffset {
-    //                             item_ix,
-    //                             offset_in_item: 0.,
-    //                         })
-    //                     });
+            //             let channel_invites = channel_store.channel_invitations();
+            //             if !channel_invites.is_empty() {
+            //                 self.match_candidates.clear();
+            //                 self.match_candidates
+            //                     .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+            //                         StringMatchCandidate {
+            //                             id: ix,
+            //                             string: channel.name.clone(),
+            //                             char_bag: channel.name.chars().collect(),
+            //                         }
+            //                     }));
+            //                 let matches = executor.block(match_strings(
+            //                     &self.match_candidates,
+            //                     &query,
+            //                     true,
+            //                     usize::MAX,
+            //                     &Default::default(),
+            //                     executor.clone(),
+            //                 ));
+            //                 request_entries.extend(matches.iter().map(|mat| {
+            //                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+            //                 }));
 
-    //                 self.list_state
-    //                     .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
-    //             }
-    //         }
+            //                 if !request_entries.is_empty() {
+            //                     self.entries
+            //                         .push(ListEntry::Header(Section::ChannelInvites));
+            //                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+            //                         self.entries.append(&mut request_entries);
+            //                     }
+            //                 }
+            //             }
+        }
+
+        self.entries.push(ListEntry::Header(Section::Contacts));
+
+        request_entries.clear();
+        let incoming = user_store.incoming_contact_requests();
+        if !incoming.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    incoming
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, user)| StringMatchCandidate {
+                            id: ix,
+                            string: user.github_login.clone(),
+                            char_bag: user.github_login.chars().collect(),
+                        }),
+                );
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+            request_entries.extend(
+                matches
+                    .iter()
+                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
+            );
+        }
+
+        let outgoing = user_store.outgoing_contact_requests();
+        if !outgoing.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    outgoing
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, user)| StringMatchCandidate {
+                            id: ix,
+                            string: user.github_login.clone(),
+                            char_bag: user.github_login.chars().collect(),
+                        }),
+                );
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+            request_entries.extend(
+                matches
+                    .iter()
+                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+            );
+        }
+
+        if !request_entries.is_empty() {
+            self.entries
+                .push(ListEntry::Header(Section::ContactRequests));
+            if !self.collapsed_sections.contains(&Section::ContactRequests) {
+                self.entries.append(&mut request_entries);
+            }
+        }
+
+        let contacts = user_store.contacts();
+        if !contacts.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    contacts
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, contact)| StringMatchCandidate {
+                            id: ix,
+                            string: contact.user.github_login.clone(),
+                            char_bag: contact.user.github_login.chars().collect(),
+                        }),
+                );
+
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+
+            let (online_contacts, offline_contacts) = matches
+                .iter()
+                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+            for (matches, section) in [
+                (online_contacts, Section::Online),
+                (offline_contacts, Section::Offline),
+            ] {
+                if !matches.is_empty() {
+                    self.entries.push(ListEntry::Header(section));
+                    if !self.collapsed_sections.contains(&section) {
+                        let active_call = &ActiveCall::global(cx).read(cx);
+                        for mat in matches {
+                            let contact = &contacts[mat.candidate_id];
+                            self.entries.push(ListEntry::Contact {
+                                contact: contact.clone(),
+                                calling: active_call.pending_invites().contains(&contact.user.id),
+                            });
+                        }
+                    }
+                }
+            }
+        }
+
+        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
+            self.entries.push(ListEntry::ContactPlaceholder);
+        }
+
+        //         if select_same_item {
+        //             if let Some(prev_selected_entry) = prev_selected_entry {
+        //                 self.selection.take();
+        //                 for (ix, entry) in self.entries.iter().enumerate() {
+        //                     if *entry == prev_selected_entry {
+        //                         self.selection = Some(ix);
+        //                         break;
+        //                     }
+        //                 }
+        //             }
+        //         } else {
+        //             self.selection = self.selection.and_then(|prev_selection| {
+        //                 if self.entries.is_empty() {
+        //                     None
+        //                 } else {
+        //                     Some(prev_selection.min(self.entries.len() - 1))
+        //                 }
+        //             });
+        //         }
+
+        //         let old_scroll_top = self.list_state.logical_scroll_top();
+
+        //         self.list_state.reset(self.entries.len());
+
+        //         if scroll_to_top {
+        //             self.list_state.scroll_to(ListOffset::default());
+        //         } else {
+        //             // Attempt to maintain the same scroll position.
+        //             if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
+        //                 let new_scroll_top = self
+        //                     .entries
+        //                     .iter()
+        //                     .position(|entry| entry == old_top_entry)
+        //                     .map(|item_ix| ListOffset {
+        //                         item_ix,
+        //                         offset_in_item: old_scroll_top.offset_in_item,
+        //                     })
+        //                     .or_else(|| {
+        //                         let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
+        //                         let item_ix = self
+        //                             .entries
+        //                             .iter()
+        //                             .position(|entry| entry == entry_after_old_top)?;
+        //                         Some(ListOffset {
+        //                             item_ix,
+        //                             offset_in_item: 0.,
+        //                         })
+        //                     })
+        //                     .or_else(|| {
+        //                         let entry_before_old_top =
+        //                             old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
+        //                         let item_ix = self
+        //                             .entries
+        //                             .iter()
+        //                             .position(|entry| entry == entry_before_old_top)?;
+        //                         Some(ListOffset {
+        //                             item_ix,
+        //                             offset_in_item: 0.,
+        //                         })
+        //                     });
+
+        //                 self.list_state
+        //                     .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+        //             }
+        //         }
 
-    //         cx.notify();
-    //     }
+        cx.notify();
+    }
 
     //     fn render_call_participant(
     //         user: &User,

crates/collab_ui2/src/collab_panel/contact_finder.rs 🔗

@@ -1,37 +1,34 @@
 use client::{ContactRequestStatus, User, UserStore};
 use gpui::{
-    elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+    div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle,
+    FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View,
+    ViewContext, VisualContext, WeakView,
 };
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
-use util::TryFutureExt;
-use workspace::Modal;
+use theme::ActiveTheme as _;
+use ui::{h_stack, v_stack, Label};
+use util::{ResultExt as _, TryFutureExt};
 
 pub fn init(cx: &mut AppContext) {
-    Picker::<ContactFinderDelegate>::init(cx);
-    cx.add_action(ContactFinder::dismiss)
+    //Picker::<ContactFinderDelegate>::init(cx);
+    //cx.add_action(ContactFinder::dismiss)
 }
 
 pub struct ContactFinder {
-    picker: ViewHandle<Picker<ContactFinderDelegate>>,
+    picker: View<Picker<ContactFinderDelegate>>,
     has_focus: bool,
 }
 
 impl ContactFinder {
-    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
-        let picker = cx.add_view(|cx| {
-            Picker::new(
-                ContactFinderDelegate {
-                    user_store,
-                    potential_contacts: Arc::from([]),
-                    selected_index: 0,
-                },
-                cx,
-            )
-            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
-        });
-
-        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
+    pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+        let delegate = ContactFinderDelegate {
+            parent: cx.view().downgrade(),
+            user_store,
+            potential_contacts: Arc::from([]),
+            selected_index: 0,
+        };
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
 
         Self {
             picker,
@@ -41,105 +38,72 @@ impl ContactFinder {
 
     pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
         self.picker.update(cx, |picker, cx| {
-            picker.set_query(query, cx);
+            // todo!()
+            // picker.set_query(query, cx);
         });
     }
-
-    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(PickerEvent::Dismiss);
-    }
-}
-
-impl Entity for ContactFinder {
-    type Event = PickerEvent;
 }
 
-impl View for ContactFinder {
-    fn ui_name() -> &'static str {
-        "ContactFinder"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let full_theme = &theme::current(cx);
-        let theme = &full_theme.collab_panel.tabbed_modal;
-
-        fn render_mode_button(
-            text: &'static str,
-            theme: &theme::TabbedModal,
-            _cx: &mut ViewContext<ContactFinder>,
-        ) -> AnyElement<ContactFinder> {
-            let contained_text = &theme.tab_button.active_state().default;
-            Label::new(text, contained_text.text.clone())
-                .contained()
-                .with_style(contained_text.container.clone())
-                .into_any()
+impl Render for ContactFinder {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        fn render_mode_button(text: &'static str) -> AnyElement {
+            Label::new(text).into_any_element()
         }
 
-        Flex::column()
-            .with_child(
-                Flex::column()
-                    .with_child(
-                        Label::new("Contacts", theme.title.text.clone())
-                            .contained()
-                            .with_style(theme.title.container.clone()),
-                    )
-                    .with_child(Flex::row().with_children([render_mode_button(
-                        "Invite new contacts",
-                        &theme,
-                        cx,
-                    )]))
-                    .expanded()
-                    .contained()
-                    .with_style(theme.header),
-            )
-            .with_child(
-                ChildView::new(&self.picker, cx)
-                    .contained()
-                    .with_style(theme.body),
+        v_stack()
+            .child(
+                v_stack()
+                    .child(Label::new("Contacts"))
+                    .child(h_stack().children([render_mode_button("Invite new contacts")]))
+                    .bg(cx.theme().colors().element_background),
             )
-            .constrained()
-            .with_max_height(theme.max_height)
-            .with_max_width(theme.max_width)
-            .contained()
-            .with_style(theme.modal)
-            .into_any()
+            .child(self.picker.clone())
+            .w_96()
     }
 
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.has_focus = true;
-        if cx.is_self_focused() {
-            cx.focus(&self.picker)
-        }
-    }
+    // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+    //     self.has_focus = true;
+    //     if cx.is_self_focused() {
+    //         cx.focus(&self.picker)
+    //     }
+    // }
 
-    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
-    }
+    // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+    //     self.has_focus = false;
+    // }
+
+    type Element = Div;
 }
 
-impl Modal for ContactFinder {
-    fn has_focus(&self) -> bool {
-        self.has_focus
-    }
+// impl Modal for ContactFinder {
+//     fn has_focus(&self) -> bool {
+//         self.has_focus
+//     }
 
-    fn dismiss_on_event(event: &Self::Event) -> bool {
-        match event {
-            PickerEvent::Dismiss => true,
-        }
-    }
-}
+//     fn dismiss_on_event(event: &Self::Event) -> bool {
+//         match event {
+//             PickerEvent::Dismiss => true,
+//         }
+//     }
+// }
 
 pub struct ContactFinderDelegate {
+    parent: WeakView<ContactFinder>,
     potential_contacts: Arc<[Arc<User>]>,
-    user_store: ModelHandle<UserStore>,
+    user_store: Model<UserStore>,
     selected_index: usize,
 }
 
-impl PickerDelegate for ContactFinderDelegate {
-    fn placeholder_text(&self) -> Arc<str> {
-        "Search collaborator by username...".into()
+impl EventEmitter<DismissEvent> for ContactFinder {}
+
+impl FocusableView for ContactFinder {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
+}
 
+impl PickerDelegate for ContactFinderDelegate {
+    type ListItem = Div;
     fn match_count(&self) -> usize {
         self.potential_contacts.len()
     }
@@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate {
         self.selected_index = ix;
     }
 
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search collaborator by username...".into()
+    }
+
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
         let search_users = self
             .user_store
@@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate {
             async {
                 let potential_contacts = search_users.await?;
                 picker.update(&mut cx, |picker, cx| {
-                    picker.delegate_mut().potential_contacts = potential_contacts.into();
+                    picker.delegate.potential_contacts = potential_contacts.into();
                     cx.notify();
                 })?;
                 anyhow::Ok(())
@@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        cx.emit(PickerEvent::Dismiss);
+        //cx.emit(PickerEvent::Dismiss);
+        self.parent
+            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
+            .log_err();
     }
 
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: &mut MouseState,
         selected: bool,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<Picker<Self>> {
-        let full_theme = &theme::current(cx);
-        let theme = &full_theme.collab_panel.contact_finder;
-        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let user = &self.potential_contacts[ix];
         let request_status = self.user_store.read(cx).contact_request_status(user);
 
@@ -214,48 +181,47 @@ impl PickerDelegate for ContactFinderDelegate {
             ContactRequestStatus::RequestSent => Some("icons/x.svg"),
             ContactRequestStatus::RequestAccepted => None,
         };
-        let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
-            &theme.disabled_contact_button
-        } else {
-            &theme.contact_button
-        };
-        let style = tabbed_modal
-            .picker
-            .item
-            .in_state(selected)
-            .style_for(mouse_state);
-        Flex::row()
-            .with_children(user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.contact_avatar)
-                    .aligned()
-                    .left()
-            }))
-            .with_child(
-                Label::new(user.github_login.clone(), style.label.clone())
-                    .contained()
-                    .with_style(theme.contact_username)
-                    .aligned()
-                    .left(),
-            )
-            .with_children(icon_path.map(|icon_path| {
-                Svg::new(icon_path)
-                    .with_color(button_style.color)
-                    .constrained()
-                    .with_width(button_style.icon_width)
-                    .aligned()
-                    .contained()
-                    .with_style(button_style.container)
-                    .constrained()
-                    .with_width(button_style.button_width)
-                    .with_height(button_style.button_width)
-                    .aligned()
-                    .flex_float()
-            }))
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_height(tabbed_modal.row_height)
-            .into_any()
+        dbg!(icon_path);
+        Some(
+            div()
+                .flex_1()
+                .justify_between()
+                .children(user.avatar.clone().map(|avatar| img().data(avatar)))
+                .child(Label::new(user.github_login.clone()))
+                .children(icon_path.map(|icon_path| svg().path(icon_path))),
+        )
+        // Flex::row()
+        //     .with_children(user.avatar.clone().map(|avatar| {
+        //         Image::from_data(avatar)
+        //             .with_style(theme.contact_avatar)
+        //             .aligned()
+        //             .left()
+        //     }))
+        //     .with_child(
+        //         Label::new(user.github_login.clone(), style.label.clone())
+        //             .contained()
+        //             .with_style(theme.contact_username)
+        //             .aligned()
+        //             .left(),
+        //     )
+        //     .with_children(icon_path.map(|icon_path| {
+        //         Svg::new(icon_path)
+        //             .with_color(button_style.color)
+        //             .constrained()
+        //             .with_width(button_style.icon_width)
+        //             .aligned()
+        //             .contained()
+        //             .with_style(button_style.container)
+        //             .constrained()
+        //             .with_width(button_style.button_width)
+        //             .with_height(button_style.button_width)
+        //             .aligned()
+        //             .flex_float()
+        //     }))
+        //     .contained()
+        //     .with_style(style.container)
+        //     .constrained()
+        //     .with_height(tabbed_modal.row_height)
+        //     .into_any()
     }
 }

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -39,7 +39,7 @@ use project::Project;
 use theme::ActiveTheme;
 use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
 use util::ResultExt;
-use workspace::Workspace;
+use workspace::{notifications::NotifyResultExt, Workspace};
 
 use crate::face_pile::FacePile;
 
@@ -290,11 +290,13 @@ impl Render for CollabTitlebarItem {
                 } else {
                     this.child(Button::new("Sign in").on_click(move |_, cx| {
                         let client = client.clone();
-                        cx.spawn(move |cx| async move {
-                            client.authenticate_and_connect(true, &cx).await?;
-                            Ok::<(), anyhow::Error>(())
+                        cx.spawn(move |mut cx| async move {
+                            client
+                                .authenticate_and_connect(true, &cx)
+                                .await
+                                .notify_async_err(&mut cx);
                         })
-                        .detach_and_log_err(cx);
+                        .detach();
                     }))
                 }
             })

crates/ui2/src/components/avatar.rs 🔗

@@ -49,6 +49,12 @@ impl Avatar {
         }
     }
 
+    pub fn source(src: ImageSource) -> Self {
+        Self {
+            src,
+            shape: Shape::Circle,
+        }
+    }
     pub fn shape(mut self, shape: Shape) -> Self {
         self.shape = shape;
         self

crates/ui2/src/components/list.rs 🔗

@@ -1,13 +1,14 @@
 use std::rc::Rc;
 
 use gpui::{
-    div, px, AnyElement, ClickEvent, Div, IntoElement, MouseButton, MouseDownEvent, Pixels,
-    Stateful, StatefulInteractiveElement,
+    div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent,
+    Pixels, Stateful, StatefulInteractiveElement,
 };
 use smallvec::SmallVec;
 
 use crate::{
-    disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
+    disclosure_control, h_stack, v_stack, Avatar, Icon, IconButton, IconElement, IconSize, Label,
+    Toggle,
 };
 use crate::{prelude::*, GraphicSlot};
 
@@ -20,8 +21,7 @@ pub enum ListItemVariant {
 }
 
 pub enum ListHeaderMeta {
-    // TODO: These should be IconButtons
-    Tools(Vec<Icon>),
+    Tools(Vec<IconButton>),
     // TODO: This should be a button
     Button(Label),
     Text(Label),
@@ -47,11 +47,7 @@ impl RenderOnce for ListHeader {
                 h_stack()
                     .gap_2()
                     .items_center()
-                    .children(icons.into_iter().map(|i| {
-                        IconElement::new(i)
-                            .color(Color::Muted)
-                            .size(IconSize::Small)
-                    })),
+                    .children(icons.into_iter().map(|i| i.color(Color::Muted))),
             ),
             Some(ListHeaderMeta::Button(label)) => div().child(label),
             Some(ListHeaderMeta::Text(label)) => div().child(label),
@@ -115,6 +111,10 @@ impl ListHeader {
         self
     }
 
+    pub fn right_button(self, button: IconButton) -> Self {
+        self.meta(Some(ListHeaderMeta::Tools(vec![button])))
+    }
+
     pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
         self.meta = meta;
         self
@@ -257,7 +257,7 @@ impl ListItem {
         self
     }
 
-    pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
+    pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
         self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
         self
     }
@@ -275,7 +275,7 @@ impl RenderOnce for ListItem {
                         .color(Color::Muted),
                 ),
             ),
-            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))),
+            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
             Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
             None => None,
         };
@@ -297,15 +297,6 @@ impl RenderOnce for ListItem {
             .when(self.selected, |this| {
                 this.bg(cx.theme().colors().ghost_element_selected)
             })
-            .when_some(self.on_click.clone(), |this, on_click| {
-                this.on_click(move |event, cx| {
-                    // HACK: GPUI currently fires `on_click` with any mouse button,
-                    // but we only care about the left button.
-                    if event.down.button == MouseButton::Left {
-                        (on_click)(event, cx)
-                    }
-                })
-            })
             .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
                 this.on_mouse_down(MouseButton::Right, move |event, cx| {
                     (on_mouse_down)(event, cx)

crates/ui2/src/components/slot.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::SharedString;
+use gpui::{ImageSource, SharedString};
 
 use crate::Icon;
 
@@ -9,6 +9,6 @@ use crate::Icon;
 /// Can be filled with a []
 pub enum GraphicSlot {
     Icon(Icon),
-    Avatar(SharedString),
+    Avatar(ImageSource),
     PublicActor(SharedString),
 }

crates/zed2/Cargo.toml 🔗

@@ -21,7 +21,7 @@ audio = { package = "audio2", path = "../audio2" }
 auto_update = { package = "auto_update2", path = "../auto_update2" }
 # breadcrumbs = { path = "../breadcrumbs" }
 call = { package = "call2", path = "../call2" }
-# channel = { path = "../channel" }
+channel = { package = "channel2", path = "../channel2" }
 cli = { path = "../cli" }
 collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
 collections = { path = "../collections" }

crates/zed2/src/main.rs 🔗

@@ -189,7 +189,7 @@ fn main() {
         let app_state = Arc::new(AppState {
             languages,
             client: client.clone(),
-            user_store,
+            user_store: user_store.clone(),
             fs,
             build_window_options,
             call_factory: call::Call::new,
@@ -210,7 +210,7 @@ fn main() {
         // outline::init(cx);
         // project_symbols::init(cx);
         project_panel::init(Assets, cx);
-        // channel::init(&client, user_store.clone(), cx);
+        channel::init(&client, user_store.clone(), cx);
         // diagnostics::init(cx);
         search::init(cx);
         // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);