collab_panel.rs

   1mod channel_modal;
   2mod contact_finder;
   3mod panel_settings;
   4
   5use anyhow::Result;
   6use call::ActiveCall;
   7use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
   8use client::{proto::PeerId, Client, Contact, User, UserStore};
   9use context_menu::{ContextMenu, ContextMenuItem};
  10use db::kvp::KEY_VALUE_STORE;
  11use editor::{Cancel, Editor};
  12use futures::StreamExt;
  13use fuzzy::{match_strings, StringMatchCandidate};
  14use gpui::{
  15    actions,
  16    elements::{
  17        Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
  18        MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
  19        Stack, Svg,
  20    },
  21    fonts::TextStyle,
  22    geometry::{
  23        rect::RectF,
  24        vector::{vec2f, Vector2F},
  25    },
  26    impl_actions,
  27    platform::{CursorStyle, MouseButton, PromptLevel},
  28    serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
  29    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
  30};
  31use menu::{Confirm, SelectNext, SelectPrev};
  32use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
  33use project::{Fs, Project};
  34use serde_derive::{Deserialize, Serialize};
  35use settings::SettingsStore;
  36use staff_mode::StaffMode;
  37use std::{borrow::Cow, mem, sync::Arc};
  38use theme::{components::ComponentExt, IconButton};
  39use util::{iife, ResultExt, TryFutureExt};
  40use workspace::{
  41    dock::{DockPosition, Panel},
  42    item::ItemHandle,
  43    Workspace,
  44};
  45
  46use crate::{
  47    channel_view::{self, ChannelView},
  48    face_pile::FacePile,
  49};
  50use channel_modal::ChannelModal;
  51
  52use self::contact_finder::ContactFinder;
  53
  54#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  55struct RemoveChannel {
  56    channel_id: u64,
  57}
  58
  59#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  60struct ToggleCollapse {
  61    channel_id: u64,
  62}
  63
  64#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  65struct NewChannel {
  66    channel_id: u64,
  67}
  68
  69#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  70struct InviteMembers {
  71    channel_id: u64,
  72}
  73
  74#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  75struct ManageMembers {
  76    channel_id: u64,
  77}
  78
  79#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  80struct RenameChannel {
  81    channel_id: u64,
  82}
  83
  84#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  85struct OpenChannelBuffer {
  86    channel_id: u64,
  87}
  88
  89actions!(
  90    collab_panel,
  91    [
  92        ToggleFocus,
  93        Remove,
  94        Secondary,
  95        CollapseSelectedChannel,
  96        ExpandSelectedChannel
  97    ]
  98);
  99
 100impl_actions!(
 101    collab_panel,
 102    [
 103        RemoveChannel,
 104        NewChannel,
 105        InviteMembers,
 106        ManageMembers,
 107        RenameChannel,
 108        ToggleCollapse,
 109        OpenChannelBuffer
 110    ]
 111);
 112
 113const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 114
 115pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
 116    settings::register::<panel_settings::CollaborationPanelSettings>(cx);
 117    contact_finder::init(cx);
 118    channel_modal::init(cx);
 119    channel_view::init(cx);
 120
 121    cx.add_action(CollabPanel::cancel);
 122    cx.add_action(CollabPanel::select_next);
 123    cx.add_action(CollabPanel::select_prev);
 124    cx.add_action(CollabPanel::confirm);
 125    cx.add_action(CollabPanel::remove);
 126    cx.add_action(CollabPanel::remove_selected_channel);
 127    cx.add_action(CollabPanel::show_inline_context_menu);
 128    cx.add_action(CollabPanel::new_subchannel);
 129    cx.add_action(CollabPanel::invite_members);
 130    cx.add_action(CollabPanel::manage_members);
 131    cx.add_action(CollabPanel::rename_selected_channel);
 132    cx.add_action(CollabPanel::rename_channel);
 133    cx.add_action(CollabPanel::toggle_channel_collapsed);
 134    cx.add_action(CollabPanel::collapse_selected_channel);
 135    cx.add_action(CollabPanel::expand_selected_channel);
 136    cx.add_action(CollabPanel::open_channel_buffer);
 137}
 138
 139#[derive(Debug)]
 140pub enum ChannelEditingState {
 141    Create {
 142        parent_id: Option<u64>,
 143        pending_name: Option<String>,
 144    },
 145    Rename {
 146        channel_id: u64,
 147        pending_name: Option<String>,
 148    },
 149}
 150
 151impl ChannelEditingState {
 152    fn pending_name(&self) -> Option<&str> {
 153        match self {
 154            ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
 155            ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
 156        }
 157    }
 158}
 159
 160pub struct CollabPanel {
 161    width: Option<f32>,
 162    fs: Arc<dyn Fs>,
 163    has_focus: bool,
 164    pending_serialization: Task<Option<()>>,
 165    context_menu: ViewHandle<ContextMenu>,
 166    filter_editor: ViewHandle<Editor>,
 167    channel_name_editor: ViewHandle<Editor>,
 168    channel_editing_state: Option<ChannelEditingState>,
 169    entries: Vec<ListEntry>,
 170    selection: Option<usize>,
 171    user_store: ModelHandle<UserStore>,
 172    client: Arc<Client>,
 173    channel_store: ModelHandle<ChannelStore>,
 174    project: ModelHandle<Project>,
 175    match_candidates: Vec<StringMatchCandidate>,
 176    list_state: ListState<Self>,
 177    subscriptions: Vec<Subscription>,
 178    collapsed_sections: Vec<Section>,
 179    collapsed_channels: Vec<ChannelId>,
 180    workspace: WeakViewHandle<Workspace>,
 181    context_menu_on_selected: bool,
 182}
 183
 184#[derive(Serialize, Deserialize)]
 185struct SerializedChannelsPanel {
 186    width: Option<f32>,
 187    collapsed_channels: Vec<ChannelId>,
 188}
 189
 190#[derive(Debug)]
 191pub enum Event {
 192    DockPositionChanged,
 193    Focus,
 194    Dismissed,
 195}
 196
 197#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 198enum Section {
 199    ActiveCall,
 200    Channels,
 201    ChannelInvites,
 202    ContactRequests,
 203    Contacts,
 204    Online,
 205    Offline,
 206}
 207
 208#[derive(Clone, Debug)]
 209enum ListEntry {
 210    Header(Section, usize),
 211    CallParticipant {
 212        user: Arc<User>,
 213        is_pending: bool,
 214    },
 215    ParticipantProject {
 216        project_id: u64,
 217        worktree_root_names: Vec<String>,
 218        host_user_id: u64,
 219        is_last: bool,
 220    },
 221    ParticipantScreen {
 222        peer_id: PeerId,
 223        is_last: bool,
 224    },
 225    IncomingRequest(Arc<User>),
 226    OutgoingRequest(Arc<User>),
 227    ChannelInvite(Arc<Channel>),
 228    Channel {
 229        channel: Arc<Channel>,
 230        depth: usize,
 231    },
 232    ChannelNotes {
 233        channel_id: ChannelId,
 234    },
 235    ChannelEditor {
 236        depth: usize,
 237    },
 238    Contact {
 239        contact: Arc<Contact>,
 240        calling: bool,
 241    },
 242    ContactPlaceholder,
 243}
 244
 245impl Entity for CollabPanel {
 246    type Event = Event;
 247}
 248
 249impl CollabPanel {
 250    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
 251        cx.add_view::<Self, _>(|cx| {
 252            let view_id = cx.view_id();
 253
 254            let filter_editor = cx.add_view(|cx| {
 255                let mut editor = Editor::single_line(
 256                    Some(Arc::new(|theme| {
 257                        theme.collab_panel.user_query_editor.clone()
 258                    })),
 259                    cx,
 260                );
 261                editor.set_placeholder_text("Filter channels, contacts", cx);
 262                editor
 263            });
 264
 265            cx.subscribe(&filter_editor, |this, _, event, cx| {
 266                if let editor::Event::BufferEdited = event {
 267                    let query = this.filter_editor.read(cx).text(cx);
 268                    if !query.is_empty() {
 269                        this.selection.take();
 270                    }
 271                    this.update_entries(true, cx);
 272                    if !query.is_empty() {
 273                        this.selection = this
 274                            .entries
 275                            .iter()
 276                            .position(|entry| !matches!(entry, ListEntry::Header(_, _)));
 277                    }
 278                }
 279            })
 280            .detach();
 281
 282            let channel_name_editor = cx.add_view(|cx| {
 283                Editor::single_line(
 284                    Some(Arc::new(|theme| {
 285                        theme.collab_panel.user_query_editor.clone()
 286                    })),
 287                    cx,
 288                )
 289            });
 290
 291            cx.subscribe(&channel_name_editor, |this, _, event, cx| {
 292                if let editor::Event::Blurred = event {
 293                    if let Some(state) = &this.channel_editing_state {
 294                        if state.pending_name().is_some() {
 295                            return;
 296                        }
 297                    }
 298                    this.take_editing_state(cx);
 299                    this.update_entries(false, cx);
 300                    cx.notify();
 301                }
 302            })
 303            .detach();
 304
 305            let list_state =
 306                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
 307                    let theme = theme::current(cx).clone();
 308                    let is_selected = this.selection == Some(ix);
 309                    let current_project_id = this.project.read(cx).remote_id();
 310
 311                    match &this.entries[ix] {
 312                        ListEntry::Header(section, depth) => {
 313                            let is_collapsed = this.collapsed_sections.contains(section);
 314                            this.render_header(
 315                                *section,
 316                                &theme,
 317                                *depth,
 318                                is_selected,
 319                                is_collapsed,
 320                                cx,
 321                            )
 322                        }
 323                        ListEntry::CallParticipant { user, is_pending } => {
 324                            Self::render_call_participant(
 325                                user,
 326                                *is_pending,
 327                                is_selected,
 328                                &theme.collab_panel,
 329                            )
 330                        }
 331                        ListEntry::ParticipantProject {
 332                            project_id,
 333                            worktree_root_names,
 334                            host_user_id,
 335                            is_last,
 336                        } => Self::render_participant_project(
 337                            *project_id,
 338                            worktree_root_names,
 339                            *host_user_id,
 340                            Some(*project_id) == current_project_id,
 341                            *is_last,
 342                            is_selected,
 343                            &theme.collab_panel,
 344                            cx,
 345                        ),
 346                        ListEntry::ParticipantScreen { peer_id, is_last } => {
 347                            Self::render_participant_screen(
 348                                *peer_id,
 349                                *is_last,
 350                                is_selected,
 351                                &theme.collab_panel,
 352                                cx,
 353                            )
 354                        }
 355                        ListEntry::Channel { channel, depth } => {
 356                            let channel_row = this.render_channel(
 357                                &*channel,
 358                                *depth,
 359                                &theme.collab_panel,
 360                                is_selected,
 361                                cx,
 362                            );
 363
 364                            if is_selected && this.context_menu_on_selected {
 365                                Stack::new()
 366                                    .with_child(channel_row)
 367                                    .with_child(
 368                                        ChildView::new(&this.context_menu, cx)
 369                                            .aligned()
 370                                            .bottom()
 371                                            .right(),
 372                                    )
 373                                    .into_any()
 374                            } else {
 375                                return channel_row;
 376                            }
 377                        }
 378                        ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
 379                            *channel_id,
 380                            &theme.collab_panel,
 381                            is_selected,
 382                            cx,
 383                        ),
 384                        ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
 385                            channel.clone(),
 386                            this.channel_store.clone(),
 387                            &theme.collab_panel,
 388                            is_selected,
 389                            cx,
 390                        ),
 391                        ListEntry::IncomingRequest(user) => Self::render_contact_request(
 392                            user.clone(),
 393                            this.user_store.clone(),
 394                            &theme.collab_panel,
 395                            true,
 396                            is_selected,
 397                            cx,
 398                        ),
 399                        ListEntry::OutgoingRequest(user) => Self::render_contact_request(
 400                            user.clone(),
 401                            this.user_store.clone(),
 402                            &theme.collab_panel,
 403                            false,
 404                            is_selected,
 405                            cx,
 406                        ),
 407                        ListEntry::Contact { contact, calling } => Self::render_contact(
 408                            contact,
 409                            *calling,
 410                            &this.project,
 411                            &theme.collab_panel,
 412                            is_selected,
 413                            cx,
 414                        ),
 415                        ListEntry::ChannelEditor { depth } => {
 416                            this.render_channel_editor(&theme, *depth, cx)
 417                        }
 418                        ListEntry::ContactPlaceholder => {
 419                            this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
 420                        }
 421                    }
 422                });
 423
 424            let mut this = Self {
 425                width: None,
 426                has_focus: false,
 427                fs: workspace.app_state().fs.clone(),
 428                pending_serialization: Task::ready(None),
 429                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
 430                channel_name_editor,
 431                filter_editor,
 432                entries: Vec::default(),
 433                channel_editing_state: None,
 434                selection: None,
 435                user_store: workspace.user_store().clone(),
 436                channel_store: workspace.app_state().channel_store.clone(),
 437                project: workspace.project().clone(),
 438                subscriptions: Vec::default(),
 439                match_candidates: Vec::default(),
 440                collapsed_sections: vec![Section::Offline],
 441                collapsed_channels: Vec::default(),
 442                workspace: workspace.weak_handle(),
 443                client: workspace.app_state().client.clone(),
 444                context_menu_on_selected: true,
 445                list_state,
 446            };
 447
 448            this.update_entries(false, cx);
 449
 450            // Update the dock position when the setting changes.
 451            let mut old_dock_position = this.position(cx);
 452            this.subscriptions
 453                .push(
 454                    cx.observe_global::<SettingsStore, _>(move |this: &mut CollabPanel, cx| {
 455                        let new_dock_position = this.position(cx);
 456                        if new_dock_position != old_dock_position {
 457                            old_dock_position = new_dock_position;
 458                            cx.emit(Event::DockPositionChanged);
 459                        }
 460                        cx.notify();
 461                    }),
 462                );
 463
 464            let active_call = ActiveCall::global(cx);
 465            this.subscriptions
 466                .push(cx.observe(&this.user_store, |this, _, cx| {
 467                    this.update_entries(true, cx)
 468                }));
 469            this.subscriptions
 470                .push(cx.observe(&this.channel_store, |this, _, cx| {
 471                    this.update_entries(true, cx)
 472                }));
 473            this.subscriptions
 474                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
 475            this.subscriptions.push(
 476                cx.observe_global::<StaffMode, _>(move |this, cx| this.update_entries(true, cx)),
 477            );
 478            this.subscriptions.push(cx.subscribe(
 479                &this.channel_store,
 480                |this, _channel_store, e, cx| match e {
 481                    ChannelEvent::ChannelCreated(channel_id)
 482                    | ChannelEvent::ChannelRenamed(channel_id) => {
 483                        if this.take_editing_state(cx) {
 484                            this.update_entries(false, cx);
 485                            this.selection = this.entries.iter().position(|entry| {
 486                                if let ListEntry::Channel { channel, .. } = entry {
 487                                    channel.id == *channel_id
 488                                } else {
 489                                    false
 490                                }
 491                            });
 492                        }
 493                    }
 494                },
 495            ));
 496
 497            this
 498        })
 499    }
 500
 501    pub fn load(
 502        workspace: WeakViewHandle<Workspace>,
 503        cx: AsyncAppContext,
 504    ) -> Task<Result<ViewHandle<Self>>> {
 505        cx.spawn(|mut cx| async move {
 506            let serialized_panel = if let Some(panel) = cx
 507                .background()
 508                .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
 509                .await
 510                .log_err()
 511                .flatten()
 512            {
 513                Some(serde_json::from_str::<SerializedChannelsPanel>(&panel)?)
 514            } else {
 515                None
 516            };
 517
 518            workspace.update(&mut cx, |workspace, cx| {
 519                let panel = CollabPanel::new(workspace, cx);
 520                if let Some(serialized_panel) = serialized_panel {
 521                    panel.update(cx, |panel, cx| {
 522                        panel.width = serialized_panel.width;
 523                        panel.collapsed_channels = serialized_panel.collapsed_channels;
 524                        cx.notify();
 525                    });
 526                }
 527                panel
 528            })
 529        })
 530    }
 531
 532    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 533        let width = self.width;
 534        let collapsed_channels = self.collapsed_channels.clone();
 535        self.pending_serialization = cx.background().spawn(
 536            async move {
 537                KEY_VALUE_STORE
 538                    .write_kvp(
 539                        COLLABORATION_PANEL_KEY.into(),
 540                        serde_json::to_string(&SerializedChannelsPanel {
 541                            width,
 542                            collapsed_channels,
 543                        })?,
 544                    )
 545                    .await?;
 546                anyhow::Ok(())
 547            }
 548            .log_err(),
 549        );
 550    }
 551
 552    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
 553        let channel_store = self.channel_store.read(cx);
 554        let user_store = self.user_store.read(cx);
 555        let query = self.filter_editor.read(cx).text(cx);
 556        let executor = cx.background().clone();
 557
 558        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 559        let old_entries = mem::take(&mut self.entries);
 560
 561        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 562            self.entries.push(ListEntry::Header(Section::ActiveCall, 0));
 563
 564            if !self.collapsed_sections.contains(&Section::ActiveCall) {
 565                let room = room.read(cx);
 566
 567                if let Some(channel_id) = room.channel_id() {
 568                    self.entries.push(ListEntry::ChannelNotes { channel_id })
 569                }
 570
 571                // Populate the active user.
 572                if let Some(user) = user_store.current_user() {
 573                    self.match_candidates.clear();
 574                    self.match_candidates.push(StringMatchCandidate {
 575                        id: 0,
 576                        string: user.github_login.clone(),
 577                        char_bag: user.github_login.chars().collect(),
 578                    });
 579                    let matches = executor.block(match_strings(
 580                        &self.match_candidates,
 581                        &query,
 582                        true,
 583                        usize::MAX,
 584                        &Default::default(),
 585                        executor.clone(),
 586                    ));
 587                    if !matches.is_empty() {
 588                        let user_id = user.id;
 589                        self.entries.push(ListEntry::CallParticipant {
 590                            user,
 591                            is_pending: false,
 592                        });
 593                        let mut projects = room.local_participant().projects.iter().peekable();
 594                        while let Some(project) = projects.next() {
 595                            self.entries.push(ListEntry::ParticipantProject {
 596                                project_id: project.id,
 597                                worktree_root_names: project.worktree_root_names.clone(),
 598                                host_user_id: user_id,
 599                                is_last: projects.peek().is_none(),
 600                            });
 601                        }
 602                    }
 603                }
 604
 605                // Populate remote participants.
 606                self.match_candidates.clear();
 607                self.match_candidates
 608                    .extend(room.remote_participants().iter().map(|(_, participant)| {
 609                        StringMatchCandidate {
 610                            id: participant.user.id as usize,
 611                            string: participant.user.github_login.clone(),
 612                            char_bag: participant.user.github_login.chars().collect(),
 613                        }
 614                    }));
 615                let matches = executor.block(match_strings(
 616                    &self.match_candidates,
 617                    &query,
 618                    true,
 619                    usize::MAX,
 620                    &Default::default(),
 621                    executor.clone(),
 622                ));
 623                for mat in matches {
 624                    let user_id = mat.candidate_id as u64;
 625                    let participant = &room.remote_participants()[&user_id];
 626                    self.entries.push(ListEntry::CallParticipant {
 627                        user: participant.user.clone(),
 628                        is_pending: false,
 629                    });
 630                    let mut projects = participant.projects.iter().peekable();
 631                    while let Some(project) = projects.next() {
 632                        self.entries.push(ListEntry::ParticipantProject {
 633                            project_id: project.id,
 634                            worktree_root_names: project.worktree_root_names.clone(),
 635                            host_user_id: participant.user.id,
 636                            is_last: projects.peek().is_none()
 637                                && participant.video_tracks.is_empty(),
 638                        });
 639                    }
 640                    if !participant.video_tracks.is_empty() {
 641                        self.entries.push(ListEntry::ParticipantScreen {
 642                            peer_id: participant.peer_id,
 643                            is_last: true,
 644                        });
 645                    }
 646                }
 647
 648                // Populate pending participants.
 649                self.match_candidates.clear();
 650                self.match_candidates
 651                    .extend(room.pending_participants().iter().enumerate().map(
 652                        |(id, participant)| StringMatchCandidate {
 653                            id,
 654                            string: participant.github_login.clone(),
 655                            char_bag: participant.github_login.chars().collect(),
 656                        },
 657                    ));
 658                let matches = executor.block(match_strings(
 659                    &self.match_candidates,
 660                    &query,
 661                    true,
 662                    usize::MAX,
 663                    &Default::default(),
 664                    executor.clone(),
 665                ));
 666                self.entries
 667                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
 668                        user: room.pending_participants()[mat.candidate_id].clone(),
 669                        is_pending: true,
 670                    }));
 671            }
 672        }
 673
 674        let mut request_entries = Vec::new();
 675        if self.include_channels_section(cx) {
 676            self.entries.push(ListEntry::Header(Section::Channels, 0));
 677
 678            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
 679                self.match_candidates.clear();
 680                self.match_candidates
 681                    .extend(
 682                        channel_store
 683                            .channels()
 684                            .enumerate()
 685                            .map(|(ix, (_, channel))| StringMatchCandidate {
 686                                id: ix,
 687                                string: channel.name.clone(),
 688                                char_bag: channel.name.chars().collect(),
 689                            }),
 690                    );
 691                let matches = executor.block(match_strings(
 692                    &self.match_candidates,
 693                    &query,
 694                    true,
 695                    usize::MAX,
 696                    &Default::default(),
 697                    executor.clone(),
 698                ));
 699                if let Some(state) = &self.channel_editing_state {
 700                    if matches!(
 701                        state,
 702                        ChannelEditingState::Create {
 703                            parent_id: None,
 704                            ..
 705                        }
 706                    ) {
 707                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
 708                    }
 709                }
 710                let mut collapse_depth = None;
 711                for mat in matches {
 712                    let (depth, channel) =
 713                        channel_store.channel_at_index(mat.candidate_id).unwrap();
 714
 715                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
 716                        collapse_depth = Some(depth);
 717                    } else if let Some(collapsed_depth) = collapse_depth {
 718                        if depth > collapsed_depth {
 719                            continue;
 720                        }
 721                        if self.is_channel_collapsed(channel.id) {
 722                            collapse_depth = Some(depth);
 723                        } else {
 724                            collapse_depth = None;
 725                        }
 726                    }
 727
 728                    match &self.channel_editing_state {
 729                        Some(ChannelEditingState::Create { parent_id, .. })
 730                            if *parent_id == Some(channel.id) =>
 731                        {
 732                            self.entries.push(ListEntry::Channel {
 733                                channel: channel.clone(),
 734                                depth,
 735                            });
 736                            self.entries
 737                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
 738                        }
 739                        Some(ChannelEditingState::Rename { channel_id, .. })
 740                            if *channel_id == channel.id =>
 741                        {
 742                            self.entries.push(ListEntry::ChannelEditor { depth });
 743                        }
 744                        _ => {
 745                            self.entries.push(ListEntry::Channel {
 746                                channel: channel.clone(),
 747                                depth,
 748                            });
 749                        }
 750                    }
 751                }
 752            }
 753
 754            let channel_invites = channel_store.channel_invitations();
 755            if !channel_invites.is_empty() {
 756                self.match_candidates.clear();
 757                self.match_candidates
 758                    .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
 759                        StringMatchCandidate {
 760                            id: ix,
 761                            string: channel.name.clone(),
 762                            char_bag: channel.name.chars().collect(),
 763                        }
 764                    }));
 765                let matches = executor.block(match_strings(
 766                    &self.match_candidates,
 767                    &query,
 768                    true,
 769                    usize::MAX,
 770                    &Default::default(),
 771                    executor.clone(),
 772                ));
 773                request_entries.extend(matches.iter().map(|mat| {
 774                    ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
 775                }));
 776
 777                if !request_entries.is_empty() {
 778                    self.entries
 779                        .push(ListEntry::Header(Section::ChannelInvites, 1));
 780                    if !self.collapsed_sections.contains(&Section::ChannelInvites) {
 781                        self.entries.append(&mut request_entries);
 782                    }
 783                }
 784            }
 785        }
 786
 787        self.entries.push(ListEntry::Header(Section::Contacts, 0));
 788
 789        request_entries.clear();
 790        let incoming = user_store.incoming_contact_requests();
 791        if !incoming.is_empty() {
 792            self.match_candidates.clear();
 793            self.match_candidates
 794                .extend(
 795                    incoming
 796                        .iter()
 797                        .enumerate()
 798                        .map(|(ix, user)| StringMatchCandidate {
 799                            id: ix,
 800                            string: user.github_login.clone(),
 801                            char_bag: user.github_login.chars().collect(),
 802                        }),
 803                );
 804            let matches = executor.block(match_strings(
 805                &self.match_candidates,
 806                &query,
 807                true,
 808                usize::MAX,
 809                &Default::default(),
 810                executor.clone(),
 811            ));
 812            request_entries.extend(
 813                matches
 814                    .iter()
 815                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 816            );
 817        }
 818
 819        let outgoing = user_store.outgoing_contact_requests();
 820        if !outgoing.is_empty() {
 821            self.match_candidates.clear();
 822            self.match_candidates
 823                .extend(
 824                    outgoing
 825                        .iter()
 826                        .enumerate()
 827                        .map(|(ix, user)| StringMatchCandidate {
 828                            id: ix,
 829                            string: user.github_login.clone(),
 830                            char_bag: user.github_login.chars().collect(),
 831                        }),
 832                );
 833            let matches = executor.block(match_strings(
 834                &self.match_candidates,
 835                &query,
 836                true,
 837                usize::MAX,
 838                &Default::default(),
 839                executor.clone(),
 840            ));
 841            request_entries.extend(
 842                matches
 843                    .iter()
 844                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
 845            );
 846        }
 847
 848        if !request_entries.is_empty() {
 849            self.entries
 850                .push(ListEntry::Header(Section::ContactRequests, 1));
 851            if !self.collapsed_sections.contains(&Section::ContactRequests) {
 852                self.entries.append(&mut request_entries);
 853            }
 854        }
 855
 856        let contacts = user_store.contacts();
 857        if !contacts.is_empty() {
 858            self.match_candidates.clear();
 859            self.match_candidates
 860                .extend(
 861                    contacts
 862                        .iter()
 863                        .enumerate()
 864                        .map(|(ix, contact)| StringMatchCandidate {
 865                            id: ix,
 866                            string: contact.user.github_login.clone(),
 867                            char_bag: contact.user.github_login.chars().collect(),
 868                        }),
 869                );
 870
 871            let matches = executor.block(match_strings(
 872                &self.match_candidates,
 873                &query,
 874                true,
 875                usize::MAX,
 876                &Default::default(),
 877                executor.clone(),
 878            ));
 879
 880            let (online_contacts, offline_contacts) = matches
 881                .iter()
 882                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
 883
 884            for (matches, section) in [
 885                (online_contacts, Section::Online),
 886                (offline_contacts, Section::Offline),
 887            ] {
 888                if !matches.is_empty() {
 889                    self.entries.push(ListEntry::Header(section, 1));
 890                    if !self.collapsed_sections.contains(&section) {
 891                        let active_call = &ActiveCall::global(cx).read(cx);
 892                        for mat in matches {
 893                            let contact = &contacts[mat.candidate_id];
 894                            self.entries.push(ListEntry::Contact {
 895                                contact: contact.clone(),
 896                                calling: active_call.pending_invites().contains(&contact.user.id),
 897                            });
 898                        }
 899                    }
 900                }
 901            }
 902        }
 903
 904        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
 905            self.entries.push(ListEntry::ContactPlaceholder);
 906        }
 907
 908        if select_same_item {
 909            if let Some(prev_selected_entry) = prev_selected_entry {
 910                self.selection.take();
 911                for (ix, entry) in self.entries.iter().enumerate() {
 912                    if *entry == prev_selected_entry {
 913                        self.selection = Some(ix);
 914                        break;
 915                    }
 916                }
 917            }
 918        } else {
 919            self.selection = self.selection.and_then(|prev_selection| {
 920                if self.entries.is_empty() {
 921                    None
 922                } else {
 923                    Some(prev_selection.min(self.entries.len() - 1))
 924                }
 925            });
 926        }
 927
 928        let old_scroll_top = self.list_state.logical_scroll_top();
 929        self.list_state.reset(self.entries.len());
 930
 931        // Attempt to maintain the same scroll position.
 932        if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
 933            let new_scroll_top = self
 934                .entries
 935                .iter()
 936                .position(|entry| entry == old_top_entry)
 937                .map(|item_ix| ListOffset {
 938                    item_ix,
 939                    offset_in_item: old_scroll_top.offset_in_item,
 940                })
 941                .or_else(|| {
 942                    let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
 943                    let item_ix = self
 944                        .entries
 945                        .iter()
 946                        .position(|entry| entry == entry_after_old_top)?;
 947                    Some(ListOffset {
 948                        item_ix,
 949                        offset_in_item: 0.,
 950                    })
 951                })
 952                .or_else(|| {
 953                    let entry_before_old_top =
 954                        old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
 955                    let item_ix = self
 956                        .entries
 957                        .iter()
 958                        .position(|entry| entry == entry_before_old_top)?;
 959                    Some(ListOffset {
 960                        item_ix,
 961                        offset_in_item: 0.,
 962                    })
 963                });
 964
 965            self.list_state
 966                .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
 967        }
 968
 969        cx.notify();
 970    }
 971
 972    fn render_call_participant(
 973        user: &User,
 974        is_pending: bool,
 975        is_selected: bool,
 976        theme: &theme::CollabPanel,
 977    ) -> AnyElement<Self> {
 978        Flex::row()
 979            .with_children(user.avatar.clone().map(|avatar| {
 980                Image::from_data(avatar)
 981                    .with_style(theme.contact_avatar)
 982                    .aligned()
 983                    .left()
 984            }))
 985            .with_child(
 986                Label::new(
 987                    user.github_login.clone(),
 988                    theme.contact_username.text.clone(),
 989                )
 990                .contained()
 991                .with_style(theme.contact_username.container)
 992                .aligned()
 993                .left()
 994                .flex(1., true),
 995            )
 996            .with_children(if is_pending {
 997                Some(
 998                    Label::new("Calling", theme.calling_indicator.text.clone())
 999                        .contained()
1000                        .with_style(theme.calling_indicator.container)
1001                        .aligned(),
1002                )
1003            } else {
1004                None
1005            })
1006            .constrained()
1007            .with_height(theme.row_height)
1008            .contained()
1009            .with_style(
1010                *theme
1011                    .contact_row
1012                    .in_state(is_selected)
1013                    .style_for(&mut Default::default()),
1014            )
1015            .into_any()
1016    }
1017
1018    fn render_participant_project(
1019        project_id: u64,
1020        worktree_root_names: &[String],
1021        host_user_id: u64,
1022        is_current: bool,
1023        is_last: bool,
1024        is_selected: bool,
1025        theme: &theme::CollabPanel,
1026        cx: &mut ViewContext<Self>,
1027    ) -> AnyElement<Self> {
1028        enum JoinProject {}
1029
1030        let host_avatar_width = theme
1031            .contact_avatar
1032            .width
1033            .or(theme.contact_avatar.height)
1034            .unwrap_or(0.);
1035        let tree_branch = theme.tree_branch;
1036        let project_name = if worktree_root_names.is_empty() {
1037            "untitled".to_string()
1038        } else {
1039            worktree_root_names.join(", ")
1040        };
1041
1042        MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
1043            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1044            let row = theme
1045                .project_row
1046                .in_state(is_selected)
1047                .style_for(mouse_state);
1048
1049            Flex::row()
1050                .with_child(render_tree_branch(
1051                    tree_branch,
1052                    &row.name.text,
1053                    is_last,
1054                    vec2f(host_avatar_width, theme.row_height),
1055                    cx.font_cache(),
1056                ))
1057                .with_child(
1058                    Svg::new("icons/file_icons/folder.svg")
1059                        .with_color(theme.channel_hash.color)
1060                        .constrained()
1061                        .with_width(theme.channel_hash.width)
1062                        .aligned()
1063                        .left(),
1064                )
1065                .with_child(
1066                    Label::new(project_name, row.name.text.clone())
1067                        .aligned()
1068                        .left()
1069                        .contained()
1070                        .with_style(row.name.container)
1071                        .flex(1., false),
1072                )
1073                .constrained()
1074                .with_height(theme.row_height)
1075                .contained()
1076                .with_style(row.container)
1077        })
1078        .with_cursor_style(if !is_current {
1079            CursorStyle::PointingHand
1080        } else {
1081            CursorStyle::Arrow
1082        })
1083        .on_click(MouseButton::Left, move |_, this, cx| {
1084            if !is_current {
1085                if let Some(workspace) = this.workspace.upgrade(cx) {
1086                    let app_state = workspace.read(cx).app_state().clone();
1087                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1088                        .detach_and_log_err(cx);
1089                }
1090            }
1091        })
1092        .into_any()
1093    }
1094
1095    fn render_participant_screen(
1096        peer_id: PeerId,
1097        is_last: bool,
1098        is_selected: bool,
1099        theme: &theme::CollabPanel,
1100        cx: &mut ViewContext<Self>,
1101    ) -> AnyElement<Self> {
1102        enum OpenSharedScreen {}
1103
1104        let font_cache = cx.font_cache();
1105        let host_avatar_height = theme
1106            .contact_avatar
1107            .width
1108            .or(theme.contact_avatar.height)
1109            .unwrap_or(0.);
1110        let row = &theme.project_row.inactive_state().default;
1111        let tree_branch = theme.tree_branch;
1112        let line_height = row.name.text.line_height(font_cache);
1113        let cap_height = row.name.text.cap_height(font_cache);
1114        let baseline_offset =
1115            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
1116
1117        MouseEventHandler::new::<OpenSharedScreen, _>(
1118            peer_id.as_u64() as usize,
1119            cx,
1120            |mouse_state, _| {
1121                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1122                let row = theme
1123                    .project_row
1124                    .in_state(is_selected)
1125                    .style_for(mouse_state);
1126
1127                Flex::row()
1128                    .with_child(
1129                        Stack::new()
1130                            .with_child(Canvas::new(move |scene, bounds, _, _, _| {
1131                                let start_x = bounds.min_x() + (bounds.width() / 2.)
1132                                    - (tree_branch.width / 2.);
1133                                let end_x = bounds.max_x();
1134                                let start_y = bounds.min_y();
1135                                let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
1136
1137                                scene.push_quad(gpui::Quad {
1138                                    bounds: RectF::from_points(
1139                                        vec2f(start_x, start_y),
1140                                        vec2f(
1141                                            start_x + tree_branch.width,
1142                                            if is_last { end_y } else { bounds.max_y() },
1143                                        ),
1144                                    ),
1145                                    background: Some(tree_branch.color),
1146                                    border: gpui::Border::default(),
1147                                    corner_radii: (0.).into(),
1148                                });
1149                                scene.push_quad(gpui::Quad {
1150                                    bounds: RectF::from_points(
1151                                        vec2f(start_x, end_y),
1152                                        vec2f(end_x, end_y + tree_branch.width),
1153                                    ),
1154                                    background: Some(tree_branch.color),
1155                                    border: gpui::Border::default(),
1156                                    corner_radii: (0.).into(),
1157                                });
1158                            }))
1159                            .constrained()
1160                            .with_width(host_avatar_height),
1161                    )
1162                    .with_child(
1163                        Svg::new("icons/disable_screen_sharing_12.svg")
1164                            .with_color(row.icon.color)
1165                            .constrained()
1166                            .with_width(row.icon.width)
1167                            .aligned()
1168                            .left()
1169                            .contained()
1170                            .with_style(row.icon.container),
1171                    )
1172                    .with_child(
1173                        Label::new("Screen", row.name.text.clone())
1174                            .aligned()
1175                            .left()
1176                            .contained()
1177                            .with_style(row.name.container)
1178                            .flex(1., false),
1179                    )
1180                    .constrained()
1181                    .with_height(theme.row_height)
1182                    .contained()
1183                    .with_style(row.container)
1184            },
1185        )
1186        .with_cursor_style(CursorStyle::PointingHand)
1187        .on_click(MouseButton::Left, move |_, this, cx| {
1188            if let Some(workspace) = this.workspace.upgrade(cx) {
1189                workspace.update(cx, |workspace, cx| {
1190                    workspace.open_shared_screen(peer_id, cx)
1191                });
1192            }
1193        })
1194        .into_any()
1195    }
1196
1197    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1198        if let Some(_) = self.channel_editing_state.take() {
1199            self.channel_name_editor.update(cx, |editor, cx| {
1200                editor.set_text("", cx);
1201            });
1202            true
1203        } else {
1204            false
1205        }
1206    }
1207
1208    fn render_header(
1209        &self,
1210        section: Section,
1211        theme: &theme::Theme,
1212        depth: usize,
1213        is_selected: bool,
1214        is_collapsed: bool,
1215        cx: &mut ViewContext<Self>,
1216    ) -> AnyElement<Self> {
1217        enum Header {}
1218        enum LeaveCallContactList {}
1219        enum AddChannel {}
1220
1221        let tooltip_style = &theme.tooltip;
1222        let text = match section {
1223            Section::ActiveCall => {
1224                let channel_name = iife!({
1225                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
1226
1227                    let name = self
1228                        .channel_store
1229                        .read(cx)
1230                        .channel_for_id(channel_id)?
1231                        .name
1232                        .as_str();
1233
1234                    Some(name)
1235                });
1236
1237                if let Some(name) = channel_name {
1238                    Cow::Owned(format!("#{}", name))
1239                } else {
1240                    Cow::Borrowed("Current Call")
1241                }
1242            }
1243            Section::ContactRequests => Cow::Borrowed("Requests"),
1244            Section::Contacts => Cow::Borrowed("Contacts"),
1245            Section::Channels => Cow::Borrowed("Channels"),
1246            Section::ChannelInvites => Cow::Borrowed("Invites"),
1247            Section::Online => Cow::Borrowed("Online"),
1248            Section::Offline => Cow::Borrowed("Offline"),
1249        };
1250
1251        enum AddContact {}
1252        let button = match section {
1253            Section::ActiveCall => Some(
1254                MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
1255                    render_icon_button(
1256                        theme
1257                            .collab_panel
1258                            .leave_call_button
1259                            .style_for(is_selected, state),
1260                        "icons/exit.svg",
1261                    )
1262                })
1263                .with_cursor_style(CursorStyle::PointingHand)
1264                .on_click(MouseButton::Left, |_, _, cx| {
1265                    Self::leave_call(cx);
1266                })
1267                .with_tooltip::<AddContact>(
1268                    0,
1269                    "Leave call",
1270                    None,
1271                    tooltip_style.clone(),
1272                    cx,
1273                ),
1274            ),
1275            Section::Contacts => Some(
1276                MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
1277                    render_icon_button(
1278                        theme
1279                            .collab_panel
1280                            .add_contact_button
1281                            .style_for(is_selected, state),
1282                        "icons/plus_16.svg",
1283                    )
1284                })
1285                .with_cursor_style(CursorStyle::PointingHand)
1286                .on_click(MouseButton::Left, |_, this, cx| {
1287                    this.toggle_contact_finder(cx);
1288                })
1289                .with_tooltip::<LeaveCallContactList>(
1290                    0,
1291                    "Search for new contact",
1292                    None,
1293                    tooltip_style.clone(),
1294                    cx,
1295                ),
1296            ),
1297            Section::Channels => Some(
1298                MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
1299                    render_icon_button(
1300                        theme
1301                            .collab_panel
1302                            .add_contact_button
1303                            .style_for(is_selected, state),
1304                        "icons/plus.svg",
1305                    )
1306                })
1307                .with_cursor_style(CursorStyle::PointingHand)
1308                .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
1309                .with_tooltip::<AddChannel>(
1310                    0,
1311                    "Create a channel",
1312                    None,
1313                    tooltip_style.clone(),
1314                    cx,
1315                ),
1316            ),
1317            _ => None,
1318        };
1319
1320        let can_collapse = depth > 0;
1321        let icon_size = (&theme.collab_panel).section_icon_size;
1322        let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
1323            let header_style = if can_collapse {
1324                theme
1325                    .collab_panel
1326                    .subheader_row
1327                    .in_state(is_selected)
1328                    .style_for(state)
1329            } else {
1330                &theme.collab_panel.header_row
1331            };
1332
1333            Flex::row()
1334                .with_children(if can_collapse {
1335                    Some(
1336                        Svg::new(if is_collapsed {
1337                            "icons/chevron_right.svg"
1338                        } else {
1339                            "icons/chevron_down.svg"
1340                        })
1341                        .with_color(header_style.text.color)
1342                        .constrained()
1343                        .with_max_width(icon_size)
1344                        .with_max_height(icon_size)
1345                        .aligned()
1346                        .constrained()
1347                        .with_width(icon_size)
1348                        .contained()
1349                        .with_margin_right(
1350                            theme.collab_panel.contact_username.container.margin.left,
1351                        ),
1352                    )
1353                } else {
1354                    None
1355                })
1356                .with_child(
1357                    Label::new(text, header_style.text.clone())
1358                        .aligned()
1359                        .left()
1360                        .flex(1., true),
1361                )
1362                .with_children(button.map(|button| button.aligned().right()))
1363                .constrained()
1364                .with_height(theme.collab_panel.row_height)
1365                .contained()
1366                .with_style(header_style.container)
1367        });
1368
1369        if can_collapse {
1370            result = result
1371                .with_cursor_style(CursorStyle::PointingHand)
1372                .on_click(MouseButton::Left, move |_, this, cx| {
1373                    if can_collapse {
1374                        this.toggle_section_expanded(section, cx);
1375                    }
1376                })
1377        }
1378
1379        result.into_any()
1380    }
1381
1382    fn render_contact(
1383        contact: &Contact,
1384        calling: bool,
1385        project: &ModelHandle<Project>,
1386        theme: &theme::CollabPanel,
1387        is_selected: bool,
1388        cx: &mut ViewContext<Self>,
1389    ) -> AnyElement<Self> {
1390        let online = contact.online;
1391        let busy = contact.busy || calling;
1392        let user_id = contact.user.id;
1393        let github_login = contact.user.github_login.clone();
1394        let initial_project = project.clone();
1395        let mut event_handler =
1396            MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
1397                Flex::row()
1398                    .with_children(contact.user.avatar.clone().map(|avatar| {
1399                        let status_badge = if contact.online {
1400                            Some(
1401                                Empty::new()
1402                                    .collapsed()
1403                                    .contained()
1404                                    .with_style(if busy {
1405                                        theme.contact_status_busy
1406                                    } else {
1407                                        theme.contact_status_free
1408                                    })
1409                                    .aligned(),
1410                            )
1411                        } else {
1412                            None
1413                        };
1414                        Stack::new()
1415                            .with_child(
1416                                Image::from_data(avatar)
1417                                    .with_style(theme.contact_avatar)
1418                                    .aligned()
1419                                    .left(),
1420                            )
1421                            .with_children(status_badge)
1422                    }))
1423                    .with_child(
1424                        Label::new(
1425                            contact.user.github_login.clone(),
1426                            theme.contact_username.text.clone(),
1427                        )
1428                        .contained()
1429                        .with_style(theme.contact_username.container)
1430                        .aligned()
1431                        .left()
1432                        .flex(1., true),
1433                    )
1434                    .with_child(
1435                        MouseEventHandler::new::<Cancel, _>(
1436                            contact.user.id as usize,
1437                            cx,
1438                            |mouse_state, _| {
1439                                let button_style = theme.contact_button.style_for(mouse_state);
1440                                render_icon_button(button_style, "icons/x.svg")
1441                                    .aligned()
1442                                    .flex_float()
1443                            },
1444                        )
1445                        .with_padding(Padding::uniform(2.))
1446                        .with_cursor_style(CursorStyle::PointingHand)
1447                        .on_click(MouseButton::Left, move |_, this, cx| {
1448                            this.remove_contact(user_id, &github_login, cx);
1449                        })
1450                        .flex_float(),
1451                    )
1452                    .with_children(if calling {
1453                        Some(
1454                            Label::new("Calling", theme.calling_indicator.text.clone())
1455                                .contained()
1456                                .with_style(theme.calling_indicator.container)
1457                                .aligned(),
1458                        )
1459                    } else {
1460                        None
1461                    })
1462                    .constrained()
1463                    .with_height(theme.row_height)
1464                    .contained()
1465                    .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
1466            })
1467            .on_click(MouseButton::Left, move |_, this, cx| {
1468                if online && !busy {
1469                    this.call(user_id, Some(initial_project.clone()), cx);
1470                }
1471            });
1472
1473        if online {
1474            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
1475        }
1476
1477        event_handler.into_any()
1478    }
1479
1480    fn render_contact_placeholder(
1481        &self,
1482        theme: &theme::CollabPanel,
1483        is_selected: bool,
1484        cx: &mut ViewContext<Self>,
1485    ) -> AnyElement<Self> {
1486        enum AddContacts {}
1487        MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1488            let style = theme.list_empty_state.style_for(is_selected, state);
1489            Flex::row()
1490                .with_child(
1491                    Svg::new("icons/plus.svg")
1492                        .with_color(theme.list_empty_icon.color)
1493                        .constrained()
1494                        .with_width(theme.list_empty_icon.width)
1495                        .aligned()
1496                        .left(),
1497                )
1498                .with_child(
1499                    Label::new("Add a contact", style.text.clone())
1500                        .contained()
1501                        .with_style(theme.list_empty_label_container),
1502                )
1503                .align_children_center()
1504                .contained()
1505                .with_style(style.container)
1506                .into_any()
1507        })
1508        .on_click(MouseButton::Left, |_, this, cx| {
1509            this.toggle_contact_finder(cx);
1510        })
1511        .into_any()
1512    }
1513
1514    fn render_channel_editor(
1515        &self,
1516        theme: &theme::Theme,
1517        depth: usize,
1518        cx: &AppContext,
1519    ) -> AnyElement<Self> {
1520        Flex::row()
1521            .with_child(
1522                Empty::new()
1523                    .constrained()
1524                    .with_width(theme.collab_panel.disclosure.button_space()),
1525            )
1526            .with_child(
1527                Svg::new("icons/hash.svg")
1528                    .with_color(theme.collab_panel.channel_hash.color)
1529                    .constrained()
1530                    .with_width(theme.collab_panel.channel_hash.width)
1531                    .aligned()
1532                    .left(),
1533            )
1534            .with_child(
1535                if let Some(pending_name) = self
1536                    .channel_editing_state
1537                    .as_ref()
1538                    .and_then(|state| state.pending_name())
1539                {
1540                    Label::new(
1541                        pending_name.to_string(),
1542                        theme.collab_panel.contact_username.text.clone(),
1543                    )
1544                    .contained()
1545                    .with_style(theme.collab_panel.contact_username.container)
1546                    .aligned()
1547                    .left()
1548                    .flex(1., true)
1549                    .into_any()
1550                } else {
1551                    ChildView::new(&self.channel_name_editor, cx)
1552                        .aligned()
1553                        .left()
1554                        .contained()
1555                        .with_style(theme.collab_panel.channel_editor)
1556                        .flex(1.0, true)
1557                        .into_any()
1558                },
1559            )
1560            .align_children_center()
1561            .constrained()
1562            .with_height(theme.collab_panel.row_height)
1563            .contained()
1564            .with_style(gpui::elements::ContainerStyle {
1565                background_color: Some(theme.editor.background),
1566                ..*theme.collab_panel.contact_row.default_style()
1567            })
1568            .with_padding_left(
1569                theme.collab_panel.contact_row.default_style().padding.left
1570                    + theme.collab_panel.channel_indent * depth as f32,
1571            )
1572            .into_any()
1573    }
1574
1575    fn render_channel(
1576        &self,
1577        channel: &Channel,
1578        depth: usize,
1579        theme: &theme::CollabPanel,
1580        is_selected: bool,
1581        cx: &mut ViewContext<Self>,
1582    ) -> AnyElement<Self> {
1583        let channel_id = channel.id;
1584        let has_children = self.channel_store.read(cx).has_children(channel_id);
1585        let disclosed =
1586            has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
1587
1588        let is_active = iife!({
1589            let call_channel = ActiveCall::global(cx)
1590                .read(cx)
1591                .room()?
1592                .read(cx)
1593                .channel_id()?;
1594            Some(call_channel == channel_id)
1595        })
1596        .unwrap_or(false);
1597
1598        const FACEPILE_LIMIT: usize = 3;
1599
1600        MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
1601            Flex::<Self>::row()
1602                .with_child(
1603                    Svg::new("icons/hash.svg")
1604                        .with_color(theme.channel_hash.color)
1605                        .constrained()
1606                        .with_width(theme.channel_hash.width)
1607                        .aligned()
1608                        .left(),
1609                )
1610                .with_child(
1611                    Label::new(channel.name.clone(), theme.channel_name.text.clone())
1612                        .contained()
1613                        .with_style(theme.channel_name.container)
1614                        .aligned()
1615                        .left()
1616                        .flex(1., true),
1617                )
1618                .with_children({
1619                    let participants = self.channel_store.read(cx).channel_participants(channel_id);
1620                    if !participants.is_empty() {
1621                        let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
1622
1623                        Some(
1624                            FacePile::new(theme.face_overlap)
1625                                .with_children(
1626                                    participants
1627                                        .iter()
1628                                        .filter_map(|user| {
1629                                            Some(
1630                                                Image::from_data(user.avatar.clone()?)
1631                                                    .with_style(theme.channel_avatar),
1632                                            )
1633                                        })
1634                                        .take(FACEPILE_LIMIT),
1635                                )
1636                                .with_children((extra_count > 0).then(|| {
1637                                    Label::new(
1638                                        format!("+{}", extra_count),
1639                                        theme.extra_participant_label.text.clone(),
1640                                    )
1641                                    .contained()
1642                                    .with_style(theme.extra_participant_label.container)
1643                                })),
1644                        )
1645                    } else {
1646                        None
1647                    }
1648                })
1649                .align_children_center()
1650                .styleable_component()
1651                .disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
1652                .with_id(channel_id as usize)
1653                .with_style(theme.disclosure.clone())
1654                .element()
1655                .constrained()
1656                .with_height(theme.row_height)
1657                .contained()
1658                .with_style(*theme.channel_row.style_for(is_selected || is_active, state))
1659                .with_padding_left(
1660                    theme.channel_row.default_style().padding.left
1661                        + theme.channel_indent * depth as f32,
1662                )
1663        })
1664        .on_click(MouseButton::Left, move |_, this, cx| {
1665            this.join_channel(channel_id, cx);
1666        })
1667        .on_click(MouseButton::Right, move |e, this, cx| {
1668            this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
1669        })
1670        .with_cursor_style(CursorStyle::PointingHand)
1671        .into_any()
1672    }
1673
1674    fn render_channel_notes(
1675        &self,
1676        channel_id: ChannelId,
1677        theme: &theme::CollabPanel,
1678        is_selected: bool,
1679        cx: &mut ViewContext<Self>,
1680    ) -> AnyElement<Self> {
1681        enum ChannelNotes {}
1682        let host_avatar_width = theme
1683            .contact_avatar
1684            .width
1685            .or(theme.contact_avatar.height)
1686            .unwrap_or(0.);
1687
1688        MouseEventHandler::new::<ChannelNotes, _>(channel_id as usize, cx, |state, cx| {
1689            let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
1690            let row = theme.project_row.in_state(is_selected).style_for(state);
1691
1692            Flex::<Self>::row()
1693                .with_child(render_tree_branch(
1694                    tree_branch,
1695                    &row.name.text,
1696                    true,
1697                    vec2f(host_avatar_width, theme.row_height),
1698                    cx.font_cache(),
1699                ))
1700                .with_child(
1701                    Svg::new("icons/radix/file.svg")
1702                        .with_color(theme.channel_hash.color)
1703                        .constrained()
1704                        .with_width(theme.channel_hash.width)
1705                        .aligned()
1706                        .left(),
1707                )
1708                .with_child(
1709                    Label::new("notes", theme.channel_name.text.clone())
1710                        .contained()
1711                        .with_style(theme.channel_name.container)
1712                        .aligned()
1713                        .left()
1714                        .flex(1., true),
1715                )
1716                .constrained()
1717                .with_height(theme.row_height)
1718                .contained()
1719                .with_style(*theme.channel_row.style_for(is_selected, state))
1720                .with_padding_left(theme.channel_row.default_style().padding.left)
1721        })
1722        .on_click(MouseButton::Left, move |_, this, cx| {
1723            this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx);
1724        })
1725        .with_cursor_style(CursorStyle::PointingHand)
1726        .into_any()
1727    }
1728
1729    fn render_channel_invite(
1730        channel: Arc<Channel>,
1731        channel_store: ModelHandle<ChannelStore>,
1732        theme: &theme::CollabPanel,
1733        is_selected: bool,
1734        cx: &mut ViewContext<Self>,
1735    ) -> AnyElement<Self> {
1736        enum Decline {}
1737        enum Accept {}
1738
1739        let channel_id = channel.id;
1740        let is_invite_pending = channel_store
1741            .read(cx)
1742            .has_pending_channel_invite_response(&channel);
1743        let button_spacing = theme.contact_button_spacing;
1744
1745        Flex::row()
1746            .with_child(
1747                Svg::new("icons/hash.svg")
1748                    .with_color(theme.channel_hash.color)
1749                    .constrained()
1750                    .with_width(theme.channel_hash.width)
1751                    .aligned()
1752                    .left(),
1753            )
1754            .with_child(
1755                Label::new(channel.name.clone(), theme.contact_username.text.clone())
1756                    .contained()
1757                    .with_style(theme.contact_username.container)
1758                    .aligned()
1759                    .left()
1760                    .flex(1., true),
1761            )
1762            .with_child(
1763                MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
1764                    let button_style = if is_invite_pending {
1765                        &theme.disabled_button
1766                    } else {
1767                        theme.contact_button.style_for(mouse_state)
1768                    };
1769                    render_icon_button(button_style, "icons/x.svg").aligned()
1770                })
1771                .with_cursor_style(CursorStyle::PointingHand)
1772                .on_click(MouseButton::Left, move |_, this, cx| {
1773                    this.respond_to_channel_invite(channel_id, false, cx);
1774                })
1775                .contained()
1776                .with_margin_right(button_spacing),
1777            )
1778            .with_child(
1779                MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
1780                    let button_style = if is_invite_pending {
1781                        &theme.disabled_button
1782                    } else {
1783                        theme.contact_button.style_for(mouse_state)
1784                    };
1785                    render_icon_button(button_style, "icons/check.svg")
1786                        .aligned()
1787                        .flex_float()
1788                })
1789                .with_cursor_style(CursorStyle::PointingHand)
1790                .on_click(MouseButton::Left, move |_, this, cx| {
1791                    this.respond_to_channel_invite(channel_id, true, cx);
1792                }),
1793            )
1794            .constrained()
1795            .with_height(theme.row_height)
1796            .contained()
1797            .with_style(
1798                *theme
1799                    .contact_row
1800                    .in_state(is_selected)
1801                    .style_for(&mut Default::default()),
1802            )
1803            .with_padding_left(
1804                theme.contact_row.default_style().padding.left + theme.channel_indent,
1805            )
1806            .into_any()
1807    }
1808
1809    fn render_contact_request(
1810        user: Arc<User>,
1811        user_store: ModelHandle<UserStore>,
1812        theme: &theme::CollabPanel,
1813        is_incoming: bool,
1814        is_selected: bool,
1815        cx: &mut ViewContext<Self>,
1816    ) -> AnyElement<Self> {
1817        enum Decline {}
1818        enum Accept {}
1819        enum Cancel {}
1820
1821        let mut row = Flex::row()
1822            .with_children(user.avatar.clone().map(|avatar| {
1823                Image::from_data(avatar)
1824                    .with_style(theme.contact_avatar)
1825                    .aligned()
1826                    .left()
1827            }))
1828            .with_child(
1829                Label::new(
1830                    user.github_login.clone(),
1831                    theme.contact_username.text.clone(),
1832                )
1833                .contained()
1834                .with_style(theme.contact_username.container)
1835                .aligned()
1836                .left()
1837                .flex(1., true),
1838            );
1839
1840        let user_id = user.id;
1841        let github_login = user.github_login.clone();
1842        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
1843        let button_spacing = theme.contact_button_spacing;
1844
1845        if is_incoming {
1846            row.add_child(
1847                MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
1848                    let button_style = if is_contact_request_pending {
1849                        &theme.disabled_button
1850                    } else {
1851                        theme.contact_button.style_for(mouse_state)
1852                    };
1853                    render_icon_button(button_style, "icons/x.svg").aligned()
1854                })
1855                .with_cursor_style(CursorStyle::PointingHand)
1856                .on_click(MouseButton::Left, move |_, this, cx| {
1857                    this.respond_to_contact_request(user_id, false, cx);
1858                })
1859                .contained()
1860                .with_margin_right(button_spacing),
1861            );
1862
1863            row.add_child(
1864                MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
1865                    let button_style = if is_contact_request_pending {
1866                        &theme.disabled_button
1867                    } else {
1868                        theme.contact_button.style_for(mouse_state)
1869                    };
1870                    render_icon_button(button_style, "icons/check.svg")
1871                        .aligned()
1872                        .flex_float()
1873                })
1874                .with_cursor_style(CursorStyle::PointingHand)
1875                .on_click(MouseButton::Left, move |_, this, cx| {
1876                    this.respond_to_contact_request(user_id, true, cx);
1877                }),
1878            );
1879        } else {
1880            row.add_child(
1881                MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
1882                    let button_style = if is_contact_request_pending {
1883                        &theme.disabled_button
1884                    } else {
1885                        theme.contact_button.style_for(mouse_state)
1886                    };
1887                    render_icon_button(button_style, "icons/x.svg")
1888                        .aligned()
1889                        .flex_float()
1890                })
1891                .with_padding(Padding::uniform(2.))
1892                .with_cursor_style(CursorStyle::PointingHand)
1893                .on_click(MouseButton::Left, move |_, this, cx| {
1894                    this.remove_contact(user_id, &github_login, cx);
1895                })
1896                .flex_float(),
1897            );
1898        }
1899
1900        row.constrained()
1901            .with_height(theme.row_height)
1902            .contained()
1903            .with_style(
1904                *theme
1905                    .contact_row
1906                    .in_state(is_selected)
1907                    .style_for(&mut Default::default()),
1908            )
1909            .into_any()
1910    }
1911
1912    fn include_channels_section(&self, cx: &AppContext) -> bool {
1913        if cx.has_global::<StaffMode>() {
1914            cx.global::<StaffMode>().0
1915        } else {
1916            false
1917        }
1918    }
1919
1920    fn deploy_channel_context_menu(
1921        &mut self,
1922        position: Option<Vector2F>,
1923        channel_id: u64,
1924        cx: &mut ViewContext<Self>,
1925    ) {
1926        self.context_menu_on_selected = position.is_none();
1927
1928        self.context_menu.update(cx, |context_menu, cx| {
1929            context_menu.set_position_mode(if self.context_menu_on_selected {
1930                OverlayPositionMode::Local
1931            } else {
1932                OverlayPositionMode::Window
1933            });
1934
1935            let expand_action_name = if self.is_channel_collapsed(channel_id) {
1936                "Expand Subchannels"
1937            } else {
1938                "Collapse Subchannels"
1939            };
1940
1941            let mut items = vec![
1942                ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
1943                ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
1944            ];
1945
1946            if self.channel_store.read(cx).is_user_admin(channel_id) {
1947                items.extend([
1948                    ContextMenuItem::Separator,
1949                    ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
1950                    ContextMenuItem::action("Rename", RenameChannel { channel_id }),
1951                    ContextMenuItem::Separator,
1952                    ContextMenuItem::action("Invite Members", InviteMembers { channel_id }),
1953                    ContextMenuItem::action("Manage Members", ManageMembers { channel_id }),
1954                    ContextMenuItem::Separator,
1955                    ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
1956                ]);
1957            }
1958
1959            context_menu.show(
1960                position.unwrap_or_default(),
1961                if self.context_menu_on_selected {
1962                    gpui::elements::AnchorCorner::TopRight
1963                } else {
1964                    gpui::elements::AnchorCorner::BottomLeft
1965                },
1966                items,
1967                cx,
1968            );
1969        });
1970
1971        cx.notify();
1972    }
1973
1974    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1975        if self.take_editing_state(cx) {
1976            cx.focus(&self.filter_editor);
1977        } else {
1978            self.filter_editor.update(cx, |editor, cx| {
1979                if editor.buffer().read(cx).len(cx) > 0 {
1980                    editor.set_text("", cx);
1981                }
1982            });
1983        }
1984
1985        self.update_entries(false, cx);
1986    }
1987
1988    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1989        let ix = self.selection.map_or(0, |ix| ix + 1);
1990        if ix < self.entries.len() {
1991            self.selection = Some(ix);
1992        }
1993
1994        self.list_state.reset(self.entries.len());
1995        if let Some(ix) = self.selection {
1996            self.list_state.scroll_to(ListOffset {
1997                item_ix: ix,
1998                offset_in_item: 0.,
1999            });
2000        }
2001        cx.notify();
2002    }
2003
2004    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
2005        let ix = self.selection.take().unwrap_or(0);
2006        if ix > 0 {
2007            self.selection = Some(ix - 1);
2008        }
2009
2010        self.list_state.reset(self.entries.len());
2011        if let Some(ix) = self.selection {
2012            self.list_state.scroll_to(ListOffset {
2013                item_ix: ix,
2014                offset_in_item: 0.,
2015            });
2016        }
2017        cx.notify();
2018    }
2019
2020    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
2021        if self.confirm_channel_edit(cx) {
2022            return;
2023        }
2024
2025        if let Some(selection) = self.selection {
2026            if let Some(entry) = self.entries.get(selection) {
2027                match entry {
2028                    ListEntry::Header(section, _) => match section {
2029                        Section::ActiveCall => Self::leave_call(cx),
2030                        Section::Channels => self.new_root_channel(cx),
2031                        Section::Contacts => self.toggle_contact_finder(cx),
2032                        Section::ContactRequests
2033                        | Section::Online
2034                        | Section::Offline
2035                        | Section::ChannelInvites => {
2036                            self.toggle_section_expanded(*section, cx);
2037                        }
2038                    },
2039                    ListEntry::Contact { contact, calling } => {
2040                        if contact.online && !contact.busy && !calling {
2041                            self.call(contact.user.id, Some(self.project.clone()), cx);
2042                        }
2043                    }
2044                    ListEntry::ParticipantProject {
2045                        project_id,
2046                        host_user_id,
2047                        ..
2048                    } => {
2049                        if let Some(workspace) = self.workspace.upgrade(cx) {
2050                            let app_state = workspace.read(cx).app_state().clone();
2051                            workspace::join_remote_project(
2052                                *project_id,
2053                                *host_user_id,
2054                                app_state,
2055                                cx,
2056                            )
2057                            .detach_and_log_err(cx);
2058                        }
2059                    }
2060                    ListEntry::ParticipantScreen { peer_id, .. } => {
2061                        if let Some(workspace) = self.workspace.upgrade(cx) {
2062                            workspace.update(cx, |workspace, cx| {
2063                                workspace.open_shared_screen(*peer_id, cx)
2064                            });
2065                        }
2066                    }
2067                    ListEntry::Channel { channel, .. } => {
2068                        self.join_channel(channel.id, cx);
2069                    }
2070                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
2071                    _ => {}
2072                }
2073            }
2074        }
2075    }
2076
2077    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
2078        if let Some(editing_state) = &mut self.channel_editing_state {
2079            match editing_state {
2080                ChannelEditingState::Create {
2081                    parent_id,
2082                    pending_name,
2083                    ..
2084                } => {
2085                    if pending_name.is_some() {
2086                        return false;
2087                    }
2088                    let channel_name = self.channel_name_editor.read(cx).text(cx);
2089
2090                    *pending_name = Some(channel_name.clone());
2091
2092                    self.channel_store
2093                        .update(cx, |channel_store, cx| {
2094                            channel_store.create_channel(&channel_name, *parent_id, cx)
2095                        })
2096                        .detach();
2097                    cx.notify();
2098                }
2099                ChannelEditingState::Rename {
2100                    channel_id,
2101                    pending_name,
2102                } => {
2103                    if pending_name.is_some() {
2104                        return false;
2105                    }
2106                    let channel_name = self.channel_name_editor.read(cx).text(cx);
2107                    *pending_name = Some(channel_name.clone());
2108
2109                    self.channel_store
2110                        .update(cx, |channel_store, cx| {
2111                            channel_store.rename(*channel_id, &channel_name, cx)
2112                        })
2113                        .detach();
2114                    cx.notify();
2115                }
2116            }
2117            cx.focus_self();
2118            true
2119        } else {
2120            false
2121        }
2122    }
2123
2124    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
2125        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2126            self.collapsed_sections.remove(ix);
2127        } else {
2128            self.collapsed_sections.push(section);
2129        }
2130        self.update_entries(false, cx);
2131    }
2132
2133    fn collapse_selected_channel(
2134        &mut self,
2135        _: &CollapseSelectedChannel,
2136        cx: &mut ViewContext<Self>,
2137    ) {
2138        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
2139            return;
2140        };
2141
2142        if self.is_channel_collapsed(channel_id) {
2143            return;
2144        }
2145
2146        self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
2147    }
2148
2149    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
2150        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
2151            return;
2152        };
2153
2154        if !self.is_channel_collapsed(channel_id) {
2155            return;
2156        }
2157
2158        self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
2159    }
2160
2161    fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
2162        let channel_id = action.channel_id;
2163
2164        match self.collapsed_channels.binary_search(&channel_id) {
2165            Ok(ix) => {
2166                self.collapsed_channels.remove(ix);
2167            }
2168            Err(ix) => {
2169                self.collapsed_channels.insert(ix, channel_id);
2170            }
2171        };
2172        self.serialize(cx);
2173        self.update_entries(true, cx);
2174        cx.notify();
2175        cx.focus_self();
2176    }
2177
2178    fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
2179        self.collapsed_channels.binary_search(&channel).is_ok()
2180    }
2181
2182    fn leave_call(cx: &mut ViewContext<Self>) {
2183        ActiveCall::global(cx)
2184            .update(cx, |call, cx| call.hang_up(cx))
2185            .detach_and_log_err(cx);
2186    }
2187
2188    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
2189        if let Some(workspace) = self.workspace.upgrade(cx) {
2190            workspace.update(cx, |workspace, cx| {
2191                workspace.toggle_modal(cx, |_, cx| {
2192                    cx.add_view(|cx| {
2193                        let mut finder = ContactFinder::new(self.user_store.clone(), cx);
2194                        finder.set_query(self.filter_editor.read(cx).text(cx), cx);
2195                        finder
2196                    })
2197                });
2198            });
2199        }
2200    }
2201
2202    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2203        self.channel_editing_state = Some(ChannelEditingState::Create {
2204            parent_id: None,
2205            pending_name: None,
2206        });
2207        self.update_entries(false, cx);
2208        self.select_channel_editor();
2209        cx.focus(self.channel_name_editor.as_any());
2210        cx.notify();
2211    }
2212
2213    fn select_channel_editor(&mut self) {
2214        self.selection = self.entries.iter().position(|entry| match entry {
2215            ListEntry::ChannelEditor { .. } => true,
2216            _ => false,
2217        });
2218    }
2219
2220    fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
2221        self.collapsed_channels
2222            .retain(|&channel| channel != action.channel_id);
2223        self.channel_editing_state = Some(ChannelEditingState::Create {
2224            parent_id: Some(action.channel_id),
2225            pending_name: None,
2226        });
2227        self.update_entries(false, cx);
2228        self.select_channel_editor();
2229        cx.focus(self.channel_name_editor.as_any());
2230        cx.notify();
2231    }
2232
2233    fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
2234        self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
2235    }
2236
2237    fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
2238        self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
2239    }
2240
2241    fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
2242        if let Some(channel) = self.selected_channel() {
2243            self.remove_channel(channel.id, cx)
2244        }
2245    }
2246
2247    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
2248        if let Some(channel) = self.selected_channel() {
2249            self.rename_channel(
2250                &RenameChannel {
2251                    channel_id: channel.id,
2252                },
2253                cx,
2254            );
2255        }
2256    }
2257
2258    fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
2259        let channel_store = self.channel_store.read(cx);
2260        if !channel_store.is_user_admin(action.channel_id) {
2261            return;
2262        }
2263        if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
2264            self.channel_editing_state = Some(ChannelEditingState::Rename {
2265                channel_id: action.channel_id,
2266                pending_name: None,
2267            });
2268            self.channel_name_editor.update(cx, |editor, cx| {
2269                editor.set_text(channel.name.clone(), cx);
2270                editor.select_all(&Default::default(), cx);
2271            });
2272            cx.focus(self.channel_name_editor.as_any());
2273            self.update_entries(false, cx);
2274            self.select_channel_editor();
2275        }
2276    }
2277
2278    fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
2279        if let Some(workspace) = self.workspace.upgrade(cx) {
2280            let pane = workspace.read(cx).active_pane().clone();
2281            let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx);
2282            cx.spawn(|_, mut cx| async move {
2283                let channel_view = channel_view.await?;
2284                pane.update(&mut cx, |pane, cx| {
2285                    pane.add_item(Box::new(channel_view), true, true, None, cx)
2286                });
2287                anyhow::Ok(())
2288            })
2289            .detach();
2290        }
2291    }
2292
2293    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
2294        let Some(channel) = self.selected_channel() else {
2295            return;
2296        };
2297
2298        self.deploy_channel_context_menu(None, channel.id, cx);
2299    }
2300
2301    fn selected_channel(&self) -> Option<&Arc<Channel>> {
2302        self.selection
2303            .and_then(|ix| self.entries.get(ix))
2304            .and_then(|entry| match entry {
2305                ListEntry::Channel { channel, .. } => Some(channel),
2306                _ => None,
2307            })
2308    }
2309
2310    fn show_channel_modal(
2311        &mut self,
2312        channel_id: ChannelId,
2313        mode: channel_modal::Mode,
2314        cx: &mut ViewContext<Self>,
2315    ) {
2316        let workspace = self.workspace.clone();
2317        let user_store = self.user_store.clone();
2318        let channel_store = self.channel_store.clone();
2319        let members = self.channel_store.update(cx, |channel_store, cx| {
2320            channel_store.get_channel_member_details(channel_id, cx)
2321        });
2322
2323        cx.spawn(|_, mut cx| async move {
2324            let members = members.await?;
2325            workspace.update(&mut cx, |workspace, cx| {
2326                workspace.toggle_modal(cx, |_, cx| {
2327                    cx.add_view(|cx| {
2328                        ChannelModal::new(
2329                            user_store.clone(),
2330                            channel_store.clone(),
2331                            channel_id,
2332                            mode,
2333                            members,
2334                            cx,
2335                        )
2336                    })
2337                });
2338            })
2339        })
2340        .detach();
2341    }
2342
2343    fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
2344        self.remove_channel(action.channel_id, cx)
2345    }
2346
2347    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2348        let channel_store = self.channel_store.clone();
2349        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2350            let prompt_message = format!(
2351                "Are you sure you want to remove the channel \"{}\"?",
2352                channel.name
2353            );
2354            let mut answer =
2355                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2356            let window = cx.window();
2357            cx.spawn(|this, mut cx| async move {
2358                if answer.next().await == Some(0) {
2359                    if let Err(e) = channel_store
2360                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
2361                        .await
2362                    {
2363                        window.prompt(
2364                            PromptLevel::Info,
2365                            &format!("Failed to remove channel: {}", e),
2366                            &["Ok"],
2367                            &mut cx,
2368                        );
2369                    }
2370                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
2371                }
2372            })
2373            .detach();
2374        }
2375    }
2376
2377    // Should move to the filter editor if clicking on it
2378    // Should move selection to the channel editor if activating it
2379
2380    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2381        let user_store = self.user_store.clone();
2382        let prompt_message = format!(
2383            "Are you sure you want to remove \"{}\" from your contacts?",
2384            github_login
2385        );
2386        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2387        let window = cx.window();
2388        cx.spawn(|_, mut cx| async move {
2389            if answer.next().await == Some(0) {
2390                if let Err(e) = user_store
2391                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
2392                    .await
2393                {
2394                    window.prompt(
2395                        PromptLevel::Info,
2396                        &format!("Failed to remove contact: {}", e),
2397                        &["Ok"],
2398                        &mut cx,
2399                    );
2400                }
2401            }
2402        })
2403        .detach();
2404    }
2405
2406    fn respond_to_contact_request(
2407        &mut self,
2408        user_id: u64,
2409        accept: bool,
2410        cx: &mut ViewContext<Self>,
2411    ) {
2412        self.user_store
2413            .update(cx, |store, cx| {
2414                store.respond_to_contact_request(user_id, accept, cx)
2415            })
2416            .detach();
2417    }
2418
2419    fn respond_to_channel_invite(
2420        &mut self,
2421        channel_id: u64,
2422        accept: bool,
2423        cx: &mut ViewContext<Self>,
2424    ) {
2425        let respond = self.channel_store.update(cx, |store, _| {
2426            store.respond_to_channel_invite(channel_id, accept)
2427        });
2428        cx.foreground().spawn(respond).detach();
2429    }
2430
2431    fn call(
2432        &mut self,
2433        recipient_user_id: u64,
2434        initial_project: Option<ModelHandle<Project>>,
2435        cx: &mut ViewContext<Self>,
2436    ) {
2437        ActiveCall::global(cx)
2438            .update(cx, |call, cx| {
2439                call.invite(recipient_user_id, initial_project, cx)
2440            })
2441            .detach_and_log_err(cx);
2442    }
2443
2444    fn join_channel(&self, channel: u64, cx: &mut ViewContext<Self>) {
2445        ActiveCall::global(cx)
2446            .update(cx, |call, cx| call.join_channel(channel, cx))
2447            .detach_and_log_err(cx);
2448    }
2449}
2450
2451fn render_tree_branch(
2452    branch_style: theme::TreeBranch,
2453    row_style: &TextStyle,
2454    is_last: bool,
2455    size: Vector2F,
2456    font_cache: &FontCache,
2457) -> gpui::elements::ConstrainedBox<CollabPanel> {
2458    let line_height = row_style.line_height(font_cache);
2459    let cap_height = row_style.cap_height(font_cache);
2460    let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
2461
2462    Canvas::new(move |scene, bounds, _, _, _| {
2463        scene.paint_layer(None, |scene| {
2464            let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
2465            let end_x = bounds.max_x();
2466            let start_y = bounds.min_y();
2467            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
2468
2469            scene.push_quad(gpui::Quad {
2470                bounds: RectF::from_points(
2471                    vec2f(start_x, start_y),
2472                    vec2f(
2473                        start_x + branch_style.width,
2474                        if is_last { end_y } else { bounds.max_y() },
2475                    ),
2476                ),
2477                background: Some(branch_style.color),
2478                border: gpui::Border::default(),
2479                corner_radii: (0.).into(),
2480            });
2481            scene.push_quad(gpui::Quad {
2482                bounds: RectF::from_points(
2483                    vec2f(start_x, end_y),
2484                    vec2f(end_x, end_y + branch_style.width),
2485                ),
2486                background: Some(branch_style.color),
2487                border: gpui::Border::default(),
2488                corner_radii: (0.).into(),
2489            });
2490        })
2491    })
2492    .constrained()
2493    .with_width(size.x())
2494}
2495
2496impl View for CollabPanel {
2497    fn ui_name() -> &'static str {
2498        "CollabPanel"
2499    }
2500
2501    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2502        if !self.has_focus {
2503            self.has_focus = true;
2504            if !self.context_menu.is_focused(cx) {
2505                if let Some(editing_state) = &self.channel_editing_state {
2506                    if editing_state.pending_name().is_none() {
2507                        cx.focus(&self.channel_name_editor);
2508                    } else {
2509                        cx.focus(&self.filter_editor);
2510                    }
2511                } else {
2512                    cx.focus(&self.filter_editor);
2513                }
2514            }
2515            cx.emit(Event::Focus);
2516        }
2517    }
2518
2519    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
2520        self.has_focus = false;
2521    }
2522
2523    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
2524        let theme = &theme::current(cx).collab_panel;
2525
2526        if self.user_store.read(cx).current_user().is_none() {
2527            enum LogInButton {}
2528
2529            return Flex::column()
2530                .with_child(
2531                    MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
2532                        let button = theme.log_in_button.style_for(state);
2533                        Label::new("Sign in to collaborate", button.text.clone())
2534                            .aligned()
2535                            .left()
2536                            .contained()
2537                            .with_style(button.container)
2538                    })
2539                    .on_click(MouseButton::Left, |_, this, cx| {
2540                        let client = this.client.clone();
2541                        cx.spawn(|_, cx| async move {
2542                            client.authenticate_and_connect(true, &cx).await.log_err();
2543                        })
2544                        .detach();
2545                    })
2546                    .with_cursor_style(CursorStyle::PointingHand),
2547                )
2548                .contained()
2549                .with_style(theme.container)
2550                .into_any();
2551        }
2552
2553        enum PanelFocus {}
2554        MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
2555            Stack::new()
2556                .with_child(
2557                    Flex::column()
2558                        .with_child(
2559                            Flex::row()
2560                                .with_child(
2561                                    ChildView::new(&self.filter_editor, cx)
2562                                        .contained()
2563                                        .with_style(theme.user_query_editor.container)
2564                                        .flex(1.0, true),
2565                                )
2566                                .constrained()
2567                                .with_width(self.size(cx)),
2568                        )
2569                        .with_child(
2570                            List::new(self.list_state.clone())
2571                                .constrained()
2572                                .with_width(self.size(cx))
2573                                .flex(1., true)
2574                                .into_any(),
2575                        )
2576                        .contained()
2577                        .with_style(theme.container)
2578                        .constrained()
2579                        .with_width(self.size(cx))
2580                        .into_any(),
2581                )
2582                .with_children(
2583                    (!self.context_menu_on_selected)
2584                        .then(|| ChildView::new(&self.context_menu, cx)),
2585                )
2586                .into_any()
2587        })
2588        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
2589        .into_any_named("collab panel")
2590    }
2591}
2592
2593impl Panel for CollabPanel {
2594    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
2595        match settings::get::<CollaborationPanelSettings>(cx).dock {
2596            CollaborationPanelDockPosition::Left => DockPosition::Left,
2597            CollaborationPanelDockPosition::Right => DockPosition::Right,
2598        }
2599    }
2600
2601    fn position_is_valid(&self, position: DockPosition) -> bool {
2602        matches!(position, DockPosition::Left | DockPosition::Right)
2603    }
2604
2605    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2606        settings::update_settings_file::<CollaborationPanelSettings>(
2607            self.fs.clone(),
2608            cx,
2609            move |settings| {
2610                let dock = match position {
2611                    DockPosition::Left | DockPosition::Bottom => {
2612                        CollaborationPanelDockPosition::Left
2613                    }
2614                    DockPosition::Right => CollaborationPanelDockPosition::Right,
2615                };
2616                settings.dock = Some(dock);
2617            },
2618        );
2619    }
2620
2621    fn size(&self, cx: &gpui::WindowContext) -> f32 {
2622        self.width
2623            .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
2624    }
2625
2626    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
2627        self.width = size;
2628        self.serialize(cx);
2629        cx.notify();
2630    }
2631
2632    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
2633        settings::get::<CollaborationPanelSettings>(cx)
2634            .button
2635            .then(|| "icons/conversations.svg")
2636    }
2637
2638    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
2639        (
2640            "Collaboration Panel".to_string(),
2641            Some(Box::new(ToggleFocus)),
2642        )
2643    }
2644
2645    fn should_change_position_on_event(event: &Self::Event) -> bool {
2646        matches!(event, Event::DockPositionChanged)
2647    }
2648
2649    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
2650        self.has_focus
2651    }
2652
2653    fn is_focus_event(event: &Self::Event) -> bool {
2654        matches!(event, Event::Focus)
2655    }
2656}
2657
2658impl PartialEq for ListEntry {
2659    fn eq(&self, other: &Self) -> bool {
2660        match self {
2661            ListEntry::Header(section_1, depth_1) => {
2662                if let ListEntry::Header(section_2, depth_2) = other {
2663                    return section_1 == section_2 && depth_1 == depth_2;
2664                }
2665            }
2666            ListEntry::CallParticipant { user: user_1, .. } => {
2667                if let ListEntry::CallParticipant { user: user_2, .. } = other {
2668                    return user_1.id == user_2.id;
2669                }
2670            }
2671            ListEntry::ParticipantProject {
2672                project_id: project_id_1,
2673                ..
2674            } => {
2675                if let ListEntry::ParticipantProject {
2676                    project_id: project_id_2,
2677                    ..
2678                } = other
2679                {
2680                    return project_id_1 == project_id_2;
2681                }
2682            }
2683            ListEntry::ParticipantScreen {
2684                peer_id: peer_id_1, ..
2685            } => {
2686                if let ListEntry::ParticipantScreen {
2687                    peer_id: peer_id_2, ..
2688                } = other
2689                {
2690                    return peer_id_1 == peer_id_2;
2691                }
2692            }
2693            ListEntry::Channel {
2694                channel: channel_1,
2695                depth: depth_1,
2696            } => {
2697                if let ListEntry::Channel {
2698                    channel: channel_2,
2699                    depth: depth_2,
2700                } = other
2701                {
2702                    return channel_1.id == channel_2.id && depth_1 == depth_2;
2703                }
2704            }
2705            ListEntry::ChannelNotes { channel_id } => {
2706                if let ListEntry::ChannelNotes {
2707                    channel_id: other_id,
2708                } = other
2709                {
2710                    return channel_id == other_id;
2711                }
2712            }
2713            ListEntry::ChannelInvite(channel_1) => {
2714                if let ListEntry::ChannelInvite(channel_2) = other {
2715                    return channel_1.id == channel_2.id;
2716                }
2717            }
2718            ListEntry::IncomingRequest(user_1) => {
2719                if let ListEntry::IncomingRequest(user_2) = other {
2720                    return user_1.id == user_2.id;
2721                }
2722            }
2723            ListEntry::OutgoingRequest(user_1) => {
2724                if let ListEntry::OutgoingRequest(user_2) = other {
2725                    return user_1.id == user_2.id;
2726                }
2727            }
2728            ListEntry::Contact {
2729                contact: contact_1, ..
2730            } => {
2731                if let ListEntry::Contact {
2732                    contact: contact_2, ..
2733                } = other
2734                {
2735                    return contact_1.user.id == contact_2.user.id;
2736                }
2737            }
2738            ListEntry::ChannelEditor { depth } => {
2739                if let ListEntry::ChannelEditor { depth: other_depth } = other {
2740                    return depth == other_depth;
2741                }
2742            }
2743            ListEntry::ContactPlaceholder => {
2744                if let ListEntry::ContactPlaceholder = other {
2745                    return true;
2746                }
2747            }
2748        }
2749        false
2750    }
2751}
2752
2753fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
2754    Svg::new(svg_path)
2755        .with_color(style.color)
2756        .constrained()
2757        .with_width(style.icon_width)
2758        .aligned()
2759        .constrained()
2760        .with_width(style.button_width)
2761        .with_height(style.button_width)
2762        .contained()
2763        .with_style(style.container)
2764}