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