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