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