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