collab_panel.rs

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