collab_panel.rs

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