collab_panel.rs

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