collab_panel.rs

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