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