collab_panel.rs

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