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