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