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 host_avatar_width = theme
1110            .contact_avatar
1111            .width
1112            .or(theme.contact_avatar.height)
1113            .unwrap_or(0.);
1114        let tree_branch = theme.tree_branch;
1115
1116        MouseEventHandler::new::<OpenSharedScreen, _>(
1117            peer_id.as_u64() as usize,
1118            cx,
1119            |mouse_state, cx| {
1120                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1121                let row = theme
1122                    .project_row
1123                    .in_state(is_selected)
1124                    .style_for(mouse_state);
1125
1126                Flex::row()
1127                    .with_child(render_tree_branch(
1128                        tree_branch,
1129                        &row.name.text,
1130                        is_last,
1131                        vec2f(host_avatar_width, theme.row_height),
1132                        cx.font_cache(),
1133                    ))
1134                    .with_child(
1135                        Svg::new("icons/disable_screen_sharing_12.svg")
1136                            .with_color(theme.channel_hash.color)
1137                            .constrained()
1138                            .with_width(theme.channel_hash.width)
1139                            .aligned()
1140                            .left(),
1141                    )
1142                    .with_child(
1143                        Label::new("Screen", row.name.text.clone())
1144                            .aligned()
1145                            .left()
1146                            .contained()
1147                            .with_style(row.name.container)
1148                            .flex(1., false),
1149                    )
1150                    .constrained()
1151                    .with_height(theme.row_height)
1152                    .contained()
1153                    .with_style(row.container)
1154            },
1155        )
1156        .with_cursor_style(CursorStyle::PointingHand)
1157        .on_click(MouseButton::Left, move |_, this, cx| {
1158            if let Some(workspace) = this.workspace.upgrade(cx) {
1159                workspace.update(cx, |workspace, cx| {
1160                    workspace.open_shared_screen(peer_id, cx)
1161                });
1162            }
1163        })
1164        .into_any()
1165    }
1166
1167    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1168        if let Some(_) = self.channel_editing_state.take() {
1169            self.channel_name_editor.update(cx, |editor, cx| {
1170                editor.set_text("", cx);
1171            });
1172            true
1173        } else {
1174            false
1175        }
1176    }
1177
1178    fn render_header(
1179        &self,
1180        section: Section,
1181        theme: &theme::Theme,
1182        depth: usize,
1183        is_selected: bool,
1184        is_collapsed: bool,
1185        cx: &mut ViewContext<Self>,
1186    ) -> AnyElement<Self> {
1187        enum Header {}
1188        enum LeaveCallContactList {}
1189        enum AddChannel {}
1190
1191        let tooltip_style = &theme.tooltip;
1192        let text = match section {
1193            Section::ActiveCall => {
1194                let channel_name = iife!({
1195                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
1196
1197                    let name = self
1198                        .channel_store
1199                        .read(cx)
1200                        .channel_for_id(channel_id)?
1201                        .name
1202                        .as_str();
1203
1204                    Some(name)
1205                });
1206
1207                if let Some(name) = channel_name {
1208                    Cow::Owned(format!("#{}", name))
1209                } else {
1210                    Cow::Borrowed("Current Call")
1211                }
1212            }
1213            Section::ContactRequests => Cow::Borrowed("Requests"),
1214            Section::Contacts => Cow::Borrowed("Contacts"),
1215            Section::Channels => Cow::Borrowed("Channels"),
1216            Section::ChannelInvites => Cow::Borrowed("Invites"),
1217            Section::Online => Cow::Borrowed("Online"),
1218            Section::Offline => Cow::Borrowed("Offline"),
1219        };
1220
1221        enum AddContact {}
1222        let button = match section {
1223            Section::ActiveCall => Some(
1224                MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
1225                    render_icon_button(
1226                        theme
1227                            .collab_panel
1228                            .leave_call_button
1229                            .style_for(is_selected, state),
1230                        "icons/exit.svg",
1231                    )
1232                })
1233                .with_cursor_style(CursorStyle::PointingHand)
1234                .on_click(MouseButton::Left, |_, _, cx| {
1235                    Self::leave_call(cx);
1236                })
1237                .with_tooltip::<AddContact>(
1238                    0,
1239                    "Leave call",
1240                    None,
1241                    tooltip_style.clone(),
1242                    cx,
1243                ),
1244            ),
1245            Section::Contacts => Some(
1246                MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
1247                    render_icon_button(
1248                        theme
1249                            .collab_panel
1250                            .add_contact_button
1251                            .style_for(is_selected, state),
1252                        "icons/plus_16.svg",
1253                    )
1254                })
1255                .with_cursor_style(CursorStyle::PointingHand)
1256                .on_click(MouseButton::Left, |_, this, cx| {
1257                    this.toggle_contact_finder(cx);
1258                })
1259                .with_tooltip::<LeaveCallContactList>(
1260                    0,
1261                    "Search for new contact",
1262                    None,
1263                    tooltip_style.clone(),
1264                    cx,
1265                ),
1266            ),
1267            Section::Channels => Some(
1268                MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
1269                    render_icon_button(
1270                        theme
1271                            .collab_panel
1272                            .add_contact_button
1273                            .style_for(is_selected, state),
1274                        "icons/plus.svg",
1275                    )
1276                })
1277                .with_cursor_style(CursorStyle::PointingHand)
1278                .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
1279                .with_tooltip::<AddChannel>(
1280                    0,
1281                    "Create a channel",
1282                    None,
1283                    tooltip_style.clone(),
1284                    cx,
1285                ),
1286            ),
1287            _ => None,
1288        };
1289
1290        let can_collapse = depth > 0;
1291        let icon_size = (&theme.collab_panel).section_icon_size;
1292        let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
1293            let header_style = if can_collapse {
1294                theme
1295                    .collab_panel
1296                    .subheader_row
1297                    .in_state(is_selected)
1298                    .style_for(state)
1299            } else {
1300                &theme.collab_panel.header_row
1301            };
1302
1303            Flex::row()
1304                .with_children(if can_collapse {
1305                    Some(
1306                        Svg::new(if is_collapsed {
1307                            "icons/chevron_right.svg"
1308                        } else {
1309                            "icons/chevron_down.svg"
1310                        })
1311                        .with_color(header_style.text.color)
1312                        .constrained()
1313                        .with_max_width(icon_size)
1314                        .with_max_height(icon_size)
1315                        .aligned()
1316                        .constrained()
1317                        .with_width(icon_size)
1318                        .contained()
1319                        .with_margin_right(
1320                            theme.collab_panel.contact_username.container.margin.left,
1321                        ),
1322                    )
1323                } else {
1324                    None
1325                })
1326                .with_child(
1327                    Label::new(text, header_style.text.clone())
1328                        .aligned()
1329                        .left()
1330                        .flex(1., true),
1331                )
1332                .with_children(button.map(|button| button.aligned().right()))
1333                .constrained()
1334                .with_height(theme.collab_panel.row_height)
1335                .contained()
1336                .with_style(header_style.container)
1337        });
1338
1339        if can_collapse {
1340            result = result
1341                .with_cursor_style(CursorStyle::PointingHand)
1342                .on_click(MouseButton::Left, move |_, this, cx| {
1343                    if can_collapse {
1344                        this.toggle_section_expanded(section, cx);
1345                    }
1346                })
1347        }
1348
1349        result.into_any()
1350    }
1351
1352    fn render_contact(
1353        contact: &Contact,
1354        calling: bool,
1355        project: &ModelHandle<Project>,
1356        theme: &theme::CollabPanel,
1357        is_selected: bool,
1358        cx: &mut ViewContext<Self>,
1359    ) -> AnyElement<Self> {
1360        let online = contact.online;
1361        let busy = contact.busy || calling;
1362        let user_id = contact.user.id;
1363        let github_login = contact.user.github_login.clone();
1364        let initial_project = project.clone();
1365        let mut event_handler =
1366            MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
1367                Flex::row()
1368                    .with_children(contact.user.avatar.clone().map(|avatar| {
1369                        let status_badge = if contact.online {
1370                            Some(
1371                                Empty::new()
1372                                    .collapsed()
1373                                    .contained()
1374                                    .with_style(if busy {
1375                                        theme.contact_status_busy
1376                                    } else {
1377                                        theme.contact_status_free
1378                                    })
1379                                    .aligned(),
1380                            )
1381                        } else {
1382                            None
1383                        };
1384                        Stack::new()
1385                            .with_child(
1386                                Image::from_data(avatar)
1387                                    .with_style(theme.contact_avatar)
1388                                    .aligned()
1389                                    .left(),
1390                            )
1391                            .with_children(status_badge)
1392                    }))
1393                    .with_child(
1394                        Label::new(
1395                            contact.user.github_login.clone(),
1396                            theme.contact_username.text.clone(),
1397                        )
1398                        .contained()
1399                        .with_style(theme.contact_username.container)
1400                        .aligned()
1401                        .left()
1402                        .flex(1., true),
1403                    )
1404                    .with_child(
1405                        MouseEventHandler::new::<Cancel, _>(
1406                            contact.user.id as usize,
1407                            cx,
1408                            |mouse_state, _| {
1409                                let button_style = theme.contact_button.style_for(mouse_state);
1410                                render_icon_button(button_style, "icons/x.svg")
1411                                    .aligned()
1412                                    .flex_float()
1413                            },
1414                        )
1415                        .with_padding(Padding::uniform(2.))
1416                        .with_cursor_style(CursorStyle::PointingHand)
1417                        .on_click(MouseButton::Left, move |_, this, cx| {
1418                            this.remove_contact(user_id, &github_login, cx);
1419                        })
1420                        .flex_float(),
1421                    )
1422                    .with_children(if calling {
1423                        Some(
1424                            Label::new("Calling", theme.calling_indicator.text.clone())
1425                                .contained()
1426                                .with_style(theme.calling_indicator.container)
1427                                .aligned(),
1428                        )
1429                    } else {
1430                        None
1431                    })
1432                    .constrained()
1433                    .with_height(theme.row_height)
1434                    .contained()
1435                    .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
1436            })
1437            .on_click(MouseButton::Left, move |_, this, cx| {
1438                if online && !busy {
1439                    this.call(user_id, Some(initial_project.clone()), cx);
1440                }
1441            });
1442
1443        if online {
1444            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
1445        }
1446
1447        event_handler.into_any()
1448    }
1449
1450    fn render_contact_placeholder(
1451        &self,
1452        theme: &theme::CollabPanel,
1453        is_selected: bool,
1454        cx: &mut ViewContext<Self>,
1455    ) -> AnyElement<Self> {
1456        enum AddContacts {}
1457        MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1458            let style = theme.list_empty_state.style_for(is_selected, state);
1459            Flex::row()
1460                .with_child(
1461                    Svg::new("icons/plus.svg")
1462                        .with_color(theme.list_empty_icon.color)
1463                        .constrained()
1464                        .with_width(theme.list_empty_icon.width)
1465                        .aligned()
1466                        .left(),
1467                )
1468                .with_child(
1469                    Label::new("Add a contact", style.text.clone())
1470                        .contained()
1471                        .with_style(theme.list_empty_label_container),
1472                )
1473                .align_children_center()
1474                .contained()
1475                .with_style(style.container)
1476                .into_any()
1477        })
1478        .on_click(MouseButton::Left, |_, this, cx| {
1479            this.toggle_contact_finder(cx);
1480        })
1481        .into_any()
1482    }
1483
1484    fn render_channel_editor(
1485        &self,
1486        theme: &theme::Theme,
1487        depth: usize,
1488        cx: &AppContext,
1489    ) -> AnyElement<Self> {
1490        Flex::row()
1491            .with_child(
1492                Empty::new()
1493                    .constrained()
1494                    .with_width(theme.collab_panel.disclosure.button_space()),
1495            )
1496            .with_child(
1497                Svg::new("icons/hash.svg")
1498                    .with_color(theme.collab_panel.channel_hash.color)
1499                    .constrained()
1500                    .with_width(theme.collab_panel.channel_hash.width)
1501                    .aligned()
1502                    .left(),
1503            )
1504            .with_child(
1505                if let Some(pending_name) = self
1506                    .channel_editing_state
1507                    .as_ref()
1508                    .and_then(|state| state.pending_name())
1509                {
1510                    Label::new(
1511                        pending_name.to_string(),
1512                        theme.collab_panel.contact_username.text.clone(),
1513                    )
1514                    .contained()
1515                    .with_style(theme.collab_panel.contact_username.container)
1516                    .aligned()
1517                    .left()
1518                    .flex(1., true)
1519                    .into_any()
1520                } else {
1521                    ChildView::new(&self.channel_name_editor, cx)
1522                        .aligned()
1523                        .left()
1524                        .contained()
1525                        .with_style(theme.collab_panel.channel_editor)
1526                        .flex(1.0, true)
1527                        .into_any()
1528                },
1529            )
1530            .align_children_center()
1531            .constrained()
1532            .with_height(theme.collab_panel.row_height)
1533            .contained()
1534            .with_style(gpui::elements::ContainerStyle {
1535                background_color: Some(theme.editor.background),
1536                ..*theme.collab_panel.contact_row.default_style()
1537            })
1538            .with_padding_left(
1539                theme.collab_panel.contact_row.default_style().padding.left
1540                    + theme.collab_panel.channel_indent * depth as f32,
1541            )
1542            .into_any()
1543    }
1544
1545    fn render_channel(
1546        &self,
1547        channel: &Channel,
1548        depth: usize,
1549        theme: &theme::CollabPanel,
1550        is_selected: bool,
1551        cx: &mut ViewContext<Self>,
1552    ) -> AnyElement<Self> {
1553        let channel_id = channel.id;
1554        let has_children = self.channel_store.read(cx).has_children(channel_id);
1555        let disclosed =
1556            has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
1557
1558        let is_active = iife!({
1559            let call_channel = ActiveCall::global(cx)
1560                .read(cx)
1561                .room()?
1562                .read(cx)
1563                .channel_id()?;
1564            Some(call_channel == channel_id)
1565        })
1566        .unwrap_or(false);
1567
1568        const FACEPILE_LIMIT: usize = 3;
1569
1570        MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
1571            Flex::<Self>::row()
1572                .with_child(
1573                    Svg::new("icons/hash.svg")
1574                        .with_color(theme.channel_hash.color)
1575                        .constrained()
1576                        .with_width(theme.channel_hash.width)
1577                        .aligned()
1578                        .left(),
1579                )
1580                .with_child(
1581                    Label::new(channel.name.clone(), theme.channel_name.text.clone())
1582                        .contained()
1583                        .with_style(theme.channel_name.container)
1584                        .aligned()
1585                        .left()
1586                        .flex(1., true),
1587                )
1588                .with_children({
1589                    let participants = self.channel_store.read(cx).channel_participants(channel_id);
1590                    if !participants.is_empty() {
1591                        let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
1592
1593                        Some(
1594                            FacePile::new(theme.face_overlap)
1595                                .with_children(
1596                                    participants
1597                                        .iter()
1598                                        .filter_map(|user| {
1599                                            Some(
1600                                                Image::from_data(user.avatar.clone()?)
1601                                                    .with_style(theme.channel_avatar),
1602                                            )
1603                                        })
1604                                        .take(FACEPILE_LIMIT),
1605                                )
1606                                .with_children((extra_count > 0).then(|| {
1607                                    Label::new(
1608                                        format!("+{}", extra_count),
1609                                        theme.extra_participant_label.text.clone(),
1610                                    )
1611                                    .contained()
1612                                    .with_style(theme.extra_participant_label.container)
1613                                })),
1614                        )
1615                    } else {
1616                        None
1617                    }
1618                })
1619                .align_children_center()
1620                .styleable_component()
1621                .disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
1622                .with_id(channel_id as usize)
1623                .with_style(theme.disclosure.clone())
1624                .element()
1625                .constrained()
1626                .with_height(theme.row_height)
1627                .contained()
1628                .with_style(*theme.channel_row.style_for(is_selected || is_active, state))
1629                .with_padding_left(
1630                    theme.channel_row.default_style().padding.left
1631                        + theme.channel_indent * depth as f32,
1632                )
1633        })
1634        .on_click(MouseButton::Left, move |_, this, cx| {
1635            this.join_channel(channel_id, cx);
1636        })
1637        .on_click(MouseButton::Right, move |e, this, cx| {
1638            this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
1639        })
1640        .with_cursor_style(CursorStyle::PointingHand)
1641        .into_any()
1642    }
1643
1644    fn render_channel_notes(
1645        &self,
1646        channel_id: ChannelId,
1647        theme: &theme::CollabPanel,
1648        is_selected: bool,
1649        cx: &mut ViewContext<Self>,
1650    ) -> AnyElement<Self> {
1651        enum ChannelNotes {}
1652        let host_avatar_width = theme
1653            .contact_avatar
1654            .width
1655            .or(theme.contact_avatar.height)
1656            .unwrap_or(0.);
1657
1658        MouseEventHandler::new::<ChannelNotes, _>(channel_id as usize, cx, |state, cx| {
1659            let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
1660            let row = theme.project_row.in_state(is_selected).style_for(state);
1661
1662            Flex::<Self>::row()
1663                .with_child(render_tree_branch(
1664                    tree_branch,
1665                    &row.name.text,
1666                    true,
1667                    vec2f(host_avatar_width, theme.row_height),
1668                    cx.font_cache(),
1669                ))
1670                .with_child(
1671                    Svg::new("icons/radix/file.svg")
1672                        .with_color(theme.channel_hash.color)
1673                        .constrained()
1674                        .with_width(theme.channel_hash.width)
1675                        .aligned()
1676                        .left(),
1677                )
1678                .with_child(
1679                    Label::new("notes", theme.channel_name.text.clone())
1680                        .contained()
1681                        .with_style(theme.channel_name.container)
1682                        .aligned()
1683                        .left()
1684                        .flex(1., true),
1685                )
1686                .constrained()
1687                .with_height(theme.row_height)
1688                .contained()
1689                .with_style(*theme.channel_row.style_for(is_selected, state))
1690                .with_padding_left(theme.channel_row.default_style().padding.left)
1691        })
1692        .on_click(MouseButton::Left, move |_, this, cx| {
1693            this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx);
1694        })
1695        .with_cursor_style(CursorStyle::PointingHand)
1696        .into_any()
1697    }
1698
1699    fn render_channel_invite(
1700        channel: Arc<Channel>,
1701        channel_store: ModelHandle<ChannelStore>,
1702        theme: &theme::CollabPanel,
1703        is_selected: bool,
1704        cx: &mut ViewContext<Self>,
1705    ) -> AnyElement<Self> {
1706        enum Decline {}
1707        enum Accept {}
1708
1709        let channel_id = channel.id;
1710        let is_invite_pending = channel_store
1711            .read(cx)
1712            .has_pending_channel_invite_response(&channel);
1713        let button_spacing = theme.contact_button_spacing;
1714
1715        Flex::row()
1716            .with_child(
1717                Svg::new("icons/hash.svg")
1718                    .with_color(theme.channel_hash.color)
1719                    .constrained()
1720                    .with_width(theme.channel_hash.width)
1721                    .aligned()
1722                    .left(),
1723            )
1724            .with_child(
1725                Label::new(channel.name.clone(), theme.contact_username.text.clone())
1726                    .contained()
1727                    .with_style(theme.contact_username.container)
1728                    .aligned()
1729                    .left()
1730                    .flex(1., true),
1731            )
1732            .with_child(
1733                MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
1734                    let button_style = if is_invite_pending {
1735                        &theme.disabled_button
1736                    } else {
1737                        theme.contact_button.style_for(mouse_state)
1738                    };
1739                    render_icon_button(button_style, "icons/x.svg").aligned()
1740                })
1741                .with_cursor_style(CursorStyle::PointingHand)
1742                .on_click(MouseButton::Left, move |_, this, cx| {
1743                    this.respond_to_channel_invite(channel_id, false, cx);
1744                })
1745                .contained()
1746                .with_margin_right(button_spacing),
1747            )
1748            .with_child(
1749                MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
1750                    let button_style = if is_invite_pending {
1751                        &theme.disabled_button
1752                    } else {
1753                        theme.contact_button.style_for(mouse_state)
1754                    };
1755                    render_icon_button(button_style, "icons/check.svg")
1756                        .aligned()
1757                        .flex_float()
1758                })
1759                .with_cursor_style(CursorStyle::PointingHand)
1760                .on_click(MouseButton::Left, move |_, this, cx| {
1761                    this.respond_to_channel_invite(channel_id, true, cx);
1762                }),
1763            )
1764            .constrained()
1765            .with_height(theme.row_height)
1766            .contained()
1767            .with_style(
1768                *theme
1769                    .contact_row
1770                    .in_state(is_selected)
1771                    .style_for(&mut Default::default()),
1772            )
1773            .with_padding_left(
1774                theme.contact_row.default_style().padding.left + theme.channel_indent,
1775            )
1776            .into_any()
1777    }
1778
1779    fn render_contact_request(
1780        user: Arc<User>,
1781        user_store: ModelHandle<UserStore>,
1782        theme: &theme::CollabPanel,
1783        is_incoming: bool,
1784        is_selected: bool,
1785        cx: &mut ViewContext<Self>,
1786    ) -> AnyElement<Self> {
1787        enum Decline {}
1788        enum Accept {}
1789        enum Cancel {}
1790
1791        let mut row = Flex::row()
1792            .with_children(user.avatar.clone().map(|avatar| {
1793                Image::from_data(avatar)
1794                    .with_style(theme.contact_avatar)
1795                    .aligned()
1796                    .left()
1797            }))
1798            .with_child(
1799                Label::new(
1800                    user.github_login.clone(),
1801                    theme.contact_username.text.clone(),
1802                )
1803                .contained()
1804                .with_style(theme.contact_username.container)
1805                .aligned()
1806                .left()
1807                .flex(1., true),
1808            );
1809
1810        let user_id = user.id;
1811        let github_login = user.github_login.clone();
1812        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
1813        let button_spacing = theme.contact_button_spacing;
1814
1815        if is_incoming {
1816            row.add_child(
1817                MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
1818                    let button_style = if is_contact_request_pending {
1819                        &theme.disabled_button
1820                    } else {
1821                        theme.contact_button.style_for(mouse_state)
1822                    };
1823                    render_icon_button(button_style, "icons/x.svg").aligned()
1824                })
1825                .with_cursor_style(CursorStyle::PointingHand)
1826                .on_click(MouseButton::Left, move |_, this, cx| {
1827                    this.respond_to_contact_request(user_id, false, cx);
1828                })
1829                .contained()
1830                .with_margin_right(button_spacing),
1831            );
1832
1833            row.add_child(
1834                MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
1835                    let button_style = if is_contact_request_pending {
1836                        &theme.disabled_button
1837                    } else {
1838                        theme.contact_button.style_for(mouse_state)
1839                    };
1840                    render_icon_button(button_style, "icons/check.svg")
1841                        .aligned()
1842                        .flex_float()
1843                })
1844                .with_cursor_style(CursorStyle::PointingHand)
1845                .on_click(MouseButton::Left, move |_, this, cx| {
1846                    this.respond_to_contact_request(user_id, true, cx);
1847                }),
1848            );
1849        } else {
1850            row.add_child(
1851                MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
1852                    let button_style = if is_contact_request_pending {
1853                        &theme.disabled_button
1854                    } else {
1855                        theme.contact_button.style_for(mouse_state)
1856                    };
1857                    render_icon_button(button_style, "icons/x.svg")
1858                        .aligned()
1859                        .flex_float()
1860                })
1861                .with_padding(Padding::uniform(2.))
1862                .with_cursor_style(CursorStyle::PointingHand)
1863                .on_click(MouseButton::Left, move |_, this, cx| {
1864                    this.remove_contact(user_id, &github_login, cx);
1865                })
1866                .flex_float(),
1867            );
1868        }
1869
1870        row.constrained()
1871            .with_height(theme.row_height)
1872            .contained()
1873            .with_style(
1874                *theme
1875                    .contact_row
1876                    .in_state(is_selected)
1877                    .style_for(&mut Default::default()),
1878            )
1879            .into_any()
1880    }
1881
1882    fn deploy_channel_context_menu(
1883        &mut self,
1884        position: Option<Vector2F>,
1885        channel_id: u64,
1886        cx: &mut ViewContext<Self>,
1887    ) {
1888        self.context_menu_on_selected = position.is_none();
1889
1890        self.context_menu.update(cx, |context_menu, cx| {
1891            context_menu.set_position_mode(if self.context_menu_on_selected {
1892                OverlayPositionMode::Local
1893            } else {
1894                OverlayPositionMode::Window
1895            });
1896
1897            let expand_action_name = if self.is_channel_collapsed(channel_id) {
1898                "Expand Subchannels"
1899            } else {
1900                "Collapse Subchannels"
1901            };
1902
1903            let mut items = vec![
1904                ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
1905                ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }),
1906            ];
1907
1908            if self.channel_store.read(cx).is_user_admin(channel_id) {
1909                items.extend([
1910                    ContextMenuItem::Separator,
1911                    ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
1912                    ContextMenuItem::action("Rename", RenameChannel { channel_id }),
1913                    ContextMenuItem::Separator,
1914                    ContextMenuItem::action("Invite Members", InviteMembers { channel_id }),
1915                    ContextMenuItem::action("Manage Members", ManageMembers { channel_id }),
1916                    ContextMenuItem::Separator,
1917                    ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
1918                ]);
1919            }
1920
1921            context_menu.show(
1922                position.unwrap_or_default(),
1923                if self.context_menu_on_selected {
1924                    gpui::elements::AnchorCorner::TopRight
1925                } else {
1926                    gpui::elements::AnchorCorner::BottomLeft
1927                },
1928                items,
1929                cx,
1930            );
1931        });
1932
1933        cx.notify();
1934    }
1935
1936    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1937        if self.take_editing_state(cx) {
1938            cx.focus(&self.filter_editor);
1939        } else {
1940            self.filter_editor.update(cx, |editor, cx| {
1941                if editor.buffer().read(cx).len(cx) > 0 {
1942                    editor.set_text("", cx);
1943                }
1944            });
1945        }
1946
1947        self.update_entries(false, cx);
1948    }
1949
1950    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1951        let ix = self.selection.map_or(0, |ix| ix + 1);
1952        if ix < self.entries.len() {
1953            self.selection = Some(ix);
1954        }
1955
1956        self.list_state.reset(self.entries.len());
1957        if let Some(ix) = self.selection {
1958            self.list_state.scroll_to(ListOffset {
1959                item_ix: ix,
1960                offset_in_item: 0.,
1961            });
1962        }
1963        cx.notify();
1964    }
1965
1966    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1967        let ix = self.selection.take().unwrap_or(0);
1968        if ix > 0 {
1969            self.selection = Some(ix - 1);
1970        }
1971
1972        self.list_state.reset(self.entries.len());
1973        if let Some(ix) = self.selection {
1974            self.list_state.scroll_to(ListOffset {
1975                item_ix: ix,
1976                offset_in_item: 0.,
1977            });
1978        }
1979        cx.notify();
1980    }
1981
1982    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1983        if self.confirm_channel_edit(cx) {
1984            return;
1985        }
1986
1987        if let Some(selection) = self.selection {
1988            if let Some(entry) = self.entries.get(selection) {
1989                match entry {
1990                    ListEntry::Header(section, _) => match section {
1991                        Section::ActiveCall => Self::leave_call(cx),
1992                        Section::Channels => self.new_root_channel(cx),
1993                        Section::Contacts => self.toggle_contact_finder(cx),
1994                        Section::ContactRequests
1995                        | Section::Online
1996                        | Section::Offline
1997                        | Section::ChannelInvites => {
1998                            self.toggle_section_expanded(*section, cx);
1999                        }
2000                    },
2001                    ListEntry::Contact { contact, calling } => {
2002                        if contact.online && !contact.busy && !calling {
2003                            self.call(contact.user.id, Some(self.project.clone()), cx);
2004                        }
2005                    }
2006                    ListEntry::ParticipantProject {
2007                        project_id,
2008                        host_user_id,
2009                        ..
2010                    } => {
2011                        if let Some(workspace) = self.workspace.upgrade(cx) {
2012                            let app_state = workspace.read(cx).app_state().clone();
2013                            workspace::join_remote_project(
2014                                *project_id,
2015                                *host_user_id,
2016                                app_state,
2017                                cx,
2018                            )
2019                            .detach_and_log_err(cx);
2020                        }
2021                    }
2022                    ListEntry::ParticipantScreen { peer_id, .. } => {
2023                        if let Some(workspace) = self.workspace.upgrade(cx) {
2024                            workspace.update(cx, |workspace, cx| {
2025                                workspace.open_shared_screen(*peer_id, cx)
2026                            });
2027                        }
2028                    }
2029                    ListEntry::Channel { channel, .. } => {
2030                        self.join_channel(channel.id, cx);
2031                    }
2032                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
2033                    _ => {}
2034                }
2035            }
2036        }
2037    }
2038
2039    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
2040        if let Some(editing_state) = &mut self.channel_editing_state {
2041            match editing_state {
2042                ChannelEditingState::Create {
2043                    parent_id,
2044                    pending_name,
2045                    ..
2046                } => {
2047                    if pending_name.is_some() {
2048                        return false;
2049                    }
2050                    let channel_name = self.channel_name_editor.read(cx).text(cx);
2051
2052                    *pending_name = Some(channel_name.clone());
2053
2054                    self.channel_store
2055                        .update(cx, |channel_store, cx| {
2056                            channel_store.create_channel(&channel_name, *parent_id, cx)
2057                        })
2058                        .detach();
2059                    cx.notify();
2060                }
2061                ChannelEditingState::Rename {
2062                    channel_id,
2063                    pending_name,
2064                } => {
2065                    if pending_name.is_some() {
2066                        return false;
2067                    }
2068                    let channel_name = self.channel_name_editor.read(cx).text(cx);
2069                    *pending_name = Some(channel_name.clone());
2070
2071                    self.channel_store
2072                        .update(cx, |channel_store, cx| {
2073                            channel_store.rename(*channel_id, &channel_name, cx)
2074                        })
2075                        .detach();
2076                    cx.notify();
2077                }
2078            }
2079            cx.focus_self();
2080            true
2081        } else {
2082            false
2083        }
2084    }
2085
2086    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
2087        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2088            self.collapsed_sections.remove(ix);
2089        } else {
2090            self.collapsed_sections.push(section);
2091        }
2092        self.update_entries(false, cx);
2093    }
2094
2095    fn collapse_selected_channel(
2096        &mut self,
2097        _: &CollapseSelectedChannel,
2098        cx: &mut ViewContext<Self>,
2099    ) {
2100        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
2101            return;
2102        };
2103
2104        if self.is_channel_collapsed(channel_id) {
2105            return;
2106        }
2107
2108        self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
2109    }
2110
2111    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
2112        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
2113            return;
2114        };
2115
2116        if !self.is_channel_collapsed(channel_id) {
2117            return;
2118        }
2119
2120        self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
2121    }
2122
2123    fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
2124        let channel_id = action.channel_id;
2125
2126        match self.collapsed_channels.binary_search(&channel_id) {
2127            Ok(ix) => {
2128                self.collapsed_channels.remove(ix);
2129            }
2130            Err(ix) => {
2131                self.collapsed_channels.insert(ix, channel_id);
2132            }
2133        };
2134        self.serialize(cx);
2135        self.update_entries(true, cx);
2136        cx.notify();
2137        cx.focus_self();
2138    }
2139
2140    fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
2141        self.collapsed_channels.binary_search(&channel).is_ok()
2142    }
2143
2144    fn leave_call(cx: &mut ViewContext<Self>) {
2145        ActiveCall::global(cx)
2146            .update(cx, |call, cx| call.hang_up(cx))
2147            .detach_and_log_err(cx);
2148    }
2149
2150    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
2151        if let Some(workspace) = self.workspace.upgrade(cx) {
2152            workspace.update(cx, |workspace, cx| {
2153                workspace.toggle_modal(cx, |_, cx| {
2154                    cx.add_view(|cx| {
2155                        let mut finder = ContactFinder::new(self.user_store.clone(), cx);
2156                        finder.set_query(self.filter_editor.read(cx).text(cx), cx);
2157                        finder
2158                    })
2159                });
2160            });
2161        }
2162    }
2163
2164    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2165        self.channel_editing_state = Some(ChannelEditingState::Create {
2166            parent_id: None,
2167            pending_name: None,
2168        });
2169        self.update_entries(false, cx);
2170        self.select_channel_editor();
2171        cx.focus(self.channel_name_editor.as_any());
2172        cx.notify();
2173    }
2174
2175    fn select_channel_editor(&mut self) {
2176        self.selection = self.entries.iter().position(|entry| match entry {
2177            ListEntry::ChannelEditor { .. } => true,
2178            _ => false,
2179        });
2180    }
2181
2182    fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
2183        self.collapsed_channels
2184            .retain(|&channel| channel != action.channel_id);
2185        self.channel_editing_state = Some(ChannelEditingState::Create {
2186            parent_id: Some(action.channel_id),
2187            pending_name: None,
2188        });
2189        self.update_entries(false, cx);
2190        self.select_channel_editor();
2191        cx.focus(self.channel_name_editor.as_any());
2192        cx.notify();
2193    }
2194
2195    fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
2196        self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
2197    }
2198
2199    fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
2200        self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
2201    }
2202
2203    fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
2204        if let Some(channel) = self.selected_channel() {
2205            self.remove_channel(channel.id, cx)
2206        }
2207    }
2208
2209    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
2210        if let Some(channel) = self.selected_channel() {
2211            self.rename_channel(
2212                &RenameChannel {
2213                    channel_id: channel.id,
2214                },
2215                cx,
2216            );
2217        }
2218    }
2219
2220    fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
2221        let channel_store = self.channel_store.read(cx);
2222        if !channel_store.is_user_admin(action.channel_id) {
2223            return;
2224        }
2225        if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
2226            self.channel_editing_state = Some(ChannelEditingState::Rename {
2227                channel_id: action.channel_id,
2228                pending_name: None,
2229            });
2230            self.channel_name_editor.update(cx, |editor, cx| {
2231                editor.set_text(channel.name.clone(), cx);
2232                editor.select_all(&Default::default(), cx);
2233            });
2234            cx.focus(self.channel_name_editor.as_any());
2235            self.update_entries(false, cx);
2236            self.select_channel_editor();
2237        }
2238    }
2239
2240    fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext<Self>) {
2241        if let Some(workspace) = self.workspace.upgrade(cx) {
2242            let pane = workspace.read(cx).active_pane().clone();
2243            let channel_id = action.channel_id;
2244            let channel_view = ChannelView::open(channel_id, pane.clone(), workspace, cx);
2245            cx.spawn(|_, mut cx| async move {
2246                let channel_view = channel_view.await?;
2247                pane.update(&mut cx, |pane, cx| {
2248                    pane.add_item(Box::new(channel_view), true, true, None, cx)
2249                });
2250                anyhow::Ok(())
2251            })
2252            .detach();
2253            let room_id = ActiveCall::global(cx)
2254                .read(cx)
2255                .room()
2256                .map(|room| room.read(cx).id());
2257
2258            ActiveCall::report_call_event_for_room(
2259                "open channel notes",
2260                room_id,
2261                Some(channel_id),
2262                &self.client,
2263                cx,
2264            );
2265        }
2266    }
2267
2268    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
2269        let Some(channel) = self.selected_channel() else {
2270            return;
2271        };
2272
2273        self.deploy_channel_context_menu(None, channel.id, cx);
2274    }
2275
2276    fn selected_channel(&self) -> Option<&Arc<Channel>> {
2277        self.selection
2278            .and_then(|ix| self.entries.get(ix))
2279            .and_then(|entry| match entry {
2280                ListEntry::Channel { channel, .. } => Some(channel),
2281                _ => None,
2282            })
2283    }
2284
2285    fn show_channel_modal(
2286        &mut self,
2287        channel_id: ChannelId,
2288        mode: channel_modal::Mode,
2289        cx: &mut ViewContext<Self>,
2290    ) {
2291        let workspace = self.workspace.clone();
2292        let user_store = self.user_store.clone();
2293        let channel_store = self.channel_store.clone();
2294        let members = self.channel_store.update(cx, |channel_store, cx| {
2295            channel_store.get_channel_member_details(channel_id, cx)
2296        });
2297
2298        cx.spawn(|_, mut cx| async move {
2299            let members = members.await?;
2300            workspace.update(&mut cx, |workspace, cx| {
2301                workspace.toggle_modal(cx, |_, cx| {
2302                    cx.add_view(|cx| {
2303                        ChannelModal::new(
2304                            user_store.clone(),
2305                            channel_store.clone(),
2306                            channel_id,
2307                            mode,
2308                            members,
2309                            cx,
2310                        )
2311                    })
2312                });
2313            })
2314        })
2315        .detach();
2316    }
2317
2318    fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
2319        self.remove_channel(action.channel_id, cx)
2320    }
2321
2322    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2323        let channel_store = self.channel_store.clone();
2324        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2325            let prompt_message = format!(
2326                "Are you sure you want to remove the channel \"{}\"?",
2327                channel.name
2328            );
2329            let mut answer =
2330                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2331            let window = cx.window();
2332            cx.spawn(|this, mut cx| async move {
2333                if answer.next().await == Some(0) {
2334                    if let Err(e) = channel_store
2335                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
2336                        .await
2337                    {
2338                        window.prompt(
2339                            PromptLevel::Info,
2340                            &format!("Failed to remove channel: {}", e),
2341                            &["Ok"],
2342                            &mut cx,
2343                        );
2344                    }
2345                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
2346                }
2347            })
2348            .detach();
2349        }
2350    }
2351
2352    // Should move to the filter editor if clicking on it
2353    // Should move selection to the channel editor if activating it
2354
2355    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2356        let user_store = self.user_store.clone();
2357        let prompt_message = format!(
2358            "Are you sure you want to remove \"{}\" from your contacts?",
2359            github_login
2360        );
2361        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2362        let window = cx.window();
2363        cx.spawn(|_, mut cx| async move {
2364            if answer.next().await == Some(0) {
2365                if let Err(e) = user_store
2366                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
2367                    .await
2368                {
2369                    window.prompt(
2370                        PromptLevel::Info,
2371                        &format!("Failed to remove contact: {}", e),
2372                        &["Ok"],
2373                        &mut cx,
2374                    );
2375                }
2376            }
2377        })
2378        .detach();
2379    }
2380
2381    fn respond_to_contact_request(
2382        &mut self,
2383        user_id: u64,
2384        accept: bool,
2385        cx: &mut ViewContext<Self>,
2386    ) {
2387        self.user_store
2388            .update(cx, |store, cx| {
2389                store.respond_to_contact_request(user_id, accept, cx)
2390            })
2391            .detach();
2392    }
2393
2394    fn respond_to_channel_invite(
2395        &mut self,
2396        channel_id: u64,
2397        accept: bool,
2398        cx: &mut ViewContext<Self>,
2399    ) {
2400        let respond = self.channel_store.update(cx, |store, _| {
2401            store.respond_to_channel_invite(channel_id, accept)
2402        });
2403        cx.foreground().spawn(respond).detach();
2404    }
2405
2406    fn call(
2407        &mut self,
2408        recipient_user_id: u64,
2409        initial_project: Option<ModelHandle<Project>>,
2410        cx: &mut ViewContext<Self>,
2411    ) {
2412        ActiveCall::global(cx)
2413            .update(cx, |call, cx| {
2414                call.invite(recipient_user_id, initial_project, cx)
2415            })
2416            .detach_and_log_err(cx);
2417    }
2418
2419    fn join_channel(&self, channel: u64, cx: &mut ViewContext<Self>) {
2420        ActiveCall::global(cx)
2421            .update(cx, |call, cx| call.join_channel(channel, cx))
2422            .detach_and_log_err(cx);
2423    }
2424}
2425
2426fn render_tree_branch(
2427    branch_style: theme::TreeBranch,
2428    row_style: &TextStyle,
2429    is_last: bool,
2430    size: Vector2F,
2431    font_cache: &FontCache,
2432) -> gpui::elements::ConstrainedBox<CollabPanel> {
2433    let line_height = row_style.line_height(font_cache);
2434    let cap_height = row_style.cap_height(font_cache);
2435    let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
2436
2437    Canvas::new(move |bounds, _, _, cx| {
2438        cx.paint_layer(None, |cx| {
2439            let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
2440            let end_x = bounds.max_x();
2441            let start_y = bounds.min_y();
2442            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
2443
2444            cx.scene().push_quad(gpui::Quad {
2445                bounds: RectF::from_points(
2446                    vec2f(start_x, start_y),
2447                    vec2f(
2448                        start_x + branch_style.width,
2449                        if is_last { end_y } else { bounds.max_y() },
2450                    ),
2451                ),
2452                background: Some(branch_style.color),
2453                border: gpui::Border::default(),
2454                corner_radii: (0.).into(),
2455            });
2456            cx.scene().push_quad(gpui::Quad {
2457                bounds: RectF::from_points(
2458                    vec2f(start_x, end_y),
2459                    vec2f(end_x, end_y + branch_style.width),
2460                ),
2461                background: Some(branch_style.color),
2462                border: gpui::Border::default(),
2463                corner_radii: (0.).into(),
2464            });
2465        })
2466    })
2467    .constrained()
2468    .with_width(size.x())
2469}
2470
2471impl View for CollabPanel {
2472    fn ui_name() -> &'static str {
2473        "CollabPanel"
2474    }
2475
2476    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2477        if !self.has_focus {
2478            self.has_focus = true;
2479            if !self.context_menu.is_focused(cx) {
2480                if let Some(editing_state) = &self.channel_editing_state {
2481                    if editing_state.pending_name().is_none() {
2482                        cx.focus(&self.channel_name_editor);
2483                    } else {
2484                        cx.focus(&self.filter_editor);
2485                    }
2486                } else {
2487                    cx.focus(&self.filter_editor);
2488                }
2489            }
2490            cx.emit(Event::Focus);
2491        }
2492    }
2493
2494    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
2495        self.has_focus = false;
2496    }
2497
2498    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
2499        let theme = &theme::current(cx).collab_panel;
2500
2501        if self.user_store.read(cx).current_user().is_none() {
2502            enum LogInButton {}
2503
2504            return Flex::column()
2505                .with_child(
2506                    MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
2507                        let button = theme.log_in_button.style_for(state);
2508                        Label::new("Sign in to collaborate", button.text.clone())
2509                            .aligned()
2510                            .left()
2511                            .contained()
2512                            .with_style(button.container)
2513                    })
2514                    .on_click(MouseButton::Left, |_, this, cx| {
2515                        let client = this.client.clone();
2516                        cx.spawn(|_, cx| async move {
2517                            client.authenticate_and_connect(true, &cx).await.log_err();
2518                        })
2519                        .detach();
2520                    })
2521                    .with_cursor_style(CursorStyle::PointingHand),
2522                )
2523                .contained()
2524                .with_style(theme.container)
2525                .into_any();
2526        }
2527
2528        enum PanelFocus {}
2529        MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
2530            Stack::new()
2531                .with_child(
2532                    Flex::column()
2533                        .with_child(
2534                            Flex::row().with_child(
2535                                ChildView::new(&self.filter_editor, cx)
2536                                    .contained()
2537                                    .with_style(theme.user_query_editor.container)
2538                                    .flex(1.0, true),
2539                            ),
2540                        )
2541                        .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
2542                        .contained()
2543                        .with_style(theme.container)
2544                        .into_any(),
2545                )
2546                .with_children(
2547                    (!self.context_menu_on_selected)
2548                        .then(|| ChildView::new(&self.context_menu, cx)),
2549                )
2550                .into_any()
2551        })
2552        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
2553        .into_any_named("collab panel")
2554    }
2555}
2556
2557impl Panel for CollabPanel {
2558    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
2559        match settings::get::<CollaborationPanelSettings>(cx).dock {
2560            CollaborationPanelDockPosition::Left => DockPosition::Left,
2561            CollaborationPanelDockPosition::Right => DockPosition::Right,
2562        }
2563    }
2564
2565    fn position_is_valid(&self, position: DockPosition) -> bool {
2566        matches!(position, DockPosition::Left | DockPosition::Right)
2567    }
2568
2569    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2570        settings::update_settings_file::<CollaborationPanelSettings>(
2571            self.fs.clone(),
2572            cx,
2573            move |settings| {
2574                let dock = match position {
2575                    DockPosition::Left | DockPosition::Bottom => {
2576                        CollaborationPanelDockPosition::Left
2577                    }
2578                    DockPosition::Right => CollaborationPanelDockPosition::Right,
2579                };
2580                settings.dock = Some(dock);
2581            },
2582        );
2583    }
2584
2585    fn size(&self, cx: &gpui::WindowContext) -> f32 {
2586        self.width
2587            .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
2588    }
2589
2590    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
2591        self.width = size;
2592        self.serialize(cx);
2593        cx.notify();
2594    }
2595
2596    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
2597        settings::get::<CollaborationPanelSettings>(cx)
2598            .button
2599            .then(|| "icons/conversations.svg")
2600    }
2601
2602    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
2603        (
2604            "Collaboration Panel".to_string(),
2605            Some(Box::new(ToggleFocus)),
2606        )
2607    }
2608
2609    fn should_change_position_on_event(event: &Self::Event) -> bool {
2610        matches!(event, Event::DockPositionChanged)
2611    }
2612
2613    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
2614        self.has_focus
2615    }
2616
2617    fn is_focus_event(event: &Self::Event) -> bool {
2618        matches!(event, Event::Focus)
2619    }
2620}
2621
2622impl PartialEq for ListEntry {
2623    fn eq(&self, other: &Self) -> bool {
2624        match self {
2625            ListEntry::Header(section_1, depth_1) => {
2626                if let ListEntry::Header(section_2, depth_2) = other {
2627                    return section_1 == section_2 && depth_1 == depth_2;
2628                }
2629            }
2630            ListEntry::CallParticipant { user: user_1, .. } => {
2631                if let ListEntry::CallParticipant { user: user_2, .. } = other {
2632                    return user_1.id == user_2.id;
2633                }
2634            }
2635            ListEntry::ParticipantProject {
2636                project_id: project_id_1,
2637                ..
2638            } => {
2639                if let ListEntry::ParticipantProject {
2640                    project_id: project_id_2,
2641                    ..
2642                } = other
2643                {
2644                    return project_id_1 == project_id_2;
2645                }
2646            }
2647            ListEntry::ParticipantScreen {
2648                peer_id: peer_id_1, ..
2649            } => {
2650                if let ListEntry::ParticipantScreen {
2651                    peer_id: peer_id_2, ..
2652                } = other
2653                {
2654                    return peer_id_1 == peer_id_2;
2655                }
2656            }
2657            ListEntry::Channel {
2658                channel: channel_1,
2659                depth: depth_1,
2660            } => {
2661                if let ListEntry::Channel {
2662                    channel: channel_2,
2663                    depth: depth_2,
2664                } = other
2665                {
2666                    return channel_1.id == channel_2.id && depth_1 == depth_2;
2667                }
2668            }
2669            ListEntry::ChannelNotes { channel_id } => {
2670                if let ListEntry::ChannelNotes {
2671                    channel_id: other_id,
2672                } = other
2673                {
2674                    return channel_id == other_id;
2675                }
2676            }
2677            ListEntry::ChannelInvite(channel_1) => {
2678                if let ListEntry::ChannelInvite(channel_2) = other {
2679                    return channel_1.id == channel_2.id;
2680                }
2681            }
2682            ListEntry::IncomingRequest(user_1) => {
2683                if let ListEntry::IncomingRequest(user_2) = other {
2684                    return user_1.id == user_2.id;
2685                }
2686            }
2687            ListEntry::OutgoingRequest(user_1) => {
2688                if let ListEntry::OutgoingRequest(user_2) = other {
2689                    return user_1.id == user_2.id;
2690                }
2691            }
2692            ListEntry::Contact {
2693                contact: contact_1, ..
2694            } => {
2695                if let ListEntry::Contact {
2696                    contact: contact_2, ..
2697                } = other
2698                {
2699                    return contact_1.user.id == contact_2.user.id;
2700                }
2701            }
2702            ListEntry::ChannelEditor { depth } => {
2703                if let ListEntry::ChannelEditor { depth: other_depth } = other {
2704                    return depth == other_depth;
2705                }
2706            }
2707            ListEntry::ContactPlaceholder => {
2708                if let ListEntry::ContactPlaceholder = other {
2709                    return true;
2710                }
2711            }
2712        }
2713        false
2714    }
2715}
2716
2717fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
2718    Svg::new(svg_path)
2719        .with_color(style.color)
2720        .constrained()
2721        .with_width(style.icon_width)
2722        .aligned()
2723        .constrained()
2724        .with_width(style.button_width)
2725        .with_height(style.button_width)
2726        .contained()
2727        .with_style(style.container)
2728}