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