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                        this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2090                    }),
2091                )
2092                .align_children_center()
2093                .styleable_component()
2094                .disclosable(
2095                    disclosed,
2096                    Box::new(ToggleCollapse {
2097                        location: path.clone(),
2098                    }),
2099                )
2100                .with_id(ix)
2101                .with_style(collab_theme.disclosure.clone())
2102                .element()
2103                .constrained()
2104                .with_height(collab_theme.row_height)
2105                .contained()
2106                .with_style(select_state(
2107                    collab_theme
2108                        .channel_row
2109                        .in_state(is_selected || is_active || is_dragged_over),
2110                ))
2111                .with_padding_left(
2112                    collab_theme.channel_row.default_style().padding.left
2113                        + collab_theme.channel_indent * depth as f32,
2114                )
2115        })
2116        .on_click(MouseButton::Left, move |_, this, cx| {
2117            if this.drag_target_channel.take().is_none() {
2118                if is_active {
2119                    this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2120                } else {
2121                    this.join_channel(channel_id, cx)
2122                }
2123            }
2124        })
2125        .on_click(MouseButton::Right, {
2126            let path = path.clone();
2127            move |e, this, cx| {
2128                this.deploy_channel_context_menu(Some(e.position), &path, ix, cx);
2129            }
2130        })
2131        .on_up(MouseButton::Left, move |e, this, cx| {
2132            if let Some((_, dragged_channel)) = cx
2133                .global::<DragAndDrop<Workspace>>()
2134                .currently_dragged::<DraggedChannel>(cx.window())
2135            {
2136                if e.modifiers.alt {
2137                    this.channel_store.update(cx, |channel_store, cx| {
2138                        channel_store
2139                            .link_channel(dragged_channel.0.id, channel_id, cx)
2140                            .detach_and_log_err(cx)
2141                    })
2142                } else {
2143                    this.channel_store.update(cx, |channel_store, cx| {
2144                        match dragged_channel.1 {
2145                            Some(parent_id) => channel_store.move_channel(
2146                                dragged_channel.0.id,
2147                                parent_id,
2148                                channel_id,
2149                                cx,
2150                            ),
2151                            None => {
2152                                channel_store.link_channel(dragged_channel.0.id, channel_id, cx)
2153                            }
2154                        }
2155                        .detach_and_log_err(cx)
2156                    })
2157                }
2158            }
2159        })
2160        .on_move({
2161            let channel = channel.clone();
2162            let path = path.clone();
2163            move |_, this, cx| {
2164                if let Some((_, _dragged_channel)) =
2165                    cx.global::<DragAndDrop<Workspace>>()
2166                        .currently_dragged::<DraggedChannel>(cx.window())
2167                {
2168                    match &this.drag_target_channel {
2169                        Some(current_target)
2170                            if current_target.0 == channel && current_target.1 == path =>
2171                        {
2172                            return
2173                        }
2174                        _ => {
2175                            this.drag_target_channel = Some((channel.clone(), path.clone()));
2176                            cx.notify();
2177                        }
2178                    }
2179                }
2180            }
2181        })
2182        .as_draggable(
2183            (channel.clone(), path.parent_id()),
2184            move |modifiers, (channel, _), cx: &mut ViewContext<Workspace>| {
2185                let theme = &theme::current(cx).collab_panel;
2186
2187                Flex::<Workspace>::row()
2188                    .with_children(modifiers.alt.then(|| {
2189                        Svg::new("icons/plus.svg")
2190                            .with_color(theme.channel_hash.color)
2191                            .constrained()
2192                            .with_width(theme.channel_hash.width)
2193                            .aligned()
2194                            .left()
2195                    }))
2196                    .with_child(
2197                        Svg::new("icons/hash.svg")
2198                            .with_color(theme.channel_hash.color)
2199                            .constrained()
2200                            .with_width(theme.channel_hash.width)
2201                            .aligned()
2202                            .left(),
2203                    )
2204                    .with_child(
2205                        Label::new(channel.name.clone(), theme.channel_name.text.clone())
2206                            .contained()
2207                            .with_style(theme.channel_name.container)
2208                            .aligned()
2209                            .left(),
2210                    )
2211                    .align_children_center()
2212                    .contained()
2213                    .with_background_color(
2214                        theme
2215                            .container
2216                            .background_color
2217                            .unwrap_or(gpui::color::Color::transparent_black()),
2218                    )
2219                    .contained()
2220                    .with_padding_left(
2221                        theme.channel_row.default_style().padding.left
2222                            + theme.channel_indent * depth as f32,
2223                    )
2224                    .into_any()
2225            },
2226        )
2227        .with_cursor_style(CursorStyle::PointingHand)
2228        .into_any()
2229    }
2230
2231    fn render_channel_notes(
2232        &self,
2233        channel_id: ChannelId,
2234        theme: &theme::CollabPanel,
2235        is_selected: bool,
2236        ix: usize,
2237        cx: &mut ViewContext<Self>,
2238    ) -> AnyElement<Self> {
2239        enum ChannelNotes {}
2240        let host_avatar_width = theme
2241            .contact_avatar
2242            .width
2243            .or(theme.contact_avatar.height)
2244            .unwrap_or(0.);
2245
2246        MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
2247            let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
2248            let row = theme.project_row.in_state(is_selected).style_for(state);
2249
2250            Flex::<Self>::row()
2251                .with_child(render_tree_branch(
2252                    tree_branch,
2253                    &row.name.text,
2254                    true,
2255                    vec2f(host_avatar_width, theme.row_height),
2256                    cx.font_cache(),
2257                ))
2258                .with_child(
2259                    Svg::new("icons/file.svg")
2260                        .with_color(theme.channel_hash.color)
2261                        .constrained()
2262                        .with_width(theme.channel_hash.width)
2263                        .aligned()
2264                        .left(),
2265                )
2266                .with_child(
2267                    Label::new("notes", theme.channel_name.text.clone())
2268                        .contained()
2269                        .with_style(theme.channel_name.container)
2270                        .aligned()
2271                        .left()
2272                        .flex(1., true),
2273                )
2274                .constrained()
2275                .with_height(theme.row_height)
2276                .contained()
2277                .with_style(*theme.channel_row.style_for(is_selected, state))
2278                .with_padding_left(theme.channel_row.default_style().padding.left)
2279        })
2280        .on_click(MouseButton::Left, move |_, this, cx| {
2281            this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2282        })
2283        .with_cursor_style(CursorStyle::PointingHand)
2284        .into_any()
2285    }
2286
2287    fn render_channel_invite(
2288        channel: Arc<Channel>,
2289        channel_store: ModelHandle<ChannelStore>,
2290        theme: &theme::CollabPanel,
2291        is_selected: bool,
2292        cx: &mut ViewContext<Self>,
2293    ) -> AnyElement<Self> {
2294        enum Decline {}
2295        enum Accept {}
2296
2297        let channel_id = channel.id;
2298        let is_invite_pending = channel_store
2299            .read(cx)
2300            .has_pending_channel_invite_response(&channel);
2301        let button_spacing = theme.contact_button_spacing;
2302
2303        Flex::row()
2304            .with_child(
2305                Svg::new("icons/hash.svg")
2306                    .with_color(theme.channel_hash.color)
2307                    .constrained()
2308                    .with_width(theme.channel_hash.width)
2309                    .aligned()
2310                    .left(),
2311            )
2312            .with_child(
2313                Label::new(channel.name.clone(), theme.contact_username.text.clone())
2314                    .contained()
2315                    .with_style(theme.contact_username.container)
2316                    .aligned()
2317                    .left()
2318                    .flex(1., true),
2319            )
2320            .with_child(
2321                MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
2322                    let button_style = if is_invite_pending {
2323                        &theme.disabled_button
2324                    } else {
2325                        theme.contact_button.style_for(mouse_state)
2326                    };
2327                    render_icon_button(button_style, "icons/x.svg").aligned()
2328                })
2329                .with_cursor_style(CursorStyle::PointingHand)
2330                .on_click(MouseButton::Left, move |_, this, cx| {
2331                    this.respond_to_channel_invite(channel_id, false, cx);
2332                })
2333                .contained()
2334                .with_margin_right(button_spacing),
2335            )
2336            .with_child(
2337                MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
2338                    let button_style = if is_invite_pending {
2339                        &theme.disabled_button
2340                    } else {
2341                        theme.contact_button.style_for(mouse_state)
2342                    };
2343                    render_icon_button(button_style, "icons/check.svg")
2344                        .aligned()
2345                        .flex_float()
2346                })
2347                .with_cursor_style(CursorStyle::PointingHand)
2348                .on_click(MouseButton::Left, move |_, this, cx| {
2349                    this.respond_to_channel_invite(channel_id, true, cx);
2350                }),
2351            )
2352            .constrained()
2353            .with_height(theme.row_height)
2354            .contained()
2355            .with_style(
2356                *theme
2357                    .contact_row
2358                    .in_state(is_selected)
2359                    .style_for(&mut Default::default()),
2360            )
2361            .with_padding_left(
2362                theme.contact_row.default_style().padding.left + theme.channel_indent,
2363            )
2364            .into_any()
2365    }
2366
2367    fn render_contact_request(
2368        user: Arc<User>,
2369        user_store: ModelHandle<UserStore>,
2370        theme: &theme::CollabPanel,
2371        is_incoming: bool,
2372        is_selected: bool,
2373        cx: &mut ViewContext<Self>,
2374    ) -> AnyElement<Self> {
2375        enum Decline {}
2376        enum Accept {}
2377        enum Cancel {}
2378
2379        let mut row = Flex::row()
2380            .with_children(user.avatar.clone().map(|avatar| {
2381                Image::from_data(avatar)
2382                    .with_style(theme.contact_avatar)
2383                    .aligned()
2384                    .left()
2385            }))
2386            .with_child(
2387                Label::new(
2388                    user.github_login.clone(),
2389                    theme.contact_username.text.clone(),
2390                )
2391                .contained()
2392                .with_style(theme.contact_username.container)
2393                .aligned()
2394                .left()
2395                .flex(1., true),
2396            );
2397
2398        let user_id = user.id;
2399        let github_login = user.github_login.clone();
2400        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
2401        let button_spacing = theme.contact_button_spacing;
2402
2403        if is_incoming {
2404            row.add_child(
2405                MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
2406                    let button_style = if is_contact_request_pending {
2407                        &theme.disabled_button
2408                    } else {
2409                        theme.contact_button.style_for(mouse_state)
2410                    };
2411                    render_icon_button(button_style, "icons/x.svg").aligned()
2412                })
2413                .with_cursor_style(CursorStyle::PointingHand)
2414                .on_click(MouseButton::Left, move |_, this, cx| {
2415                    this.respond_to_contact_request(user_id, false, cx);
2416                })
2417                .contained()
2418                .with_margin_right(button_spacing),
2419            );
2420
2421            row.add_child(
2422                MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
2423                    let button_style = if is_contact_request_pending {
2424                        &theme.disabled_button
2425                    } else {
2426                        theme.contact_button.style_for(mouse_state)
2427                    };
2428                    render_icon_button(button_style, "icons/check.svg")
2429                        .aligned()
2430                        .flex_float()
2431                })
2432                .with_cursor_style(CursorStyle::PointingHand)
2433                .on_click(MouseButton::Left, move |_, this, cx| {
2434                    this.respond_to_contact_request(user_id, true, cx);
2435                }),
2436            );
2437        } else {
2438            row.add_child(
2439                MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
2440                    let button_style = if is_contact_request_pending {
2441                        &theme.disabled_button
2442                    } else {
2443                        theme.contact_button.style_for(mouse_state)
2444                    };
2445                    render_icon_button(button_style, "icons/x.svg")
2446                        .aligned()
2447                        .flex_float()
2448                })
2449                .with_padding(Padding::uniform(2.))
2450                .with_cursor_style(CursorStyle::PointingHand)
2451                .on_click(MouseButton::Left, move |_, this, cx| {
2452                    this.remove_contact(user_id, &github_login, cx);
2453                })
2454                .flex_float(),
2455            );
2456        }
2457
2458        row.constrained()
2459            .with_height(theme.row_height)
2460            .contained()
2461            .with_style(
2462                *theme
2463                    .contact_row
2464                    .in_state(is_selected)
2465                    .style_for(&mut Default::default()),
2466            )
2467            .into_any()
2468    }
2469
2470    fn has_subchannels(&self, ix: usize) -> bool {
2471        self.entries
2472            .get(ix)
2473            .zip(self.entries.get(ix + 1))
2474            .map(|entries| match entries {
2475                (
2476                    ListEntry::Channel {
2477                        path: this_path, ..
2478                    },
2479                    ListEntry::Channel {
2480                        path: next_path, ..
2481                    },
2482                ) => next_path.starts_with(this_path),
2483                _ => false,
2484            })
2485            .unwrap_or(false)
2486    }
2487
2488    fn deploy_channel_context_menu(
2489        &mut self,
2490        position: Option<Vector2F>,
2491        path: &ChannelPath,
2492        ix: usize,
2493        cx: &mut ViewContext<Self>,
2494    ) {
2495        self.context_menu_on_selected = position.is_none();
2496
2497        let channel_name = self.channel_clipboard.as_ref().and_then(|channel| {
2498            let channel_name = self
2499                .channel_store
2500                .read(cx)
2501                .channel_for_id(channel.channel_id)
2502                .map(|channel| channel.name.clone())?;
2503            Some(channel_name)
2504        });
2505
2506        self.context_menu.update(cx, |context_menu, cx| {
2507            context_menu.set_position_mode(if self.context_menu_on_selected {
2508                OverlayPositionMode::Local
2509            } else {
2510                OverlayPositionMode::Window
2511            });
2512
2513            let mut items = Vec::new();
2514
2515            let select_action_name = if self.selection == Some(ix) {
2516                "Unselect"
2517            } else {
2518                "Select"
2519            };
2520
2521            items.push(ContextMenuItem::action(
2522                select_action_name,
2523                ToggleSelectedIx { ix },
2524            ));
2525
2526            if self.has_subchannels(ix) {
2527                let expand_action_name = if self.is_channel_collapsed(&path) {
2528                    "Expand Subchannels"
2529                } else {
2530                    "Collapse Subchannels"
2531                };
2532                items.push(ContextMenuItem::action(
2533                    expand_action_name,
2534                    ToggleCollapse {
2535                        location: path.clone(),
2536                    },
2537                ));
2538            }
2539
2540            items.push(ContextMenuItem::action(
2541                "Open Notes",
2542                OpenChannelNotes {
2543                    channel_id: path.channel_id(),
2544                },
2545            ));
2546
2547            items.push(ContextMenuItem::action(
2548                "Open Chat",
2549                JoinChannelChat {
2550                    channel_id: path.channel_id(),
2551                },
2552            ));
2553
2554            if self.channel_store.read(cx).is_user_admin(path.channel_id()) {
2555                let parent_id = path.parent_id();
2556
2557                items.extend([
2558                    ContextMenuItem::Separator,
2559                    ContextMenuItem::action(
2560                        "New Subchannel",
2561                        NewChannel {
2562                            location: path.clone(),
2563                        },
2564                    ),
2565                    ContextMenuItem::action(
2566                        "Rename",
2567                        RenameChannel {
2568                            location: path.clone(),
2569                        },
2570                    ),
2571                    ContextMenuItem::Separator,
2572                ]);
2573
2574                if let Some(parent_id) = parent_id {
2575                    items.push(ContextMenuItem::action(
2576                        "Unlink from parent",
2577                        UnlinkChannel {
2578                            channel_id: path.channel_id(),
2579                            parent_id,
2580                        },
2581                    ));
2582                }
2583
2584                items.extend([
2585                    ContextMenuItem::action(
2586                        "Move this channel",
2587                        StartMoveChannelFor {
2588                            channel_id: path.channel_id(),
2589                            parent_id,
2590                        },
2591                    ),
2592                    ContextMenuItem::action(
2593                        "Link this channel",
2594                        StartLinkChannelFor {
2595                            channel_id: path.channel_id(),
2596                            parent_id,
2597                        },
2598                    ),
2599                ]);
2600
2601                if let Some(channel_name) = channel_name {
2602                    items.push(ContextMenuItem::Separator);
2603                    items.push(ContextMenuItem::action(
2604                        format!("Move '#{}' here", channel_name),
2605                        MoveChannel {
2606                            to: path.channel_id(),
2607                        },
2608                    ));
2609                    items.push(ContextMenuItem::action(
2610                        format!("Link '#{}' here", channel_name),
2611                        LinkChannel {
2612                            to: path.channel_id(),
2613                        },
2614                    ));
2615                }
2616
2617                items.extend([
2618                    ContextMenuItem::Separator,
2619                    ContextMenuItem::action(
2620                        "Invite Members",
2621                        InviteMembers {
2622                            channel_id: path.channel_id(),
2623                        },
2624                    ),
2625                    ContextMenuItem::action(
2626                        "Manage Members",
2627                        ManageMembers {
2628                            channel_id: path.channel_id(),
2629                        },
2630                    ),
2631                    ContextMenuItem::Separator,
2632                    ContextMenuItem::action(
2633                        "Delete",
2634                        RemoveChannel {
2635                            channel_id: path.channel_id(),
2636                        },
2637                    ),
2638                ]);
2639            }
2640
2641            context_menu.show(
2642                position.unwrap_or_default(),
2643                if self.context_menu_on_selected {
2644                    gpui::elements::AnchorCorner::TopRight
2645                } else {
2646                    gpui::elements::AnchorCorner::BottomLeft
2647                },
2648                items,
2649                cx,
2650            );
2651        });
2652
2653        cx.notify();
2654    }
2655
2656    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
2657        if self.take_editing_state(cx) {
2658            cx.focus(&self.filter_editor);
2659        } else {
2660            self.filter_editor.update(cx, |editor, cx| {
2661                if editor.buffer().read(cx).len(cx) > 0 {
2662                    editor.set_text("", cx);
2663                }
2664            });
2665        }
2666
2667        self.update_entries(false, cx);
2668    }
2669
2670    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
2671        let ix = self.selection.map_or(0, |ix| ix + 1);
2672        if ix < self.entries.len() {
2673            self.selection = Some(ix);
2674        }
2675
2676        self.list_state.reset(self.entries.len());
2677        if let Some(ix) = self.selection {
2678            self.list_state.scroll_to(ListOffset {
2679                item_ix: ix,
2680                offset_in_item: 0.,
2681            });
2682        }
2683        cx.notify();
2684    }
2685
2686    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
2687        let ix = self.selection.take().unwrap_or(0);
2688        if ix > 0 {
2689            self.selection = Some(ix - 1);
2690        }
2691
2692        self.list_state.reset(self.entries.len());
2693        if let Some(ix) = self.selection {
2694            self.list_state.scroll_to(ListOffset {
2695                item_ix: ix,
2696                offset_in_item: 0.,
2697            });
2698        }
2699        cx.notify();
2700    }
2701
2702    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
2703        if self.confirm_channel_edit(cx) {
2704            return;
2705        }
2706
2707        if let Some(selection) = self.selection {
2708            if let Some(entry) = self.entries.get(selection) {
2709                match entry {
2710                    ListEntry::Header(section) => match section {
2711                        Section::ActiveCall => Self::leave_call(cx),
2712                        Section::Channels => self.new_root_channel(cx),
2713                        Section::Contacts => self.toggle_contact_finder(cx),
2714                        Section::ContactRequests
2715                        | Section::Online
2716                        | Section::Offline
2717                        | Section::ChannelInvites => {
2718                            self.toggle_section_expanded(*section, cx);
2719                        }
2720                    },
2721                    ListEntry::Contact { contact, calling } => {
2722                        if contact.online && !contact.busy && !calling {
2723                            self.call(contact.user.id, Some(self.project.clone()), cx);
2724                        }
2725                    }
2726                    ListEntry::ParticipantProject {
2727                        project_id,
2728                        host_user_id,
2729                        ..
2730                    } => {
2731                        if let Some(workspace) = self.workspace.upgrade(cx) {
2732                            let app_state = workspace.read(cx).app_state().clone();
2733                            workspace::join_remote_project(
2734                                *project_id,
2735                                *host_user_id,
2736                                app_state,
2737                                cx,
2738                            )
2739                            .detach_and_log_err(cx);
2740                        }
2741                    }
2742                    ListEntry::ParticipantScreen { peer_id, .. } => {
2743                        if let Some(workspace) = self.workspace.upgrade(cx) {
2744                            workspace.update(cx, |workspace, cx| {
2745                                workspace.open_shared_screen(*peer_id, cx)
2746                            });
2747                        }
2748                    }
2749                    ListEntry::Channel { channel, .. } => {
2750                        let is_active = iife!({
2751                            let call_channel = ActiveCall::global(cx)
2752                                .read(cx)
2753                                .room()?
2754                                .read(cx)
2755                                .channel_id()?;
2756
2757                            Some(call_channel == channel.id)
2758                        })
2759                        .unwrap_or(false);
2760                        if is_active {
2761                            self.open_channel_notes(
2762                                &OpenChannelNotes {
2763                                    channel_id: channel.id,
2764                                },
2765                                cx,
2766                            )
2767                        } else {
2768                            self.join_channel(channel.id, cx)
2769                        }
2770                    }
2771                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
2772                    _ => {}
2773                }
2774            }
2775        }
2776    }
2777
2778    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
2779        if self.channel_editing_state.is_some() {
2780            self.channel_name_editor.update(cx, |editor, cx| {
2781                editor.insert(" ", cx);
2782            });
2783        }
2784    }
2785
2786    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
2787        if let Some(editing_state) = &mut self.channel_editing_state {
2788            match editing_state {
2789                ChannelEditingState::Create {
2790                    location,
2791                    pending_name,
2792                    ..
2793                } => {
2794                    if pending_name.is_some() {
2795                        return false;
2796                    }
2797                    let channel_name = self.channel_name_editor.read(cx).text(cx);
2798
2799                    *pending_name = Some(channel_name.clone());
2800
2801                    self.channel_store
2802                        .update(cx, |channel_store, cx| {
2803                            channel_store.create_channel(
2804                                &channel_name,
2805                                location.as_ref().map(|location| location.channel_id()),
2806                                cx,
2807                            )
2808                        })
2809                        .detach();
2810                    cx.notify();
2811                }
2812                ChannelEditingState::Rename {
2813                    location,
2814                    pending_name,
2815                } => {
2816                    if pending_name.is_some() {
2817                        return false;
2818                    }
2819                    let channel_name = self.channel_name_editor.read(cx).text(cx);
2820                    *pending_name = Some(channel_name.clone());
2821
2822                    self.channel_store
2823                        .update(cx, |channel_store, cx| {
2824                            channel_store.rename(location.channel_id(), &channel_name, cx)
2825                        })
2826                        .detach();
2827                    cx.notify();
2828                }
2829            }
2830            cx.focus_self();
2831            true
2832        } else {
2833            false
2834        }
2835    }
2836
2837    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
2838        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2839            self.collapsed_sections.remove(ix);
2840        } else {
2841            self.collapsed_sections.push(section);
2842        }
2843        self.update_entries(false, cx);
2844    }
2845
2846    fn collapse_selected_channel(
2847        &mut self,
2848        _: &CollapseSelectedChannel,
2849        cx: &mut ViewContext<Self>,
2850    ) {
2851        let Some((_, path)) = self
2852            .selected_channel()
2853            .map(|(channel, parent)| (channel.id, parent))
2854        else {
2855            return;
2856        };
2857
2858        if self.is_channel_collapsed(&path) {
2859            return;
2860        }
2861
2862        self.toggle_channel_collapsed(&path.clone(), cx);
2863    }
2864
2865    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
2866        let Some((_, path)) = self
2867            .selected_channel()
2868            .map(|(channel, parent)| (channel.id, parent))
2869        else {
2870            return;
2871        };
2872
2873        if !self.is_channel_collapsed(&path) {
2874            return;
2875        }
2876
2877        self.toggle_channel_collapsed(path.to_owned(), cx)
2878    }
2879
2880    fn toggle_channel_collapsed_action(
2881        &mut self,
2882        action: &ToggleCollapse,
2883        cx: &mut ViewContext<Self>,
2884    ) {
2885        self.toggle_channel_collapsed(&action.location, cx);
2886    }
2887
2888    fn toggle_channel_collapsed<'a>(
2889        &mut self,
2890        path: impl Into<Cow<'a, ChannelPath>>,
2891        cx: &mut ViewContext<Self>,
2892    ) {
2893        let path = path.into();
2894        match self.collapsed_channels.binary_search(&path) {
2895            Ok(ix) => {
2896                self.collapsed_channels.remove(ix);
2897            }
2898            Err(ix) => {
2899                self.collapsed_channels.insert(ix, path.into_owned());
2900            }
2901        };
2902        self.serialize(cx);
2903        self.update_entries(true, cx);
2904        cx.notify();
2905        cx.focus_self();
2906    }
2907
2908    fn is_channel_collapsed(&self, path: &ChannelPath) -> bool {
2909        self.collapsed_channels.binary_search(path).is_ok()
2910    }
2911
2912    fn leave_call(cx: &mut ViewContext<Self>) {
2913        ActiveCall::global(cx)
2914            .update(cx, |call, cx| call.hang_up(cx))
2915            .detach_and_log_err(cx);
2916    }
2917
2918    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
2919        if let Some(workspace) = self.workspace.upgrade(cx) {
2920            workspace.update(cx, |workspace, cx| {
2921                workspace.toggle_modal(cx, |_, cx| {
2922                    cx.add_view(|cx| {
2923                        let mut finder = ContactFinder::new(self.user_store.clone(), cx);
2924                        finder.set_query(self.filter_editor.read(cx).text(cx), cx);
2925                        finder
2926                    })
2927                });
2928            });
2929        }
2930    }
2931
2932    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2933        self.channel_editing_state = Some(ChannelEditingState::Create {
2934            location: None,
2935            pending_name: None,
2936        });
2937        self.update_entries(false, cx);
2938        self.select_channel_editor();
2939        cx.focus(self.channel_name_editor.as_any());
2940        cx.notify();
2941    }
2942
2943    fn select_channel_editor(&mut self) {
2944        self.selection = self.entries.iter().position(|entry| match entry {
2945            ListEntry::ChannelEditor { .. } => true,
2946            _ => false,
2947        });
2948    }
2949
2950    fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
2951        self.collapsed_channels
2952            .retain(|channel| *channel != action.location);
2953        self.channel_editing_state = Some(ChannelEditingState::Create {
2954            location: Some(action.location.to_owned()),
2955            pending_name: None,
2956        });
2957        self.update_entries(false, cx);
2958        self.select_channel_editor();
2959        cx.focus(self.channel_name_editor.as_any());
2960        cx.notify();
2961    }
2962
2963    fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
2964        self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
2965    }
2966
2967    fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
2968        self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
2969    }
2970
2971    fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
2972        if let Some((channel, _)) = self.selected_channel() {
2973            self.remove_channel(channel.id, cx)
2974        }
2975    }
2976
2977    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
2978        if let Some((_, parent)) = self.selected_channel() {
2979            self.rename_channel(
2980                &RenameChannel {
2981                    location: parent.to_owned(),
2982                },
2983                cx,
2984            );
2985        }
2986    }
2987
2988    fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
2989        let channel_store = self.channel_store.read(cx);
2990        if !channel_store.is_user_admin(action.location.channel_id()) {
2991            return;
2992        }
2993        if let Some(channel) = channel_store
2994            .channel_for_id(action.location.channel_id())
2995            .cloned()
2996        {
2997            self.channel_editing_state = Some(ChannelEditingState::Rename {
2998                location: action.location.to_owned(),
2999                pending_name: None,
3000            });
3001            self.channel_name_editor.update(cx, |editor, cx| {
3002                editor.set_text(channel.name.clone(), cx);
3003                editor.select_all(&Default::default(), cx);
3004            });
3005            cx.focus(self.channel_name_editor.as_any());
3006            self.update_entries(false, cx);
3007            self.select_channel_editor();
3008        }
3009    }
3010
3011    fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
3012        if let Some(workspace) = self.workspace.upgrade(cx) {
3013            ChannelView::open(action.channel_id, workspace, cx).detach();
3014        }
3015    }
3016
3017    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
3018        let Some((_, path)) = self.selected_channel() else {
3019            return;
3020        };
3021
3022        self.deploy_channel_context_menu(None, &path.to_owned(), self.selection.unwrap(), cx);
3023    }
3024
3025    fn selected_channel(&self) -> Option<(&Arc<Channel>, &ChannelPath)> {
3026        self.selection
3027            .and_then(|ix| self.entries.get(ix))
3028            .and_then(|entry| match entry {
3029                ListEntry::Channel {
3030                    channel,
3031                    path: parent,
3032                    ..
3033                } => Some((channel, parent)),
3034                _ => None,
3035            })
3036    }
3037
3038    fn show_channel_modal(
3039        &mut self,
3040        channel_id: ChannelId,
3041        mode: channel_modal::Mode,
3042        cx: &mut ViewContext<Self>,
3043    ) {
3044        let workspace = self.workspace.clone();
3045        let user_store = self.user_store.clone();
3046        let channel_store = self.channel_store.clone();
3047        let members = self.channel_store.update(cx, |channel_store, cx| {
3048            channel_store.get_channel_member_details(channel_id, cx)
3049        });
3050
3051        cx.spawn(|_, mut cx| async move {
3052            let members = members.await?;
3053            workspace.update(&mut cx, |workspace, cx| {
3054                workspace.toggle_modal(cx, |_, cx| {
3055                    cx.add_view(|cx| {
3056                        ChannelModal::new(
3057                            user_store.clone(),
3058                            channel_store.clone(),
3059                            channel_id,
3060                            mode,
3061                            members,
3062                            cx,
3063                        )
3064                    })
3065                });
3066            })
3067        })
3068        .detach();
3069    }
3070
3071    fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
3072        self.remove_channel(action.channel_id, cx)
3073    }
3074
3075    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
3076        let channel_store = self.channel_store.clone();
3077        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
3078            let prompt_message = format!(
3079                "Are you sure you want to remove the channel \"{}\"?",
3080                channel.name
3081            );
3082            let mut answer =
3083                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
3084            let window = cx.window();
3085            cx.spawn(|this, mut cx| async move {
3086                if answer.next().await == Some(0) {
3087                    if let Err(e) = channel_store
3088                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
3089                        .await
3090                    {
3091                        window.prompt(
3092                            PromptLevel::Info,
3093                            &format!("Failed to remove channel: {}", e),
3094                            &["Ok"],
3095                            &mut cx,
3096                        );
3097                    }
3098                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
3099                }
3100            })
3101            .detach();
3102        }
3103    }
3104
3105    // Should move to the filter editor if clicking on it
3106    // Should move selection to the channel editor if activating it
3107
3108    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
3109        let user_store = self.user_store.clone();
3110        let prompt_message = format!(
3111            "Are you sure you want to remove \"{}\" from your contacts?",
3112            github_login
3113        );
3114        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
3115        let window = cx.window();
3116        cx.spawn(|_, mut cx| async move {
3117            if answer.next().await == Some(0) {
3118                if let Err(e) = user_store
3119                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
3120                    .await
3121                {
3122                    window.prompt(
3123                        PromptLevel::Info,
3124                        &format!("Failed to remove contact: {}", e),
3125                        &["Ok"],
3126                        &mut cx,
3127                    );
3128                }
3129            }
3130        })
3131        .detach();
3132    }
3133
3134    fn respond_to_contact_request(
3135        &mut self,
3136        user_id: u64,
3137        accept: bool,
3138        cx: &mut ViewContext<Self>,
3139    ) {
3140        self.user_store
3141            .update(cx, |store, cx| {
3142                store.respond_to_contact_request(user_id, accept, cx)
3143            })
3144            .detach();
3145    }
3146
3147    fn respond_to_channel_invite(
3148        &mut self,
3149        channel_id: u64,
3150        accept: bool,
3151        cx: &mut ViewContext<Self>,
3152    ) {
3153        let respond = self.channel_store.update(cx, |store, _| {
3154            store.respond_to_channel_invite(channel_id, accept)
3155        });
3156        cx.foreground().spawn(respond).detach();
3157    }
3158
3159    fn call(
3160        &mut self,
3161        recipient_user_id: u64,
3162        initial_project: Option<ModelHandle<Project>>,
3163        cx: &mut ViewContext<Self>,
3164    ) {
3165        ActiveCall::global(cx)
3166            .update(cx, |call, cx| {
3167                call.invite(recipient_user_id, initial_project, cx)
3168            })
3169            .detach_and_log_err(cx);
3170    }
3171
3172    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
3173        let workspace = self.workspace.clone();
3174        let window = cx.window();
3175        let active_call = ActiveCall::global(cx);
3176        cx.spawn(|_, mut cx| async move {
3177            if active_call.read_with(&mut cx, |active_call, cx| {
3178                if let Some(room) = active_call.room() {
3179                    let room = room.read(cx);
3180                    room.is_sharing_project() && room.remote_participants().len() > 0
3181                } else {
3182                    false
3183                }
3184            }) {
3185                let answer = window.prompt(
3186                    PromptLevel::Warning,
3187                    "Leaving this call will unshare your current project.\nDo you want to switch channels?",
3188                    &["Yes, Join Channel", "Cancel"],
3189                    &mut cx,
3190                );
3191
3192                if let Some(mut answer) = answer {
3193                    if answer.next().await == Some(1) {
3194                        return anyhow::Ok(());
3195                    }
3196                }
3197            }
3198
3199            let room = active_call
3200                .update(&mut cx, |call, cx| call.join_channel(channel_id, cx))
3201                .await?;
3202
3203            let task = room.update(&mut cx, |room, cx| {
3204                let workspace = workspace.upgrade(cx)?;
3205                let (project, host) = room.most_active_project()?;
3206                let app_state = workspace.read(cx).app_state().clone();
3207                Some(workspace::join_remote_project(project, host, app_state, cx))
3208            });
3209            if let Some(task) = task {
3210                task.await?;
3211            }
3212
3213            anyhow::Ok(())
3214        })
3215        .detach_and_log_err(cx);
3216    }
3217
3218    fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
3219        let channel_id = action.channel_id;
3220        if let Some(workspace) = self.workspace.upgrade(cx) {
3221            cx.app_context().defer(move |cx| {
3222                workspace.update(cx, |workspace, cx| {
3223                    if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
3224                        panel.update(cx, |panel, cx| {
3225                            panel.select_channel(channel_id, cx).detach_and_log_err(cx);
3226                        });
3227                    }
3228                });
3229            });
3230        }
3231    }
3232}
3233
3234fn render_tree_branch(
3235    branch_style: theme::TreeBranch,
3236    row_style: &TextStyle,
3237    is_last: bool,
3238    size: Vector2F,
3239    font_cache: &FontCache,
3240) -> gpui::elements::ConstrainedBox<CollabPanel> {
3241    let line_height = row_style.line_height(font_cache);
3242    let cap_height = row_style.cap_height(font_cache);
3243    let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
3244
3245    Canvas::new(move |bounds, _, _, cx| {
3246        cx.paint_layer(None, |cx| {
3247            let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
3248            let end_x = bounds.max_x();
3249            let start_y = bounds.min_y();
3250            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
3251
3252            cx.scene().push_quad(gpui::Quad {
3253                bounds: RectF::from_points(
3254                    vec2f(start_x, start_y),
3255                    vec2f(
3256                        start_x + branch_style.width,
3257                        if is_last { end_y } else { bounds.max_y() },
3258                    ),
3259                ),
3260                background: Some(branch_style.color),
3261                border: gpui::Border::default(),
3262                corner_radii: (0.).into(),
3263            });
3264            cx.scene().push_quad(gpui::Quad {
3265                bounds: RectF::from_points(
3266                    vec2f(start_x, end_y),
3267                    vec2f(end_x, end_y + branch_style.width),
3268                ),
3269                background: Some(branch_style.color),
3270                border: gpui::Border::default(),
3271                corner_radii: (0.).into(),
3272            });
3273        })
3274    })
3275    .constrained()
3276    .with_width(size.x())
3277}
3278
3279impl View for CollabPanel {
3280    fn ui_name() -> &'static str {
3281        "CollabPanel"
3282    }
3283
3284    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
3285        if !self.has_focus {
3286            self.has_focus = true;
3287            if !self.context_menu.is_focused(cx) {
3288                if let Some(editing_state) = &self.channel_editing_state {
3289                    if editing_state.pending_name().is_none() {
3290                        cx.focus(&self.channel_name_editor);
3291                    } else {
3292                        cx.focus(&self.filter_editor);
3293                    }
3294                } else {
3295                    cx.focus(&self.filter_editor);
3296                }
3297            }
3298            cx.emit(Event::Focus);
3299        }
3300    }
3301
3302    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
3303        self.has_focus = false;
3304    }
3305
3306    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
3307        let theme = &theme::current(cx).collab_panel;
3308
3309        if self.user_store.read(cx).current_user().is_none() {
3310            enum LogInButton {}
3311
3312            return Flex::column()
3313                .with_child(
3314                    MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
3315                        let button = theme.log_in_button.style_for(state);
3316                        Label::new("Sign in to collaborate", button.text.clone())
3317                            .aligned()
3318                            .left()
3319                            .contained()
3320                            .with_style(button.container)
3321                    })
3322                    .on_click(MouseButton::Left, |_, this, cx| {
3323                        let client = this.client.clone();
3324                        cx.spawn(|_, cx| async move {
3325                            client.authenticate_and_connect(true, &cx).await.log_err();
3326                        })
3327                        .detach();
3328                    })
3329                    .with_cursor_style(CursorStyle::PointingHand),
3330                )
3331                .contained()
3332                .with_style(theme.container)
3333                .into_any();
3334        }
3335
3336        enum PanelFocus {}
3337        MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3338            Stack::new()
3339                .with_child(
3340                    Flex::column()
3341                        .with_child(
3342                            Flex::row().with_child(
3343                                ChildView::new(&self.filter_editor, cx)
3344                                    .contained()
3345                                    .with_style(theme.user_query_editor.container)
3346                                    .flex(1.0, true),
3347                            ),
3348                        )
3349                        .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3350                        .contained()
3351                        .with_style(theme.container)
3352                        .into_any(),
3353                )
3354                .with_children(
3355                    (!self.context_menu_on_selected)
3356                        .then(|| ChildView::new(&self.context_menu, cx)),
3357                )
3358                .into_any()
3359        })
3360        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3361        .into_any_named("collab panel")
3362    }
3363
3364    fn update_keymap_context(
3365        &self,
3366        keymap: &mut gpui::keymap_matcher::KeymapContext,
3367        _: &AppContext,
3368    ) {
3369        Self::reset_to_default_keymap_context(keymap);
3370        if self.channel_editing_state.is_some() {
3371            keymap.add_identifier("editing");
3372        } else {
3373            keymap.add_identifier("not_editing");
3374        }
3375    }
3376}
3377
3378impl Panel for CollabPanel {
3379    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3380        settings::get::<CollaborationPanelSettings>(cx).dock
3381    }
3382
3383    fn position_is_valid(&self, position: DockPosition) -> bool {
3384        matches!(position, DockPosition::Left | DockPosition::Right)
3385    }
3386
3387    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3388        settings::update_settings_file::<CollaborationPanelSettings>(
3389            self.fs.clone(),
3390            cx,
3391            move |settings| settings.dock = Some(position),
3392        );
3393    }
3394
3395    fn size(&self, cx: &gpui::WindowContext) -> f32 {
3396        self.width
3397            .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
3398    }
3399
3400    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3401        self.width = size;
3402        self.serialize(cx);
3403        cx.notify();
3404    }
3405
3406    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
3407        settings::get::<CollaborationPanelSettings>(cx)
3408            .button
3409            .then(|| "icons/user_group_16.svg")
3410    }
3411
3412    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
3413        (
3414            "Collaboration Panel".to_string(),
3415            Some(Box::new(ToggleFocus)),
3416        )
3417    }
3418
3419    fn should_change_position_on_event(event: &Self::Event) -> bool {
3420        matches!(event, Event::DockPositionChanged)
3421    }
3422
3423    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
3424        self.has_focus
3425    }
3426
3427    fn is_focus_event(event: &Self::Event) -> bool {
3428        matches!(event, Event::Focus)
3429    }
3430}
3431
3432impl PartialEq for ListEntry {
3433    fn eq(&self, other: &Self) -> bool {
3434        match self {
3435            ListEntry::Header(section_1) => {
3436                if let ListEntry::Header(section_2) = other {
3437                    return section_1 == section_2;
3438                }
3439            }
3440            ListEntry::CallParticipant { user: user_1, .. } => {
3441                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3442                    return user_1.id == user_2.id;
3443                }
3444            }
3445            ListEntry::ParticipantProject {
3446                project_id: project_id_1,
3447                ..
3448            } => {
3449                if let ListEntry::ParticipantProject {
3450                    project_id: project_id_2,
3451                    ..
3452                } = other
3453                {
3454                    return project_id_1 == project_id_2;
3455                }
3456            }
3457            ListEntry::ParticipantScreen {
3458                peer_id: peer_id_1, ..
3459            } => {
3460                if let ListEntry::ParticipantScreen {
3461                    peer_id: peer_id_2, ..
3462                } = other
3463                {
3464                    return peer_id_1 == peer_id_2;
3465                }
3466            }
3467            ListEntry::Channel {
3468                channel: channel_1,
3469                depth: depth_1,
3470                path: parent_1,
3471            } => {
3472                if let ListEntry::Channel {
3473                    channel: channel_2,
3474                    depth: depth_2,
3475                    path: parent_2,
3476                } = other
3477                {
3478                    return channel_1.id == channel_2.id
3479                        && depth_1 == depth_2
3480                        && parent_1 == parent_2;
3481                }
3482            }
3483            ListEntry::ChannelNotes { channel_id } => {
3484                if let ListEntry::ChannelNotes {
3485                    channel_id: other_id,
3486                } = other
3487                {
3488                    return channel_id == other_id;
3489                }
3490            }
3491            ListEntry::ChannelInvite(channel_1) => {
3492                if let ListEntry::ChannelInvite(channel_2) = other {
3493                    return channel_1.id == channel_2.id;
3494                }
3495            }
3496            ListEntry::IncomingRequest(user_1) => {
3497                if let ListEntry::IncomingRequest(user_2) = other {
3498                    return user_1.id == user_2.id;
3499                }
3500            }
3501            ListEntry::OutgoingRequest(user_1) => {
3502                if let ListEntry::OutgoingRequest(user_2) = other {
3503                    return user_1.id == user_2.id;
3504                }
3505            }
3506            ListEntry::Contact {
3507                contact: contact_1, ..
3508            } => {
3509                if let ListEntry::Contact {
3510                    contact: contact_2, ..
3511                } = other
3512                {
3513                    return contact_1.user.id == contact_2.user.id;
3514                }
3515            }
3516            ListEntry::ChannelEditor { depth } => {
3517                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3518                    return depth == other_depth;
3519                }
3520            }
3521            ListEntry::ContactPlaceholder => {
3522                if let ListEntry::ContactPlaceholder = other {
3523                    return true;
3524                }
3525            }
3526        }
3527        false
3528    }
3529}
3530
3531fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3532    Svg::new(svg_path)
3533        .with_color(style.color)
3534        .constrained()
3535        .with_width(style.icon_width)
3536        .aligned()
3537        .constrained()
3538        .with_width(style.button_width)
3539        .with_height(style.button_width)
3540        .contained()
3541        .with_style(style.container)
3542}