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