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