mod channel_modal;mod contact_finder; use self::channel_modal::ChannelModal;use crate::{channel_view::ChannelView, chat_panel::ChatPanel, CollaborationPanelSettings};use call::ActiveCall;use channel::{Channel, ChannelEvent, ChannelStore};use client::{ChannelId, Client, Contact, User, UserStore};use contact_finder::ContactFinder;use db::kvp::KEY_VALUE_STORE;use editor::{Editor, EditorElement, EditorStyle};use fuzzy::{match_strings, StringMatchCandidate};use gpui::{ actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent, Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement, ListOffset, ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, SharedString, Styled, Subscription, Task, TextStyle, WeakEntity, Window,};use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};use project::{Fs, Project};use rpc::{ proto::{self, ChannelVisibility, PeerId}, ErrorCode, ErrorExt,};use serde_derive::{Deserialize, Serialize};use settings::Settings;use smallvec::SmallVec;use std::{mem, sync::Arc};use theme::{ActiveTheme, ThemeSettings};use ui::{ prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,};use util::{maybe, ResultExt, TryFutureExt};use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt}, OpenChannelNotes, Workspace,}; actions!( collab_panel, [ ToggleFocus, Remove, Secondary, CollapseSelectedChannel, ExpandSelectedChannel, StartMoveChannel, MoveSelected, InsertSpace, ]); #[derive(Debug, Copy, Clone, PartialEq, Eq)]struct ChannelMoveClipboard { channel_id: ChannelId,} const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel"; pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::<CollabPanel>(window, cx); }); workspace.register_action(|_, _: &OpenChannelNotes, window, cx| { let channel_id = ActiveCall::global(cx) .read(cx) .room() .and_then(|room| room.read(cx).channel_id()); if let Some(channel_id) = channel_id { let workspace = cx.entity().clone(); window.defer(cx, move |window, cx| { ChannelView::open(channel_id, None, workspace, window, cx) .detach_and_log_err(cx) }); } }); }) .detach();} #[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<String> { match self { ChannelEditingState::Create { pending_name, .. } => pending_name.clone(), ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(), } }} pub struct CollabPanel { width: Option<Pixels>, fs: Arc<dyn Fs>, focus_handle: FocusHandle, channel_clipboard: Option<ChannelMoveClipboard>, pending_serialization: Task<Option<()>>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, list_state: ListState, filter_editor: Entity<Editor>, channel_name_editor: Entity<Editor>, channel_editing_state: Option<ChannelEditingState>, entries: Vec<ListEntry>, selection: Option<usize>, channel_store: Entity<ChannelStore>, user_store: Entity<UserStore>, client: Arc<Client>, project: Entity<Project>, match_candidates: Vec<StringMatchCandidate>, subscriptions: Vec<Subscription>, collapsed_sections: Vec<Section>, collapsed_channels: Vec<ChannelId>, workspace: WeakEntity<Workspace>,} #[derive(Serialize, Deserialize)]struct SerializedCollabPanel { width: Option<Pixels>, collapsed_channels: Option<Vec<u64>>,} #[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, role: proto::ChannelRole, }, 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 CollabPanel { pub fn new( workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) -> Entity<Self> { cx.new(|cx| { let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter...", cx); editor }); cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { if let editor::EditorEvent::BufferEdited = event { let query = this.filter_editor.read(cx).text(cx); if !query.is_empty() { this.selection.take(); } this.update_entries(true, cx); if !query.is_empty() { this.selection = this .entries .iter() .position(|entry| !matches!(entry, ListEntry::Header(_))); } } }) .detach(); let channel_name_editor = cx.new(|cx| Editor::single_line(window, cx)); cx.subscribe_in( &channel_name_editor, window, |this: &mut Self, _, event, window, cx| { if let editor::EditorEvent::Blurred = event { if let Some(state) = &this.channel_editing_state { if state.pending_name().is_some() { return; } } this.take_editing_state(window, cx); this.update_entries(false, cx); cx.notify(); } }, ) .detach(); let entity = cx.entity().downgrade(); let list_state = ListState::new( 0, gpui::ListAlignment::Top, px(1000.), move |ix, window, cx| { if let Some(entity) = entity.upgrade() { entity.update(cx, |this, cx| this.render_list_entry(ix, window, cx)) } else { div().into_any() } }, ); let mut this = Self { width: None, focus_handle: cx.focus_handle(), channel_clipboard: None, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: None, list_state, channel_name_editor, filter_editor, entries: Vec::default(), channel_editing_state: None, selection: None, channel_store: ChannelStore::global(cx), user_store: workspace.user_store().clone(), 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(), }; this.update_entries(false, cx); 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.channel_store, move |this, _, cx| { this.update_entries(true, cx) })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); this.subscriptions.push(cx.subscribe_in( &this.channel_store, window, |this, _channel_store, e, window, cx| match e { ChannelEvent::ChannelCreated(channel_id) | ChannelEvent::ChannelRenamed(channel_id) => { if this.take_editing_state(window, cx) { this.update_entries(false, cx); this.selection = this.entries.iter().position(|entry| { if let ListEntry::Channel { channel, .. } = entry { channel.id == *channel_id } else { false } }); } } }, )); this }) } pub async fn load( workspace: WeakEntity<Workspace>, mut cx: AsyncWindowContext, ) -> anyhow::Result<Entity<Self>> { let serialized_panel = cx .background_spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) }) .await .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store")) .log_err() .flatten() .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel)) .transpose() .log_err() .flatten(); workspace.update_in(&mut cx, |workspace, window, cx| { let panel = CollabPanel::new(workspace, window, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); panel.collapsed_channels = serialized_panel .collapsed_channels .unwrap_or_else(Vec::new) .iter() .map(|cid| ChannelId(*cid)) .collect(); cx.notify(); }); } panel }) } fn serialize(&mut self, cx: &mut Context<Self>) { let width = self.width; let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background_spawn( async move { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), serde_json::to_string(&SerializedCollabPanel { width, collapsed_channels: Some( collapsed_channels.iter().map(|cid| cid.0).collect(), ), })?, ) .await?; anyhow::Ok(()) } .log_err(), ); } fn scroll_to_item(&mut self, ix: usize) { self.list_state.scroll_to_reveal_item(ix) } fn update_entries(&mut self, select_same_item: bool, cx: &mut Context<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 query.is_empty() { 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::new(0, &user.github_login)); 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, role: room.local_participant().role, }); 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().values().map(|participant| { StringMatchCandidate::new( participant.user.id as usize, &participant.user.github_login, ) })); let mut matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); matches.sort_by(|a, b| { let a_is_guest = room.role_for_user(a.candidate_id as u64) == Some(proto::ChannelRole::Guest); let b_is_guest = room.role_for_user(b.candidate_id as u64) == Some(proto::ChannelRole::Guest); a_is_guest .cmp(&b_is_guest) .then_with(|| a.string.cmp(&b.string)) }); 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, role: participant.role, }); 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.has_video_tracks(), }); } if participant.has_video_tracks() { 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)| {Lines 1–500 of 3128. Load more
mod channel_modal; mod contact_finder; use self::channel_modal::ChannelModal; use crate::{channel_view::ChannelView, chat_panel::ChatPanel, CollaborationPanelSettings}; use call::ActiveCall; use channel::{Channel, ChannelEvent, ChannelStore}; use client::{ChannelId, Client, Contact, User, UserStore}; use contact_finder::ContactFinder; use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorElement, EditorStyle}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent, Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement, ListOffset, ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, SharedString, Styled, Subscription, Task, TextStyle, WeakEntity, Window, }; use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev}; use project::{Fs, Project}; use rpc::{ proto::{self, ChannelVisibility, PeerId}, ErrorCode, ErrorExt, }; use serde_derive::{Deserialize, Serialize}; use settings::Settings; use smallvec::SmallVec; use std::{mem, sync::Arc}; use theme::{ActiveTheme, ThemeSettings}; use ui::{ prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt}, OpenChannelNotes, Workspace, }; actions!( collab_panel, [ ToggleFocus, Remove, Secondary, CollapseSelectedChannel, ExpandSelectedChannel, StartMoveChannel, MoveSelected, InsertSpace, ] ); #[derive(Debug, Copy, Clone, PartialEq, Eq)] struct ChannelMoveClipboard { channel_id: ChannelId, } const COLLABORATION_PANEL_KEY: &str = "CollaborationPanel"; pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::<CollabPanel>(window, cx); }); workspace.register_action(|_, _: &OpenChannelNotes, window, cx| { let channel_id = ActiveCall::global(cx) .read(cx) .room() .and_then(|room| room.read(cx).channel_id()); if let Some(channel_id) = channel_id { let workspace = cx.entity().clone(); window.defer(cx, move |window, cx| { ChannelView::open(channel_id, None, workspace, window, cx) .detach_and_log_err(cx) }); } }); }) .detach(); } #[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<String> { match self { ChannelEditingState::Create { pending_name, .. } => pending_name.clone(), ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(), } } } pub struct CollabPanel { width: Option<Pixels>, fs: Arc<dyn Fs>, focus_handle: FocusHandle, channel_clipboard: Option<ChannelMoveClipboard>, pending_serialization: Task<Option<()>>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, list_state: ListState, filter_editor: Entity<Editor>, channel_name_editor: Entity<Editor>, channel_editing_state: Option<ChannelEditingState>, entries: Vec<ListEntry>, selection: Option<usize>, channel_store: Entity<ChannelStore>, user_store: Entity<UserStore>, client: Arc<Client>, project: Entity<Project>, match_candidates: Vec<StringMatchCandidate>, subscriptions: Vec<Subscription>, collapsed_sections: Vec<Section>, collapsed_channels: Vec<ChannelId>, workspace: WeakEntity<Workspace>, } #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { width: Option<Pixels>, collapsed_channels: Option<Vec<u64>>, } #[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, role: proto::ChannelRole, }, 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 CollabPanel { pub fn new( workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) -> Entity<Self> { cx.new(|cx| { let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter...", cx); editor }); cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { if let editor::EditorEvent::BufferEdited = event { let query = this.filter_editor.read(cx).text(cx); if !query.is_empty() { this.selection.take(); } this.update_entries(true, cx); if !query.is_empty() { this.selection = this .entries .iter() .position(|entry| !matches!(entry, ListEntry::Header(_))); } } }) .detach(); let channel_name_editor = cx.new(|cx| Editor::single_line(window, cx)); cx.subscribe_in( &channel_name_editor, window, |this: &mut Self, _, event, window, cx| { if let editor::EditorEvent::Blurred = event { if let Some(state) = &this.channel_editing_state { if state.pending_name().is_some() { return; } } this.take_editing_state(window, cx); this.update_entries(false, cx); cx.notify(); } }, ) .detach(); let entity = cx.entity().downgrade(); let list_state = ListState::new( 0, gpui::ListAlignment::Top, px(1000.), move |ix, window, cx| { if let Some(entity) = entity.upgrade() { entity.update(cx, |this, cx| this.render_list_entry(ix, window, cx)) } else { div().into_any() } }, ); let mut this = Self { width: None, focus_handle: cx.focus_handle(), channel_clipboard: None, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: None, list_state, channel_name_editor, filter_editor, entries: Vec::default(), channel_editing_state: None, selection: None, channel_store: ChannelStore::global(cx), user_store: workspace.user_store().clone(), 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(), }; this.update_entries(false, cx); 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.channel_store, move |this, _, cx| { this.update_entries(true, cx) })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); this.subscriptions.push(cx.subscribe_in( &this.channel_store, window, |this, _channel_store, e, window, cx| match e { ChannelEvent::ChannelCreated(channel_id) | ChannelEvent::ChannelRenamed(channel_id) => { if this.take_editing_state(window, cx) { this.update_entries(false, cx); this.selection = this.entries.iter().position(|entry| { if let ListEntry::Channel { channel, .. } = entry { channel.id == *channel_id } else { false } }); } } }, )); this }) } pub async fn load( workspace: WeakEntity<Workspace>, mut cx: AsyncWindowContext, ) -> anyhow::Result<Entity<Self>> { let serialized_panel = cx .background_spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) }) .await .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store")) .log_err() .flatten() .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel)) .transpose() .log_err() .flatten(); workspace.update_in(&mut cx, |workspace, window, cx| { let panel = CollabPanel::new(workspace, window, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); panel.collapsed_channels = serialized_panel .collapsed_channels .unwrap_or_else(Vec::new) .iter() .map(|cid| ChannelId(*cid)) .collect(); cx.notify(); }); } panel }) } fn serialize(&mut self, cx: &mut Context<Self>) { let width = self.width; let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background_spawn( async move { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), serde_json::to_string(&SerializedCollabPanel { width, collapsed_channels: Some( collapsed_channels.iter().map(|cid| cid.0).collect(), ), })?, ) .await?; anyhow::Ok(()) } .log_err(), ); } fn scroll_to_item(&mut self, ix: usize) { self.list_state.scroll_to_reveal_item(ix) } fn update_entries(&mut self, select_same_item: bool, cx: &mut Context<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 query.is_empty() { 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::new(0, &user.github_login)); 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, role: room.local_participant().role, }); 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().values().map(|participant| { StringMatchCandidate::new( participant.user.id as usize, &participant.user.github_login, ) })); let mut matches = executor.block(match_strings( &self.match_candidates, &query, true, usize::MAX, &Default::default(), executor.clone(), )); matches.sort_by(|a, b| { let a_is_guest = room.role_for_user(a.candidate_id as u64) == Some(proto::ChannelRole::Guest); let b_is_guest = room.role_for_user(b.candidate_id as u64) == Some(proto::ChannelRole::Guest); a_is_guest .cmp(&b_is_guest) .then_with(|| a.string.cmp(&b.string)) }); 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, role: participant.role, }); 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.has_video_tracks(), }); } if participant.has_video_tracks() { 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)| {