collab_panel.rs

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