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 CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
  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::default(),
 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(CHANNELS_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                        CHANNELS_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)
1187                        .read(cx)
1188                        .room()?
1189                        .read(cx)
1190                        .channel_id()?;
1191                    let name = self
1192                        .channel_store
1193                        .read(cx)
1194                        .channel_for_id(channel_id)?
1195                        .name
1196                        .as_str();
1197
1198                    Some(name)
1199                });
1200
1201                if let Some(name) = channel_name {
1202                    Cow::Owned(format!("Current Call - #{}", name))
1203                } else {
1204                    Cow::Borrowed("Current Call")
1205                }
1206            }
1207            Section::ContactRequests => Cow::Borrowed("Requests"),
1208            Section::Contacts => Cow::Borrowed("Contacts"),
1209            Section::Channels => Cow::Borrowed("Channels"),
1210            Section::ChannelInvites => Cow::Borrowed("Invites"),
1211            Section::Online => Cow::Borrowed("Online"),
1212            Section::Offline => Cow::Borrowed("Offline"),
1213        };
1214
1215        enum AddContact {}
1216        let button = match section {
1217            Section::ActiveCall => Some(
1218                MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
1219                    render_icon_button(
1220                        theme
1221                            .collab_panel
1222                            .leave_call_button
1223                            .style_for(is_selected, state),
1224                        "icons/exit.svg",
1225                    )
1226                })
1227                .with_cursor_style(CursorStyle::PointingHand)
1228                .on_click(MouseButton::Left, |_, _, cx| {
1229                    Self::leave_call(cx);
1230                })
1231                .with_tooltip::<AddContact>(
1232                    0,
1233                    "Leave call",
1234                    None,
1235                    tooltip_style.clone(),
1236                    cx,
1237                ),
1238            ),
1239            Section::Contacts => Some(
1240                MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
1241                    render_icon_button(
1242                        theme
1243                            .collab_panel
1244                            .add_contact_button
1245                            .style_for(is_selected, state),
1246                        "icons/plus_16.svg",
1247                    )
1248                })
1249                .with_cursor_style(CursorStyle::PointingHand)
1250                .on_click(MouseButton::Left, |_, this, cx| {
1251                    this.toggle_contact_finder(cx);
1252                })
1253                .with_tooltip::<LeaveCallContactList>(
1254                    0,
1255                    "Search for new contact",
1256                    None,
1257                    tooltip_style.clone(),
1258                    cx,
1259                ),
1260            ),
1261            Section::Channels => Some(
1262                MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
1263                    render_icon_button(
1264                        theme
1265                            .collab_panel
1266                            .add_contact_button
1267                            .style_for(is_selected, state),
1268                        "icons/plus.svg",
1269                    )
1270                })
1271                .with_cursor_style(CursorStyle::PointingHand)
1272                .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
1273                .with_tooltip::<AddChannel>(
1274                    0,
1275                    "Create a channel",
1276                    None,
1277                    tooltip_style.clone(),
1278                    cx,
1279                ),
1280            ),
1281            _ => None,
1282        };
1283
1284        let can_collapse = depth > 0;
1285        let icon_size = (&theme.collab_panel).section_icon_size;
1286        let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
1287            let header_style = if can_collapse {
1288                theme
1289                    .collab_panel
1290                    .subheader_row
1291                    .in_state(is_selected)
1292                    .style_for(state)
1293            } else {
1294                &theme.collab_panel.header_row
1295            };
1296
1297            Flex::row()
1298                .with_children(if can_collapse {
1299                    Some(
1300                        Svg::new(if is_collapsed {
1301                            "icons/chevron_right.svg"
1302                        } else {
1303                            "icons/chevron_down.svg"
1304                        })
1305                        .with_color(header_style.text.color)
1306                        .constrained()
1307                        .with_max_width(icon_size)
1308                        .with_max_height(icon_size)
1309                        .aligned()
1310                        .constrained()
1311                        .with_width(icon_size)
1312                        .contained()
1313                        .with_margin_right(
1314                            theme.collab_panel.contact_username.container.margin.left,
1315                        ),
1316                    )
1317                } else {
1318                    None
1319                })
1320                .with_child(
1321                    Label::new(text, header_style.text.clone())
1322                        .aligned()
1323                        .left()
1324                        .flex(1., true),
1325                )
1326                .with_children(button.map(|button| button.aligned().right()))
1327                .constrained()
1328                .with_height(theme.collab_panel.row_height)
1329                .contained()
1330                .with_style(header_style.container)
1331        });
1332
1333        if can_collapse {
1334            result = result
1335                .with_cursor_style(CursorStyle::PointingHand)
1336                .on_click(MouseButton::Left, move |_, this, cx| {
1337                    if can_collapse {
1338                        this.toggle_expanded(section, cx);
1339                    }
1340                })
1341        }
1342
1343        result.into_any()
1344    }
1345
1346    fn render_contact(
1347        contact: &Contact,
1348        calling: bool,
1349        project: &ModelHandle<Project>,
1350        theme: &theme::CollabPanel,
1351        is_selected: bool,
1352        cx: &mut ViewContext<Self>,
1353    ) -> AnyElement<Self> {
1354        let online = contact.online;
1355        let busy = contact.busy || calling;
1356        let user_id = contact.user.id;
1357        let github_login = contact.user.github_login.clone();
1358        let initial_project = project.clone();
1359        let mut event_handler =
1360            MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
1361                Flex::row()
1362                    .with_children(contact.user.avatar.clone().map(|avatar| {
1363                        let status_badge = if contact.online {
1364                            Some(
1365                                Empty::new()
1366                                    .collapsed()
1367                                    .contained()
1368                                    .with_style(if busy {
1369                                        theme.contact_status_busy
1370                                    } else {
1371                                        theme.contact_status_free
1372                                    })
1373                                    .aligned(),
1374                            )
1375                        } else {
1376                            None
1377                        };
1378                        Stack::new()
1379                            .with_child(
1380                                Image::from_data(avatar)
1381                                    .with_style(theme.contact_avatar)
1382                                    .aligned()
1383                                    .left(),
1384                            )
1385                            .with_children(status_badge)
1386                    }))
1387                    .with_child(
1388                        Label::new(
1389                            contact.user.github_login.clone(),
1390                            theme.contact_username.text.clone(),
1391                        )
1392                        .contained()
1393                        .with_style(theme.contact_username.container)
1394                        .aligned()
1395                        .left()
1396                        .flex(1., true),
1397                    )
1398                    .with_child(
1399                        MouseEventHandler::new::<Cancel, _>(
1400                            contact.user.id as usize,
1401                            cx,
1402                            |mouse_state, _| {
1403                                let button_style = theme.contact_button.style_for(mouse_state);
1404                                render_icon_button(button_style, "icons/x.svg")
1405                                    .aligned()
1406                                    .flex_float()
1407                            },
1408                        )
1409                        .with_padding(Padding::uniform(2.))
1410                        .with_cursor_style(CursorStyle::PointingHand)
1411                        .on_click(MouseButton::Left, move |_, this, cx| {
1412                            this.remove_contact(user_id, &github_login, cx);
1413                        })
1414                        .flex_float(),
1415                    )
1416                    .with_children(if calling {
1417                        Some(
1418                            Label::new("Calling", theme.calling_indicator.text.clone())
1419                                .contained()
1420                                .with_style(theme.calling_indicator.container)
1421                                .aligned(),
1422                        )
1423                    } else {
1424                        None
1425                    })
1426                    .constrained()
1427                    .with_height(theme.row_height)
1428                    .contained()
1429                    .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
1430            })
1431            .on_click(MouseButton::Left, move |_, this, cx| {
1432                if online && !busy {
1433                    this.call(user_id, Some(initial_project.clone()), cx);
1434                }
1435            });
1436
1437        if online {
1438            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
1439        }
1440
1441        event_handler.into_any()
1442    }
1443
1444    fn render_contact_placeholder(
1445        &self,
1446        theme: &theme::CollabPanel,
1447        is_selected: bool,
1448        cx: &mut ViewContext<Self>,
1449    ) -> AnyElement<Self> {
1450        enum AddContacts {}
1451        MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1452            let style = theme.list_empty_state.style_for(is_selected, state);
1453            Flex::row()
1454                .with_child(
1455                    Svg::new("icons/plus.svg")
1456                        .with_color(theme.list_empty_icon.color)
1457                        .constrained()
1458                        .with_width(theme.list_empty_icon.width)
1459                        .aligned()
1460                        .left(),
1461                )
1462                .with_child(
1463                    Label::new("Add a contact", style.text.clone())
1464                        .contained()
1465                        .with_style(theme.list_empty_label_container),
1466                )
1467                .align_children_center()
1468                .contained()
1469                .with_style(style.container)
1470                .into_any()
1471        })
1472        .on_click(MouseButton::Left, |_, this, cx| {
1473            this.toggle_contact_finder(cx);
1474        })
1475        .into_any()
1476    }
1477
1478    fn render_channel_editor(
1479        &self,
1480        theme: &theme::Theme,
1481        depth: usize,
1482        cx: &AppContext,
1483    ) -> AnyElement<Self> {
1484        Flex::row()
1485            .with_child(
1486                Svg::new("icons/hash.svg")
1487                    .with_color(theme.collab_panel.channel_hash.color)
1488                    .constrained()
1489                    .with_width(theme.collab_panel.channel_hash.width)
1490                    .aligned()
1491                    .left(),
1492            )
1493            .with_child(
1494                if let Some(pending_name) = self
1495                    .channel_editing_state
1496                    .as_ref()
1497                    .and_then(|state| state.pending_name())
1498                {
1499                    Label::new(
1500                        pending_name.to_string(),
1501                        theme.collab_panel.contact_username.text.clone(),
1502                    )
1503                    .contained()
1504                    .with_style(theme.collab_panel.contact_username.container)
1505                    .aligned()
1506                    .left()
1507                    .flex(1., true)
1508                    .into_any()
1509                } else {
1510                    ChildView::new(&self.channel_name_editor, cx)
1511                        .aligned()
1512                        .left()
1513                        .contained()
1514                        .with_style(theme.collab_panel.channel_editor)
1515                        .flex(1.0, true)
1516                        .into_any()
1517                },
1518            )
1519            .align_children_center()
1520            .constrained()
1521            .with_height(theme.collab_panel.row_height)
1522            .contained()
1523            .with_style(gpui::elements::ContainerStyle {
1524                background_color: Some(theme.editor.background),
1525                ..*theme.collab_panel.contact_row.default_style()
1526            })
1527            .with_padding_left(
1528                theme.collab_panel.contact_row.default_style().padding.left
1529                    + theme.collab_panel.channel_indent * depth as f32,
1530            )
1531            .into_any()
1532    }
1533
1534    fn render_channel(
1535        &self,
1536        channel: &Channel,
1537        depth: usize,
1538        theme: &theme::CollabPanel,
1539        is_selected: bool,
1540        cx: &mut ViewContext<Self>,
1541    ) -> AnyElement<Self> {
1542        let channel_id = channel.id;
1543        let is_active = iife!({
1544            let call_channel = ActiveCall::global(cx)
1545                .read(cx)
1546                .room()?
1547                .read(cx)
1548                .channel_id()?;
1549            Some(call_channel == channel_id)
1550        })
1551        .unwrap_or(false);
1552
1553        const FACEPILE_LIMIT: usize = 3;
1554
1555        MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
1556            Flex::row()
1557                .with_child(
1558                    Svg::new("icons/hash.svg")
1559                        .with_color(theme.channel_hash.color)
1560                        .constrained()
1561                        .with_width(theme.channel_hash.width)
1562                        .aligned()
1563                        .left(),
1564                )
1565                .with_child(
1566                    Label::new(channel.name.clone(), theme.channel_name.text.clone())
1567                        .contained()
1568                        .with_style(theme.channel_name.container)
1569                        .aligned()
1570                        .left()
1571                        .flex(1., true),
1572                )
1573                .with_children({
1574                    let participants = self.channel_store.read(cx).channel_participants(channel_id);
1575                    if !participants.is_empty() {
1576                        let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
1577
1578                        Some(
1579                            FacePile::new(theme.face_overlap)
1580                                .with_children(
1581                                    participants
1582                                        .iter()
1583                                        .filter_map(|user| {
1584                                            Some(
1585                                                Image::from_data(user.avatar.clone()?)
1586                                                    .with_style(theme.channel_avatar),
1587                                            )
1588                                        })
1589                                        .take(FACEPILE_LIMIT),
1590                                )
1591                                .with_children((extra_count > 0).then(|| {
1592                                    Label::new(
1593                                        format!("+{}", extra_count),
1594                                        theme.extra_participant_label.text.clone(),
1595                                    )
1596                                    .contained()
1597                                    .with_style(theme.extra_participant_label.container)
1598                                })),
1599                        )
1600                    } else {
1601                        None
1602                    }
1603                })
1604                .align_children_center()
1605                .constrained()
1606                .with_height(theme.row_height)
1607                .contained()
1608                .with_style(*theme.channel_row.style_for(is_selected || is_active, state))
1609                .with_padding_left(
1610                    theme.channel_row.default_style().padding.left
1611                        + theme.channel_indent * depth as f32,
1612                )
1613        })
1614        .on_click(MouseButton::Left, move |_, this, cx| {
1615            this.join_channel(channel_id, cx);
1616        })
1617        .on_click(MouseButton::Right, move |e, this, cx| {
1618            this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
1619        })
1620        .with_cursor_style(CursorStyle::PointingHand)
1621        .into_any()
1622    }
1623
1624    fn render_channel_invite(
1625        channel: Arc<Channel>,
1626        channel_store: ModelHandle<ChannelStore>,
1627        theme: &theme::CollabPanel,
1628        is_selected: bool,
1629        cx: &mut ViewContext<Self>,
1630    ) -> AnyElement<Self> {
1631        enum Decline {}
1632        enum Accept {}
1633
1634        let channel_id = channel.id;
1635        let is_invite_pending = channel_store
1636            .read(cx)
1637            .has_pending_channel_invite_response(&channel);
1638        let button_spacing = theme.contact_button_spacing;
1639
1640        Flex::row()
1641            .with_child(
1642                Svg::new("icons/hash.svg")
1643                    .with_color(theme.channel_hash.color)
1644                    .constrained()
1645                    .with_width(theme.channel_hash.width)
1646                    .aligned()
1647                    .left(),
1648            )
1649            .with_child(
1650                Label::new(channel.name.clone(), theme.contact_username.text.clone())
1651                    .contained()
1652                    .with_style(theme.contact_username.container)
1653                    .aligned()
1654                    .left()
1655                    .flex(1., true),
1656            )
1657            .with_child(
1658                MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
1659                    let button_style = if is_invite_pending {
1660                        &theme.disabled_button
1661                    } else {
1662                        theme.contact_button.style_for(mouse_state)
1663                    };
1664                    render_icon_button(button_style, "icons/x.svg").aligned()
1665                })
1666                .with_cursor_style(CursorStyle::PointingHand)
1667                .on_click(MouseButton::Left, move |_, this, cx| {
1668                    this.respond_to_channel_invite(channel_id, false, cx);
1669                })
1670                .contained()
1671                .with_margin_right(button_spacing),
1672            )
1673            .with_child(
1674                MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
1675                    let button_style = if is_invite_pending {
1676                        &theme.disabled_button
1677                    } else {
1678                        theme.contact_button.style_for(mouse_state)
1679                    };
1680                    render_icon_button(button_style, "icons/check_8.svg")
1681                        .aligned()
1682                        .flex_float()
1683                })
1684                .with_cursor_style(CursorStyle::PointingHand)
1685                .on_click(MouseButton::Left, move |_, this, cx| {
1686                    this.respond_to_channel_invite(channel_id, true, cx);
1687                }),
1688            )
1689            .constrained()
1690            .with_height(theme.row_height)
1691            .contained()
1692            .with_style(
1693                *theme
1694                    .contact_row
1695                    .in_state(is_selected)
1696                    .style_for(&mut Default::default()),
1697            )
1698            .with_padding_left(
1699                theme.contact_row.default_style().padding.left + theme.channel_indent,
1700            )
1701            .into_any()
1702    }
1703
1704    fn render_contact_request(
1705        user: Arc<User>,
1706        user_store: ModelHandle<UserStore>,
1707        theme: &theme::CollabPanel,
1708        is_incoming: bool,
1709        is_selected: bool,
1710        cx: &mut ViewContext<Self>,
1711    ) -> AnyElement<Self> {
1712        enum Decline {}
1713        enum Accept {}
1714        enum Cancel {}
1715
1716        let mut row = Flex::row()
1717            .with_children(user.avatar.clone().map(|avatar| {
1718                Image::from_data(avatar)
1719                    .with_style(theme.contact_avatar)
1720                    .aligned()
1721                    .left()
1722            }))
1723            .with_child(
1724                Label::new(
1725                    user.github_login.clone(),
1726                    theme.contact_username.text.clone(),
1727                )
1728                .contained()
1729                .with_style(theme.contact_username.container)
1730                .aligned()
1731                .left()
1732                .flex(1., true),
1733            );
1734
1735        let user_id = user.id;
1736        let github_login = user.github_login.clone();
1737        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
1738        let button_spacing = theme.contact_button_spacing;
1739
1740        if is_incoming {
1741            row.add_child(
1742                MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
1743                    let button_style = if is_contact_request_pending {
1744                        &theme.disabled_button
1745                    } else {
1746                        theme.contact_button.style_for(mouse_state)
1747                    };
1748                    render_icon_button(button_style, "icons/x.svg").aligned()
1749                })
1750                .with_cursor_style(CursorStyle::PointingHand)
1751                .on_click(MouseButton::Left, move |_, this, cx| {
1752                    this.respond_to_contact_request(user_id, false, cx);
1753                })
1754                .contained()
1755                .with_margin_right(button_spacing),
1756            );
1757
1758            row.add_child(
1759                MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
1760                    let button_style = if is_contact_request_pending {
1761                        &theme.disabled_button
1762                    } else {
1763                        theme.contact_button.style_for(mouse_state)
1764                    };
1765                    render_icon_button(button_style, "icons/check_8.svg")
1766                        .aligned()
1767                        .flex_float()
1768                })
1769                .with_cursor_style(CursorStyle::PointingHand)
1770                .on_click(MouseButton::Left, move |_, this, cx| {
1771                    this.respond_to_contact_request(user_id, true, cx);
1772                }),
1773            );
1774        } else {
1775            row.add_child(
1776                MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
1777                    let button_style = if is_contact_request_pending {
1778                        &theme.disabled_button
1779                    } else {
1780                        theme.contact_button.style_for(mouse_state)
1781                    };
1782                    render_icon_button(button_style, "icons/x.svg")
1783                        .aligned()
1784                        .flex_float()
1785                })
1786                .with_padding(Padding::uniform(2.))
1787                .with_cursor_style(CursorStyle::PointingHand)
1788                .on_click(MouseButton::Left, move |_, this, cx| {
1789                    this.remove_contact(user_id, &github_login, cx);
1790                })
1791                .flex_float(),
1792            );
1793        }
1794
1795        row.constrained()
1796            .with_height(theme.row_height)
1797            .contained()
1798            .with_style(
1799                *theme
1800                    .contact_row
1801                    .in_state(is_selected)
1802                    .style_for(&mut Default::default()),
1803            )
1804            .into_any()
1805    }
1806
1807    fn include_channels_section(&self, cx: &AppContext) -> bool {
1808        if cx.has_global::<StaffMode>() {
1809            cx.global::<StaffMode>().0
1810        } else {
1811            false
1812        }
1813    }
1814
1815    fn deploy_channel_context_menu(
1816        &mut self,
1817        position: Option<Vector2F>,
1818        channel_id: u64,
1819        cx: &mut ViewContext<Self>,
1820    ) {
1821        if self.channel_store.read(cx).is_user_admin(channel_id) {
1822            self.context_menu_on_selected = position.is_none();
1823
1824            self.context_menu.update(cx, |context_menu, cx| {
1825                context_menu.set_position_mode(if self.context_menu_on_selected {
1826                    OverlayPositionMode::Local
1827                } else {
1828                    OverlayPositionMode::Window
1829                });
1830
1831                context_menu.show(
1832                    position.unwrap_or_default(),
1833                    if self.context_menu_on_selected {
1834                        gpui::elements::AnchorCorner::TopRight
1835                    } else {
1836                        gpui::elements::AnchorCorner::BottomLeft
1837                    },
1838                    vec![
1839                        ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
1840                        ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }),
1841                        ContextMenuItem::action("Manage members", ManageMembers { channel_id }),
1842                        ContextMenuItem::action("Invite members", InviteMembers { channel_id }),
1843                        ContextMenuItem::action("Rename Channel", RenameChannel { 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(|_, 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                }
2184            })
2185            .detach();
2186        }
2187    }
2188
2189    // Should move to the filter editor if clicking on it
2190    // Should move selection to the channel editor if activating it
2191
2192    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2193        let user_store = self.user_store.clone();
2194        let prompt_message = format!(
2195            "Are you sure you want to remove \"{}\" from your contacts?",
2196            github_login
2197        );
2198        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2199        let window = cx.window();
2200        cx.spawn(|_, mut cx| async move {
2201            if answer.next().await == Some(0) {
2202                if let Err(e) = user_store
2203                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
2204                    .await
2205                {
2206                    window.prompt(
2207                        PromptLevel::Info,
2208                        &format!("Failed to remove contact: {}", e),
2209                        &["Ok"],
2210                        &mut cx,
2211                    );
2212                }
2213            }
2214        })
2215        .detach();
2216    }
2217
2218    fn respond_to_contact_request(
2219        &mut self,
2220        user_id: u64,
2221        accept: bool,
2222        cx: &mut ViewContext<Self>,
2223    ) {
2224        self.user_store
2225            .update(cx, |store, cx| {
2226                store.respond_to_contact_request(user_id, accept, cx)
2227            })
2228            .detach();
2229    }
2230
2231    fn respond_to_channel_invite(
2232        &mut self,
2233        channel_id: u64,
2234        accept: bool,
2235        cx: &mut ViewContext<Self>,
2236    ) {
2237        let respond = self.channel_store.update(cx, |store, _| {
2238            store.respond_to_channel_invite(channel_id, accept)
2239        });
2240        cx.foreground().spawn(respond).detach();
2241    }
2242
2243    fn call(
2244        &mut self,
2245        recipient_user_id: u64,
2246        initial_project: Option<ModelHandle<Project>>,
2247        cx: &mut ViewContext<Self>,
2248    ) {
2249        ActiveCall::global(cx)
2250            .update(cx, |call, cx| {
2251                call.invite(recipient_user_id, initial_project, cx)
2252            })
2253            .detach_and_log_err(cx);
2254    }
2255
2256    fn join_channel(&self, channel: u64, cx: &mut ViewContext<Self>) {
2257        ActiveCall::global(cx)
2258            .update(cx, |call, cx| call.join_channel(channel, cx))
2259            .detach_and_log_err(cx);
2260    }
2261}
2262
2263impl View for CollabPanel {
2264    fn ui_name() -> &'static str {
2265        "CollabPanel"
2266    }
2267
2268    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2269        if !self.has_focus {
2270            self.has_focus = true;
2271            if !self.context_menu.is_focused(cx) {
2272                if let Some(editing_state) = &self.channel_editing_state {
2273                    if editing_state.pending_name().is_none() {
2274                        cx.focus(&self.channel_name_editor);
2275                    } else {
2276                        cx.focus(&self.filter_editor);
2277                    }
2278                } else {
2279                    cx.focus(&self.filter_editor);
2280                }
2281            }
2282            cx.emit(Event::Focus);
2283        }
2284    }
2285
2286    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
2287        self.has_focus = false;
2288    }
2289
2290    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
2291        let theme = &theme::current(cx).collab_panel;
2292
2293        if self.user_store.read(cx).current_user().is_none() {
2294            enum LogInButton {}
2295
2296            return Flex::column()
2297                .with_child(
2298                    MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
2299                        let button = theme.log_in_button.style_for(state);
2300                        Label::new("Sign in to collaborate", button.text.clone())
2301                            .contained()
2302                            .with_style(button.container)
2303                    })
2304                    .on_click(MouseButton::Left, |_, this, cx| {
2305                        let client = this.client.clone();
2306                        cx.spawn(|_, cx| async move {
2307                            client.authenticate_and_connect(true, &cx).await.log_err();
2308                        })
2309                        .detach();
2310                    })
2311                    .with_cursor_style(CursorStyle::PointingHand),
2312                )
2313                .contained()
2314                .with_style(theme.container)
2315                .into_any();
2316        }
2317
2318        enum PanelFocus {}
2319        MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
2320            Stack::new()
2321                .with_child(
2322                    Flex::column()
2323                        .with_child(
2324                            Flex::row()
2325                                .with_child(
2326                                    ChildView::new(&self.filter_editor, cx)
2327                                        .contained()
2328                                        .with_style(theme.user_query_editor.container)
2329                                        .flex(1.0, true),
2330                                )
2331                                .constrained()
2332                                .with_width(self.size(cx)),
2333                        )
2334                        .with_child(
2335                            List::new(self.list_state.clone())
2336                                .constrained()
2337                                .with_width(self.size(cx))
2338                                .flex(1., true)
2339                                .into_any(),
2340                        )
2341                        .contained()
2342                        .with_style(theme.container)
2343                        .constrained()
2344                        .with_width(self.size(cx))
2345                        .into_any(),
2346                )
2347                .with_children(
2348                    (!self.context_menu_on_selected)
2349                        .then(|| ChildView::new(&self.context_menu, cx)),
2350                )
2351                .into_any()
2352        })
2353        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
2354        .into_any_named("channels panel")
2355    }
2356}
2357
2358impl Panel for CollabPanel {
2359    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
2360        match settings::get::<CollaborationPanelSettings>(cx).dock {
2361            CollaborationPanelDockPosition::Left => DockPosition::Left,
2362            CollaborationPanelDockPosition::Right => DockPosition::Right,
2363        }
2364    }
2365
2366    fn position_is_valid(&self, position: DockPosition) -> bool {
2367        matches!(position, DockPosition::Left | DockPosition::Right)
2368    }
2369
2370    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
2371        settings::update_settings_file::<CollaborationPanelSettings>(
2372            self.fs.clone(),
2373            cx,
2374            move |settings| {
2375                let dock = match position {
2376                    DockPosition::Left | DockPosition::Bottom => {
2377                        CollaborationPanelDockPosition::Left
2378                    }
2379                    DockPosition::Right => CollaborationPanelDockPosition::Right,
2380                };
2381                settings.dock = Some(dock);
2382            },
2383        );
2384    }
2385
2386    fn size(&self, cx: &gpui::WindowContext) -> f32 {
2387        self.width
2388            .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
2389    }
2390
2391    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
2392        self.width = Some(size);
2393        self.serialize(cx);
2394        cx.notify();
2395    }
2396
2397    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
2398        settings::get::<CollaborationPanelSettings>(cx)
2399            .button
2400            .then(|| "icons/conversations.svg")
2401    }
2402
2403    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
2404        ("Channels Panel".to_string(), Some(Box::new(ToggleFocus)))
2405    }
2406
2407    fn should_change_position_on_event(event: &Self::Event) -> bool {
2408        matches!(event, Event::DockPositionChanged)
2409    }
2410
2411    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
2412        self.has_focus
2413    }
2414
2415    fn is_focus_event(event: &Self::Event) -> bool {
2416        matches!(event, Event::Focus)
2417    }
2418}
2419
2420impl PartialEq for ListEntry {
2421    fn eq(&self, other: &Self) -> bool {
2422        match self {
2423            ListEntry::Header(section_1, depth_1) => {
2424                if let ListEntry::Header(section_2, depth_2) = other {
2425                    return section_1 == section_2 && depth_1 == depth_2;
2426                }
2427            }
2428            ListEntry::CallParticipant { user: user_1, .. } => {
2429                if let ListEntry::CallParticipant { user: user_2, .. } = other {
2430                    return user_1.id == user_2.id;
2431                }
2432            }
2433            ListEntry::ParticipantProject {
2434                project_id: project_id_1,
2435                ..
2436            } => {
2437                if let ListEntry::ParticipantProject {
2438                    project_id: project_id_2,
2439                    ..
2440                } = other
2441                {
2442                    return project_id_1 == project_id_2;
2443                }
2444            }
2445            ListEntry::ParticipantScreen {
2446                peer_id: peer_id_1, ..
2447            } => {
2448                if let ListEntry::ParticipantScreen {
2449                    peer_id: peer_id_2, ..
2450                } = other
2451                {
2452                    return peer_id_1 == peer_id_2;
2453                }
2454            }
2455            ListEntry::Channel {
2456                channel: channel_1,
2457                depth: depth_1,
2458            } => {
2459                if let ListEntry::Channel {
2460                    channel: channel_2,
2461                    depth: depth_2,
2462                } = other
2463                {
2464                    return channel_1.id == channel_2.id && depth_1 == depth_2;
2465                }
2466            }
2467            ListEntry::ChannelInvite(channel_1) => {
2468                if let ListEntry::ChannelInvite(channel_2) = other {
2469                    return channel_1.id == channel_2.id;
2470                }
2471            }
2472            ListEntry::IncomingRequest(user_1) => {
2473                if let ListEntry::IncomingRequest(user_2) = other {
2474                    return user_1.id == user_2.id;
2475                }
2476            }
2477            ListEntry::OutgoingRequest(user_1) => {
2478                if let ListEntry::OutgoingRequest(user_2) = other {
2479                    return user_1.id == user_2.id;
2480                }
2481            }
2482            ListEntry::Contact {
2483                contact: contact_1, ..
2484            } => {
2485                if let ListEntry::Contact {
2486                    contact: contact_2, ..
2487                } = other
2488                {
2489                    return contact_1.user.id == contact_2.user.id;
2490                }
2491            }
2492            ListEntry::ChannelEditor { depth } => {
2493                if let ListEntry::ChannelEditor { depth: other_depth } = other {
2494                    return depth == other_depth;
2495                }
2496            }
2497            ListEntry::ContactPlaceholder => {
2498                if let ListEntry::ContactPlaceholder = other {
2499                    return true;
2500                }
2501            }
2502        }
2503        false
2504    }
2505}
2506
2507fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
2508    Svg::new(svg_path)
2509        .with_color(style.color)
2510        .constrained()
2511        .with_width(style.icon_width)
2512        .aligned()
2513        .constrained()
2514        .with_width(style.button_width)
2515        .with_height(style.button_width)
2516        .contained()
2517        .with_style(style.container)
2518}