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