collab_panel.rs

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