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