collab_panel.rs

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