collab_panel.rs

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