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